// Expr -> CExpr lowering: hot leaves (slots, property reads) carry // resolved indices/readers; correlated subqueries gain a result memo keyed // on their outer reads; scalar functions with constant arguments fold at // compile time; the remaining rarely-hot nodes fall back to the // interpreter.
package compile import ( chickpeas ) // Compiled is a compiled expression bound to a snapshot; it satisfies the // executor's RowEval seam. fast, when derivable, is the whole-expression // monomorphic form (rowfast.go), result-identical to the tree. type Compiled struct { c cnode g *chickpeas.Snapshot fast rowFast } // newCompiled binds a lowered tree, deriving the monomorphic fast form // when the root shape supports it. Every construction site (initial // lowering and the hoisting rewrites) routes through here so the fast // form always reflects the final tree. func newCompiled( cnode, *chickpeas.Snapshot) *Compiled { return &Compiled{c: , g: , fast: deriveRowFast(, )} } // Eval evaluates the compiled expression against a row -- identical in // result to the interpreter on the source expression. func ( *Compiled) ( *eval.Ctx, []value.Value, map[string]int) value.Value { if .fast != nil { return .fast(, , ) } return ceval(, .c, .g, , ) } // cnode is one compiled node. type cnode interface{ isC() } type cLit struct{ v value.Value } type cSlot struct{ s int } type cProp struct { slot int reader propReader } type cNot struct{ e cnode } type cNeg struct{ e cnode } type cBin struct { op ast.BinOp l, r cnode } type cList struct{ xs []cnode } type cIn struct{ e, list cnode } // cInConst is IN over a batch-constant list, lowered once to a prebuilt // membership index. type cInConst struct { e cnode m inMembership } // cInCarried is IN over a loop-invariant (carried, not batch-constant) // list: the membership index rebuilds once per match-call epoch and is // reused across that call's candidates. lastList short-circuits the // rebuild entirely when the epoch's list is payload-identical to the // previous one (a segment-stable slot). type cInCarried struct { e, list cnode epoch uint64 built bool notList bool m inMembership lastList value.Value } type cIsNull struct { e cnode negated bool } // cSubquery is a correlated EXISTS/COUNT with a result memo keyed on the // outer slots it reads; memoSlots nil disables the memo (a nested // subquery's dependencies can't be enumerated). type cSubquery struct { pattern *ast.Pattern where ast.Expr isCount bool memoSlots []int hasMemo bool memo map[string]int // memoI is the entity-id fast-path memo: when every correlated slot // holds a node id (<=2 of them), the key packs into a uint64. A flat // open-addressed table -- one backing-array doubling amortizes the fill // -- rather than a built-in map, whose per-insert bucket/overflow growth // dominated the correlated-subquery allocation profile (SPB a14). Both // memos are lazy; a subquery uses whichever its correlated slots select. memoI flatset.U64Map // key is the reusable memo-key buffer: lookups probe string(key) // without allocating; only a miss's insert converts. key []byte // decor holds the decorrelation plan for a COUNT{} over a linear chain // with BOTH endpoints bound to outer variables (task 084): instead of a // DFS per outer row, evaluate the subquery ONCE per distinct anchor value // (grouped by the other endpoint) and answer each row with an O(1) map // read. decorOK is set at compile time; the remaining fields are // per-execution scratch (the compiled tree is rebuilt each run, so their // zero values reset correctly). Nil pattern/absent decorOK leaves the // existing memo/DFS path untouched. decorOK bool decorStartVar, decorEndVar string decorStartSlot, decorEndSlot int // decorAnchorDecided pins which endpoint anchors the single scan (the // hubbier end, decided once by typed first-hop degree); correctness is // anchor-independent, only speed depends on it. decorAnchorDecided bool decorAnchorIsEnd bool // decorCanon is the node's canonical table identity (decor_canon.go), // computed once after the anchor role is decided; tables live on the // Ctx keyed by (canon, anchor node) so sibling subqueries share them. // decorBuilds counts the builds THIS node triggered (asserted in // tests: a shared anchor builds ONCE; a sibling hitting another // node's table builds ZERO). decorOff falls back to the DFS path once // too many builds prove the anchor is not amortizing. decorCanon string decorCanonDone bool decorBuilds int decorOff bool // One-entry anchor cache in front of the shared store: consecutive // rows overwhelmingly share their anchor (a seeked hub never changes // across a segment), and the shared store's key hashes the canonical // identity string -- the cache answers those rows with one integer // compare. decorProbes counts shared-store consultations (asserted in // tests: a constant anchor probes once, not once per row). Sound // per-node: the canon is fixed once decided, so the anchor id alone // determines the table. decorLastAnchor uint32 decorLastTbl map[chickpeas.NodeID]int decorProbes int } // decorAnchorCap bounds the distinct anchor tables built before decorrelation // concedes the anchor is high-cardinality (not amortizing) and falls back to // the per-row path. Bounds worst-case wasted builds; correctness holds either // way. const decorAnchorCap = 64 type cCase struct { operand cnode whens [][2]cnode els cnode } type cFunc struct { op eval.FuncOp args []cnode // argv is the reused per-eval argument row. Safe because a compiled // tree is built per execution (compile.New takes the live Ctx), a node // cannot be its own descendant, and no ApplyFunc arm retains the argv // slice -- arms read elements by value or build fresh outputs. argv []value.Value } // cCmpPropConst fuses <prop> <comparison> <constant> -- the dominant // filter shape -- into one node: the hoisted column read and the shared // value.Compare run with no child dispatch. rev marks a constant left // operand. Semantics are identical to the unfused cBin: same reader, // same Compare coercions, same three-valued collapse. type cCmpPropConst struct { prop *cProp c value.Value op ast.BinOp rev bool } // cSlow defers to the interpreter (unrecognized functions, list // machinery, map forms, label predicates, cost). type cSlow struct{ e ast.Expr } func (*cLit) () {} func (*cSlot) () {} func (*cProp) () {} func (*cCmpPropConst) () {} func (*cNot) () {} func (*cNeg) () {} func (*cBin) () {} func (*cList) () {} func (*cIn) () {} func (*cInConst) () {} func (*cInCarried) () {} func (*cIsNull) () {} func (*cSubquery) () {} func (*cCase) () {} func (*cFunc) () {} func (*cSlow) () {} // New compiles e against the segment's slots and snapshot. Parameters // resolve at compile time (their values are constant for the execution). func ( *eval.Ctx, ast.Expr, map[string]int, *chickpeas.Snapshot) *Compiled { return newCompiled(comp(, , , ), ) } // ConstValue reports the expression's compile-time constant value: the // bottom-up literal folding is the one authority on constness (a slot or // property read never folds), so ok=false exactly when evaluation could // depend on a row. slots must be the caller's real slot map -- with an // empty map an unbound name folds to a null LITERAL, which would launder // row-dependent expressions (coalesce(m.ts, d) folds to d) into // constants. func ( *eval.Ctx, ast.Expr, map[string]int, *chickpeas.Snapshot) (value.Value, bool) { if , := comp(, , , ).(*cLit); { return .v, true } return value.Value{}, false } func comp( *eval.Ctx, ast.Expr, map[string]int, *chickpeas.Snapshot) cnode { switch n := .(type) { case *ast.Lit: return &cLit{v: eval.LitValue(, .Value)} case *ast.Var: if , := [.Name]; { return &cSlot{s: } } return &cLit{v: value.Null()} case *ast.Prop: if , := [.Var]; { return &cProp{slot: , reader: newPropReader(, .Key)} } return &cLit{v: value.Null()} case *ast.Unary: := (, .Expr, , ) var cnode if .Op == ast.Not { = &cNot{e: } } else { = &cNeg{e: } } return foldLits(, , , ) case *ast.Binary: , := (, .LHS, , ), (, .RHS, , ) := foldLits(, &cBin{op: .Op, l: , r: }, , , ) if := fuseCmpPropConst(.Op, , , ); != nil { return } return case *ast.ListExpr: := make([]cnode, len(.Elems)) for , := range .Elems { [] = (, , , ) } return foldLits(, &cList{xs: }, , ...) case *ast.In: , := (, .Expr, , ), (, .List, , ) return foldLits(, &cIn{e: , list: }, , , ) case *ast.IsNull: := (, .Expr, , ) return foldLits(, &cIsNull{e: , negated: .Negated}, , ) case *ast.Exists: , := correlatedSlots(.Pattern, .Where, ) // A single fixed hop with both endpoints outer-bound and no WHERE // answers in near-constant time (a bound-pair relationship count // off the edge-key sets), so memoizing it costs more than it // saves: the memo map for a large outer row set dwarfs every // other execution allocation while each hit barely beats the // probe it caches. := cheapExistsProbe(.Pattern, .Where, ) if && { = false } := &cSubquery{pattern: .Pattern, where: .Where, memoSlots: , hasMemo: , memo: map[string]int{}} // EXISTS decorrelates exactly like COUNT (the table answers >0): // an existential filter over a (bound, bound) multi-hop pattern // otherwise walks the inner pattern once per candidate row. The // near-constant bound-pair probe keeps its direct path. if ! { setupDecor(, .Pattern, .Where, ) } return case *ast.CountSub: , := correlatedSlots(.Pattern, .Where, ) := &cSubquery{pattern: .Pattern, where: .Where, isCount: true, memoSlots: , hasMemo: , memo: map[string]int{}} setupDecor(, .Pattern, .Where, ) return case *ast.Case: := &cCase{whens: make([][2]cnode, len(.Whens))} if .Operand != nil { .operand = (, .Operand, , ) } for , := range .Whens { .whens[] = [2]cnode{(, .Cond, , ), (, .Result, , )} } if .Else != nil { .els = (, .Else, , ) } return case *ast.Func: // A recognized scalar function with positional args compiles (and // constant-folds); DISTINCT/star forms and unknown names stay // interpreted. if , := eval.ResolveFuncOp(.Name); && !.Distinct && !.Star { := make([]cnode, len(.Args)) for , := range .Args { [] = (, , , ) } return foldFunc(, ) } return &cSlow{e: } default: // Cost (kernel dispatch lands with M17), list predicates, // comprehensions, reduce, index/slice, property-of-expression, // map forms, label predicates: interpreter-backed -- but a // row-independent subtree still folds to its literal (the dominant // win is a constant map argument to a temporal constructor in a // hot filter, e.g. duration({hours: 4}), which otherwise // rebuilds map + value per row). if constExpr(, nil) { return &cLit{v: eval.Eval(, , nil, nil)} } return &cSlow{e: } } } // fuseCmpPropConst collapses <prop> <comparison> <constant> (either // operand order) into a cCmpPropConst when the binary did not already // fold to a literal; nil when the shape doesn't apply. func fuseCmpPropConst( ast.BinOp, , , cnode) cnode { switch { case ast.OpEq, ast.OpNeq, ast.OpLt, ast.OpLte, ast.OpGt, ast.OpGte: default: return nil } if , := .(*cLit); { return nil } if , := .(*cProp); { if , := .(*cLit); { return &cCmpPropConst{prop: , c: .v, op: } } } if , := .(*cProp); { if , := .(*cLit); { return &cCmpPropConst{prop: , c: .v, op: , rev: true} } } return nil } // foldLits replaces built with its evaluated literal when every input is // already a literal -- the cnode ops are pure, so one ceval with no row is // result-identical to per-row evaluation. Folding composes bottom-up // through comp, so nested constant expressions collapse to one cLit. func foldLits( *eval.Ctx, cnode, *chickpeas.Snapshot, ...cnode) cnode { for , := range { if , := .(*cLit); ! { return } } return &cLit{v: ceval(, , , nil, nil)} } // constExpr reports whether e reads nothing from the row or graph -- no // free variable (iteration variables bound by an enclosing list scope // pass), no property access, no subquery, pattern, path, or label form -- // so it evaluates identically for every row. Parameters count as constant: // New resolves them at compile time, once per execution. Scalar functions // are all deterministic; startNode/endNode sit outside ResolveFuncOp and // so fail the resolution check. func constExpr( ast.Expr, map[string]bool) bool { switch n := .(type) { case *ast.Lit: return true case *ast.Var: return [.Name] case *ast.Unary: return (.Expr, ) case *ast.Binary: return (.LHS, ) && (.RHS, ) case *ast.ListExpr: for , := range .Elems { if !(, ) { return false } } return true case *ast.MapLit: for , := range .Fields { if !(.Val, ) { return false } } return true case *ast.In: return (.Expr, ) && (.List, ) case *ast.IsNull: return (.Expr, ) case *ast.Index: return (.Base, ) && (.Idx, ) case *ast.Slice: return (.Base, ) && (.From == nil || (.From, )) && (.To == nil || (.To, )) case *ast.PropOf: return (.Base, ) case *ast.Case: if .Operand != nil && !(.Operand, ) { return false } for , := range .Whens { if !(.Cond, ) || !(.Result, ) { return false } } return .Else == nil || (.Else, ) case *ast.Func: if .Distinct || .Star { return false } if , := eval.ResolveFuncOp(.Name); ! { return false } for , := range .Args { if !(, ) { return false } } return true case *ast.ListPred: return (.List, ) && (.Pred, boundWith(, .Var)) case *ast.Reduce: return (.List, ) && (.Init, ) && (.Body, boundWith(, .Acc, .Var)) case *ast.ListComp: if !(.List, ) { return false } := boundWith(, .Var) return (.Filter == nil || (.Filter, )) && (.Map == nil || (.Map, )) default: return false } } // boundWith extends a bound-variable set with list-scope iteration names. func boundWith( map[string]bool, ...string) map[string]bool { := make(map[string]bool, len()+len()) maps.Copy(, ) for , := range { [] = true } return } // foldFunc constant-folds a scalar function whose arguments are all // compile-time constants -- every FuncOp is deterministic, so this is // result-identical to per-row evaluation (the dominant win is a temporal // constructor in a hot filter parsing its ISO string once). // foldFunc evaluates a scalar function at plan time when every argument is a // literal, so a hot filter's constant call (e.g. duration({hours: 4})) is one // value instead of a per-row rebuild. A ZERO-arg call folds vacuously (the // loop below is empty) -- sound only because every scalar function here is // deterministic: the only zero-arg forms, date()/datetime()/localdatetime(), // return Null, not the current instant. A volatile function (rand/timestamp) // must NOT reach this fold -- its plan-time value would be baked into the // cached plan and replayed forever -- so it would need excluding here and at // constExpr both (the two fold sites). Locked by the gql package's zero-arg // fold tests. func foldFunc( eval.FuncOp, []cnode) cnode { := make([]value.Value, len()) for , := range { , := .(*cLit) if ! { return &cFunc{op: , args: } } [] = .v } return &cLit{v: eval.ApplyFunc(, )} }