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 <noreply@anthropic.com>
This commit is contained in:
Sergey Chernov 2026-04-07 19:41:18 +03:00
parent 2f145a0ea7
commit f145a90845
5 changed files with 56 additions and 3 deletions

View File

@ -31,6 +31,7 @@ sealed class CodeContext {
var typeParamDecls: List<TypeDecl.TypeParam> = emptyList()
val pendingInitializations = mutableMapOf<String, Pos>()
val declaredMembers = mutableSetOf<String>()
val declaredMethodNames = mutableSetOf<String>()
val classScopeMembers = mutableSetOf<String>()
val memberOverrides = mutableMapOf<String, Boolean>()
val memberFieldIds = mutableMapOf<String, Int>()

View File

@ -456,7 +456,7 @@ class Compiler(
}
}
private fun predeclareClassMembers(target: MutableSet<String>, overrides: MutableMap<String, Boolean>) {
private fun predeclareClassMembers(target: MutableSet<String>, overrides: MutableMap<String, Boolean>, methodNames: MutableSet<String>? = 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,

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -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()
)
}
}