implements PR-19.4 global semantics identity and collision validation

This commit is contained in:
bQUARKz 2026-03-26 19:12:42 +00:00
parent 738eea71ee
commit 71a993ad4a
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 179 additions and 0 deletions

View File

@ -160,6 +160,79 @@ final class PbsDeclarationRuleValidator {
}
}
void validateGlobalDeclaration(final PbsAst.GlobalDecl globalDecl) {
if (globalDecl.explicitType() == null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MISSING_GLOBAL_TYPE_ANNOTATION.name(),
"Global declaration '%s' must include an explicit type annotation".formatted(globalDecl.name()),
globalDecl.span());
} else {
PbsTypeSurfaceSemanticsValidator.validateTypeSurfaceRecursive(
List.of(globalDecl.explicitType()),
"global '%s'".formatted(globalDecl.name()),
false,
diagnostics);
}
if (globalDecl.initializer() == null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MISSING_GLOBAL_INITIALIZER.name(),
"Global declaration '%s' must include an initializer".formatted(globalDecl.name()),
globalDecl.span());
return;
}
final var invalidSpan = firstUnsupportedGlobalInitializerSpan(globalDecl.initializer());
if (invalidSpan != null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_GLOBAL_UNSUPPORTED_INITIALIZER.name(),
"Global initializer for '%s' uses an unsupported form".formatted(globalDecl.name()),
invalidSpan);
}
}
private Span firstUnsupportedGlobalInitializerSpan(final PbsAst.Expression expression) {
if (expression == null) {
return null;
}
if (expression instanceof PbsAst.IntLiteralExpr
|| expression instanceof PbsAst.FloatLiteralExpr
|| expression instanceof PbsAst.BoundedLiteralExpr
|| expression instanceof PbsAst.StringLiteralExpr
|| expression instanceof PbsAst.BoolLiteralExpr
|| expression instanceof PbsAst.IdentifierExpr
|| expression instanceof PbsAst.ThisExpr
|| expression instanceof PbsAst.UnitExpr) {
return null;
}
if (expression instanceof PbsAst.GroupExpr groupExpr) {
return firstUnsupportedGlobalInitializerSpan(groupExpr.expression());
}
if (expression instanceof PbsAst.UnaryExpr unaryExpr) {
return firstUnsupportedGlobalInitializerSpan(unaryExpr.expression());
}
if (expression instanceof PbsAst.BinaryExpr binaryExpr) {
final var left = firstUnsupportedGlobalInitializerSpan(binaryExpr.left());
if (left != null) {
return left;
}
return firstUnsupportedGlobalInitializerSpan(binaryExpr.right());
}
if (expression instanceof PbsAst.MemberExpr memberExpr) {
return firstUnsupportedGlobalInitializerSpan(memberExpr.receiver());
}
if (expression instanceof PbsAst.NewExpr newExpr) {
for (final var argument : newExpr.arguments()) {
final var invalid = firstUnsupportedGlobalInitializerSpan(argument);
if (invalid != null) {
return invalid;
}
}
return null;
}
return expression.span();
}
private void validateReturnSurface(
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,

View File

@ -61,6 +61,7 @@ public final class PbsDeclarationSemanticsValidator {
false,
binder,
rules);
binder.registerVisibleTopLevelSurface(functionDecl.name(), functionDecl.span(), "function");
continue;
}
@ -79,6 +80,7 @@ public final class PbsDeclarationSemanticsValidator {
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
binder.registerType(serviceDecl.name(), serviceDecl.span(), "service");
binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton");
binder.registerVisibleTopLevelSurface(serviceDecl.name(), serviceDecl.span(), "service");
validateServiceDeclaration(serviceDecl, binder, rules);
continue;
}
@ -124,8 +126,16 @@ public final class PbsDeclarationSemanticsValidator {
continue;
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
binder.registerValue(globalDecl.name(), globalDecl.span(), "global");
binder.registerVisibleTopLevelSurface(globalDecl.name(), globalDecl.span(), "global");
rules.validateGlobalDeclaration(globalDecl);
continue;
}
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
binder.registerValue(constDecl.name(), constDecl.span(), "const");
binder.registerVisibleTopLevelSurface(constDecl.name(), constDecl.span(), "const");
final var allowMissingInitializer = validateConstDeclarationAttributes(constDecl, interfaceModule, diagnostics);
rules.validateConstDeclaration(constDecl, !allowMissingInitializer);
continue;

View File

