diff --git a/archived/README.md b/archived/README.md new file mode 100644 index 0000000..d96b15a --- /dev/null +++ b/archived/README.md @@ -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.__ diff --git a/docs/development/String.md b/archived/development/String.md similarity index 100% rename from docs/development/String.md rename to archived/development/String.md diff --git a/docs/development/including_modules.md b/archived/development/including_modules.md similarity index 100% rename from docs/development/including_modules.md rename to archived/development/including_modules.md diff --git a/docs/development/scope_resolution.md b/archived/development/scope_resolution.md similarity index 100% rename from docs/development/scope_resolution.md rename to archived/development/scope_resolution.md diff --git a/archived/proposals/delegates.lyng b/archived/proposals/delegates.lyng new file mode 100644 index 0000000..dd06b52 --- /dev/null +++ b/archived/proposals/delegates.lyng @@ -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 } } + + diff --git a/docs/proposals/map_literal.md b/archived/proposals/map_literal.md similarity index 100% rename from docs/proposals/map_literal.md rename to archived/proposals/map_literal.md diff --git a/docs/proposals/named_args.md b/archived/proposals/named_args.md similarity index 100% rename from docs/proposals/named_args.md rename to archived/proposals/named_args.md diff --git a/docs/tutorial.md b/docs/tutorial.md index 67511a2..0edfe26 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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 diff --git a/lyngio/build.gradle.kts b/lyngio/build.gradle.kts index 5732e25..4b6890c 100644 --- a/lyngio/build.gradle.kts +++ b/lyngio/build.gradle.kts @@ -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) +// } +// } } } diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 346e94c..dffe95f 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 0b4bbfa..337070d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 7c1170a..a47b729 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 7405f3f..13f362a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -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) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index e1d9439..6404265 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -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) } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 86ab4e0..4f0e2b8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -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 diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt index 1cca6b7..90b5a57 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt @@ -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()) + } + } diff --git a/site/src/jsMain/resources/index.html b/site/src/jsMain/resources/index.html index edc5d89..601c077 100644 --- a/site/src/jsMain/resources/index.html +++ b/site/src/jsMain/resources/index.html @@ -329,7 +329,7 @@
- v1.1.0-SNAPSHOT + v1.1.1-SNAPSHOT