implements PR029

This commit is contained in:
bQUARKz 2026-03-06 15:35:09 +00:00
parent fc734403b5
commit 01c5b6649a
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
19 changed files with 497 additions and 256 deletions

View File

@ -1,72 +0,0 @@
# PR-029 - PBS Builtin Slot Inference and Canonical Layout Semantics
## Briefing
Hoje o frontend aceita `Slot(index=...)` manual em `declare builtin type`, e parte da validacao de layout fica empurrada para `load-facing`.
Isso abre espaco para inconsistencias entre declaracao de campos, flattening e offsets.
Esta PR move ownership de slot para o compilador: offsets passam a ser inferidos deterministicamente a partir do layout canonico flattenado, sem autoria manual de `Slot`.
## Motivation
- Reduzir superficie de erro humano em builtin layout.
- Alinhar semantica estatica com o contrato de layout flattenado.
- Evitar que erros de layout aparecam tarde em `load-facing` quando sao semanticos.
## Target
- Tornar `Slot(index=...)` proibido na superficie de fonte de builtin field.
- Banir `Slot(...)` da gramatica PBS (erro de sintaxe), sem fallback transitorio.
- Inferir `slotStart` automaticamente no frontend com base em:
- ordem canonica dos campos,
- largura flattenada dos tipos admissiveis,
- composicao builtin aninhada.
- Mover validacoes de layout para `static semantics`.
- Atualizar extracao de metadata reservada para usar slots inferidos (nao lidos de atributo).
- Atualizar fixtures do SDK minimo (`@core:color` etc.) removendo `Slot`.
## Scope
- `frontends/prometeu-frontend-pbs` (semantica + metadata extraction + testes).
- Atualizacao de specs PBS relacionadas a builtin layout e diagnosticos.
## Method
1. Introduzir um resolvedor de layout builtin na fase semantica.
2. Calcular largura flattenada por tipo builtin e mapear offsets por campo.
3. Banir `Slot(...)` no parser (erro de sintaxe deterministico) com remocao da forma da superficie da linguagem.
4. Produzir diagnosticos `E_SEM_*` deterministicos para:
- tipo de campo nao admissivel para layout builtin,
- composicao ciclica de builtin layout,
- impossibilidade de resolver largura flattenada.
5. Persistir offsets inferidos no metadata model de lowering (`IRReservedMetadata`).
6. Remover a exigencia de `Slot` no loader/extractor.
## Acceptance Criteria
- Builtin fields sem `Slot` compilam quando o layout e admissivel.
- Qualquer `Slot(...)` manual e rejeitado como erro de sintaxe (breaking imediato).
- `Pixel(x:int, y:int, color:Color)` recebe offsets inferidos consistentes com flattening.
- Erros de layout builtin deixam de depender de `E_LOAD_*` para serem detectados.
- Suite Gate U cobre positivo e negativo para inferencia de slot.
## Tests
- Positivo: inferencia de offsets em builtin simples e composto.
- Negativo: `Slot` manual presente (falha de parser).
- Negativo: campo com tipo nao admissivel.
- Negativo: ciclo de composicao builtin.
- Asserts por `code`, `phase`, `templateId`, `span`.
## Non-Goals
- Alterar o contrato de runtime/verifier para alem da inferencia de layout no frontend.
- Definir encoding final de artifact/PBX.
## Affected Documents
- `docs/pbs/specs/4. Static Semantics Specification.md`
- `docs/pbs/specs/3. Core Syntax Specification.md`
- `docs/pbs/specs/6.1. Intrinsics and Builtin Types Specification.md`
- `docs/pbs/specs/8. Stdlib Environment Packaging and Loading Specification.md`
- `docs/pbs/specs/12. Diagnostics Specification.md`

View File

@ -248,6 +248,7 @@ Rules:
- Reserved stdlib/toolchain interface modules MAY use attributes where explicitly allowed by syntax and semantics. - Reserved stdlib/toolchain interface modules MAY use attributes where explicitly allowed by syntax and semantics.
- Attribute interpretation is compile-time only unless another specification explicitly lowers its effect into runtime-facing metadata. - Attribute interpretation is compile-time only unless another specification explicitly lowers its effect into runtime-facing metadata.
- Reserved attribute names used by builtin-type and intrinsic shells do not become ordinary value-level syntax. - Reserved attribute names used by builtin-type and intrinsic shells do not become ordinary value-level syntax.
- `Slot(...)` is not part of the v1 attribute surface; source that uses `Slot` must be rejected in syntax phase.
### 6.2 Top-level declarations ### 6.2 Top-level declarations

View File

