From 5f78df750d3cc3078176e804255cf6688c1a38e3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 7 Mar 2026 18:10:39 +0000 Subject: [PATCH] implements PR-O2.1 --- .../bytecode/BytecodeLinkPrecheckService.java | 91 +++++++++++++++++++ .../bytecode/BytecodeMarshalingErrorCode.java | 4 + .../workspaces/BuilderPipelineService.java | 3 +- .../stages/LinkBytecodePipelineStage.java | 48 ++++++++++ .../BytecodeLinkPrecheckServiceTest.java | 77 ++++++++++++++++ .../BuilderPipelineServiceOrderTest.java | 5 +- .../stages/LinkBytecodePipelineStageTest.java | 58 ++++++++++++ 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckService.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStage.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckServiceTest.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStageTest.java diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckService.java new file mode 100644 index 00000000..6caaab7a --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckService.java @@ -0,0 +1,91 @@ +package p.studio.compiler.backend.bytecode; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class BytecodeLinkPrecheckService { + + public void validate(final BytecodeModule module) { + final var input = module == null ? BytecodeModule.empty() : module; + validateFunctionBounds(input); + validateExports(input); + validateHostcallIndices(input); + } + + private void validateFunctionBounds(final BytecodeModule module) { + var previousEnd = 0; + for (final var function : module.functions()) { + if (function.codeOffset() < 0 || function.codeLen() < 0) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_FUNCTION_BOUNDS_INVALID, + "negative function code bounds"); + } + if (function.codeOffset() < previousEnd) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_FUNCTION_BOUNDS_INVALID, + "function offsets must be monotonic"); + } + final var end = function.codeOffset() + function.codeLen(); + if (end < function.codeOffset() || end > module.code().length) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_FUNCTION_BOUNDS_INVALID, + "function code range exceeds module code length"); + } + previousEnd = end; + } + } + + private void validateExports(final BytecodeModule module) { + for (final var export : module.exports()) { + if (export.funcIdx() < 0 || export.funcIdx() >= module.functions().size()) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_EXPORT_FUNC_INDEX_INVALID, + "export references invalid func_idx: " + export.funcIdx()); + } + } + } + + private void validateHostcallIndices(final BytecodeModule module) { + var pc = 0; + while (pc < module.code().length) { + if (pc + 2 > module.code().length) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_TRUNCATED_INSTRUCTION, + "truncated opcode at pc=" + pc); + } + final var opcode = readU16(module.code(), pc); + final var size = sizeForOpcode(opcode); + if (pc + size > module.code().length) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_TRUNCATED_INSTRUCTION, + "truncated instruction at pc=" + pc); + } + if (opcode == 0x71) { + final var index = readU32(module.code(), pc + 2); + if (index < 0 || index >= module.syscalls().size()) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_HOSTCALL_INDEX_INVALID, + "hostcall index out of bounds: " + index); + } + } + pc += size; + } + } + + private int sizeForOpcode(final int opcode) { + return switch (opcode) { + case 0x50, 0x71, 0x72, 0x02, 0x03, 0x04, 0x10, 0x18, 0x40, 0x41, 0x42, 0x43, 0x56 -> 6; + case 0x14, 0x15, 0x52, 0x54 -> 10; + case 0x16 -> 3; + default -> 2; + }; + } + + private int readU16(final byte[] code, final int offset) { + return ByteBuffer.wrap(code, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF; + } + + private int readU32(final byte[] code, final int offset) { + return ByteBuffer.wrap(code, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java index b4597c45..35725683 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java @@ -7,4 +7,8 @@ public enum BytecodeMarshalingErrorCode { MARSHAL_FORMAT_PC_SPAN_OUT_OF_BOUNDS, MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, MARSHAL_LINKAGE_HOST_ABI_MISMATCH, + MARSHAL_VERIFY_PRECHECK_FUNCTION_BOUNDS_INVALID, + MARSHAL_VERIFY_PRECHECK_EXPORT_FUNC_INDEX_INVALID, + MARSHAL_VERIFY_PRECHECK_HOSTCALL_INDEX_INVALID, + MARSHAL_VERIFY_PRECHECK_TRUNCATED_INSTRUCTION, } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java index cb163940..4c0628ab 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java @@ -21,7 +21,8 @@ public class BuilderPipelineService { new FrontendPhasePipelineStage(), new LowerToIRVMPipelineStage(), new OptimizeIRVMPipelineStage(), - new EmitBytecodePipelineStage() + new EmitBytecodePipelineStage(), + new LinkBytecodePipelineStage() ); INSTANCE = new BuilderPipelineService(stages); } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStage.java new file mode 100644 index 00000000..1417234c --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStage.java @@ -0,0 +1,48 @@ +package p.studio.compiler.workspaces.stages; + +import lombok.extern.slf4j.Slf4j; +import p.studio.compiler.backend.bytecode.BytecodeLinkPrecheckService; +import p.studio.compiler.backend.bytecode.BytecodeMarshalingException; +import p.studio.compiler.messages.BuildingIssueSink; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.workspaces.PipelineStage; +import p.studio.utilities.logs.LogAggregator; + +@Slf4j +public class LinkBytecodePipelineStage implements PipelineStage { + private final BytecodeLinkPrecheckService linkPrecheckService; + + public LinkBytecodePipelineStage() { + this(new BytecodeLinkPrecheckService()); + } + + LinkBytecodePipelineStage(final BytecodeLinkPrecheckService linkPrecheckService) { + this.linkPrecheckService = linkPrecheckService; + } + + @Override + public BuildingIssueSink run(BuilderPipelineContext ctx, LogAggregator logs) { + if (ctx.bytecodeModule == null) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_LINK_PRECHECK") + .code("MARSHAL_FORMAT_STAGE_INPUT_MISSING") + .message("[BUILD]: bytecode module is missing before LinkBytecode stage")); + } + + try { + linkPrecheckService.validate(ctx.bytecodeModule); + } catch (BytecodeMarshalingException e) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_LINK_PRECHECK") + .code(e.code().name()) + .message("[BUILD]: bytecode link precheck failed: " + e.getMessage()) + .exception(e)); + } + + return BuildingIssueSink.empty(); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckServiceTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckServiceTest.java new file mode 100644 index 00000000..44246265 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeLinkPrecheckServiceTest.java @@ -0,0 +1,77 @@ +package p.studio.compiler.backend.bytecode; + +import org.junit.jupiter.api.Test; +import p.studio.utilities.structures.ReadOnlyList; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BytecodeLinkPrecheckServiceTest { + + private final BytecodeLinkPrecheckService service = new BytecodeLinkPrecheckService(); + + @Test + void validateMustAcceptWellFormedModule() { + final var module = new BytecodeEmitter().emit(new BytecodeEmitter.EmissionPlan( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.Export("main", 0)), + ReadOnlyList.from(new BytecodeEmitter.FunctionPlan( + "main", + 0, + 0, + 0, + 1, + ReadOnlyList.from(BytecodeEmitter.Operation.ret()))))); + + assertDoesNotThrow(() -> service.validate(module)); + } + + @Test + void validateMustRejectInvalidExportFunctionIndex() { + final var module = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)), + codeRet(), + null, + ReadOnlyList.from(new BytecodeModule.Export("main", 2)), + ReadOnlyList.empty()); + + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.validate(module)); + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_EXPORT_FUNC_INDEX_INVALID, thrown.code()); + } + + @Test + void validateMustRejectHostcallIndexOutOfBounds() { + final var module = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 8, 0, 0, 0, 1)), + codeHostcall(3), + null, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 0, 0))); + + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.validate(module)); + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_HOSTCALL_INDEX_INVALID, thrown.code()); + } + + private byte[] codeHostcall(final int index) { + final var out = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x71); + out.putInt(index); + out.putShort((short) 0x51); + return out.array(); + } + + private byte[] codeRet() { + final var out = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x51); + return out.array(); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java index 28ac2fd4..01539d02 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java @@ -3,6 +3,7 @@ package p.studio.compiler.workspaces; import org.junit.jupiter.api.Test; import p.studio.compiler.workspaces.stages.EmitBytecodePipelineStage; import p.studio.compiler.workspaces.stages.FrontendPhasePipelineStage; +import p.studio.compiler.workspaces.stages.LinkBytecodePipelineStage; import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage; import p.studio.compiler.workspaces.stages.LowerToIRVMPipelineStage; import p.studio.compiler.workspaces.stages.OptimizeIRVMPipelineStage; @@ -31,8 +32,8 @@ class BuilderPipelineServiceOrderTest { FrontendPhasePipelineStage.class, LowerToIRVMPipelineStage.class, OptimizeIRVMPipelineStage.class, - EmitBytecodePipelineStage.class), + EmitBytecodePipelineStage.class, + LinkBytecodePipelineStage.class), stageTypes); } } - diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStageTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStageTest.java new file mode 100644 index 00000000..76c64e98 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LinkBytecodePipelineStageTest.java @@ -0,0 +1,58 @@ +package p.studio.compiler.workspaces.stages; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.backend.bytecode.BytecodeModule; +import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.utilities.logs.LogAggregator; +import p.studio.utilities.structures.ReadOnlyList; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LinkBytecodePipelineStageTest { + + @Test + void runMustFailWhenBytecodeModuleIsMissing() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + final var stage = new LinkBytecodePipelineStage(); + + final var issues = stage.run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("BACKEND_LINK_PRECHECK", firstIssue.getPhase()); + assertEquals("MARSHAL_FORMAT_STAGE_INPUT_MISSING", firstIssue.getCode()); + } + + @Test + void runMustReportHostcallIndexPrecheckFailure() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.bytecodeModule = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 8, 0, 0, 0, 1)), + codeHostcall(1), + null, + ReadOnlyList.empty(), + ReadOnlyList.empty()); + + final var stage = new LinkBytecodePipelineStage(); + final var issues = stage.run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("MARSHAL_VERIFY_PRECHECK_HOSTCALL_INDEX_INVALID", firstIssue.getCode()); + } + + private byte[] codeHostcall(final int index) { + final var out = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x71); + out.putInt(index); + out.putShort((short) 0x51); + return out.array(); + } +}