implements PR-O3.1
This commit is contained in:
parent
ea27561e65
commit
a38fedc591
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user