@ -62,7 +62,7 @@ Rules:
- Attributes are not first-class values and are not reflectable in v1 core. - Attributes are not first-class values and are not reflectable in v1 core.
- Attributes do not automatically survive into runtime or bytecode artifacts. - Attributes do not automatically survive into runtime or bytecode artifacts.
- An attribute affects runtime artifacts only when another specification defines an explicit lowering for its semantic effect. - An attribute affects runtime artifacts only when another specification defines an explicit lowering for its semantic effect.
- In v1 core, the normative reserved attributes are `Host`, `BuiltinType`, `BuiltinConst`, `IntrinsicCall`, and `Slot`. - In v1 core, the normative reserved attributes are `Host`, `BuiltinType`, `BuiltinConst`, and `IntrinsicCall`.
- `Host` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body. - `Host` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body.
- `Host` is invalid on ordinary user-authored modules, top-level `fn`, struct methods, service methods, callbacks, contracts, and constants. - `Host` is invalid on ordinary user-authored modules, top-level `fn`, struct methods, service methods, callbacks, contracts, and constants.
- `Host` metadata is consumed by the compiler during host-binding lowering. - `Host` metadata is consumed by the compiler during host-binding lowering.
@ -70,7 +70,6 @@ Rules:
- `BuiltinType` is valid only on a reserved top-level `declare builtin type` declaration. - `BuiltinType` is valid only on a reserved top-level `declare builtin type` declaration.
- `BuiltinConst` is valid only on a reserved top-level `declare const` declaration that omits an initializer. - `BuiltinConst` is valid only on a reserved top-level `declare const` declaration that omits an initializer.
- `IntrinsicCall` is valid only on a method signature declared directly inside a reserved `declare builtin type` body. - `IntrinsicCall` is valid only on a method signature declared directly inside a reserved `declare builtin type` body.
- `Slot` is valid only on a field declaration in the header of a reserved `declare builtin type`.
- Builtin metadata is consumed by the compiler during VM-owned builtin and intrinsic lowering rather than by host-binding lowering. - Builtin metadata is consumed by the compiler during VM-owned builtin and intrinsic lowering rather than by host-binding lowering.
### 2.4 Tuple shapes ### 2.4 Tuple shapes
@ -184,7 +183,9 @@ Rules:
- `BuiltinType.version` MUST be a positive integer literal. - `BuiltinType.version` MUST be a positive integer literal.
- Two builtin type declarations in the same resolved stdlib environment MUST NOT lower to the same canonical `(name, version)` unless they denote the same builtin declaration after project/module resolution. - Two builtin type declarations in the same resolved stdlib environment MUST NOT lower to the same canonical `(name, version)` unless they denote the same builtin declaration after project/module resolution.
- A builtin field type must be builtin-layout-admissible as defined by the builtin/intrinsics specification. - A builtin field type must be builtin-layout-admissible as defined by the builtin/intrinsics specification.
- A builtin field `Slot` attribute, when present, MUST declare a non-negative integer literal and must match the declaration's canonical flattened layout. - Builtin field slot offsets are inferred by the compiler from canonical field order and flattened builtin layout width.
- Builtin layout composition must be acyclic.
- A builtin field that depends on a builtin layout with unresolved flattened width is statically invalid.
- `IntrinsicCall` MUST declare exactly the named argument `name` and MAY additionally declare `version`. - `IntrinsicCall` MUST declare exactly the named argument `name` and MAY additionally declare `version`.
- `IntrinsicCall.name` MUST be a non-empty string literal. - `IntrinsicCall.name` MUST be a non-empty string literal.
- `IntrinsicCall.version`, when present, MUST be a positive integer literal. - `IntrinsicCall.version`, when present, MUST be a positive integer literal.
@ -657,7 +658,6 @@ At minimum, deterministic static diagnostics are required for:
- invalid `BuiltinType` attribute target, - invalid `BuiltinType` attribute target,
- invalid `BuiltinConst` attribute target, - invalid `BuiltinConst` attribute target,
- invalid `IntrinsicCall` attribute target, - invalid `IntrinsicCall` attribute target,
- invalid `Slot` attribute target,
- duplicate `Host` attribute on one host method, - duplicate `Host` attribute on one host method,
- missing required `Host` attribute on a reserved host method, - missing required `Host` attribute on a reserved host method,
- malformed `Host(module=..., name=..., version=...)` argument set, - malformed `Host(module=..., name=..., version=...)` argument set,
@ -675,9 +675,9 @@ At minimum, deterministic static diagnostics are required for:
- malformed `BuiltinConst(target=..., name=..., version=...)` argument set, - malformed `BuiltinConst(target=..., name=..., version=...)` argument set,
- invalid empty `BuiltinConst.target` or `BuiltinConst.name`, - invalid empty `BuiltinConst.target` or `BuiltinConst.name`,
- invalid non-positive `BuiltinConst.version`, - invalid non-positive `BuiltinConst.version`,
- invalid builtin field `Slot(...)`,
- overlapping builtin field slot range,
- builtin field type not builtin-layout-admissible, - builtin field type not builtin-layout-admissible,
- cyclic builtin layout composition,
- unresolved flattened builtin layout width,
- duplicate canonical builtin type identity, - duplicate canonical builtin type identity,
- duplicate canonical builtin intrinsic identity, - duplicate canonical builtin intrinsic identity,
- duplicate canonical builtin constant identity, - duplicate canonical builtin constant identity,

View File

