// Per-row evaluation of a compiled expression, mirroring the interpreter // exactly (the shared eval kernels keep the arithmetic, comparison, and // function semantics identical).
package compile import ( chickpeas ) func ceval( *eval.Ctx, cnode, *chickpeas.Snapshot, []value.Value, map[string]int) value.Value { switch n := .(type) { case *cLit: return .v case *cSlot: if .s >= 0 && .s < len() { return [.s] } return value.Null() case *cProp: return cevalProp(, ) case *cCmpPropConst: , := cevalProp(.prop, ), .c if .rev { , = , } , := value.Compare(, ) if ! { return value.Null() } var bool switch .op { case ast.OpEq: = == 0 case ast.OpNeq: = != 0 case ast.OpLt: = < 0 case ast.OpLte: = <= 0 case ast.OpGt: = > 0 default: // ast.OpGte = >= 0 } return value.Bool() case *cNot: if , := (, .e, , , ).AsBool(); { return value.Bool(!) } return value.Null() case *cNeg: := (, .e, , , ) if , := .AsInt(); { if == -9223372036854775808 { return value.Null() } return value.Int(-) } if .Kind() == value.KindFloat { , := .AsFloat() return value.Float(-) } return value.Null() case *cBin: return cevalBin(, , , , ) case *cList: := make([]value.Value, len(.xs)) for , := range .xs { [] = (, , , , ) } return value.List() case *cIn: := (, .e, , , ) if .IsNull() { return value.Null() } , := (, .list, , , ).AsList() if ! { return value.Null() } return inResult(, ) case *cInConst: := (, .e, , , ) if .IsNull() { return value.Null() } return .m.resultFor() case *cInCarried: .refresh(, , , ) := (, .e, , , ) if .IsNull() || .notList { return value.Null() } return .m.resultFor() case *cIsNull: return value.Bool((, .e, , , ).IsNull() != .negated) case *cSubquery: if .decorOK && !.decorOff { if , := decorCount(, , , ); { if .isCount { return value.Int(int64()) } return value.Bool( > 0) } } var int switch { case .hasMemo: if , := packNodeKey(.memoSlots, ); { = .memoI.GetOrCreate(, func() int { SubqueryWalks++ return eval.SubqueryCount(, .pattern, .where, , , !.isCount) }) break } .key = .key[:0] for , := range .memoSlots { .key = value.AppendKey(.key, []) } if , := .memo[string(.key)]; { = } else { SubqueryWalks++ = eval.SubqueryCount(, .pattern, .where, , , !.isCount) .memo[string(.key)] = } default: SubqueryWalks++ = eval.SubqueryCount(, .pattern, .where, , , !.isCount) } if .isCount { return value.Int(int64()) } return value.Bool( > 0) case *cCase: if .operand != nil { := (, .operand, , , ) for , := range .whens { if value.Equal((, [0], , , ), ) { return (, [1], , , ) } } } else { for , := range .whens { if (, [0], , , ).IsTruthy() { return (, [1], , , ) } } } if .els != nil { return (, .els, , , ) } return value.Null() case *cFunc: if .argv == nil { .argv = make([]value.Value, len(.args)) } for , := range .args { .argv[] = (, , , , ) } return eval.ApplyFunc(.op, .argv) case *cSlow: return eval.Eval(, .e, , ) } return value.Null() } // refresh re-evaluates a carried IN's list once per match-call epoch, // rebuilding the membership index only when the list is a different value // than last epoch's (a segment-stable slot skips every rebuild). Shared // by the tree evaluation and the per-candidate specialized form. func ( *cInCarried) ( *eval.Ctx, *chickpeas.Snapshot, []value.Value, map[string]int) { if .built && .epoch == .MatchEpoch { return } := ceval(, .list, , , ) if , := .AsList(); { if !.built || .notList || !value.SameBacking(, .lastList) { .m = buildMembership() .lastList = } .notList = false } else { .notList = true } .built = true .epoch = .MatchEpoch } // packNodeKey packs the correlated slots of a subquery into a single // uint64 memo key when they are one or two node ids, ok=false otherwise // (falling the caller back to the byte-string memo). Two node slots pack as // id0<<32 | id1; node ids fit u32, matching AppendKey's tagNode encoding. // Restricting the fast path to node kinds keeps a node and a relationship // of equal raw id from conflating, since a fixed subquery's memoSlots hold // a stable kind per position. func packNodeKey( []int, []value.Value) (uint64, bool) { switch len() { case 1: if , := [[0]].AsNode(); { return uint64(uint32()), true } case 2: , := [[0]].AsNode() , := [[1]].AsNode() if && { return uint64(uint32())<<32 | uint64(uint32()), true } } return 0, false } // decorFanoutCap bounds the anchor-selection degree probe: distinguishing a // hub endpoint from a leaf needs only enough of a count to compare, and the // probe runs once, on the first non-null row of a hot query. const decorFanoutCap = 1 << 16 // astDirToGraph maps a pattern direction to the graph traversal direction. func astDirToGraph( ast.Dir) chickpeas.Direction { switch { case ast.DirOut: return chickpeas.Outgoing case ast.DirIn: return chickpeas.Incoming } return chickpeas.Both } // decorFanout counts node's typed neighbors in dir, capped: a hub only needs // to out-count a leaf, not be measured exactly. func decorFanout( *eval.Ctx, chickpeas.NodeID, chickpeas.Direction, []string) int { := 0 for range .G.NeighborsByType(, , ) { if ++; >= decorFanoutCap { break } } return } // decorCount answers a decorrelatable COUNT{} from a per-anchor side table // (task 084): the subquery is evaluated ONCE per distinct anchor node (grouped // by the other endpoint) and each row is an O(1) map read. The anchor endpoint // -- the hubbier end by typed first-hop degree -- is chosen once and reused; // correctness is anchor-independent, so only the amortization depends on it. // ok=false hands the row back to the per-row memo/DFS path: a null endpoint, // or too many distinct anchors (the anchor is not amortizing, so decor is // switched off for the rest of the run). func decorCount( *eval.Ctx, *cSubquery, []value.Value, map[string]int) (int, bool) { , := [.decorStartSlot].AsNode() , := [.decorEndSlot].AsNode() if ! || ! { return 0, false } := .pattern.Hops if !.decorAnchorDecided { := decorFanout(, , astDirToGraph([0].Rel.Dir), [0].Rel.Types) := [len()-1] := decorFanout(, , astDirToGraph(.Rel.Dir).Reverse(), .Rel.Types) .decorAnchorIsEnd = > .decorAnchorDecided = true } , := .decorStartVar, .decorEndVar , := , if .decorAnchorIsEnd { , = .decorEndVar, .decorStartVar , = , } // Tables live on the Ctx keyed by (canonical identity, anchor node), so // sibling subqueries that differ only in their outer endpoint names // share one table per anchor. The canonical identity embeds the anchor // ROLE (the substitution marker sits where the anchor sits in the // pattern), so siblings whose runtime anchor choice diverged simply // stop sharing rather than mix tables. A shape with no canonical // identity keys privately by node pointer -- same store, no sharing. if !.decorCanonDone { .decorCanon = decorCanon(.pattern, .where, , ) if .decorCanon == "" { .decorCanon = fmt.Sprintf("\x00%p", ) } .decorCanonDone = true } // The one-entry cache answers the constant-anchor common case with an // integer compare; only an anchor CHANGE consults the shared store // (whose key hashes the canonical identity string). if .decorLastTbl != nil && .decorLastAnchor == uint32() { return .decorLastTbl[], true } := eval.DecorTableKey{Canon: .decorCanon, Anchor: uint32()} .decorProbes++ , := .DecorTables[] if ! { if .decorBuilds >= decorAnchorCap { .decorOff = true return 0, false } = eval.SubqueryGroupCount(, .pattern, .where, , , , ) if .DecorTables == nil { .DecorTables = map[eval.DecorTableKey]map[chickpeas.NodeID]int{} } .DecorTables[] = .decorBuilds++ .DecorBuilds++ } .decorLastAnchor, .decorLastTbl = uint32(), return [], true } // cevalBin mirrors the interpreter's binary dispatch over compiled // operands. // cevalProp reads a compiled property access against the row: a graph // column for node/rel slot values, a map field, or a temporal component // of an epoch-millis int; anything else is Null. func cevalProp( *cProp, []value.Value) value.Value { if .slot < 0 || .slot >= len() { return value.Null() } := [.slot] switch .Kind() { case value.KindNode: , := .AsNode() return .reader.readNode() case value.KindRel: , := .AsRel() return .reader.readRel() case value.KindMap: , := .AsMap() for , := range { if .Key == .reader.key { return .Val } } return value.Null() case value.KindInt: // Temporal accessor over an i64 epoch-millis value. , := .AsInt() if , := eval.Component(, .reader.key); { return value.Int() } return value.Null() case value.KindTemporal: , , := .AsTemporal() if , := eval.Component(, .reader.key); { return value.Int() } return value.Null() case value.KindDuration: , , , := .AsDuration() if , := eval.DurationComponent(, , , .reader.key); { return value.Int() } return value.Null() } return value.Null() } func cevalBin( *eval.Ctx, *cBin, *chickpeas.Snapshot, []value.Value, map[string]int) value.Value { switch .op { case ast.OpAnd: , := value.ThreeValued(ceval(, .l, , , )) if && ! { return value.Bool(false) } , := value.ThreeValued(ceval(, .r, , , )) return value.KleeneAnd(, , , ) case ast.OpOr: , := value.ThreeValued(ceval(, .l, , , )) if && { return value.Bool(true) } , := value.ThreeValued(ceval(, .r, , , )) return value.KleeneOr(, , , ) case ast.OpXor: , := value.ThreeValued(ceval(, .l, , , )) , := value.ThreeValued(ceval(, .r, , , )) if ! || ! { return value.Null() } return value.Bool( != ) } := ceval(, .l, , , ) := ceval(, .r, , , ) switch .op { case ast.OpEq: return eval.CmpBool(, , func( int) bool { return == 0 }) case ast.OpNeq: return eval.CmpBool(, , func( int) bool { return != 0 }) case ast.OpLt: return eval.CmpBool(, , func( int) bool { return < 0 }) case ast.OpLte: return eval.CmpBool(, , func( int) bool { return <= 0 }) case ast.OpGt: return eval.CmpBool(, , func( int) bool { return > 0 }) case ast.OpGte: return eval.CmpBool(, , func( int) bool { return >= 0 }) case ast.OpStartsWith, ast.OpEndsWith, ast.OpContains: return eval.StrPred(.op, , ) case ast.OpConcat: return eval.Concat(, ) default: return eval.Arith(.op, , ) } } // inResult is the openCypher IN result over a materialized list: true on // a hit, else null when the list contains a null element, else false. func inResult( []value.Value, value.Value) value.Value { := false for , := range { if value.Equal(, ) { return value.Bool(true) } if .IsNull() { = true } } if { return value.Null() } return value.Bool(false) } // SubqueryWalks counts actual correlated-subquery graph walks (memo // misses and unmemoized evaluations) -- the load-independent oracle for // predicate-placement tests: a walk that runs before a shrinking op is // visible as walk count, never as a row difference. var SubqueryWalks int