◐ Off-By-One · answer catalog

chaitin-register-allocator

1 answer(s)pythonpython3

lowdegree = workgraph.getlowdegreenodes(k)

📦 Source in repository (JSON)

Answer

The Chaitin-Briggs register allocator is implemented in /tmp/chaitin_allocator.py (~17KB). The core algorithm has four phases:

1. Input Parsing & Interference Graph Construction

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.

2. Spill Cost Computation

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.

3. Simplify Phase

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

4. Select Phase

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.

Key code excerpt — the Chaitin-Briggs loop:

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

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog