diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 77cf5f0..4cf3b7e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -516,6 +516,23 @@ open class Scope( open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure) + /** + * Resolve and evaluate a qualified identifier exactly as compiled code would. + * For input like `A.B.C`, it builds the same ObjRef chain the compiler emits: + * `LocalVarRef("A", Pos.builtIn)` followed by `FieldRef` for each segment, then evaluates it. + * This mirrors `eval("A.B.C")` resolution semantics without invoking the compiler. + */ + suspend fun resolveQualifiedIdentifier(qualifiedName: String): Obj { + val trimmed = qualifiedName.trim() + if (trimmed.isEmpty()) raiseSymbolNotFound("empty identifier") + val parts = trimmed.split('.') + var ref: ObjRef = LocalVarRef(parts[0], Pos.builtIn) + for (i in 1 until parts.size) { + ref = FieldRef(ref, parts[i], false) + } + return ref.evalValue(this) + } + companion object { fun new(): Scope = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt index 72ea62b..0be066e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt @@ -17,9 +17,10 @@ package net.sergeych.lynon -import net.sergeych.lyng.Pos import net.sergeych.lyng.Scope -import net.sergeych.lyng.obj.* +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjString open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) { @@ -82,28 +83,15 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") it } ?: run { - // Precisely mimic what `scope.eval(className.value)` would compile and execute for - // a simple qualified name: LocalVarRef(head) followed by chained FieldRef segments. - val evaluated = evaluateQualifiedNameAsCompiled(scope, className.value) + // Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers + val evaluated = scope.resolveQualifiedIdentifier(className.value) if (evaluated !is ObjClass) scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}") evaluated } } - // Build and execute the exact ObjRef chain that the compiler would emit for an identifier - // with optional dotted selectors, e.g., "A.B.C" -> LocalVarRef("A") -> FieldRef(..., "B") -> FieldRef(..., "C") - private suspend fun evaluateQualifiedNameAsCompiled(scope: Scope, name: String): Obj { - val trimmed = name.trim() - if (trimmed.isEmpty()) scope.raiseSymbolNotFound("empty identifier") - val parts = trimmed.split('.') - // Use built-in position; eval() would carry a Source position, which does not affect resolution semantics here - var ref: ObjRef = LocalVarRef(parts[0], Pos.builtIn) - for (i in 1 until parts.size) { - ref = FieldRef(ref, parts[i], false) - } - return ref.evalValue(scope) - } + // helper moved to Scope as resolveQualifiedIdentifier suspend fun decodeAnyList(scope: Scope, fixedSize: Int? = null): MutableList { return if (bin.getBit() == 1) {