implements PR-040

This commit is contained in:
bQUARKz 2026-03-07 17:05:38 +00:00
parent 4e2cad60cb
commit e1be8bbf49
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 422 additions and 0 deletions

View File

@ -12,6 +12,7 @@ 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_CALL = 0x50;
private static final int OP_HOSTCALL = 0x71;
private static final int OP_SYSCALL = 0x70;
private static final int OP_INTRINSIC = 0x72;
@ -31,6 +32,7 @@ public class BytecodeEmitter {
switch (op.kind()) {
case HALT -> writeOpNoImm(code, OP_HALT);
case RET -> writeOpNoImm(code, OP_RET);
case CALL_FUNC -> writeOpU32(code, OP_CALL, op.immediate());
case INTRINSIC -> {
writeOpU32(code, OP_INTRINSIC, op.immediate());
if (op.span() != null) {
@ -134,6 +136,7 @@ public class BytecodeEmitter {
public enum OperationKind {
HALT,
RET,
CALL_FUNC,
HOSTCALL,
RAW_SYSCALL,
INTRINSIC,
@ -162,6 +165,10 @@ public class BytecodeEmitter {
return new Operation(OperationKind.INTRINSIC, intrinsicId, null, null, null, null);
}
public static Operation callFunc(final int funcId) {
return new Operation(OperationKind.CALL_FUNC, funcId, null, null, null, null);
}
public static Operation hostcall(
final BytecodeModule.SyscallDecl decl,
final Integer expectedArgSlots,

View File

@ -0,0 +1,7 @@
package p.studio.compiler.backend.irvm;
public enum IRVMLoweringErrorCode {
LOWER_IRVM_EMPTY_EXECUTABLE_INPUT,
LOWER_IRVM_MISSING_CALLEE,
}

View File

@ -0,0 +1,17 @@
package p.studio.compiler.backend.irvm;
public class IRVMLoweringException extends RuntimeException {
private final IRVMLoweringErrorCode code;
public IRVMLoweringException(
final IRVMLoweringErrorCode code,
final String message) {
super(message);
this.code = code;
}
public IRVMLoweringErrorCode code() {
return code;
}
}

View File

@ -0,0 +1,145 @@
package p.studio.compiler.backend.irvm;
import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
public class LowerToIRVMService {
private static final Comparator<IRBackendExecutableFunction> FUNCTION_ORDER = Comparator
.comparing(IRBackendExecutableFunction::moduleKey)
.thenComparing(IRBackendExecutableFunction::callableName)
.thenComparingInt(IRBackendExecutableFunction::sourceStart);
private final IRVMValidator validator;
public LowerToIRVMService() {
this(new IRVMValidator());
}
LowerToIRVMService(final IRVMValidator validator) {
this.validator = validator;
}
public IRVMProgram lower(final IRBackend backend) {
if (backend == null || backend.getExecutableFunctions().isEmpty()) {
throw new IRVMLoweringException(
IRVMLoweringErrorCode.LOWER_IRVM_EMPTY_EXECUTABLE_INPUT,
"IRBackend has no executable functions");
}
final var ordered = orderFunctions(backend.getExecutableFunctions());
final var funcIdByModuleAndName = new HashMap<String, Integer>();
for (var i = 0; i < ordered.size(); i++) {
final var fn = ordered.get(i);
final var key = callableKey(fn.moduleKey(), fn.callableName());
funcIdByModuleAndName.putIfAbsent(key, i);
}
final var loweredFunctions = new ArrayList<IRVMFunction>(ordered.size());
final var emissionFunctions = new ArrayList<BytecodeEmitter.FunctionPlan>(ordered.size());
for (var i = 0; i < ordered.size(); i++) {
final var fn = ordered.get(i);
final var instructions = new ArrayList<IRVMInstruction>(fn.instructions().size());
final var operations = new ArrayList<BytecodeEmitter.Operation>(fn.instructions().size());
for (final var instr : fn.instructions()) {
switch (instr.kind()) {
case HALT -> {
instructions.add(new IRVMInstruction(IRVMOp.HALT, null));
operations.add(BytecodeEmitter.Operation.halt());
}
case RET -> {
instructions.add(new IRVMInstruction(IRVMOp.RET, null));
operations.add(BytecodeEmitter.Operation.ret());
}
case CALL_FUNC -> {
final var key = callableKey(instr.calleeModuleKey(), instr.calleeCallableName());
final var calleeId = funcIdByModuleAndName.get(key);
if (calleeId == null) {
throw new IRVMLoweringException(
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
"missing callee function: " + key);
}
instructions.add(new IRVMInstruction(IRVMOp.CALL, calleeId));
operations.add(BytecodeEmitter.Operation.callFunc(calleeId));
}
case CALL_HOST -> {
final var host = instr.hostCall();
instructions.add(new IRVMInstruction(IRVMOp.HOSTCALL, 0));
operations.add(BytecodeEmitter.Operation.hostcall(
new p.studio.compiler.backend.bytecode.BytecodeModule.SyscallDecl(
host.module(),
host.name(),
(int) host.version(),
host.argSlots(),
host.retSlots()),
host.argSlots(),
host.retSlots()));
}
case CALL_INTRINSIC -> {
final var intrinsic = instr.intrinsicCall();
instructions.add(new IRVMInstruction(IRVMOp.INTRINSIC, intrinsic.intrinsicId()));
operations.add(BytecodeEmitter.Operation.intrinsic(intrinsic.intrinsicId()));
}
}
}
loweredFunctions.add(new IRVMFunction(
fn.callableName(),
fn.paramSlots(),
fn.localSlots(),
fn.returnSlots(),
fn.maxStackSlots(),
ReadOnlyList.wrap(instructions)));
emissionFunctions.add(new BytecodeEmitter.FunctionPlan(
fn.callableName(),
fn.paramSlots(),
fn.localSlots(),
fn.returnSlots(),
fn.maxStackSlots(),
ReadOnlyList.wrap(operations)));
}
final var module = new IRVMModule("core-v1", ReadOnlyList.wrap(loweredFunctions));
validator.validate(module, false);
return new IRVMProgram(
module,
new BytecodeEmitter.EmissionPlan(
0,
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.wrap(emissionFunctions)));
}
private ReadOnlyList<IRBackendExecutableFunction> orderFunctions(
final ReadOnlyList<IRBackendExecutableFunction> functions) {
final var sorted = new ArrayList<>(functions.asList());
sorted.sort(FUNCTION_ORDER);
var entrypoint = sorted.getFirst();
for (final var candidate : sorted) {
if ("main".equals(candidate.callableName())) {
entrypoint = candidate;
break;
}
}
final var ordered = new ArrayList<IRBackendExecutableFunction>(sorted.size());
ordered.add(entrypoint);
for (final var fn : sorted) {
if (fn != entrypoint) {
ordered.add(fn);
}
}
return ReadOnlyList.wrap(ordered);
}
private String callableKey(
final String moduleKey,
final String callableName) {
return (moduleKey == null ? "" : moduleKey) + "::" + callableName;
}
}

View File

@ -1,6 +1,9 @@
package p.studio.compiler.workspaces.stages;
import lombok.extern.slf4j.Slf4j;
import p.studio.compiler.backend.irvm.IRVMLoweringException;
import p.studio.compiler.backend.irvm.IRVMValidationException;
import p.studio.compiler.backend.irvm.LowerToIRVMService;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.workspaces.PipelineStage;
@ -8,8 +11,41 @@ import p.studio.utilities.logs.LogAggregator;
@Slf4j
public class LowerToIRVMPipelineStage implements PipelineStage {
private final LowerToIRVMService lowerToIRVMService;
public LowerToIRVMPipelineStage() {
this(new LowerToIRVMService());
}
LowerToIRVMPipelineStage(final LowerToIRVMService lowerToIRVMService) {
this.lowerToIRVMService = lowerToIRVMService;
}
@Override
public BuildingIssueSink run(BuilderPipelineContext ctx, LogAggregator logs) {
if (ctx.irBackend == null) {
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[BUILD]: IRBackend is missing before LowerToIRVM stage"));
}
try {
ctx.irvm = lowerToIRVMService.lower(ctx.irBackend);
} catch (IRVMLoweringException e) {
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[BUILD]: lower to irvm failed (%s): %s"
.formatted(e.code().name(), e.getMessage()))
.exception(e));
} catch (IRVMValidationException e) {
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[BUILD]: lower to irvm validation failed (%s): %s"
.formatted(e.code().name(), e.getMessage()))
.exception(e));
}
return BuildingIssueSink.empty();
}
}

View File

@ -0,0 +1,148 @@
package p.studio.compiler.backend.irvm;
import org.junit.jupiter.api.Test;
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.utilities.structures.ReadOnlyList;
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 LowerToIRVMServiceTest {
@Test
void lowerMustAssignEntrypointIdZeroAndSortRemainingDeterministically() {
final var backend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(
fn("aux", "app", ReadOnlyList.from(
callFunc("app", "main"),
ret())),
fn("main", "app", ReadOnlyList.from(
ret()))))
.build();
final var lowered = new LowerToIRVMService().lower(backend);
assertEquals(2, lowered.module().functions().size());
assertEquals("main", lowered.module().functions().get(0).name());
assertEquals("aux", lowered.module().functions().get(1).name());
assertEquals(IRVMOp.CALL, lowered.module().functions().get(1).instructions().get(0).op());
assertEquals(0, lowered.module().functions().get(1).instructions().get(0).immediate());
}
@Test
void lowerMustMapHostAndIntrinsicCallsites() {
final var backend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(
fn("main", "app", ReadOnlyList.from(
callHost("gfx", "draw_pixel", 1, 2, 0),
callIntrinsic("core.color.pack", 1, 0x2001),
ret()))))
.build();
final var lowered = new LowerToIRVMService().lower(backend);
final var instructions = lowered.module().functions().getFirst().instructions();
assertEquals(IRVMOp.HOSTCALL, instructions.get(0).op());
assertEquals(IRVMOp.INTRINSIC, instructions.get(1).op());
assertEquals(0x2001, instructions.get(1).immediate());
final var emissionOps = lowered.emissionPlan().functions().getFirst().operations();
assertEquals(p.studio.compiler.backend.bytecode.BytecodeEmitter.OperationKind.HOSTCALL, emissionOps.get(0).kind());
assertEquals(p.studio.compiler.backend.bytecode.BytecodeEmitter.OperationKind.INTRINSIC, emissionOps.get(1).kind());
}
@Test
void lowerMustRejectUnterminatedFunction() {
final var backend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(
fn("main", "app", ReadOnlyList.from(
callFunc("app", "main")))))
.build();
final var thrown = assertThrows(IRVMValidationException.class, () -> new LowerToIRVMService().lower(backend));
assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_UNTERMINATED_PATH, thrown.code());
}
@Test
void lowerMustRejectMissingCallee() {
final var backend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(
fn("main", "app", ReadOnlyList.from(
callFunc("app", "missing"),
ret()))))
.build();
final var thrown = assertThrows(IRVMLoweringException.class, () -> new LowerToIRVMService().lower(backend));
assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE, thrown.code());
}
private static IRBackendExecutableFunction fn(
final String name,
final String moduleKey,
final ReadOnlyList<IRBackendExecutableFunction.Instruction> instructions) {
return new IRBackendExecutableFunction(
new FileId(0),
moduleKey,
name,
0,
10,
0,
0,
0,
4,
instructions,
Span.none());
}
private static IRBackendExecutableFunction.Instruction ret() {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
"",
null,
null,
Span.none());
}
private static IRBackendExecutableFunction.Instruction callFunc(final String moduleKey, final String name) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
moduleKey,
name,
null,
null,
Span.none());
}
private static 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 static 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());
}
}

View File

@ -0,0 +1,62 @@
package p.studio.compiler.workspaces.stages;
import org.junit.jupiter.api.Test;
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.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class LowerToIRVMPipelineStageTest {
@Test
void runMustFailWhenIrBackendIsMissing() {
final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, "."));
final var stage = new LowerToIRVMPipelineStage();
final var issues = stage.run(ctx, LogAggregator.empty());
assertTrue(issues.hasErrors());
assertEquals(1, issues.size());
}
@Test
void runMustLowerExecutableFunctionsToIrvm() {
final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, "."));
ctx.irBackend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
new FileId(0),
"app",
"main",
0,
10,
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
"",
null,
null,
Span.none())),
Span.none())))
.build();
final var stage = new LowerToIRVMPipelineStage();
final var issues = stage.run(ctx, LogAggregator.empty());
assertTrue(!issues.hasErrors());
assertNotNull(ctx.irvm);
assertEquals(1, ctx.irvm.module().functions().size());
}
}