Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/architecture/semantics-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,8 @@ are `types.go`, `syntax.go`, `relations.go`, `compatibility.go`, `lookup.go`,
- Expected numeric types influence literal construction and identifier adaptation.
- Numeric literals use default or explicit numeric types and target-aware parsing.
- Boolean and string literals produce typed constant values.
- Constant identifiers must resolve to `SymbolConst`.
- Constant identifiers consume their resolver-bound `SymbolIndex` entry and must
denote `SymbolConst`; evaluation never repeats lexical name lookup.
- Unary and binary operations delegate folding to `constvalue`.
- Variant constructions use private typechecker evidence before THIR publication.
- Non-copyable variant types are not evaluated as constants.
Expand Down
119 changes: 70 additions & 49 deletions internal/pipeline/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,12 @@ func TestPipelineLowersSequenceIndexesAsUsizeAcrossTargets(t *testing.T) {
Extension: peeper.SourceExt,
TargetOS: "linux",
TargetArch: test.arch,
}, "", `fn main() {
}, "", `const selected: i32 = 0;
fn main() {
const saved: i32 = selected;
const selected: i32 = 1;
let items = [1]i32{1};
let chosen = items[saved];
for index, value in items {}
}`, func(entry *module.Module) {
if entry.MIR == nil || len(entry.MIR.Funcs) != 1 {
Expand Down Expand Up @@ -1147,55 +1151,52 @@ func TestPipelineFinalizesMissingReturnDiagnosticInCFGPhase(t *testing.T) {
t.Fatalf("missing-return diagnostic unavailable at CFG phase:\n%s", diag.EmitAllToString())
}

func TestPipelineReportsConstantConditionInCFGPhase(t *testing.T) {
diag := diagnostics.NewDiagnosticBag()
const entryPath = "entry" + peeper.SourceExt
entry := parseModuleSource(entryPath, `fn main() -> i32 {
if false {
return 1;
}
func TestPipelineReportsConstantConditionsInCFGPhase(t *testing.T) {
for _, test := range []struct {
name, source, warning string
}{
{"literal false", `fn main() -> i32 {
if false { return 1; }
return 0;
}`, diag)
entry.Phase = phase.Parsed
ctx := project.NewWithConfig(project.Config{RootDir: ".", Extension: peeper.SourceExt}, diag)
ctx.AddModule(entry)

for entry.Phase < phase.Typechecked {
if !advanceModulePhase(ctx, entry, diag) {
t.Fatalf("advanceModulePhase stopped at %v", entry.Phase)
}
}
for _, item := range diag.Diagnostics() {
if item != nil && item.Code == diagnostics.WarnConstantConditionFalse {
t.Fatal("constant-condition warning emitted before CFG phase")
}
}`, diagnostics.WarnConstantConditionFalse},
{"local true", `fn main() { const Flag = true; if Flag { print(1); } }`, diagnostics.WarnConstantConditionTrue},
{"local false", `fn main() { const Flag = false; if Flag { print(1); } }`, diagnostics.WarnConstantConditionFalse},
{"const shadow true", `fn main() {
const value: i32 = 10;
{
const saved: i32 = value;
const value: i32 = 20;
if saved == 10 { print(1); }
}
entry.AST = nil
if !advanceModulePhase(ctx, entry, diag) {
t.Fatal("CFG phase requires AST")
}`, diagnostics.WarnConstantConditionTrue},
{"const shadow false", `fn main() {
const value: i32 = 10;
{
const saved: i32 = value;
const value: i32 = 20;
if saved == 20 { print(1); }
}
if entry.MIR != nil {
t.Fatalf("CFG phase produced MIR: %#v", entry.MIR)
}`, diagnostics.WarnConstantConditionFalse},
{"let shadow true", `fn main() {
const value: i32 = 10;
{
const saved: i32 = value;
let value: i32 = 20;
if saved == 10 { print(1); }
}
for _, item := range diag.Diagnostics() {
if item != nil && item.Code == diagnostics.WarnConstantConditionFalse {
return
}
}`, diagnostics.WarnConstantConditionTrue},
{"let shadow false", `fn main() {
const value: i32 = 10;
{
const saved: i32 = value;
let value: i32 = 20;
if saved == 20 { print(1); }
}
t.Fatalf("constant-condition diagnostic unavailable at CFG phase:\n%s", diag.EmitAllToString())
}

func TestPipelinePublishesLocalConstantConditions(t *testing.T) {
for _, test := range []struct {
name, value string
warning string
}{
{"true", "true", diagnostics.WarnConstantConditionTrue},
{"false", "false", diagnostics.WarnConstantConditionFalse},
}`, diagnostics.WarnConstantConditionFalse},
} {
t.Run(test.name, func(t *testing.T) {
diag := diagnostics.NewDiagnosticBag()
entry := parseModuleSource("entry"+peeper.SourceExt, "fn main() { const Flag = "+test.value+"; if Flag { print(1); } }", diag)
entry := parseModuleSource("entry"+peeper.SourceExt, test.source, diag)
entry.Phase = phase.Parsed
ctx := project.NewWithConfig(project.Config{RootDir: ".", Extension: peeper.SourceExt}, diag)
ctx.AddModule(entry)
Expand All @@ -1204,25 +1205,45 @@ func TestPipelinePublishesLocalConstantConditions(t *testing.T) {
t.Fatalf("advanceModulePhase stopped at %v", entry.Phase)
}
}
if diag.HasErrors() {
t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString())
}
for _, item := range diag.Diagnostics() {
if item != nil && (item.Code == diagnostics.WarnConstantConditionTrue || item.Code == diagnostics.WarnConstantConditionFalse) {
t.Fatal("constant-condition warning emitted before CFG phase")
}
}
entry.AST = nil
if !advanceModulePhase(ctx, entry, diag) {
t.Fatal("CFG phase requires AST")
if !advanceModulePhase(ctx, entry, diag) || entry.Phase != phase.CFG {
t.Fatal("CFG phase did not consume THIR without AST")
}
if diag.HasErrors() {
t.Fatalf("unexpected CFG diagnostics:\n%s", diag.EmitAllToString())
}
if entry.MIR != nil {
t.Fatalf("CFG phase produced MIR: %#v", entry.MIR)
}
warnings := 0
for _, item := range diag.Diagnostics() {
if item.Code == test.warning {
return
if item == nil || (item.Code != diagnostics.WarnConstantConditionTrue && item.Code != diagnostics.WarnConstantConditionFalse) {
continue
}
if item.Code != test.warning {
t.Fatalf("wrong constant-condition warning %s:\n%s", item.Code, diag.EmitAllToString())
}
warnings++
}
if warnings != 1 {
t.Fatalf("got %d warnings %s, want exactly one:\n%s", warnings, test.warning, diag.EmitAllToString())
}
t.Fatalf("missing %s: %s", test.warning, diag.EmitAllToString())
})
}
}

func TestPipelineDefersConstantEvaluationCycleToCFG(t *testing.T) {
diag := diagnostics.NewDiagnosticBag()
entry := parseModuleSource("entry"+peeper.SourceExt, `fn main() {
const First: bool = Second;
const Second: bool = First;
const First: bool = First;
if First { print(1); }
}`, diag)
entry.Phase = phase.Parsed
Expand Down
4 changes: 2 additions & 2 deletions internal/semantics/typechecker/check_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ func (c *checker) typeBinaryExpr(scope *symbols.Scope, node *ast.BinaryExpr, exp
return &typeinfo.InvalidType{}
}
c.recordImplicitConversion(node.Right, conversion)
if value, ok := c.evaluateConstant(c.ctx, scope, node.Right, right); ok {
if value, ok := c.evaluateConstant(c.ctx, node.Right, right); ok {
if count, ok := value.(*constvalue.IntConst); ok && count != nil {
_, bits, _ := typeinfo.NumericInfo(left)
normalized, normalizedOK := constvalue.NormalizeInteger(count.Int(),
Expand Down Expand Up @@ -653,7 +653,7 @@ func (c *checker) typeIndexExpr(scope *symbols.Scope, node *ast.IndexExpr) typei
return elem
}
array := typeinfo.Underlying(baseType).(*typeinfo.ArrayType)
value, ok := c.evaluateConstant(c.ctx, scope, node.Index, typeinfo.DefaultIntegerType())
value, ok := c.evaluateConstant(c.ctx, node.Index, typeinfo.DefaultIntegerType())
if !ok {
return elem
}
Expand Down
4 changes: 2 additions & 2 deletions internal/semantics/typechecker/check_stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,8 @@ func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, return
}
}
if valid {
startValue, startFound := c.evaluateConstant(c.ctx, scope, rangeExpr.Start, elemType)
endValue, endFound := c.evaluateConstant(c.ctx, scope, rangeExpr.End, elemType)
startValue, startFound := c.evaluateConstant(c.ctx, rangeExpr.Start, elemType)
endValue, endFound := c.evaluateConstant(c.ctx, rangeExpr.End, elemType)
start, startIntegral := startValue.(*constvalue.IntConst)
end, endIntegral := endValue.(*constvalue.IntConst)
if startFound && endFound && startIntegral && endIntegral && start.Int().Cmp(end.Int()) < 0 {
Expand Down
58 changes: 22 additions & 36 deletions internal/semantics/typechecker/constant_eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,24 @@ func (e *constantEvaluator) finalizeModuleValues() {
return
}
e.module.SymbolIndex.ClearConstants()
for _, sym := range e.module.ModuleScope.Symbols() {
moduleSymbols := e.module.ModuleScope.Symbols()
// Invalidate every module constant before evaluating any dependency.
for _, sym := range moduleSymbols {
if sym != nil && sym.Kind == symbols.SymbolConst {
delete(e.cache, sym.ID)
}
}
previous := e.publishModuleValues
e.publishModuleValues = true
e.evalModuleConstants()
e.publishModuleValues = previous
}

func (e *constantEvaluator) evalModuleConstants() {
for _, sym := range e.module.ModuleScope.Symbols() {
for _, sym := range moduleSymbols {
if sym != nil && sym.Kind == symbols.SymbolConst {
e.evalConstSymbol(sym, e.module.ModuleScope)
e.evalConstSymbol(sym)
}
}
e.publishModuleValues = previous
}

func (e *constantEvaluator) evalConstSymbol(sym *symbols.Symbol, scope *symbols.Scope) (constvalue.Value, bool) {
func (e *constantEvaluator) evalConstSymbol(sym *symbols.Symbol) (constvalue.Value, bool) {
if e == nil || e.module == nil || sym == nil {
return nil, false
}
Expand Down Expand Up @@ -94,20 +92,11 @@ func (e *constantEvaluator) evalConstSymbol(sym *symbols.Symbol, scope *symbols.
return nil, false
}
e.inProgress[sym.ID] = struct{}{}
valueScope := scope
if e.module.ModuleScope != nil {
if found, ok := e.module.ModuleScope.LookupLocal(sym.Name); ok && found != nil && found.ID == sym.ID {
valueScope = e.module.ModuleScope
}
}
if valueScope == nil {
valueScope = e.module.ModuleScope
}
expected := typeinfo.Type(nil)
if sym.Type != nil && !typeinfo.IsInvalidOrUnknown(sym.Type) {
expected = sym.Type
}
value, ok := e.evalExpr(valueScope, decl.Value, expected)
value, ok := e.evalExpr(decl.Value, expected)
delete(e.inProgress, sym.ID)
if !ok {
return nil, false
Expand All @@ -123,7 +112,7 @@ func (e *constantEvaluator) evalConstSymbol(sym *symbols.Symbol, scope *symbols.
return value, true
}

func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) (constvalue.Value, bool) {
func (e *constantEvaluator) evalExpr(expr ast.Expr, expected typeinfo.Type) (constvalue.Value, bool) {
if e.evidence != nil {
if construction, ok := e.evidence.VariantConstruction(expr.ID()); ok {
if typeinfo.OwnershipCapabilityOf(construction.EnumType).Copy != typeinfo.CopyImplicit {
Expand All @@ -148,15 +137,15 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect
fields := make([]constvalue.Value, len(payload.Fields))
for index, field := range payload.Fields {
valueExpr := valuesByName[field.Name]
value, ok := e.evalExpr(scope, valueExpr, field.Type)
value, ok := e.evalExpr(valueExpr, field.Type)
if !ok {
return nil, false
}
fields[index] = value
}
return constvalue.NewVariant(descriptor.Identity, typeinfo.TypeText(construction.EnumType), construction.Case, fields)
}
value, ok := e.evalExpr(scope, construction.Value, construction.Payload)
value, ok := e.evalExpr(construction.Value, construction.Payload)
if !ok {
return nil, false
}
Expand All @@ -173,7 +162,7 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect
if !found || test.Family != typeinfo.VariantFamilyNamed {
return nil, false
}
value, ok := e.evalExpr(scope, node.Value, e.evidence.ExprType(node.Value.ID()))
value, ok := e.evalExpr(node.Value, e.evidence.ExprType(node.Value.ID()))
variant, isConstant := value.(*constvalue.VariantConst)
if !ok || !isConstant || variant == nil {
return nil, false
Expand Down Expand Up @@ -224,15 +213,12 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect
case *ast.BoolLit:
return constvalue.NewBool(node.Value), true
case *ast.Ident:
lookup := scope
if lookup == nil {
lookup = e.module.ModuleScope
}
sym, ok := lookup.Lookup(node.Name)
if !ok || sym == nil || sym.Kind != symbols.SymbolConst {
// Later declarations may shadow the name, but not its resolved binding.
sym := e.module.SymbolIndex.Symbol(node)
if sym == nil || sym.Kind != symbols.SymbolConst {
return nil, false
}
value, ok := e.evalConstSymbol(sym, lookup)
value, ok := e.evalConstSymbol(sym)
if !ok {
return nil, false
}
Expand All @@ -251,7 +237,7 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect
}
// Cast input keeps its own width; destination context must not change
// overflow before the explicit finite-width conversion.
value, ok := e.evalExpr(scope, node.Expr, sourceType)
value, ok := e.evalExpr(node.Expr, sourceType)
integer, isInteger := value.(*constvalue.IntConst)
if !ok || !isInteger || integer == nil {
return nil, false
Expand All @@ -262,14 +248,14 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect
}
return expectedNumericConstValue(converted, expected)
case *ast.UnaryExpr:
value, ok := e.evalExpr(scope, node.Expr, expected)
value, ok := e.evalExpr(node.Expr, expected)
if !ok {
return nil, false
}
return constvalue.FoldUnary(node.Op, value)
case *ast.BinaryExpr:
left, lok := e.evalExpr(scope, node.Left, expected)
right, rok := e.evalExpr(scope, node.Right, expected)
left, lok := e.evalExpr(node.Left, expected)
right, rok := e.evalExpr(node.Right, expected)
if !lok || !rok {
return nil, false
}
Expand All @@ -283,8 +269,8 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect
if commonType == nil {
return nil, false
}
left, lok = e.evalExpr(scope, node.Left, commonType)
right, rok = e.evalExpr(scope, node.Right, commonType)
left, lok = e.evalExpr(node.Left, commonType)
right, rok = e.evalExpr(node.Right, commonType)
if !lok || !rok {
return nil, false
}
Expand Down
Loading
Loading