implements PR-O3.1

This commit is contained in:
bQUARKz 2026-03-07 18:29:01 +00:00
parent ea27561e65
commit a38fedc591
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 251 additions and 1 deletions

View File

@ -13,6 +13,9 @@ public class BytecodeEmitter {
private static final int OP_HALT = 0x01;
private static final int OP_RET = 0x51;
private static final int OP_CALL = 0x50;
private static final int OP_JMP = 0x02;
private static final int OP_JMP_IF_FALSE = 0x03;
private static final int OP_JMP_IF_TRUE = 0x04;
private static final int OP_HOSTCALL = 0x71;
private static final int OP_SYSCALL = 0x70;
private static final int OP_INTRINSIC = 0x72;
@ -42,6 +45,18 @@ public class BytecodeEmitter {
writeOpU32(code, OP_CALL, op.immediate());
spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, spanOrUnknown(op.span())));
}
case JMP -> {
writeOpU32(code, OP_JMP, op.immediate());
spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, spanOrUnknown(op.span())));
}
case JMP_IF_TRUE -> {
writeOpU32(code, OP_JMP_IF_TRUE, op.immediate());
spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, spanOrUnknown(op.span())));
}
case JMP_IF_FALSE -> {
writeOpU32(code, OP_JMP_IF_FALSE, op.immediate());
spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, spanOrUnknown(op.span())));
}
case INTRINSIC -> {
writeOpU32(code, OP_INTRINSIC, op.immediate());
spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, spanOrUnknown(op.span())));
@ -149,6 +164,9 @@ public class BytecodeEmitter {
HALT,
RET,
CALL_FUNC,
JMP,
JMP_IF_TRUE,
JMP_IF_FALSE,
HOSTCALL,
RAW_SYSCALL,
INTRINSIC,
@ -212,6 +230,18 @@ public class BytecodeEmitter {
return new Operation(OperationKind.HOSTCALL, 0, decl, expectedArgSlots, expectedRetSlots, span);
}
public static Operation jmp(final int targetPc, final BytecodeModule.SourceSpan span) {
return new Operation(OperationKind.JMP, targetPc, null, null, null, span);
}
public static Operation jmpIfTrue(final int targetPc, final BytecodeModule.SourceSpan span) {
return new Operation(OperationKind.JMP_IF_TRUE, targetPc, null, null, null, span);
}
public static Operation jmpIfFalse(final int targetPc, final BytecodeModule.SourceSpan span) {
return new Operation(OperationKind.JMP_IF_FALSE, targetPc, null, null, null, span);
}
public static Operation rawSyscall(final int syscallId) {
return new Operation(OperationKind.RAW_SYSCALL, syscallId, null, null, null, null);
}

View File

@ -5,4 +5,5 @@ public enum IRVMLoweringErrorCode {
LOWER_IRVM_MISSING_CALLEE,
LOWER_IRVM_CALL_ARG_SLOTS_MISMATCH,
LOWER_IRVM_CALL_RET_SLOTS_MISMATCH,
LOWER_IRVM_MISSING_JUMP_TARGET,
}

View File

@ -49,16 +49,21 @@ public class LowerToIRVMService {
final var fn = ordered.get(i);
final var instructions = new ArrayList<IRVMInstruction>(fn.instructions().size());
final var operations = new ArrayList<BytecodeEmitter.Operation>(fn.instructions().size());
final var labelToPc = new HashMap<String, Integer>();
final var jumpPatches = new ArrayList<JumpPatch>();
var functionPc = 0;
for (final var instr : fn.instructions()) {
final var sourceSpan = toBytecodeSpan(fn.fileId().getId(), instr.span());
switch (instr.kind()) {
case HALT -> {
instructions.add(new IRVMInstruction(IRVMOp.HALT, null));
operations.add(BytecodeEmitter.Operation.halt(sourceSpan));
functionPc += IRVMOp.HALT.immediateSize() + 2;
}
case RET -> {
instructions.add(new IRVMInstruction(IRVMOp.RET, null));
operations.add(BytecodeEmitter.Operation.ret(sourceSpan));
functionPc += IRVMOp.RET.immediateSize() + 2;
}
case CALL_FUNC -> {
final var key = callableKey(instr.calleeModuleKey(), instr.calleeCallableName());
@ -89,6 +94,7 @@ public class LowerToIRVMService {
}
instructions.add(new IRVMInstruction(IRVMOp.CALL, calleeId));
operations.add(BytecodeEmitter.Operation.callFunc(calleeId, sourceSpan));
functionPc += IRVMOp.CALL.immediateSize() + 2;
}
case CALL_HOST -> {
final var host = instr.hostCall();
@ -103,14 +109,62 @@ public class LowerToIRVMService {
host.argSlots(),
host.retSlots(),
sourceSpan));
functionPc += IRVMOp.HOSTCALL.immediateSize() + 2;
}
case CALL_INTRINSIC -> {
final var intrinsic = instr.intrinsicCall();
instructions.add(new IRVMInstruction(IRVMOp.INTRINSIC, intrinsic.intrinsicId()));
operations.add(BytecodeEmitter.Operation.intrinsic(intrinsic.intrinsicId(), sourceSpan));
functionPc += IRVMOp.INTRINSIC.immediateSize() + 2;
}
case LABEL -> {
final var label = instr.label();
if (labelToPc.putIfAbsent(label, functionPc) != null) {
throw new IRVMLoweringException(
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET,
"duplicate label in function '" + fn.callableName() + "': " + label);
}
}
case JMP -> {
instructions.add(new IRVMInstruction(IRVMOp.JMP, 0));
operations.add(BytecodeEmitter.Operation.jmp(0, sourceSpan));
jumpPatches.add(new JumpPatch(instructions.size() - 1, operations.size() - 1, instr.targetLabel(), IRVMOp.JMP, sourceSpan));
functionPc += IRVMOp.JMP.immediateSize() + 2;
}
case JMP_IF_TRUE -> {
instructions.add(new IRVMInstruction(IRVMOp.JMP_IF_TRUE, 0));
operations.add(BytecodeEmitter.Operation.jmpIfTrue(0, sourceSpan));
jumpPatches.add(new JumpPatch(instructions.size() - 1, operations.size() - 1, instr.targetLabel(), IRVMOp.JMP_IF_TRUE, sourceSpan));
functionPc += IRVMOp.JMP_IF_TRUE.immediateSize() + 2;
}
case JMP_IF_FALSE -> {
instructions.add(new IRVMInstruction(IRVMOp.JMP_IF_FALSE, 0));
operations.add(BytecodeEmitter.Operation.jmpIfFalse(0, sourceSpan));
jumpPatches.add(new JumpPatch(instructions.size() - 1, operations.size() - 1, instr.targetLabel(), IRVMOp.JMP_IF_FALSE, sourceSpan));
functionPc += IRVMOp.JMP_IF_FALSE.immediateSize() + 2;
}
}
}
for (final var patch : jumpPatches) {
final var targetPc = labelToPc.get(patch.targetLabel());
if (targetPc == null) {
throw new IRVMLoweringException(
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET,
"missing jump target label '" + patch.targetLabel() + "' in function '" + fn.callableName() + "'");
}
instructions.set(patch.instructionIndex(), new IRVMInstruction(patch.op(), targetPc));
final BytecodeEmitter.Operation operation;
if (patch.op() == IRVMOp.JMP) {
operation = BytecodeEmitter.Operation.jmp(targetPc, patch.span());
} else if (patch.op() == IRVMOp.JMP_IF_TRUE) {
operation = BytecodeEmitter.Operation.jmpIfTrue(targetPc, patch.span());
} else if (patch.op() == IRVMOp.JMP_IF_FALSE) {
operation = BytecodeEmitter.Operation.jmpIfFalse(targetPc, patch.span());
} else {
throw new IllegalStateException("unexpected jump op: " + patch.op().name());
}
operations.set(patch.operationIndex(), operation);
}
loweredFunctions.add(new IRVMFunction(
fn.callableName(),
fn.paramSlots(),
@ -187,4 +241,12 @@ public class LowerToIRVMService {
}
return (int) value;
}
private record JumpPatch(
int instructionIndex,
int operationIndex,
String targetLabel,
IRVMOp op,
BytecodeModule.SourceSpan span) {
}
}

View File

@ -143,6 +143,30 @@ class BytecodeEmitterTest {
assertEquals(14, module.debugInfo().pcToSpan().get(3).pc());
}
@Test
void emitMustEncodeJumpOpcodesWithU32Immediate() {
final var emitter = new BytecodeEmitter();
final var module = emitter.emit(new BytecodeEmitter.EmissionPlan(
0,
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.from(new BytecodeEmitter.FunctionPlan(
"main",
0,
0,
0,
2,
ReadOnlyList.from(
BytecodeEmitter.Operation.jmp(12, new BytecodeModule.SourceSpan(1, 0, 1)),
BytecodeEmitter.Operation.jmpIfFalse(2, new BytecodeModule.SourceSpan(1, 2, 3)),
BytecodeEmitter.Operation.ret(new BytecodeModule.SourceSpan(1, 4, 5)))))));
assertEquals(0x02, readU16(module.code(), 0));
assertEquals(12, readU32(module.code(), 2));
assertEquals(0x03, readU16(module.code(), 6));
assertEquals(2, readU32(module.code(), 8));
}
private static int readU16(final byte[] bytes, final int offset) {
return ByteBuffer.wrap(bytes, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
}

View File

@ -94,6 +94,38 @@ class LowerToIRVMServiceTest {
assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_CALL_ARG_SLOTS_MISMATCH, thrown.code());
}
@Test
void lowerMustResolveJumpTargetsFromLabels() {
final var backend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(
fn("main", "app", ReadOnlyList.from(
label("entry"),
jmp("exit"),
label("exit"),
ret()))))
.build();
final var lowered = new LowerToIRVMService().lower(backend);
final var emitted = lowered.module().functions().getFirst().instructions();
assertEquals(IRVMOp.JMP, emitted.get(0).op());
assertEquals(6, emitted.get(0).immediate());
assertEquals(IRVMOp.RET, emitted.get(1).op());
}
@Test
void lowerMustRejectMissingJumpTargetLabel() {
final var backend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(
fn("main", "app", ReadOnlyList.from(
jmp("missing"),
ret()))))
.build();
final var thrown = assertThrows(IRVMLoweringException.class, () -> new LowerToIRVMService().lower(backend));
assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET, thrown.code());
}
private static IRBackendExecutableFunction fn(
final String name,
final String moduleKey,
@ -175,4 +207,32 @@ class LowerToIRVMServiceTest {
new IRBackendExecutableFunction.IntrinsicCallMetadata(canonicalName, canonicalVersion, intrinsicId),
Span.none());
}
private static IRBackendExecutableFunction.Instruction label(final String label) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.LABEL,
"",
"",
null,
null,
label,
"",
null,
null,
Span.none());
}
private static IRBackendExecutableFunction.Instruction jmp(final String target) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.JMP,
"",
"",
null,
null,
"",
target,
null,
null,
Span.none());
}
}

