From f145a90845ce5819b493ea2726964d3aaed9d846 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 7 Apr 2026 19:41:18 +0300 Subject: [PATCH] Fix method slot ID collision between instance and static methods When createField() was called with a pre-assigned methodId, the ID was used but methodIdMap was not updated and nextMethodId was not advanced. This caused assignMethodId() for static methods to reuse slot IDs already occupied by instance methods. In complex.lyng, fromInt/imaginary got IDs 12/14 (same as plus/mul), causing binary operator dispatch via CALL_MEMBER_SLOT to call the wrong function (e.g. a*b would invoke imaginary instead of mul). Fix: after computing effectiveMethodId in createField, always register it in methodIdMap and advance nextMethodId past it so subsequent auto-assignments start from a clean range. Also pre-assigns method IDs for non-static fun/fn declarations during class body pre-scan so forward references resolve correctly in class bodies, and adds ComplexModuleTest coverage for operator slot dispatch. Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/net/sergeych/lyng/CodeContext.kt | 1 + .../kotlin/net/sergeych/lyng/Compiler.kt | 15 +++++++++++++-- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 7 ++++++- lynglib/src/commonTest/kotlin/OOTest.kt | 18 ++++++++++++++++++ .../net/sergeych/lyng/ComplexModuleTest.kt | 18 ++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt index 3a6c6fc..1a1e7e4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt @@ -31,6 +31,7 @@ sealed class CodeContext { var typeParamDecls: List = emptyList() val pendingInitializations = mutableMapOf() val declaredMembers = mutableSetOf() + val declaredMethodNames = mutableSetOf() val classScopeMembers = mutableSetOf() val memberOverrides = mutableMapOf() val memberFieldIds = mutableMapOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 7259d0c..514d860 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -456,7 +456,7 @@ class Compiler( } } - private fun predeclareClassMembers(target: MutableSet, overrides: MutableMap) { + private fun predeclareClassMembers(target: MutableSet, overrides: MutableMap, methodNames: MutableSet? = null) { val saved = cc.savePos() var depth = 0 val modifiers = setOf( @@ -478,18 +478,22 @@ class Compiler( Token.Type.RBRACE -> if (depth == 0) break else depth-- Token.Type.ID -> if (depth == 0) { var sawOverride = false + var sawStatic = false while (t.type == Token.Type.ID && t.value in modifiers) { if (t.value == "override") sawOverride = true + if (t.value == "static") sawStatic = true t = nextNonWs() } when (t.value) { "fun", "fn", "val", "var" -> { + val isMethod = t.value == "fun" || t.value == "fn" val nameToken = nextNonWs() if (nameToken.type == Token.Type.ID) { val afterName = cc.peekNextNonWhitespace() if (afterName.type != Token.Type.DOT) { target.add(nameToken.value) overrides[nameToken.value] = sawOverride + if (isMethod && !sawStatic) methodNames?.add(nameToken.value) } } } @@ -7757,7 +7761,7 @@ class Compiler( classCtx?.let { ctx -> val callableMembers = classScopeCallableMembersByClassName.getOrPut(qualifiedName) { mutableSetOf() } predeclareClassScopeMembers(qualifiedName, ctx.classScopeMembers, callableMembers) - predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides) + predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides, ctx.declaredMethodNames) val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null if (existingExternInfo != null) { ctx.memberFieldIds.putAll(existingExternInfo.fieldIds) @@ -7806,6 +7810,13 @@ class Compiler( ctx.memberFieldIds[param.name] = ctx.nextFieldId++ } } + // Pre-assign method IDs for all declared methods so forward + // references within the class body resolve correctly. + for (method in ctx.declaredMethodNames) { + if (!ctx.memberMethodIds.containsKey(method)) { + ctx.memberMethodIds[method] = ctx.nextMethodId++ + } + } compileClassInfos[qualifiedName] = CompileClassInfo( name = qualifiedName, packageName = packageName, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 75a065b..8166f74 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -926,11 +926,16 @@ open class ObjClass( candidate.methodId } else null } - methodId ?: inherited ?: methodIdMap[name]?.let { it } ?: run { + val id = methodId ?: inherited ?: methodIdMap[name]?.let { it } ?: run { methodIdMap[name] = nextMethodId nextMethodId++ methodIdMap[name]!! } + // Register the resolved ID so subsequent assignMethodId calls (e.g. for static + // methods) don't reuse the same numeric slot for a different member. + methodIdMap[name] = id + if (id >= nextMethodId) nextMethodId = id + 1 + id } else { methodId } diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index d3db727..9fdcd41 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -1199,4 +1199,22 @@ class OOTest { """.trimIndent()) } + @Test + fun testForwardSymbolsUsageMustBeAllowed() = runTest { + eval(""" + class Foo(x) { + fn fn2() { + fn1() + println("fn2") + } + fn fn1() { + println("fn1") + } + } + + val foo = Foo(33) + foo.fn2() + """.trimIndent()) + } + } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt index 3e17fb4..bd02a43 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt @@ -111,4 +111,22 @@ class ComplexModuleTest { ) } + @Test + fun testOperatorSlotDispatch() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.complex + val a = Complex(1.0, 2.0) + val b = Complex(3.0, -1.0) + val product = a * b + assertEquals(5.0, product.re) + assertEquals(5.0, product.im) + val sum = a + Complex(0.0, 0.0) + assertEquals(1.0, sum.re) + assertEquals(2.0, sum.im) + """.trimIndent() + ) + } + }