1.1.1-SNPASHOT some serious bugs in initilazation fixed. They were revealed by delegation real world usage

This commit is contained in:
sergeych committed 2026-01-05 21:13:42 +01:00
1 parent 20f777f9f6
commit f792c73b8f
17 files changed
+283 -48

No files matched your search

+6 -7
View File
@@ -17,11 +17,10 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.1.0-SNAPSHOT"
version = "1.1.1-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@@ -66,11 +65,11 @@ kotlin {
browser()
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs() {
browser()
nodejs()
}
// @OptIn(ExperimentalWasmDsl::class)
// wasmJs() {
// browser()
// nodejs()
// }
// Suppress Beta warning for expect/actual classes across all targets
targets.configureEach {
@@ -623,9 +623,16 @@ open class Scope(
suspend fun resolve(rec: ObjRecord, name: String): Obj {
if (rec.type == ObjRecord.Type.Delegated) {
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate")
val del = rec.delegate ?: run {
if (thisObj is ObjInstance) {
val res = (thisObj as ObjInstance).resolveRecord(this, rec, name, rec.declaringClass).value
rec.value = res
return res
}
raiseError("Internal error: delegated property $name has no delegate")
}
val th = if (thisObj === ObjVoid) ObjNull else thisObj
return del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
val res = del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
// If getValue not found, return a wrapper that calls invoke
object : Statement() {
override val pos: Pos = Pos.builtIn
@@ -636,13 +643,21 @@ open class Scope(
}
}
})!!
rec.value = res
return res
}
return rec.value
}
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
if (rec.type == ObjRecord.Type.Delegated) {
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate")
val del = rec.delegate ?: run {
if (thisObj is ObjInstance) {
(thisObj as ObjInstance).writeField(this, name, newValue)
return
}
raiseError("Internal error: delegated property $name has no delegate")
}
val th = if (thisObj === ObjVoid) ObjNull else thisObj
del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue))
return
@@ -137,11 +137,20 @@ open class Obj {
// methods that to override
open suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other === this) return 0
if( other === ObjNull ) return 2
if (other === this) return 0
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
scope.raiseNotImplemented()
}
open suspend fun equals(scope: Scope, other: Obj): Boolean {
if (other === this) return true
return try {
compareTo(scope, other) == 0
} catch (e: ExecutionError) {
false
}
}
open suspend fun contains(scope: Scope, other: Obj): Boolean {
return invokeInstanceMethod(scope, "contains", other).toBool()
}
@@ -364,7 +373,7 @@ open class Obj {
)
}
protected open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
if (obj.type == ObjRecord.Type.Delegated) {
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
return obj.copy(
@@ -235,7 +235,7 @@ open class ObjClass(
// 1) members-defined methods
for ((k, v) in members) {
if (v.value is Statement || v.type == ObjRecord.Type.Delegated) {
instance.instanceScope.objects[k] = v
instance.instanceScope.objects[k] = if (v.type == ObjRecord.Type.Delegated) v.copy() else v
}
}
// 2) class-scope methods registered during class-body execution
@@ -243,7 +243,7 @@ open class ObjClass(
if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) {
// if not already present, copy reference for dispatch
if (!instance.instanceScope.objects.containsKey(k)) {
instance.instanceScope.objects[k] = rec
instance.instanceScope.objects[k] = if (rec.type == ObjRecord.Type.Delegated) rec.copy() else rec
}
}
}
@@ -480,7 +480,7 @@ open class ObjClass(
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createClassField(name, statement { code() }, isOpen)
createClassField(name, statement { code() }, isOpen, type = ObjRecord.Type.Fun)
}
@@ -588,11 +588,21 @@ open class ObjClass(
getInstanceMemberOrNull(name)?.let { rec ->
val decl = rec.declaringClass ?: findDeclaringClassOf(name) ?: this
if (rec.type == ObjRecord.Type.Delegated) {
val del = rec.delegate ?: scope.raiseError("Internal error: delegated function $name has no delegate")
val del = rec.delegate ?: scope.raiseError("Internal error: delegated member $name has no delegate")
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = {
// Fallback: property delegation
val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
propVal.invoke(scope, this, args, decl)
})!!
}
if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(scope, this, args, decl)
} else {
// Resolved field or property value
val resolved = readField(scope, name)
return resolved.value.invoke(scope, this, args, decl)
}
return rec.value.invoke(scope, this, args, decl)
}
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
@@ -92,10 +92,9 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
}
}
del = del ?: obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
return obj.copy(
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))),
type = ObjRecord.Type.Other
)
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
obj.value = res
return obj
}
return super.resolveRecord(scope, obj, name, decl)
}
@@ -197,11 +196,15 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${cls.className}::$name"
val del = instanceScope[storageName]?.delegate ?: rec.delegate
?: scope.raiseError("Internal error: delegated function $name has no delegate (tried $storageName)")
?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)")
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = {
// Fallback: property delegation
val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
propVal.invoke(scope, this, args, rec.declaringClass ?: cls)
})!!
}
if (rec.type != ObjRecord.Type.Property && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Fun && !rec.isAbstract) {
val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
@@ -217,6 +220,9 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
args,
decl
)
} else if ((rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property) && !rec.isAbstract) {
val resolved = readField(scope, name)
return resolved.value.invoke(scope, this, args, resolved.declaringClass)
}
}
}
@@ -243,8 +243,8 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
BinOp.OR -> a.logicalOr(scope, b)
BinOp.AND -> a.logicalAnd(scope, b)
BinOp.EQARROW -> ObjMapEntry(a, b)
BinOp.EQ -> ObjBool(a.compareTo(scope, b) == 0)
BinOp.NEQ -> ObjBool(a.compareTo(scope, b) != 0)
BinOp.EQ -> ObjBool(a.equals(scope, b))
BinOp.NEQ -> ObjBool(!a.equals(scope, b))
BinOp.REF_EQ -> ObjBool(a === b)
BinOp.REF_NEQ -> ObjBool(a !== b)
BinOp.MATCH -> a.operatorMatch(scope, b)
@@ -611,7 +611,7 @@ class FieldRef(
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = target.get(scope).value
val base = target.evalValue(scope)
if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment
return
@@ -714,10 +714,9 @@ class FieldRef(
override suspend fun evalValue(scope: Scope): Obj {
// Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths
val fastRval = PerfFlags.RVAL_FASTPATH
val fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
val base = target.evalValue(scope)
if (base == ObjNull && isOptional) return ObjNull
if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base)
@@ -1583,7 +1582,7 @@ class FastLocalVarRef(
if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
return actualOwner.resolve(rec, name)
return scope.resolve(rec, name)
}
}
// Try per-frame local binding maps in the ancestry first
@@ -210,4 +210,81 @@ class DelegationTest {
assert(l is Delegate)
""")
}
@Test
fun testRealLifeBug1() = runTest {
eval("""
class Cell {
val tags = [1,2,3]
}
class T {
val cell by lazy { Cell() }
val tags get() = cell.tags
}
assertEquals([1,2,3], T().tags)
""".trimIndent())
}
@Test
fun testInstanceIsolation() = runTest {
eval("""
class CounterDelegate() {
private var count = 0
fun getValue(thisRef, name) = ++count
}
class Foo {
val x by CounterDelegate()
}
val f1 = Foo()
val f2 = Foo()
assertEquals(1, f1.x)
assertEquals(1, f2.x)
assertEquals(2, f1.x)
assertEquals(2, f2.x)
""")
}
@Test
fun testLazyRegexBug() = runTest {
eval("""
class T {
val re by lazy { Regex(".*") }
}
val t = T()
t.re
// Second access triggered the bug before fix (value == Unset failed)
t.re
""")
}
@Test
fun testEqualityRobustness() = runTest {
eval("""
val re1 = Regex("a")
val re2 = Regex("a")
// Equality should not throw even if types don't implement compareTo
assertEquals(true, re1 == re1)
assertEquals(false, re1 == re2)
assertEquals(false, re1 == Unset)
assertEquals(false, re1 == null)
""")
}
@Test
fun testLazy2() = runTest {
eval("""
class A {
val tags = [1,2,3]
}
class B {
val tags by lazy { myA.tags }
val myA by lazy { A() }
}
assert( B().tags == [1,2,3])
""".trimIndent())
}
}