// The exhaustive expression-variable walkers: the shared read descent // behind the reorder correlation guard (collectAllVars: every mentioned // name, binder locals included) and the scope-subset validators // (freeVarsOutside: free references only), plus the alpha-rename // (renameFree). A new AST kind is added here and covered by the per-kind // roll call in the walker invariant tests.
package plan import ( ) // MentionsVar reports whether e references name anywhere (binder locals // included -- over-reporting only declines an optimization, never // unsound). func ( ast.Expr, string) bool { := map[string]bool{} collectAllVars(, ) return [] } // collectAllVars gathers every variable name e mentions, descending into // EXISTS/COUNT subquery patterns and inner WHEREs. Unlike CheckRefs it // adds binder-introduced locals (a subquery's pattern variables, a // comprehension's iteration variable) too -- the caller filters against // the query's real variables to tell a correlation from a local name; // over-collecting only makes the reorder more conservative, never unsound. func collectAllVars( ast.Expr, map[string]bool) { collectVars(, , nil) } // freeVarsOutside returns e's free variable references not in allowed, // sorted for deterministic reporting. Binder-introduced locals // (quantifier/comprehension/reduce variables, subquery pattern variables) // are in scope for their own sub-expressions and never reported. func freeVarsOutside( ast.Expr, []string) []string { := make(map[string]bool, len()+4) for , := range { [] = true } := map[string]bool{} collectVars(, , ) := make([]string, 0, len()) for := range { = append(, ) } slices.Sort() return } // collectVars walks e adding variable references to out. With locals nil, // binder-introduced names are added to out as well (collectAllVars' // over-collecting mode); with a locals scope, binders extend it for their // sub-scope only and out receives just the free references. func collectVars( ast.Expr, , map[string]bool) { := func( string) { if == nil || ![] { [] = true } } // bound runs fn with names in scope, restoring the scope after; in the // over-collecting mode the names land in out instead. := func( func(), ...string) { if == nil { for , := range { [] = true } () return } var []string for , := range { if ![] { [] = true = append(, ) } } () for , := range { delete(, ) } } switch n := .(type) { case nil: return case *ast.Var: (.Name) case *ast.Prop: (.Var) case *ast.MapProj: (.Var) for , := range .Entries { if .Expr != nil { (.Expr, , ) } } case *ast.HasLabelExpr: (.Var) case *ast.Cost: (.From) (.To) case *ast.Unary: (.Expr, , ) case *ast.IsNull: (.Expr, , ) case *ast.IsTruth: (.Expr, , ) case *ast.IsTyped: (.Expr, , ) case *ast.Binary: (.LHS, , ) (.RHS, , ) case *ast.Func: for , := range .Args { (, , ) } case *ast.ListExpr: for , := range .Elems { (, , ) } case *ast.In: (.Expr, , ) (.List, , ) case *ast.Case: if .Operand != nil { (.Operand, , ) } for , := range .Whens { (.Cond, , ) (.Result, , ) } if .Else != nil { (.Else, , ) } case *ast.Exists: (func() { (.Where, , ) }, patternVarNames(.Pattern)...) case *ast.CountSub: (func() { (.Where, , ) }, patternVarNames(.Pattern)...) case *ast.PatternComp: (func() { (.Where, , ) (.Proj, , ) }, patternVarNames(.Pattern)...) case *ast.ListPred: (.List, , ) (func() { (.Pred, , ) }, .Var) case *ast.Reduce: (.Init, , ) (.List, , ) (func() { (.Body, , ) }, .Acc, .Var) case *ast.ListComp: (.List, , ) (func() { if .Filter != nil { (.Filter, , ) } if .Map != nil { (.Map, , ) } }, .Var) case *ast.Index: (.Base, , ) (.Idx, , ) case *ast.Slice: (.Base, , ) if .From != nil { (.From, , ) } if .To != nil { (.To, , ) } case *ast.PropOf: (.Base, , ) case *ast.MapLit: for , := range .Fields { (.Val, , ) } } } // renameFree rewrites every free reference to from -- Var nodes and the // by-name holders (Prop, HasLabelExpr, MapProj, Cost endpoints) -- into to, // stopping wherever an inner binder rebinds from (its references mean the // local). Children are rewritten in place; the possibly-replaced node is // returned. Callers pick a `to` that is fresh in e's scope, so the rename // can never capture. func renameFree( ast.Expr, , string) ast.Expr { := func( ast.Expr) ast.Expr { return (, , ) } := func( ...string) bool { return slices.Contains(, ) } switch n := .(type) { case *ast.Var: if .Name == { return &ast.Var{Name: } } case *ast.Prop: if .Var == { .Var = } case *ast.HasLabelExpr: if .Var == { .Var = } case *ast.MapProj: if .Var == { .Var = } for := range .Entries { if .Entries[].Kind == ast.MapProjField { .Entries[].Expr = (.Entries[].Expr) } } case *ast.Cost: if .From == { .From = } if .To == { .To = } case *ast.Unary: .Expr = (.Expr) case *ast.IsNull: .Expr = (.Expr) case *ast.IsTruth: .Expr = (.Expr) case *ast.IsTyped: .Expr = (.Expr) case *ast.PropOf: .Base = (.Base) case *ast.Binary: .LHS = (.LHS) .RHS = (.RHS) case *ast.In: .Expr = (.Expr) .List = (.List) case *ast.Index: .Base = (.Base) .Idx = (.Idx) case *ast.Func: for := range .Args { .Args[] = (.Args[]) } case *ast.ListExpr: for := range .Elems { .Elems[] = (.Elems[]) } case *ast.Case: if .Operand != nil { .Operand = (.Operand) } for := range .Whens { .Whens[].Cond = (.Whens[].Cond) .Whens[].Result = (.Whens[].Result) } if .Else != nil { .Else = (.Else) } case *ast.Slice: .Base = (.Base) if .From != nil { .From = (.From) } if .To != nil { .To = (.To) } case *ast.MapLit: for := range .Fields { .Fields[].Val = (.Fields[].Val) } case *ast.ListPred: .List = (.List) if !(.Var) { .Pred = (.Pred) } case *ast.ListComp: .List = (.List) if !(.Var) { if .Filter != nil { .Filter = (.Filter) } if .Map != nil { .Map = (.Map) } } case *ast.Reduce: .Init = (.Init) .List = (.List) if !(.Acc, .Var) { .Body = (.Body) } case *ast.Exists: if .Where != nil && !(patternVarNames(.Pattern)...) { .Where = (.Where) } case *ast.CountSub: if .Where != nil && !(patternVarNames(.Pattern)...) { .Where = (.Where) } case *ast.PatternComp: if !(patternVarNames(.Pattern)...) { if .Where != nil { .Where = (.Where) } .Proj = (.Proj) } } return }