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

This commit is contained in:
Sergey Chernov 2026-01-05 21:13:15 +01:00
parent 20f777f9f6
commit f792c73b8f
17 changed files with 283 additions and 48 deletions

4
archived/README.md Normal file
View File

@ -0,0 +1,4 @@
# Obsolete files
__Do not rely on contents of the files in this directory. They are kept for historical reference only and may not be up-to-date or relevant.__

View File

@ -0,0 +1,117 @@
/*
This is a tech proposal under construction, please do not use it yet
for any purpose
*/
/*
Abstract delegate can be used to proxy read/wrtie field access
or method call. Default implementation reports error.
*/
interface Delegate {
fun getValue() = Unset
fun setValue(newValue) { throw NotImplementedException("delegate setter is not implemented") }
fun invoke(args...) { throw NotImplementedException("delegate setter is not implemented") }
}
/*
Delegate cam be used to implement a val, var or fun, so there are
access type enum to distinguish:
*/
enum DelegateAccess {
Val,
Var,
Callable
}
// Delegate can be associated by a val/var/fun in a declaraion site using `by` keyword
val proxiedVal by proxy(1)
var proxiedVar by proxy(2, 3)
fun proxiedFun by proxy()
// each proxy is a Lyng expression returning instance of the Proxy interface:
/*
Proxy interface is connecting some named property of a given kind with the `Delegate`.
It removes the burden of dealing with property name and this ref on each get/set value
or invoke allowing having one delegate per instance, execution buff.
*/
interface Proxy {
fun getDelegate(propertyName: String,access: DelegateAccess,thisRef: Obj?): Delegate
}
// val, var and fun can be delegated, local or class instance:
class TestProxy: Proxy {
override getDelegate(name,access,thisRef) {
Delegate()
}
}
val proxy = TestProxy()
class Allowed {
val v1 by proxy
var v2 by proxy
fun f1 by proxy
}
val v3 by proxy
var v4 by proxy
fun f2 by proxy
/*
It means that for example
Allowed().f1("foo")
would call a delegate.invoke("foo") on the `Delegate` instance supplied by `proxy`, etc.
*/
// The practic sample: lazy value
/*
The delegate that caches single time evaluated value
*/
class LazyDelegate(creator): Delegate {
private var currentValue=Unset
override fun getValue() {
if( currentValue == Unset )
currentValue = creator()
currentValue
}
}
/*
The proxy to assign it
*/
class LazyProxy(creator) {
fun getDelegate(name,access,thisRef) {
if( access != DelegateAccess.Val )
throw IllegalArgumentException("only lazy val are allowed")
LazyDelegate(creator)
}
}
/*
A helper function to simplify creation:
*/
fun lazy(creator) {
LazyProxy(creator)
}
// Usage sample and the test:
var callCounter = 0
assertEquals(0, clallCounter)
val lazyText by lazy { "evaluated text" }
// the lazy property is not yet evaluated:
assertEquals(0, clallCounter)
// now evaluate it by using it:
assertEquals("evaluated text", lazyText)
assertEquals(1, callCounter)
// lazy delegate should fail on vars or funs:
assertThrows { var bad by lazy { "should not happen" } }
assertThrows { fun bad by lazy { 42 } }

View File

@ -1546,7 +1546,7 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
[Range]: Range.md
[String]: development/String.md
[String]: ../archived/development/String.md
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary

View File

@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,7 +20,6 @@
*/
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@ -53,11 +52,11 @@ kotlin {
browser()
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs() {
browser()
nodejs()
}
// @OptIn(ExperimentalWasmDsl::class)
// wasmJs() {
// browser()
// nodejs()
// }
// Keep expect/actual warning suppressed consistently with other modules
targets.configureEach {
@ -94,13 +93,13 @@ kotlin {
implementation("com.squareup.okio:okio-nodefilesystem:${libs.versions.okioVersion.get()}")
}
}
// For Wasm we use in-memory VFS for now
val wasmJsMain by getting {
dependencies {
api(libs.okio)
implementation(libs.okio.fakefilesystem)
}
}
// // For Wasm we use in-memory VFS for now
// val wasmJsMain by getting {
// dependencies {
// api(libs.okio)
// implementation(libs.okio.fakefilesystem)
// }
// }
}
}

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 {

View File

@ -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

View File

@ -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(

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -329,7 +329,7 @@
<!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em">
v1.1.0-SNAPSHOT
v1.1.1-SNAPSHOT
</span>
</div>
<!-- Fixed top navbar for the whole site -->