@ -117,8 +117,8 @@ Illustrative form:
```pbs ```pbs
[BuiltinType(name="vec2", version=1)] [BuiltinType(name="vec2", version=1)]
declare builtin type Vec2( declare builtin type Vec2(
[Slot(0)] pub x: float, pub x: float,
[Slot(1)] pub y: float, pub y: float,
) { ) {
[IntrinsicCall(name="dot")] [IntrinsicCall(name="dot")]
fn dot(other: Vec2) -> float; fn dot(other: Vec2) -> float;
@ -196,22 +196,16 @@ Consequences:
- a builtin field declaration does not imply that builtin values are structs, - a builtin field declaration does not imply that builtin values are structs,
- and a builtin field declaration does not allow user code to provide storage-level implementation bodies. - and a builtin field declaration does not allow user code to provide storage-level implementation bodies.
## 10. `Slot` Metadata ## 10. Builtin Slot Inference
`Slot(n)` identifies the starting flattened slot offset of a builtin field within Builtin field slot offsets are inferred by the compiler, not authored in source.
the enclosing builtin layout.
Rules: Rules:
- `n` is zero-based, - slot offsets are zero-based and deterministic,
- `Slot(n)` refers to the starting slot offset after flattening nested builtin-layout-admissible fields, - offsets are computed from declaration order plus flattened widths of field types,
- slot offsets must be unique, - nested builtin fields contribute their full flattened width to subsequent offsets,
- slot offsets must match the declaration's canonical flattened layout, - and source-level `Slot(...)` metadata is forbidden in v1.
- and overlapping slot ranges are invalid.
`Slot` exists to make canonical layout explicit.
Future grammar may allow offset inference by declaration order, but explicit slot
metadata is valid and authoritative in this temporary contract.
## 11. Builtin Layout Admissibility ## 11. Builtin Layout Admissibility
@ -256,14 +250,14 @@ Example:
```pbs ```pbs
[BuiltinType(name="color", version=1)] [BuiltinType(name="color", version=1)]
declare builtin type Color( declare builtin type Color(
[Slot(0)] pub raw: int, pub raw: int,
); );
[BuiltinType(name="pixel", version=1)] [BuiltinType(name="pixel", version=1)]
declare builtin type Pixel( declare builtin type Pixel(
[Slot(0)] pub x: int, pub x: int,
[Slot(1)] pub y: int, pub y: int,
[Slot(2)] pub color: Color, pub color: Color,
); );
``` ```
@ -332,8 +326,8 @@ Example:
```pbs ```pbs
[BuiltinType(name="vec2", version=1)] [BuiltinType(name="vec2", version=1)]
declare builtin type Vec2( declare builtin type Vec2(
[Slot(0)] pub x: float, pub x: float,
[Slot(1)] pub y: float, pub y: float,
) { ) {
[IntrinsicCall(name="dot")] [IntrinsicCall(name="dot")]
fn dot(other: Vec2) -> float; fn dot(other: Vec2) -> float;
@ -530,13 +524,13 @@ At minimum, a conforming implementation must reject:
1. `declare builtin type` in an ordinary user-authored module, 1. `declare builtin type` in an ordinary user-authored module,
2. missing required `BuiltinType` metadata on a builtin declaration, 2. missing required `BuiltinType` metadata on a builtin declaration,
3. duplicate builtin field names within one declaration, 3. duplicate builtin field names within one declaration,
4. duplicate or overlapping slot assignments, 4. source-level `Slot(...)` usage in builtin declarations,
5. builtin field types that are not builtin-layout-admissible, 5. builtin field types that are not builtin-layout-admissible,
6. `IntrinsicCall` used outside a builtin declaration body, 6. `IntrinsicCall` used outside a builtin declaration body,
7. `BuiltinConst` used outside a reserved top-level `declare const` shell, 7. `BuiltinConst` used outside a reserved top-level `declare const` shell,
8. duplicate canonical builtin type identities in the same compilation environment, 8. duplicate canonical builtin type identities in the same compilation environment,
9. duplicate intrinsic canonical identities within the same builtin owner and version line, 9. duplicate intrinsic canonical identities within the same builtin owner and version line,
10. mismatched declared field slot metadata versus flattened layout, 10. cyclic or unresolved flattened builtin layout composition,
11. executable member bodies inside builtin declarations, 11. executable member bodies inside builtin declarations,
12. any lowering that routes VM-owned builtin operations through the host-binding pipeline. 12. any lowering that routes VM-owned builtin operations through the host-binding pipeline.

View File

@ -64,11 +64,7 @@ public final class PbsFrontendCompiler {
return IRBackendFile.empty(fileId); return IRBackendFile.empty(fileId);
} }
final var admissionErrorBaseline = diagnostics.errorCount(); final var reservedMetadata = reservedMetadataExtractor.extract(ast, sourceKind);
final var reservedMetadata = reservedMetadataExtractor.extract(ast, sourceKind, diagnostics);
if (diagnostics.errorCount() > admissionErrorBaseline) {
return IRBackendFile.empty(fileId);
}
final ReadOnlyList<IRFunction> functions = sourceKind == SourceKind.SDK_INTERFACE final ReadOnlyList<IRFunction> functions = sourceKind == SourceKind.SDK_INTERFACE
? ReadOnlyList.empty() ? ReadOnlyList.empty()

View File

@ -1,6 +0,0 @@
package p.studio.compiler.pbs;
public enum PbsLoadErrors {
E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE,
E_LOAD_DUPLICATE_BUILTIN_SLOT_INDEX,
}

View File

@ -2,31 +2,26 @@ package p.studio.compiler.pbs.metadata;
import p.studio.compiler.models.IRReservedMetadata; import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.models.SourceKind; import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsLoadErrors;
import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span; import p.studio.compiler.pbs.semantics.PbsBuiltinLayoutResolver;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.diagnostics.Diagnostics;
import p.studio.compiler.source.diagnostics.RelatedSpan;
import p.studio.utilities.structures.ReadOnlyList; import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
import java.util.stream.Collectors;
public final class PbsReservedMetadataExtractor { public final class PbsReservedMetadataExtractor {
private static final String ATTR_HOST = "Host"; private static final String ATTR_HOST = "Host";
private static final String ATTR_BUILTIN_TYPE = "BuiltinType"; private static final String ATTR_BUILTIN_TYPE = "BuiltinType";
private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall"; private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall";
private static final String ATTR_BUILTIN_CONST = "BuiltinConst"; private static final String ATTR_BUILTIN_CONST = "BuiltinConst";
private static final String ATTR_SLOT = "Slot";
public IRReservedMetadata extract( public IRReservedMetadata extract(
final PbsAst.File ast, final PbsAst.File ast,
final SourceKind sourceKind, final SourceKind sourceKind) {
final DiagnosticSink diagnostics) {
if (sourceKind != SourceKind.SDK_INTERFACE) { if (sourceKind != SourceKind.SDK_INTERFACE) {
return IRReservedMetadata.empty(); return IRReservedMetadata.empty();
} }
@ -34,6 +29,10 @@ public final class PbsReservedMetadataExtractor {
final var hostMethodBindings = new ArrayList<IRReservedMetadata.HostMethodBinding>(); final var hostMethodBindings = new ArrayList<IRReservedMetadata.HostMethodBinding>();
final var builtinTypeSurfaces = new ArrayList<IRReservedMetadata.BuiltinTypeSurface>(); final var builtinTypeSurfaces = new ArrayList<IRReservedMetadata.BuiltinTypeSurface>();
final var builtinConstSurfaces = new ArrayList<IRReservedMetadata.BuiltinConstSurface>(); final var builtinConstSurfaces = new ArrayList<IRReservedMetadata.BuiltinConstSurface>();
final var inferredLayoutsByName = PbsBuiltinLayoutResolver.resolve(ast).layouts().stream()
.collect(Collectors.toMap(
PbsBuiltinLayoutResolver.ResolvedBuiltinLayout::sourceTypeName,
layout -> layout));
for (final var topDecl : ast.topDecls()) { for (final var topDecl : ast.topDecls()) {
if (topDecl instanceof PbsAst.HostDecl hostDecl) { if (topDecl instanceof PbsAst.HostDecl hostDecl) {
@ -41,7 +40,7 @@ public final class PbsReservedMetadataExtractor {
continue; continue;
} }
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
extractBuiltinTypeSurface(builtinTypeDecl, builtinTypeSurfaces, diagnostics); extractBuiltinTypeSurface(builtinTypeDecl, inferredLayoutsByName, builtinTypeSurfaces);
continue; continue;
} }
if (topDecl instanceof PbsAst.ConstDecl constDecl) { if (topDecl instanceof PbsAst.ConstDecl constDecl) {
@ -79,8 +78,8 @@ public final class PbsReservedMetadataExtractor {
private void extractBuiltinTypeSurface( private void extractBuiltinTypeSurface(
final PbsAst.BuiltinTypeDecl builtinTypeDecl, final PbsAst.BuiltinTypeDecl builtinTypeDecl,
final List<IRReservedMetadata.BuiltinTypeSurface> builtinTypeSurfaces, final Map<String, PbsBuiltinLayoutResolver.ResolvedBuiltinLayout> inferredLayoutsByName,
final DiagnosticSink diagnostics) { final List<IRReservedMetadata.BuiltinTypeSurface> builtinTypeSurfaces) {
final var builtinTypeAttribute = firstAttributeNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE); final var builtinTypeAttribute = firstAttributeNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE);
if (builtinTypeAttribute.isEmpty()) { if (builtinTypeAttribute.isEmpty()) {
return; return;
@ -89,45 +88,18 @@ public final class PbsReservedMetadataExtractor {
final var builtinTypeMetadata = builtinTypeAttribute.get(); final var builtinTypeMetadata = builtinTypeAttribute.get();
final var canonicalTypeName = stringArgument(builtinTypeMetadata, "name").orElse(builtinTypeDecl.name()); final var canonicalTypeName = stringArgument(builtinTypeMetadata, "name").orElse(builtinTypeDecl.name());
final var canonicalVersion = longArgument(builtinTypeMetadata, "version").orElse(0L); final var canonicalVersion = longArgument(builtinTypeMetadata, "version").orElse(0L);
final var inferredLayout = inferredLayoutsByName.get(builtinTypeDecl.name());
final var fields = new ArrayList<IRReservedMetadata.BuiltinFieldSurface>(builtinTypeDecl.fields().size()); final var fields = new ArrayList<IRReservedMetadata.BuiltinFieldSurface>(builtinTypeDecl.fields().size());
final var firstFieldBySlot = new HashMap<Long, Span>(); for (int i = 0; i < builtinTypeDecl.fields().size(); i++) {
for (final var field : builtinTypeDecl.fields()) { final var field = builtinTypeDecl.fields().get(i);
final var slotAttribute = firstAttributeNamed(field.attributes(), ATTR_SLOT); final var slotStart = inferredLayout != null && i < inferredLayout.fields().size()
if (slotAttribute.isEmpty()) { ? inferredLayout.fields().get(i).slotStart()
reportUnsupportedSurface( : i;
diagnostics,
field.span(),
"Builtin field '%s' in '%s' requires Slot(index=...) for lowering admission"
.formatted(field.name(), builtinTypeDecl.name()));
continue;
}
final var slotStart = longArgument(slotAttribute.get(), "index");
if (slotStart.isEmpty()) {
reportUnsupportedSurface(
diagnostics,
slotAttribute.get().span(),
"Builtin field '%s' in '%s' requires numeric Slot(index=...) for lowering admission"
.formatted(field.name(), builtinTypeDecl.name()));
continue;
}
final var firstField = firstFieldBySlot.putIfAbsent(slotStart.getAsLong(), field.span());
if (firstField != null) {
Diagnostics.error(
diagnostics,
PbsLoadErrors.E_LOAD_DUPLICATE_BUILTIN_SLOT_INDEX.name(),
"Duplicate builtin slot index %d in '%s'".formatted(slotStart.getAsLong(), builtinTypeDecl.name()),
field.span(),
List.of(new RelatedSpan("First builtin field using this slot is here", firstField)));
continue;
}
fields.add(new IRReservedMetadata.BuiltinFieldSurface( fields.add(new IRReservedMetadata.BuiltinFieldSurface(
field.name(), field.name(),
typeSurfaceKey(field.typeRef()), typeSurfaceKey(field.typeRef()),
slotStart.getAsLong(), slotStart,
field.span())); field.span()));
} }
@ -171,17 +143,6 @@ public final class PbsReservedMetadataExtractor {
constDecl.span())); constDecl.span()));
} }
private void reportUnsupportedSurface(
final DiagnosticSink diagnostics,
final Span span,
final String message) {
Diagnostics.error(
diagnostics,
PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name(),
message,
span);
}
private Optional<PbsAst.Attribute> firstAttributeNamed( private Optional<PbsAst.Attribute> firstAttributeNamed(
final ReadOnlyList<PbsAst.Attribute> attributes, final ReadOnlyList<PbsAst.Attribute> attributes,
final String attributeName) { final String attributeName) {

View File

@ -472,6 +472,10 @@ public final class PbsParser {
if (cursor.match(PbsTokenKind.LEFT_PAREN)) { if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
parseAttributeArguments(arguments); parseAttributeArguments(arguments);
} }
if ("Slot".equals(name.lexeme())) {
report(name, ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Attribute 'Slot' is not part of PBS syntax; builtin slots are inferred by the compiler");
}
long end = Math.max(leftBracket.end(), name.end()); long end = Math.max(leftBracket.end(), name.end());
if (cursor.match(PbsTokenKind.RIGHT_BRACKET)) { if (cursor.match(PbsTokenKind.RIGHT_BRACKET)) {

View File

@ -0,0 +1,277 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public final class PbsBuiltinLayoutResolver {
private static final Set<String> BUILTIN_SCALAR_TYPES = Set.of("int", "float", "bool", "str");
private enum ResolveState {
UNVISITED,
RESOLVING,
RESOLVED,
FAILED
}
private enum WidthStatus {
OK,
CYCLE,
FAILED
}
private final Map<String, PbsAst.BuiltinTypeDecl> builtinDeclByName = new LinkedHashMap<>();
private final Map<String, ResolveState> stateByBuiltinName = new HashMap<>();
private final Map<String, ResolvedBuiltinLayout> resolvedLayouts = new HashMap<>();
private final Set<String> reportedCycles = new HashSet<>();
private final DiagnosticSink diagnostics;
private PbsBuiltinLayoutResolver(
final PbsAst.File ast,
final DiagnosticSink diagnostics) {
this.diagnostics = diagnostics;
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl)) {
continue;
}
builtinDeclByName.putIfAbsent(builtinTypeDecl.name(), builtinTypeDecl);
}
for (final var builtinTypeName : builtinDeclByName.keySet()) {
stateByBuiltinName.put(builtinTypeName, ResolveState.UNVISITED);
}
}
public static ResolvedBuiltinLayoutTable resolve(final PbsAst.File ast) {
return new PbsBuiltinLayoutResolver(ast, null).resolveAll();
}
public static ResolvedBuiltinLayoutTable resolve(
final PbsAst.File ast,
final DiagnosticSink diagnostics) {
return new PbsBuiltinLayoutResolver(ast, diagnostics).resolveAll();
}
private ResolvedBuiltinLayoutTable resolveAll() {
for (final var builtinTypeName : builtinDeclByName.keySet()) {
resolveBuiltinLayout(builtinTypeName);
}
return new ResolvedBuiltinLayoutTable(ReadOnlyList.wrap(new ArrayList<>(resolvedLayouts.values())));
}
private WidthResult resolveBuiltinLayout(final String builtinTypeName) {
final var state = stateByBuiltinName.getOrDefault(builtinTypeName, ResolveState.UNVISITED);
if (state == ResolveState.RESOLVED) {
return WidthResult.ok(resolvedLayouts.get(builtinTypeName).totalWidth());
}
if (state == ResolveState.FAILED) {
return WidthResult.failed();
}
if (state == ResolveState.RESOLVING) {
return WidthResult.cycle();
}
final var declaration = builtinDeclByName.get(builtinTypeName);
if (declaration == null) {
return WidthResult.failed();
}
stateByBuiltinName.put(builtinTypeName, ResolveState.RESOLVING);
long slotCursor = 0L;
var failed = false;
final var fieldLayouts = new ArrayList<ResolvedBuiltinFieldLayout>(declaration.fields().size());
for (final var field : declaration.fields()) {
final var width = resolveFieldWidth(builtinTypeName, field);
if (width.status() != WidthStatus.OK) {
failed = true;
continue;
}
fieldLayouts.add(new ResolvedBuiltinFieldLayout(
field.name(),
slotCursor,
width.width(),
field.span()));
slotCursor += width.width();
}
if (failed) {
stateByBuiltinName.put(builtinTypeName, ResolveState.FAILED);
return WidthResult.failed();
}
final var layout = new ResolvedBuiltinLayout(
declaration.name(),
slotCursor,
ReadOnlyList.wrap(fieldLayouts));
resolvedLayouts.put(builtinTypeName, layout);
stateByBuiltinName.put(builtinTypeName, ResolveState.RESOLVED);
return WidthResult.ok(slotCursor);
}
private WidthResult resolveFieldWidth(
final String ownerBuiltinName,
final PbsAst.BuiltinFieldDecl field) {
final var typeRef = normalizeFieldType(field.typeRef());
if (typeRef == null || typeRef.kind() != PbsAst.TypeRefKind.SIMPLE) {
reportTypeNotAdmissible(ownerBuiltinName, field, field.typeRef());
return WidthResult.failed();
}
if (BUILTIN_SCALAR_TYPES.contains(typeRef.name())) {
return WidthResult.ok(1L);
}
final var nestedBuiltin = builtinDeclByName.get(typeRef.name());
if (nestedBuiltin == null) {
reportTypeNotAdmissible(ownerBuiltinName, field, field.typeRef());
return WidthResult.failed();
}
final var nestedWidth = resolveBuiltinLayout(typeRef.name());
if (nestedWidth.status() == WidthStatus.OK) {
return nestedWidth;
}
if (nestedWidth.status() == WidthStatus.CYCLE) {
reportCycleIfNeeded(typeRef.name(), ownerBuiltinName, field.name(), field.span());
return WidthResult.cycle();
}
reportUnresolvableWidth(ownerBuiltinName, field, typeRef.name());
return WidthResult.failed();
}
private PbsAst.TypeRef normalizeFieldType(final PbsAst.TypeRef typeRef) {
var current = typeRef;
while (current != null && current.kind() == PbsAst.TypeRefKind.GROUP) {
current = current.inner();
}
return current;
}
private void reportTypeNotAdmissible(
final String ownerBuiltinName,
final PbsAst.BuiltinFieldDecl field,
final PbsAst.TypeRef typeRef) {
if (diagnostics == null) {
return;
}
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name(),
"Builtin field '%s.%s' uses non-admissible layout type '%s'"
.formatted(ownerBuiltinName, field.name(), renderTypeSurface(typeRef)),
field.span());
}
private void reportCycleIfNeeded(
final String targetBuiltinName,
final String ownerBuiltinName,
final String ownerFieldName,
final Span span) {
if (diagnostics == null) {
return;
}
final String cycleKey = ownerBuiltinName == null
? "cycle:" + targetBuiltinName
: "cycle:" + ownerBuiltinName + "." + ownerFieldName + "->" + targetBuiltinName;
if (!reportedCycles.add(cycleKey)) {
return;
}
final var primarySpan = span != null
? span
: Optional.ofNullable(builtinDeclByName.get(targetBuiltinName))
.map(PbsAst.BuiltinTypeDecl::span)
.orElse(Span.none());
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_CYCLIC_COMPOSITION.name(),
ownerBuiltinName == null
? "Builtin layout cycle detected for '%s'".formatted(targetBuiltinName)
: "Builtin field '%s.%s' introduces cyclic builtin layout dependency on '%s'"
.formatted(ownerBuiltinName, ownerFieldName, targetBuiltinName),
primarySpan);
}
private void reportUnresolvableWidth(
final String ownerBuiltinName,
final PbsAst.BuiltinFieldDecl field,
final String nestedBuiltinName) {
if (diagnostics == null) {
return;
}
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_UNRESOLVABLE_WIDTH.name(),
"Builtin field '%s.%s' cannot resolve flattened layout width for '%s'"
.formatted(ownerBuiltinName, field.name(), nestedBuiltinName),
field.span());
}
private String renderTypeSurface(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return "void";
}
return switch (typeRef.kind()) {
case SIMPLE -> typeRef.name();
case SELF -> "Self";
case OPTIONAL -> "optional %s".formatted(renderTypeSurface(typeRef.inner()));
case UNIT -> "void";
case GROUP -> "(%s)".formatted(renderTypeSurface(typeRef.inner()));
case NAMED_TUPLE -> "(%s)".formatted(typeRef.fields().stream()
.map(field -> "%s: %s".formatted(field.label(), renderTypeSurface(field.typeRef())))
.reduce((left, right) -> left + ", " + right)
.orElse(""));
case ERROR -> "<error>";
};
}
private record WidthResult(
WidthStatus status,
long width) {
static WidthResult ok(final long width) {
return new WidthResult(WidthStatus.OK, width);
}
static WidthResult cycle() {
return new WidthResult(WidthStatus.CYCLE, 0L);
}
static WidthResult failed() {
return new WidthResult(WidthStatus.FAILED, 0L);
}
}
public record ResolvedBuiltinLayoutTable(
ReadOnlyList<ResolvedBuiltinLayout> layouts) {
public Optional<ResolvedBuiltinLayout> findByName(final String builtinTypeName) {
for (final var layout : layouts) {
if (layout.sourceTypeName().equals(builtinTypeName)) {
return Optional.of(layout);
}
}
return Optional.empty();
}
}
public record ResolvedBuiltinLayout(
String sourceTypeName,
long totalWidth,
ReadOnlyList<ResolvedBuiltinFieldLayout> fields) {
}
public record ResolvedBuiltinFieldLayout(
String fieldName,
long slotStart,
long slotWidth,
Span span) {
}
}

View File

@ -19,14 +19,12 @@ public final class PbsDeclarationSemanticsValidator {
private static final String ATTR_BUILTIN_TYPE = "BuiltinType"; private static final String ATTR_BUILTIN_TYPE = "BuiltinType";
private static final String ATTR_BUILTIN_CONST = "BuiltinConst"; private static final String ATTR_BUILTIN_CONST = "BuiltinConst";
private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall"; private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall";
private static final String ATTR_SLOT = "Slot";
private static final Set<String> RESERVED_ATTRIBUTES = Set.of( private static final Set<String> RESERVED_ATTRIBUTES = Set.of(
ATTR_HOST, ATTR_HOST,
ATTR_BUILTIN_TYPE, ATTR_BUILTIN_TYPE,
ATTR_BUILTIN_CONST, ATTR_BUILTIN_CONST,
ATTR_INTRINSIC_CALL, ATTR_INTRINSIC_CALL);
ATTR_SLOT);
private final NameTable nameTable = new NameTable(); private final NameTable nameTable = new NameTable();
private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator(); private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
@ -150,6 +148,9 @@ public final class PbsDeclarationSemanticsValidator {
} }
constSemanticsValidator.validate(ast, diagnostics); constSemanticsValidator.validate(ast, diagnostics);
if (interfaceModule) {
PbsBuiltinLayoutResolver.resolve(ast, diagnostics);
}
} }
private void validateStructDeclaration( private void validateStructDeclaration(
@ -448,29 +449,15 @@ public final class PbsDeclarationSemanticsValidator {
private void validateBuiltinFieldAttributes( private void validateBuiltinFieldAttributes(
final PbsAst.BuiltinFieldDecl field, final PbsAst.BuiltinFieldDecl field,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
final var slotAttributes = attributesNamed(field.attributes(), ATTR_SLOT);
for (final var attribute : field.attributes()) { for (final var attribute : field.attributes()) {
if (!isReservedAttribute(attribute.name())) { if (!isReservedAttribute(attribute.name())) {
continue; continue;
} }
if (ATTR_SLOT.equals(attribute.name())) {
continue;
}
reportInvalidReservedAttributeTarget( reportInvalidReservedAttributeTarget(
attribute, attribute,
"Attribute '%s' is not valid on builtin fields".formatted(attribute.name()), "Attribute '%s' is not valid on builtin fields".formatted(attribute.name()),
diagnostics); diagnostics);
} }
if (slotAttributes.size() > 1) {
for (int i = 1; i < slotAttributes.size(); i++) {
reportDuplicateReservedAttribute(slotAttributes.get(i), ATTR_SLOT, diagnostics);
}
}
if (!slotAttributes.isEmpty()) {
validateSlotAttributeShape(slotAttributes.getFirst(), diagnostics);
}
} }
private void validateIntrinsicCallAttributes( private void validateIntrinsicCallAttributes(
@ -553,17 +540,6 @@ public final class PbsDeclarationSemanticsValidator {
} }
} }
private void validateSlotAttributeShape(
final PbsAst.Attribute attribute,
final DiagnosticSink diagnostics) {
final var args = validateNamedArguments(attribute, Set.of("index"), Set.of(), diagnostics);
if (args == null) {
return;
}
validateRequiredIntArgument(attribute, args, "index", false, diagnostics);
}
private Map<String, PbsAst.AttributeValue> validateNamedArguments( private Map<String, PbsAst.AttributeValue> validateNamedArguments(
final PbsAst.Attribute attribute, final PbsAst.Attribute attribute,
final Set<String> requiredNames, final Set<String> requiredNames,

View File

@ -21,6 +21,9 @@ public enum PbsSemanticsErrors {
E_SEM_CONST_INITIALIZER_TYPE_MISMATCH, E_SEM_CONST_INITIALIZER_TYPE_MISMATCH,
E_SEM_CONST_CYCLIC_DEPENDENCY, E_SEM_CONST_CYCLIC_DEPENDENCY,
E_SEM_CONST_UNRESOLVED_REFERENCE, E_SEM_CONST_UNRESOLVED_REFERENCE,
E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE,
E_SEM_BUILTIN_LAYOUT_CYCLIC_COMPOSITION,
E_SEM_BUILTIN_LAYOUT_UNRESOLVABLE_WIDTH,
E_SEM_INVALID_RETURN_INSIDE_CTOR, E_SEM_INVALID_RETURN_INSIDE_CTOR,
E_SEM_APPLY_NON_CALLABLE_TARGET, E_SEM_APPLY_NON_CALLABLE_TARGET,
E_SEM_APPLY_UNRESOLVED_OVERLOAD, E_SEM_APPLY_UNRESOLVED_OVERLOAD,

View File

@ -1,9 +1,9 @@
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 1)] pub g: int, pub g: int,
[Slot(index = 2)] pub b: int, pub b: int,
[Slot(index = 3)] pub a: int pub a: int
) { ) {
[IntrinsicCall(name = "core.color.pack", version = 1)] [IntrinsicCall(name = "core.color.pack", version = 1)]
fn pack() -> int; fn pack() -> int;

View File

@ -82,22 +82,24 @@ class PbsDiagnosticsContractTest {
} }
@Test @Test
void shouldTagLoadFacingDiagnosticsWithStableTemplateAndAttribution() { void shouldTagBuiltinLayoutDiagnosticsWithStaticSemanticsPhase() {
final var source = """ final var source = """
declare struct S(value: int);
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
pub r: int pub raw: S
); ) {
}
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(9), source, diagnostics, p.studio.compiler.models.SourceKind.SDK_INTERFACE); new PbsFrontendCompiler().compileFile(new FileId(9), source, diagnostics, p.studio.compiler.models.SourceKind.SDK_INTERFACE);
final var diagnostic = diagnostics.stream() final var diagnostic = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name())) .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name()))
.findFirst() .findFirst()
.orElseThrow(); .orElseThrow();
assertEquals(DiagnosticPhase.LOAD_FACING_REJECTION, diagnostic.getPhase()); assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase());
assertEquals(diagnostic.getCode(), diagnostic.getTemplateId()); assertEquals(diagnostic.getCode(), diagnostic.getTemplateId());
assertEquals(9, diagnostic.getSpan().getFileId().getId()); assertEquals(9, diagnostic.getSpan().getFileId().getId());
} }

