lowdegree = workgraph.getlowdegreenodes(k)
The Chaitin-Briggs register allocator is implemented in /tmp/chaitin_allocator.py (~17KB). The core algorithm has four phases:
Three input formats are supported:
- live-ranges: each virtual register has (vreg, start, end) defining its live range. Overlapping ranges produce interference edges.
- ir: three-address code (e.g., v1 = v2 + v3). Live ranges are computed by tracking defs and last uses, then the interference graph is built from overlaps.
- edges: direct edge list u v for each interference pair.
Spill cost is inversely proportional to live range length (shorter ranges = cheaper to spill). For IR input, costs are scaled by 1 + ln(use_count) to reflect that frequently-used variables are more expensive to reload from memory.
Iteratively pops nodes off the graph:
- Low-degree nodes (degree < k) are pushed onto a stack (they're guaranteed colorable)
- High-degree nodes are spilled based on spill_cost / degree ratio — the variable with the lowest cost per interference edge is spilled first
Nodes are popped from the stack in reverse order. Each node gets the lowest-numbered physical register (0..k-1) not used by any already-colored neighbor. If no color is available, the node is spilled.
def chaitin_briggs_allocate(graph: InterferenceGraph) -> dict[int, int | None]:
k = graph.num_regs
work_graph = graph.copy()
stack = []
spilled = set()
# ---- SIMPLIFY ----
while work_graph.nodes:
low_degree = work_graph.get_low_degree_nodes(k)
if low_degree:
# Remove highest-cost low-degree node (better coloring)
node = max(low_degree, key=lambda n: work_graph.spill_cost.get(n, 1.0))
else:
# All high-degree: spill cheapest
node = min(work_graph.get_high_degree_nodes(k),
key=lambda n: work_graph.spill_cost.get(n, 1.0) /
max(work_graph.degree(n), 1))
spilled.add(node)
neighbors = list(work_graph.adj[node])
stack.append((node, neighbors))
work_graph.remove_node(node)
# ---- SELECT ----
assignment = {}
for node, neighbors in reversed(stack):
if node in spilled:
assignment[node] = None
continue
used = {assignment[n] for n in neighbors
if n in assignment and assignment[n] is not None}
for c in range(k):
if c not in used:
assignment[node] = c
break
else:
spilled.add(node)
assignment[node] = None
return assignment
The allocator was verified with **28 test cases** covering: | Category | Tests | What's verified | |---|---|---| | **Basic graph ops** | `test_graph_basic`, `test_graph_self_loop`, `test_empty_graph` | add_edge, degree, remove_node, self-loops ignored | | **No interference** | `test_no_interference`, `test_single_register_k1_no_interference` | All vars share register when no edges | | **Simple coloring** | `test_simple_interference`, `test_chordal_graph`, `test_alternating_path` | 2 vars with edge → distinct colors; path graphs 2-colorable | | **Spilling** | `test_three_nodes_two_registers`, `test_triangle_k2`, `test_k5_k3`, `test_spill_all_except_one` | Correct number spilled for cliques larger than k | | **Spill cost** | `test_spill_cost_preference`, `test_spill_heuristic_same_cost` | High-cost vars preferred for registers; equal costs handled | | **Input parsing** | `test_live_ranges_input_parsing`, `test_ir_input_parsing`, `test_ir_compute_live_ranges`, `test_edges_input_parsing`, `test_ir_with_copy_moves`, `test_instruction_with_constants` | All three formats parsed correctly; live range computation works for defs, uses, undefined vars, mov-only instructions | | **Large graphs** | `test_large_chordal_graph` (100 nodes), `test_linear_scan_case` | Scaling to 100+ node graphs; linear scan interference patterns | | **Integration** | `test_main_integration`, `test_format_output` | End-to-end CLI pipeline; output format validated | All 28 tests pass cleanly: ``` Results: 28 passed, 0 failed out of 28 tests ``` ### CLI verification: ``` $ echo -e "3\n4\n1 0 5\n2 2 8\n3 3 7\n4 1 6" | python3 chaitin_allocator.py --format live-ranges v1 -> r1 v2 -> spill v3 -> r2 v4 -> r0 ``` With 3 registers and 4 mutually-interfering variables, exactly 3 get registers and 1 spills — correct. ``` $ echo -e "3\n4\nv1 = v2 + v3\nv4 = v1 + v2\nv5 = v4 + v1\nv6 = v5 + v3" | python3 chaitin_allocator.py --format ir v1 -> r1 v2 -> r2 v3 -> r0 v4 -> r2 v5 -> r1 v6 -> r0 ``` Each interfering pair gets distinct registers. No conflicts. ---
{"model": "deepseek-v4-flash", "problem_class": "chaitin-register-allocator", "result": "passed", "tests": 28}