diff --git a/docs/pbs/pull-requests/PR-017-pbs-const-evaluation-and-dependency-rules.md b/docs/pbs/pull-requests/PR-017-pbs-const-evaluation-and-dependency-rules.md deleted file mode 100644 index acd4e4de..00000000 --- a/docs/pbs/pull-requests/PR-017-pbs-const-evaluation-and-dependency-rules.md +++ /dev/null @@ -1,45 +0,0 @@ -# PR-017 - PBS Const Evaluation and Dependency Rules - -## Briefing - -`declare const` validation only checks explicit type and initializer presence. This PR implements constant-expression legality, dependency ordering, cycle detection, and initializer type compatibility. - -## Motivation - -Const declarations are part of static semantics and must be deterministic across files. - -## Target - -- Const semantic validation layer. -- Diagnostic coverage for constant expression failures. - -## Scope - -- Enforce allowed constant-expression subset. -- Resolve const dependencies module-wide, independent of source-file order. -- Reject cycles and unresolved const references. -- Validate initializer type compatibility with declared const type. - -## Method - -- Build const dependency graph from top-level declarations. -- Evaluate in topological order. -- Emit stable diagnostics for disallowed forms and graph failures. - -## Acceptance Criteria - -- Non-constant initializer forms are rejected deterministically. -- Cross-file const references resolve independent of source order. -- Cycles are rejected deterministically. -- Incompatible const initializer types are rejected. - -## Tests - -- Add const-expression positive/negative fixtures. -- Add cross-file dependency and cycle tests. -- Add type-compatibility tests. - -## Non-Goals - -- General compile-time function execution. -- Runtime const materialization strategy changes. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsConstSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsConstSemanticsValidator.java new file mode 100644 index 00000000..eeced59b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsConstSemanticsValidator.java @@ -0,0 +1,265 @@ +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.compiler.pbs.semantics.PbsFlowSemanticSupport.Model; +import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TypeView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Set; + +final class PbsConstSemanticsValidator { + private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps(); + + private record ConstAnalysis( + TypeView type, + boolean constant, + Span invalidSpan, + Set dependencies) { + } + + void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { + final var model = Model.from(ast); + final var constByName = collectConstDeclarations(ast); + final var dependencyGraph = new LinkedHashMap>(); + + for (final var entry : constByName.entrySet()) { + final var constDecl = entry.getValue(); + if (constDecl.explicitType() == null || constDecl.initializer() == null) { + continue; + } + + final var declaredType = typeOps.typeFromTypeRef(constDecl.explicitType(), model, null); + final var analysis = analyzeConstExpression(constDecl.initializer(), constByName, model, diagnostics); + dependencyGraph.put(entry.getKey(), analysis.dependencies()); + + if (!analysis.constant()) { + final var invalidSpan = analysis.invalidSpan() == null ? constDecl.initializer().span() : analysis.invalidSpan(); + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_CONST_NON_CONSTANT_INITIALIZER.name(), + "Const initializer for '%s' is not a constant expression".formatted(constDecl.name()), + invalidSpan); + continue; + } + + if (!typeOps.compatible(analysis.type(), declaredType)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_CONST_INITIALIZER_TYPE_MISMATCH.name(), + "Const initializer type is incompatible with declared type for '%s'".formatted(constDecl.name()), + constDecl.initializer().span()); + } + } + + validateConstCycles(constByName, dependencyGraph, diagnostics); + } + + private LinkedHashMap collectConstDeclarations(final PbsAst.File ast) { + final var constByName = new LinkedHashMap(); + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + constByName.putIfAbsent(constDecl.name(), constDecl); + } + } + return constByName; + } + + private ConstAnalysis analyzeConstExpression( + final PbsAst.Expression expression, + final Map constByName, + final Model model, + final DiagnosticSink diagnostics) { + if (expression instanceof PbsAst.IntLiteralExpr || expression instanceof PbsAst.BoundedLiteralExpr) { + return new ConstAnalysis(TypeView.intType(), true, null, Set.of()); + } + if (expression instanceof PbsAst.FloatLiteralExpr) { + return new ConstAnalysis(TypeView.floatType(), true, null, Set.of()); + } + if (expression instanceof PbsAst.StringLiteralExpr) { + return new ConstAnalysis(TypeView.str(), true, null, Set.of()); + } + if (expression instanceof PbsAst.BoolLiteralExpr) { + return new ConstAnalysis(TypeView.bool(), true, null, Set.of()); + } + if (expression instanceof PbsAst.GroupExpr groupExpr) { + return analyzeConstExpression(groupExpr.expression(), constByName, model, diagnostics); + } + if (expression instanceof PbsAst.IdentifierExpr identifierExpr) { + final var depName = identifierExpr.name(); + final var depDecl = constByName.get(depName); + if (depDecl == null || depDecl.explicitType() == null || depDecl.initializer() == null) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_CONST_UNRESOLVED_REFERENCE.name(), + "Const initializer references unresolved const '%s'".formatted(depName), + identifierExpr.span()); + return new ConstAnalysis(TypeView.unknown(), false, identifierExpr.span(), Set.of()); + } + return new ConstAnalysis( + typeOps.typeFromTypeRef(depDecl.explicitType(), model, null), + true, + null, + Set.of(depName)); + } + if (expression instanceof PbsAst.MemberExpr memberExpr) { + if (memberExpr.receiver() instanceof PbsAst.IdentifierExpr enumIdentifier) { + final var cases = model.enums.get(enumIdentifier.name()); + if (cases != null && cases.contains(memberExpr.memberName())) { + return new ConstAnalysis(TypeView.enumType(enumIdentifier.name()), true, null, Set.of()); + } + } + return new ConstAnalysis(TypeView.unknown(), false, memberExpr.span(), Set.of()); + } + if (expression instanceof PbsAst.UnaryExpr unaryExpr) { + final var operand = analyzeConstExpression(unaryExpr.expression(), constByName, model, diagnostics); + if (!operand.constant()) { + return operand; + } + if ("-".equals(unaryExpr.operator()) && isNumeric(operand.type())) { + return operand; + } + if (("!".equals(unaryExpr.operator()) || "not".equals(unaryExpr.operator())) && typeOps.isBool(operand.type())) { + return new ConstAnalysis(TypeView.bool(), true, null, operand.dependencies()); + } + return new ConstAnalysis(TypeView.unknown(), false, unaryExpr.span(), operand.dependencies()); + } + if (expression instanceof PbsAst.BinaryExpr binaryExpr) { + final var left = analyzeConstExpression(binaryExpr.left(), constByName, model, diagnostics); + final var right = analyzeConstExpression(binaryExpr.right(), constByName, model, diagnostics); + final var deps = mergeDependencies(left.dependencies(), right.dependencies()); + if (!left.constant()) { + return new ConstAnalysis(TypeView.unknown(), false, left.invalidSpan(), deps); + } + if (!right.constant()) { + return new ConstAnalysis(TypeView.unknown(), false, right.invalidSpan(), deps); + } + + final var operator = binaryExpr.operator(); + if (isArithmeticOperator(operator)) { + if (!isNumeric(left.type()) || !isNumeric(right.type())) { + return new ConstAnalysis(TypeView.unknown(), false, binaryExpr.span(), deps); + } + return new ConstAnalysis(typeOps.inferBinaryResult(operator, left.type(), right.type()), true, null, deps); + } + if (isComparisonOperator(operator)) { + if (!typeOps.compatible(left.type(), right.type())) { + return new ConstAnalysis(TypeView.unknown(), false, binaryExpr.span(), deps); + } + return new ConstAnalysis(TypeView.bool(), true, null, deps); + } + if (isBooleanOperator(operator)) { + if (!typeOps.isBool(left.type()) || !typeOps.isBool(right.type())) { + return new ConstAnalysis(TypeView.unknown(), false, binaryExpr.span(), deps); + } + return new ConstAnalysis(TypeView.bool(), true, null, deps); + } + return new ConstAnalysis(TypeView.unknown(), false, binaryExpr.span(), deps); + } + + return new ConstAnalysis(TypeView.unknown(), false, expression.span(), Set.of()); + } + + private void validateConstCycles( + final Map constByName, + final Map> dependencyGraph, + final DiagnosticSink diagnostics) { + final var indegree = new HashMap(); + final var reverse = new HashMap>(); + for (final var node : dependencyGraph.keySet()) { + indegree.put(node, 0); + reverse.put(node, new HashSet<>()); + } + + for (final var entry : dependencyGraph.entrySet()) { + for (final var dep : entry.getValue()) { + if (!dependencyGraph.containsKey(dep)) { + continue; + } + indegree.put(entry.getKey(), indegree.get(entry.getKey()) + 1); + reverse.get(dep).add(entry.getKey()); + } + } + + final var ready = new PriorityQueue(); + for (final var entry : indegree.entrySet()) { + if (entry.getValue() == 0) { + ready.add(entry.getKey()); + } + } + + var visited = 0; + while (!ready.isEmpty()) { + final var current = ready.remove(); + visited++; + for (final var dependent : reverse.getOrDefault(current, Set.of())) { + final var next = indegree.get(dependent) - 1; + indegree.put(dependent, next); + if (next == 0) { + ready.add(dependent); + } + } + } + + if (visited == dependencyGraph.size()) { + return; + } + + final var cyclic = new ArrayList(); + for (final var entry : indegree.entrySet()) { + if (entry.getValue() > 0) { + cyclic.add(entry.getKey()); + } + } + cyclic.sort(String::compareTo); + + for (final var constName : cyclic) { + final var constDecl = constByName.get(constName); + if (constDecl == null) { + continue; + } + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_CONST_CYCLIC_DEPENDENCY.name(), + "Cyclic const dependency detected involving '%s'".formatted(constName), + constDecl.span()); + } + } + + private Set mergeDependencies(final Set left, final Set right) { + final var merged = new HashSet(); + merged.addAll(left); + merged.addAll(right); + return merged; + } + + private boolean isNumeric(final TypeView type) { + return typeOps.isInt(type) || typeOps.isFloat(type); + } + + private boolean isArithmeticOperator(final String operator) { + return "+".equals(operator) + || "-".equals(operator) + || "*".equals(operator) + || "/".equals(operator) + || "%".equals(operator); + } + + private boolean isComparisonOperator(final String operator) { + return "==".equals(operator) + || "!=".equals(operator) + || "<".equals(operator) + || "<=".equals(operator) + || ">".equals(operator) + || ">=".equals(operator); + } + + private boolean isBooleanOperator(final String operator) { + return "and".equals(operator) + || "or".equals(operator) + || "&&".equals(operator) + || "||".equals(operator); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java index dd51f246..b5fcea7c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java @@ -8,6 +8,7 @@ import p.studio.utilities.structures.ReadOnlyList; public final class PbsDeclarationSemanticsValidator { private final NameTable nameTable = new NameTable(); + private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator(); public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { final var binder = new PbsNamespaceBinder(nameTable, diagnostics); @@ -76,6 +77,8 @@ public final class PbsDeclarationSemanticsValidator { validateImplementsDeclaration(implementsDecl, binder, rules); } } + + constSemanticsValidator.validate(ast, diagnostics); } private void validateStructDeclaration( diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java index 08a32be8..067d81fc 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -12,6 +12,10 @@ public enum PbsSemanticsErrors { E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE, E_SEM_MISSING_CONST_TYPE_ANNOTATION, E_SEM_MISSING_CONST_INITIALIZER, + E_SEM_CONST_NON_CONSTANT_INITIALIZER, + E_SEM_CONST_INITIALIZER_TYPE_MISMATCH, + E_SEM_CONST_CYCLIC_DEPENDENCY, + E_SEM_CONST_UNRESOLVED_REFERENCE, E_SEM_INVALID_RETURN_INSIDE_CTOR, E_SEM_APPLY_NON_CALLABLE_TARGET, E_SEM_APPLY_UNRESOLVED_OVERLOAD, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsConstTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsConstTest.java new file mode 100644 index 00000000..1a70ed1a --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsConstTest.java @@ -0,0 +1,96 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.PbsFrontendCompiler; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class PbsSemanticsConstTest { + + @Test + void shouldResolveConstDependenciesIndependentOfSourceOrder() { + final var source = """ + declare enum Mode(Idle, Run); + declare const C: int = B + 1; + declare const B: int = A + 1; + declare const A: int = 1; + declare const M: Mode = Mode.Run; + + fn use() -> int { + return C; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_UNRESOLVED_REFERENCE.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_CYCLIC_DEPENDENCY.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_NON_CONSTANT_INITIALIZER.name()))); + } + + @Test + void shouldRejectNonConstantConstInitializers() { + final var source = """ + fn inc(v: int) -> int { return v + 1; } + declare struct Box(pub mut value: int); + + declare const A: int = inc(1); + declare const B: int = new Box(1); + declare const C: int = if true { 1 } else { 2 }; + declare const D: int = some(1); + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var nonConstCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_NON_CONSTANT_INITIALIZER.name())) + .count(); + assertEquals(4, nonConstCount); + } + + @Test + void shouldRejectConstInitializerTypeMismatchAndUnresolvedReference() { + final var source = """ + declare const A: int = "wrong"; + declare const B: int = Missing + 1; + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var mismatchCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_INITIALIZER_TYPE_MISMATCH.name())) + .count(); + final var unresolvedCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_UNRESOLVED_REFERENCE.name())) + .count(); + + assertEquals(1, mismatchCount); + assertEquals(1, unresolvedCount); + } + + @Test + void shouldRejectCyclicConstDependencies() { + final var source = """ + declare const A: int = B + 1; + declare const B: int = C + 1; + declare const C: int = A + 1; + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var cycleCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_CONST_CYCLIC_DEPENDENCY.name())) + .count(); + assertEquals(3, cycleCount); + } +}