lyng/library/src/commonTest/kotlin/ScriptTest.kt

567 lines
17 KiB
Kotlin

package io.github.kotlin.fibonacci
import kotlinx.coroutines.test.runTest
import net.sergeych.ling.*
import kotlin.math.PI
import kotlin.test.*
class ScriptTest {
@Test
fun parseNewlines() {
fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) {
val source = src.toSource()
assertEquals(
Token(expected, source.posAt(row, col), type),
parseLing(source)[offset]
)
}
check("1", Token.Type.INT, 0, 0, "1 + x\n2", 0)
check("+", Token.Type.PLUS, 0, 2, "1 + x\n2", 1)
check("x", Token.Type.ID, 0, 4, "1 + x\n2", 2)
check("\n", Token.Type.NEWLINE, 0, 5, "1 + x\n2", 3)
// check("2", Token.Type.INT, 1, 0, "1 + x\n2", 4)
// check("", Token.Type.EOF, 1, 0, "1 + x\n2", 5)
}
@Test
fun parseNumbersTest() {
fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) {
val source = src.toSource()
assertEquals(
Token(expected, source.posAt(row, col), type),
parseLing(source)[offset]
)
}
check("1", Token.Type.INT, 0, 0, "1")
check("7", Token.Type.INT, 0, 0, "7")
check("17", Token.Type.INT, 0, 0, "17")
check("17", Token.Type.INT, 0, 0, "17.")
check(".", Token.Type.DOT, 0, 2, "17.", 1)
// decimals
check("17.2", Token.Type.REAL, 0, 0, "17.2")
check("17.2", Token.Type.REAL, 0, 0, "17.2")
check("17.2", Token.Type.REAL, 0, 0, "17.2 ")
check("17.2", Token.Type.REAL, 0, 1, " 17.2")
check("17.2", Token.Type.REAL, 0, 2, " 17.2 ")
check("17.2e0", Token.Type.REAL, 0, 0, "17.2e0")
check("17.2e-22", Token.Type.REAL, 0, 0, "17.2e-22")
check("17.2e22", Token.Type.REAL, 0, 0, "17.2e+22")
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E+22")
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E22")
check("17.2e-22", Token.Type.REAL, 0, 0, "17.2E-22")
// hex
check("1", Token.Type.HEX, 0, 0, "0x1")
check("12", Token.Type.HEX, 0, 0, "0x12")
check("12abcdef", Token.Type.HEX, 0, 0, "0x12abcdef.gh")
check(".", Token.Type.DOT, 0, 10, "0x12abcdef.gh", 1)
check("gh", Token.Type.ID, 0, 11, "0x12abcdef.gh", 2)
check("5", Token.Type.INT, 0, 0, "5 6")
check("6", Token.Type.INT, 0, 2, "5 6", 1)
}
@Test
fun parserLabelsTest() {
val src = "label@ break@label".toSource()
val tt = parseLing(src)
assertEquals(Token("label", src.posAt(0, 0), Token.Type.LABEL), tt[0])
assertEquals(Token("break", src.posAt(0, 7), Token.Type.ID), tt[1])
assertEquals(Token("label", src.posAt(0, 12), Token.Type.ATLABEL), tt[2])
}
@Test
fun parse0Test() {
val src = """
println("Hello")
println( "world" )
""".trimIndent().toSource()
val p = parseLing(src).listIterator()
assertEquals(Token("println", src.posAt(0, 0), Token.Type.ID), p.next())
assertEquals(Token("(", src.posAt(0, 7), Token.Type.LPAREN), p.next())
assertEquals(Token("Hello", src.posAt(0, 8), Token.Type.STRING), p.next())
assertEquals(Token(")", src.posAt(0, 15), Token.Type.RPAREN), p.next())
assertEquals(Token("\n", src.posAt(0, 16), Token.Type.NEWLINE), p.next())
assertEquals(Token("println", src.posAt(1, 0), Token.Type.ID), p.next())
assertEquals(Token("(", src.posAt(1, 7), Token.Type.LPAREN), p.next())
assertEquals(Token("world", src.posAt(1, 9), Token.Type.STRING), p.next())
assertEquals(Token(")", src.posAt(1, 17), Token.Type.RPAREN), p.next())
}
@Test
fun parse1Test() {
val src = "2 + 7".toSource()
val p = parseLing(src).listIterator()
assertEquals(Token("2", src.posAt(0, 0), Token.Type.INT), p.next())
assertEquals(Token("+", src.posAt(0, 2), Token.Type.PLUS), p.next())
assertEquals(Token("7", src.posAt(0, 4), Token.Type.INT), p.next())
}
@Test
fun compileNumbersTest() = runTest {
assertEquals(ObjInt(17), eval("17"))
assertEquals(ObjInt(17), eval("+17"))
assertEquals(ObjInt(-17), eval("-17"))
assertEquals(ObjInt(1970), eval("1900 + 70"))
assertEquals(ObjInt(1970), eval("2000 - 30"))
// assertEquals(ObjReal(3.14), eval("3.14"))
assertEquals(ObjReal(314.0), eval("3.14e2"))
assertEquals(ObjReal(314.0), eval("100 3.14e2"))
assertEquals(ObjReal(314.0), eval("100\n 3.14e2"))
}
@Test
fun compileBuiltinCallsTest() = runTest {
// println(eval("π"))
val pi = eval("Math.PI")
assertIs<ObjReal>(pi)
assertTrue(pi.value - PI < 0.000001)
assertTrue(eval("Math.PI+1").toDouble() - PI - 1.0 < 0.000001)
assertTrue(eval("sin(Math.PI)").toDouble() - 1 < 0.000001)
assertTrue(eval("sin(π)").toDouble() - 1 < 0.000001)
}
@Test
fun varsAndConstsTest() = runTest {
val context = Context(pos = Pos.builtIn)
assertEquals(
ObjVoid, context.eval(
"""
val a = 17
var b = 3
""".trimIndent()
)
)
assertEquals(17, context.eval("a").toInt())
assertEquals(20, context.eval("b + a").toInt())
assertFailsWith<ScriptError> {
context.eval("a = 10")
}
assertEquals(10, context.eval("b = a - 3 - 4; b").toInt())
assertEquals(10, context.eval("b").toInt())
}
@Test
fun functionTest() = runTest {
val context = Context(pos = Pos.builtIn)
context.eval(
"""
fun foo(a, b) {
a + b
}
""".trimIndent()
)
assertEquals(17, context.eval("foo(3,14)").toInt())
assertFailsWith<ScriptError> {
assertEquals(17, context.eval("foo(3)").toInt())
}
context.eval(
"""
fn bar(a, b=10) {
a + b + 1
}
""".trimIndent()
)
assertEquals(10, context.eval("bar(3, 6)").toInt())
assertEquals(14, context.eval("bar(3)").toInt())
}
@Test
fun simpleClosureTest() = runTest {
val context = Context(pos = Pos.builtIn)
context.eval(
"""
var global = 10
fun foo(a, b) {
global + a + b
}
""".trimIndent()
)
assertEquals(27, context.eval("foo(3,14)").toInt())
context.eval("global = 20")
assertEquals(37, context.eval("foo(3,14)").toInt())
}
@Test
fun nullAndVoidTest() = runTest {
val context = Context(pos = Pos.builtIn)
assertEquals(ObjVoid, context.eval("void"))
assertEquals(ObjNull, context.eval("null"))
}
@Test
fun arithmeticOperatorsTest() = runTest {
assertEquals(2, eval("5/2").toInt())
assertEquals(2.5, eval("5.0/2").toDouble())
assertEquals(2.5, eval("5/2.0").toDouble())
assertEquals(2.5, eval("5.0/2.0").toDouble())
assertEquals(1, eval("5%2").toInt())
assertEquals(1.0, eval("5.0%2").toDouble())
assertEquals(77, eval("11 * 7").toInt())
assertEquals(2.0, eval("floor(5.0/2)").toDouble())
assertEquals(3, eval("ceil(5.0/2)").toInt())
assertEquals(2.0, eval("round(4.7/2)").toDouble())
assertEquals(3.0, eval("round(5.1/2)").toDouble())
}
@Test
fun arithmeticParenthesisTest() = runTest {
assertEquals(17, eval("2 + 3 * 5").toInt())
assertEquals(17, eval("2 + (3 * 5)").toInt())
assertEquals(25, eval("(2 + 3) * 5").toInt())
assertEquals(24, eval("(2 + 3) * 5 -1").toInt())
}
@Test
fun stringOpTest() = runTest {
assertEquals("foobar", eval(""" "foo" + "bar" """).toString())
assertEquals("foo17", eval(""" "foo" + 17 """).toString())
}
@Test
fun eqNeqTest() = runTest {
assertEquals(ObjBool(true), eval("val x = 2; x == 2"))
assertEquals(ObjBool(false), eval("val x = 3; x == 2"))
assertEquals(ObjBool(true), eval("val x = 3; x != 2"))
assertEquals(ObjBool(false), eval("val x = 3; x != 3"))
assertTrue { eval("1 == 1").toBool() }
assertTrue { eval("true == true").toBool() }
assertTrue { eval("true != false").toBool() }
assertFalse { eval("true == false").toBool() }
assertFalse { eval("false != false").toBool() }
assertTrue { eval("2 == 2 && 3 != 4").toBool() }
}
@Test
fun gtLtTest() = runTest {
assertTrue { eval("3 > 2").toBool() }
assertFalse { eval("3 > 3").toBool() }
assertTrue { eval("3 >= 2").toBool() }
assertFalse { eval("3 >= 4").toBool() }
assertFalse { eval("3 < 2").toBool() }
assertFalse { eval("3 <= 2").toBool() }
assertTrue { eval("3 <= 3").toBool() }
assertTrue { eval("3 <= 4").toBool() }
assertTrue { eval("3 < 4").toBool() }
assertFalse { eval("4 < 3").toBool() }
assertFalse { eval("4 <= 3").toBool() }
}
@Test
fun ifTest() = runTest {
// if - single line
var context = Context(pos = Pos.builtIn)
context.eval(
"""
fn test1(n) {
var result = "more"
if( n >= 10 )
result = "enough"
result
}
""".trimIndent()
)
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
// if - multiline (block)
context = Context(pos = Pos.builtIn)
context.eval(
"""
fn test1(n) {
var prefix = "answer: "
var result = "more"
if( n >= 10 ) {
var prefix = "bad:" // local prefix
prefix = "too bad:"
result = "enough"
}
prefix + result
}
""".trimIndent()
)
assertEquals("answer: enough", context.eval("test1(11)").toString())
assertEquals("answer: more", context.eval("test1(1)").toString())
// else single line1
context = Context(pos = Pos.builtIn)
context.eval(
"""
fn test1(n) {
if( n >= 10 )
"enough"
else
"more"
}
""".trimIndent()
)
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
// if/else with blocks
context = Context(pos = Pos.builtIn)
context.eval(
"""
fn test1(n) {
if( n > 20 ) {
"too much"
} else if( n >= 10 ) {
"enough"
}
else {
"more"
}
}
""".trimIndent()
)
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
assertEquals("too much", context.eval("test1(100)").toString())
}
@Test
fun lateInitTest() = runTest {
assertEquals(
"ok", eval(
"""
var late
fun init() {
late = "ok"
}
init()
late
""".trimIndent()
).toString()
)
}
@Test
fun whileTest() = runTest {
assertEquals(
5.0,
eval(
"""
var acc = 0
while( acc < 5 ) acc = acc + 0.5
acc
"""
).toDouble()
)
assertEquals(
5.0,
eval(
"""
var acc = 0
// return from while
while( acc < 5 ) {
acc = acc + 0.5
acc
}
"""
).toDouble()
)
assertEquals(
3.0,
eval(
"""
var acc = 0
while( acc < 5 ) {
acc = acc + 0.5
if( acc >= 3 ) break
}
acc
"""
).toDouble()
)
assertEquals(
17.0,
eval(
"""
var acc = 0
while( acc < 5 ) {
acc = acc + 0.5
if( acc >= 3 ) break 17
}
"""
).toDouble()
)
}
@Test
fun whileNonLocalBreakTest() = runTest {
assertEquals(
"ok2:3:7", eval(
"""
var t1 = 10
outer@ while( t1 > 0 ) {
var t2 = 10
while( t2 > 0 ) {
t2 = t2 - 1
if( t2 == 3 && t1 == 7) {
break@outer "ok2:"+t2+":"+t1
}
}
t1 = t1 - 1
t1
}
""".trimIndent()
).toString()
)
}
@Test
fun bookTest0() = runTest {
assertEquals(
"just 3",
eval(
"""
val count = 3
val res = if( count > 10 ) "too much" else "just " + count
println(count)
println(res)
res
""".trimIndent()
)
.toString()
)
assertEquals(
"just 3",
eval(
"""
val count = 3
var res = if( count > 10 ) "too much" else "it's " + count
res = if( count > 10 ) "too much" else "just " + count
res
""".trimIndent()
)
.toString()
)
}
@Test
fun testIncr() = runTest {
val c = Context()
c.eval("var x = 10")
assertEquals(10, c.eval("x++").toInt())
assertEquals(11, c.eval("x++").toInt())
assertEquals(12, c.eval("x").toInt())
assertEquals(12, c.eval("x").toInt())
assertEquals(12, c.eval("x").toInt())
}
@Test
fun testDecr() = runTest {
val c = Context()
c.eval("var x = 9")
assertEquals(9, c.eval("x--").toInt())
assertEquals(8, c.eval("x--").toInt())
assertEquals(7, c.eval("x--").toInt())
assertEquals(6, c.eval("x--").toInt())
assertEquals(5, c.eval("x").toInt())
}
@Test
fun testDecrIncr() = runTest {
val c = Context()
c.eval("var x = 9")
assertEquals(9, c.eval("x++").toInt())
assertEquals(10, c.eval("x++").toInt())
assertEquals(11, c.eval("x").toInt())
assertEquals(11, c.eval("x--").toInt())
assertEquals(10, c.eval("x--").toInt())
assertEquals(9, c.eval("x--").toInt())
assertEquals(8, c.eval("x--").toInt())
assertEquals(7, c.eval("x + 0").toInt())
}
@Test
fun testDecrIncr2() = runTest {
val c = Context()
c.eval("var x = 9")
assertEquals(9, c.eval("x--").toInt())
assertEquals(8, c.eval("x--").toInt())
assertEquals(7, c.eval("x--").toInt())
assertEquals(6, c.eval("x").toInt())
assertEquals(6, c.eval("x++").toInt())
assertEquals(7, c.eval("x++").toInt())
assertEquals(8, c.eval("x")
.also {
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
}
.toInt())
}
@Test
fun testDecrIncr3() = runTest {
val c = Context()
c.eval("var x = 9")
assertEquals(9, c.eval("x++").toInt())
assertEquals(10, c.eval("x++").toInt())
assertEquals(11, c.eval("x++").toInt())
assertEquals(12, c.eval("x").toInt())
assertEquals(12, c.eval("x--").toInt())
assertEquals(11, c.eval("x").toInt())
}
@Test
fun testIncrAndDecr() = runTest {
val c = Context()
assertEquals(
"8", c.eval(
"""
var x = 5
x--
x--
x++
x * 2
"""
).toString()
)
assertEquals("4", c.eval("x").toString())
// assertEquals( "8", c.eval("x*2").toString())
// assertEquals( "4", c.eval("x+0").toString())
}
@Test
fun bookTest2() = runTest {
val src = """
fn check(amount, prefix = "answer: ") {
prefix + if( amount > 100 )
"enough"
else
"more"
}
""".trimIndent()
eval(src)
}
}