implements PR-040
This commit is contained in:
parent
4e2cad60cb
commit
e1be8bbf49
@ -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,
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
public enum IRVMLoweringErrorCode {
|
||||
LOWER_IRVM_EMPTY_EXECUTABLE_INPUT,
|
||||
LOWER_IRVM_MISSING_CALLEE,
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user