1.1.1-SNPASHOT some serious bugs in initilazation fixed. They were revealed by delegation real world usage
This commit is contained in:
parent
20f777f9f6
commit
f792c73b8f
4
archived/README.md
Normal file
4
archived/README.md
Normal 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.__
|
||||
117
archived/proposals/delegates.lyng
Normal file
117
archived/proposals/delegates.lyng
Normal 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 } }
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user