diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeEmitter.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeEmitter.java index 7a7e415f..0b1510ee 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeEmitter.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeEmitter.java @@ -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); } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java index c5b977d3..c665520a 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java @@ -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, } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java index 5a8d63da..dff1abbc 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java @@ -49,16 +49,21 @@ public class LowerToIRVMService { final var fn = ordered.get(i); final var instructions = new ArrayList(fn.instructions().size()); final var operations = new ArrayList(fn.instructions().size()); + final var labelToPc = new HashMap(); + final var jumpPatches = new ArrayList(); + 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) { + } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeEmitterTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeEmitterTest.java index c69e7555..197506e5 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeEmitterTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeEmitterTest.java @@ -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; } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java index 4c03fbc3..d76a5c28 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java @@ -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()); + } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java index 4880d5c2..69cfab36 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java @@ -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( diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java index 45b68589..743726a7 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java @@ -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(