Files
lyng/inner_bugs/fixed/1.md
T

3.8 KiB

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

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:

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:

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:

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).