| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package flow_test | |
| import ( | |
| "iter" | |
| "math/rand" | |
| "slices" | |
| "testing" | |
| "time" | |
| "golang.org/x/tools/internal/flow" | |
| ) | |
| type simpleGraph struct { | |
| numNodes int | |
| edges [][]int | |
| } | |
| func (g *simpleGraph) NumNodes() int { | |
| return g.numNodes | |
| } | |
| func (g *simpleGraph) Nodes() iter.Seq[int] { | |
| return func(yield func(int) bool) { | |
| for i := range g.numNodes { | |
| if !yield(i) { | |
| break | |
| } | |
| } | |
| } | |
| } | |
| func (g *simpleGraph) Out(nid int) iter.Seq[int] { | |
| return slices.Values(g.edges[nid]) | |
| } | |
| func checkAnalysis[Fact comparable](t *testing.T, analysis *flow.Analysis[Fact, int], wantIns map[int]Fact, wantEdges map[[2]int]Fact) { | |
| t.Helper() | |
| for nid, want := range wantIns { | |
| if got := analysis.In(nid); got != want { | |
| t.Errorf("In(%d): got %v, want %v", nid, got, want) | |
| } | |
| } | |
| for edge, want := range wantEdges { | |
| from, to := edge[0], edge[1] | |
| if got := analysis.Edge(from, to); got != want { | |
| t.Errorf("Edge(%d, %d): got %v, want %v", from, to, got, want) | |
| } | |
| } | |
| } | |
| func TestBasic(t *testing.T) { | |
| // Define a trivial graph of 4 nodes in sequence: 0 -> 1 -> 2 -> 3 | |
| g := &simpleGraph{ | |
| numNodes: 4, | |
| edges: [][]int{ | |
| 0: {1}, | |
| 1: {2}, | |
| 2: {3}, | |
| 3: {}, | |
| }, | |
| } | |
| // Transfer function: OR source node's ID into the bitset. | |
| transfer := func(from, to int, fact nodeSet) nodeSet { | |
| return fact | (1 << uint(from)) | |
| } | |
| analysis := flow.Forward[nodeSetUnion](g, nil, transfer) | |
| checkAnalysis(t, analysis, | |
| map[int]nodeSet{ | |
| 0: 0, | |
| 1: set(0), | |
| 2: set(0, 1), | |
| 3: set(0, 1, 2), | |
| }, | |
| map[[2]int]nodeSet{ | |
| {0, 1}: set(0), | |
| {1, 2}: set(0, 1), | |
| {2, 3}: set(0, 1, 2), | |
| }, | |
| ) | |
| } | |
| func TestDiamond(t *testing.T) { | |
| // Diamond graph: | |
| // 0 | |
| // / \ | |
| // 1 2 | |
| // \ / | |
| // 3 | |
| g := &simpleGraph{ | |
| numNodes: 4, | |
| edges: [][]int{ | |
| 0: {1, 2}, | |
| 1: {3}, | |
| 2: {3}, | |
| 3: {}, | |
| }, | |
| } | |
| transfer := func(from, to int, fact nodeSet) nodeSet { | |
| return fact | (1 << uint(from)) | |
| } | |
| analysis := flow.Forward[nodeSetUnion](g, nil, transfer) | |
| checkAnalysis(t, analysis, | |
| map[int]nodeSet{ | |
| 0: 0, | |
| 1: set(0), // Edge (0, 1) carries NodeID 0 | |
| 2: set(0), // Edge (0, 2) carries NodeID 0 | |
| 3: set(0, 1, 2), | |
| }, | |
| map[[2]int]nodeSet{ | |
| {0, 1}: set(0), | |
| {0, 2}: set(0), | |
| {1, 3}: set(0, 1), | |
| {2, 3}: set(0, 2), | |
| }, | |
| ) | |
| } | |
| func TestCycle(t *testing.T) { | |
| // Graph with a cycle: | |
| // 0 -> 1 -> 2 | |
| // ^ | | |
| // +----+ | |
| g := &simpleGraph{ | |
| numNodes: 3, | |
| edges: [][]int{ | |
| 0: {1}, | |
| 1: {2}, | |
| 2: {1}, | |
| }, | |
| } | |
| transfer := func(from, to int, fact nodeSet) nodeSet { | |
| return fact | (1 << uint(from)) | |
| } | |
| analysis := flow.Forward[nodeSetUnion](g, nil, transfer) | |
| checkAnalysis(t, analysis, | |
| map[int]nodeSet{ | |
| 0: 0, | |
| 1: set(0, 1, 2), | |
| 2: set(0, 1, 2), | |
| }, | |
| map[[2]int]nodeSet{ | |
| {0, 1}: set(0), | |
| {1, 2}: set(0, 1, 2), | |
| {2, 1}: set(0, 1, 2), | |
| }, | |
| ) | |
| } | |
| func TestFlowOrder(t *testing.T) { | |
| // This test constructs a "Spine with Bottleneck" graph in which a naive | |
| // visit order would result in quadratic time. | |
| // | |
| // Structure: | |
| // - Spine: Chain 0 -> 1 -> ... -> T-1. | |
| // - Bottleneck: All spine nodes (0..T-1) also point to T. | |
| // - Tail: T begins a chain T -> T+1 -> ... -> N-1. | |
| // | |
| // However, we randomly permute the node IDs to prevent a "lucky" order | |
| // derived from the node IDs. | |
| // | |
| // With a naive order, updates to T from the spine would arrive one by one | |
| // (or in small batches), triggering re-evaluation of the entire tail | |
| // multiple times, and O(T*N) transfer calls. | |
| // | |
| // With RPO, T is evaluated once after all spine nodes are processed. | |
| T := 64 // Max ID allowed by nodeSet | |
| N := 2 * T | |
| // Permute IDs to prevent "lucky" ordering. | |
| perm := rand.New(rand.NewSource(42)).Perm(N) | |
| invPerm := make([]int, N) | |
| for i, p := range perm { | |
| invPerm[p] = i | |
| } | |
| // node maps a logical node ID to a permuted NodeID. | |
| node := func(logical int) int { | |
| return int(perm[logical]) | |
| } | |
| // Build the edges. | |
| edges := make([][]int, N) | |
| nEdges := 0 | |
| addEdge := func(u, v int) { | |
| uID := node(u) | |
| edges[uID] = append(edges[uID], node(v)) | |
| nEdges++ | |
| } | |
| // Spine: 0 -> 1 -> ... -> T-1 | |
| for i := range T { | |
| if i+1 != T { | |
| addEdge(i, i+1) | |
| } | |
| addEdge(i, T) | |
| } | |
| // Tail: T -> T+1 -> ... -> N-1 | |
| for i := T; i < N-2; i++ { | |
| addEdge(i, i+1) | |
| } | |
| g := &simpleGraph{ | |
| numNodes: N, | |
| edges: edges, | |
| } | |
| transfers := 0 | |
| // Transfer: if pred is on the spine (< T), set its bit. | |
| transfer := func(from, to int, in nodeSet) nodeSet { | |
| transfers++ | |
| logicalPred := invPerm[from] | |
| if logicalPred < T { | |
| return in | (1 << uint64(logicalPred)) | |
| } | |
| return in | |
| } | |
| // Measure flow.Forward (RPO) | |
| start := time.Now() | |
| _ = flow.Forward[nodeSetUnion](g, nil, transfer) | |
| durRPO := time.Since(start) | |
| t.Logf("RPO time: %v", durRPO) | |
| // RPO should result in optimal evaluation, visiting each edge exactly once. | |
| if transfers != nEdges { | |
| t.Errorf("got %d transfer calls, expected %d for optimal evaluation strategy", transfers, nEdges) | |
| } | |
| } |