diff --git a/docs/pbs/pull-requests/PR-027-pbs-builtin-metadata-extraction-and-ir-lowering-admission.md b/docs/pbs/pull-requests/PR-027-pbs-builtin-metadata-extraction-and-ir-lowering-admission.md deleted file mode 100644 index 15b4f8ea..00000000 --- a/docs/pbs/pull-requests/PR-027-pbs-builtin-metadata-extraction-and-ir-lowering-admission.md +++ /dev/null @@ -1,52 +0,0 @@ -# PR-027 - PBS Builtin Metadata Extraction and IR Lowering Admission - -## Briefing - -Com interface modules e SDK minimo ativos, precisamos fechar a fronteira entre metadata reservada e lowering frontend (`IRBackend`), sem degradacao silenciosa. - -## Motivation - -As specs exigem que metadata reservada seja compilacao-only e consumivel por lowering posterior. -Tambem exigem rejeicao deterministica quando suporte ainda nao cobre algum caso. - -## Target - -- Extracao de metadata de attributes reservadas no grafo de interface. -- Regras de admissao/rejeicao no lowering frontend (`IRBackend` boundary). - -## Scope - -- Extrair e armazenar canonical metadata de `Host`, `BuiltinType`, `IntrinsicCall`, `Slot`, `BuiltinConst`. -- Preservar informacao minima necessaria no contrato de lowering frontend. -- Emitir diagnostico deterministico para formas nao suportadas pela fronteira atual. - -## Method - -- Introduzir modelo interno de metadata reservada desacoplado da sintaxe bruta. -- Atualizar contrato de admissao do frontend para recusar deterministicamente casos fora da fatia implementada. -- Garantir fase/codigo/template/attribution estaveis nos diagnosticos de rejeicao. - -## Acceptance Criteria - -- Metadata reservada valida e acessivel apos parse+semantica+linking. -- Nenhum caso nao suportado passa silenciosamente para `IRBackend` com comportamento alterado. -- Rejeicoes de lowering frontend sao deterministicas e rastreaveis. - -## Tests - -- Fixtures positivas para extracao de metadata de `Color`/`Gfx`. -- Fixtures negativas com metadata invalida e rejeicao deterministica. -- Asserts de contrato diagnostico (phase, code, templateId, span). - -## Non-Goals - -- Implementar `IRBackend -> IRVM`. -- Definir encoding final de artifact/PBX. - -## Affected Documents - -- `docs/pbs/specs/12. Diagnostics Specification.md` -- `docs/pbs/specs/13. Lowering IRBackend Specification.md` -- `docs/pbs/specs/6.1. Intrinsics and Builtin Types Specification.md` -- `docs/pbs/specs/6.2. Host ABI Binding and Loader Resolution Specification.md` - diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java new file mode 100644 index 00000000..26be806e --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java @@ -0,0 +1,353 @@ +package p.studio.compiler.pbs; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import p.studio.compiler.messages.BuildingIssueSink; +import p.studio.compiler.messages.FrontendPhaseContext; +import p.studio.compiler.models.BuildStack; +import p.studio.compiler.models.IRBackend; +import p.studio.compiler.models.ProjectDescriptor; +import p.studio.compiler.models.SourceHandle; +import p.studio.compiler.models.SourceKind; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.linking.PbsLinkErrors; +import p.studio.compiler.pbs.linking.PbsModuleVisibilityValidator; +import p.studio.compiler.pbs.parser.ParseErrors; +import p.studio.compiler.pbs.parser.PbsBarrelParser; +import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.pbs.semantics.PbsSemanticsErrors; +import p.studio.compiler.services.PBSFrontendPhaseService; +import p.studio.compiler.source.diagnostics.Diagnostic; +import p.studio.compiler.source.diagnostics.DiagnosticPhase; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.compiler.source.identifiers.ProjectId; +import p.studio.compiler.source.tables.FileTable; +import p.studio.compiler.source.tables.ProjectTable; +import p.studio.compiler.utilities.SourceProviderFactory; +import p.studio.utilities.logs.LogAggregator; +import p.studio.utilities.structures.ReadOnlyList; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PbsGateUSdkInterfaceConformanceTest { + + @TempDir + Path tempDir; + + @Test + void gateU_shouldClassifySourceKindForInterfaceDeclarations() { + final var source = """ + [BuiltinType(name = "Color", version = 1)] + declare builtin type Color( + [Slot(index = 0)] pub r: int + ) { + } + + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int, color: Color) -> void; + } + """; + + final var projectDiagnostics = DiagnosticSink.empty(); + final var projectBackend = new PbsFrontendCompiler().compileFile( + new FileId(10), + source, + projectDiagnostics, + SourceKind.PROJECT); + assertTrue(projectDiagnostics.stream().anyMatch(d -> + d.getCode().equals(ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name()) + || d.getCode().equals(ParseErrors.E_PARSE_RESERVED_DECLARATION.name()))); + assertEquals(0, projectBackend.functions().size()); + + final var sdkDiagnostics = DiagnosticSink.empty(); + final var sdkBackend = new PbsFrontendCompiler().compileFile( + new FileId(11), + source, + sdkDiagnostics, + SourceKind.SDK_INTERFACE); + assertFalse(sdkDiagnostics.hasErrors()); + assertEquals(0, sdkBackend.functions().size()); + assertEquals(1, sdkBackend.reservedMetadata().builtinTypeSurfaces().size()); + assertEquals(1, sdkBackend.reservedMetadata().hostMethodBindings().size()); + } + + @Test + void gateU_shouldResolveReservedImportsAndRejectMissingReservedModuleDeterministically() throws IOException { + final var positive = compileWorkspaceModule( + tempDir.resolve("gate-u-reserved-import-positive"), + """ + import { Color } from @core:color; + import { Gfx } from @sdk:gfx; + + declare contract Renderer { + fn render(gfx: Gfx, color: Color) -> void; + } + """, + "pub contract Renderer;", + 1, + null); + assertFalse(positive.diagnostics().stream().anyMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name()))); + assertFalse(positive.diagnostics().stream().anyMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_UNRESOLVED.name()))); + assertTrue(positive.irBackend().getReservedMetadata().builtinTypeSurfaces().stream() + .anyMatch(t -> t.sourceTypeName().equals("Color"))); + assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream() + .anyMatch(h -> h.ownerName().equals("Gfx"))); + + final var negative = compileWorkspaceModule( + tempDir.resolve("gate-u-reserved-import-negative"), + """ + import { Missing } from @sdk:missing; + fn run() -> int { return 1; } + """, + "pub fn run() -> int;", + 1, + d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name())); + final var missingModule = firstDiagnostic(negative.diagnostics(), + d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name())); + assertStableDiagnosticIdentity(missingModule, PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name(), DiagnosticPhase.LINKING); + } + + @Test + void gateU_shouldParseReservedDeclarationsInInterfaceModeOnly() { + final var source = """ + declare host Gfx { + fn draw_pixel(x: int, y: int) -> void; + } + """; + final var interfaceDiagnostics = DiagnosticSink.empty(); + final var interfaceAst = PbsParser.parse( + PbsLexer.lex(source, new FileId(20), interfaceDiagnostics), + new FileId(20), + interfaceDiagnostics, + PbsParser.ParseMode.INTERFACE_MODULE); + assertFalse(interfaceDiagnostics.hasErrors()); + assertEquals(1, interfaceAst.topDecls().size()); + assertInstanceOf(PbsAst.HostDecl.class, interfaceAst.topDecls().getFirst()); + + final var ordinaryDiagnostics = DiagnosticSink.empty(); + PbsParser.parse( + PbsLexer.lex(source, new FileId(21), ordinaryDiagnostics), + new FileId(21), + ordinaryDiagnostics, + PbsParser.ParseMode.ORDINARY); + final var reservedDeclDiagnostic = firstDiagnostic(ordinaryDiagnostics, + d -> d.getCode().equals(ParseErrors.E_PARSE_RESERVED_DECLARATION.name())); + assertStableDiagnosticIdentity(reservedDeclDiagnostic, ParseErrors.E_PARSE_RESERVED_DECLARATION.name(), DiagnosticPhase.SYNTAX); + } + + @Test + void gateU_shouldValidateAndLinkHostBuiltinShellsWithDeterministicFailures() { + final var sourceId = new FileId(30); + final var barrelId = new FileId(31); + final var source = """ + [BuiltinType(name = "Color", version = 1)] + declare builtin type Color( + [Slot(index = 0)] pub r: int + ) { + } + + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int, color: Color) -> void; + } + """; + final var semanticsDiagnostics = DiagnosticSink.empty(); + new PbsFrontendCompiler().compileFile(sourceId, source, semanticsDiagnostics, SourceKind.SDK_INTERFACE); + assertFalse(semanticsDiagnostics.hasErrors()); + + final var linkDiagnostics = DiagnosticSink.empty(); + final var sourceAst = PbsParser.parse( + PbsLexer.lex(source, sourceId, linkDiagnostics), + sourceId, + linkDiagnostics, + PbsParser.ParseMode.INTERFACE_MODULE); + final var validBarrelAst = PbsBarrelParser.parse( + PbsLexer.lex("pub struct Color;\npub host Gfx;\n", barrelId, linkDiagnostics), + barrelId, + linkDiagnostics); + final var validModule = new PbsModuleVisibilityValidator.ModuleUnit( + new PbsModuleVisibilityValidator.ModuleCoordinates("sdk", ReadOnlyList.wrap(List.of("gfx"))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceId, sourceAst))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(barrelId, validBarrelAst)))); + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(validModule)), linkDiagnostics); + assertFalse(linkDiagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name()))); + + final var negativeSemanticsDiagnostics = DiagnosticSink.empty(); + new PbsFrontendCompiler().compileFile( + new FileId(32), + "fn run() -> int { return 1; }", + negativeSemanticsDiagnostics, + SourceKind.SDK_INTERFACE); + final var nonDeclarative = firstDiagnostic(negativeSemanticsDiagnostics, + d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name())); + assertStableDiagnosticIdentity(nonDeclarative, PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name(), DiagnosticPhase.STATIC_SEMANTICS); + + final var negativeLinkDiagnostics = DiagnosticSink.empty(); + final var invalidBarrelAst = PbsBarrelParser.parse( + PbsLexer.lex("pub host Missing;", new FileId(33), negativeLinkDiagnostics), + new FileId(33), + negativeLinkDiagnostics); + final var invalidModule = new PbsModuleVisibilityValidator.ModuleUnit( + new PbsModuleVisibilityValidator.ModuleCoordinates("sdk", ReadOnlyList.wrap(List.of("gfx"))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceId, sourceAst))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(new FileId(33), invalidBarrelAst)))); + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(invalidModule)), negativeLinkDiagnostics); + final var unresolvedBarrel = firstDiagnostic(negativeLinkDiagnostics, + d -> d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name())); + assertStableDiagnosticIdentity(unresolvedBarrel, PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name(), DiagnosticPhase.LINKING); + } + + @Test + void gateU_shouldExposeReservedMetadataAndRejectUnsupportedLoadAdmission() { + final var validSource = """ + [BuiltinType(name = "Color", version = 1)] + declare builtin type Color( + [Slot(index = 0)] pub r: int, + [Slot(index = 1)] pub g: int + ) { + [IntrinsicCall(name = "core.color.pack", version = 1)] + fn pack() -> int; + } + + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int, color: Color) -> void; + } + + [BuiltinConst(target = "Color", name = "white", version = 1)] + declare const WHITE: Color; + """; + final var validDiagnostics = DiagnosticSink.empty(); + final var validBackend = new PbsFrontendCompiler().compileFile( + new FileId(40), + validSource, + validDiagnostics, + SourceKind.SDK_INTERFACE); + assertFalse(validDiagnostics.hasErrors()); + assertEquals(1, validBackend.reservedMetadata().builtinTypeSurfaces().size()); + assertEquals(1, validBackend.reservedMetadata().hostMethodBindings().size()); + assertEquals(1, validBackend.reservedMetadata().builtinConstSurfaces().size()); + + final var invalidSource = """ + [BuiltinType(name = "Color", version = 1)] + declare builtin type Color( + pub r: int + ) { + } + """; + final var invalidDiagnostics = DiagnosticSink.empty(); + new PbsFrontendCompiler().compileFile( + new FileId(41), + invalidSource, + invalidDiagnostics, + SourceKind.SDK_INTERFACE); + final var unsupportedLoad = firstDiagnostic(invalidDiagnostics, + d -> d.getCode().equals(PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name())); + assertStableDiagnosticIdentity(unsupportedLoad, PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name(), DiagnosticPhase.LOAD_FACING_REJECTION); + } + + private WorkspaceCompileResult compileWorkspaceModule( + final Path projectRoot, + final String sourceContent, + final String barrelContent, + final int stdlibVersion, + final Predicate awaitedDiagnostic) throws IOException { + final var sourceRoot = projectRoot.resolve("src"); + final var modulePath = sourceRoot.resolve("app"); + Files.createDirectories(modulePath); + + final var sourceFile = modulePath.resolve("source.pbs"); + final var barrelFile = modulePath.resolve("mod.barrel"); + Files.writeString(sourceFile, sourceContent); + Files.writeString(barrelFile, barrelContent); + + final var projectTable = new ProjectTable(); + final var fileTable = new FileTable(1); + final var projectId = projectTable.register(ProjectDescriptor.builder() + .rootPath(projectRoot) + .name("app") + .version("1.0.0") + .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot))) + .build()); + + registerFile(projectId, projectRoot, sourceFile, fileTable); + registerFile(projectId, projectRoot, barrelFile, fileTable); + + final var context = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId))), + stdlibVersion); + final var diagnostics = DiagnosticSink.empty(); + + final var irBackend = new PBSFrontendPhaseService().compile( + context, + diagnostics, + LogAggregator.empty(), + BuildingIssueSink.empty()); + + if (awaitedDiagnostic != null) { + assertTrue(diagnostics.stream().anyMatch(awaitedDiagnostic)); + } + + return new WorkspaceCompileResult(irBackend, diagnostics); + } + + private void registerFile( + final ProjectId projectId, + final Path projectRoot, + final Path file, + final FileTable fileTable) throws IOException { + final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); + fileTable.register(new SourceHandle( + projectId, + projectRoot.relativize(file), + file, + attributes.size(), + attributes.lastModifiedTime().toMillis(), + SourceProviderFactory.filesystem())); + } + + private Diagnostic firstDiagnostic( + final DiagnosticSink diagnostics, + final Predicate filter) { + final Optional match = diagnostics.stream().filter(filter).findFirst(); + assertTrue(match.isPresent()); + return match.get(); + } + + private void assertStableDiagnosticIdentity( + final Diagnostic diagnostic, + final String code, + final DiagnosticPhase phase) { + assertNotNull(diagnostic); + assertEquals(code, diagnostic.getCode()); + assertEquals(phase, diagnostic.getPhase()); + assertEquals(code, diagnostic.getTemplateId()); + assertNotNull(diagnostic.getSpan()); + assertNotNull(diagnostic.getSpan().getFileId()); + } + + private record WorkspaceCompileResult( + IRBackend irBackend, + DiagnosticSink diagnostics) { + } +}