// 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 compileimport (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.typeCompiledstruct { 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(, , ) }returnceval(, .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 = 64type 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 {returnnewCompiled(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 }returnvalue.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, , )varcnodeif .Op == ast.Not { = &cNot{e: } } else { = &cNeg{e: } }returnfoldLits(, , , )case *ast.Binary: , := (, .LHS, , ), (, .RHS, , ) := foldLits(, &cBin{op: .Op, l: , r: }, , , )if := fuseCmpPropConst(.Op, , , ); != nil {return }returncase *ast.ListExpr: := make([]cnode, len(.Elems))for , := range .Elems { [] = (, , , ) }returnfoldLits(, &cList{xs: }, , ...)case *ast.In: , := (, .Expr, , ), (, .List, , )returnfoldLits(, &cIn{e: , list: }, , , )case *ast.IsNull: := (, .Expr, , )returnfoldLits(, &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, ) }returncase *ast.CountSub: , := correlatedSlots(.Pattern, .Where, ) := &cSubquery{pattern: .Pattern, where: .Where, isCount: true, memoSlots: , hasMemo: , memo: map[string]int{}}setupDecor(, .Pattern, .Where, )returncase *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, , ) }returncase *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 { [] = (, , , ) }returnfoldFunc(, ) }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).ifconstExpr(, 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 {caseast.OpEq, ast.OpNeq, ast.OpLt, ast.OpLte, ast.OpGt, ast.OpGte:default:returnnil }if , := .(*cLit); {returnnil }if , := .(*cProp); {if , := .(*cLit); {return &cCmpPropConst{prop: , c: .v, op: } } }if , := .(*cProp); {if , := .(*cLit); {return &cCmpPropConst{prop: , c: .v, op: , rev: true} } }returnnil}// 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:returntruecase *ast.Var:return [.Name]case *ast.Unary:return (.Expr, )case *ast.Binary:return (.LHS, ) && (.RHS, )case *ast.ListExpr:for , := range .Elems {if !(, ) {returnfalse } }returntruecase *ast.MapLit:for , := range .Fields {if !(.Val, ) {returnfalse } }returntruecase *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, ) {returnfalse }for , := range .Whens {if !(.Cond, ) || !(.Result, ) {returnfalse } }return .Else == nil || (.Else, )case *ast.Func:if .Distinct || .Star {returnfalse }if , := eval.ResolveFuncOp(.Name); ! {returnfalse }for , := range .Args {if !(, ) {returnfalse } }returntruecase *ast.ListPred:return (.List, ) && (.Pred, boundWith(, .Var))case *ast.Reduce:return (.List, ) && (.Init, ) && (.Body, boundWith(, .Acc, .Var))case *ast.ListComp:if !(.List, ) {returnfalse } := boundWith(, .Var)return (.Filter == nil || (.Filter, )) && (.Map == nil || (.Map, ))default:returnfalse }}// 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(, )}}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.