implements PR017

This commit is contained in:
bQUARKz 2026-03-05 19:54:07 +00:00
parent e4be2a033f
commit 2041e34496
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 368 additions and 45 deletions

View File

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

View File

@ -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<String> 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<String, Set<String>>();
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<String, PbsAst.ConstDecl> collectConstDeclarations(final PbsAst.File ast) {
final var constByName = new LinkedHashMap<String, PbsAst.ConstDecl>();
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<String, PbsAst.ConstDecl> 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<String, PbsAst.ConstDecl> constByName,
final Map<String, Set<String>> dependencyGraph,
final DiagnosticSink diagnostics) {
final var indegree = new HashMap<String, Integer>();
final var reverse = new HashMap<String, Set<String>>();
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<String>();
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<String>();
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<String> mergeDependencies(final Set<String> left, final Set<String> right) {
final var merged = new HashSet<String>();
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);
}
}

View File

@ -8,6 +8,7 @@ import p.studio.utilities.structures.ReadOnlyList;
public final class PbsDeclarationSemanticsValidator { public final class PbsDeclarationSemanticsValidator {
private final NameTable nameTable = new NameTable(); private final NameTable nameTable = new NameTable();
private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) {
final var binder = new PbsNamespaceBinder(nameTable, diagnostics); final var binder = new PbsNamespaceBinder(nameTable, diagnostics);
@ -76,6 +77,8 @@ public final class PbsDeclarationSemanticsValidator {
validateImplementsDeclaration(implementsDecl, binder, rules); validateImplementsDeclaration(implementsDecl, binder, rules);
} }
} }
constSemanticsValidator.validate(ast, diagnostics);
} }
private void validateStructDeclaration( private void validateStructDeclaration(

View File

@ -12,6 +12,10 @@ public enum PbsSemanticsErrors {
E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE, E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE,
E_SEM_MISSING_CONST_TYPE_ANNOTATION, E_SEM_MISSING_CONST_TYPE_ANNOTATION,
E_SEM_MISSING_CONST_INITIALIZER, 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_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

@ -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);
}
}