implements PR-034
This commit is contained in:
parent
a9304f2515
commit
69da7a5924
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user