Tighten compile-time slot resolution

This commit is contained in:
Sergey Chernov 2026-02-03 02:29:58 +03:00
parent 523b9d338b
commit 824a58bbc5
4 changed files with 156 additions and 179 deletions

View File

@ -529,10 +529,7 @@ class Compiler(
return LocalVarRef(name, pos) return LocalVarRef(name, pos)
} }
resolutionSink?.reference(name, pos) resolutionSink?.reference(name, pos)
seedScope?.chainLookupIgnoreClosure(name)?.let { if (allowUnresolvedRefs) {
return LocalVarRef(name, pos)
}
if (allowUnresolvedRefs || (name.isNotEmpty() && name[0].isUpperCase())) {
return LocalVarRef(name, pos) return LocalVarRef(name, pos)
} }
throw ScriptError(pos, "unresolved name: $name") throw ScriptError(pos, "unresolved name: $name")
@ -556,7 +553,7 @@ class Compiler(
val miniAstSink: MiniAstSink? = null, val miniAstSink: MiniAstSink? = null,
val resolutionSink: ResolutionSink? = null, val resolutionSink: ResolutionSink? = null,
val useBytecodeStatements: Boolean = true, val useBytecodeStatements: Boolean = true,
val strictSlotRefs: Boolean = false, val strictSlotRefs: Boolean = true,
val allowUnresolvedRefs: Boolean = false, val allowUnresolvedRefs: Boolean = false,
val seedScope: Scope? = null, val seedScope: Scope? = null,
) )
@ -5485,7 +5482,7 @@ class Compiler(
miniSink: MiniAstSink? = null, miniSink: MiniAstSink? = null,
resolutionSink: ResolutionSink? = null, resolutionSink: ResolutionSink? = null,
useBytecodeStatements: Boolean = true, useBytecodeStatements: Boolean = true,
strictSlotRefs: Boolean = false, strictSlotRefs: Boolean = true,
allowUnresolvedRefs: Boolean = false, allowUnresolvedRefs: Boolean = false,
seedScope: Scope? = null seedScope: Scope? = null
): Script { ): Script {

View File

@ -817,11 +817,25 @@ open class Scope(
val trimmed = qualifiedName.trim() val trimmed = qualifiedName.trim()
if (trimmed.isEmpty()) raiseSymbolNotFound("empty identifier") if (trimmed.isEmpty()) raiseSymbolNotFound("empty identifier")
val parts = trimmed.split('.') val parts = trimmed.split('.')
var ref: ObjRef = LocalVarRef(parts[0], Pos.builtIn) val first = parts[0]
for (i in 1 until parts.size) { val ref: ObjRef = if (first == "this") {
ref = FieldRef(ref, parts[i], false) 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
} }
return ref.evalValue(this) if (slot == null) raiseSymbolNotFound(first)
LocalSlotRef(first, slot, 0, isMutable = false, isDelegated = false, Pos.builtIn, strict = true)
}
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 { suspend fun resolve(rec: ObjRecord, name: String): Obj {

View File

@ -2483,6 +2483,7 @@ class BytecodeCompiler(
} }
private fun compileWhen(stmt: WhenStatement, wantResult: Boolean): CompiledValue? { private fun compileWhen(stmt: WhenStatement, wantResult: Boolean): CompiledValue? {
val subjectRef = extractFlowTypeSubject(stmt.value)
val subjectValue = compileStatementValueOrFallback(stmt.value) ?: return null val subjectValue = compileStatementValueOrFallback(stmt.value) ?: return null
val subjectObj = ensureObjSlot(subjectValue) val subjectObj = ensureObjSlot(subjectValue)
val resultSlot = allocSlot() val resultSlot = allocSlot()
@ -2504,11 +2505,14 @@ class BytecodeCompiler(
} }
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(nextCaseLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(nextCaseLabel)))
builder.mark(caseLabel) builder.mark(caseLabel)
val caseOverride = flowTypeOverrideForWhenCase(subjectRef, case.conditions)
val caseRestore = applyFlowTypeOverride(caseOverride)
val bodyValue = compileStatementValueOrFallback(case.block, wantResult) ?: return null val bodyValue = compileStatementValueOrFallback(case.block, wantResult) ?: return null
if (wantResult) { if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue) val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
} }
restoreFlowTypeOverride(caseRestore)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nextCaseLabel) builder.mark(nextCaseLabel)
} }
@ -3952,11 +3956,15 @@ class BytecodeCompiler(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel))
) )
val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true))
compileStatementValueOrFallback(stmt.ifBody, false) ?: return null compileStatementValueOrFallback(stmt.ifBody, false) ?: return null
restoreFlowTypeOverride(thenRestore)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(elseLabel) builder.mark(elseLabel)
stmt.elseBody?.let { stmt.elseBody?.let {
val elseRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = false))
compileStatementValueOrFallback(it, false) ?: return null compileStatementValueOrFallback(it, false) ?: return null
restoreFlowTypeOverride(elseRestore)
} }
builder.mark(endLabel) builder.mark(endLabel)
return condition return condition
@ -3985,13 +3993,17 @@ class BytecodeCompiler(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) 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 val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null
restoreFlowTypeOverride(thenRestore)
val thenObj = ensureObjSlot(thenValue) val thenObj = ensureObjSlot(thenValue)
builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(elseLabel) builder.mark(elseLabel)
if (stmt.elseBody != null) { if (stmt.elseBody != null) {
val elseRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = false))
val elseValue = compileStatementValueOrFallback(stmt.elseBody) ?: return null val elseValue = compileStatementValueOrFallback(stmt.elseBody) ?: return null
restoreFlowTypeOverride(elseRestore)
val elseObj = ensureObjSlot(elseValue) val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
} else { } 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<WhenCondition>
): 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? { private fun findLoopContextIndex(label: String?): Int? {
if (loopStack.isEmpty()) return null if (loopStack.isEmpty()) return null
val stack = loopStack.toList() val stack = loopStack.toList()

View File

@ -1832,98 +1832,47 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos scope.pos = atPos
if (!PerfFlags.LOCAL_SLOT_PIC) { if (name == "this") return scope.thisObj.asReadonly
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
}
}
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope) val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
val rec = scope.getSlotRecord(slot) val rec = scope.getSlotRecord(slot)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { if (rec.declaringClass != null &&
// Not visible via slot, fallback to other lookups !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)
} else { ) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
if (PerfFlags.PIC_DEBUG_COUNTERS) { if (PerfFlags.PIC_DEBUG_COUNTERS) {
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++ if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
} }
return rec return rec
} }
} scope.raiseSymbolNotFound(name)
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
}
} }
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos scope.pos = atPos
if (name == "this") return scope.thisObj
scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) } scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) }
// fallback to current-scope object or field on `this` scope.raiseSymbolNotFound(name)
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
}
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos scope.pos = atPos
if (!PerfFlags.LOCAL_SLOT_PIC) { if (name == "this") scope.raiseError("can't assign to this")
scope.getSlotIndexOf(name)?.let { val slot =
val rec = scope.getSlotRecord(it) if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot
scope.assign(rec, name, newValue) else resolveSlot(scope)
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 (slot >= 0) { if (slot >= 0) {
val rec = scope.getSlotRecord(slot) 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) scope.assign(rec, name, newValue)
return return
} }
} }
scope.chainLookupIgnoreClosure(name, followClosure = true, caller = scope.currentClassCtx)?.let { rec -> scope.raiseSymbolNotFound(name)
scope.assign(rec, name, newValue)
return
}
scope[name]?.let { stored ->
scope.assign(stored, name, newValue)
return
}
scope.thisObj.writeField(scope, name, newValue)
return
} }
} }
@ -2029,34 +1978,7 @@ class FastLocalVarRef(
return rec return rec
} }
} }
// Try per-frame local binding maps in the ancestry first (locals declared in frames) scope.raiseSymbolNotFound(name)
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)
} }
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
@ -2070,39 +1992,7 @@ class FastLocalVarRef(
return scope.resolve(rec, name) return scope.resolve(rec, name)
} }
} }
// Try per-frame local binding maps in the ancestry first scope.raiseSymbolNotFound(name)
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
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
@ -2117,29 +2007,7 @@ class FastLocalVarRef(
return return
} }
} }
// Try per-frame local binding maps in the ancestry first scope.raiseSymbolNotFound(name)
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
} }
} }
@ -2287,18 +2155,22 @@ class LocalSlotRef(
override fun forEachVariable(block: (String) -> Unit) { override fun forEachVariable(block: (String) -> Unit) {
block(name) block(name)
} }
private fun resolveOwner(scope: Scope): Scope? {
private val fallbackRef = LocalVarRef(name, atPos) var s: Scope? = scope
private fun resolveOwner(scope: Scope): Scope { var guard = 0
return scope 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 { override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos 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 (slot < 0 || slot >= owner.slotCount()) {
if (strict) scope.raiseError("slot index out of range for $name") scope.raiseError("slot index out of range for $name")
return fallbackRef.get(scope)
} }
val rec = owner.getSlotRecord(slot) val rec = owner.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)) {
@ -2309,10 +2181,9 @@ class LocalSlotRef(
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos 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 (slot < 0 || slot >= owner.slotCount()) {
if (strict) scope.raiseError("slot index out of range for $name") scope.raiseError("slot index out of range for $name")
return fallbackRef.evalValue(scope)
} }
val rec = owner.getSlotRecord(slot) val rec = owner.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)) {
@ -2323,11 +2194,9 @@ class LocalSlotRef(
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos 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 (slot < 0 || slot >= owner.slotCount()) {
if (strict) scope.raiseError("slot index out of range for $name") scope.raiseError("slot index out of range for $name")
fallbackRef.setAt(pos, scope, newValue)
return
} }
val rec = owner.getSlotRecord(slot) val rec = owner.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)) {