33#include " Dialect/EmitPythonBytecode/IR/EmitPythonBytecode.hpp"
44#include " RegisterAllocationLogger.hpp"
55#include " RegisterAllocationTypes.hpp"
6+ #include " mlir/Analysis/Liveness.h"
67#include " mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
78#include " mlir/Dialect/Func/IR/FuncOps.h"
89#include " mlir/IR/AsmState.h"
2122namespace codegen {
2223
2324/* *
24- * LiveAnalysis performs backward dataflow analysis to determine which values are alive
25- * at each point in the program. This is the first step in register allocation.
25+ * LiveAnalysis determines which values are alive at each point in the program.
26+ * This is the first step in register allocation.
2627 *
27- * Uses the standard liveness algorithm:
28- * LiveOut[B] = union of LiveIn[S] for all successors S of B
29- * LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B])
30- * Iterate until fixed point
28+ * The block-level fixed-point dataflow is delegated to mlir::Liveness — it
29+ * already implements the standard LiveOut[B] = ∪ LiveIn[S] for S in succ(B),
30+ * LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B]) algorithm and handles loops /
31+ * back-edges. What's project-specific stays here:
3132 *
32- * This correctly handles loops and back edges.
33+ * 1. ForwardedOutput for FOR_ITER. The body block's first argument is the
34+ * loop value produced by FOR_ITER's terminator, which MLIR can't model
35+ * natively. We track it as a synthetic Value-like entity.
36+ *
37+ * 2. alive_at_timestep — a linearised per-operation list of "what's alive
38+ * here". mlir::Liveness only exposes per-block / per-value queries; the
39+ * register allocator wants the linearised view, so we materialise it on
40+ * top of mlir::Liveness's block-level results.
41+ *
42+ * 3. block_input_mappings — for each value, the set of block arguments it
43+ * could flow into via a CFG edge. Computed from the terminator operands.
3344 */
3445class LiveAnalysis
3546{
@@ -53,18 +64,20 @@ class LiveAnalysis
5364 block_input_mappings;
5465
5566 /* *
56- * Analyze the function to determine liveness information using backward dataflow
67+ * Analyze the function to determine liveness information.
5768 */
5869 void analyse (mlir::func::FuncOp &fn)
5970 {
6071 auto logger = get_regalloc_logger ();
61- logger->info (
62- " Starting backward dataflow live analysis for function: {}" , fn.getName ().str ());
72+ logger->info (" Starting live analysis for function: {}" , fn.getName ().str ());
6373
6474 auto ®ion = fn.getRegion ();
6575 auto sorted_blocks = sortBlocks (region);
6676
67- // Build block information and Use/Def sets
77+ // Collect block operations and the project-specific bits MLIR doesn't
78+ // model natively (ForwardedOutputs from FOR_ITER, value→block-arg
79+ // edge mapping). Use/def computation is no longer needed here —
80+ // mlir::Liveness owns the dataflow.
6881 std::map<mlir::Block *, BlockInfo> block_info;
6982 std::vector<std::pair<std::variant<mlir::Value, ForwardedOutput>, mlir::BlockArgument>>
7083 block_parameters_to_args;
@@ -73,12 +86,13 @@ class LiveAnalysis
7386 build_block_info (block, block_info[block], block_parameters_to_args, logger);
7487 }
7588
76- // Run backward dataflow to compute LiveIn/LiveOut
77- compute_liveness (sorted_blocks, block_info, logger );
89+ // Block-level live-in / live-out via the upstream analysis.
90+ mlir::Liveness liveness (fn );
7891
79- // Build alive_at_timestep from LiveIn/LiveOut
92+ // Materialise alive_at_timestep on top of mlir::Liveness's block
93+ // results, injecting ForwardedOutputs into the live sets as needed.
8094 std::map<mlir::Block *, std::pair<size_t , size_t >> blocks_span;
81- build_timesteps (sorted_blocks, block_info, blocks_span);
95+ build_timesteps (sorted_blocks, block_info, liveness, blocks_span);
8296
8397 // Propagate block argument inputs through the liveness information
8498 propagate_block_arguments (block_parameters_to_args, blocks_span);
@@ -91,22 +105,13 @@ class LiveAnalysis
91105
92106 private:
93107 /* *
94- * Information about a single block for dataflow analysis
108+ * Per-block ancillary state: the operation list (in topological order,
109+ * needed because the linearised timesteps must match the order ops are
110+ * walked by the bytecode emitter) plus the ForwardedOutputs created by
111+ * this block's terminator (FOR_ITER's synthetic loop-var value).
95112 */
96113 struct BlockInfo
97114 {
98- // Values used in this block (before being defined)
99- ValueSet use;
100-
101- // Values defined in this block
102- ValueSet def;
103-
104- // Values live at entry to this block (computed by dataflow)
105- ValueSet live_in;
106-
107- // Values live at exit from this block (computed by dataflow)
108- ValueSet live_out;
109-
110115 // Operations in this block (in order)
111116 std::vector<mlir::Operation *> operations;
112117
@@ -115,7 +120,8 @@ class LiveAnalysis
115120 };
116121
117122 /* *
118- * Build Use/Def sets for a block
123+ * Collect operations and project-specific terminator metadata for a block.
124+ * Block-level live-in/out is computed separately by mlir::Liveness.
119125 */
120126 void build_block_info (mlir::Block *block,
121127 BlockInfo &info,
@@ -129,26 +135,12 @@ class LiveAnalysis
129135 std::abort ();
130136 }
131137
132- // Build Use/Def sets
133- // For each operation, add operands to Use (if not already in Def), and add results to Def
134- for (auto &op : block->getOperations ()) {
135- info.operations .push_back (&op);
136-
137- // Add operands to Use (if not already defined)
138- for (const auto &operand : op.getOperands ()) {
139- if (!info.def .contains (operand)) { info.use .insert (operand); }
140- }
141-
142- // Add results to Def
143- for (const auto &result : op.getResults ()) { info.def .insert (result); }
144- }
138+ for (auto &op : block->getOperations ()) { info.operations .push_back (&op); }
145139
146140 // Handle terminators specially
147141 if (auto *terminator = block->getTerminator ()) {
148142 handle_terminator (terminator, info, block_parameters_to_args, logger);
149143 }
150-
151- logger->debug (" Block has {} uses, {} defs" , info.use .size (), info.def .size ());
152144 }
153145
154146 /* *
@@ -217,77 +209,15 @@ class LiveAnalysis
217209 }
218210
219211 /* *
220- * Compute LiveIn/LiveOut using backward dataflow iteration
221- */
222- void compute_liveness (const std::vector<mlir::Block *> &sorted_blocks,
223- std::map<mlir::Block *, BlockInfo> &block_info,
224- std::shared_ptr<spdlog::logger> &logger)
225- {
226- logger->info (" Running backward dataflow iteration to compute liveness" );
227-
228- // Initialize all LiveIn and LiveOut to empty (already done by default)
229-
230- // Iterate until fixed point
231- bool changed = true ;
232- int iteration = 0 ;
233-
234- while (changed) {
235- changed = false ;
236- iteration++;
237-
238- logger->debug (" Dataflow iteration {}" , iteration);
239-
240- // Process blocks in reverse post-order for better convergence
241- for (auto it = sorted_blocks.rbegin (); it != sorted_blocks.rend (); ++it) {
242- auto *block = *it;
243- auto &info = block_info[block];
244-
245- // Save old LiveIn for convergence check
246- auto old_live_in = info.live_in ;
247-
248- // LiveOut[B] = union of LiveIn[S] for all successors S
249- info.live_out .clear ();
250- for (auto *successor : block->getSuccessors ()) {
251- const auto &succ_info = block_info[successor];
252- info.live_out .insert (succ_info.live_in .begin (), succ_info.live_in .end ());
253- }
254-
255- // LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B])
256- info.live_in = info.use ;
257- for (const auto &val : info.live_out ) {
258- if (!info.def .contains (val)) { info.live_in .insert (val); }
259- }
260-
261- // Add ForwardedOutputs to LiveIn (they're "defined" by the terminator but need
262- // to be live for the successor)
263- for (const auto &fwd : info.forwarded_outputs ) { info.live_in .insert (fwd); }
264-
265- // Check if LiveIn changed
266- if (info.live_in != old_live_in) { changed = true ; }
267- }
268- }
269-
270- logger->info (" Dataflow converged after {} iterations" , iteration);
271-
272- // Debug: print LiveIn/LiveOut for each block
273- for (auto *block : sorted_blocks) {
274- const auto &info = block_info[block];
275- logger->debug (" Block {} LiveIn: {} values, LiveOut: {} values" ,
276- static_cast <void *>(block),
277- info.live_in .size (),
278- info.live_out .size ());
279- }
280- }
281-
282- /* *
283- * Build alive_at_timestep from LiveIn/LiveOut
212+ * Build alive_at_timestep from mlir::Liveness's block live-out info.
284213 *
285214 * Computes precise per-operation liveness by propagating backward within each block.
286215 * This ensures values are only marked alive when actually needed, not conservatively
287216 * throughout the entire block.
288217 */
289218 void build_timesteps (const std::vector<mlir::Block *> &sorted_blocks,
290219 const std::map<mlir::Block *, BlockInfo> &block_info,
220+ const mlir::Liveness &liveness,
291221 std::map<mlir::Block *, std::pair<size_t , size_t >> &blocks_span)
292222 {
293223 auto logger = get_regalloc_logger ();
@@ -301,8 +231,13 @@ class LiveAnalysis
301231 std::vector<ValueSet> alive_before_op;
302232 alive_before_op.resize (info.operations .size ());
303233
304- // Start from LiveOut (values alive at block exit) and work backward
305- ValueSet alive_after = info.live_out ;
234+ // Start from LiveOut (values alive at block exit) and work backward.
235+ // mlir::Liveness returns a SmallPtrSet<Value>; the project's
236+ // ValueSet is a variant<Value, ForwardedOutput> set, so we
237+ // convert. ForwardedOutputs aren't part of mlir::Liveness's view
238+ // and are added separately below.
239+ ValueSet alive_after;
240+ for (mlir::Value v : liveness.getLiveOut (block)) { alive_after.insert (v); }
306241
307242 for (size_t i = info.operations .size (); i-- > 0 ;) {
308243 auto *op = info.operations [i];
@@ -349,19 +284,24 @@ class LiveAnalysis
349284 }
350285 }
351286
352- // Add ForwardedOutputs to the first operation if they're in LiveIn
353- // (they're "defined" by the terminator but need to be live for the successor)
287+ // Add ForwardedOutputs to the first operation's alive set.
288+ // These are "defined" by the terminator but need to be live
289+ // throughout the block so the successor (the FOR_ITER body)
290+ // can pick the loop variable up via the same register. The
291+ // previous custom dataflow unconditionally injected these
292+ // into LiveIn before this point; mlir::Liveness doesn't know
293+ // about them, so they're injected here directly.
354294 if (!info.operations .empty ()) {
355- for (const auto &fwd : info.forwarded_outputs ) {
356- if (info.live_in .contains (fwd)) { alive_before_op[0 ].insert (fwd); }
357- }
295+ for (const auto &fwd : info.forwarded_outputs ) { alive_before_op[0 ].insert (fwd); }
358296 }
359297
360- // Note: alive_before_op[0] may differ from LiveIn because the needs_tracking
361- // pass above adds impure operation results to the timestep of their defining op.
362- // These results are defined within this block so they cannot be in LiveIn.
363- // This discrepancy is expected and intentional — it ensures impure ops get
364- // proper register assignments at their definition site.
298+ // Note: alive_before_op[0] may differ from mlir::Liveness's
299+ // LiveIn(block) because the needs_tracking pass above adds
300+ // impure operation results to the timestep of their defining
301+ // op. These results are defined within this block so they
302+ // cannot be in LiveIn. The discrepancy is intentional — it
303+ // ensures impure ops get a register assignment at their
304+ // definition site.
365305
366306 // Now build timesteps in forward order using the computed liveness
367307 for (size_t i = 0 ; i < info.operations .size (); i++) {
@@ -382,7 +322,7 @@ class LiveAnalysis
382322 start,
383323 end,
384324 info.operations .size (),
385- info. live_in .size ());
325+ liveness. getLiveIn (block) .size ());
386326 if (block->getTerminator ()
387327 && mlir::isa<mlir::emitpybytecode::ForIter>(block->getTerminator ())) {
388328 logger->debug (" ^ FOR_ITER block" );
0 commit comments