implements PR-036

This commit is contained in:
bQUARKz 2026-03-07 16:33:51 +00:00
parent 68bd72bb21
commit e6bd796f4f
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
8 changed files with 439 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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,
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}