diff --git a/docs/general/specs/22. Backend Spec-to-Test Conformance Matrix.md b/docs/general/specs/22. Backend Spec-to-Test Conformance Matrix.md index 24c3714e..b9162c0a 100644 --- a/docs/general/specs/22. Backend Spec-to-Test Conformance Matrix.md +++ b/docs/general/specs/22. Backend Spec-to-Test Conformance Matrix.md @@ -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. | diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java index e5b91baf..b352cc32 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringException.java @@ -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; + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java index 907272dc..bf5a788a 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java @@ -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, diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java index 2ca11a26..5b79bace 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStage.java @@ -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) { diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java index d43483a5..720db4ca 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java @@ -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()); + } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssue.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssue.java index 3d3256af..3b6ccfe9 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssue.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssue.java @@ -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(']'); } diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java index 721fa3a6..348cdbbb 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java @@ -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()); + } }