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