diff --git a/docs/ai_language_reference.md b/docs/ai_language_reference.md index a90e0a6..df1b154 100644 --- a/docs/ai_language_reference.md +++ b/docs/ai_language_reference.md @@ -133,6 +133,8 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T - generic: `type Box = List` - aliases are expanded structurally. - Classes/objects/enums/interfaces: + - Constructors use the class name. A direct member function (including `static`) cannot have the same name as its enclosing class; local functions may still shadow names normally. + - `this(...)` and `this@Type(...)` are not secondary-constructor delegation syntax; call the class by name to construct another instance. - `interface` is parsed as abstract class synonym. - `object` supports named singleton and anonymous object expression forms. - enums support lifted entries: `enum E* { A, B }`. diff --git a/inner_bugs/fixed/1.md b/inner_bugs/fixed/1.md new file mode 100644 index 0000000..3e463bc --- /dev/null +++ b/inner_bugs/fixed/1.md @@ -0,0 +1,74 @@ + +### Ticket 1 — Name resolution: a member function named like its class silently shadows the primary constructor inside the class body + +**Summary.** `class` bodies accept a member function whose name equals the class name. That member then wins name resolution for **all unqualified calls inside the class body**, including `init` blocks, method bodies and nested lambdas. The primary constructor becomes unreachable from inside the class, with no escape hatch. Compiles cleanly, breaks at runtime. + +**Repro** +```lyng +class C1(x) { + fun C1(y): String = "member " + y.toString() + init { println(C1(9)) } // -> "member C1(9)", NOT the constructor +} + +class C2(x) { + fun C2(y, z): String = y.toString() + z.toString() + fun probe(): String = C2(7, 8) // member chosen although ctor takes 1 arg +} + +class Fact(x) { + fun Fact(y): Fact = Fact(y * 2) // compiles; unqualified Fact(...) = itself +} +Fact(1).Fact(3) // java.lang.StackOverflowError +``` +Local functions shadow too: +```lyng +class M3(x) { + fun probe(y): String { + fun M3(z): String = "local " + z.toString() + return M3(y) // local member wins, ctor unreachable + } +} +``` +No workaround found: +```lyng +class Q1(x) { + fun Q1(y): String = "member" + fun a(): String = Q1.Q1(3) // IllegalArgumentException: expected 1 arguments, got 0 + fun b(): String = (Q1(5) as Q1).Q1(6) // ClassCastException: Cannot cast String to Q1 +} +class G1(x) { fun g(): G1 = this(x + 1) } // Exception: operation is not implemented +``` +Related symptom: adding `Type::class` in a class that also has a same-name member turns the **declaration** itself into a hard error pointing at the wrong place: +```lyng +class D16(x) { + fun D16(y): String = "member " + y.toString() // Error: expected 1 arguments, got 0 + fun lit(): String = D16::class.toString() +} +``` +Control cases compile fine: same-name member alone ✔, `::class` alone ✔, the combination ✘. + +**Expected.** Either (a) reject a member whose name collides with the class name at compile time (preferred — it is never a constructor in Lyng, there are no secondary constructors), or (b) keep constructor calls resolvable for the primary arity and fall back to members only for non-matching arity, plus a working `this(...)`/`this@Type(...)` delegation. In any case `Type::class` must not be affected by a same-name member, and diagnostics must point at the actual construct. + +**Suggested tests.** `fun SameName(...)` declared in a class with the same name → expected compile error; self-recursive factory → must not reach runtime stack overflow; `Type::class` inside such a class → must compile; `this(...)` → either implement or emit a clear compile-time error instead of `operation is not implemented`. + + +## Resolution + +Valid: direct instance/static member functions named after their enclosing class +are now rejected at the function name. This also rejects the collision before +any `::class` expression can produce a misleading downstream diagnostic. +The supported control is `Type::class` in a class without the illegal member; +the suggested test asking the illegal declaration to compile contradicts the +report's preferred resolution. + +Direct `this(...)` / `this@Type(...)` calls on the enclosing class instance now +report unsupported constructor delegation during compilation. Use `Type(...)` +to construct another instance; no secondary constructors were introduced. + +Local functions shadowing class names are ordinary lexical shadowing, not a bug, +and remain supported. Regression coverage: `ClassNameMemberRegressionTest`. +Before the fix, 2 of its 4 tests failed; the constructor/class-literal and local +shadowing controls passed. + +Validation: all 4 focused JVM regression tests passed, followed by the full +`:lynglib:jvmTest` suite (benchmarks excluded by the repository default). diff --git a/inner_bugs/invalid/README.md b/inner_bugs/invalid/README.md new file mode 100644 index 0000000..0174530 --- /dev/null +++ b/inner_bugs/invalid/README.md @@ -0,0 +1,5 @@ +# Invalid reports + +Place reports here when reproduction and the language contract show that they +are not bugs. Append the reason and verification evidence to each moved report. +Valid reports with an invalid subclaim stay with their resolution notes. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 6ce4e2e..556fd52 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -7458,6 +7458,14 @@ class Compiler( isOptional: Boolean, explicitTypeArgs: List? = null ): ObjRef { + val thisTypeName = when (left) { + is LocalVarRef -> if (left.name == "this") currentImplicitThisTypeName() else null + is QualifiedThisRef -> left.typeName + else -> null + } + if (thisTypeName != null && codeContexts.any { it is CodeContext.ClassBody && it.name == thisTypeName }) { + throw ScriptError(cc.currentPos(), "constructor delegation through this is not supported; call the class by name") + } var detectedBlockArgument = blockArgument val expectedReceiver = tailBlockReceiverType(left) val expectedTailCallable = expectedCallableArgumentType(left, 0) @@ -9984,6 +9992,10 @@ class Compiler( name = t.value nameStartPos = t.pos } + if (parentContext is CodeContext.ClassBody && extTypeName == null && + name == parentContext.name.substringAfterLast('.')) { + throw ScriptError(nameStartPos, "member function cannot have the same name as its enclosing class: $name") + } val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) } val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null diff --git a/lynglib/src/commonTest/kotlin/ClassNameMemberRegressionTest.kt b/lynglib/src/commonTest/kotlin/ClassNameMemberRegressionTest.kt new file mode 100644 index 0000000..152a547 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/ClassNameMemberRegressionTest.kt @@ -0,0 +1,62 @@ +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.ScriptError +import net.sergeych.lyng.eval +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ClassNameMemberRegressionTest { + @Test + fun rejectsClassNamedMembersAtTheirDeclaration() = runTest { + for (member in listOf( + "fun Same(y): String = y.toString()", + "fun Same(y, z): String = y.toString() + z.toString()", + "fun Same(y): Same = Same(y * 2)", + "static fun Same(y): Same = Same(y)", + "fun Same(y): String = \"member\"; fun literal() = Same::class.toString()" + )) { + val error = assertFailsWith { + eval("class Same(x) {\n $member\n}") + } + assertContains(error.errorMessage, "member function cannot have the same name as its enclosing class") + assertEquals(1, error.pos.line) + } + } + + @Test + fun thisIsNotConstructorDelegation() = runTest { + for (receiver in listOf("this", "this@Factory")) { + val error = assertFailsWith { + eval("class Factory(x) { fun again(): Factory = $receiver(x + 1) }") + } + assertContains(error.errorMessage, "constructor delegation through this is not supported") + } + } + + @Test + fun constructorAndClassLiteralRemainAvailable() = runTest { + assertEquals("3", eval(""" + class Factory(val x: Int) { + fun again(y: Int): Factory = Factory(y) + fun literal(): String = Factory::class.toString() + } + val f = Factory(1) + f.literal() + f.again(3).x.toString() + """.trimIndent()).toString()) + } + + @Test + fun localFunctionsCanStillShadowClassNames() = runTest { + assertEquals("local 7", eval(""" + class Local(x) { + fun probe(y): String { + fun Local(z): String = "local " + z.toString() + return Local(y) + } + } + Local(1).probe(7) + """.trimIndent()).toString()) + } +}