implements PR028
This commit is contained in:
parent
bb5cc05c0b
commit
5c29efcbda
@ -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`
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user