fixed problem with var initialization with val

added support for by-value types and in-place assignment
This commit is contained in:
Sergey Chernov 2025-05-29 11:48:04 +04:00
parent ca93d73b9c
commit f881faf89f
5 changed files with 111 additions and 14 deletions

View File

@ -389,7 +389,9 @@ class Compiler(
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
cc.previous() cc.previous()
val n = parseNumber(true, cc) val n = parseNumber(true, cc)
Accessor{ n.asReadonly } Accessor{
n.asReadonly
}
} }
Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly } Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly }
@ -494,6 +496,8 @@ class Compiler(
var result: Obj = ObjVoid var result: Obj = ObjVoid
while (condition.execute(it).toBool()) { while (condition.execute(it).toBool()) {
try { 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) result = body.execute(it)
} catch (lbe: LoopBreakContinueException) { } catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) { if (lbe.label == label || lbe.label == null) {
@ -711,6 +715,7 @@ class Compiler(
if (nameToken.type != Token.Type.ID) if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier after '$kind'") throw ScriptError(nameToken.pos, "Expected identifier after '$kind'")
val name = nameToken.value val name = nameToken.value
val eqToken = tokens.next() val eqToken = tokens.next()
var setNull = false var setNull = false
if (eqToken.type != Token.Type.ASSIGN) { if (eqToken.type != Token.Type.ASSIGN) {
@ -721,12 +726,18 @@ class Compiler(
setNull = true setNull = true
} }
} }
val initialExpression = if (setNull) null else parseStatement(tokens) val initialExpression = if (setNull) null else parseStatement(tokens)
?: throw ScriptError(eqToken.pos, "Expected initializer expression") ?: throw ScriptError(eqToken.pos, "Expected initializer expression")
return statement(nameToken.pos) { context -> return statement(nameToken.pos) { context ->
if (context.containsLocal(name)) if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined") 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) context.addItem(name, mutable, initValue)
ObjVoid ObjVoid
} }
@ -843,6 +854,12 @@ class Compiler(
Operator.simple(Token.Type.PERCENT, lastPrty) { ctx, a, b -> a.mod(ctx, b) }, 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 lastLevel = lastPrty + 1
val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l -> val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l ->

View File

@ -57,13 +57,8 @@ class Context(
val newFn = object : Statement() { val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj { override suspend fun execute(context: Context): Obj = context.fn()
return try {
context.fn()
} catch (e: Exception) {
raise(e.message ?: "unexpected error")
}
}
} }
for (name in names) { for (name in names) {
addItem( addItem(

View File

@ -31,6 +31,13 @@ sealed class Obj {
// private val memberMutex = Mutex() // private val memberMutex = Mutex()
private val parentInstances = listOf<Obj>() private val parentInstances = listOf<Obj>()
/**
* 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. * 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 toObjInt: ObjInt by lazy { ObjInt(longValue) }
override val toObjReal: ObjReal by lazy { ObjReal(value) } override val toObjReal: ObjReal by lazy { ObjReal(value) }
override fun byValueCopy(): Obj = ObjReal(value)
override suspend fun compareTo(context: Context, other: Obj): Int { override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is Numeric) context.raiseError("cannot compare $this with $other") if (other !is Numeric) context.raiseError("cannot compare $this with $other")
return value.compareTo(other.doubleValue) return value.compareTo(other.doubleValue)
@ -316,6 +325,8 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
override val toObjInt get() = this override val toObjInt get() = this
override val toObjReal = ObjReal(doubleValue) override val toObjReal = ObjReal(doubleValue)
override fun byValueCopy(): Obj = ObjInt(value)
override suspend fun getAndIncrement(context: Context): Obj { override suspend fun getAndIncrement(context: Context): Obj {
return ObjInt(value).also { value++ } return ObjInt(value).also { value++ }
} }
@ -368,6 +379,17 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
ObjInt(this.value % other.value) ObjInt(this.value % other.value)
else ObjReal(this.value.toDouble() % other.toDouble()) 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 { companion object {
val type = ObjClass("Int") val type = ObjClass("Int")
} }

View File

@ -2,7 +2,7 @@ package net.sergeych.ling
data class Pos(val source: Source, val line: Int, val column: Int) { data class Pos(val source: Source, val line: Int, val column: Int) {
override fun toString(): String { override fun toString(): String {
return "${source.fileName}:$line:$column" return "${source.fileName}:${line+1}:${column}"
} }
fun back(): Pos = fun back(): Pos =

View File

@ -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 @Test
fun whileNonLocalBreakTest() = runTest { fun whileNonLocalBreakTest() = runTest {
assertEquals( assertEquals(
@ -459,14 +515,18 @@ class ScriptTest {
var t1 = 10 var t1 = 10
outer@ while( t1 > 0 ) { outer@ while( t1 > 0 ) {
var t2 = 10 var t2 = 10
println("starting t2 = " + t2)
while( t2 > 0 ) { while( t2 > 0 ) {
t2 = t2 - 1 t2 = t2 - 1
println("t2 " + t2 + " t1 " + t1) println("t2 " + t2 + " t1 " + t1)
if( t2 == 3 && t1 == 7) { if( t2 == 3 && t1 == 7) {
println("will break")
break@outer "ok2:"+t2+":"+t1 break@outer "ok2:"+t2+":"+t1
} }
} }
println("next t1")
t1 = t1 - 1 t1 = t1 - 1
println("t1 now "+t1)
t1 t1
} }
""".trimIndent() """.trimIndent()
@ -607,12 +667,15 @@ class ScriptTest {
fun testAssign1() = runTest { fun testAssign1() = runTest {
assertEquals(10, eval("var x = 5; x=10; x").toInt()) assertEquals(10, eval("var x = 5; x=10; x").toInt())
val ctx = Context() val ctx = Context()
ctx.eval(""" ctx.eval(
"""
var a = 1 var a = 1
""".trimIndent()) var b = 1
""".trimIndent()
)
assertEquals(3, ctx.eval("a + a + 1").toInt()) assertEquals(3, ctx.eval("a + a + 1").toInt())
assertEquals(12, ctx.eval("a + (a = 10) + 1").toInt()) assertEquals(12, ctx.eval("a + (b = 10) + 1").toInt())
assertEquals(10, ctx.eval("a").toInt()) assertEquals(10, ctx.eval("b").toInt())
} }
@Test @Test