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(...) { ... }`.
|
- extension functions: `fun Type.name(...) { ... }`.
|
||||||
- context-aware extension functions: `context(Tag) fun String.unaryPlus() { this@Tag.addText(this) }`.
|
- 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() = ...`.
|
- 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(...)`.
|
- static extension functions are callable on the type object: `static fun List<T>.fill(...)` -> `List.fill(...)`.
|
||||||
- delegated callable: `fun f(...) by delegate`.
|
- delegated callable: `fun f(...) by delegate`.
|
||||||
- Type aliases:
|
- 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)
|
classScope.addConst("object", newClass)
|
||||||
|
|
||||||
spec.bodyInit?.let { executeBytecodeWithSeed(classScope, it, "object body init") }
|
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))
|
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
|
||||||
if (spec.declaredName != null) {
|
if (spec.declaredName != null) {
|
||||||
|
|||||||
@@ -3121,7 +3121,7 @@ class Compiler(
|
|||||||
val field = FieldRef(left, next.value, isOptional)
|
val field = FieldRef(left, next.value, isOptional)
|
||||||
operand = CallRef(field, args, tailBlock, isOptional, memberCallTypeArgs)
|
operand = CallRef(field, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||||
} else {
|
} else {
|
||||||
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs, classScopeCall = true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// instance method call
|
// instance method call
|
||||||
@@ -3238,14 +3238,14 @@ class Compiler(
|
|||||||
resolutionSink?.referenceReflection(name.value, next.pos)
|
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 {
|
} else {
|
||||||
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
|
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
|
||||||
if (unionCall != null) {
|
if (unionCall != null) {
|
||||||
unionCall
|
unionCall
|
||||||
} else {
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
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 ->
|
is QualifiedThisRef ->
|
||||||
@@ -3265,7 +3265,7 @@ class Compiler(
|
|||||||
unionCall
|
unionCall
|
||||||
} else {
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
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 initScope = popInitScope()
|
||||||
val wrappedBodyInit = bodyInit?.let { wrapClassBodyBytecode(it, "object@$className") }
|
val wrappedBodyInit = bodyInit?.let { wrapClassBodyBytecode(it, "object@$className") }
|
||||||
|
val wrappedInitScope = initScope.map { wrapClassBodyBytecode(it, "objectInit@$className") }
|
||||||
|
|
||||||
val spec = ClassDeclSpec(
|
val spec = ClassDeclSpec(
|
||||||
declaredName = declaredName,
|
declaredName = declaredName,
|
||||||
@@ -8701,7 +8702,7 @@ class Compiler(
|
|||||||
constructorArgs = null,
|
constructorArgs = null,
|
||||||
constructorFieldIds = null,
|
constructorFieldIds = null,
|
||||||
bodyInit = wrappedBodyInit,
|
bodyInit = wrappedBodyInit,
|
||||||
initScope = emptyList()
|
initScope = wrappedInitScope
|
||||||
)
|
)
|
||||||
return ClassDeclStatement(spec)
|
return ClassDeclStatement(spec)
|
||||||
}
|
}
|
||||||
@@ -10071,7 +10072,7 @@ class Compiler(
|
|||||||
pendingTypeParamStack.removeLast()
|
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(
|
classMemberTypeDeclByName.getOrPut(parentContext.name) { mutableMapOf() }[name] = TypeDecl.Function(
|
||||||
receiver = receiverTypeDecl,
|
receiver = receiverTypeDecl,
|
||||||
contextReceivers = contextReceiverTypeDecls,
|
contextReceivers = contextReceiverTypeDecls,
|
||||||
@@ -10245,7 +10246,7 @@ class Compiler(
|
|||||||
val inferredReturnDecl = returnTypeDecl ?: inferReturnTypeDeclFromStatement(rawFnStatements)
|
val inferredReturnDecl = returnTypeDecl ?: inferReturnTypeDeclFromStatement(rawFnStatements)
|
||||||
val inferredReturnClass = inferredReturnDecl?.let { resolveTypeDeclObjClass(it) }
|
val inferredReturnClass = inferredReturnDecl?.let { resolveTypeDeclObjClass(it) }
|
||||||
?: inferReturnClassFromStatement(rawFnStatements)
|
?: inferReturnClassFromStatement(rawFnStatements)
|
||||||
if (parentContext is CodeContext.ClassBody && !isStatic && extTypeName == null) {
|
if (parentContext is CodeContext.ClassBody && extTypeName == null) {
|
||||||
val ownerClassName = parentContext.name
|
val ownerClassName = parentContext.name
|
||||||
run {
|
run {
|
||||||
val memberTypeDecl = TypeDecl.Function(
|
val memberTypeDecl = TypeDecl.Function(
|
||||||
|
|||||||
@@ -4876,11 +4876,16 @@ class BytecodeCompiler(
|
|||||||
val receiverClass = resolveReceiverClass(ref.receiver) ?: ObjDynamic.type
|
val receiverClass = resolveReceiverClass(ref.receiver) ?: ObjDynamic.type
|
||||||
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
|
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
|
||||||
val dst = allocSlot()
|
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 nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||||
val memberSlot = allocSlot()
|
val memberSlot = allocSlot()
|
||||||
if (!ref.isOptional) {
|
if (!ref.isOptional) {
|
||||||
builder.emit(Opcode.GET_CLASS_SCOPE, receiver.slot, nameId, memberSlot)
|
builder.emit(Opcode.GET_CLASS_SCOPE, classSlot, nameId, memberSlot)
|
||||||
} else {
|
} else {
|
||||||
val nullSlot = allocSlot()
|
val nullSlot = allocSlot()
|
||||||
builder.emit(Opcode.CONST_NULL, nullSlot)
|
builder.emit(Opcode.CONST_NULL, nullSlot)
|
||||||
@@ -4892,7 +4897,7 @@ class BytecodeCompiler(
|
|||||||
Opcode.JMP_IF_TRUE,
|
Opcode.JMP_IF_TRUE,
|
||||||
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
|
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.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
|
||||||
builder.mark(nullLabel)
|
builder.mark(nullLabel)
|
||||||
builder.emit(Opcode.CONST_NULL, memberSlot)
|
builder.emit(Opcode.CONST_NULL, memberSlot)
|
||||||
|
|||||||
@@ -484,6 +484,7 @@ class MethodCallRef(
|
|||||||
internal val tailBlock: Boolean,
|
internal val tailBlock: Boolean,
|
||||||
internal val isOptional: Boolean,
|
internal val isOptional: Boolean,
|
||||||
internal val explicitTypeArgs: List<TypeDecl>? = null,
|
internal val explicitTypeArgs: List<TypeDecl>? = null,
|
||||||
|
internal val classScopeCall: Boolean = false,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
// 4-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC)
|
// 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
|
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