implements PR028

This commit is contained in:
bQUARKz 2026-03-06 14:19:18 +00:00
parent bb5cc05c0b
commit 5c29efcbda
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 353 additions and 52 deletions

View File

@ -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`

View File

@ -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<Diagnostic> 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<Diagnostic> filter) {
final Optional<Diagnostic> 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) {
}
}