From c035b4c34ceaeb5d99e3d5bf7b76c760404b6d0c Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 9 Feb 2026 12:34:23 +0300 Subject: [PATCH] Step 22: delegated locals in bytecode --- bytecode_migration_plan.md | 8 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 10 +- .../lyng/bytecode/BytecodeCompiler.kt | 100 +++++++++++++++++- .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 3 + .../sergeych/lyng/bytecode/CmdDisassembler.kt | 3 + .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 17 +++ .../net/sergeych/lyng/bytecode/Opcode.kt | 1 + .../kotlin/BytecodeRecentOpsTest.kt | 19 ++++ 8 files changed, 146 insertions(+), 15 deletions(-) diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 4f496f2..7bb03a4 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -73,10 +73,10 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Step 21: Union mismatch path in bytecode. - [x] Replace `UnionTypeMismatchStatement` branch with a bytecode-compilable throw path (no custom `StatementRef` that blocks bytecode). - [x] Add a JVM test that forces the union mismatch at runtime and asserts the error message. -- [ ] Step 22: Delegated local slots in bytecode. - - [ ] Support reads/writes/assign-ops/inc/dec for delegated locals (`LocalSlotRef.isDelegated`) in `BytecodeCompiler`. - - [ ] Remove `containsDelegatedRefs` guard once delegated locals are bytecode-safe. - - [ ] Add JVM tests that use delegated locals inside bytecode-compiled functions. +- [x] Step 22: Delegated local slots in bytecode. + - [x] Support reads/writes/assign-ops/inc/dec for delegated locals (`LocalSlotRef.isDelegated`) in `BytecodeCompiler`. + - [x] Remove `containsDelegatedRefs` guard once delegated locals are bytecode-safe. + - [x] Add JVM tests that use delegated locals inside bytecode-compiled functions. ## Notes diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 47f8b97..511206a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1545,8 +1545,7 @@ class Compiler( statements.isNotEmpty() && codeContexts.lastOrNull() is CodeContext.Module && resolutionScriptDepth == 1 && - statements.none { containsUnsupportedForBytecode(it) } && - statements.none { containsDelegatedRefs(it) } + statements.none { containsUnsupportedForBytecode(it) } val (finalStatements, moduleBytecode) = if (wrapScriptBytecode) { val unwrapped = statements.map { unwrapBytecodeDeep(it) } val block = InlineBlockStatement(unwrapped, start) @@ -1829,9 +1828,6 @@ class Compiler( if (codeContexts.any { it is CodeContext.Function }) { return stmt } - if (containsDelegatedRefs(stmt)) { - return stmt - } if (containsUnsupportedForBytecode(stmt)) { return stmt } @@ -1871,7 +1867,6 @@ class Compiler( extraKnownNameObjClass: Map = emptyMap() ): Statement { if (!useBytecodeStatements) return stmt - if (containsDelegatedRefs(stmt)) return stmt if (containsUnsupportedForBytecode(stmt)) return stmt val returnLabels = returnLabelStack.lastOrNull() ?: emptySet() val allowedScopeNames = moduleSlotPlan()?.slots?.keys @@ -6965,8 +6960,7 @@ class Compiler( val fnStatements = rawFnStatements?.let { stmt -> if (useBytecodeStatements && parentContext !is CodeContext.ClassBody && - !containsUnsupportedForBytecode(stmt) && - !containsDelegatedRefs(stmt) + !containsUnsupportedForBytecode(stmt) ) { val paramKnownClasses = mutableMapOf() for (param in argsDeclaration.params) { 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 d779a5a..fddb4b3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -303,7 +303,18 @@ class BytecodeCompiler( return CompiledValue(slot, resolved) } if (!allowLocalSlots) return null - if (ref.isDelegated) return compileEvalRef(ref) + if (ref.isDelegated) { + val mapped = resolveSlot(ref) ?: return null + if (mapped < scopeSlotCount) { + val addrSlot = ensureScopeAddr(mapped) + val local = allocSlot() + builder.emit(Opcode.LOAD_OBJ_ADDR, addrSlot, local) + updateSlotType(local, SlotType.OBJ) + return CompiledValue(local, SlotType.OBJ) + } + updateSlotType(mapped, SlotType.OBJ) + return CompiledValue(mapped, SlotType.OBJ) + } if (ref.name.isEmpty()) return null if (ref.captureOwnerScopeId == null && refScopeId(ref) == 0) { val byName = scopeSlotIndexByName[ref.name] @@ -1663,6 +1674,14 @@ class BytecodeCompiler( val localTarget = assignTarget(ref) if (localTarget != null) { if (!allowLocalSlots) return null + if (localTarget.isDelegated) { + val slot = resolveSlot(localTarget) ?: return null + if (slot >= scopeSlotCount) return null + val value = compileRef(assignValue(ref)) ?: return null + builder.emit(Opcode.ASSIGN_SCOPE_SLOT, slot, value.slot) + updateSlotType(slot, SlotType.OBJ) + return value + } val value = compileRef(assignValue(ref)) ?: return null if (!localTarget.isMutable || localTarget.isDelegated) { val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val ${localTarget.name}")) @@ -1970,6 +1989,30 @@ class BytecodeCompiler( val localTarget = ref.target as? LocalSlotRef if (localTarget != null) { if (!allowLocalSlots) return compileEvalRef(ref) + if (localTarget.isDelegated) { + val slot = resolveSlot(localTarget) ?: return null + if (slot >= scopeSlotCount) return null + val addrSlot = ensureScopeAddr(slot) + val current = allocSlot() + builder.emit(Opcode.LOAD_OBJ_ADDR, addrSlot, current) + updateSlotType(current, SlotType.OBJ) + val rhs = compileRef(ref.value) ?: return compileEvalRef(ref) + val rhsObj = ensureObjSlot(rhs) + val objOp = when (ref.op) { + BinOp.PLUS -> Opcode.ADD_OBJ + BinOp.MINUS -> Opcode.SUB_OBJ + BinOp.STAR -> Opcode.MUL_OBJ + BinOp.SLASH -> Opcode.DIV_OBJ + BinOp.PERCENT -> Opcode.MOD_OBJ + else -> null + } ?: return compileEvalRef(ref) + val result = allocSlot() + builder.emit(objOp, current, rhsObj.slot, result) + updateSlotType(result, SlotType.OBJ) + builder.emit(Opcode.ASSIGN_SCOPE_SLOT, slot, result) + updateSlotType(slot, SlotType.OBJ) + return CompiledValue(result, SlotType.OBJ) + } if (localTarget.isDelegated) return compileEvalRef(ref) val slot = resolveSlot(localTarget) ?: return null val targetType = slotTypes[slot] ?: SlotType.OBJ @@ -2252,7 +2295,32 @@ class BytecodeCompiler( builder.emit(Opcode.SET_CLASS_SCOPE, classObj.slot, nameId, newValue.slot) } is LocalSlotRef -> { - if (!allowLocalSlots || !target.isMutable || target.isDelegated) return null + if (!allowLocalSlots || !target.isMutable) return null + if (target.isDelegated) { + val slot = resolveSlot(target) ?: return null + if (slot >= scopeSlotCount) return null + val addrSlot = ensureScopeAddr(slot) + val current = allocSlot() + builder.emit(Opcode.LOAD_OBJ_ADDR, addrSlot, current) + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, current, nullSlot, cmpSlot) + val assignLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(assignLabel)) + ) + builder.emit(Opcode.MOVE_OBJ, current, resultSlot) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(assignLabel) + builder.emit(Opcode.ASSIGN_SCOPE_SLOT, slot, newValue.slot) + builder.emit(Opcode.MOVE_OBJ, newValue.slot, resultSlot) + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } val slot = resolveSlot(target) ?: return null if (slot < scopeSlotCount) { val addrSlot = ensureScopeAddr(slot) @@ -2733,7 +2801,30 @@ class BytecodeCompiler( val target = ref.target as? LocalSlotRef if (target != null) { if (!allowLocalSlots) return null - if (!target.isMutable || target.isDelegated) return null + if (!target.isMutable) return null + if (target.isDelegated) { + val slot = resolveSlot(target) ?: return null + if (slot >= scopeSlotCount) return null + val addrSlot = ensureScopeAddr(slot) + val current = allocSlot() + builder.emit(Opcode.LOAD_OBJ_ADDR, addrSlot, current) + updateSlotType(current, SlotType.OBJ) + val oneSlot = allocSlot() + val oneId = builder.addConst(BytecodeConst.ObjRef(ObjInt.One)) + builder.emit(Opcode.CONST_OBJ, oneId, oneSlot) + updateSlotType(oneSlot, SlotType.OBJ) + val result = allocSlot() + val op = if (ref.isIncrement) Opcode.ADD_OBJ else Opcode.SUB_OBJ + builder.emit(op, current, oneSlot, result) + updateSlotType(result, SlotType.OBJ) + builder.emit(Opcode.ASSIGN_SCOPE_SLOT, slot, result) + updateSlotType(slot, SlotType.OBJ) + return if (wantResult && ref.isPost) { + CompiledValue(current, SlotType.OBJ) + } else { + CompiledValue(result, SlotType.OBJ) + } + } val slot = resolveSlot(target) ?: return null val slotType = slotTypes[slot] ?: SlotType.UNKNOWN if (slot < scopeSlotCount && slotType != SlotType.UNKNOWN) { @@ -6071,6 +6162,9 @@ class BytecodeCompiler( } stmt.initializer?.let { collectScopeSlots(it) } } + is DelegatedVarDeclStatement -> { + collectScopeSlots(stmt.initializer) + } is IfStatement -> { collectScopeSlots(stmt.condition) collectScopeSlots(stmt.ifBody) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index 851f0a7..85b3cf1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -135,6 +135,8 @@ class CmdBuilder { listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.RESOLVE_SCOPE_SLOT -> listOf(OperandKind.SLOT, OperandKind.ADDR) + Opcode.ASSIGN_SCOPE_SLOT -> + listOf(OperandKind.SLOT, OperandKind.SLOT) Opcode.LOAD_OBJ_ADDR, Opcode.LOAD_INT_ADDR, Opcode.LOAD_REAL_ADDR, Opcode.LOAD_BOOL_ADDR -> listOf(OperandKind.ADDR, OperandKind.SLOT) Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR -> @@ -254,6 +256,7 @@ class CmdBuilder { Opcode.THROW -> CmdThrow(operands[0], operands[1]) Opcode.RETHROW_PENDING -> CmdRethrowPending() Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1]) + Opcode.ASSIGN_SCOPE_SLOT -> CmdAssignScopeSlot(operands[0], operands[1]) Opcode.LOAD_OBJ_ADDR -> CmdLoadObjAddr(operands[0], operands[1]) Opcode.STORE_OBJ_ADDR -> CmdStoreObjAddr(operands[0], operands[1]) Opcode.LOAD_INT_ADDR -> CmdLoadIntAddr(operands[0], operands[1]) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt index 915a368..4fd1f7d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -84,6 +84,7 @@ object CmdDisassembler { cmd.dst ) is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot) + is CmdAssignScopeSlot -> Opcode.ASSIGN_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.valueSlot) is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst) is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot) is CmdLoadIntAddr -> Opcode.LOAD_INT_ADDR to intArrayOf(cmd.addrSlot, cmd.dst) @@ -243,6 +244,8 @@ object CmdDisassembler { listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.RESOLVE_SCOPE_SLOT -> listOf(OperandKind.SLOT, OperandKind.ADDR) + Opcode.ASSIGN_SCOPE_SLOT -> + listOf(OperandKind.SLOT, OperandKind.SLOT) Opcode.LOAD_OBJ_ADDR, Opcode.LOAD_INT_ADDR, Opcode.LOAD_REAL_ADDR, Opcode.LOAD_BOOL_ADDR -> listOf(OperandKind.ADDR, OperandKind.SLOT) Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 51a1c89..9943a8e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -358,6 +358,13 @@ class CmdStoreBoolAddr(internal val src: Int, internal val addrSlot: Int) : Cmd( } } +class CmdAssignScopeSlot(internal val scopeSlot: Int, internal val valueSlot: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.assignScopeSlot(scopeSlot, frame.slotToObj(valueSlot)) + return + } +} + class CmdIntToReal(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { frame.setReal(dst, frame.getReal(src)) @@ -2124,6 +2131,16 @@ class CmdFrame( } } + suspend fun assignScopeSlot(scopeSlot: Int, value: Obj) { + ensureScope() + val target = scopeTarget(scopeSlot) + val index = ensureScopeSlot(target, scopeSlot) + val name = fn.scopeSlotNames.getOrNull(scopeSlot) + ?: target.raiseSymbolNotFound("slot $scopeSlot") + val record = target.getSlotRecord(index) + target.assign(record, name, value) + } + suspend fun getInt(slot: Int): Long { return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot).toLong() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index 869cdf6..b866775 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -161,6 +161,7 @@ enum class Opcode(val code: Int) { ITER_PUSH(0xBF), ITER_POP(0xC0), ITER_CANCEL(0xC1), + ASSIGN_SCOPE_SLOT(0xC2), ; companion object { diff --git a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt index de960c3..4ee85dd 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt @@ -199,6 +199,25 @@ class BytecodeRecentOpsTest { ) } + @Test + fun delegatedLocalAssignAndIncDec() = runTest { + eval( + """ + class BoxDelegate(var v) : Delegate { + override fun getValue(thisRef: Object, name: String): Object = v + override fun setValue(thisRef: Object, name: String, value: Object) { v = value } + } + fun calc() { + var x by BoxDelegate(1) + x += 2 + x++ + return x + } + assertEquals(4, calc()) + """.trimIndent() + ) + } + @Test fun unionMemberDispatchSubtype() = runTest { eval(