// Bounded trail enumeration: the depth-first walk that emits one endpoint // per qualifying trail of length min..=max, enforcing relationship // uniqueness (and ACYCLIC node uniqueness), carried-state pruning, and // optional per-trail rel/pair collection. Split from varexpand.go, which // holds the candidate generation and reachability (BFS) paths.
package exec import ( ) // varWalk is the bounded trail enumeration's state: emitted endpoints (one // per qualifying trail), the named-rel arena, the relationship-uniqueness // edge stack, and per-depth candidate buffers reused across siblings. type varWalk struct { ctx *eval.Ctx op *plan.BindOp m *graph.NodeMatcher rm *graph.RelMatcher gate hopGate bound graph.NodeID haveBound bool collectRels bool max uint64 // MATCH-scope uniqueness: a checking op skips edges whose canonical // pair is on the scope's used stack; a contributing op collects each // trail's pairs (collectPairs) for the DFS to push while bound. uniq *uniqEnv collectPairs bool pathRels []uint32 pathPairs [][2]graph.NodeID used [][2]graph.NodeID // visited is the ACYCLIC mode's node stack (seeded with the start // node); empty means trail semantics (rel uniqueness only). visited []graph.NodeID scratch [][]nodePos cand *[]graph.NodeID relData *[]uint32 relRanges *[][2]int pairData *[][2]graph.NodeID pairRanges *[][2]int // nbufN/nbufP are the per-hop batch buffers (parallel neighbor/pos), // reused across dfs levels: each level copies them into its own // scratch before recursing. nbufN []graph.NodeID nbufP []uint32 } type nodePos struct { nb graph.NodeID pos uint32 } // dfs enumerates trails of length min..=max with no repeated relationship, // emitting each qualifying endpoint (one entry per trail, matching the // per-path semantics). st is the gate carry's per-path state, threaded // down the recursion. func ( *varWalk) ( graph.NodeID, uint64, carryState) { if >= .max { return } := int() for len(.scratch) <= { .scratch = append(.scratch, nil) } := .scratch[][:0] // Walk relationships (carrying positions) when the gate prunes by // position or the rel list is recorded; otherwise the leaner neighbor // batch. Batch appends replace the iter.Seq forms, which heap-allocate // their closures per call. if .gate.pred != nil || .gate.carry != nil || .collectRels { .nbufN, .nbufP = .ctx.G.AppendRelationshipsMatched(.nbufN[:0], .nbufP[:0], , .op.Dir, .rm) for , := range .nbufN { if := .nbufP[]; .gate.pred == nil || .gate.pred.keep(.ctx, ) { = append(, nodePos{, }) } } } else { .nbufN = .ctx.G.AppendNeighborsMatched(.nbufN[:0], , .op.Dir, .rm) for , := range .nbufN { = append(, nodePos{, 0}) } } .scratch[] = for := 0; < len(.scratch[]); ++ { , := .scratch[][].nb, .scratch[][].pos // Relationship-uniqueness: undirected edges are unordered. := [2]graph.NodeID{, } if .op.Dir == graph.Both && < { = [2]graph.NodeID{, } } if containsEdge(.used, ) { continue } // MATCH-scope uniqueness: a checking op also skips edges whose // canonical pair an earlier op in the clause already bound. var [2]graph.NodeID if .uniq != nil && .op.Uniq != nil { [0], [1] = uniqPair(.op.Dir, , ) if .op.Uniq.Check && .uniq.used(.op.Uniq.Scope, [0], [1]) { continue } } // ACYCLIC additionally rejects a hop that revisits any node on // this path (the start included). if .op.Acyclic && containsNode(.visited, ) { continue } // Carried-state pruning: a hop the carry rejects (e.g. its key does // not continue the monotonic order) admits no qualifying trail // through it. A path's first hop starts the state, never fails it. := if .gate.carry != nil { var bool if , = .gate.carry.step(.ctx, , ); ! { continue } } .used = append(.used, ) if .op.Acyclic { .visited = append(.visited, ) } if .collectRels { .pathRels = append(.pathRels, ) } if .collectPairs { .pathPairs = append(.pathPairs, ) } := + 1 if >= .op.Min && (!.haveBound || .bound == ) && .ctx.G.NodeMatcherAccepts(.m, ) { *.cand = append(*.cand, ) if .collectRels { *.relRanges = append(*.relRanges, [2]int{len(*.relData), len(.pathRels)}) *.relData = append(*.relData, .pathRels...) } if .collectPairs { *.pairRanges = append(*.pairRanges, [2]int{len(*.pairData), len(.pathPairs)}) *.pairData = append(*.pairData, .pathPairs...) } } .(, , ) if .collectRels { .pathRels = .pathRels[:len(.pathRels)-1] } if .collectPairs { .pathPairs = .pathPairs[:len(.pathPairs)-1] } if .op.Acyclic { .visited = .visited[:len(.visited)-1] } .used = .used[:len(.used)-1] } } // containsNode is the acyclic stack's membership test -- same short // linear scan rationale as containsEdge. func containsNode( []graph.NodeID, graph.NodeID) bool { return slices.Contains(, ) } // containsEdge is the trail stack's membership test -- a short linear scan // (never longer than max, e.g. 3 for {1,3}) beats a hashing set. func containsEdge( [][2]graph.NodeID, [2]graph.NodeID) bool { return slices.Contains(, ) }