Preserve static return types and initialize singleton static methods
This commit is contained in:
1 parent
04c9343a9d
commit
c34371b784
7 files changed
+162
-10
No files matched your search
@@ -126,6 +126,7 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- extension functions: `fun Type.name(...) { ... }`.
|
||||
- context-aware extension functions: `context(Tag) fun String.unaryPlus() { this@Tag.addText(this) }`.
|
||||
- named singleton `object` declarations can be extension receivers too: `fun Config.describe(...) { ... }`, `val Config.tag get() = ...`.
|
||||
- Static class/object methods preserve declared or inferable return types at call sites, including chained member access and generic return types inferred from arguments. For example, `static fun of<T>(v: T): Box<T> = Box(v)` lets `Box.of("abc").value.length` retain the `String` member type.
|
||||
- static extension functions are callable on the type object: `static fun List<T>.fill(...)` -> `List.fill(...)`.
|
||||
- delegated callable: `fun f(...) by delegate`.
|
||||
- Type aliases:
|
||||
|
||||
@@ -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.
|
||||
@@ -88,6 +88,9 @@ internal suspend fun executeClassDecl(
|
||||
classScope.addConst("object", newClass)
|
||||
|
||||
spec.bodyInit?.let { executeBytecodeWithSeed(classScope, it, "object body init") }
|
||||
for (statement in spec.initScope) {
|
||||
executeBytecodeWithSeed(classScope, statement, "object static init")
|
||||
}
|
||||
|
||||
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
|
||||
if (spec.declaredName != null) {
|
||||
|
||||
@@ -3121,7 +3121,7 @@ class Compiler(
|
||||
val field = FieldRef(left, next.value, isOptional)
|
||||
operand = CallRef(field, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
} else {
|
||||
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs, classScopeCall = true)
|
||||
}
|
||||
} else {
|
||||
// instance method call
|
||||
@@ -3238,14 +3238,14 @@ class Compiler(
|
||||
resolutionSink?.referenceReflection(name.value, next.pos)
|
||||
}
|
||||
}
|
||||
MethodCallRef(left, next.value, args, true, isOptional)
|
||||
MethodCallRef(left, next.value, args, true, isOptional, classScopeCall = shouldTreatAsClassScopeCall(left, next.value))
|
||||
} else {
|
||||
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
|
||||
if (unionCall != null) {
|
||||
unionCall
|
||||
} else {
|
||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||
MethodCallRef(left, next.value, args, true, isOptional)
|
||||
MethodCallRef(left, next.value, args, true, isOptional, classScopeCall = shouldTreatAsClassScopeCall(left, next.value))
|
||||
}
|
||||
}
|
||||
is QualifiedThisRef ->
|
||||
@@ -3265,7 +3265,7 @@ class Compiler(
|
||||
unionCall
|
||||
} else {
|
||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||
MethodCallRef(left, next.value, args, true, isOptional)
|
||||
MethodCallRef(left, next.value, args, true, isOptional, classScopeCall = shouldTreatAsClassScopeCall(left, next.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8686,6 +8686,7 @@ class Compiler(
|
||||
|
||||
val initScope = popInitScope()
|
||||
val wrappedBodyInit = bodyInit?.let { wrapClassBodyBytecode(it, "object@$className") }
|
||||
val wrappedInitScope = initScope.map { wrapClassBodyBytecode(it, "objectInit@$className") }
|
||||
|
||||
val spec = ClassDeclSpec(
|
||||
declaredName = declaredName,
|
||||
@@ -8701,7 +8702,7 @@ class Compiler(
|
||||
constructorArgs = null,
|
||||
constructorFieldIds = null,
|
||||
bodyInit = wrappedBodyInit,
|
||||
initScope = emptyList()
|
||||
initScope = wrappedInitScope
|
||||
)
|
||||
return ClassDeclStatement(spec)
|
||||
}
|
||||
@@ -10071,7 +10072,7 @@ class Compiler(
|
||||
pendingTypeParamStack.removeLast()
|
||||
}
|
||||
|
||||
if (parentContext is CodeContext.ClassBody && !isStatic && extTypeName == null) {
|
||||
if (parentContext is CodeContext.ClassBody && extTypeName == null) {
|
||||
classMemberTypeDeclByName.getOrPut(parentContext.name) { mutableMapOf() }[name] = TypeDecl.Function(
|
||||
receiver = receiverTypeDecl,
|
||||
contextReceivers = contextReceiverTypeDecls,
|
||||
@@ -10245,7 +10246,7 @@ class Compiler(
|
||||
val inferredReturnDecl = returnTypeDecl ?: inferReturnTypeDeclFromStatement(rawFnStatements)
|
||||
val inferredReturnClass = inferredReturnDecl?.let { resolveTypeDeclObjClass(it) }
|
||||
?: inferReturnClassFromStatement(rawFnStatements)
|
||||
if (parentContext is CodeContext.ClassBody && !isStatic && extTypeName == null) {
|
||||
if (parentContext is CodeContext.ClassBody && extTypeName == null) {
|
||||
val ownerClassName = parentContext.name
|
||||
run {
|
||||
val memberTypeDecl = TypeDecl.Function(
|
||||
|
||||
@@ -4876,11 +4876,16 @@ class BytecodeCompiler(
|
||||
val receiverClass = resolveReceiverClass(ref.receiver) ?: ObjDynamic.type
|
||||
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
|
||||
val dst = allocSlot()
|
||||
if (shouldUseClassScopeReceiver(ref.receiver, receiver, receiverClass)) {
|
||||
val classReceiver = shouldUseClassScopeReceiver(ref.receiver, receiver, receiverClass)
|
||||
if (ref.classScopeCall || classReceiver) {
|
||||
// Singleton objects carry their static members on the instance's class.
|
||||
val classSlot = if (classReceiver) receiver.slot else allocSlot().also {
|
||||
builder.emit(Opcode.GET_OBJ_CLASS, receiver.slot, it)
|
||||
}
|
||||
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||
val memberSlot = allocSlot()
|
||||
if (!ref.isOptional) {
|
||||
builder.emit(Opcode.GET_CLASS_SCOPE, receiver.slot, nameId, memberSlot)
|
||||
builder.emit(Opcode.GET_CLASS_SCOPE, classSlot, nameId, memberSlot)
|
||||
} else {
|
||||
val nullSlot = allocSlot()
|
||||
builder.emit(Opcode.CONST_NULL, nullSlot)
|
||||
@@ -4892,7 +4897,7 @@ class BytecodeCompiler(
|
||||
Opcode.JMP_IF_TRUE,
|
||||
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
|
||||
)
|
||||
builder.emit(Opcode.GET_CLASS_SCOPE, receiver.slot, nameId, memberSlot)
|
||||
builder.emit(Opcode.GET_CLASS_SCOPE, classSlot, nameId, memberSlot)
|
||||
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
|
||||
builder.mark(nullLabel)
|
||||
builder.emit(Opcode.CONST_NULL, memberSlot)
|
||||
|
||||
@@ -484,6 +484,7 @@ class MethodCallRef(
|
||||
internal val tailBlock: Boolean,
|
||||
internal val isOptional: Boolean,
|
||||
internal val explicitTypeArgs: List<TypeDecl>? = null,
|
||||
internal val classScopeCall: Boolean = false,
|
||||
) : ObjRef {
|
||||
// 4-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC)
|
||||
private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StaticReturnTypeRegressionTest {
|
||||
@Test
|
||||
fun declaredFactoryReturnSupportsInferredVariablesAndChains() = runTest {
|
||||
assertEquals("hi hi hi", eval("""
|
||||
class Factory(x) {
|
||||
static fun make(y): Factory = Factory(y)
|
||||
fun hi(): String = "hi"
|
||||
}
|
||||
val a: Factory = Factory.make(1)
|
||||
val b = Factory.make(2)
|
||||
a.hi() + " " + b.hi() + " " + Factory.make(3).hi()
|
||||
""".trimIndent()).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun declaredScalarReturnsSupportMembersAndArithmetic() = runTest {
|
||||
assertEquals("4", eval("""
|
||||
class Scalars {
|
||||
static fun text(y): String = "abc"
|
||||
static fun num(y: Int): Int = y
|
||||
}
|
||||
val s = Scalars.text(1)
|
||||
val n = Scalars.num(1)
|
||||
s.length + n
|
||||
""".trimIndent()).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singletonStaticReturnSupportsMembers() = runTest {
|
||||
assertEquals("3", eval("""
|
||||
object Singleton { static fun text(): String = "abc" }
|
||||
val text = Singleton.text()
|
||||
text.length
|
||||
""".trimIndent()).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singletonStaticTailBlockAndInstanceMethodsUseTheirOwnDispatch() = runTest {
|
||||
assertEquals("6", eval("""
|
||||
object Singleton {
|
||||
static fun text(block: ()->String): String = block()
|
||||
fun instanceText(): String = "abc"
|
||||
}
|
||||
Singleton.text { "abc" }.length + Singleton.instanceText().length
|
||||
""".trimIndent()).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun inferredStaticReturnMatchesInstanceAndTopLevelFunctions() = runTest {
|
||||
assertEquals("9", eval("""
|
||||
class Factory {
|
||||
static fun text() = "abc"
|
||||
fun instanceText() = "abc"
|
||||
}
|
||||
fun topText() = "abc"
|
||||
Factory.text().length + Factory().instanceText().length + topText().length
|
||||
""".trimIndent()).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun genericStaticReturnSubstitutesArgumentType() = runTest {
|
||||
assertEquals("7", eval("""
|
||||
class Box<T>(val value: T) {
|
||||
static fun of<T>(v: T): Box<T> = Box(v)
|
||||
}
|
||||
val box = Box.of("abc")
|
||||
val explicit = Box.of<String>("abcd")
|
||||
box.value.length + explicit.value.length
|
||||
""".trimIndent()).toString())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user