From d739833c577010214f6284ab5360ccc2aec15cbb Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 9 Feb 2026 02:02:10 +0300 Subject: [PATCH] Step 8: bytecode ObjDynamic member access --- bytecode_migration_plan.md | 4 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 2 - .../lyng/bytecode/BytecodeCompiler.kt | 72 ++++++++++++++++ .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 9 ++ .../sergeych/lyng/bytecode/CmdDisassembler.kt | 9 ++ .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 84 +++++++++++++++++++ .../net/sergeych/lyng/bytecode/Opcode.kt | 3 + 7 files changed, 179 insertions(+), 4 deletions(-) diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 623e215..e3ba613 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -27,8 +27,8 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Replace `MapLiteralEntry.Spread` bytecode exception with runtime `putAll`/merge logic. - [x] Step 7: Class-scope member refs in bytecode. - [x] Support `ClassScopeMemberRef` without scope-map fallback. -- [ ] Step 8: ObjDynamic member access in bytecode. - - [ ] Allow dynamic receiver field/method lookup without falling back to interpreter. +- [x] Step 8: ObjDynamic member access in bytecode. + - [x] Allow dynamic receiver field/method lookup without falling back to interpreter. - [ ] Step 9: Module-level bytecode execution. - [ ] Compile `Script` bodies to bytecode instead of interpreting at module scope. - [ ] Keep import/module slot seeding in frame-only flow. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 2c66a14..e771dff 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1944,7 +1944,6 @@ class Compiler( is ElvisRef -> containsUnsupportedRef(ref.left) || containsUnsupportedRef(ref.right) is FieldRef -> { val receiverClass = resolveReceiverClassForMember(ref.target) ?: return true - if (receiverClass == ObjDynamic.type) return true val hasMember = receiverClass.instanceFieldIdMap()[ref.name] != null || receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] != null if (!hasMember && !hasExtensionFor(receiverClass.className, ref.name)) return true @@ -1975,7 +1974,6 @@ class Compiler( is MethodCallRef -> { if (ref.name == "delay") return true val receiverClass = resolveReceiverClassForMember(ref.receiver) ?: return true - if (receiverClass == ObjDynamic.type) return true val hasMember = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] != null if (!hasMember && !hasExtensionFor(receiverClass.className, ref.name)) return true containsUnsupportedRef(ref.receiver) || ref.args.any { containsUnsupportedForBytecode(it.value) } 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 50e8932..5f48bc4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -1670,6 +1670,25 @@ class BytecodeCompiler( Pos.builtIn ) val receiver = compileRefWithFallback(target.target, null, Pos.builtIn) ?: return null + if (receiverClass == ObjDynamic.type) { + val nameId = builder.addConst(BytecodeConst.StringVal(target.name)) + if (!target.isOptional) { + builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, value.slot) + } else { + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, nullSlot, cmpSlot) + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel)) + ) + builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, value.slot) + builder.mark(endLabel) + } + return value + } val fieldId = receiverClass.instanceFieldIdMap()[target.name] val methodId = if (fieldId == null) { receiverClass.instanceMethodIdMap(includeAbstract = true)[target.name] @@ -2098,6 +2117,32 @@ class BytecodeCompiler( Pos.builtIn ) } + if (receiverClass == ObjDynamic.type) { + val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null + val dst = allocSlot() + val nameId = builder.addConst(BytecodeConst.StringVal(ref.name)) + if (!ref.isOptional) { + builder.emit(Opcode.GET_DYNAMIC_MEMBER, receiver.slot, nameId, dst) + } else { + 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)) + ) + builder.emit(Opcode.GET_DYNAMIC_MEMBER, receiver.slot, nameId, dst) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, dst) + builder.mark(endLabel) + } + updateSlotType(dst, SlotType.OBJ) + return CompiledValue(dst, SlotType.OBJ) + } val fieldId = receiverClass.instanceFieldIdMap()[ref.name] val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] val encodedFieldId = encodeMemberId(receiverClass, fieldId) @@ -2926,6 +2971,33 @@ class BytecodeCompiler( } val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null val dst = allocSlot() + if (receiverClass == ObjDynamic.type) { + val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null + val encodedCount = encodeCallArgCount(args) ?: return null + val nameId = builder.addConst(BytecodeConst.StringVal(ref.name)) + if (!ref.isOptional) { + setPos(callPos) + builder.emit(Opcode.CALL_DYNAMIC_MEMBER, receiver.slot, nameId, args.base, encodedCount, dst) + return CompiledValue(dst, SlotType.OBJ) + } + 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)) + ) + setPos(callPos) + builder.emit(Opcode.CALL_DYNAMIC_MEMBER, receiver.slot, nameId, args.base, encodedCount, dst) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, dst) + builder.mark(endLabel) + return CompiledValue(dst, SlotType.OBJ) + } val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] if (methodId != null) { val encodedMethodId = encodeMemberId(receiverClass, methodId) ?: methodId 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 6cb7fc2..ce31952 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -181,6 +181,8 @@ class CmdBuilder { listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.CALL_SLOT -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) + Opcode.CALL_DYNAMIC_MEMBER -> + listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.GET_INDEX -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.SET_INDEX -> @@ -197,6 +199,10 @@ class CmdBuilder { listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) Opcode.SET_CLASS_SCOPE -> listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) + Opcode.GET_DYNAMIC_MEMBER -> + listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) + Opcode.SET_DYNAMIC_MEMBER -> + listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) Opcode.ITER_PUSH -> listOf(OperandKind.SLOT) Opcode.ITER_POP, Opcode.ITER_CANCEL -> @@ -394,6 +400,7 @@ class CmdBuilder { Opcode.CALL_DIRECT -> CmdCallDirect(operands[0], operands[1], operands[2], operands[3]) Opcode.CALL_MEMBER_SLOT -> CmdCallMemberSlot(operands[0], operands[1], operands[2], operands[3], operands[4]) Opcode.CALL_SLOT -> CmdCallSlot(operands[0], operands[1], operands[2], operands[3]) + Opcode.CALL_DYNAMIC_MEMBER -> CmdCallDynamicMember(operands[0], operands[1], operands[2], operands[3], operands[4]) Opcode.GET_INDEX -> CmdGetIndex(operands[0], operands[1], operands[2]) Opcode.SET_INDEX -> CmdSetIndex(operands[0], operands[1], operands[2]) Opcode.LIST_LITERAL -> CmdListLiteral(operands[0], operands[1], operands[2], operands[3]) @@ -401,6 +408,8 @@ class CmdBuilder { Opcode.SET_MEMBER_SLOT -> CmdSetMemberSlot(operands[0], operands[1], operands[2], operands[3]) Opcode.GET_CLASS_SCOPE -> CmdGetClassScope(operands[0], operands[1], operands[2]) Opcode.SET_CLASS_SCOPE -> CmdSetClassScope(operands[0], operands[1], operands[2]) + Opcode.GET_DYNAMIC_MEMBER -> CmdGetDynamicMember(operands[0], operands[1], operands[2]) + Opcode.SET_DYNAMIC_MEMBER -> CmdSetDynamicMember(operands[0], operands[1], operands[2]) Opcode.ITER_PUSH -> CmdIterPush(operands[0]) Opcode.ITER_POP -> CmdIterPop() Opcode.ITER_CANCEL -> CmdIterCancel() 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 32c016a..fcd653f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -195,6 +195,7 @@ object CmdDisassembler { is CmdCallDirect -> Opcode.CALL_DIRECT to intArrayOf(cmd.id, cmd.argBase, cmd.argCount, cmd.dst) is CmdCallMemberSlot -> Opcode.CALL_MEMBER_SLOT to intArrayOf(cmd.recvSlot, cmd.methodId, cmd.argBase, cmd.argCount, cmd.dst) is CmdCallSlot -> Opcode.CALL_SLOT to intArrayOf(cmd.calleeSlot, cmd.argBase, cmd.argCount, cmd.dst) + is CmdCallDynamicMember -> Opcode.CALL_DYNAMIC_MEMBER to intArrayOf(cmd.recvSlot, cmd.nameId, cmd.argBase, cmd.argCount, cmd.dst) is CmdGetIndex -> Opcode.GET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.dst) is CmdSetIndex -> Opcode.SET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.valueSlot) is CmdListLiteral -> Opcode.LIST_LITERAL to intArrayOf(cmd.planId, cmd.baseSlot, cmd.count, cmd.dst) @@ -202,6 +203,8 @@ object CmdDisassembler { is CmdSetMemberSlot -> Opcode.SET_MEMBER_SLOT to intArrayOf(cmd.recvSlot, cmd.fieldId, cmd.methodId, cmd.valueSlot) is CmdGetClassScope -> Opcode.GET_CLASS_SCOPE to intArrayOf(cmd.classSlot, cmd.nameId, cmd.dst) is CmdSetClassScope -> Opcode.SET_CLASS_SCOPE to intArrayOf(cmd.classSlot, cmd.nameId, cmd.valueSlot) + is CmdGetDynamicMember -> Opcode.GET_DYNAMIC_MEMBER to intArrayOf(cmd.recvSlot, cmd.nameId, cmd.dst) + is CmdSetDynamicMember -> Opcode.SET_DYNAMIC_MEMBER to intArrayOf(cmd.recvSlot, cmd.nameId, cmd.valueSlot) is CmdIterPush -> Opcode.ITER_PUSH to intArrayOf(cmd.iterSlot) is CmdIterPop -> Opcode.ITER_POP to intArrayOf() is CmdIterCancel -> Opcode.ITER_CANCEL to intArrayOf() @@ -285,6 +288,8 @@ object CmdDisassembler { listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.CALL_MEMBER_SLOT -> listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) + Opcode.CALL_DYNAMIC_MEMBER -> + listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.GET_INDEX -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.SET_INDEX -> @@ -299,6 +304,10 @@ object CmdDisassembler { listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) Opcode.SET_CLASS_SCOPE -> listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) + Opcode.GET_DYNAMIC_MEMBER -> + listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) + Opcode.SET_DYNAMIC_MEMBER -> + listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT) } } } 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 a867a39..9fd3a81 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -1404,6 +1404,21 @@ private fun decodeMemberId(id: Int): Pair { } } +private suspend fun resolveDynamicFieldValue(scope: Scope, receiver: Obj, name: String, rec: ObjRecord): Obj { + if (rec.type == ObjRecord.Type.Delegated || rec.value is ObjProperty || rec.type == ObjRecord.Type.Property) { + val recv = rec.receiver ?: receiver + return recv.resolveRecord(scope, rec, name, rec.declaringClass).value + } + if (rec.receiver != null && rec.declaringClass != null) { + return rec.receiver!!.resolveRecord(scope, rec, name, rec.declaringClass).value + } + if (rec.type == ObjRecord.Type.Fun && !rec.isAbstract) { + val recv = rec.receiver ?: receiver + return rec.value.invoke(scope, recv, Arguments.EMPTY, rec.declaringClass) + } + return rec.value +} + class CmdGetMemberSlot( internal val recvSlot: Int, internal val fieldId: Int, @@ -1527,6 +1542,75 @@ class CmdSetClassScope( } } +class CmdGetDynamicMember( + internal val recvSlot: Int, + internal val nameId: Int, + internal val dst: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.fn.localSlotNames.isNotEmpty()) { + frame.syncFrameToScope(useRefs = true) + } + val nameConst = frame.fn.constants.getOrNull(nameId) as? BytecodeConst.StringVal + ?: error("GET_DYNAMIC_MEMBER expects StringVal at $nameId") + val scope = frame.ensureScope() + val receiver = frame.slotToObj(recvSlot) + val rec = receiver.readField(scope, nameConst.value) + val value = resolveDynamicFieldValue(scope, receiver, nameConst.value, rec) + if (frame.fn.localSlotNames.isNotEmpty()) { + frame.syncScopeToFrame() + } + frame.storeObjResult(dst, value) + return + } +} + +class CmdSetDynamicMember( + internal val recvSlot: Int, + internal val nameId: Int, + internal val valueSlot: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.fn.localSlotNames.isNotEmpty()) { + frame.syncFrameToScope(useRefs = true) + } + val nameConst = frame.fn.constants.getOrNull(nameId) as? BytecodeConst.StringVal + ?: error("SET_DYNAMIC_MEMBER expects StringVal at $nameId") + val scope = frame.ensureScope() + val receiver = frame.slotToObj(recvSlot) + receiver.writeField(scope, nameConst.value, frame.slotToObj(valueSlot)) + if (frame.fn.localSlotNames.isNotEmpty()) { + frame.syncScopeToFrame() + } + return + } +} + +class CmdCallDynamicMember( + internal val recvSlot: Int, + internal val nameId: Int, + internal val argBase: Int, + internal val argCount: Int, + internal val dst: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.fn.localSlotNames.isNotEmpty()) { + frame.syncFrameToScope(useRefs = true) + } + val nameConst = frame.fn.constants.getOrNull(nameId) as? BytecodeConst.StringVal + ?: error("CALL_DYNAMIC_MEMBER expects StringVal at $nameId") + val scope = frame.ensureScope() + val receiver = frame.slotToObj(recvSlot) + val callArgs = frame.buildArguments(argBase, argCount) + val result = receiver.invokeInstanceMethod(scope, nameConst.value, callArgs) + if (frame.fn.localSlotNames.isNotEmpty()) { + frame.syncScopeToFrame() + } + frame.storeObjResult(dst, result) + return + } +} + class CmdCallMemberSlot( internal val recvSlot: Int, internal val methodId: Int, 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 635c8e0..2fd2a58 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -142,6 +142,9 @@ enum class Opcode(val code: Int) { SET_MEMBER_SLOT(0xA9), GET_CLASS_SCOPE(0xAA), SET_CLASS_SCOPE(0xAB), + GET_DYNAMIC_MEMBER(0xAC), + SET_DYNAMIC_MEMBER(0xAD), + CALL_DYNAMIC_MEMBER(0xAE), RESOLVE_SCOPE_SLOT(0xB1), LOAD_OBJ_ADDR(0xB2),