implements PR-06.1

This commit is contained in:
bQUARKz 2026-03-09 07:47:23 +00:00
parent 7ccdd7b7e2
commit 0ec5693b0d
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 195 additions and 11 deletions

View File

@ -54,7 +54,7 @@ to concrete positive/negative test evidence and current status.
| G20-10 | Backend MUST run structural pre-verification before emission. | `LowerToIRVMService` + `OptimizeIRVMService` validator invocation; `LowerToIRVMServiceTest` positive fixtures | `IRVMValidatorTest#validateMustRejectStackMismatchJoin`; `IRVMValidatorTest#validateProgramMustApplyHostcallStackEffectsFromMetadata` | pass | Pre-verification checks jump/stack/ret/callsite constraints. |
| G20-11.1 | Lowering rejection MUST be deterministic. | `BackendSafetyGateSUTest#lowerStageMustExposeDeterministicFailureCodeForSameInvalidInput` | same | pass | |
| G20-11.2 | Diagnostics identity/phase MUST remain stable. | `BackendSafetyGateSUTest#lowerStageMustExposeDeterministicFailureCodeForSameInvalidInput` | `BackendSafetyGateSUTest#emitStageMustExposeMarshalingLinkageFailureDeterministically` | pass | Stable rejection families are exercised. |
| G20-11.3 | Source attribution MUST be preserved when source-actionable. | `LowerToIRVMServiceTest#lowerMustMapHostAndIntrinsicCallsites` (span propagation) | N/A | partial | Needs dedicated assertion for all rejection diagnostics with source spans. |
| G20-11.3 | Source attribution MUST be preserved when source-actionable. | `LowerToIRVMPipelineStageTest#runMustAttachSourceAttributionForLoweringFailure`; `LowerToIRVMServiceTest#lowerMustMapHostAndIntrinsicCallsites` | `LowerToIRVMServiceTest#lowerMustRejectMissingCallee` | pass | Stage-level failure now carries explicit `file/start/end` attribution for lowering errors. |
| G21-5 | `OptimizeIRVM` MUST NOT be skipped in canonical pipeline order. | `BuilderPipelineServiceOrderTest#canonicalOrderMustContainOptimizeBetweenLowerAndEmit` | N/A | pass | Canonical stage order is enforced. |
| G21-6.1 | Optimize input MUST satisfy lowering obligations/profile/structural validity. | `OptimizeIRVMPipelineStageTest#runMustAcceptSupportedNonDefaultVmProfile` | `OptimizeIRVMPipelineStageTest#runMustRejectUnsupportedVmProfile` | pass | Input validation occurs before pass execution. |
| G21-6.2 | Optimize output MUST preserve semantics/contracts and remain emission-valid. | `OptimizeIRVMServiceTest#optimizeDefaultPassesMustRemoveUnreachableInstructions`; `EmitBytecodePipelineStageTest#runMustEmitBytecodeWhenPreconditionsAreSatisfied` | `OptimizeIRVMServiceTest#optimizeMustRejectPassThatMutatesVmProfile` | partial | Full semantic-equivalence fixture corpus still incremental. |
@ -70,7 +70,7 @@ to concrete positive/negative test evidence and current status.
| PBS13-12.1.1 | Callable identity MUST be preserved at handoff. | `IRBackendExecutableContractTest#aggregatorMustPreserveExecutableFunctionOrderDeterministically` | N/A | pass | |
| PBS13-12.1.2 | Observable callable signature MUST be preserved at handoff. | `IRBackendExecutableContractTest#functionContractMustRejectInvalidSlotAndSpanBounds` | N/A | pass | |
| PBS13-12.1.3 | Callable category MUST be preserved at handoff. | `PbsFrontendCompilerTest#shouldLowerExecutableFunctionsWithCallsiteCategories` | N/A | pass | |
| PBS13-12.1.4 | Source anchor (`fileId/start/end`) MUST be preserved. | `IRBackendExecutableContractTest#functionContractMustRejectInvalidSlotAndSpanBounds` | N/A | partial | Needs explicit assertion of per-instruction source spans in rejection diagnostics. |
| PBS13-12.1.4 | Source anchor (`fileId/start/end`) MUST be preserved. | `IRBackendExecutableContractTest#aggregatorMustPreserveExecutableSourceAttribution`; `IRBackendExecutableContractTest#functionContractMustRejectInvalidSlotAndSpanBounds` | N/A | pass | Handoff preserves function and instruction source anchors after aggregation/reindexing. |
| PBS13-12.1.5 | Executable body representation MUST be backend-lowerable. | `LowerToIRVMServiceTest#lowerMustMapHostAndIntrinsicCallsites` | `LowerToIRVMServiceTest#lowerMustRejectMissingCallee` | pass | |
| PBS13-12.2.1 | Callsite MUST be exactly one of `CALL_FUNC/CALL_HOST/CALL_INTRINSIC`. | `IRBackendExecutableContractTest#callInstructionMustRequireCategorySpecificMetadata` | `IRBackendExecutableContractTest#instructionContractMustRejectMixedMetadataKinds` | pass | |
| PBS13-12.2.2 | Backend MUST NOT infer callsite category by textual heuristics. | `PbsFrontendCompilerTest#shouldLowerExecutableFunctionsWithCallsiteCategories`; `IRBackendExecutableContractTest#callInstructionMustRequireCategorySpecificMetadata` | N/A | partial | Semantic-identity coverage exists; dedicated ambiguous-identity regression fixture is still missing. |

