From f881faf89f326ddc4d17bcc66613ae8477bb3778 Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 29 May 2025 11:48:04 +0400 Subject: [PATCH] fixed problem with var initialization with val added support for by-value types and in-place assignment --- .../kotlin/net/sergeych/ling/Compiler.kt | 21 +++++- .../kotlin/net/sergeych/ling/Context.kt | 9 +-- .../kotlin/net/sergeych/ling/Obj.kt | 22 ++++++ .../kotlin/net/sergeych/ling/Pos.kt | 2 +- library/src/commonTest/kotlin/ScriptTest.kt | 71 +++++++++++++++++-- 5 files changed, 111 insertions(+), 14 deletions(-) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 763061d..430a881 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -389,7 +389,9 @@ class Compiler( Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { cc.previous() val n = parseNumber(true, cc) - Accessor{ n.asReadonly } + Accessor{ + n.asReadonly + } } Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly } @@ -494,6 +496,8 @@ class Compiler( var result: Obj = ObjVoid while (condition.execute(it).toBool()) { try { + // we don't need to create new context here: if body is a block, + // parse block will do it, otherwise single statement doesn't need it: result = body.execute(it) } catch (lbe: LoopBreakContinueException) { if (lbe.label == label || lbe.label == null) { @@ -711,6 +715,7 @@ class Compiler( if (nameToken.type != Token.Type.ID) throw ScriptError(nameToken.pos, "Expected identifier after '$kind'") val name = nameToken.value + val eqToken = tokens.next() var setNull = false if (eqToken.type != Token.Type.ASSIGN) { @@ -721,12 +726,18 @@ class Compiler( setNull = true } } + val initialExpression = if (setNull) null else parseStatement(tokens) ?: throw ScriptError(eqToken.pos, "Expected initializer expression") + return statement(nameToken.pos) { context -> if (context.containsLocal(name)) throw ScriptError(nameToken.pos, "Variable $name is already defined") - val initValue = initialExpression?.execute(context) ?: ObjNull + + // init value could be a val; when we init by-value type var with it, we need to + // create a separate copy: + val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull + context.addItem(name, mutable, initValue) ObjVoid } @@ -843,6 +854,12 @@ class Compiler( Operator.simple(Token.Type.PERCENT, lastPrty) { ctx, a, b -> a.mod(ctx, b) }, ) +// private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN } +// +// fun performAssignment(context: Context, left: Accessor, right: Accessor) { +// assigner.generate(context.pos, left, right) +// } + val lastLevel = lastPrty + 1 val byLevel: List> = (0.. diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 53ed39b..93849d4 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -57,13 +57,8 @@ class Context( val newFn = object : Statement() { override val pos: Pos = Pos.builtIn - override suspend fun execute(context: Context): Obj { - return try { - context.fn() - } catch (e: Exception) { - raise(e.message ?: "unexpected error") - } - } + override suspend fun execute(context: Context): Obj = context.fn() + } for (name in names) { addItem( diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 9fefee8..a993e52 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -31,6 +31,13 @@ sealed class Obj { // private val memberMutex = Mutex() private val parentInstances = listOf() + /** + * Some objects are by-value, historically [ObjInt] and [ObjReal] are usually treated as such. + * When initializing a var with it, by value objects must be copied. By-reference ones aren't. + * + * Almost all objects are by-reference. + */ + open fun byValueCopy(): Obj = this /** * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. @@ -273,6 +280,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric { override val toObjInt: ObjInt by lazy { ObjInt(longValue) } override val toObjReal: ObjReal by lazy { ObjReal(value) } + override fun byValueCopy(): Obj = ObjReal(value) + override suspend fun compareTo(context: Context, other: Obj): Int { if (other !is Numeric) context.raiseError("cannot compare $this with $other") return value.compareTo(other.doubleValue) @@ -316,6 +325,8 @@ data class ObjInt(var value: Long) : Obj(), Numeric { override val toObjInt get() = this override val toObjReal = ObjReal(doubleValue) + override fun byValueCopy(): Obj = ObjInt(value) + override suspend fun getAndIncrement(context: Context): Obj { return ObjInt(value).also { value++ } } @@ -368,6 +379,17 @@ data class ObjInt(var value: Long) : Obj(), Numeric { ObjInt(this.value % other.value) else ObjReal(this.value.toDouble() % other.toDouble()) + /** + * We are by-value type ([byValueCopy] is implemented) so we can do in-place + * assignment + */ + override suspend fun assign(context: Context, other: Obj): Obj? { + return if( other is ObjInt) { + value = other.value + this + } else null + } + companion object { val type = ObjClass("Int") } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt index 9b9e316..f826fe8 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt @@ -2,7 +2,7 @@ package net.sergeych.ling data class Pos(val source: Source, val line: Int, val column: Int) { override fun toString(): String { - return "${source.fileName}:$line:$column" + return "${source.fileName}:${line+1}:${column}" } fun back(): Pos = diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index ff15377..7a1327c 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -451,6 +451,62 @@ class ScriptTest { ) } + @Test + fun testWhileBlockIsolation1() = runTest { + eval( + """ + var x = 100 + var cnt = 2 + while( cnt-- > 0 ) { + var x = cnt + 1 + assert(x == cnt + 1) + } + assert( x == 100 ) + assert( cnt == -1 ) + """.trimIndent() + ) + } + + @Test + fun testWhileBlockIsolation2() = runTest { + assertFails { + eval( + """ + var cnt = 2 + while( cnt-- > 0 ) { + var inner = cnt + 1 + assert(inner == cnt + 1) + } + println("inner "+inner) + """.trimIndent() + ) + } + } + + @Test + fun testWhileBlockIsolation3() = runTest { + eval(""" + var outer = 7 + var sum = 0 + var cnt1 = 0 + val initialForCnt2 = 0 + while( ++cnt1 < 3 ) { + var cnt2 = initialForCnt2 + + assert(cnt2 == 0) + assert(outer == 7) + + while(++cnt2 < 5) { + assert(initialForCnt2 == 0) + var outer = 1 + sum = sum + outer + } + } + println("sum "+sum) + """.trimIndent() + ) + } + @Test fun whileNonLocalBreakTest() = runTest { assertEquals( @@ -459,14 +515,18 @@ class ScriptTest { var t1 = 10 outer@ while( t1 > 0 ) { var t2 = 10 + println("starting t2 = " + t2) while( t2 > 0 ) { t2 = t2 - 1 println("t2 " + t2 + " t1 " + t1) if( t2 == 3 && t1 == 7) { + println("will break") break@outer "ok2:"+t2+":"+t1 } } + println("next t1") t1 = t1 - 1 + println("t1 now "+t1) t1 } """.trimIndent() @@ -607,12 +667,15 @@ class ScriptTest { fun testAssign1() = runTest { assertEquals(10, eval("var x = 5; x=10; x").toInt()) val ctx = Context() - ctx.eval(""" + ctx.eval( + """ var a = 1 - """.trimIndent()) + var b = 1 + """.trimIndent() + ) assertEquals(3, ctx.eval("a + a + 1").toInt()) - assertEquals(12, ctx.eval("a + (a = 10) + 1").toInt()) - assertEquals(10, ctx.eval("a").toInt()) + assertEquals(12, ctx.eval("a + (b = 10) + 1").toInt()) + assertEquals(10, ctx.eval("b").toInt()) } @Test