implements PR-041

This commit is contained in:
bQUARKz 2026-03-07 17:09:33 +00:00
parent e1be8bbf49
commit f1a621c7fa
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8

View File

@ -0,0 +1,326 @@
package p.studio.compiler.integration;
import org.junit.jupiter.api.Test;
import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.backend.bytecode.BytecodeMarshalingErrorCode;
import p.studio.compiler.backend.bytecode.BytecodeMarshalingException;
import p.studio.compiler.backend.bytecode.BytecodeModule;
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.compiler.workspaces.stages.EmitBytecodePipelineStage;
import p.studio.compiler.workspaces.stages.LowerToIRVMPipelineStage;
import p.studio.compiler.workspaces.stages.OptimizeIRVMPipelineStage;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
class BackendGateIIntegrationTest {
@Test
void gateI_syscSectionPresentAndEmpty() {
final var module = emitFromBackend(backendWithSingleFunction(
fn("main", ReadOnlyList.from(ret()))));
assertTrue(module.syscalls().isEmpty());
assertEquals(CompatibilityError.NONE, new RuntimePreloadCompatibilityChecker().check(module).error());
}
@Test
void gateI_validHostcallPath() {
final var module = emitFromBackend(backendWithSingleFunction(
fn("main", ReadOnlyList.from(
callHost("gfx", "draw_pixel", 1, 2, 0),
ret()))));
final var check = new RuntimePreloadCompatibilityChecker().check(module);
assertEquals(CompatibilityError.NONE, check.error());
assertTrue(check.hostcallCount() > 0);
}
@Test
void gateI_rejectHostcallOutOfBounds() {
final var module = new BytecodeModule(
0,
ReadOnlyList.empty(),
ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 8, 0, 0, 0, 1)),
codeHostcall(1),
null,
ReadOnlyList.empty(),
ReadOnlyList.from(new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 1, 0)));
final var check = new RuntimePreloadCompatibilityChecker().check(module);
assertEquals(CompatibilityError.HOSTCALL_INDEX_OUT_OF_BOUNDS, check.error());
}
@Test
void gateI_rejectUnusedSyscallDeclaration() {
final var module = new BytecodeModule(
0,
ReadOnlyList.empty(),
ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)),
codeRet(),
null,
ReadOnlyList.empty(),
ReadOnlyList.from(new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 1, 0)));
final var check = new RuntimePreloadCompatibilityChecker().check(module);
assertEquals(CompatibilityError.UNUSED_SYSC_DECL, check.error());
}
@Test
void gateI_rejectRawSyscallInPreloadArtifact() {
final var module = new BytecodeModule(
0,
ReadOnlyList.empty(),
ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 6, 0, 0, 0, 1)),
codeRawSyscall(0x1001),
null,
ReadOnlyList.empty(),
ReadOnlyList.empty());
final var check = new RuntimePreloadCompatibilityChecker().check(module);
assertEquals(CompatibilityError.RAW_SYSCALL_IN_PRELOAD, check.error());
}
@Test
void gateI_rejectHostAbiMismatchAtEmitTime() {
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(
new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 2, 0),
1,
0),
BytecodeEmitter.Operation.ret()))))));
assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_HOST_ABI_MISMATCH, thrown.code());
}
@Test
void gateI_rejectMissingCapability() {
final var module = emitFromBackend(backendWithSingleFunction(
fn("main", ReadOnlyList.from(
callHost("gfx", "draw_pixel", 1, 2, 0),
ret()))));
final var caps = Set.of("input");
final var required = new HashMap<String, String>();
required.put("gfx::draw_pixel::1", "gfx");
final var check = new RuntimePreloadCompatibilityChecker(caps, required).check(module);
assertEquals(CompatibilityError.MISSING_CAPABILITY, check.error());
}
@Test
void gateI_validIntrinsicPath() {
final var module = emitFromBackend(backendWithSingleFunction(
fn("main", ReadOnlyList.from(
callIntrinsic("core.color.pack", 1, 0x2000),
ret()))));
final var check = new RuntimePreloadCompatibilityChecker().check(module);
assertEquals(CompatibilityError.NONE, check.error());
assertTrue(module.syscalls().isEmpty());
}
private BytecodeModule emitFromBackend(final IRBackend backend) {
final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, "."));
ctx.irBackend = backend;
final var lowerIssues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
assertFalse(lowerIssues.hasErrors(), "LowerToIRVM must succeed in fixture");
final var optimizeIssues = new OptimizeIRVMPipelineStage().run(ctx, LogAggregator.empty());
assertFalse(optimizeIssues.hasErrors(), "OptimizeIRVM must succeed in fixture");
final var emitIssues = new EmitBytecodePipelineStage().run(ctx, LogAggregator.empty());
assertFalse(emitIssues.hasErrors(), "EmitBytecode must succeed in fixture");
assertNotNull(ctx.bytecodeModule);
return ctx.bytecodeModule;
}
private IRBackend backendWithSingleFunction(final IRBackendExecutableFunction function) {
return IRBackend.builder()
.executableFunctions(ReadOnlyList.from(function))
.build();
}
private IRBackendExecutableFunction fn(
final String name,
final ReadOnlyList<IRBackendExecutableFunction.Instruction> instructions) {
return new IRBackendExecutableFunction(
new FileId(1),
"app/main",
name,
0,
10,
0,
0,
0,
4,
instructions,
Span.none());
}
private IRBackendExecutableFunction.Instruction ret() {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
"",
null,
null,
Span.none());
}
private 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 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());
}
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[] codeRawSyscall(final int id) {
final var out = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
out.putShort((short) 0x70);
out.putInt(id);
return out.array();
}
private byte[] codeRet() {
final var out = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
out.putShort((short) 0x51);
return out.array();
}
private enum CompatibilityError {
NONE,
RAW_SYSCALL_IN_PRELOAD,
HOSTCALL_INDEX_OUT_OF_BOUNDS,
UNUSED_SYSC_DECL,
MISSING_CAPABILITY,
}
private record CompatibilityResult(
CompatibilityError error,
int hostcallCount) {
}
private static final class RuntimePreloadCompatibilityChecker {
private final Set<String> capabilities;
private final Map<String, String> requiredCapabilityBySyscallIdentity;
private RuntimePreloadCompatibilityChecker() {
this(Set.of("gfx", "input", "audio", "system"), Map.of());
}
private RuntimePreloadCompatibilityChecker(
final Set<String> capabilities,
final Map<String, String> requiredCapabilityBySyscallIdentity) {
this.capabilities = capabilities == null ? Set.of() : new HashSet<>(capabilities);
this.requiredCapabilityBySyscallIdentity = requiredCapabilityBySyscallIdentity == null
? Map.of()
: new HashMap<>(requiredCapabilityBySyscallIdentity);
}
CompatibilityResult check(final BytecodeModule module) {
final var used = new boolean[module.syscalls().size()];
var hostcallCount = 0;
var pc = 0;
while (pc < module.code().length) {
final var opcode = readU16(module.code(), pc);
switch (opcode) {
case 0x70 -> {
return new CompatibilityResult(CompatibilityError.RAW_SYSCALL_IN_PRELOAD, hostcallCount);
}
case 0x71 -> {
final var syscIndex = readU32(module.code(), pc + 2);
if (syscIndex < 0 || syscIndex >= module.syscalls().size()) {
return new CompatibilityResult(CompatibilityError.HOSTCALL_INDEX_OUT_OF_BOUNDS, hostcallCount);
}
used[syscIndex] = true;
hostcallCount++;
final var decl = module.syscalls().get(syscIndex);
final var identity = decl.module() + "::" + decl.name() + "::" + decl.version();
final var requiredCap = requiredCapabilityBySyscallIdentity.get(identity);
if (requiredCap != null && !capabilities.contains(requiredCap)) {
return new CompatibilityResult(CompatibilityError.MISSING_CAPABILITY, hostcallCount);
}
pc += 6;
}
case 0x50, 0x72, 0x02, 0x03, 0x04, 0x10, 0x18, 0x40, 0x41, 0x42, 0x43, 0x56 -> pc += 6;
case 0x14, 0x15, 0x52, 0x54 -> pc += 10;
case 0x16 -> pc += 3;
default -> pc += 2;
}
}
for (var i = 0; i < used.length; i++) {
if (!used[i]) {
return new CompatibilityResult(CompatibilityError.UNUSED_SYSC_DECL, hostcallCount);
}
}
return new CompatibilityResult(CompatibilityError.NONE, hostcallCount);
}
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();
}
}
}