View File

@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test;
import p.studio.compiler.models.SourceKind; import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.lexer.LexErrors; import p.studio.compiler.pbs.lexer.LexErrors;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors; import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.FileId;
@ -107,9 +106,9 @@ class PbsFrontendCompilerTest {
final var source = """ final var source = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 1)] pub g: int, pub g: int,
[Slot(index = 2)] pub b: int pub b: int
) { ) {
[IntrinsicCall(name = "core.color.pack")] [IntrinsicCall(name = "core.color.pack")]
fn pack() -> int; fn pack() -> int;
@ -136,49 +135,61 @@ class PbsFrontendCompilerTest {
} }
@Test @Test
void shouldRejectUnsupportedBuiltinSlotInferenceAtLoadAdmissionBoundary() { void shouldInferBuiltinFieldSlotsFromDeclarationOrder() {
final var source = """ final var source = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
pub r: int, pub r: int,
[Slot(index = 1)] pub g: int pub g: int
); ) {
}
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler(); final var compiler = new PbsFrontendCompiler();
final var fileBackend = compiler.compileFile(new FileId(3), source, diagnostics, SourceKind.SDK_INTERFACE); final var fileBackend = compiler.compileFile(new FileId(3), source, diagnostics, SourceKind.SDK_INTERFACE);
final var rejectionDiagnostic = diagnostics.stream() assertTrue(diagnostics.isEmpty());
.filter(d -> d.getCode().equals(PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name())) final var fields = fileBackend.reservedMetadata().builtinTypeSurfaces().getFirst().fields();
.findFirst() assertEquals(2, fields.size());
.orElseThrow(); assertEquals(0L, fields.get(0).slotStart());
assertEquals(DiagnosticPhase.LOAD_FACING_REJECTION, rejectionDiagnostic.getPhase()); assertEquals(1L, fields.get(1).slotStart());
assertEquals(rejectionDiagnostic.getCode(), rejectionDiagnostic.getTemplateId());
assertEquals(3, rejectionDiagnostic.getSpan().getFileId().getId());
assertEquals(0, fileBackend.functions().size()); assertEquals(0, fileBackend.functions().size());
} }
@Test @Test
void shouldRejectDuplicateBuiltinSlotIndexesAtLoadAdmissionBoundary() { void shouldInferBuiltinFieldSlotsUsingFlattenedNestedWidth() {
final var source = """ final var source = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 0)] pub g: int pub g: int,
); pub b: int,
pub a: int
) {
}
[BuiltinType(name = "Pixel", version = 1)]
declare builtin type Pixel(
pub x: int,
pub y: int,
pub color: Color
) {
}
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler(); final var compiler = new PbsFrontendCompiler();
compiler.compileFile(new FileId(5), source, diagnostics, SourceKind.SDK_INTERFACE); final var fileBackend = compiler.compileFile(new FileId(5), source, diagnostics, SourceKind.SDK_INTERFACE);
final var rejectionDiagnostic = diagnostics.stream() assertTrue(diagnostics.isEmpty());
.filter(d -> d.getCode().equals(PbsLoadErrors.E_LOAD_DUPLICATE_BUILTIN_SLOT_INDEX.name())) final var pixel = fileBackend.reservedMetadata().builtinTypeSurfaces().stream()
.filter(t -> t.sourceTypeName().equals("Pixel"))
.findFirst() .findFirst()
.orElseThrow(); .orElseThrow();
assertEquals(DiagnosticPhase.LOAD_FACING_REJECTION, rejectionDiagnostic.getPhase()); assertEquals(3, pixel.fields().size());
assertEquals(rejectionDiagnostic.getCode(), rejectionDiagnostic.getTemplateId()); assertEquals(0L, pixel.fields().get(0).slotStart());
assertEquals(1, rejectionDiagnostic.getRelated().size()); assertEquals(1L, pixel.fields().get(1).slotStart());
assertEquals(2L, pixel.fields().get(2).slotStart());
} }
} }

View File

@ -53,7 +53,7 @@ class PbsGateUSdkInterfaceConformanceTest {
final var source = """ final var source = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int pub r: int
) { ) {
} }
@ -159,7 +159,7 @@ class PbsGateUSdkInterfaceConformanceTest {
final var source = """ final var source = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int pub r: int
) { ) {
} }
@ -216,12 +216,12 @@ class PbsGateUSdkInterfaceConformanceTest {
} }
@Test @Test
void gateU_shouldExposeReservedMetadataAndRejectUnsupportedLoadAdmission() { void gateU_shouldExposeReservedMetadataAndRejectInvalidBuiltinLayoutAtSemantics() {
final var validSource = """ final var validSource = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 1)] pub g: int pub g: int
) { ) {
[IntrinsicCall(name = "core.color.pack", version = 1)] [IntrinsicCall(name = "core.color.pack", version = 1)]
fn pack() -> int; fn pack() -> int;
@ -247,9 +247,10 @@ class PbsGateUSdkInterfaceConformanceTest {
assertEquals(1, validBackend.reservedMetadata().builtinConstSurfaces().size()); assertEquals(1, validBackend.reservedMetadata().builtinConstSurfaces().size());
final var invalidSource = """ final var invalidSource = """
declare struct RGBA(raw: int);
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
pub r: int pub raw: RGBA
) { ) {
} }
"""; """;
@ -259,9 +260,12 @@ class PbsGateUSdkInterfaceConformanceTest {
invalidSource, invalidSource,
invalidDiagnostics, invalidDiagnostics,
SourceKind.SDK_INTERFACE); SourceKind.SDK_INTERFACE);
final var unsupportedLoad = firstDiagnostic(invalidDiagnostics, final var invalidLayout = firstDiagnostic(invalidDiagnostics,
d -> d.getCode().equals(PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name())); d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name()));
assertStableDiagnosticIdentity(unsupportedLoad, PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name(), DiagnosticPhase.LOAD_FACING_REJECTION); assertStableDiagnosticIdentity(
invalidLayout,
PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name(),
DiagnosticPhase.STATIC_SEMANTICS);
} }
private WorkspaceCompileResult compileWorkspaceModule( private WorkspaceCompileResult compileWorkspaceModule(

View File

@ -256,9 +256,9 @@ class PbsModuleVisibilityTest {
""" """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 1)] pub g: int, pub g: int,
[Slot(index = 2)] pub b: int pub b: int
) { ) {
[IntrinsicCall(name = "core.color.pack", version = 1)] [IntrinsicCall(name = "core.color.pack", version = 1)]
fn pack() -> int; fn pack() -> int;

View File

@ -221,9 +221,9 @@ class PbsParserTest {
final var source = """ final var source = """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 1)] pub g: int, pub g: int,
[Slot(index = 2)] pub b: int, pub b: int,
) { ) {
[IntrinsicCall(name = "color.pack", version = 1)] [IntrinsicCall(name = "color.pack", version = 1)]
fn pack() -> int; fn pack() -> int;
@ -251,7 +251,7 @@ class PbsParserTest {
assertEquals(1, builtinType.attributes().size()); assertEquals(1, builtinType.attributes().size());
assertEquals(3, builtinType.fields().size()); assertEquals(3, builtinType.fields().size());
assertEquals(1, builtinType.signatures().size()); assertEquals(1, builtinType.signatures().size());
assertEquals(1, builtinType.fields().getFirst().attributes().size()); assertEquals(0, builtinType.fields().getFirst().attributes().size());
assertEquals(1, builtinType.signatures().getFirst().attributes().size()); assertEquals(1, builtinType.signatures().getFirst().attributes().size());
final var hostDecl = assertInstanceOf(PbsAst.HostDecl.class, ast.topDecls().get(1)); final var hostDecl = assertInstanceOf(PbsAst.HostDecl.class, ast.topDecls().get(1));
@ -303,6 +303,27 @@ class PbsParserTest {
assertTrue(diagnostics.hasErrors(), "Function-level else fallback syntax is not valid PBS core syntax"); assertTrue(diagnostics.hasErrors(), "Function-level else fallback syntax is not valid PBS core syntax");
} }
@Test
void shouldRejectSlotAttributeInInterfaceModuleMode() {
final var source = """
[BuiltinType(name = "Color", version = 1)]
declare builtin type Color(
[Slot(index = 0)] pub raw: int
) {
}
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
PbsParser.parse(
PbsLexer.lex(source, fileId, diagnostics),
fileId,
diagnostics,
PbsParser.ParseMode.INTERFACE_MODULE);
assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_DECL_SHAPE.name())));
}
@Test @Test
void shouldReportEnumMixedAndDuplicateCases() { void shouldReportEnumMixedAndDuplicateCases() {
final var source = """ final var source = """

View File

@ -16,7 +16,7 @@ class PbsInterfaceModuleSemanticsTest {
final var source = """ final var source = """
[BuiltinType(name = "color", version = 1)] [BuiltinType(name = "color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub raw: int pub raw: int
) { ) {
[IntrinsicCall(name = "pack", version = 1)] [IntrinsicCall(name = "pack", version = 1)]
fn pack() -> int; fn pack() -> int;
@ -98,4 +98,71 @@ class PbsInterfaceModuleSemanticsTest {
.count(); .count();
assertTrue(nonDeclarativeCount >= 3); assertTrue(nonDeclarativeCount >= 3);
} }
@Test
void shouldRejectBuiltinFieldWithNonAdmissibleLayoutType() {
final var source = """
declare struct RGBA(raw: int);
[BuiltinType(name = "Color", version = 1)]
declare builtin type Color(
pub raw: RGBA
) {
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics, SourceKind.SDK_INTERFACE);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name())));
}
@Test
void shouldRejectCyclicBuiltinLayoutComposition() {
final var source = """
[BuiltinType(name = "A", version = 1)]
declare builtin type A(
pub b: B
) {
}
[BuiltinType(name = "B", version = 1)]
declare builtin type B(
pub a: A
) {
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(1), source, diagnostics, SourceKind.SDK_INTERFACE);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_CYCLIC_COMPOSITION.name())));
}
@Test
void shouldRejectBuiltinFieldWhenNestedBuiltinWidthIsUnresolvable() {
final var source = """
declare struct RGBA(raw: int);
[BuiltinType(name = "Bad", version = 1)]
declare builtin type Bad(
pub raw: RGBA
) {
}
[BuiltinType(name = "Pixel", version = 1)]
declare builtin type Pixel(
pub bad: Bad
) {
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(2), source, diagnostics, SourceKind.SDK_INTERFACE);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_UNRESOLVABLE_WIDTH.name())));
}
} }

