implements PR-034

This commit is contained in:
bQUARKz 2026-03-07 16:27:25 +00:00
parent a9304f2515
commit 69da7a5924
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 305 additions and 0 deletions

View File

@ -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<BytecodeFunctionLayoutBuilder.FunctionFragment>(inputPlan.functions().size());
final var orderedSyscalls = new LinkedHashMap<SyscallIdentity, BytecodeModule.SyscallDecl>();
final var syscallIndexByIdentity = new LinkedHashMap<SyscallIdentity, Integer>();
for (final var function : inputPlan.functions()) {
final var code = new ByteArrayOutputStream();
final var spans = new ArrayList<BytecodeFunctionLayoutBuilder.InstructionSpan>();
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<BytecodeModule.ConstantPoolEntry> constPool,
ReadOnlyList<BytecodeModule.Export> exports,
ReadOnlyList<FunctionPlan> 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<Operation> 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);
}
}
}

View File

@ -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,
}

View File

@ -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();
}
}