Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 56 additions & 17 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,53 @@ def verify(g, chain, srcs, dst, adj):
if dst in srcs: found = 0
return bad, found

EVERY_BUDGET = int(os.environ.get('AXIOMCODE_EVERY_BUDGET') or 200000)
ROUTE_CAP = 1000 # past this many routes --every says "1000+" and asks for a narrower endpoint

def k_shortest_routes(E, starts, ends, k):
"""the k shortest simple routes from any start to any end, shortest first, and whether more exist (Yen's algorithm).

Each route costs a bounded number of breadth-first searches over the subgraph, so the work grows with k, not with
the number of routes the subgraph holds. Enumerating partial paths best-first instead kept every one of them
alive: through a cycle they multiply with every hop, and a query with fewer than k short routes ran out of
memory (#1341). A route stops at the first end it reaches, and a start is never a route by itself."""
import heapq
SRC, DST = '\0src', '\0dst' # never equal to a method id
adj = {n: [b for b, _ in bs] for n, bs in E.items()}
for n in ends: adj[n] = [DST] # a route ends at the first end it reaches
adj[SRC] = list(starts)
def bfs(a, banned_nodes, banned_edges):
prev = {a: None}; frontier = [a]
while frontier:
nxt = []
for u in frontier:
for v in adj.get(u, ()):
if v in prev or v in banned_nodes or (u, v) in banned_edges: continue
prev[v] = u
if v == DST:
path = [v]
while prev[path[-1]] is not None: path.append(prev[path[-1]])
return path[::-1]
nxt.append(v)
frontier = nxt
return None
first = bfs(SRC, set(), set())
if not first: return [], False
A, B, seen = [first], [], {tuple(first)}
real = lambda p: p[1:-1]
found = [real(first)] if len(real(first)) > 1 else []
while len(found) <= k:
last = A[-1]
for i in range(len(last) - 2):
spur, root = last[i], last[:i + 1]
banned_edges = {(p[i], p[i + 1]) for p in A if len(p) > i + 1 and p[:i + 1] == root}
tail = bfs(spur, set(root[:-1]), banned_edges)
if tail:
cand = root[:-1] + tail
if tuple(cand) not in seen: seen.add(tuple(cand)); heapq.heappush(B, (len(cand), cand))
if not B: break
_, nxt = heapq.heappop(B); A.append(nxt)
if len(real(nxt)) > 1: found.append(real(nxt))
return found[:k], len(found) > k

def every_route(g, res, q, srcs, dsts, max_paths, adj):
"""the subgraph of every method on ANY chain from a source to a target, and the simple paths through it (bounded)"""
Expand All @@ -636,22 +682,15 @@ def every_route(g, res, q, srcs, dsts, max_paths, adj):
# enumerate simple paths, shortest first, up to max_paths — each one re-checked against the edge set like the nearest chain
out = []; edge_set = {(a, b): t for a, b, t in g.edges()}
starts = sorted(n for n in nodes if n in srcs); ends = {n for n in nodes if n in dsts}
import heapq
pq = [(0, i, [s0], None) for i, s0 in enumerate(starts)]; cnt = len(starts)
# the route count alone does not bound this: through a cycle the partial paths multiply with every hop, and when
# fewer than max_paths short routes exist the heap grows until memory runs out (#1341). Bound the work as well.
budget = EVERY_BUDGET; spent = False
while pq and len(out) < max_paths:
if cnt >= budget: spent = True; break
d, _, pth, _ = heapq.heappop(pq)
if pth[-1] in ends and len(pth) > 1: out.append(pth); continue
if pth[-1] in ends and len(pth) == 1 and pth[-1] in dsts and len(starts) == 1: pass
for b, t in E.get(pth[-1], []):
if b in pth: continue # simple: no node twice
cnt += 1; heapq.heappush(pq, (d + 1, cnt, pth + [b], t))
print(f" {len(out)} simple path(s)" + (f" (first {max_paths}; --paths N for more)" if len(pq) and len(out) >= max_paths else '')
+ (f" (the search stopped after {budget:,} partial routes: the methods between the endpoints form cycles, so longer"
f" routes multiply without end — narrow an endpoint, e.g. Owner.method or --in <path>)" if spent else '') + ", shortest first:")
# count the routes up to ROUTE_CAP so the reader knows how many there are, and print the first max_paths of them
cap = max(ROUTE_CAP, max_paths)
found, more = k_shortest_routes(E, starts, ends, cap)
out = found[:max_paths]
if more:
print(f" {cap}+ simple paths — too many to list; narrow an endpoint (Owner.method, file:line or --in <path>) for a"
f" complete list. The {len(out)} shortest follow (--paths N for more):")
else:
print(f" {len(found)} simple path(s)" + (f", the {len(out)} shortest follow (--paths N for more)" if len(out) < len(found) else '') + ", shortest first:")
for pth in out:
hops = [(pth[i], pth[i + 1]) for i in range(len(pth) - 1)]
bad = [h for h in hops if h not in edge_set]
Expand Down
Loading