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() var typeParamDecls: List<TypeDecl.TypeParam> = emptyList()
val pendingInitializations = mutableMapOf<String, Pos>() val pendingInitializations = mutableMapOf<String, Pos>()
val declaredMembers = mutableSetOf<String>() val declaredMembers = mutableSetOf<String>()
val declaredMethodNames = mutableSetOf<String>()
val classScopeMembers = mutableSetOf<String>() val classScopeMembers = mutableSetOf<String>()
val memberOverrides = mutableMapOf<String, Boolean>() val memberOverrides = mutableMapOf<String, Boolean>()
val memberFieldIds = mutableMapOf<String, Int>() 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() val saved = cc.savePos()
var depth = 0 var depth = 0
val modifiers = setOf( val modifiers = setOf(
@ -478,18 +478,22 @@ class Compiler(
Token.Type.RBRACE -> if (depth == 0) break else depth-- Token.Type.RBRACE -> if (depth == 0) break else depth--
Token.Type.ID -> if (depth == 0) { Token.Type.ID -> if (depth == 0) {
var sawOverride = false var sawOverride = false
var sawStatic = false
while (t.type == Token.Type.ID && t.value in modifiers) { while (t.type == Token.Type.ID && t.value in modifiers) {
if (t.value == "override") sawOverride = true if (t.value == "override") sawOverride = true
if (t.value == "static") sawStatic = true
t = nextNonWs() t = nextNonWs()
} }
when (t.value) { when (t.value) {
"fun", "fn", "val", "var" -> { "fun", "fn", "val", "var" -> {
val isMethod = t.value == "fun" || t.value == "fn"
val nameToken = nextNonWs() val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) { if (nameToken.type == Token.Type.ID) {
val afterName = cc.peekNextNonWhitespace() val afterName = cc.peekNextNonWhitespace()
if (afterName.type != Token.Type.DOT) { if (afterName.type != Token.Type.DOT) {
target.add(nameToken.value) target.add(nameToken.value)
overrides[nameToken.value] = sawOverride overrides[nameToken.value] = sawOverride
if (isMethod && !sawStatic) methodNames?.add(nameToken.value)
} }
} }
} }
@ -7757,7 +7761,7 @@ class Compiler(
classCtx?.let { ctx -> classCtx?.let { ctx ->
val callableMembers = classScopeCallableMembersByClassName.getOrPut(qualifiedName) { mutableSetOf() } val callableMembers = classScopeCallableMembersByClassName.getOrPut(qualifiedName) { mutableSetOf() }
predeclareClassScopeMembers(qualifiedName, ctx.classScopeMembers, callableMembers) 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 val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null
if (existingExternInfo != null) { if (existingExternInfo != null) {
ctx.memberFieldIds.putAll(existingExternInfo.fieldIds) ctx.memberFieldIds.putAll(existingExternInfo.fieldIds)
@ -7806,6 +7810,13 @@ class Compiler(
ctx.memberFieldIds[param.name] = ctx.nextFieldId++ 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( compileClassInfos[qualifiedName] = CompileClassInfo(
name = qualifiedName, name = qualifiedName,
packageName = packageName, packageName = packageName,

View File

@ -926,11 +926,16 @@ open class ObjClass(
candidate.methodId candidate.methodId
} else null } else null
} }
methodId ?: inherited ?: methodIdMap[name]?.let { it } ?: run { val id = methodId ?: inherited ?: methodIdMap[name]?.let { it } ?: run {
methodIdMap[name] = nextMethodId methodIdMap[name] = nextMethodId
nextMethodId++ nextMethodId++
methodIdMap[name]!! 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 { } else {
methodId methodId
} }

View File

@ -1199,4 +1199,22 @@ class OOTest {
""".trimIndent()) """.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()
)
}
} }