From 69da7a592449aefe7bd18fc05b1c474b1afefe73 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 7 Mar 2026 16:27:25 +0000 Subject: [PATCH] implements PR-034 --- .../backend/bytecode/BytecodeEmitter.java | 176 ++++++++++++++++++ .../bytecode/BytecodeMarshalingErrorCode.java | 2 + .../backend/bytecode/BytecodeEmitterTest.java | 127 +++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeEmitter.java create mode 100644 prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeEmitterTest.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 new file mode 100644 index 00000000..5296b598 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeEmitter.java @@ -0,0 +1,176 @@ +package p.studio.compiler.backend.bytecode; + +import p.studio.utilities.structures.ReadOnlyList; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +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_HOSTCALL = 0x71; + private static final int OP_SYSCALL = 0x70; + private static final int OP_INTRINSIC = 0x72; + + public BytecodeModule emit( + final EmissionPlan plan) { + final var inputPlan = plan == null ? EmissionPlan.empty() : plan; + final var functionFragments = new ArrayList(inputPlan.functions().size()); + final var orderedSyscalls = new LinkedHashMap(); + final var syscallIndexByIdentity = new LinkedHashMap(); + + for (final var function : inputPlan.functions()) { + final var code = new ByteArrayOutputStream(); + final var spans = new ArrayList(); + for (final var op : function.operations()) { + final var pc = code.size(); + switch (op.kind()) { + case HALT -> writeOpNoImm(code, OP_HALT); + case RET -> writeOpNoImm(code, OP_RET); + case INTRINSIC -> { + writeOpU32(code, OP_INTRINSIC, op.immediate()); + if (op.span() != null) { + spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, op.span())); + } + } + case HOSTCALL -> { + final var decl = Objects.requireNonNull(op.syscallDecl(), "syscallDecl"); + if (op.expectedArgSlots() != null && op.expectedArgSlots() != decl.argSlots()) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_HOST_ABI_MISMATCH, + "host arg_slots mismatch for " + decl.module() + "." + decl.name()); + } + if (op.expectedRetSlots() != null && op.expectedRetSlots() != decl.retSlots()) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_HOST_ABI_MISMATCH, + "host ret_slots mismatch for " + decl.module() + "." + decl.name()); + } + + final var identity = new SyscallIdentity(decl.module(), decl.name(), decl.version()); + if (!syscallIndexByIdentity.containsKey(identity)) { + syscallIndexByIdentity.put(identity, syscallIndexByIdentity.size()); + orderedSyscalls.put(identity, decl); + } + final var index = syscallIndexByIdentity.get(identity); + writeOpU32(code, OP_HOSTCALL, index); + if (op.span() != null) { + spans.add(new BytecodeFunctionLayoutBuilder.InstructionSpan(pc, op.span())); + } + } + case RAW_SYSCALL -> throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, + "raw syscall is forbidden in pre-load artifact"); + } + } + functionFragments.add(new BytecodeFunctionLayoutBuilder.FunctionFragment( + function.name(), + code.toByteArray(), + function.paramSlots(), + function.localSlots(), + function.returnSlots(), + function.maxStackSlots(), + ReadOnlyList.wrap(spans))); + } + + final var layout = BytecodeFunctionLayoutBuilder.build(ReadOnlyList.wrap(functionFragments)); + return new BytecodeModule( + inputPlan.version(), + inputPlan.constPool(), + layout.functions(), + layout.code(), + layout.debugInfo(), + inputPlan.exports(), + ReadOnlyList.wrap(orderedSyscalls.values())); + } + + private static void writeOpNoImm(final ByteArrayOutputStream out, final int opcode) { + out.writeBytes(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) opcode).array()); + } + + private static void writeOpU32(final ByteArrayOutputStream out, final int opcode, final int immediate) { + out.writeBytes(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) opcode).array()); + out.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(immediate).array()); + } + + private record SyscallIdentity( + String module, + String name, + int version) { + } + + public record EmissionPlan( + int version, + ReadOnlyList constPool, + ReadOnlyList exports, + ReadOnlyList functions) { + public EmissionPlan { + constPool = constPool == null ? ReadOnlyList.empty() : constPool; + exports = exports == null ? ReadOnlyList.empty() : exports; + functions = functions == null ? ReadOnlyList.empty() : functions; + } + + public static EmissionPlan empty() { + return new EmissionPlan(0, ReadOnlyList.empty(), ReadOnlyList.empty(), ReadOnlyList.empty()); + } + } + + public record FunctionPlan( + String name, + int paramSlots, + int localSlots, + int returnSlots, + int maxStackSlots, + ReadOnlyList operations) { + public FunctionPlan { + Objects.requireNonNull(name, "name"); + operations = operations == null ? ReadOnlyList.empty() : operations; + } + } + + public enum OperationKind { + HALT, + RET, + HOSTCALL, + RAW_SYSCALL, + INTRINSIC, + } + + public record Operation( + OperationKind kind, + int immediate, + BytecodeModule.SyscallDecl syscallDecl, + Integer expectedArgSlots, + Integer expectedRetSlots, + BytecodeModule.SourceSpan span) { + public Operation { + Objects.requireNonNull(kind, "kind"); + } + + public static Operation halt() { + return new Operation(OperationKind.HALT, 0, null, null, null, null); + } + + public static Operation ret() { + return new Operation(OperationKind.RET, 0, null, null, null, null); + } + + public static Operation intrinsic(final int intrinsicId) { + return new Operation(OperationKind.INTRINSIC, intrinsicId, null, null, null, null); + } + + public static Operation hostcall( + final BytecodeModule.SyscallDecl decl, + final Integer expectedArgSlots, + final Integer expectedRetSlots) { + return new Operation(OperationKind.HOSTCALL, 0, decl, expectedArgSlots, expectedRetSlots, null); + } + + 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/bytecode/BytecodeMarshalingErrorCode.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java index 23999e6c..b4597c45 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 @@ -5,4 +5,6 @@ public enum BytecodeMarshalingErrorCode { MARSHAL_FORMAT_SYSC_MODULE_TOO_LONG, MARSHAL_FORMAT_SYSC_NAME_TOO_LONG, MARSHAL_FORMAT_PC_SPAN_OUT_OF_BOUNDS, + MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, + MARSHAL_LINKAGE_HOST_ABI_MISMATCH, } 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 new file mode 100644 index 00000000..99502327 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeEmitterTest.java @@ -0,0 +1,127 @@ +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BytecodeEmitterTest { + + @Test + void emitMustDeduplicateSyscallsByCanonicalIdentityAndPreserveFirstOccurrenceOrder() { + final var declA = new BytecodeModule.SyscallDecl("gfx", "draw_line", 1, 2, 0); + final var declB = new BytecodeModule.SyscallDecl("audio", "play", 1, 1, 0); + 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, + 4, + ReadOnlyList.from( + BytecodeEmitter.Operation.hostcall(declA, 2, 0), + BytecodeEmitter.Operation.hostcall(declA, 2, 0), + BytecodeEmitter.Operation.halt())), + new BytecodeEmitter.FunctionPlan( + "aux", + 0, + 0, + 0, + 2, + ReadOnlyList.from( + BytecodeEmitter.Operation.hostcall(declB, 1, 0), + BytecodeEmitter.Operation.ret()))))); + + assertEquals(2, module.syscalls().size()); + assertEquals("gfx", module.syscalls().get(0).module()); + assertEquals("audio", module.syscalls().get(1).module()); + assertEquals(0x71, readU16(module.code(), 0)); + assertEquals(0, readU32(module.code(), 2)); + assertEquals(0x71, readU16(module.code(), 6)); + assertEquals(0, readU32(module.code(), 8)); + assertEquals(0x71, readU16(module.code(), 14)); + assertEquals(1, readU32(module.code(), 16)); + } + + @Test + void emitMustRejectRawSyscallInPreloadPlan() { + final var emitter = new BytecodeEmitter(); + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> emitter.emit(new BytecodeEmitter.EmissionPlan( + 0, + ReadOnlyList.empty(), + ReadOnlyList.empty(), + ReadOnlyList.from( + new BytecodeEmitter.FunctionPlan( + "main", + 0, + 0, + 0, + 1, + ReadOnlyList.from(BytecodeEmitter.Operation.rawSyscall(0x1001))))))); + + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, thrown.code()); + } + + @Test + void emitMustRejectAbiMismatch() { + final var declA = new BytecodeModule.SyscallDecl("gfx", "draw_line", 1, 2, 0); + final var emitter = new BytecodeEmitter(); + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> emitter.emit(new BytecodeEmitter.EmissionPlan( + 0, + ReadOnlyList.empty(), + ReadOnlyList.empty(), + ReadOnlyList.from( + new BytecodeEmitter.FunctionPlan( + "main", + 0, + 0, + 0, + 1, + ReadOnlyList.from(BytecodeEmitter.Operation.hostcall(declA, 1, 0))))))); + + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_HOST_ABI_MISMATCH, thrown.code()); + } + + @Test + void emitIntrinsicMustNotCreateSyscalls() { + 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, + 1, + ReadOnlyList.from( + BytecodeEmitter.Operation.intrinsic(0x2000), + BytecodeEmitter.Operation.halt()))))); + + assertTrue(module.syscalls().isEmpty()); + assertEquals(0x72, readU16(module.code(), 0)); + assertEquals(0x2000, readU32(module.code(), 2)); + } + + private static int readU16(final byte[] bytes, final int offset) { + return ByteBuffer.wrap(bytes, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF; + } + + private static int readU32(final byte[] bytes, final int offset) { + return ByteBuffer.wrap(bytes, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } +} +