@ -168,6 +168,7 @@ final class PbsFlowSemanticSupport {
final Map<String, Set<String>> enums = new HashMap<>();
final Map<String, Set<String>> errors = new HashMap<>();
final Map<String, TypeView> constTypes = new HashMap<>();
final Map<String, TypeView> globalTypes = new HashMap<>();
final Map<String, TypeView> serviceSingletons = new HashMap<>();
final Set<String> knownStructNames = new HashSet<>();
final Set<String> knownServiceNames = new HashSet<>();
@ -369,6 +370,10 @@ final class PbsFlowSemanticSupport {
}
if (topDecl instanceof PbsAst.ConstDecl constDecl && constDecl.explicitType() != null) {
constTypes.put(constDecl.name(), typeFrom(constDecl.explicitType()));
return;
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl && globalDecl.explicitType() != null) {
globalTypes.put(globalDecl.name(), typeFrom(globalDecl.explicitType()));
}
}

View File

@ -85,6 +85,11 @@ final class PbsFlowStructuralExpressionAnalyzer {
return ExprResult.type(constType);
}
final var globalType = model.globalTypes.get(identifierExpr.name());
if (globalType != null) {
return ExprResult.type(globalType);
}
final var callbackSignature = model.callbacks.get(identifierExpr.name());
if (callbackSignature != null) {
return ExprResult.type(TypeView.callback(identifierExpr.name(), callbackSignature.inputTypes(), callbackSignature.outputType()));

View File

@ -19,6 +19,7 @@ final class PbsNamespaceBinder {
private final Map<NameId, Span> valueNamespace = new HashMap<>();
private final Map<NameId, Span> hostOwnerNamespace = new HashMap<>();
private final Map<CallableIdentity, Span> callableNamespace = new HashMap<>();
private final Map<NameId, VisibleTopLevelSymbol> visibleTopLevelNamespace = new HashMap<>();
PbsNamespaceBinder(final NameTable nameTable, final DiagnosticSink diagnostics) {
this.nameTable = nameTable;
@ -33,6 +34,24 @@ final class PbsNamespaceBinder {
registerByNamespace(valueNamespace, name, span, "value namespace", kind);
}
void registerVisibleTopLevelSurface(final String name, final Span span, final String kind) {
final var nameId = nameTable.register(name);
final var first = visibleTopLevelNamespace.putIfAbsent(nameId, new VisibleTopLevelSymbol(kind, span));
if (first == null) {
return;
}
if ("function".equals(kind) && "function".equals(first.kind())) {
return;
}
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name(),
"Visible top-level symbol '%s' collides between %s and %s".formatted(name, kind, first.kind()),
span,
List.of(new RelatedSpan("First visible top-level symbol is here", first.span())));
}
@SuppressWarnings("unused")
void registerHostOwner(final String name, final Span span, final String kind) {
registerByNamespace(hostOwnerNamespace, name, span, "host-owner namespace", kind);
@ -103,4 +122,9 @@ final class PbsNamespaceBinder {
NameId callableNameId,
String shape) {
}
private record VisibleTopLevelSymbol(
String kind,
Span span) {
}
}

View File

@ -17,6 +17,9 @@ public enum PbsSemanticsErrors {
E_SEM_MALFORMED_RESERVED_ATTRIBUTE,
E_SEM_MISSING_CONST_TYPE_ANNOTATION,
E_SEM_MISSING_CONST_INITIALIZER,
E_SEM_MISSING_GLOBAL_TYPE_ANNOTATION,
E_SEM_MISSING_GLOBAL_INITIALIZER,
E_SEM_GLOBAL_UNSUPPORTED_INITIALIZER,
E_SEM_CONST_NON_CONSTANT_INITIALIZER,
E_SEM_CONST_INITIALIZER_TYPE_MISMATCH,
E_SEM_CONST_CYCLIC_DEPENDENCY,

View File

@ -144,6 +144,65 @@ class PbsSemanticsDeclarationsTest {
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name())));
}
@Test
void shouldRejectVisibleTopLevelCollisionsBetweenFunctionServiceGlobalAndConst() {
final var source = """
fn Tick() -> int { return 1; }
declare service Tick {
fn run() -> void { return; }
}
declare global Score: int = 0;
declare const Score: int = 1;
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
final var duplicateCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name()))
.count();
assertTrue(duplicateCount >= 2);
}
@Test
void shouldAcceptSupportedGlobalInitializerForms() {
final var source = """
declare struct Box(value: int);
declare global BASE: int = 1;
declare global CLONE: int = BASE + 1;
declare global DEFAULT_BOX: Box = new Box(BASE);
fn read() -> int {
return CLONE;
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_GLOBAL_UNSUPPORTED_INITIALIZER.name())));
}
@Test
void shouldRejectUnsupportedGlobalInitializerForms() {
final var source = """
fn inc(v: int) -> int { return v + 1; }
declare global A: int = inc(1);
declare global B: int = if true { 1 } else { 2 };
declare global C: optional int = some(1);
declare global D: optional int = none;
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
final var invalidCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_GLOBAL_UNSUPPORTED_INITIALIZER.name()))
.count();
assertEquals(4, invalidCount);
}
@Test
void shouldAllowSelfInStructServiceMethodsAndCtors() {
final var source = """