implements PR-036
This commit is contained in:
parent
68bd72bb21
commit
e6bd796f4f
@ -0,0 +1,19 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IRVMFunction(
|
||||
String name,
|
||||
int paramSlots,
|
||||
int localSlots,
|
||||
int returnSlots,
|
||||
int maxStackSlots,
|
||||
ReadOnlyList<IRVMInstruction> instructions) {
|
||||
public IRVMFunction {
|
||||
Objects.requireNonNull(name, "name");
|
||||
instructions = instructions == null ? ReadOnlyList.empty() : instructions;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IRVMInstruction(
|
||||
IRVMOp op,
|
||||
Integer immediate) {
|
||||
public IRVMInstruction {
|
||||
Objects.requireNonNull(op, "op");
|
||||
if (op.immediateSize() > 0 && immediate == null) {
|
||||
throw new IllegalArgumentException("immediate is required for opcode: " + op.name());
|
||||
}
|
||||
if (op.immediateSize() == 0 && immediate != null) {
|
||||
throw new IllegalArgumentException("immediate is not allowed for opcode: " + op.name());
|
||||
}
|
||||
}
|
||||
|
||||
public int encodedSize() {
|
||||
return 2 + op.immediateSize();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
public record IRVMModule(
|
||||
String vmProfile,
|
||||
ReadOnlyList<IRVMFunction> functions) {
|
||||
public IRVMModule {
|
||||
vmProfile = vmProfile == null || vmProfile.isBlank() ? "core-v1" : vmProfile;
|
||||
functions = functions == null ? ReadOnlyList.empty() : functions;
|
||||
}
|
||||
|
||||
public static IRVMModule empty() {
|
||||
return new IRVMModule("core-v1", ReadOnlyList.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
public record IRVMOp(
|
||||
String name,
|
||||
int opcode,
|
||||
int immediateSize,
|
||||
int pops,
|
||||
int pushes,
|
||||
boolean branch,
|
||||
boolean terminator,
|
||||
boolean internal) {
|
||||
|
||||
public static final IRVMOp HALT = new IRVMOp("HALT", 0x01, 0, 0, 0, false, true, false);
|
||||
public static final IRVMOp RET = new IRVMOp("RET", 0x51, 0, 0, 0, false, true, false);
|
||||
public static final IRVMOp CALL = new IRVMOp("CALL", 0x50, 4, 0, 0, false, false, false);
|
||||
public static final IRVMOp JMP = new IRVMOp("JMP", 0x02, 4, 0, 0, true, true, false);
|
||||
public static final IRVMOp JMP_IF_TRUE = new IRVMOp("JMP_IF_TRUE", 0x04, 4, 1, 0, true, false, false);
|
||||
public static final IRVMOp JMP_IF_FALSE = new IRVMOp("JMP_IF_FALSE", 0x03, 4, 1, 0, true, false, false);
|
||||
public static final IRVMOp HOSTCALL = new IRVMOp("HOSTCALL", 0x71, 4, 0, 0, false, false, false);
|
||||
public static final IRVMOp INTRINSIC = new IRVMOp("INTRINSIC", 0x72, 4, 0, 0, false, false, false);
|
||||
public static final IRVMOp PUSH_I32 = new IRVMOp("PUSH_I32", 0x17, 4, 0, 1, false, false, false);
|
||||
public static final IRVMOp INTERNAL_EXT = new IRVMOp("IRVM_EXT_NOP", 0xFFFF, 0, 0, 0, false, false, true);
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
public enum IRVMValidationErrorCode {
|
||||
MARSHAL_VERIFY_PRECHECK_INVALID_JUMP_TARGET,
|
||||
MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW,
|
||||
MARSHAL_VERIFY_PRECHECK_STACK_OVERFLOW,
|
||||
MARSHAL_VERIFY_PRECHECK_STACK_MISMATCH_JOIN,
|
||||
MARSHAL_VERIFY_PRECHECK_BAD_RET_STACK_HEIGHT,
|
||||
MARSHAL_VERIFY_PRECHECK_INVALID_FUNC_ID,
|
||||
MARSHAL_VERIFY_PRECHECK_UNTERMINATED_PATH,
|
||||
MARSHAL_VERIFY_PRECHECK_INTERNAL_OPCODE_RESIDUAL,
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
public class IRVMValidationException extends RuntimeException {
|
||||
private final IRVMValidationErrorCode code;
|
||||
private final int functionIndex;
|
||||
private final int pc;
|
||||
|
||||
public IRVMValidationException(
|
||||
final IRVMValidationErrorCode code,
|
||||
final String message,
|
||||
final int functionIndex,
|
||||
final int pc) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.functionIndex = functionIndex;
|
||||
this.pc = pc;
|
||||
}
|
||||
|
||||
public IRVMValidationErrorCode code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public int functionIndex() {
|
||||
return functionIndex;
|
||||
}
|
||||
|
||||
public int pc() {
|
||||
return pc;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,225 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class IRVMValidator {
|
||||
|
||||
public void validate(
|
||||
final IRVMModule module,
|
||||
final boolean rejectInternalOpcodes) {
|
||||
final var input = module == null ? IRVMModule.empty() : module;
|
||||
for (var functionIndex = 0; functionIndex < input.functions().size(); functionIndex++) {
|
||||
validateFunction(input, functionIndex, rejectInternalOpcodes);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFunction(
|
||||
final IRVMModule module,
|
||||
final int functionIndex,
|
||||
final boolean rejectInternalOpcodes) {
|
||||
final var function = module.functions().get(functionIndex);
|
||||
final var instructions = function.instructions();
|
||||
if (instructions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var pcByIndex = new int[instructions.size()];
|
||||
final var indexByPc = new HashMap<Integer, Integer>();
|
||||
var pc = 0;
|
||||
for (var i = 0; i < instructions.size(); i++) {
|
||||
final var instr = instructions.get(i);
|
||||
pcByIndex[i] = pc;
|
||||
indexByPc.put(pc, i);
|
||||
pc += instr.encodedSize();
|
||||
if (rejectInternalOpcodes && instr.op().internal()) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INTERNAL_OPCODE_RESIDUAL,
|
||||
"internal opcode must be eliminated before emission",
|
||||
functionIndex,
|
||||
pcByIndex[i]);
|
||||
}
|
||||
}
|
||||
|
||||
final var worklist = new ArrayDeque<Integer>();
|
||||
final var incomingHeights = new HashMap<Integer, Integer>();
|
||||
worklist.add(0);
|
||||
incomingHeights.put(0, 0);
|
||||
final var visited = new HashSet<Integer>();
|
||||
|
||||
while (!worklist.isEmpty()) {
|
||||
final var index = worklist.removeFirst();
|
||||
visited.add(index);
|
||||
final var instr = instructions.get(index);
|
||||
final var instrPc = pcByIndex[index];
|
||||
var inHeight = incomingHeights.get(index);
|
||||
|
||||
if (instr.op() == IRVMOp.CALL) {
|
||||
final var targetFn = instr.immediate();
|
||||
if (targetFn == null || targetFn < 0 || targetFn >= module.functions().size()) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INVALID_FUNC_ID,
|
||||
"invalid callee function id: " + targetFn,
|
||||
functionIndex,
|
||||
instrPc);
|
||||
}
|
||||
final var callee = module.functions().get(targetFn);
|
||||
final var outHeight = applyStackEffect(
|
||||
inHeight,
|
||||
callee.paramSlots(),
|
||||
callee.returnSlots(),
|
||||
function.maxStackSlots(),
|
||||
functionIndex,
|
||||
instrPc);
|
||||
enqueueSuccessors(
|
||||
module,
|
||||
functionIndex,
|
||||
function,
|
||||
index,
|
||||
instr,
|
||||
instrPc,
|
||||
pcByIndex,
|
||||
indexByPc,
|
||||
outHeight,
|
||||
incomingHeights,
|
||||
worklist);
|
||||
continue;
|
||||
}
|
||||
|
||||
final var outHeight = applyStackEffect(
|
||||
inHeight,
|
||||
instr.op().pops(),
|
||||
instr.op().pushes(),
|
||||
function.maxStackSlots(),
|
||||
functionIndex,
|
||||
instrPc);
|
||||
if (instr.op() == IRVMOp.RET && outHeight != function.returnSlots()) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_BAD_RET_STACK_HEIGHT,
|
||||
"ret stack height mismatch. expected=%d actual=%d".formatted(function.returnSlots(), outHeight),
|
||||
functionIndex,
|
||||
instrPc);
|
||||
}
|
||||
enqueueSuccessors(
|
||||
module,
|
||||
functionIndex,
|
||||
function,
|
||||
index,
|
||||
instr,
|
||||
instrPc,
|
||||
pcByIndex,
|
||||
indexByPc,
|
||||
outHeight,
|
||||
incomingHeights,
|
||||
worklist);
|
||||
}
|
||||
|
||||
if (visited.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private int applyStackEffect(
|
||||
final int inHeight,
|
||||
final int pops,
|
||||
final int pushes,
|
||||
final int maxStackSlots,
|
||||
final int functionIndex,
|
||||
final int pc) {
|
||||
if (inHeight < pops) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW,
|
||||
"stack underflow",
|
||||
functionIndex,
|
||||
pc);
|
||||
}
|
||||
final var outHeight = inHeight - pops + pushes;
|
||||
if (outHeight > maxStackSlots) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_OVERFLOW,
|
||||
"stack overflow. max=" + maxStackSlots + " actual=" + outHeight,
|
||||
functionIndex,
|
||||
pc);
|
||||
}
|
||||
return outHeight;
|
||||
}
|
||||
|
||||
private void enqueueSuccessors(
|
||||
final IRVMModule module,
|
||||
final int functionIndex,
|
||||
final IRVMFunction function,
|
||||
final int index,
|
||||
final IRVMInstruction instr,
|
||||
final int instrPc,
|
||||
final int[] pcByIndex,
|
||||
final HashMap<Integer, Integer> indexByPc,
|
||||
final int outHeight,
|
||||
final HashMap<Integer, Integer> incomingHeights,
|
||||
final ArrayDeque<Integer> worklist) {
|
||||
if (instr.op() == IRVMOp.HALT || instr.op() == IRVMOp.RET) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (instr.op() == IRVMOp.JMP || instr.op() == IRVMOp.JMP_IF_TRUE || instr.op() == IRVMOp.JMP_IF_FALSE) {
|
||||
final var targetPc = instr.immediate();
|
||||
final var targetIndex = indexByPc.get(targetPc);
|
||||
if (targetIndex == null) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INVALID_JUMP_TARGET,
|
||||
"invalid jump target pc=" + targetPc,
|
||||
functionIndex,
|
||||
instrPc);
|
||||
}
|
||||
mergeIncomingHeight(
|
||||
functionIndex,
|
||||
pcByIndex[targetIndex],
|
||||
targetIndex,
|
||||
outHeight,
|
||||
incomingHeights,
|
||||
worklist);
|
||||
if (instr.op() == IRVMOp.JMP) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final var nextIndex = index + 1;
|
||||
if (nextIndex >= function.instructions().size()) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_UNTERMINATED_PATH,
|
||||
"reachable path ends without terminator",
|
||||
functionIndex,
|
||||
instrPc);
|
||||
}
|
||||
mergeIncomingHeight(
|
||||
functionIndex,
|
||||
pcByIndex[nextIndex],
|
||||
nextIndex,
|
||||
outHeight,
|
||||
incomingHeights,
|
||||
worklist);
|
||||
}
|
||||
|
||||
private void mergeIncomingHeight(
|
||||
final int functionIndex,
|
||||
final int pc,
|
||||
final int index,
|
||||
final int newHeight,
|
||||
final HashMap<Integer, Integer> incomingHeights,
|
||||
final ArrayDeque<Integer> worklist) {
|
||||
final var existing = incomingHeights.get(index);
|
||||
if (existing == null) {
|
||||
incomingHeights.put(index, newHeight);
|
||||
worklist.add(index);
|
||||
return;
|
||||
}
|
||||
if (existing != newHeight) {
|
||||
throw new IRVMValidationException(
|
||||
IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_MISMATCH_JOIN,
|
||||
"stack height mismatch at join. existing=%d new=%d".formatted(existing, newHeight),
|
||||
functionIndex,
|
||||
pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
package p.studio.compiler.backend.irvm;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class IRVMValidatorTest {
|
||||
private final IRVMValidator validator = new IRVMValidator();
|
||||
|
||||
@Test
|
||||
void validateMustRejectInvalidJumpTarget() {
|
||||
final var module = new IRVMModule(
|
||||
"core-v1",
|
||||
ReadOnlyList.from(
|
||||
new IRVMFunction(
|
||||
"main",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
ReadOnlyList.from(
|
||||
new IRVMInstruction(IRVMOp.JMP, 999)))));
|
||||
|
||||
final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
|
||||
assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INVALID_JUMP_TARGET, thrown.code());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateMustRejectStackMismatchJoin() {
|
||||
final var module = new IRVMModule(
|
||||
"core-v1",
|
||||
ReadOnlyList.from(
|
||||
new IRVMFunction(
|
||||
"main",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
ReadOnlyList.from(
|
||||
new IRVMInstruction(IRVMOp.PUSH_I32, 1),
|
||||
new IRVMInstruction(IRVMOp.JMP_IF_TRUE, 18),
|
||||
new IRVMInstruction(IRVMOp.PUSH_I32, 2),
|
||||
new IRVMInstruction(IRVMOp.RET, null)))));
|
||||
|
||||
final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
|
||||
assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_MISMATCH_JOIN, thrown.code());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateMustRejectRetShapeMismatch() {
|
||||
final var module = new IRVMModule(
|
||||
"core-v1",
|
||||
ReadOnlyList.from(
|
||||
new IRVMFunction(
|
||||
"main",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
ReadOnlyList.from(
|
||||
new IRVMInstruction(IRVMOp.RET, null)))));
|
||||
|
||||
final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
|
||||
assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_BAD_RET_STACK_HEIGHT, thrown.code());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateMustRejectInternalOpcodeWhenConfigured() {
|
||||
final var module = new IRVMModule(
|
||||
"core-v1",
|
||||
ReadOnlyList.from(
|
||||
new IRVMFunction(
|
||||
"main",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
ReadOnlyList.from(
|
||||
new IRVMInstruction(IRVMOp.INTERNAL_EXT, null),
|
||||
new IRVMInstruction(IRVMOp.HALT, null)))));
|
||||
|
||||
final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, true));
|
||||
assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INTERNAL_OPCODE_RESIDUAL, thrown.code());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user