Reject class-named members and unsupported constructor delegation

This commit is contained in:
sergeych committed 2026-10-20 23:57:41 +04:00
1 parent 26a1ca9710
commit 04c9343a9d
5 files changed
+155

No files matched your search

+2
View File
@@ -133,6 +133,8 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
- generic: `type Box<T> = List<T>` - generic: `type Box<T> = List<T>`
- aliases are expanded structurally. - aliases are expanded structurally.
- Classes/objects/enums/interfaces: - 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. - `interface` is parsed as abstract class synonym.
- `object` supports named singleton and anonymous object expression forms. - `object` supports named singleton and anonymous object expression forms.
- enums support lifted entries: `enum E* { A, B }`. - enums support lifted entries: `enum E* { A, B }`.
+74
View File
@@ -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).
+5
View File
@@ -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.
@@ -7458,6 +7458,14 @@ class Compiler(
isOptional: Boolean, isOptional: Boolean,
explicitTypeArgs: List<TypeDecl>? = null explicitTypeArgs: List<TypeDecl>? = null
): ObjRef { ): 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 var detectedBlockArgument = blockArgument
val expectedReceiver = tailBlockReceiverType(left) val expectedReceiver = tailBlockReceiverType(left)
val expectedTailCallable = expectedCallableArgumentType(left, 0) val expectedTailCallable = expectedCallableArgumentType(left, 0)
@@ -9984,6 +9992,10 @@ class Compiler(
name = t.value name = t.value
nameStartPos = t.pos 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 extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
@@ -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<ScriptError> {
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<ScriptError> {
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())
}
}