View File

@ -2,16 +2,42 @@ package p.studio.compiler.backend.irvm;
public class IRVMLoweringException extends RuntimeException {
private final IRVMLoweringErrorCode code;
private final Integer fileId;
private final Integer start;
private final Integer end;
public IRVMLoweringException(
final IRVMLoweringErrorCode code,
final String message) {
this(code, message, null, null, null);
}
public IRVMLoweringException(
final IRVMLoweringErrorCode code,
final String message,
final Integer fileId,
final Integer start,
final Integer end) {
super(message);
this.code = code;
this.fileId = fileId;
this.start = start;
this.end = end;
}
public IRVMLoweringErrorCode code() {
return code;
}
}
public Integer fileId() {
return fileId;
}
public Integer start() {
return start;
}
public Integer end() {
return end;
}
}

View File

@ -47,7 +47,8 @@ public class LowerToIRVMService {
for (var i = 0; i < ordered.size(); i++) {
final var fn = ordered.get(i);
if (funcIdByCallableId.putIfAbsent(fn.callableId(), i) != null) {
throw new IRVMLoweringException(
throw loweringError(
fn,
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
"duplicate callable id in executable set: " + fn.callableId());
}
@ -78,20 +79,26 @@ public class LowerToIRVMService {
}
case CALL_FUNC -> {
if (instr.calleeCallableId() == null) {
throw new IRVMLoweringException(
throw loweringError(
fn,
instr,
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
"missing callee callable id");
}
final var calleeId = funcIdByCallableId.get(instr.calleeCallableId());
if (calleeId == null) {
throw new IRVMLoweringException(
throw loweringError(
fn,
instr,
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
"missing callee function for callable_id=" + instr.calleeCallableId());
}
final var calleeFunction = ordered.get(calleeId);
if (instr.expectedArgSlots() != null
&& instr.expectedArgSlots() != calleeFunction.paramSlots()) {
throw new IRVMLoweringException(
throw loweringError(
fn,
instr,
IRVMLoweringErrorCode.LOWER_IRVM_CALL_ARG_SLOTS_MISMATCH,
"call arg_slots mismatch for %s. expected=%d actual=%d".formatted(
instr.calleeCallableName(),
@ -100,7 +107,9 @@ public class LowerToIRVMService {
}
if (instr.expectedRetSlots() != null
&& instr.expectedRetSlots() != calleeFunction.returnSlots()) {
throw new IRVMLoweringException(
throw loweringError(
fn,
instr,
IRVMLoweringErrorCode.LOWER_IRVM_CALL_RET_SLOTS_MISMATCH,
"call ret_slots mismatch for %s. expected=%d actual=%d".formatted(
instr.calleeCallableName(),
@ -130,7 +139,9 @@ public class LowerToIRVMService {
final var intrinsic = instr.intrinsicCall();
final var intrinsicIndex = intrinsic.intrinsicId().getIndex();
if (intrinsicIndex < 0 || intrinsicIndex >= backend.getIntrinsicPool().size()) {
throw new IRVMLoweringException(
throw loweringError(
fn,
instr,
IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID,
"invalid intrinsic id: " + intrinsic.intrinsicId());
}
@ -145,7 +156,9 @@ public class LowerToIRVMService {
case LABEL -> {
final var label = instr.label();
if (labelToPc.putIfAbsent(label, functionPc) != null) {
throw new IRVMLoweringException(
throw loweringError(
fn,
instr,
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET,
"duplicate label in function '" + fn.callableName() + "': " + label);
}
@ -173,7 +186,8 @@ public class LowerToIRVMService {
for (final var patch : jumpPatches) {
final var targetPc = labelToPc.get(patch.targetLabel());
if (targetPc == null) {
throw new IRVMLoweringException(
throw loweringError(
fn,
IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET,
"missing jump target label '" + patch.targetLabel() + "' in function '" + fn.callableName() + "'");
}
@ -268,6 +282,47 @@ public class LowerToIRVMService {
return (int) value;
}
private IRVMLoweringException loweringError(
final IRBackendExecutableFunction function,
final IRVMLoweringErrorCode code,
final String message) {
return new IRVMLoweringException(
code,
message,
function.fileId().getId(),
function.sourceStart(),
function.sourceEnd());
}
private IRVMLoweringException loweringError(
final IRBackendExecutableFunction function,
final IRBackendExecutableFunction.Instruction instruction,
final IRVMLoweringErrorCode code,
final String message) {
return loweringError(function, instruction == null ? Span.none() : instruction.span(), code, message);
}
private IRVMLoweringException loweringError(
final IRBackendExecutableFunction function,
final Span span,
final IRVMLoweringErrorCode code,
final String message) {
final var effectiveSpan = span == null ? Span.none() : span;
final var fileId = effectiveSpan.getFileId() == null || effectiveSpan.getFileId().isNone()
? function.fileId().getId()
: effectiveSpan.getFileId().getId();
final int start;
final int end;
if (effectiveSpan.getFileId() == null || effectiveSpan.getFileId().isNone()) {
start = function.sourceStart();
end = function.sourceEnd();
} else {
start = safeToInt(effectiveSpan.getStart());
end = safeToInt(effectiveSpan.getEnd());
}
return new IRVMLoweringException(code, message, fileId, start, end);
}
private record JumpPatch(
int instructionIndex,
int operationIndex,

View File

@ -39,6 +39,9 @@ public class LowerToIRVMPipelineStage implements PipelineStage {
.error(true)
.phase("BACKEND_LOWER_TO_IRVM")
.code(e.code().name())
.fileId(e.fileId())
.start(e.start())
.end(e.end())
.message("[BUILD]: lower to irvm failed: " + e.getMessage())
.exception(e));
} catch (IRVMValidationException e) {

View File

@ -96,4 +96,49 @@ class LowerToIRVMPipelineStageTest {
assertNotNull(ctx.irvm);
assertEquals("experimental-v1", ctx.irvm.module().vmProfile());
}
@Test
void runMustAttachSourceAttributionForLoweringFailure() {
final var callSpan = new Span(new FileId(9), 12, 24);
final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, "."));
ctx.irBackend = IRBackend.builder()
.executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
new FileId(9),
"app",
"main",
new CallableId(1),
10,
30,
0,
0,
0,
1,
ReadOnlyList.from(
new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
"app",
"missing",
new CallableId(99),
null,
null,
callSpan),
new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
"",
null,
null,
Span.none())),
new Span(new FileId(9), 10, 30))))
.build();
final var issues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
final var firstIssue = issues.asCollection().iterator().next();
assertTrue(issues.hasErrors());
assertEquals("LOWER_IRVM_MISSING_CALLEE", firstIssue.getCode());
assertEquals(9, firstIssue.getFileId());
assertEquals(12, firstIssue.getStart());
assertEquals(24, firstIssue.getEnd());
}
}

View File

@ -12,6 +12,9 @@ public class BuildingIssue {
private final Throwable exception;
private final String phase;
private final String code;
private final Integer fileId;
private final Integer start;
private final Integer end;
private final Integer functionIndex;
private final Integer pc;
@ -37,6 +40,14 @@ public class BuildingIssue {
sb.append('(').append(code).append(')').append(' ');
}
sb.append(message == null ? "" : message);
if (fileId != null && fileId >= 0) {
final var safeStart = start == null ? -1 : start;
final var safeEnd = end == null ? -1 : end;
sb.append(" [file=").append(fileId)
.append(':').append(safeStart)
.append('-').append(safeEnd)
.append(']');
}
if (functionIndex != null && functionIndex >= 0) {
sb.append(" [fn=").append(functionIndex).append(']');
}

View File

@ -246,4 +246,48 @@ class IRBackendExecutableContractTest {
assertEquals(firstIntrinsicId, secondIntrinsicId);
assertEquals("core.color.pack", backend.getIntrinsicPool().getFirst().canonicalName());
}
@Test
void aggregatorMustPreserveExecutableSourceAttribution() {
final var instructionSpan = new Span(new FileId(7), 21, 34);
final var functionSpan = new Span(new FileId(7), 10, 40);
final var file = new IRBackendFile(
new FileId(7),
ReadOnlyList.empty(),
ReadOnlyList.from(new IRBackendExecutableFunction(
new FileId(7),
"app/main",
"main",
new CallableId(0),
10,
40,
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
"",
null,
null,
instructionSpan)),
functionSpan)),
IRReservedMetadata.empty(),
ReadOnlyList.from(new CallableSignatureRef("app/main", "main", 0, "() -> unit")),
ReadOnlyList.empty());
final var aggregator = IRBackend.aggregator();
aggregator.merge(file);
final var backend = aggregator.emit();
final var executable = backend.getExecutableFunctions().getFirst();
final var emittedInstructionSpan = executable.instructions().getFirst().span();
assertEquals(new FileId(7), executable.fileId());
assertEquals(10, executable.sourceStart());
assertEquals(40, executable.sourceEnd());
assertEquals(7, emittedInstructionSpan.getFileId().getId());
assertEquals(21, emittedInstructionSpan.getStart());
assertEquals(34, emittedInstructionSpan.getEnd());
}
}