diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 503cee44..caacab81 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -7,7 +7,7 @@ {"type":"discussion","id":"DSC-0060","status":"open","ticket":"multi-frontend-validation-boundaries","title":"Separar validacoes de linguagem e validacoes de plataforma","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","backend","validation","multi-frontend"],"agendas":[{"id":"AGD-0063","file":"AGD-0063-multi-frontend-validation-boundaries.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0059","status":"open","ticket":"multi-frontend-common-lifecycle","title":"Extrair lifecycle comum das responsabilidades do frontend PBS","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","lifecycle","backend","multi-frontend"],"agendas":[{"id":"AGD-0062","file":"AGD-0062-multi-frontend-common-lifecycle.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0058","status":"open","ticket":"multi-frontend-serializable-ir","title":"Manter a IR comum serializavel por design","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","ir","backend","serialization","multi-frontend"],"agendas":[{"id":"AGD-0061","file":"AGD-0061-multi-frontend-serializable-ir.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0057","status":"in_progress","ticket":"multi-frontend-frontend-backend-contract","title":"Estabilizar contrato entre frontend e backend comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","ir","backend","multi-frontend"],"agendas":[{"id":"AGD-0060","file":"AGD-0060-multi-frontend-frontend-backend-contract.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[{"id":"DEC-0043","file":"DEC-0043-common-irbackend-frontend-backend-handoff.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15","ref_agenda":"AGD-0060"}],"plans":[{"id":"PLN-0117","file":"PLN-0117-common-irbackend-contract-specs.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0043"]},{"id":"PLN-0118","file":"PLN-0118-common-irbackend-backend-guardrails.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0043"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0057","status":"in_progress","ticket":"multi-frontend-frontend-backend-contract","title":"Estabilizar contrato entre frontend e backend comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","ir","backend","multi-frontend"],"agendas":[{"id":"AGD-0060","file":"AGD-0060-multi-frontend-frontend-backend-contract.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[{"id":"DEC-0043","file":"DEC-0043-common-irbackend-frontend-backend-handoff.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15","ref_agenda":"AGD-0060"}],"plans":[{"id":"PLN-0117","file":"PLN-0117-common-irbackend-contract-specs.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0043"]},{"id":"PLN-0118","file":"PLN-0118-common-irbackend-backend-guardrails.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0043"]}],"lessons":[]} {"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0055","status":"done","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0056","file":"discussion/lessons/DSC-0055-multi-frontend-compiler-vs-language-services/LSN-0056-compile-first-frontends-with-optional-editorial-capabilities.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} diff --git a/discussion/workflow/plans/PLN-0118-common-irbackend-backend-guardrails.md b/discussion/workflow/plans/PLN-0118-common-irbackend-backend-guardrails.md index 8183639b..9e570501 100644 --- a/discussion/workflow/plans/PLN-0118-common-irbackend-backend-guardrails.md +++ b/discussion/workflow/plans/PLN-0118-common-irbackend-backend-guardrails.md @@ -2,7 +2,7 @@ id: PLN-0118 ticket: multi-frontend-frontend-backend-contract title: Common IRBackend backend guardrails -status: open +status: done created: 2026-07-15 ref_decisions: [DEC-0043] tags: [compiler, compiler-general, compiler-pbs, ir, backend, multi-frontend] @@ -81,13 +81,13 @@ Included: ## Acceptance Criteria -- [ ] Common backend source code has no dependency on `p.studio.compiler.pbs`. -- [ ] Public `IRBackend` contract types expose no PBS AST, token, parser, semantic, or editorial classes. -- [ ] A backend test manually constructs executable `IRBackend` and lowers it without invoking PBS parser or PBS frontend compiler services. -- [ ] Common backend lifecycle/entrypoint validation names and diagnostics are neutral where the behavior is common. -- [ ] Existing PBS frontend lowering tests still pass. -- [ ] Existing backend lowering tests still pass. -- [ ] No IR model redesign or `IRBackend` rename was introduced. +- [x] Common backend source code has no dependency on `p.studio.compiler.pbs`. +- [x] Public `IRBackend` contract types expose no PBS AST, token, parser, semantic, or editorial classes. +- [x] A backend test manually constructs executable `IRBackend` and lowers it without invoking PBS parser or PBS frontend compiler services. +- [x] Common backend lifecycle/entrypoint validation names and diagnostics are neutral where the behavior is common. +- [x] Existing PBS frontend lowering tests still pass. +- [x] Existing backend lowering tests still pass. +- [x] No IR model redesign or `IRBackend` rename was introduced. ## Tests diff --git a/docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md b/docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md index 88cf6ca7..43f970d7 100644 --- a/docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md +++ b/docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md @@ -40,6 +40,9 @@ to concrete positive/negative test evidence and current status. | G19-5.2.6 | Gate S-I MUST reject missing capability at load-time. | N/A | `BackendGateIIntegrationTest#gateI_rejectMissingCapability` | pass | | | G19-5.2.7 | Gate S-I MUST cover valid VM-owned intrinsic path. | `BackendGateIIntegrationTest#gateI_validIntrinsicPath` | N/A | pass | | | G19-5.2.8 | Gate S-I MUST cover repeatability across runtime line. | `RuntimeBackedCompatibilityAdapterTest#checkMustPassInStrictModeWhenRuntimeCommandIsValid`; `RuntimeBackedCompatibilityAdapterTest#checkMustBeRepeatableAcrossDeclaredRuntimeLinesInStrictMode` | `RuntimeBackedCompatibilityAdapterTest#checkMustFailInStrictModeWhenRuntimeCommandIsUnavailable` | pass | Multi-line repeatability is asserted with strict runtime-backed checks over distinct declared runtime lines. | +| G20-4.1.1 | `IRBackend` MUST be the common frontend-to-backend executable handoff. | `LowerToIRVMServiceTest#lowerMustAcceptManuallyConstructedCommonIRBackend` | N/A | pass | Direct backend lowering from manually constructed common IRBackend proves PBS parser/frontend services are not required. | +| G20-4.1.2 | Common backend code MUST NOT depend on `p.studio.compiler.pbs`. | `CommonBackendArchitectureTest#commonBackendMustNotImportPbsFrontendPackages` | N/A | pass | Source-level architectural guard rejects PBS frontend imports in common backend packages. | +| G20-4.1.3 | Public `IRBackend` contract MUST NOT expose PBS AST, token, parser, semantic, or editorial types. | `IRBackendExecutableContractTest#publicIRBackendContractMustNotExposePbsTypes` | N/A | pass | Reflection guard covers public contract fields, constructors, methods, and record components. | | G20-6.2 | `IRVM_EXT` MUST declare structural metadata (`pops/pushes/is_branch/is_terminator`). | `IRVMValidatorTest#validateMustApplyStructuralMetadataForCustomInternalExtension`; `IRVMValidatorTest#validateMustRejectCustomInternalExtensionWhenStructuralMetadataUnderflowsStack`; `IRVMOp` record contract (`pops/pushes/branch/terminator/internal`) | N/A | pass | Dedicated extension fixtures now assert structural metadata is consumed by validation behavior. | | G20-6.3 | `IRVM_EXT` MUST be eliminable before bytecode emission. | `OptimizeIRVMServiceTest#optimizeDefaultPassesMustEliminateUnreachableInternalExtensionBeforeEmission` | `EmitBytecodePipelineStageTest#runMustFailWhenInternalOpcodesRemain`; `EmitBytecodePipelineStageTest#runMustFailWhenInternalOpcodesRemainEvenWithNonEmptyEmissionPlan`; `IRVMValidatorTest#validateMustRejectInternalOpcodeWhenConfigured` | pass | Optimizer elimination path and emit-stage hard rejection path are both covered. | | G20-6.4 | IRVM MUST preserve per-function slot and identity headers. | `IRVMProgramTest#constructorMustRejectModuleAndEmissionPlanMismatch` | `IRVMProgramTest#constructorMustRejectModuleAndEmissionPlanMismatch` | pass | Header mismatch is rejected deterministically. | @@ -56,9 +59,6 @@ to concrete positive/negative test evidence and current status. | 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. | `LowerToIRVMPipelineStageTest#runMustAttachSourceAttributionForLoweringFailure`; `LowerToIRVMServiceTest#lowerMustMapHostAndIntrinsicCallsites` | `LowerToIRVMServiceTest#lowerMustRejectMissingCallee` | pass | Stage-level failure now carries explicit `file/start/end` attribution for lowering errors. | -| G20-4.1.1 | `IRBackend` MUST be the common frontend-to-backend executable handoff. | `LowerToIRVMServiceTest#lowerMustAcceptManuallyConstructedCommonIRBackend` | N/A | missing | Required by DEC-0043; PLN-0118 adds the named evidence. | -| G20-4.1.2 | Common backend code MUST NOT depend on `p.studio.compiler.pbs`. | `CommonBackendArchitectureTest#commonBackendMustNotImportPbsFrontendPackages` | N/A | missing | Required by DEC-0043; PLN-0118 adds the named evidence. | -| G20-4.1.3 | Public `IRBackend` contract MUST NOT expose PBS AST, token, parser, semantic, or editorial types. | `IRBackendExecutableContractTest#publicIRBackendContractMustNotExposePbsTypes` | N/A | missing | Required by DEC-0043; PLN-0118 adds the named evidence. | | 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. | `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForLoweredHostIntrinsicFixture`; `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForConditionalJoinFixture`; `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForSimpleLoopFixture`; `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForLinearCallFixture`; `EmitBytecodePipelineStageTest#runMustEmitBytecodeWhenPreconditionsAreSatisfied` | `OptimizeIRVMServiceTest#optimizeMustRejectPassThatMutatesVmProfile` | pass | Equivalence harness now validates on/off semantics and emission validity over CFG corpus with host/intrinsic paths. | 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 bfc62e20..4703fe8d 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 @@ -40,7 +40,7 @@ public class LowerToIRVMService { "IRBackend has no executable functions"); } - validatePbsLifecycleStructure(backend); + validateCommonLifecycleStructure(backend); final var ordered = orderFunctions(backend); final var funcIdByCallableId = new HashMap(); for (var i = 0; i < ordered.size(); i++) { @@ -499,7 +499,7 @@ public class LowerToIRVMService { return ReadOnlyList.wrap(ordered); } - private void validatePbsLifecycleStructure(final IRBackend backend) { + private void validateCommonLifecycleStructure(final IRBackend backend) { if (backend.getSyntheticFunctions().isEmpty()) { return; } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java index bec0f3fe..51a4bbf9 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java @@ -229,6 +229,35 @@ class LowerToIRVMServiceTest { assertTrue(emissionOps.stream().allMatch(op -> op.span() != null)); } + @Test + void lowerMustAcceptManuallyConstructedCommonIRBackend() { + final var backend = IRBackend.builder() + .entryPointCallableName("tick") + .entryPointModuleId(new ModuleId(0)) + .modulePool(ReadOnlyList.from(new ModuleReference("game", ReadOnlyList.from("runtime")))) + .executableFunctions(ReadOnlyList.from( + fnWithModule("tick", "game/runtime", 0, 10, ReadOnlyList.from( + callHost("timer", "poll", 1, 0, 0), + callIntrinsic("input.pad", 1, 0), + callFuncWithExpected("game/runtime", "helper", 20, 0, 0), + ret())), + fnWithModule("helper", "game/runtime", 0, 20, ReadOnlyList.from( + ret())))) + .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("input.pad", 1))) + .build(); + + final var lowered = new LowerToIRVMService().lower(backend); + + assertEquals(2, lowered.module().functions().size()); + assertEquals("tick", lowered.module().functions().getFirst().name()); + final var instructions = lowered.module().functions().getFirst().instructions(); + assertEquals(IRVMOp.HOSTCALL, instructions.get(0).op()); + assertEquals(IRVMOp.INTRINSIC, instructions.get(1).op()); + assertEquals(IRVMOp.CALL, instructions.get(2).op()); + assertEquals(1, instructions.get(2).immediate()); + assertEquals(IRVMOp.RET, instructions.get(3).op()); + } + @Test void lowerMustRejectUnterminatedFunction() { final var backend = IRBackend.builder() diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/BackendConformanceMatrixSpecTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/BackendConformanceMatrixSpecTest.java index a1f0ee72..46c847b0 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/BackendConformanceMatrixSpecTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/BackendConformanceMatrixSpecTest.java @@ -36,6 +36,9 @@ class BackendConformanceMatrixSpecTest { "G19-5.2.6", "G19-5.2.7", "G19-5.2.8", + "G20-4.1.1", + "G20-4.1.2", + "G20-4.1.3", "G20-6.2", "G20-6.3", "G20-6.4", diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/CommonBackendArchitectureTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/CommonBackendArchitectureTest.java new file mode 100644 index 00000000..e4fd1faf --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/CommonBackendArchitectureTest.java @@ -0,0 +1,60 @@ +package p.studio.compiler.specs; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class CommonBackendArchitectureTest { + private static final String BACKEND_SOURCE_ROOT = + "prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend"; + private static final String FORBIDDEN_PBS_PACKAGE = "p.studio.compiler.pbs"; + + @Test + void commonBackendMustNotImportPbsFrontendPackages() throws IOException { + final var backendRoot = locateRepoRoot().resolve(BACKEND_SOURCE_ROOT); + final var violations = new ArrayList(); + + try (final var paths = Files.walk(backendRoot)) { + paths.filter(path -> path.toString().endsWith(".java")) + .forEach(path -> assertNoPbsImport(path, violations)); + } + + assertTrue( + violations.isEmpty(), + "common backend must not import PBS frontend packages: " + violations); + } + + private static void assertNoPbsImport( + final Path path, + final ArrayList violations) { + try { + final var content = Files.readString(path); + if (content.contains("import " + FORBIDDEN_PBS_PACKAGE) + || content.contains("import static " + FORBIDDEN_PBS_PACKAGE)) { + violations.add(locateRepoRoot().relativize(path).toString()); + } + } catch (IOException e) { + throw new IllegalStateException("failed to read source file: " + path, e); + } + } + + private static Path locateRepoRoot() { + var cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize(); + while (cursor != null) { + final var hasDocs = Files.isDirectory(cursor.resolve("docs/specs/compiler")); + final var hasCompiler = Files.isDirectory(cursor.resolve("prometeu-compiler")); + if (hasDocs && hasCompiler) { + return cursor; + } + cursor = cursor.getParent(); + } + fail("could not locate repository root from working directory"); + throw new IllegalStateException("unreachable"); + } +} 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 82628bd7..842dffb2 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 @@ -11,9 +11,75 @@ import p.studio.compiler.source.tables.IntrinsicReference; import p.studio.compiler.source.tables.ModuleReference; import p.studio.utilities.structures.ReadOnlyList; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; class IRBackendExecutableContractTest { + private static final String FORBIDDEN_PBS_PACKAGE = "p.studio.compiler.pbs"; + private static final List> PUBLIC_CONTRACT_TYPES = List.of( + IRBackend.class, + IRBackend.IRBackendAggregator.class, + IRBackendReader.class, + IRBackendFile.class, + IRBackendExecutableFunction.class, + IRBackendExecutableFunction.Instruction.class, + IRBackendExecutableFunction.InstructionKind.class, + IRBackendExecutableFunction.HostCallMetadata.class, + IRBackendExecutableFunction.IntrinsicCallMetadata.class, + IRFunction.class, + IRSyntheticFunction.class, + IRSyntheticCallableKind.class, + IRSyntheticOrigin.class, + IRGlobal.class, + IRGlobalOrigin.class, + IRGlobalVisibility.class, + IRHiddenGlobalKind.class, + IRReservedMetadata.class, + IRReservedMetadata.HostMethodBinding.class, + IRReservedMetadata.BuiltinTypeSurface.class, + IRReservedMetadata.BuiltinFieldSurface.class, + IRReservedMetadata.IntrinsicSurface.class, + IRReservedMetadata.BuiltinConstSurface.class); + + @Test + void publicIRBackendContractMustNotExposePbsTypes() { + final var violations = new ArrayList(); + + for (final var contractType : PUBLIC_CONTRACT_TYPES) { + inspectType(contractType.getName(), contractType, violations); + for (final var field : contractType.getFields()) { + inspectType(contractType.getName() + "#" + field.getName(), field.getGenericType(), violations); + } + for (final var constructor : contractType.getConstructors()) { + for (final var parameterType : constructor.getGenericParameterTypes()) { + inspectType(contractType.getName() + "#", parameterType, violations); + } + } + for (final var method : contractType.getMethods()) { + if (method.getDeclaringClass().equals(Object.class) + || !Modifier.isPublic(method.getModifiers())) { + continue; + } + inspectType(contractType.getName() + "#" + method.getName(), method.getGenericReturnType(), violations); + for (final var parameterType : method.getGenericParameterTypes()) { + inspectType(contractType.getName() + "#" + method.getName(), parameterType, violations); + } + } + if (contractType.isRecord()) { + for (final var component : contractType.getRecordComponents()) { + inspectType(contractType.getName() + "#" + component.getName(), component.getGenericType(), violations); + } + } + } + + assertTrue( + violations.isEmpty(), + "public IRBackend contract must not expose PBS frontend types: " + violations); + } @Test void callInstructionMustRequireCategorySpecificMetadata() { @@ -366,4 +432,17 @@ class IRBackendExecutableContractTest { assertEquals(0, backend.getCallableSignatures().get(0).moduleId().getIndex()); assertEquals(1, backend.getCallableSignatures().get(1).moduleId().getIndex()); } + + private static void inspectType( + final String owner, + final Type type, + final ArrayList violations) { + if (type == null) { + return; + } + final var typeName = type.getTypeName(); + if (typeName.contains(FORBIDDEN_PBS_PACKAGE)) { + violations.add(owner + " -> " + typeName); + } + } }