From e1be8bbf49109019dafc97b9e2f3f98e1e502b5c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 7 Mar 2026 17:05:38 +0000 Subject: [PATCH] implements PR-040 --- .../backend/bytecode/BytecodeEmitter.java | 7 + .../backend/irvm/IRVMLoweringErrorCode.java | 7 + .../backend/irvm/IRVMLoweringException.java | 17 ++ .../backend/irvm/LowerToIRVMService.java | 145 +++++++++++++++++ .../stages/LowerToIRVMPipelineStage.java | 36 +++++ .../backend/irvm/LowerToIRVMServiceTest.java | 148 ++++++++++++++++++ .../stages/LowerToIRVMPipelineStageTest.java | 62 ++++++++ 7 files changed, 422 insertions(+) create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java 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 5296b598..4bf60375 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 @@ -12,6 +12,7 @@ import java.util.Objects; 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_HOSTCALL = 0x71; private static final int OP_SYSCALL = 0x70; private static final int OP_INTRINSIC = 0x72; @@ -31,6 +32,7 @@ public class BytecodeEmitter { switch (op.kind()) { case HALT -> writeOpNoImm(code, OP_HALT); case RET -> writeOpNoImm(code, OP_RET); + case CALL_FUNC -> writeOpU32(code, OP_CALL, op.immediate()); case INTRINSIC -> { writeOpU32(code, OP_INTRINSIC, op.immediate()); if (op.span() != null) { @@ -134,6 +136,7 @@ public class BytecodeEmitter { public enum OperationKind { HALT, RET, + CALL_FUNC, HOSTCALL, RAW_SYSCALL, INTRINSIC, @@ -162,6 +165,10 @@ public class BytecodeEmitter { return new Operation(OperationKind.INTRINSIC, intrinsicId, null, null, null, null); } + public static Operation callFunc(final int funcId) { + return new Operation(OperationKind.CALL_FUNC, funcId, null, null, null, null); + } + public static Operation hostcall( final BytecodeModule.SyscallDecl decl, final Integer expectedArgSlots, 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 new file mode 100644 index 00000000..c27ac894 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java @@ -0,0 +1,7 @@ +package p.studio.compiler.backend.irvm; + +public enum IRVMLoweringErrorCode { + LOWER_IRVM_EMPTY_EXECUTABLE_INPUT, + LOWER_IRVM_MISSING_CALLEE, +} + diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java new file mode 100644 index 00000000..e5b91baf --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java @@ -0,0 +1,17 @@ +package p.studio.compiler.backend.irvm; + +public class IRVMLoweringException extends RuntimeException { + private final IRVMLoweringErrorCode code; + + public IRVMLoweringException( + final IRVMLoweringErrorCode code, + final String message) { + super(message); + this.code = code; + } + + public IRVMLoweringErrorCode code() { + return code; + } +} + 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 new file mode 100644 index 00000000..2f9e23d4 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java @@ -0,0 +1,145 @@ +package p.studio.compiler.backend.irvm; + +import p.studio.compiler.backend.bytecode.BytecodeEmitter; +import p.studio.compiler.models.IRBackend; +import p.studio.compiler.models.IRBackendExecutableFunction; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; + +public class LowerToIRVMService { + private static final Comparator FUNCTION_ORDER = Comparator + .comparing(IRBackendExecutableFunction::moduleKey) + .thenComparing(IRBackendExecutableFunction::callableName) + .thenComparingInt(IRBackendExecutableFunction::sourceStart); + + private final IRVMValidator validator; + + public LowerToIRVMService() { + this(new IRVMValidator()); + } + + LowerToIRVMService(final IRVMValidator validator) { + this.validator = validator; + } + + public IRVMProgram lower(final IRBackend backend) { + if (backend == null || backend.getExecutableFunctions().isEmpty()) { + throw new IRVMLoweringException( + IRVMLoweringErrorCode.LOWER_IRVM_EMPTY_EXECUTABLE_INPUT, + "IRBackend has no executable functions"); + } + + final var ordered = orderFunctions(backend.getExecutableFunctions()); + final var funcIdByModuleAndName = new HashMap(); + for (var i = 0; i < ordered.size(); i++) { + final var fn = ordered.get(i); + final var key = callableKey(fn.moduleKey(), fn.callableName()); + funcIdByModuleAndName.putIfAbsent(key, i); + } + + final var loweredFunctions = new ArrayList(ordered.size()); + final var emissionFunctions = new ArrayList(ordered.size()); + + for (var i = 0; i < ordered.size(); i++) { + final var fn = ordered.get(i); + final var instructions = new ArrayList(fn.instructions().size()); + final var operations = new ArrayList(fn.instructions().size()); + for (final var instr : fn.instructions()) { + switch (instr.kind()) { + case HALT -> { + instructions.add(new IRVMInstruction(IRVMOp.HALT, null)); + operations.add(BytecodeEmitter.Operation.halt()); + } + case RET -> { + instructions.add(new IRVMInstruction(IRVMOp.RET, null)); + operations.add(BytecodeEmitter.Operation.ret()); + } + case CALL_FUNC -> { + final var key = callableKey(instr.calleeModuleKey(), instr.calleeCallableName()); + final var calleeId = funcIdByModuleAndName.get(key); + if (calleeId == null) { + throw new IRVMLoweringException( + IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE, + "missing callee function: " + key); + } + instructions.add(new IRVMInstruction(IRVMOp.CALL, calleeId)); + operations.add(BytecodeEmitter.Operation.callFunc(calleeId)); + } + case CALL_HOST -> { + final var host = instr.hostCall(); + instructions.add(new IRVMInstruction(IRVMOp.HOSTCALL, 0)); + operations.add(BytecodeEmitter.Operation.hostcall( + new p.studio.compiler.backend.bytecode.BytecodeModule.SyscallDecl( + host.module(), + host.name(), + (int) host.version(), + host.argSlots(), + host.retSlots()), + host.argSlots(), + host.retSlots())); + } + case CALL_INTRINSIC -> { + final var intrinsic = instr.intrinsicCall(); + instructions.add(new IRVMInstruction(IRVMOp.INTRINSIC, intrinsic.intrinsicId())); + operations.add(BytecodeEmitter.Operation.intrinsic(intrinsic.intrinsicId())); + } + } + } + loweredFunctions.add(new IRVMFunction( + fn.callableName(), + fn.paramSlots(), + fn.localSlots(), + fn.returnSlots(), + fn.maxStackSlots(), + ReadOnlyList.wrap(instructions))); + emissionFunctions.add(new BytecodeEmitter.FunctionPlan( + fn.callableName(), + fn.paramSlots(), + fn.localSlots(), + fn.returnSlots(), + fn.maxStackSlots(), + ReadOnlyList.wrap(operations))); + } + + final var module = new IRVMModule("core-v1", ReadOnlyList.wrap(loweredFunctions)); + validator.validate(module, false); + return new IRVMProgram( + module, + new BytecodeEmitter.EmissionPlan( + 0, + ReadOnlyList.empty(), + ReadOnlyList.empty(), + ReadOnlyList.wrap(emissionFunctions))); + } + + private ReadOnlyList orderFunctions( + final ReadOnlyList functions) { + final var sorted = new ArrayList<>(functions.asList()); + sorted.sort(FUNCTION_ORDER); + var entrypoint = sorted.getFirst(); + for (final var candidate : sorted) { + if ("main".equals(candidate.callableName())) { + entrypoint = candidate; + break; + } + } + final var ordered = new ArrayList(sorted.size()); + ordered.add(entrypoint); + for (final var fn : sorted) { + if (fn != entrypoint) { + ordered.add(fn); + } + } + return ReadOnlyList.wrap(ordered); + } + + private String callableKey( + final String moduleKey, + final String callableName) { + return (moduleKey == null ? "" : moduleKey) + "::" + callableName; + } +} + diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java index 828cf834..0f936f81 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java @@ -1,6 +1,9 @@ package p.studio.compiler.workspaces.stages; import lombok.extern.slf4j.Slf4j; +import p.studio.compiler.backend.irvm.IRVMLoweringException; +import p.studio.compiler.backend.irvm.IRVMValidationException; +import p.studio.compiler.backend.irvm.LowerToIRVMService; import p.studio.compiler.messages.BuildingIssueSink; import p.studio.compiler.models.BuilderPipelineContext; import p.studio.compiler.workspaces.PipelineStage; @@ -8,8 +11,41 @@ import p.studio.utilities.logs.LogAggregator; @Slf4j public class LowerToIRVMPipelineStage implements PipelineStage { + private final LowerToIRVMService lowerToIRVMService; + + public LowerToIRVMPipelineStage() { + this(new LowerToIRVMService()); + } + + LowerToIRVMPipelineStage(final LowerToIRVMService lowerToIRVMService) { + this.lowerToIRVMService = lowerToIRVMService; + } + @Override public BuildingIssueSink run(BuilderPipelineContext ctx, LogAggregator logs) { + if (ctx.irBackend == null) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .message("[BUILD]: IRBackend is missing before LowerToIRVM stage")); + } + try { + ctx.irvm = lowerToIRVMService.lower(ctx.irBackend); + } catch (IRVMLoweringException e) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .message("[BUILD]: lower to irvm failed (%s): %s" + .formatted(e.code().name(), e.getMessage())) + .exception(e)); + } catch (IRVMValidationException e) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .message("[BUILD]: lower to irvm validation failed (%s): %s" + .formatted(e.code().name(), e.getMessage())) + .exception(e)); + } return BuildingIssueSink.empty(); } } 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 new file mode 100644 index 00000000..20e887ac --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java @@ -0,0 +1,148 @@ +package p.studio.compiler.backend.irvm; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.models.IRBackend; +import p.studio.compiler.models.IRBackendExecutableFunction; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LowerToIRVMServiceTest { + + @Test + void lowerMustAssignEntrypointIdZeroAndSortRemainingDeterministically() { + final var backend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from( + fn("aux", "app", ReadOnlyList.from( + callFunc("app", "main"), + ret())), + fn("main", "app", ReadOnlyList.from( + ret())))) + .build(); + + final var lowered = new LowerToIRVMService().lower(backend); + + assertEquals(2, lowered.module().functions().size()); + assertEquals("main", lowered.module().functions().get(0).name()); + assertEquals("aux", lowered.module().functions().get(1).name()); + assertEquals(IRVMOp.CALL, lowered.module().functions().get(1).instructions().get(0).op()); + assertEquals(0, lowered.module().functions().get(1).instructions().get(0).immediate()); + } + + @Test + void lowerMustMapHostAndIntrinsicCallsites() { + final var backend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from( + fn("main", "app", ReadOnlyList.from( + callHost("gfx", "draw_pixel", 1, 2, 0), + callIntrinsic("core.color.pack", 1, 0x2001), + ret())))) + .build(); + + final var lowered = new LowerToIRVMService().lower(backend); + + final var instructions = lowered.module().functions().getFirst().instructions(); + assertEquals(IRVMOp.HOSTCALL, instructions.get(0).op()); + assertEquals(IRVMOp.INTRINSIC, instructions.get(1).op()); + assertEquals(0x2001, instructions.get(1).immediate()); + final var emissionOps = lowered.emissionPlan().functions().getFirst().operations(); + assertEquals(p.studio.compiler.backend.bytecode.BytecodeEmitter.OperationKind.HOSTCALL, emissionOps.get(0).kind()); + assertEquals(p.studio.compiler.backend.bytecode.BytecodeEmitter.OperationKind.INTRINSIC, emissionOps.get(1).kind()); + } + + @Test + void lowerMustRejectUnterminatedFunction() { + final var backend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from( + fn("main", "app", ReadOnlyList.from( + callFunc("app", "main"))))) + .build(); + + final var thrown = assertThrows(IRVMValidationException.class, () -> new LowerToIRVMService().lower(backend)); + assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_UNTERMINATED_PATH, thrown.code()); + } + + @Test + void lowerMustRejectMissingCallee() { + final var backend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from( + fn("main", "app", ReadOnlyList.from( + callFunc("app", "missing"), + ret())))) + .build(); + + final var thrown = assertThrows(IRVMLoweringException.class, () -> new LowerToIRVMService().lower(backend)); + assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE, thrown.code()); + } + + private static IRBackendExecutableFunction fn( + final String name, + final String moduleKey, + final ReadOnlyList instructions) { + return new IRBackendExecutableFunction( + new FileId(0), + moduleKey, + name, + 0, + 10, + 0, + 0, + 0, + 4, + instructions, + Span.none()); + } + + private static IRBackendExecutableFunction.Instruction ret() { + return new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.RET, + "", + "", + null, + null, + Span.none()); + } + + private static IRBackendExecutableFunction.Instruction callFunc(final String moduleKey, final String name) { + return new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.CALL_FUNC, + moduleKey, + name, + null, + null, + Span.none()); + } + + private static IRBackendExecutableFunction.Instruction callHost( + final String module, + final String name, + final long version, + final int argSlots, + final int retSlots) { + return new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.CALL_HOST, + "", + "", + new IRBackendExecutableFunction.HostCallMetadata(module, name, version, argSlots, retSlots), + null, + Span.none()); + } + + private static IRBackendExecutableFunction.Instruction callIntrinsic( + final String canonicalName, + final long canonicalVersion, + final int intrinsicId) { + return new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC, + "", + "", + null, + new IRBackendExecutableFunction.IntrinsicCallMetadata(canonicalName, canonicalVersion, intrinsicId), + Span.none()); + } +} + diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java new file mode 100644 index 00000000..b93df5a0 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java @@ -0,0 +1,62 @@ +package p.studio.compiler.workspaces.stages; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.models.IRBackend; +import p.studio.compiler.models.IRBackendExecutableFunction; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.logs.LogAggregator; +import p.studio.utilities.structures.ReadOnlyList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LowerToIRVMPipelineStageTest { + + @Test + void runMustFailWhenIrBackendIsMissing() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + final var stage = new LowerToIRVMPipelineStage(); + + final var issues = stage.run(ctx, LogAggregator.empty()); + + assertTrue(issues.hasErrors()); + assertEquals(1, issues.size()); + } + + @Test + void runMustLowerExecutableFunctionsToIrvm() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.irBackend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction( + new FileId(0), + "app", + "main", + 0, + 10, + 0, + 0, + 0, + 1, + ReadOnlyList.from(new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.RET, + "", + "", + null, + null, + Span.none())), + Span.none()))) + .build(); + final var stage = new LowerToIRVMPipelineStage(); + + final var issues = stage.run(ctx, LogAggregator.empty()); + + assertTrue(!issues.hasErrors()); + assertNotNull(ctx.irvm); + assertEquals(1, ctx.irvm.module().functions().size()); + } +} +