implements PR017
This commit is contained in:
parent
e4be2a033f
commit
2041e34496
@ -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.
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user