blob: e7c81cce7d1d4957c80997eb108cfc690a1fa84d [file]
// 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)
}
}