View File

@ -48,6 +48,8 @@ public record IRBackendExecutableFunction(
String calleeCallableName,
HostCallMetadata hostCall,
IntrinsicCallMetadata intrinsicCall,
String label,
String targetLabel,
Integer expectedArgSlots,
Integer expectedRetSlots,
Span span) {
@ -58,7 +60,19 @@ public record IRBackendExecutableFunction(
final HostCallMetadata hostCall,
final IntrinsicCallMetadata intrinsicCall,
final Span span) {
this(kind, calleeModuleKey, calleeCallableName, hostCall, intrinsicCall, null, null, span);
this(kind, calleeModuleKey, calleeCallableName, hostCall, intrinsicCall, null, null, null, null, span);
}
public Instruction(
final InstructionKind kind,
final String calleeModuleKey,
final String calleeCallableName,
final HostCallMetadata hostCall,
final IntrinsicCallMetadata intrinsicCall,
final Integer expectedArgSlots,
final Integer expectedRetSlots,
final Span span) {
this(kind, calleeModuleKey, calleeCallableName, hostCall, intrinsicCall, null, null, expectedArgSlots, expectedRetSlots, span);
}
public Instruction {
@ -66,6 +80,8 @@ public record IRBackendExecutableFunction(
span = span == null ? Span.none() : span;
calleeModuleKey = calleeModuleKey == null ? "" : calleeModuleKey;
calleeCallableName = calleeCallableName == null ? "" : calleeCallableName;
label = label == null ? "" : label;
targetLabel = targetLabel == null ? "" : targetLabel;
if (expectedArgSlots != null && expectedArgSlots < 0) {
throw new IllegalArgumentException("expectedArgSlots must be non-negative");
}
@ -105,6 +121,32 @@ public record IRBackendExecutableFunction(
throw new IllegalArgumentException(kind + " must not carry expected slot metadata");
}
}
case LABEL -> {
if (label.isBlank()) {
throw new IllegalArgumentException("LABEL requires label");
}
if (!targetLabel.isBlank()
|| !calleeCallableName.isBlank()
|| hostCall != null
|| intrinsicCall != null
|| expectedArgSlots != null
|| expectedRetSlots != null) {
throw new IllegalArgumentException("LABEL must not carry call metadata");
}
}
case JMP, JMP_IF_TRUE, JMP_IF_FALSE -> {
if (targetLabel.isBlank()) {
throw new IllegalArgumentException(kind + " requires targetLabel");
}
if (!label.isBlank()
|| !calleeCallableName.isBlank()
|| hostCall != null
|| intrinsicCall != null
|| expectedArgSlots != null
|| expectedRetSlots != null) {
throw new IllegalArgumentException(kind + " must not carry call metadata");
}
}
}
}
}
@ -115,6 +157,10 @@ public record IRBackendExecutableFunction(
CALL_FUNC,
CALL_HOST,
CALL_INTRINSIC,
LABEL,
JMP,
JMP_IF_TRUE,
JMP_IF_FALSE,
}
public record HostCallMetadata(

View File

@ -79,6 +79,33 @@ class IRBackendExecutableContractTest {
0));
}
@Test
void jumpAndLabelContractMustRequireNames() {
assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.LABEL,
"",
"",
null,
null,
"",
"",
null,
null,
Span.none()));
assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.JMP,
"",
"",
null,
null,
"",
"",
null,
null,
Span.none()));
}
@Test
void aggregatorMustPreserveExecutableFunctionOrderDeterministically() {
final var fileA = new IRBackendFile(