View File

@ -237,9 +237,9 @@ class PBSFrontendPhaseServiceTest {
Files.writeString(sourceFile, """ Files.writeString(sourceFile, """
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
[Slot(index = 0)] pub r: int, pub r: int,
[Slot(index = 1)] pub g: int, pub g: int,
[Slot(index = 2)] pub b: int pub b: int
) { ) {
[IntrinsicCall(name = "color.pack", version = 1)] [IntrinsicCall(name = "color.pack", version = 1)]
fn pack() -> int; fn pack() -> int;
@ -557,7 +557,7 @@ class PBSFrontendPhaseServiceTest {
} }
@Test @Test
void shouldEmitLoadFacingRejectionForUnsupportedStdlibReservedMetadataShape() throws IOException { void shouldEmitStaticSemanticsErrorForInvalidStdlibBuiltinLayoutShape() throws IOException {
final var projectRoot = tempDir.resolve("project-stdlib-invalid-metadata"); final var projectRoot = tempDir.resolve("project-stdlib-invalid-metadata");
final var sourceRoot = projectRoot.resolve("src"); final var sourceRoot = projectRoot.resolve("src");
final var modulePath = sourceRoot.resolve("app"); final var modulePath = sourceRoot.resolve("app");
@ -589,10 +589,12 @@ class PBSFrontendPhaseServiceTest {
ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile( ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile(
"main.pbs", "main.pbs",
""" """
declare struct RGBA(raw: int);
[BuiltinType(name = "Color", version = 1)] [BuiltinType(name = "Color", version = 1)]
declare builtin type Color( declare builtin type Color(
pub r: int pub raw: RGBA
); ) {
}
"""))), """))),
"pub struct Color;"); "pub struct Color;");
final var frontendService = new PBSFrontendPhaseService( final var frontendService = new PBSFrontendPhaseService(
@ -611,12 +613,12 @@ class PBSFrontendPhaseServiceTest {
LogAggregator.empty(), LogAggregator.empty(),
BuildingIssueSink.empty()); BuildingIssueSink.empty());
final var rejectionDiagnostic = diagnostics.stream() final var semanticsDiagnostic = diagnostics.stream()
.filter(d -> d.getCode().equals(p.studio.compiler.pbs.PbsLoadErrors.E_LOAD_UNSUPPORTED_RESERVED_METADATA_SURFACE.name())) .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name()))
.findFirst() .findFirst()
.orElseThrow(); .orElseThrow();
assertEquals(p.studio.compiler.source.diagnostics.DiagnosticPhase.LOAD_FACING_REJECTION, rejectionDiagnostic.getPhase()); assertEquals(p.studio.compiler.source.diagnostics.DiagnosticPhase.STATIC_SEMANTICS, semanticsDiagnostic.getPhase());
assertEquals(rejectionDiagnostic.getCode(), rejectionDiagnostic.getTemplateId()); assertEquals(semanticsDiagnostic.getCode(), semanticsDiagnostic.getTemplateId());
assertEquals(1, irBackend.getFunctions().size()); assertEquals(1, irBackend.getFunctions().size());
assertEquals("run", irBackend.getFunctions().getFirst().name()); assertEquals("run", irBackend.getFunctions().getFirst().name());
} }