diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 91a3148..d564e5e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -529,10 +529,7 @@ class Compiler( return LocalVarRef(name, pos) } resolutionSink?.reference(name, pos) - seedScope?.chainLookupIgnoreClosure(name)?.let { - return LocalVarRef(name, pos) - } - if (allowUnresolvedRefs || (name.isNotEmpty() && name[0].isUpperCase())) { + if (allowUnresolvedRefs) { return LocalVarRef(name, pos) } throw ScriptError(pos, "unresolved name: $name") @@ -556,7 +553,7 @@ class Compiler( val miniAstSink: MiniAstSink? = null, val resolutionSink: ResolutionSink? = null, val useBytecodeStatements: Boolean = true, - val strictSlotRefs: Boolean = false, + val strictSlotRefs: Boolean = true, val allowUnresolvedRefs: Boolean = false, val seedScope: Scope? = null, ) @@ -5485,7 +5482,7 @@ class Compiler( miniSink: MiniAstSink? = null, resolutionSink: ResolutionSink? = null, useBytecodeStatements: Boolean = true, - strictSlotRefs: Boolean = false, + strictSlotRefs: Boolean = true, allowUnresolvedRefs: Boolean = false, seedScope: Scope? = null ): Script { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 26003d9..a1bc191 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -817,11 +817,25 @@ open class Scope( 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) + val first = parts[0] + val ref: ObjRef = if (first == "this") { + ConstRef(thisObj.asReadonly) + } else { + var s: Scope? = this + var slot: Int? = null + var guard = 0 + while (s != null && guard++ < 1024 && slot == null) { + slot = s.getSlotIndexOf(first) + s = s.parent + } + if (slot == null) raiseSymbolNotFound(first) + LocalSlotRef(first, slot, 0, isMutable = false, isDelegated = false, Pos.builtIn, strict = true) } - return ref.evalValue(this) + var ref0: ObjRef = ref + for (i in 1 until parts.size) { + ref0 = FieldRef(ref0, parts[i], false) + } + return ref0.evalValue(this) } suspend fun resolve(rec: ObjRecord, name: String): Obj { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 490e44a..588a067 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -2483,6 +2483,7 @@ class BytecodeCompiler( } private fun compileWhen(stmt: WhenStatement, wantResult: Boolean): CompiledValue? { + val subjectRef = extractFlowTypeSubject(stmt.value) val subjectValue = compileStatementValueOrFallback(stmt.value) ?: return null val subjectObj = ensureObjSlot(subjectValue) val resultSlot = allocSlot() @@ -2504,11 +2505,14 @@ class BytecodeCompiler( } builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(nextCaseLabel))) builder.mark(caseLabel) + val caseOverride = flowTypeOverrideForWhenCase(subjectRef, case.conditions) + val caseRestore = applyFlowTypeOverride(caseOverride) val bodyValue = compileStatementValueOrFallback(case.block, wantResult) ?: return null if (wantResult) { val bodyObj = ensureObjSlot(bodyValue) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) } + restoreFlowTypeOverride(caseRestore) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(nextCaseLabel) } @@ -3952,11 +3956,15 @@ class BytecodeCompiler( Opcode.JMP_IF_FALSE, listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) ) + val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true)) compileStatementValueOrFallback(stmt.ifBody, false) ?: return null + restoreFlowTypeOverride(thenRestore) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(elseLabel) stmt.elseBody?.let { + val elseRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = false)) compileStatementValueOrFallback(it, false) ?: return null + restoreFlowTypeOverride(elseRestore) } builder.mark(endLabel) return condition @@ -3985,13 +3993,17 @@ class BytecodeCompiler( Opcode.JMP_IF_FALSE, listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) ) + val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true)) val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null + restoreFlowTypeOverride(thenRestore) val thenObj = ensureObjSlot(thenValue) builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(elseLabel) if (stmt.elseBody != null) { + val elseRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = false)) val elseValue = compileStatementValueOrFallback(stmt.elseBody) ?: return null + restoreFlowTypeOverride(elseRestore) val elseObj = ensureObjSlot(elseValue) builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) } else { @@ -4016,6 +4028,91 @@ class BytecodeCompiler( } } + private data class FlowTypeSubject(val name: String, val slot: Int?) + private data class FlowTypeInfo(val name: String, val slot: Int?, val cls: ObjClass) + private data class FlowTypeRestore( + val name: String, + val prevNameClass: ObjClass?, + val slot: Int?, + val prevSlotClass: ObjClass?, + ) + + private fun extractFlowTypeSubject(stmt: Statement): FlowTypeSubject? { + val target = if (stmt is BytecodeStatement) stmt.original else stmt + val expr = target as? ExpressionStatement ?: return null + return flowTypeSubjectFromRef(expr.ref) + } + + private fun flowTypeSubjectFromRef(ref: ObjRef): FlowTypeSubject? { + return when (ref) { + is LocalSlotRef -> FlowTypeSubject(ref.name, resolveSlot(ref)) + is LocalVarRef -> FlowTypeSubject(ref.name, resolveDirectNameSlot(ref.name)?.slot) + else -> null + } + } + + private fun flowTypeOverrideForIf(condition: Statement, applyForThen: Boolean): FlowTypeInfo? { + val target = if (condition is BytecodeStatement) condition.original else condition + val expr = target as? ExpressionStatement ?: return null + val ref = expr.ref as? BinaryOpRef ?: return null + val op = binaryOp(ref) + val apply = when (op) { + BinOp.IS -> applyForThen + BinOp.NOTIS -> !applyForThen + else -> false + } + if (!apply) return null + val cls = resolveTypeRefClass(binaryRight(ref)) ?: return null + return flowTypeInfoForRef(binaryLeft(ref), cls) + } + + private fun flowTypeOverrideForWhenCase( + subject: FlowTypeSubject?, + conditions: List + ): FlowTypeInfo? { + if (subject == null || conditions.size != 1) return null + val cond = conditions.first() as? WhenIsCondition ?: return null + if (cond.negated) return null + val expr = cond.expr as? ExpressionStatement ?: return null + val cls = resolveTypeRefClass(expr.ref) ?: return null + return FlowTypeInfo(subject.name, subject.slot, cls) + } + + private fun flowTypeInfoForRef(ref: ObjRef, cls: ObjClass): FlowTypeInfo? { + return when (ref) { + is LocalSlotRef -> FlowTypeInfo(ref.name, resolveSlot(ref), cls) + is LocalVarRef -> FlowTypeInfo(ref.name, resolveDirectNameSlot(ref.name)?.slot, cls) + else -> null + } + } + + private fun applyFlowTypeOverride(info: FlowTypeInfo?): FlowTypeRestore? { + if (info == null) return null + val prevNameClass = nameObjClass[info.name] + nameObjClass[info.name] = info.cls + val prevSlotClass = info.slot?.let { slotObjClass[it] } + if (info.slot != null) { + slotObjClass[info.slot] = info.cls + } + return FlowTypeRestore(info.name, prevNameClass, info.slot, prevSlotClass) + } + + private fun restoreFlowTypeOverride(restore: FlowTypeRestore?) { + if (restore == null) return + if (restore.prevNameClass == null) { + nameObjClass.remove(restore.name) + } else { + nameObjClass[restore.name] = restore.prevNameClass + } + if (restore.slot != null) { + if (restore.prevSlotClass == null) { + slotObjClass.remove(restore.slot) + } else { + slotObjClass[restore.slot] = restore.prevSlotClass + } + } + } + private fun findLoopContextIndex(label: String?): Int? { if (loopStack.isEmpty()) return null val stack = loopStack.toList() 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 7e5817b..c57cd83 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -1832,98 +1832,47 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { scope.pos = atPos - if (!PerfFlags.LOCAL_SLOT_PIC) { - scope.getSlotIndexOf(name)?.let { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++ - return scope.getSlotRecord(it) - } - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++ - // 2) Fallback to current-scope object or field on `this` - scope[name]?.let { return it } - try { - return scope.thisObj.readField(scope, name) - } catch (e: ExecutionError) { - // Map missing symbol during unqualified lookup to SymbolNotFound (SymbolNotDefinedException) - // to preserve legacy behavior expected by tests. - if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name) - throw e - } - } + if (name == "this") return scope.thisObj.asReadonly val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) val slot = if (hit) cachedSlot else resolveSlot(scope) if (slot >= 0) { val rec = scope.getSlotRecord(slot) - if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { - // Not visible via slot, fallback to other lookups - } else { - if (PerfFlags.PIC_DEBUG_COUNTERS) { - if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++ - } - return rec + if (rec.declaringClass != null && + !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name) + ) { + scope.raiseError(ObjIllegalAccessException(scope, "private field access")) } + if (PerfFlags.PIC_DEBUG_COUNTERS) { + if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++ + } + return rec } - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++ - // 2) Fallback name in scope or field on `this` - scope[name]?.let { return it } - try { - return scope.thisObj.readField(scope, name) - } catch (e: ExecutionError) { - if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name) - throw e - } + scope.raiseSymbolNotFound(name) } override suspend fun evalValue(scope: Scope): Obj { scope.pos = atPos + if (name == "this") return scope.thisObj scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) } - // fallback to current-scope object or field on `this` - scope[name]?.let { return scope.resolve(it, name) } - return try { - scope.thisObj.readField(scope, name).value - } catch (e: ExecutionError) { - if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name) - throw e - } + scope.raiseSymbolNotFound(name) } override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { scope.pos = atPos - if (!PerfFlags.LOCAL_SLOT_PIC) { - scope.getSlotIndexOf(name)?.let { - val rec = scope.getSlotRecord(it) - scope.assign(rec, name, newValue) - return - } - scope.chainLookupIgnoreClosure(name, followClosure = true, caller = scope.currentClassCtx)?.let { rec -> - scope.assign(rec, name, newValue) - return - } - scope[name]?.let { stored -> - scope.assign(stored, name, newValue) - return - } - // Fallback: write to field on `this` - scope.thisObj.writeField(scope, name, newValue) - return - } - val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope) + if (name == "this") scope.raiseError("can't assign to this") + val slot = + if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot + else resolveSlot(scope) if (slot >= 0) { val rec = scope.getSlotRecord(slot) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { + if (rec.declaringClass == null || + canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name) + ) { scope.assign(rec, name, newValue) return } } - scope.chainLookupIgnoreClosure(name, followClosure = true, caller = scope.currentClassCtx)?.let { rec -> - scope.assign(rec, name, newValue) - return - } - scope[name]?.let { stored -> - scope.assign(stored, name, newValue) - return - } - scope.thisObj.writeField(scope, name, newValue) - return + scope.raiseSymbolNotFound(name) } } @@ -2029,34 +1978,7 @@ class FastLocalVarRef( return rec } } - // Try per-frame local binding maps in the ancestry first (locals declared in frames) - run { - var s: Scope? = scope - var guard = 0 - while (s != null) { - s.localBindings[name]?.let { return it } - val next = s.parent - if (next === s) break - s = next - if (++guard > 4096) break - } - } - // Try to find a direct local binding in the current ancestry (without invoking name resolution that may prefer fields) - run { - var s: Scope? = scope - var guard = 0 - while (s != null) { - s.objects[name]?.let { return it } - val next = s.parent - if (next === s) break - s = next - if (++guard > 4096) break - } - } - // Fallback to standard name lookup (locals or closure chain) if the slot owner changed across suspension - scope[name]?.let { return it } - // As a last resort, treat as field on `this` - return scope.thisObj.readField(scope, name) + scope.raiseSymbolNotFound(name) } override suspend fun evalValue(scope: Scope): Obj { @@ -2070,39 +1992,7 @@ class FastLocalVarRef( return scope.resolve(rec, name) } } - // Try per-frame local binding maps in the ancestry first - run { - var s: Scope? = scope - var guard = 0 - while (s != null) { - s.localBindings[name]?.let { - return s.resolve(it, name) - } - val next = s.parent - if (next === s) break - s = next - if (++guard > 4096) break - } - } - // Try to find a direct local binding in the current ancestry first - run { - var s: Scope? = scope - var guard = 0 - while (s != null) { - s.objects[name]?.let { - return s.resolve(it, name) - } - val next = s.parent - if (next === s) break - s = next - if (++guard > 4096) break - } - } - // Fallback to standard name lookup (locals or closure chain) - scope[name]?.let { - return scope.resolve(it, name) - } - return scope.thisObj.readField(scope, name).value + scope.raiseSymbolNotFound(name) } override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { @@ -2117,29 +2007,7 @@ class FastLocalVarRef( return } } - // Try per-frame local binding maps in the ancestry first - run { - var s: Scope? = scope - var guard = 0 - while (s != null) { - val rec = s.localBindings[name] - if (rec != null) { - s.assign(rec, name, newValue) - return - } - val next = s.parent - if (next === s) break - s = next - if (++guard > 4096) break - } - } - // Fallback to standard name lookup - scope[name]?.let { stored -> - scope.assign(stored, name, newValue) - return - } - scope.thisObj.writeField(scope, name, newValue) - return + scope.raiseSymbolNotFound(name) } } @@ -2287,18 +2155,22 @@ class LocalSlotRef( override fun forEachVariable(block: (String) -> Unit) { block(name) } - - private val fallbackRef = LocalVarRef(name, atPos) - private fun resolveOwner(scope: Scope): Scope { - return scope + private fun resolveOwner(scope: Scope): Scope? { + var s: Scope? = scope + var guard = 0 + while (s != null && guard++ < 1024) { + val idx = s.getSlotIndexOf(name) + if (idx != null && idx == slot) return s + s = s.parent + } + return null } override suspend fun get(scope: Scope): ObjRecord { scope.pos = atPos - val owner = resolveOwner(scope) + val owner = resolveOwner(scope) ?: scope.raiseError("slot owner not found for $name") if (slot < 0 || slot >= owner.slotCount()) { - if (strict) scope.raiseError("slot index out of range for $name") - return fallbackRef.get(scope) + scope.raiseError("slot index out of range for $name") } val rec = owner.getSlotRecord(slot) if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { @@ -2309,10 +2181,9 @@ class LocalSlotRef( override suspend fun evalValue(scope: Scope): Obj { scope.pos = atPos - val owner = resolveOwner(scope) + val owner = resolveOwner(scope) ?: scope.raiseError("slot owner not found for $name") if (slot < 0 || slot >= owner.slotCount()) { - if (strict) scope.raiseError("slot index out of range for $name") - return fallbackRef.evalValue(scope) + scope.raiseError("slot index out of range for $name") } val rec = owner.getSlotRecord(slot) if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { @@ -2323,11 +2194,9 @@ class LocalSlotRef( override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { scope.pos = atPos - val owner = resolveOwner(scope) + val owner = resolveOwner(scope) ?: scope.raiseError("slot owner not found for $name") if (slot < 0 || slot >= owner.slotCount()) { - if (strict) scope.raiseError("slot index out of range for $name") - fallbackRef.setAt(pos, scope, newValue) - return + scope.raiseError("slot index out of range for $name") } val rec = owner.getSlotRecord(slot) if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {