Scope.copy renamed to createChild for clarity; fixed error with Exception self serializing.

This commit is contained in:
Sergey Chernov 2025-10-31 14:55:23 +04:00
parent c854b06683
commit 7e289382ed
13 changed files with 101 additions and 64 deletions

View File

@ -21,7 +21,7 @@ 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 = "0.9.2-SNAPSHOT" version = "0.9.3-SNAPSHOT"
buildscript { buildscript {
repositories { repositories {

View File

@ -796,7 +796,7 @@ class Compiler(
val v = left.getter(context) val v = left.getter(context)
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
v.value.callOn( v.value.callOn(
context.copy( context.createChildScope(
context.pos, context.pos,
args.toArguments(context, detectedBlockArgument) args.toArguments(context, detectedBlockArgument)
// Arguments( // Arguments(
@ -902,7 +902,7 @@ class Compiler(
val args = extras?.let { required + it } ?: required val args = extras?.let { required + it } ?: required
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}") val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}") if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
(fn.execute(scope.copy(Arguments(args))) as? Statement) (fn.execute(scope.createChildScope(Arguments(args))) as? Statement)
?: scope.raiseClassCastError("function annotation must return callable") ?: scope.raiseClassCastError("function annotation must return callable")
} }
} }
@ -1197,7 +1197,7 @@ class Compiler(
} }
} }
if (exceptionObject != null) { if (exceptionObject != null) {
val catchContext = this.copy(pos = cdata.catchVar.pos) val catchContext = this.createChildScope(pos = cdata.catchVar.pos)
catchContext.addItem(cdata.catchVar.value, false, objException) catchContext.addItem(cdata.catchVar.value, false, objException)
result = cdata.block.execute(catchContext) result = cdata.block.execute(catchContext)
isCaught = true isCaught = true
@ -1313,7 +1313,7 @@ class Compiler(
// accessors, constructor registration, etc. // accessors, constructor registration, etc.
addItem(className, false, newClass) addItem(className, false, newClass)
if (initScope.isNotEmpty()) { if (initScope.isNotEmpty()) {
val classScope = copy(newThisObj = newClass) val classScope = createChildScope(newThisObj = newClass)
newClass.classScope = classScope newClass.classScope = classScope
for (s in initScope) for (s in initScope)
s.execute(classScope) s.execute(classScope)
@ -1367,7 +1367,7 @@ class Compiler(
return statement(body.pos) { cxt -> return statement(body.pos) { cxt ->
val forContext = cxt.copy(start) val forContext = cxt.createChildScope(start)
// loop var: StoredObject // loop var: StoredObject
val loopSO = forContext.addItem(tVar.value, true, ObjNull) val loopSO = forContext.addItem(tVar.value, true, ObjNull)
@ -1536,7 +1536,7 @@ class Compiler(
var result: Obj = ObjVoid var result: Obj = ObjVoid
lateinit var doScope: Scope lateinit var doScope: Scope
do { do {
doScope = it.copy().apply { skipScopeCreation = true } doScope = it.createChildScope().apply { skipScopeCreation = true }
try { try {
result = body.execute(doScope) result = body.execute(doScope)
} catch (e: LoopBreakContinueException) { } catch (e: LoopBreakContinueException) {
@ -1816,7 +1816,7 @@ class Compiler(
val block = parseScript() val block = parseScript()
return statement(startPos) { return statement(startPos) {
// block run on inner context: // block run on inner context:
block.execute(if (it.skipScopeCreation) it else it.copy(startPos)) block.execute(if (it.skipScopeCreation) it else it.createChildScope(startPos))
}.also { }.also {
val t1 = cc.next() val t1 = cc.next()
if (t1.type != Token.Type.RBRACE) if (t1.type != Token.Type.RBRACE)

View File

@ -138,13 +138,27 @@ open class Scope(
) )
} }
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope = /**
* Creates a new child scope using the provided arguments and optional `thisObj`.
*/
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj) Scope(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope = /**
* Creates a new child scope using the provided arguments and optional `thisObj`.
* The child scope inherits the current scope's properties such as position and the existing `thisObj` if no new `thisObj` is provided.
*
* @param args The arguments to associate with the child scope. Defaults to [Arguments.EMPTY].
* @param newThisObj The new `thisObj` to associate with the child scope. Defaults to the current scope's `thisObj` if not provided.
* @return A new instance of [Scope] initialized with the specified arguments and `thisObj`.
*/
fun createChildScope(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj) Scope(this, args, pos, newThisObj ?: thisObj)
fun copy() = Scope(this, args, pos, thisObj) /**
* @return A child scope with the same arguments, position and [thisObj]
*/
fun createChildScope() = Scope(this, args, pos, thisObj)
fun addItem( fun addItem(
name: String, name: String,
@ -240,8 +254,6 @@ open class Scope(
p = p.parent p = p.parent
} }
println("--------------------") println("--------------------")
ObjVoid
} }
open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure) open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure)

View File

@ -17,15 +17,12 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
import net.sergeych.synctools.ProtectedOp
open class Obj { open class Obj {
@ -39,12 +36,12 @@ open class Obj {
var isFrozen: Boolean = false var isFrozen: Boolean = false
private val monitor = Mutex() // private val monitor = Mutex()
// private val memberMutex = Mutex() // private val memberMutex = Mutex()
internal var parentInstances: MutableList<Obj> = mutableListOf() // internal var parentInstances: MutableList<Obj> = mutableListOf()
private val opInstances = ProtectedOp() // private val opInstances = ProtectedOp()
open suspend fun inspect(scope: Scope): String = toString(scope).value open suspend fun inspect(scope: Scope): String = toString(scope).value
@ -100,7 +97,7 @@ open class Obj {
// note that getInstanceMember traverses the hierarchy // note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(scope.pos, name).value objClass.getInstanceMember(scope.pos, name).value
fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value // fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value
// methods that to override // methods that to override
@ -232,14 +229,14 @@ open class Obj {
if (isFrozen) scope.raiseError("attempt to mutate frozen object") if (isFrozen) scope.raiseError("attempt to mutate frozen object")
} }
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() } // suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
open suspend fun readField(scope: Scope, name: String): ObjRecord { open suspend fun readField(scope: Scope, name: String): ObjRecord {
// could be property or class field: // could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name") val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
return when (val value = obj.value) { return when (val value = obj.value) {
is Statement -> { is Statement -> {
ObjRecord(value.execute(scope.copy(scope.pos, newThisObj = this)), obj.isMutable) ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
} }
// could be writable property naturally // could be writable property naturally
// null -> ObjNull.asReadonly // null -> ObjNull.asReadonly
@ -268,11 +265,11 @@ open class Obj {
} }
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj = suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
callOn(scope.copy(scope.pos, args = args, newThisObj = thisObj)) callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj))
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj = suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn( callOn(
scope.copy( scope.createChildScope(
scope.pos, scope.pos,
args = Arguments(args.toList()), args = Arguments(args.toList()),
newThisObj = thisObj newThisObj = thisObj
@ -281,7 +278,7 @@ open class Obj {
suspend fun invoke(scope: Scope, thisObj: Obj): Obj = suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
callOn( callOn(
scope.copy( scope.createChildScope(
scope.pos, scope.pos,
args = Arguments.EMPTY, args = Arguments.EMPTY,
newThisObj = thisObj newThisObj = thisObj
@ -289,7 +286,7 @@ open class Obj {
) )
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj = suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(scope.copy(atPos, args = args, newThisObj = thisObj)) callOn(scope.createChildScope(atPos, args = args, newThisObj = thisObj))
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) } val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
@ -302,7 +299,7 @@ open class Obj {
} }
fun autoInstanceScope(parent: Scope): Scope { fun autoInstanceScope(parent: Scope): Scope {
val scope = parent.copy(newThisObj = this, args = parent.args) val scope = parent.createChildScope(newThisObj = this, args = parent.args)
for (m in objClass.members) { for (m in objClass.members) {
scope.objects[m.key] = m.value scope.objects[m.key] = m.value
} }
@ -335,7 +332,7 @@ open class Obj {
} }
// utilities // utilities
addFn("let") { addFn("let") {
args.firstAndOnly().callOn(copy(Arguments(thisObj))) args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
} }
addFn("apply") { addFn("apply") {
val body = args.firstAndOnly() val body = args.firstAndOnly()
@ -347,7 +344,7 @@ open class Obj {
thisObj thisObj
} }
addFn("also") { addFn("also") {
args.firstAndOnly().callOn(copy(Arguments(thisObj))) args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
thisObj thisObj
} }
addFn("run") { addFn("run") {

View File

@ -61,7 +61,7 @@ open class ObjClass(
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
val instance = ObjInstance(this) val instance = ObjInstance(this)
instance.instanceScope = scope.copy(newThisObj = instance, args = scope.args) instance.instanceScope = scope.createChildScope(newThisObj = instance, args = scope.args)
if (instanceConstructor != null) { if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceScope) instanceConstructor!!.execute(instance.instanceScope)
} }
@ -69,7 +69,7 @@ open class ObjClass(
} }
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj { suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
return callOn(scope.copy(Arguments(*plainArgs))) return callOn(scope.createChildScope(Arguments(*plainArgs)))
} }

View File

@ -53,7 +53,7 @@ class ObjDynamic : Obj() {
internal var writeCallback: Statement? = null internal var writeCallback: Statement? = null
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
return readCallback?.execute(scope.copy(Arguments(ObjString(name))))?.let { return readCallback?.execute(scope.createChildScope(Arguments(ObjString(name))))?.let {
if (writeCallback != null) if (writeCallback != null)
it.asMutable it.asMutable
else else
@ -63,17 +63,17 @@ class ObjDynamic : Obj() {
} }
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) { override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
writeCallback?.execute(scope.copy(Arguments(ObjString(name), newValue))) writeCallback?.execute(scope.createChildScope(Arguments(ObjString(name), newValue)))
?: super.writeField(scope, name, newValue) ?: super.writeField(scope, name, newValue)
} }
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
return readCallback?.execute( scope.copy(Arguments(index))) return readCallback?.execute( scope.createChildScope(Arguments(index)))
?: super.getAt(scope, index) ?: super.getAt(scope, index)
} }
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
writeCallback?.execute(scope.copy(Arguments(index, newValue))) writeCallback?.execute(scope.createChildScope(Arguments(index, newValue)))
?: super.putAt(scope, index, newValue) ?: super.putAt(scope, index, newValue)
} }
@ -82,7 +82,7 @@ class ObjDynamic : Obj() {
suspend fun create(scope: Scope, builder: Statement): ObjDynamic { suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
val delegate = ObjDynamic() val delegate = ObjDynamic()
val context = ObjDynamicContext(delegate) val context = ObjDynamicContext(delegate)
builder.execute(scope.copy(newThisObj = context)) builder.execute(scope.createChildScope(newThisObj = context))
return delegate return delegate
} }

View File

@ -28,9 +28,9 @@ import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
/** /**
* note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated * Note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
* from the current scope which is treated as exception scope. It is used to restore serialized * from the current scope, which is treated as an exception scope. It is used to restore a serialized
* exception with stack trace; the scope of the de-serialized exception is not valid * exception with stack trace; the scope of the deserialized exception is not valid
* for stack unwinding. * for stack unwinding.
*/ */
open class ObjException( open class ObjException(
@ -127,7 +127,7 @@ open class ObjException(
} }
} }
val Root = ExceptionClass("Throwable").apply { val Root = ExceptionClass("Exception").apply {
addConst("message", statement { addConst("message", statement {
(thisObj as ObjException).message.toObj() (thisObj as ObjException).message.toObj()
}) })

View File

@ -58,7 +58,7 @@ class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChannel<Obj> { private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChannel<Obj> {
val channel = Channel<Obj>(Channel.RENDEZVOUS) val channel = Channel<Obj>(Channel.RENDEZVOUS)
val builder = ObjFlowBuilder(channel) val builder = ObjFlowBuilder(channel)
val builderScope = scope.copy(newThisObj = builder) val builderScope = scope.createChildScope(newThisObj = builder)
globalLaunch { globalLaunch {
try { try {
producer.execute(builderScope) producer.execute(builderScope)

View File

@ -32,7 +32,7 @@ class ObjInstanceClass(val name: String) : ObjClass(name) {
val actualSize = constructorMeta?.params?.size ?: 0 val actualSize = constructorMeta?.params?.size ?: 0
if (args.size > actualSize) if (args.size > actualSize)
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}") scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
val newScope = scope.copy(args = Arguments(args)) val newScope = scope.createChildScope(args = Arguments(args))
return (callOn(newScope) as ObjInstance).apply { return (callOn(newScope) as ObjInstance).apply {
deserializeStateVars(scope, decoder) deserializeStateVars(scope, decoder)
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid } invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }

View File

@ -93,7 +93,7 @@ val ObjIterable by lazy {
val fn = requiredArg<Statement>(0) val fn = requiredArg<Statement>(0)
while (it.invokeInstanceMethod(this, "hasNext").toBool()) { while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
val x = it.invokeInstanceMethod(this, "next") val x = it.invokeInstanceMethod(this, "next")
fn.execute(this.copy(Arguments(listOf(x)))) fn.execute(this.createChildScope(Arguments(listOf(x))))
} }
ObjVoid ObjVoid
} }

View File

@ -71,7 +71,7 @@ abstract class ImportProvider(
newModuleAt(pos).also { newModuleAt(pos).also {
it.eval("import lyng.stdlib\n") it.eval("import lyng.stdlib\n")
} }
}.copy() }.createChildScope()
} }

View File

@ -60,7 +60,7 @@ abstract class Statement(
val type = ObjClass("Callable") val type = ObjClass("Callable")
} }
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.copy(args = Arguments(*args))) suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.createChildScope(args = Arguments(*args)))
} }

View File

@ -3170,6 +3170,34 @@ class ScriptTest {
) )
} }
@Test
fun testExceptionSerializationPlain() = runTest {
eval(
"""
import lyng.serialization
val x = [1,2,3]
assertEquals( "[1,2,3]", x.toString() )
try {
throw "test"
}
catch (e) {
println(e.stackTrace)
e.printStackTrace()
val coded = Lynon.encode(e)
val decoded = Lynon.decode(coded)
assertEquals( e::class, decoded::class )
assertEquals( e.stackTrace, decoded.stackTrace )
assertEquals( e.message, decoded.message )
println("-------------------- e")
println(e.toString())
println("-------------------- dee")
println(decoded.toString())
assertEquals( e.toString(), decoded.toString() )
}
""".trimIndent()
)
}
@Test @Test
fun testThisInClosure() = runTest { fun testThisInClosure() = runTest {
eval( eval(
@ -3287,25 +3315,25 @@ class ScriptTest {
// @Test // @Test
fun testMinimumOptimization() = runTest { // fun testMinimumOptimization() = runTest {
val x = Scope().eval( // val x = Scope().eval(
""" // """
fun naiveCountHappyNumbers() { // fun naiveCountHappyNumbers() {
var count = 0 // var count = 0
for( n1 in 0..9 ) // for( n1 in 0..9 )
for( n2 in 0..9 ) // for( n2 in 0..9 )
for( n3 in 0..9 ) // for( n3 in 0..9 )
for( n4 in 0..9 ) // for( n4 in 0..9 )
for( n5 in 0..9 ) // for( n5 in 0..9 )
for( n6 in 0..9 ) // for( n6 in 0..9 )
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++ // if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
count // count
} // }
naiveCountHappyNumbers() // naiveCountHappyNumbers()
""".trimIndent() // """.trimIndent()
).toInt() // ).toInt()
assertEquals(55252, x) // assertEquals(55252, x)
} // }
@Test @Test
fun testRegex1() = runTest { fun testRegex1() = runTest {