implements PR-06.1
This commit is contained in:
parent
7ccdd7b7e2
commit
0ec5693b0d
@ -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. |
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(']');
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user