Skip to content

feat(ir): move and splice statements, blocks and CFGs in Rewriter - #848

Open
alexcgatea wants to merge 14 commits into
rustfrom
alex/code-motion
Open

alexcgatea wants to merge 14 commits into
rustfrom
alex/code-motion

Conversation

@alexcgatea

@alexcgatea alexcgatea commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds the Rewriter operations for code motion: moving statements, splicing a block's statements, and splicing a whole CFG. Walks now detect when the statement being visited is moved, and the rewrite driver is held to the same walk contract.

New operations

Operation What it does
replace_uses_if, replace_all_uses_except_stmt(s) replace_all_uses restricted to selected use sites
move_before / move_after Move a statement next to an anchor in any block body
splice_block_before / splice_block_after Move a block's statements next to an anchor, redirecting its arguments
splice_cfg_after Move all blocks of a CFG after an anchor block in another CFG

Each records a new event (MovedStatement, SplicedBlock, SplicedCfg). Moving a statement into its own bodies, or splicing a block or CFG into itself, is rejected (MovedIntoItself, SplicedIntoItself, CfgSplicedIntoItself). Moving a statement to where it already is (for example move_before(a, b) when a is directly before b) does nothing and records no event.

API changes

  • drain_events is replaced by into_events(self), so the event log only grows while a Rewriter lives, and positions in it stay valid.
  • Walk cursors are no longer public. Outside kirin-ir, a walk is WalkOrder::walk, walk_mut or schedule; WalkCursor, the cursor types and WalkOrder::start are crate-private. A cursor does not check the walk contract (only walk_mut does, using the Rewriter's event log), so every walk that edits now goes through walk_mut.
  • WalkOrder::ALLOWS_ERASING_VISITED is renamed ALLOWS_ERASING_OR_MOVING_VISITED.

Walk safety

Moving or splicing the statement being visited breaks pre-order, post-order and BFS walks, which find the next statement through the visited one. After each visit, walk_mut fails with WalkError::VisitedMoved if the statement was:

  • moved with move_before / move_after (from the event log);
  • carried along by splice_block_* or splice_cfg_after (from the event log, with its block or CFG compared before and after the visit, so a terminator left behind by a block splice, or a splice that moves nothing, is fine);
  • moved by merging its block into another (its block from before the visit is now erased; a split keeps the original block, so it passes).

The reverse BFS orders, which build their order up front, still allow all of these.

All contract checks now run in one place: right after each visit, walk_mut checks erasure (each order's own rule, e.g. BFS allows erasing the visited statement together with an ancestor), then moves, before the cursor steps on. As a result:

  • The checks run whatever the visit returns, Interrupt included: erasing or moving the visited statement and then interrupting is an error too.
  • The walk root is no longer exempt: merging its block fails like any other visited statement.
  • A cursor's next() only steps and cannot fail; walk and schedule no longer discard errors silently.

Rewrite driver

RewriteDriver's sweep now runs through walk_mut, so a rule that moves, splices or erases the statement being visited where the walk order does not allow it fails the run with DriverError::Walk, as its docs already said. A rule's own error is reported before a walk error. This is temporary: #854 replaces the sweep with a worklist over a snapshot, where moves are allowed.

Internal

  • Linking and unlinking statements in a block and blocks in a CFG go through one set of helpers (rewrite/list.rs: link, unlink, split_off, over a small ListItem trait for Statement and Block), in the spirit of MLIR's ilist_traits. They replace six hand-written copies; split_block uses split_off + link.
  • merge_blocks and split_block hand a terminator to its new block through one helper, Rewriter::set_terminator.
  • Inserts, moves and block splices pick their place with a Side enum (Before / After) instead of a bool.
  • Error docs for moves and splices name the variants actually returned for unknown ids.

alexcgatea and others added 5 commits November 10, 2026 08:21
Add replace_uses_if, replace_all_uses_except_stmt and
replace_all_uses_except_stmts, which redirect only the use sites a
predicate accepts. ReplacedUses is recorded only once the old value has no
uses left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The event log now only grows while a Rewriter lives, so positions in
events() stay valid. Tests track positions with an EventCursor helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves record MovedStatement; walk_mut rejects moving the visited
statement in link-following orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore/after

Moves every non-terminator statement of a block next to an anchor and
redirects its arguments; records SplicedBlock, which walk_mut checks.
Drops the planned block move (step 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves every block of a CFG after an anchor block in another CFG; records
SplicedCfg. walk_mut now records a VisitedPlace (block and CFG) before
each visit to check splices against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@planza-quera planza-quera left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, though if I'm honest this has become quite hard to review since it's a lot of code that I haven't worked with. I suggest also getting an AI to review it.

Comment on lines +72 to +73
let result = visit(rewriter, stmt);
Self::check_not_moved_since(rewriter, stmt, place, seen)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we first check that result is an interrupt to end the walk, or check that stmt wasn't moved? The former allows us to move a stmt and end the walk without failing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but I think in this case we do want the walk to fail because the contract specifies the current statement shouldn't move (regardless of the walk result).

Comment thread crates/kirin-ir/src/rewrite/moves.rs Resolved
alexcgatea and others added 9 commits November 11, 2026 11:34
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- run the rewrite driver's sweep through walk_mut
- check erases and merges of the visited statement on interrupt too
- no longer exempt the walk root from the move checks
- make walk cursors crate-private
- rename ALLOWS_DISPLACING_VISITED to ALLOWS_ERASING_OR_MOVING_VISITED

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…moved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ebug

- share the depth-first cursors' visited-statement check
- merge split imports and rewrap the Skip docs
- debug-assert that unlink gets a live item from a non-empty list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Side enum replaces the before/after bool in slot_at and its callers
- Rewriter::set_terminator replaces duplicated code in merge and split
- check_not_moved_since no longer generic over the walk order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- walk_mut checks erasure and moves (including merges) after each visit
- cursors no longer track the visited block; next() is infallible
- rename check_visited to check_erasure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexcgatea

Copy link
Copy Markdown
Collaborator Author

A couple of iterations of AI review suggested some refactoring such as linking and unlinking statements in a block and blocks in a CFG via a ListItem trait for Statement and Block, and checking the walk contract in one place in walk_mut only. I think the changes are helpful but now there's quite a bit more code change in this PR.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants