Step 15: class-scope ?= in bytecode

This commit is contained in:
Sergey Chernov 2026-02-09 10:34:36 +03:00
parent 541738646f
commit 0b94b46d40
3 changed files with 96 additions and 51 deletions

View File

@ -50,9 +50,9 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te
- [x] Support `FastLocalVarRef` reads with the same slot resolution as `LocalVarRef`. - [x] Support `FastLocalVarRef` reads with the same slot resolution as `LocalVarRef`.
- [x] If `BoundLocalVarRef` is still emitted, map it to a direct slot read instead of failing. - [x] If `BoundLocalVarRef` is still emitted, map it to a direct slot read instead of failing.
- [x] Add a JVM test that exercises fast-local reads in a bytecode-compiled function. - [x] Add a JVM test that exercises fast-local reads in a bytecode-compiled function.
- [ ] Step 15: Class-scope `?=` in bytecode. - [x] Step 15: Class-scope `?=` in bytecode.
- [ ] Handle `C.x ?= v` and `C?.x ?= v` for class-scope members without falling back. - [x] Handle `C.x ?= v` and `C?.x ?= v` for class-scope members without falling back.
- [ ] Add a JVM test for class-scope `?=` on static vars. - [x] Add a JVM test for class-scope `?=` on static vars.
## Notes ## Notes

View File

@ -2220,6 +2220,20 @@ class BytecodeCompiler(
val newValue = compileRefWithFallback(ref.value, null, Pos.builtIn) ?: return null val newValue = compileRefWithFallback(ref.value, null, Pos.builtIn) ?: return null
when (target) { when (target) {
is ClassScopeMemberRef -> {
val className = target.ownerClassName()
val classSlot = compileRef(LocalVarRef(className, Pos.builtIn)) ?: run {
val cls = resolveTypeNameClass(className) ?: return null
val id = builder.addConst(BytecodeConst.ObjRef(cls))
val slot = allocSlot()
builder.emit(Opcode.CONST_OBJ, id, slot)
updateSlotType(slot, SlotType.OBJ)
CompiledValue(slot, SlotType.OBJ)
}
val classObj = ensureObjSlot(classSlot)
val nameId = builder.addConst(BytecodeConst.StringVal(target.name))
builder.emit(Opcode.SET_CLASS_SCOPE, classObj.slot, nameId, newValue.slot)
}
is LocalSlotRef -> { is LocalSlotRef -> {
if (!allowLocalSlots || !target.isMutable || target.isDelegated) return null if (!allowLocalSlots || !target.isMutable || target.isDelegated) return null
val slot = resolveSlot(target) ?: return null val slot = resolveSlot(target) ?: return null
@ -2244,7 +2258,24 @@ class BytecodeCompiler(
Pos.builtIn Pos.builtIn
) )
val receiver = compileRefWithFallback(target.target, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(target.target, null, Pos.builtIn) ?: return null
if (receiverClass == ObjDynamic.type) { if (isKnownClassReceiver(target.target)) {
val nameId = builder.addConst(BytecodeConst.StringVal(target.name))
if (!target.isOptional) {
builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, newValue.slot)
} else {
val recvNull = allocSlot()
builder.emit(Opcode.CONST_NULL, recvNull)
val recvCmp = allocSlot()
builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, recvNull, recvCmp)
val skipLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(recvCmp), CmdBuilder.Operand.LabelRef(skipLabel))
)
builder.emit(Opcode.SET_CLASS_SCOPE, receiver.slot, nameId, newValue.slot)
builder.mark(skipLabel)
}
} else if (receiverClass == ObjDynamic.type) {
val nameId = builder.addConst(BytecodeConst.StringVal(target.name)) val nameId = builder.addConst(BytecodeConst.StringVal(target.name))
if (!target.isOptional) { if (!target.isOptional) {
builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, newValue.slot) builder.emit(Opcode.SET_DYNAMIC_MEMBER, receiver.slot, nameId, newValue.slot)
@ -2266,55 +2297,56 @@ class BytecodeCompiler(
builder.mark(endLabel) builder.mark(endLabel)
updateSlotType(resultSlot, SlotType.OBJ) updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ) return CompiledValue(resultSlot, SlotType.OBJ)
}
val fieldId = receiverClass.instanceFieldIdMap()[target.name]
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[target.name]
if (fieldId != null || methodId != null) {
if (!target.isOptional) {
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, newValue.slot)
} else {
val recvNull = allocSlot()
builder.emit(Opcode.CONST_NULL, recvNull)
val recvCmp = allocSlot()
builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, recvNull, recvCmp)
val skipLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(recvCmp), CmdBuilder.Operand.LabelRef(skipLabel))
)
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, newValue.slot)
builder.mark(skipLabel)
}
} else { } else {
val extSlot = resolveExtensionSetterSlot(receiverClass, target.name) val fieldId = receiverClass.instanceFieldIdMap()[target.name]
?: throw BytecodeCompileException( val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[target.name]
"Unknown member ${target.name} on ${receiverClass.className}", if (fieldId != null || methodId != null) {
Pos.builtIn if (!target.isOptional) {
) builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, newValue.slot)
val callee = ensureObjSlot(extSlot) } else {
val receiverObj = ensureObjSlot(receiver) val recvNull = allocSlot()
val valueObj = ensureObjSlot(newValue) builder.emit(Opcode.CONST_NULL, recvNull)
val argSlots = intArrayOf(allocSlot(), allocSlot()) val recvCmp = allocSlot()
builder.emit(Opcode.MOVE_OBJ, receiverObj.slot, argSlots[0]) builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, recvNull, recvCmp)
builder.emit(Opcode.MOVE_OBJ, valueObj.slot, argSlots[1]) val skipLabel = builder.label()
updateSlotType(argSlots[0], SlotType.OBJ) builder.emit(
updateSlotType(argSlots[1], SlotType.OBJ) Opcode.JMP_IF_TRUE,
val callArgs = CallArgs(base = argSlots[0], count = argSlots.size, planId = null) listOf(CmdBuilder.Operand.IntVal(recvCmp), CmdBuilder.Operand.LabelRef(skipLabel))
val encodedCount = encodeCallArgCount(callArgs) ?: return null )
if (!target.isOptional) { builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, newValue.slot)
builder.emit(Opcode.CALL_SLOT, callee.slot, callArgs.base, encodedCount, resultSlot) builder.mark(skipLabel)
}
} else { } else {
val recvNull = allocSlot() val extSlot = resolveExtensionSetterSlot(receiverClass, target.name)
builder.emit(Opcode.CONST_NULL, recvNull) ?: throw BytecodeCompileException(
val recvCmp = allocSlot() "Unknown member ${target.name} on ${receiverClass.className}",
builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, recvNull, recvCmp) Pos.builtIn
val skipLabel = builder.label() )
builder.emit( val callee = ensureObjSlot(extSlot)
Opcode.JMP_IF_TRUE, val receiverObj = ensureObjSlot(receiver)
listOf(CmdBuilder.Operand.IntVal(recvCmp), CmdBuilder.Operand.LabelRef(skipLabel)) val valueObj = ensureObjSlot(newValue)
) val argSlots = intArrayOf(allocSlot(), allocSlot())
builder.emit(Opcode.CALL_SLOT, callee.slot, callArgs.base, encodedCount, resultSlot) builder.emit(Opcode.MOVE_OBJ, receiverObj.slot, argSlots[0])
builder.mark(skipLabel) builder.emit(Opcode.MOVE_OBJ, valueObj.slot, argSlots[1])
updateSlotType(argSlots[0], SlotType.OBJ)
updateSlotType(argSlots[1], SlotType.OBJ)
val callArgs = CallArgs(base = argSlots[0], count = argSlots.size, planId = null)
val encodedCount = encodeCallArgCount(callArgs) ?: return null
if (!target.isOptional) {
builder.emit(Opcode.CALL_SLOT, callee.slot, callArgs.base, encodedCount, resultSlot)
} else {
val recvNull = allocSlot()
builder.emit(Opcode.CONST_NULL, recvNull)
val recvCmp = allocSlot()
builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, recvNull, recvCmp)
val skipLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(recvCmp), CmdBuilder.Operand.LabelRef(skipLabel))
)
builder.emit(Opcode.CALL_SLOT, callee.slot, callArgs.base, encodedCount, resultSlot)
builder.mark(skipLabel)
}
} }
} }
} }

View File

@ -146,6 +146,19 @@ class BytecodeRecentOpsTest {
) )
} }
@Test
fun classScopeIfNullAssign() = runTest {
eval(
"""
class C { static var x: Object? = null }
C.x ?= 7
assertEquals(7, C.x)
C.x ?= 9
assertEquals(7, C.x)
""".trimIndent()
)
}
@Test @Test
fun qualifiedThisValueRef() = runTest { fun qualifiedThisValueRef() = runTest {
eval( eval(