From 473a5dd6ed94ec13763d9702c11531ebbfa44ddb Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 9 Feb 2026 10:04:37 +0300 Subject: [PATCH] Handle optional assign-ops and inc/dec in bytecode --- bytecode_migration_plan.md | 6 +- .../lyng/bytecode/BytecodeCompiler.kt | 206 ++++++++++++++++-- 2 files changed, 196 insertions(+), 16 deletions(-) diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 1f3504e..5f4a718 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -38,9 +38,11 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Ensure module object member refs compile as instance access (not class-scope). - [x] Step 11: Destructuring assignment bytecode. - [x] Handle `[a, b] = expr` (AssignRef target `ListLiteralRef`) without interpreter fallback. -- [ ] Step 12: Optional member assign-ops and inc/dec in bytecode. - - [ ] Support `a?.b += 1` and `a?.b++` for `FieldRef` targets. +- [x] Step 12: Optional member assign-ops and inc/dec in bytecode. + - [x] Support `a?.b += 1` and `a?.b++` for `FieldRef` targets. - [x] Fix post-inc return value for object slots stored in scope frames. + - [x] Handle optional receivers for member assign-ops and inc/dec without evaluating operands on null. + - [x] Support class-scope and index optional inc/dec paths in bytecode. ## Notes 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 13d5e06..31685d6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -2002,8 +2002,7 @@ class BytecodeCompiler( builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, result) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(nullLabel) - builder.emit(Opcode.CONST_NULL, current) - builder.emit(objOp, current, rhs.slot, result) + builder.emit(Opcode.CONST_NULL, result) builder.mark(endLabel) updateSlotType(result, SlotType.OBJ) return CompiledValue(result, SlotType.OBJ) @@ -2032,8 +2031,7 @@ class BytecodeCompiler( builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, result) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(nullLabel) - builder.emit(Opcode.CONST_NULL, current) - builder.emit(objOp, current, rhs.slot, result) + builder.emit(Opcode.CONST_NULL, result) builder.mark(endLabel) } updateSlotType(result, SlotType.OBJ) @@ -2060,8 +2058,7 @@ class BytecodeCompiler( builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, result) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(nullLabel) - builder.emit(Opcode.CONST_NULL, current) - builder.emit(objOp, current, rhs.slot, result) + builder.emit(Opcode.CONST_NULL, result) builder.mark(endLabel) } updateSlotType(result, SlotType.OBJ) @@ -2150,8 +2147,7 @@ class BytecodeCompiler( builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.mark(nullLabel) - builder.emit(Opcode.CONST_NULL, current) - builder.emit(objOp, current, rhs.slot, result) + builder.emit(Opcode.CONST_NULL, result) builder.mark(endLabel) updateSlotType(result, SlotType.OBJ) return CompiledValue(result, SlotType.OBJ) @@ -2878,7 +2874,6 @@ class BytecodeCompiler( val fieldTarget = ref.target as? FieldRef if (fieldTarget != null) { - if (fieldTarget.isOptional) return null val receiverClass = resolveReceiverClass(fieldTarget.target) ?: throw BytecodeCompileException( "Member access requires compile-time receiver type: ${fieldTarget.name}", @@ -2887,6 +2882,45 @@ class BytecodeCompiler( if (receiverClass == ObjDynamic.type) { val receiver = compileRefWithFallback(fieldTarget.target, null, Pos.builtIn) ?: return null val nameId = builder.addConst(BytecodeConst.StringVal(fieldTarget.name)) + val resultSlot = allocSlot() + if (fieldTarget.isOptional) { + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val current = allocSlot() + builder.emit(Opcode.GET_DYNAMIC_MEMBER, receiver.slot, nameId, 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 + if (wantResult && ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_OBJ, current, old) + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, result) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + } else { + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, result) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, resultSlot) + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } val current = allocSlot() builder.emit(Opcode.GET_DYNAMIC_MEMBER, receiver.slot, nameId, current) updateSlotType(current, SlotType.OBJ) @@ -2901,16 +2935,121 @@ class BytecodeCompiler( builder.emit(Opcode.MOVE_OBJ, current, old) builder.emit(op, current, oneSlot, result) builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, result) - return CompiledValue(old, SlotType.OBJ) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + return CompiledValue(resultSlot, SlotType.OBJ) } builder.emit(op, current, oneSlot, result) builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, result) - return CompiledValue(result, SlotType.OBJ) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + return CompiledValue(resultSlot, SlotType.OBJ) } val fieldId = receiverClass.instanceFieldIdMap()[fieldTarget.name] val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[fieldTarget.name] + if (fieldId == null && methodId == null && isKnownClassReceiver(fieldTarget.target)) { + val receiver = compileRefWithFallback(fieldTarget.target, null, Pos.builtIn) ?: return null + val nameId = builder.addConst(BytecodeConst.StringVal(fieldTarget.name)) + val resultSlot = allocSlot() + if (fieldTarget.isOptional) { + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val current = allocSlot() + builder.emit(Opcode.GET_CLASS_SCOPE, receiver.slot, nameId, 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 + if (wantResult && ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_OBJ, current, old) + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, result) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + } else { + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, result) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, resultSlot) + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } + val current = allocSlot() + builder.emit(Opcode.GET_CLASS_SCOPE, receiver.slot, nameId, 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 + if (wantResult && ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_OBJ, current, old) + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, result) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + return CompiledValue(resultSlot, SlotType.OBJ) + } + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, result) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + return CompiledValue(resultSlot, SlotType.OBJ) + } if (fieldId == null && methodId == null) return null val receiver = compileRefWithFallback(fieldTarget.target, null, Pos.builtIn) ?: return null + val resultSlot = allocSlot() + if (fieldTarget.isOptional) { + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val current = allocSlot() + builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, 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 + if (wantResult && ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_OBJ, current, old) + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, result) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + } else { + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, result) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, resultSlot) + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } val current = allocSlot() builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, current) updateSlotType(current, SlotType.OBJ) @@ -2925,16 +3064,55 @@ class BytecodeCompiler( builder.emit(Opcode.MOVE_OBJ, current, old) builder.emit(op, current, oneSlot, result) builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, result) - return CompiledValue(old, SlotType.OBJ) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + return CompiledValue(resultSlot, SlotType.OBJ) } builder.emit(op, current, oneSlot, result) builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, result) - return CompiledValue(result, SlotType.OBJ) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + return CompiledValue(resultSlot, SlotType.OBJ) } val indexTarget = ref.target as? IndexRef ?: return null - if (indexTarget.optionalRef) return null val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null + if (indexTarget.optionalRef) { + val resultSlot = allocSlot() + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null + val current = allocSlot() + builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current) + updateSlotType(current, SlotType.OBJ) + val oneSlot = allocSlot() + val oneId = builder.addConst(BytecodeConst.ObjRef(ObjInt.One)) + builder.emit(Opcode.CONST_OBJ, oneId, oneSlot) + val result = allocSlot() + val op = if (ref.isIncrement) Opcode.ADD_OBJ else Opcode.SUB_OBJ + if (wantResult && ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_OBJ, current, old) + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result) + builder.emit(Opcode.MOVE_OBJ, old, resultSlot) + } else { + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result) + builder.emit(Opcode.MOVE_OBJ, result, resultSlot) + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, resultSlot) + builder.mark(endLabel) + return CompiledValue(resultSlot, SlotType.OBJ) + } val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null val current = allocSlot() builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current)