Preserve static return types and initialize singleton static methods

This commit is contained in:
sergeych committed 2026-10-21 00:01:11 +04:00
1 parent 04c9343a9d
commit c34371b784
7 files changed
+162 -10

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
### Ticket 2 — Static member calls: declared return type is not propagated, receiver becomes `Object`
**Summary.** The **invocation** of a `static` member (class or `object` singleton) loses its declared return type at the call site; the result is treated as a general receiver, so every type-specific member access fails to compile. The runtime value is correct — this is purely a static-typing defect. Constructor calls, instance-method calls and top-level function calls all preserve the declared type correctly, so the inconsistency is specific to static members.
**Repro**
```lyng
class S(x) {
static fun make(y): S = S(y) // return type is explicitly declared
fun hi(): String = "hi"
}
val a: S = S.make(1) // OK — annotation rescues it
println(a.hi())
val b = S.make(2) // b is S == true at runtime
println(b.hi()) // Error: member access requires compile-time receiver type: hi
println(S.make(1).hi())// Error: same, chained call
println((S.make(3) as S).hi()) // OK — explicit cast rescues it
fun need(v: S) = v.hi(); need(S.make(4)) // OK — typed argument position rescues it
```
Not limited to self-returning factories — any declared return type is dropped:
```lyng
class K(x) { static fun text(y): String = "t"; static fun num(y): Int = y }
val s = K.text(1); println(s.length) // Error: ... compile-time receiver type: length
val n = K.num(1); println(n + 1) // same class of failure
object O { static fun text(y): String = "o" }
val o = O.text(1); println(o.length) // Error: ... on O.text result
class B(x) { static fun noRet(y) = B(y) } // untyped static: also general
class Box<T>(val value: T) { static fun of<T>(v: T): Box<T> = Box(v) }
val bx = Box.of(42); println(bx.value) // Error; val bx2: Box<Int> = Box.of(43) works
```
Controls that **do** work correctly: `val d = K8(9); d.hi()` (constructor), `val c = S(3).again(4); c.hi()` (instance method), `val t = topLevelMake(1); t.x` (top-level function).
**Expected.** `StaticClass.member(args)` must produce the declared return type as the static compile-time receiver type, exactly like instance and top-level calls. Only genuinely untyped statics (`fun` without a return type and uninferrable bodies) should fall back to `Object`/`Obj`.
**Suggested tests.** Matrix over `{constructor | instance method | top-level fun | static fun} × {declared ret type | inferred | omitted}` asserting that `.length`, arithmetic, `is T`-then-member, chained calls and generic instantiation behave identically for statics as for the other three; plus `object` singletons and generic statics.
## Resolution
Valid: static method signatures were excluded from the compiler's member type
metadata, both before body compilation and when inferred returns were recorded.
They now use the same callable signature and generic substitution path as
instance methods. Explicit annotations, inferred expression returns, primitive
returns, chained calls, and inferred/explicit generic arguments are covered by
`StaticReturnTypeRegressionTest`.
The singleton example additionally exposed runtime bugs once typing was
corrected: object static initializers were discarded, and a static call on an
object was sent to instance-method dispatch. Object declarations now preserve
and execute their static initialization statements before constructing the
singleton instance.
The compiler now retains its class-scope call decision for bytecode generation;
singleton static calls use the object's class and the existing class-scope
access opcode. Instance methods retain their dispatch path. Tests also cover
static calls with a trailing lambda.
Before the fix, all 5 original regression cases failed with missing compile-time
receiver types. The initial type-only correction passed 4, with the singleton
case then failing at runtime; dispatch is corrected in this same report.
Validation: all 6 focused JVM regression tests passed, followed by all 1,040
`:lynglib:jvmTest` tests (benchmarks excluded by the repository default).
`git diff --check` also passed.