diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 62460bb..b8c75ce 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -97,6 +97,10 @@ class Compiler { parseExpression(tokens) } } + Token.Type.PLUS2, Token.Type.MINUS2 -> { + tokens.previous() + parseExpression(tokens) + } Token.Type.LABEL -> continue Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue @@ -222,6 +226,25 @@ class Compiler { throw ScriptError(t.pos, "Expected identifier after '.'") } + Token.Type.PLUS2 -> { + statement(id.pos) { context -> + context.pos = id.pos + val v = resolve(context).get(id.value) + ?: throw ScriptError(id.pos, "Undefined symbol: ${id.value}") + v.value?.getAndIncrement(context) + ?: context.raiseNPE() + } + } + + Token.Type.MINUS2 -> { + statement(id.pos) { context -> + context.pos = id.pos + val v = resolve(context).get(id.value) + ?: throw ScriptError(id.pos, "Undefined symbol: ${id.value}") + v.value?.getAndDecrement(context) + ?: context.raiseNPE() + } + } Token.Type.LPAREN -> { // function call // Load arg list @@ -243,10 +266,11 @@ class Compiler { resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined function: ${id.value}") (v.value as? Statement)?.execute( context.copy( + id.pos, Arguments( nt.pos, args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } - ) + ), ) ) ?: throw ScriptError(id.pos, "Variable $id is not callable ($id)") @@ -501,8 +525,9 @@ class Compiler { var closure: Context? = null val fnBody = statement(t.pos) { callerContext -> + callerContext.pos = start // restore closure where the function was defined: - val context = closure ?: Context() + val context = closure ?: callerContext.raiseError("bug: closure not set") // load params from caller context for ((i, d) in params.withIndex()) { if (i < callerContext.args.size) @@ -539,7 +564,7 @@ class Compiler { val block = parseScript(t.pos, tokens) return statement(t.pos) { // block run on inner context: - block.execute(it.copy()) + block.execute(it.copy(t.pos)) }.also { val t1 = tokens.next() if (t1.type != Token.Type.RBRACE) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 1d0e08d..47e8a94 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -1,9 +1,27 @@ package net.sergeych.ling class Context( - val parent: Context? = Script.defaultContext.copy(), - val args: Arguments = Arguments.EMPTY + val parent: Context?, + val args: Arguments = Arguments.EMPTY, + var pos: Pos = Pos.builtIn ) { + constructor( + args: Arguments = Arguments.EMPTY, + pos: Pos = Pos.builtIn + ) + : this(Script.defaultContext, args, pos) + + fun raiseNotImplemented(): Nothing = raiseError("operation not implemented") + + fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) + + fun raiseError(message: String): Nothing { + throw ExecutionError(ObjError(this, message)) + } + + fun raiseError(obj: ObjError): Nothing { + throw ExecutionError(obj) + } private val objects = mutableMapOf() @@ -11,14 +29,20 @@ class Context( objects[name] ?: parent?.get(name) - fun copy(args: Arguments = Arguments.EMPTY): Context = Context(this, args) + fun copy(pos: Pos, args: Arguments = Arguments.EMPTY): Context = Context(this, args, pos) fun addItem(name: String, isMutable: Boolean, value: Obj?) { objects.put(name, StoredObj(name, value, isMutable)) } fun getOrCreateNamespace(name: String) = - (objects.getOrPut(name) { StoredObj(name, ObjNamespace(name,copy()), isMutable = false) }.value as ObjNamespace) + (objects.getOrPut(name) { + StoredObj( + name, + ObjNamespace(name, copy(pos)), + isMutable = false + ) + }.value as ObjNamespace) .context inline fun addFn(vararg names: String, crossinline fn: suspend Context.() -> T) { @@ -43,7 +67,7 @@ class Context( } } - inline fun addConst(value: T,vararg names: String) { + inline fun addConst(value: T, vararg names: String) { val obj = Obj.from(value) for (name in names) { addItem( @@ -59,8 +83,5 @@ class Context( fun containsLocal(name: String): Boolean = name in objects - companion object { - operator fun invoke() = Context() - } } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index b01f90e..d9c8a74 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -27,7 +27,7 @@ sealed class ClassDef( ) { instanceLock.withLock { instanceMethods[name]?.let { - if( !it.isMutable ) + if (!it.isMutable) throw ScriptError(pos, "existing method $name is frozen and can't be updated") it.value = body } ?: instanceMethods.put(name, Item(body, freeze)) @@ -36,7 +36,7 @@ sealed class ClassDef( //suspend fun callInstanceMethod(context: Context, self: Obj,args: Arguments): Obj { // - // } + // } } object ObjClassDef : ClassDef("Obj") @@ -49,6 +49,18 @@ sealed class Obj : Comparable { open val definition: ClassDef = ObjClassDef + open fun plus(context: Context, other: Obj): Obj { + context.raiseNotImplemented() + } + + open fun getAndIncrement(context: Context): Obj { + context.raiseNotImplemented() + } + + open fun getAndDecrement(context: Context): Obj { + context.raiseNotImplemented() + } + @Suppress("unused") enum class Type { @SerialName("Void") @@ -179,12 +191,27 @@ data class ObjReal(val value: Double) : Obj(), Numeric { @Serializable @SerialName("int") -data class ObjInt(val value: Long) : Obj(), Numeric { - override val asStr by lazy { ObjString(value.toString()) } - override val longValue: Long by lazy { value } - override val doubleValue: Double by lazy { value.toDouble() } - override val toObjInt: ObjInt by lazy { ObjInt(value) } - override val toObjReal: ObjReal by lazy { ObjReal(doubleValue) } +data class ObjInt(var value: Long) : Obj(), Numeric { + override val asStr get() = ObjString(value.toString()) + override val longValue get() = value + override val doubleValue get() = value.toDouble() + override val toObjInt get() = this + override val toObjReal = ObjReal(doubleValue) + + override fun getAndIncrement(context: Context): Obj { + return ObjInt(value).also { value++ } + } + + override fun getAndDecrement(context: Context): Obj { + return ObjInt(value).also { value-- } + } + +// override fun plus(context: Context, other: Obj): Obj { +// if (other !is Numeric) +// context.raiseError("cannot add $this with $other") +// return if (other is ObjInt) +// +// } override fun compareTo(other: Obj): Int { if (other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other") @@ -215,4 +242,13 @@ data class ObjNamespace(val name: String, val context: Context) : Obj() { override fun compareTo(other: Obj): Int { throw IllegalArgumentException("cannot compare namespaces") } -} \ No newline at end of file +} + +open class ObjError(val context: Context, val message: String) : Obj() { + override val asStr: ObjString by lazy { ObjString("Error: $message") } + override fun compareTo(other: Obj): Int { + if (other === this) return 0 else return -1 + } +} + +class ObjNullPointerError(context: Context) : ObjError(context, "object is null") diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index a7a4b4c..c7c0752 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -50,8 +50,21 @@ private class Parser(fromPos: Pos) { Token("=", from, Token.Type.ASSIGN) } - '+' -> Token("+", from, Token.Type.PLUS) - '-' -> Token("-", from, Token.Type.MINUS) + '+' -> { + if( currentChar == '+') { + advance() + Token("+", from, Token.Type.PLUS2) + } + else + Token("+", from, Token.Type.PLUS) + } + '-' -> { + if (currentChar == '-') { + advance() + Token("--", from, Token.Type.MINUS2) + } else + Token("-", from, Token.Type.MINUS) + } '*' -> Token("*", from, Token.Type.STAR) '/' -> { if( currentChar == '/') { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 0cee68d..0921a6f 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -15,10 +15,10 @@ class Script( return lastResult } - suspend fun execute() = execute(defaultContext.copy()) + suspend fun execute() = execute(defaultContext.copy(pos)) companion object { - val defaultContext: Context = Context(null).apply { + val defaultContext: Context = Context().apply { addFn("println") { print("yn: ") for( (i,a) in args.withIndex() ) { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt index a3bba71..6d17d8d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt @@ -1,9 +1,13 @@ +@file:Suppress("CanBeParameter") + package net.sergeych.ling -class ScriptError(val pos: Pos, val errorMessage: String) : Exception( +open class ScriptError(val pos: Pos, val errorMessage: String) : Exception( """ $pos: Error: $errorMessage ${pos.currentLine} ${"-".repeat(pos.column)}^ """.trimIndent() ) + +class ExecutionError(val errorObject: ObjError) : ScriptError(errorObject.context.pos, errorObject.message) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index 7acbfa7..bcf86b0 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -8,6 +8,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) { ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA, SEMICOLON, COLON, PLUS, MINUS, STAR, SLASH, ASSIGN, + PLUS2, MINUS2, EQ, NEQ, LT, LTE, GT, GTE, AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT, SINLGE_LINE_COMMENT, MULTILINE_COMMENT, diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 30570f8..265ca24 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -117,7 +117,7 @@ class ScriptTest { @Test fun varsAndConstsTest() = runTest { - val context = Context() + val context = Context(pos = Pos.builtIn) assertEquals( ObjVoid, context.eval( """ @@ -137,7 +137,7 @@ class ScriptTest { @Test fun functionTest() = runTest { - val context = Context() + val context = Context(pos = Pos.builtIn) context.eval( """ fun foo(a, b) { @@ -163,7 +163,7 @@ class ScriptTest { @Test fun simpleClosureTest() = runTest { - val context = Context() + val context = Context(pos = Pos.builtIn) context.eval( """ var global = 10 @@ -180,7 +180,7 @@ class ScriptTest { @Test fun nullAndVoidTest() = runTest { - val context = Context() + val context = Context(pos = Pos.builtIn) assertEquals(ObjVoid, context.eval("void")) assertEquals(ObjNull, context.eval("null")) } @@ -252,7 +252,7 @@ class ScriptTest { @Test fun ifTest() = runTest { // if - single line - var context = Context() + var context = Context(pos = Pos.builtIn) context.eval( """ fn test1(n) { @@ -267,7 +267,7 @@ class ScriptTest { assertEquals("more", context.eval("test1(1)").toString()) // if - multiline (block) - context = Context() + context = Context(pos = Pos.builtIn) context.eval( """ fn test1(n) { @@ -286,7 +286,7 @@ class ScriptTest { assertEquals("answer: more", context.eval("test1(1)").toString()) // else single line1 - context = Context() + context = Context(pos = Pos.builtIn) context.eval( """ fn test1(n) { @@ -301,7 +301,7 @@ class ScriptTest { assertEquals("more", context.eval("test1(1)").toString()) // if/else with blocks - context = Context() + context = Context(pos = Pos.builtIn ) context.eval( """ fn test1(n) { @@ -443,4 +443,86 @@ class ScriptTest { .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()) + } } \ No newline at end of file