diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java new file mode 100644 index 00000000..423e0a4d --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java @@ -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(); + 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 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 capabilities; + private final Map requiredCapabilityBySyscallIdentity; + + private RuntimePreloadCompatibilityChecker() { + this(Set.of("gfx", "input", "audio", "system"), Map.of()); + } + + private RuntimePreloadCompatibilityChecker( + final Set capabilities, + final Map 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(); + } + } +} +