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

View File

@ -17,11 +17,10 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" 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 // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@ -66,11 +65,11 @@ kotlin {
browser() browser()
nodejs() nodejs()
} }
@OptIn(ExperimentalWasmDsl::class) // @OptIn(ExperimentalWasmDsl::class)
wasmJs() { // wasmJs() {
browser() // browser()
nodejs() // nodejs()
} // }
// Suppress Beta warning for expect/actual classes across all targets // Suppress Beta warning for expect/actual classes across all targets
targets.configureEach { targets.configureEach {

View File

@ -623,9 +623,16 @@ open class Scope(
suspend fun resolve(rec: ObjRecord, name: String): Obj { suspend fun resolve(rec: ObjRecord, name: String): Obj {
if (rec.type == ObjRecord.Type.Delegated) { 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 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 // If getValue not found, return a wrapper that calls invoke
object : Statement() { object : Statement() {
override val pos: Pos = Pos.builtIn override val pos: Pos = Pos.builtIn
@ -636,13 +643,21 @@ open class Scope(
} }
} }
})!! })!!
rec.value = res
return res
} }
return rec.value return rec.value
} }
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) { suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
if (rec.type == ObjRecord.Type.Delegated) { 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 val th = if (thisObj === ObjVoid) ObjNull else thisObj
del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue)) del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue))
return return

View File

@ -137,11 +137,20 @@ open class Obj {
// methods that to override // methods that to override
open suspend fun compareTo(scope: Scope, other: Obj): Int { open suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other === this) return 0 if (other === this) return 0
if( other === ObjNull ) return 2 if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
scope.raiseNotImplemented() 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 { open suspend fun contains(scope: Scope, other: Obj): Boolean {
return invokeInstanceMethod(scope, "contains", other).toBool() 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) { if (obj.type == ObjRecord.Type.Delegated) {
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate") val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
return obj.copy( return obj.copy(

View File

@ -235,7 +235,7 @@ open class ObjClass(
// 1) members-defined methods // 1) members-defined methods
for ((k, v) in members) { for ((k, v) in members) {
if (v.value is Statement || v.type == ObjRecord.Type.Delegated) { 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 // 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 (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) {
// if not already present, copy reference for dispatch // if not already present, copy reference for dispatch
if (!instance.instanceScope.objects.containsKey(k)) { 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 addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) { 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 -> getInstanceMemberOrNull(name)?.let { rec ->
val decl = rec.declaringClass ?: findDeclaringClassOf(name) ?: this val decl = rec.declaringClass ?: findDeclaringClassOf(name) ?: this
if (rec.type == ObjRecord.Type.Delegated) { 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() 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) 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)") del = del ?: obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
return obj.copy( val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))), obj.value = res
type = ObjRecord.Type.Other return obj
)
} }
return super.resolveRecord(scope, obj, name, decl) return super.resolveRecord(scope, obj, name, decl)
} }
@ -197,11 +196,15 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (rec.type == ObjRecord.Type.Delegated) { if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${cls.className}::$name" val storageName = "${cls.className}::$name"
val del = instanceScope[storageName]?.delegate ?: rec.delegate 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() 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 decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller))
@ -217,6 +220,9 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
args, args,
decl 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.OR -> a.logicalOr(scope, b)
BinOp.AND -> a.logicalAnd(scope, b) BinOp.AND -> a.logicalAnd(scope, b)
BinOp.EQARROW -> ObjMapEntry(a, b) BinOp.EQARROW -> ObjMapEntry(a, b)
BinOp.EQ -> ObjBool(a.compareTo(scope, b) == 0) BinOp.EQ -> ObjBool(a.equals(scope, b))
BinOp.NEQ -> ObjBool(a.compareTo(scope, b) != 0) BinOp.NEQ -> ObjBool(!a.equals(scope, b))
BinOp.REF_EQ -> ObjBool(a === b) BinOp.REF_EQ -> ObjBool(a === b)
BinOp.REF_NEQ -> ObjBool(a !== b) BinOp.REF_NEQ -> ObjBool(a !== b)
BinOp.MATCH -> a.operatorMatch(scope, b) BinOp.MATCH -> a.operatorMatch(scope, b)
@ -611,7 +611,7 @@ class FieldRef(
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fieldPic = PerfFlags.FIELD_PIC val fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = target.get(scope).value val base = target.evalValue(scope)
if (base == ObjNull && isOptional) { if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment // no-op on null receiver for optional chaining assignment
return return
@ -714,10 +714,9 @@ class FieldRef(
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
// Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths // 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 fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS 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 (base == ObjNull && isOptional) return ObjNull
if (fieldPic) { if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
@ -1583,7 +1582,7 @@ class FastLocalVarRef(
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot) val rec = actualOwner.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { 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 // Try per-frame local binding maps in the ancestry first

View File

@ -210,4 +210,81 @@ class DelegationTest {
assert(l is Delegate) 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 --> <!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white"> <div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em"> <span style="margin-left: -5em">
v1.1.0-SNAPSHOT v1.1.1-SNAPSHOT
</span> </span>
</div> </div>
<!-- Fixed top navbar for the whole site --> <!-- Fixed top navbar for the whole site -->