Skip to content

feat(ir): rewriter operations for code motion, copying and inlining - #846

Closed
alexcgatea wants to merge 16 commits into
rustfrom
alex/remaining-ir-operations
Closed

alexcgatea wants to merge 16 commits into
rustfrom
alex/remaining-ir-operations

Conversation

@alexcgatea

@alexcgatea alexcgatea commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds the Rewriter operations needed for code motion and inlining — moving and splicing IR, deep copies, and statement creation at an insertion point — plus a one-level call inliner in kirin-function that runs in linear time.

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
create(insertion_pt, definition) / insert(insertion_pt, stmt) Create a statement at an InsertionPoint (Before, After, Terminator, DiGraph, UnGraph), or place a detached one
clone_statement, clone_cfg + IRMap Detached deep copies with fresh ids, including graph bodies. Results and block arguments are always copied, overwriting their IRMap entries, so a map can be reused, even after an earlier copy is gone
kirin_function::inline_calls Inline one level of same-stage CallSpecialized calls in a CFG

API changes

  • insert_before / insert_after / insert_terminator / insert_node / insert_ungraph_node / insert_ungraph_edge are replaced by create(InsertionPoint, definition). One method, with the insertion point as data. An UnGraph member is a node or an edge statement according to its definition, so UnexpectedEdgeStatement / ExpectedEdgeStatement are removed.
  • insert(insertion_pt, stmt) only accepts a detached statement of this Rewriter, such as a clone (NotDetached otherwise). Detached statements and CFGs that are never placed are tombstoned when the Rewriter drops; readers of their values are left with dangling operands, which verify_derived reports and run_pass quarantines.
  • A detached producer can be placed after its consumers. When insert places it in a graph, it adds the DiGraph edges and UnGraph wire endpoints to consumers already there.
  • create adopts the unowned bodies its definition names (e.g. a cloned CFG), with the same checks as replace_statement.
  • drain_events is replaced by into_events(self), so the event log only grows while a Rewriter lives, and positions in it stay valid.
  • New CFGInfo::parent() accessor. AnchorInsideMoved is renamed MovedIntoItself.

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. walk_mut now checks the Rewriter's event log after each visit (MovedStatement, SplicedBlock, SplicedCfg, compared against a VisitedPlace recorded before the visit) and reports WalkError::VisitedMoved. Reverse BFS orders, which build their order up front, still allow it. A loop driving a cursor by hand records VisitedPlace::of(stage, stmt) and the event-log length before each visit, and calls WalkOrder::check_not_moved_since itself.

Inlining

kirin-python's Walk(Inline(...)) scales as O(n²) (#554): each multi-block inline splits the call's block, and the split walks every statement after the call. In heavily inlined kernels most of the program ends up in one block, so every split costs close to the whole program. Calls are also visited first to last, so the same tail is moved again for every later call. inline_calls is built to stay linear:

  • Single-block callee: its statements are copied in front of the call, with no split.
  • Multi-block callee: the block is split after the call and the copied CFG is spliced in between. It is entered with a branch passing the call's arguments to the copied entry block's arguments, so a callee whose entry block is a loop header keeps its loop-carried values. Each copied ret becomes a branch to the second half.
  • Last-to-first order: each block is walked from its last statement to its first, so a split only moves statements after the call, which no earlier split has moved. Each statement moves at most once, and a run is linear in the body's size plus the size of the copied callees. Only the whole-body inline_calls is public, so no single run can inline in a quadratic order.
  • Moving the copied blocks is O(blocks): splice_cfg_after relinks them, with no list removal from the front.
  • One level: calls brought in by inlined code are left for another run. Running it on callees before their callers inlines everything with one run per function.
  • No recursion: it is the caller's responsibility to ensure there is no recursion, self or mutual, among the calls inlined.
  • A language implements FunctionOps<T> (as_call / as_return) to tell the inliner which of its statements are calls and returns; a closure builds its branch op.

Not included

  • Recursion detection in the inliner (it's one level, so it always terminates).

alexcgatea and others added 13 commits November 5, 2026 11:42
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>
Replaces the six insert_* methods with create(point, definition) and
insert(point, stmt) for detached statements. UnGraph members are nodes
or edge statements as their definition says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clone_statement and clone_cfg make detached deep copies in two passes,
mapping ids through an IRMap. create adopts the unowned bodies its
definition names; detached CFGs never adopted are tombstoned on drop.
Graph bodies are rejected until they can be cloned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copied graph gets fresh ports, copied members in their roles and
yields; its topology is built from the copied operands once their uses
are recorded. Removes CopyOfGraphBody.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
inline_calls inlines one level of same-stage CallSpecialized calls in a
CFG's blocks and returns how many. Single-block callees are copied in
place; others are copied between the halves of a split block. Walking
each block from last to first keeps a run linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List splicing and cloning in the Rewriter module docs, count fifteen
MutationEvent kinds, and remove the remaining-IR-operations plan now
that it is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renames check_point to check_insertion_pt, body_encloses_point to
body_encloses_insertion_pt, and InsertionPoint parameters to
insertion_pt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexcgatea and others added 3 commits November 9, 2026 15:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Placing a detached DiGraph member now adds edges to member slots that
already read its results, and placing a detached UnGraph edge statement
resyncs its wires. Documents that a detached statement never placed
leaves its readers dangling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mapping a block argument no longer drops it from the copy, which left
incoming edges passing values to a block with fewer arguments. The
liveness check skips values the copy defines, so a map can be reused
after its earlier copy is gone. The inliner enters a multi-block copy
with a branch passing the call's arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexcgatea
alexcgatea force-pushed the alex/remaining-ir-operations branch from e0dd591 to 0962525 Compare November 10, 2026 00:07
@alexcgatea

alexcgatea commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

Closing to split into 3 smaller PRs: #848, #849, #851

@alexcgatea alexcgatea closed this Nov 10, 2026
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.

1 participant