From 80b3fd30be4766bca428338454f33c006a5419fc Mon Sep 17 00:00:00 2001 From: itsfuad Date: Mon, 5 Oct 2026 15:12:17 +0600 Subject: [PATCH] Preserve resolved bindings during constant evaluation Use resolver-bound symbols so later shadowing cannot change earlier constant initializers. Remove obsolete scope plumbing and reuse one symbol snapshot across both finalization passes, keeping cache invalidation before evaluation. Cover runtime indexing, rejected constant bounds, CFG warning phases, and 32-/64-bit LLVM lowering with focused regressions and source fixtures. --- docs/architecture/semantics-bindings.md | 3 +- internal/pipeline/pipeline_test.go | 119 ++++++++++-------- internal/semantics/typechecker/check_expr.go | 4 +- internal/semantics/typechecker/check_stmt.go | 4 +- .../semantics/typechecker/constant_eval.go | 58 ++++----- .../typechecker/constant_eval_test.go | 52 +++++++- internal/semantics/typechecker/thir_build.go | 12 +- internal/semantics/typechecker/typechecker.go | 4 +- .../peeper.toml | 7 ++ .../src/main.peep | 9 ++ x_test/runtime_constant_shadow/peeper.toml | 6 + x_test/runtime_constant_shadow/src/main.peep | 24 ++++ 12 files changed, 198 insertions(+), 104 deletions(-) create mode 100644 x_test/negative_constant_shadow_index/peeper.toml create mode 100644 x_test/negative_constant_shadow_index/src/main.peep create mode 100644 x_test/runtime_constant_shadow/peeper.toml create mode 100644 x_test/runtime_constant_shadow/src/main.peep diff --git a/docs/architecture/semantics-bindings.md b/docs/architecture/semantics-bindings.md index 802d6c7c..bc13e4f3 100644 --- a/docs/architecture/semantics-bindings.md +++ b/docs/architecture/semantics-bindings.md @@ -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. diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 90890248..32e397b4 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -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 { @@ -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) @@ -1204,16 +1205,37 @@ 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()) }) } } @@ -1221,8 +1243,7 @@ func TestPipelinePublishesLocalConstantConditions(t *testing.T) { 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 diff --git a/internal/semantics/typechecker/check_expr.go b/internal/semantics/typechecker/check_expr.go index 5c3cc832..8cb943ca 100644 --- a/internal/semantics/typechecker/check_expr.go +++ b/internal/semantics/typechecker/check_expr.go @@ -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(), @@ -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 } diff --git a/internal/semantics/typechecker/check_stmt.go b/internal/semantics/typechecker/check_stmt.go index f3bb0a75..629514ed 100644 --- a/internal/semantics/typechecker/check_stmt.go +++ b/internal/semantics/typechecker/check_stmt.go @@ -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 { diff --git a/internal/semantics/typechecker/constant_eval.go b/internal/semantics/typechecker/constant_eval.go index 2af7b8e9..1b40f8c5 100644 --- a/internal/semantics/typechecker/constant_eval.go +++ b/internal/semantics/typechecker/constant_eval.go @@ -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 } @@ -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 @@ -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 { @@ -148,7 +137,7 @@ 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 } @@ -156,7 +145,7 @@ func (e *constantEvaluator) evalExpr(scope *symbols.Scope, expr ast.Expr, expect } 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 } @@ -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 @@ -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 } @@ -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 @@ -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 } @@ -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 } diff --git a/internal/semantics/typechecker/constant_eval_test.go b/internal/semantics/typechecker/constant_eval_test.go index 18a07363..5bfa4260 100644 --- a/internal/semantics/typechecker/constant_eval_test.go +++ b/internal/semantics/typechecker/constant_eval_test.go @@ -1,6 +1,7 @@ package typechecker import ( + "fmt" "testing" "compiler/internal/constvalue" @@ -8,6 +9,7 @@ import ( "compiler/internal/frontend/ast" "compiler/internal/frontend/lexer" "compiler/internal/frontend/parser" + "compiler/internal/ir/thir" "compiler/internal/module" "compiler/internal/moduleid" "compiler/internal/project" @@ -224,7 +226,7 @@ func TestFinalizeValuesRecomputesLazyConstantsWithFinalSymbolTypes(t *testing.T) t.Fatal("missing symbol Value") } evaluator := newConstantEvaluator(ctx, module, newEvidence()) - if _, ok := evaluator.evalConstSymbol(sym, module.ModuleScope); !ok { + if _, ok := evaluator.evalConstSymbol(sym); !ok { t.Fatal("failed to lazily evaluate Value") } if _, found := evaluator.cache[sym.ID]; !found { @@ -258,7 +260,7 @@ fn main() { reference := fn.Body.Stmts[1].(*ast.LetDecl).Value.(*ast.Ident) scope := module.SymbolIndex.Scope(fn.Body) evaluator := newConstantEvaluator(ctx, module, newEvidence()) - if _, ok := evaluator.evalExpr(scope, reference, nil); !ok { + if _, ok := evaluator.evalExpr(reference, nil); !ok { t.Fatal("failed to evaluate local constant reference") } localSymbol, found := scope.LookupLocal(local.Name.Name) @@ -280,6 +282,48 @@ fn main() { } } +func TestEvaluateUsesResolvedConstantBindings(t *testing.T) { + for _, shadow := range []string{"const", "let"} { + t.Run(shadow, func(t *testing.T) { + module, diag := checkTypeModule(t, fmt.Sprintf(`const index: i32 = 0; +fn main() { + const saved: i32 = index; + %s index: i32 = 2; + let values = [3]i32{10, 20, 30}; + let selected = values[saved]; + if saved == 0 { print(selected); } +}`, shadow)) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + fn := module.AST.Stmts[1].(*ast.FnDecl) + index := fn.Body.Stmts[3].(*ast.LetDecl).Value.(*ast.IndexExpr) + checkedIndex := module.THIR.Node(index.ID()).(*thir.Index) + if checkedIndex.Constant == nil || checkedIndex.Constant.Text != "0" { + t.Errorf("constant index = %#v, want resolved outer value 0", checkedIndex.Constant) + } + condition := fn.Body.Stmts[4].(*ast.IfStmt) + checkedCondition := module.THIR.Node(condition.ID()).(*thir.If) + if checkedCondition.ConstantCondition == nil || !*checkedCondition.ConstantCondition { + t.Errorf("constant condition = %v, want true from resolved outer value", checkedCondition.ConstantCondition) + } + }) + } +} + +func TestEvaluateRejectsUnboundConstantReferences(t *testing.T) { + module, diag := constevalModule(t, `const Value: i32 = 7;`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + ctx := project.New(".", peeper.SourceExt, diag) + ctx.AddModule(module) + evaluator := newConstantEvaluator(ctx, module, newEvidence()) + if value, ok := evaluator.evalExpr(&ast.Ident{Name: "Value"}, nil); ok { + t.Fatalf("unbound identifier evaluated to %#v through name lookup", value) + } +} + func TestEvaluateReadsForeignPublishedConstantWithoutConsumerCache(t *testing.T) { diag := diagnostics.NewDiagnosticBag() ctx := project.New(".", peeper.SourceExt, diag) @@ -318,7 +362,7 @@ func TestEvaluateReadsForeignPublishedConstantWithoutConsumerCache(t *testing.T) t.Fatal("missing consumer constant") } evaluator := newConstantEvaluator(ctx, consumer, newEvidence()) - if _, ok := evaluator.evalConstSymbol(local, consumer.ModuleScope); !ok { + if _, ok := evaluator.evalConstSymbol(local); !ok { t.Fatal("failed to lazily evaluate imported constant") } cached := evaluator.cache[local.ID] @@ -420,7 +464,7 @@ func TestConstantQueryCacheIsPrivateUntilFinalization(t *testing.T) { } evaluator := newConstantEvaluator(ctx, mod, newEvidence()) - if _, ok := evaluator.evalConstSymbol(sym, mod.ModuleScope); !ok { + if _, ok := evaluator.evalConstSymbol(sym); !ok { t.Fatal("lazy constant evaluation failed") } if got := mod.SymbolIndex.ConstantValue(sym.ID); got != nil { diff --git a/internal/semantics/typechecker/thir_build.go b/internal/semantics/typechecker/thir_build.go index 0e7fc158..a3bb47ab 100644 --- a/internal/semantics/typechecker/thir_build.go +++ b/internal/semantics/typechecker/thir_build.go @@ -18,9 +18,9 @@ func (c *checker) buildTHIR() *thir.Module { if c == nil || c.module == nil || c.module.AST == nil { return nil } - constantCondition := func(expr ast.Expr, scope *symbols.Scope) (*bool, []*diagnostics.Diagnostic) { + constantCondition := func(expr ast.Expr) (*bool, []*diagnostics.Diagnostic) { pending := diagnostics.NewDiagnosticBag() - value, ok := c.evaluateConstant(c.ctx.WithDiagnostics(pending), scope, expr, &typeinfo.BoolType{}) + value, ok := c.evaluateConstant(c.ctx.WithDiagnostics(pending), expr, &typeinfo.BoolType{}) if !ok { return nil, pending.Diagnostics() } @@ -43,8 +43,7 @@ func (c *checker) buildTHIR() *thir.Module { type thirBuilder struct { symbolIndex *symbols.Index typing *evidence - currentScope *symbols.Scope - constantCondition func(ast.Expr, *symbols.Scope) (*bool, []*diagnostics.Diagnostic) + constantCondition func(ast.Expr) (*bool, []*diagnostics.Diagnostic) } func (b *thirBuilder) function(source *ast.FnDecl) *thir.Function { @@ -85,9 +84,6 @@ func (b *thirBuilder) block(source *ast.BlockStmt) *thir.Block { return nil } block := &thir.Block{StmtInfo: stmtInfo(source), Scope: b.scope(source), Stmts: make([]thir.Stmt, 0, len(source.Stmts))} - previousScope := b.currentScope - b.currentScope = block.Scope - defer func() { b.currentScope = previousScope }() for _, statement := range source.Stmts { if lowered := b.statement(statement); lowered != nil { block.Stmts = append(block.Stmts, lowered) @@ -115,7 +111,7 @@ func (b *thirBuilder) statement(statement ast.Stmt) thir.Stmt { case *ast.IfStmt: statement := &thir.If{StmtInfo: stmtInfo(node), Condition: b.expression(node.Cond)} if b.constantCondition != nil { - statement.ConstantCondition, statement.ConditionDiagnostics = b.constantCondition(node.Cond, b.currentScope) + statement.ConstantCondition, statement.ConditionDiagnostics = b.constantCondition(node.Cond) } statement.Then = b.block(node.Then) statement.Else = b.statement(node.Else) diff --git a/internal/semantics/typechecker/typechecker.go b/internal/semantics/typechecker/typechecker.go index 6212dcdc..3f1fef67 100644 --- a/internal/semantics/typechecker/typechecker.go +++ b/internal/semantics/typechecker/typechecker.go @@ -134,7 +134,7 @@ func runCheck(ctx *project.CompilerContext, module *module.Module) (*thir.Module return c.buildTHIR(), evidence } -func (c *checker) evaluateConstant(ctx *project.CompilerContext, scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) (constvalue.Value, bool) { +func (c *checker) evaluateConstant(ctx *project.CompilerContext, expr ast.Expr, expected typeinfo.Type) (constvalue.Value, bool) { if c == nil || c.module == nil || expr == nil { return nil, false } @@ -142,7 +142,7 @@ func (c *checker) evaluateConstant(ctx *project.CompilerContext, scope *symbols. c.constantEval = newConstantEvaluator(c.ctx, c.module, c.evidence) } evaluator := c.constantEval.withContext(ctx) - return evaluator.evalExpr(scope, expr, expected) + return evaluator.evalExpr(expr, expected) } // CanAdaptFirstCallArgument reports whether argType can occupy a function's diff --git a/x_test/negative_constant_shadow_index/peeper.toml b/x_test/negative_constant_shadow_index/peeper.toml new file mode 100644 index 00000000..185b3a67 --- /dev/null +++ b/x_test/negative_constant_shadow_index/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_constant_shadow_index" +build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["array index out of bounds: index 3 for length 3"] diff --git a/x_test/negative_constant_shadow_index/src/main.peep b/x_test/negative_constant_shadow_index/src/main.peep new file mode 100644 index 00000000..befa7851 --- /dev/null +++ b/x_test/negative_constant_shadow_index/src/main.peep @@ -0,0 +1,9 @@ +fn main() -> i32 { + const index: i32 = 3; + { + const saved: i32 = index; + const index: i32 = 0; + let values = [3]i32{10, 20, 30}; + return values[saved]; + } +} diff --git a/x_test/runtime_constant_shadow/peeper.toml b/x_test/runtime_constant_shadow/peeper.toml new file mode 100644 index 00000000..f1edf985 --- /dev/null +++ b/x_test/runtime_constant_shadow/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_constant_shadow" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_constant_shadow/src/main.peep b/x_test/runtime_constant_shadow/src/main.peep new file mode 100644 index 00000000..ba174fa8 --- /dev/null +++ b/x_test/runtime_constant_shadow/src/main.peep @@ -0,0 +1,24 @@ +fn main() -> i32 { + const index: i32 = 0; + { + const saved: i32 = index; + const index: i32 = 2; + let values = [3]i32{10, 20, 30}; + println(values[saved]); + println(saved); + if values[saved] != 10 || saved != 0 { + return 1; + } + } + { + const saved: i32 = index; + let index: i32 = 2; + let values = [3]i32{10, 20, 30}; + println(values[saved]); + println(saved); + if values[saved] != 10 || saved != 0 { + return 2; + } + } + return 0; +}