diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index ece86535cd..49aeceafe2 100644 --- a/crates/kirin-ir/src/lib.rs +++ b/crates/kirin-ir/src/lib.rs @@ -54,7 +54,9 @@ pub use node::{ pub use pipeline::Pipeline; pub use product::{HasProduct, Product}; pub use project::{Project, ProjectError, TryProject, TryProjectTo}; -pub use rewrite::{MutationEvent, QuarantineCause, Quarantined, RewriteError, Rewriter}; +pub use rewrite::{ + InsertionPoint, MutationEvent, QuarantineCause, Quarantined, RewriteError, Rewriter, +}; pub use signature::{ ExactSemantics, HasSignature, LatticeSemantics, Signature, SignatureCmp, SignatureSemantics, }; diff --git a/crates/kirin-ir/src/rewrite/alloc.rs b/crates/kirin-ir/src/rewrite/alloc.rs deleted file mode 100644 index fe2fad9e21..0000000000 --- a/crates/kirin-ir/src/rewrite/alloc.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Statement allocation shared by every insert: block splices, -//! `insert_terminator`, and `DiGraph` nodes. Each insert checks and places -//! the statement its own way; these do the common part. - -use crate::arena::GetInfo; -use crate::node::linked_list::LinkedListNode; -use crate::{Dialect, SSAValue, Statement, StatementInfo, StatementParent, Use}; - -use crate::rewrite::rewriter::{RewriteError, Rewriter, add_use}; - -impl<'a, L: Dialect> Rewriter<'a, L> { - /// Check `definition`'s results and operands for insertion, returning - /// both: every result must be a fresh [`Rewriter::new_result`] value - /// ([`RewriteError::ResultNotFresh`]) and every operand live - /// ([`RewriteError::UnknownValue`]). - pub(super) fn prepare_insert( - &self, - definition: &L, - ) -> Result<(Vec, Vec), RewriteError> { - let results = self.fresh_results(definition)?; - let operands: Vec = definition.arguments().copied().collect(); - for &operand in &operands { - if operand.get_info(self.stage).is_none() { - return Err(RewriteError::UnknownValue(operand)); - } - } - Ok((results, operands)) - } - - /// Allocate `definition` as a statement under `parent` with chain links - /// `prev`/`next`, bind `results`, and record `operands` as its uses — the - /// two lists from [`prepare_insert`](Rewriter::prepare_insert). Placing it - /// (block chain, terminator slot, graph node) is left to the caller. - pub(super) fn alloc_statement( - &mut self, - definition: L, - parent: StatementParent, - prev: Option, - next: Option, - results: Vec, - operands: &[SSAValue], - ) -> Statement { - let stmt = self - .stage - .statement_arena_mut() - .alloc_with_id(|id| StatementInfo { - node: LinkedListNode { - ptr: id, - next, - prev, - }, - parent: Some(parent), - definition, - }); - self.bind_results(stmt, results); - for (index, &operand) in operands.iter().enumerate() { - add_use(self.stage, operand, Use::StatementOperand { stmt, index }); - } - stmt - } -} diff --git a/crates/kirin-ir/src/rewrite/bodies.rs b/crates/kirin-ir/src/rewrite/bodies.rs index 947fa02c91..08204ec2ba 100644 --- a/crates/kirin-ir/src/rewrite/bodies.rs +++ b/crates/kirin-ir/src/rewrite/bodies.rs @@ -1,9 +1,11 @@ -//! Bodies owned by a statement, for [`Rewriter::replace_statement`] and -//! statement erasure. +//! Bodies owned by a statement, for [`Rewriter::replace_statement`], +//! statement erasure, and the checks that keep a statement out of its own +//! bodies. use smallvec::SmallVec; use crate::arena::{GetInfo, GetInfoMut}; +use crate::rewrite::walk::tree; use crate::{BlockParent, Body, Dialect, RewriteError, Rewriter, Statement, StatementParent}; impl Rewriter<'_, L> { @@ -97,6 +99,26 @@ impl Rewriter<'_, L> { false } + /// Whether `stmt` is `owner` or encloses it, at any depth. `owner` is the + /// statement whose body holds a place, so `true` means placing `stmt` + /// there would make it its own ancestor. + /// + /// Only a statement owning a body can enclose anything, so for the many + /// that own none this skips the walk up from `owner`. + pub(super) fn encloses(&self, stmt: Statement, owner: Option) -> bool { + if !tree::owns_bodies(self.stage, stmt) { + return false; + } + let mut current = owner; + while let Some(outer) = current { + if outer == stmt { + return true; + } + current = tree::parent(self.stage, outer); + } + false + } + /// Make `owner` the parent of `body`. pub(super) fn adopt_body(&mut self, body: Body, owner: Statement) { match body { diff --git a/crates/kirin-ir/src/rewrite/cfg/split.rs b/crates/kirin-ir/src/rewrite/cfg/split.rs index 15589f777b..00abddb484 100644 --- a/crates/kirin-ir/src/rewrite/cfg/split.rs +++ b/crates/kirin-ir/src/rewrite/cfg/split.rs @@ -10,9 +10,9 @@ impl<'a, L: Dialect> Rewriter<'a, L> { /// /// The original block is left **without a terminator**; the caller closes /// it, typically with a branch to the new block, through - /// [`Rewriter::insert_terminator`]. The new block has no arguments. - /// Splitting at the terminator moves only the terminator. Statement ids, - /// results, and uses are unchanged. + /// [`InsertionPoint::Terminator`](crate::InsertionPoint::Terminator). The + /// new block has no arguments. Splitting at the terminator moves only the + /// terminator. Statement ids, results, and uses are unchanged. /// /// Rejects, leaving the stage untouched, if `at` is unknown or not in a /// CFG block. diff --git a/crates/kirin-ir/src/rewrite/digraph/mod.rs b/crates/kirin-ir/src/rewrite/digraph/mod.rs index 0a18864ba6..e21bb83d4c 100644 --- a/crates/kirin-ir/src/rewrite/digraph/mod.rs +++ b/crates/kirin-ir/src/rewrite/digraph/mod.rs @@ -3,8 +3,8 @@ //! A `DiGraph`'s petgraph mirrors its members' operands: one edge //! `producer → consumer` per operand slot whose value is a result of another //! member. The rewriter keeps it (and the member map beside it) -//! correct incrementally; there is no public edge API. Members are added with -//! [`Rewriter::insert_node`](super::Rewriter::insert_node) and erased through +//! correct incrementally; there is no public edge API. Members are added at +//! [`InsertionPoint::DiGraph`](crate::InsertionPoint::DiGraph) and erased through //! [`Rewriter::erase_statement`](super::Rewriter::erase_statement); yields are //! edited with [`Rewriter::replace_yield`](super::Rewriter::replace_yield). diff --git a/crates/kirin-ir/src/rewrite/digraph/surgery.rs b/crates/kirin-ir/src/rewrite/digraph/surgery.rs index 16ed9864cf..07ea97588b 100644 --- a/crates/kirin-ir/src/rewrite/digraph/surgery.rs +++ b/crates/kirin-ir/src/rewrite/digraph/surgery.rs @@ -1,59 +1,10 @@ use crate::arena::{GetInfo, GetInfoMut}; -use crate::{DiGraph, DiGraphInfo, Dialect, SSAValue, Statement, StatementParent, Use}; +use crate::{DiGraph, DiGraphInfo, Dialect, SSAValue, Statement, Use}; use crate::rewrite::rewriter::{MutationEvent, RewriteError, Rewriter, add_use, remove_use}; -/// Adding, erasing, and tombstoning `DiGraph` members, and editing yields. +/// Erasing and tombstoning `DiGraph` members, and editing yields. impl<'a, L: Dialect> Rewriter<'a, L> { - /// Add `definition` to `graph` as a new member statement and node. - /// - /// `definition` must be a non-terminator (a `DiGraph` has none) with no - /// successors (only a terminator may transfer control). Every - /// operand must be live, and every result a fresh - /// [`Rewriter::new_result`] value, which becomes a result of the new - /// statement. Each operand slot reading a result of another member gets an - /// edge from that member; operands from outside the graph get none. - /// - /// Rejects, leaving the stage untouched, if `graph` is unknown, or - /// `definition` is a terminator or fails the operand/result checks. - /// Records [`MutationEvent::InsertedStatement`] then - /// [`MutationEvent::ChangedDiGraphTopology`]. - pub fn insert_node( - &mut self, - graph: DiGraph, - definition: L, - ) -> Result { - self.live_digraph(graph)?; - if definition.is_terminator() { - return Err(RewriteError::UnexpectedTerminator); - } - if definition.successors().next().is_some() { - return Err(RewriteError::NonTerminatorWithSuccessors); - } - let (results, operands) = self.prepare_insert(&definition)?; - - let stmt = self.alloc_statement( - definition, - StatementParent::DiGraph(graph), - None, - None, - results, - &operands, - ); - graph - .get_info_mut(self.stage) - .expect("graph is live in this stage") - .add_node(stmt); - for &operand in &operands { - self.add_operand_edge(graph, stmt, operand); - } - - self.events.push(MutationEvent::InsertedStatement { stmt }); - self.events - .push(MutationEvent::ChangedDiGraphTopology { graph }); - Ok(stmt) - } - /// The `DiGraph` arm of [`Rewriter::erase_statement`]: `stmt` is a live /// member of `graph`. pub(in crate::rewrite) fn erase_digraph_member( diff --git a/crates/kirin-ir/src/rewrite/insert.rs b/crates/kirin-ir/src/rewrite/insert.rs new file mode 100644 index 0000000000..478cf04e7f --- /dev/null +++ b/crates/kirin-ir/src/rewrite/insert.rs @@ -0,0 +1,385 @@ +//! Creating and inserting statements at an [`InsertionPoint`]. +//! +//! [`Rewriter::create`] builds a new statement and places it in one step. +//! [`Rewriter::insert`] places a detached statement, such as a copy. Both +//! check the insertion point against the definition before changing anything, +//! so a rejection leaves the stage untouched. + +use std::collections::HashSet; + +use crate::arena::{GetInfo, GetInfoMut}; +use crate::node::linked_list::{LinkedList, LinkedListNode}; +use crate::{ + Block, BlockParent, DiGraph, Dialect, SSAValue, Statement, StatementInfo, StatementParent, + UnGraph, Use, +}; + +use crate::rewrite::rewriter::{ + MutationEvent, RewriteError, Rewriter, Side, add_predecessor, add_use, +}; +use crate::rewrite::walk::tree; + +/// Where [`Rewriter::create`] or [`Rewriter::insert`] places a statement. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InsertionPoint { + /// Immediately before the anchor, a statement in a block body. Before the + /// block's terminator means at the end of its statement chain. The + /// statement must be a non-terminator with no successors. + Before(Statement), + /// Immediately after the anchor, a non-terminator in a block body + /// ([`RewriteError::AnchorIsTerminator`] otherwise: nothing follows a + /// terminator). The statement must be a non-terminator with no + /// successors. + After(Statement), + /// The terminator slot of a block that has none + /// ([`RewriteError::BlockHasTerminator`] otherwise), in a CFG or a + /// statement-owned body. The statement must be a terminator + /// ([`RewriteError::ExpectedTerminator`]) with no results + /// ([`RewriteError::CannotInsertTerminatorWithResults`]) and live + /// successors. The predecessor mirror gains an entry per successor. + Terminator(Block), + /// A new member and node of the `DiGraph`. The statement must be a + /// non-terminator with no successors. Each operand slot reading a result + /// of another member gets an edge from that member; operands from outside + /// the graph get none. A detached statement's results may already be read + /// by members; each such slot gets an edge from the new node. + DiGraph(DiGraph), + /// A new member of the `UnGraph`: an edge statement if the definition is + /// one ([`IsEdge`](crate::IsEdge)), a node otherwise. Neither may be a + /// terminator or carry successors, and an edge statement has no operands + /// ([`RewriteError::EdgeStatementWithOperands`]): its results are new + /// wires. Each operand of a node that is a wire of the graph becomes an + /// endpoint of that wire, and a wire read by two node slots is an edge, + /// including the wires of a detached edge statement that nodes already + /// read. A wire may end up with more than two endpoints; that is invalid + /// IR, left for [`verify_derived`](crate::verify_derived) to report, and + /// such a wire has no edge. + UnGraph(UnGraph), +} + +/// An [`InsertionPoint`] checked against a definition, ready to place. +enum Placement { + Chain { + block: Block, + prev: Option, + next: Option, + }, + Terminator { + block: Block, + targets: HashSet, + }, + DiGraph(DiGraph), + UnGraphNode(UnGraph), + UnGraphEdge(UnGraph), +} + +impl<'a, L: Dialect> Rewriter<'a, L> { + /// Create a statement from `definition` at `insertion_pt`, returning its id. + /// + /// Every operand must be live ([`RewriteError::UnknownValue`]) and every + /// result a fresh [`Rewriter::new_result`] value + /// ([`RewriteError::ResultNotFresh`]), which becomes a result of the new + /// statement. The statement must also fit `insertion_pt` (see + /// [`InsertionPoint`]); if it does not, the error is + /// [`RewriteError::CannotInsert`], naming `insertion_pt` and the reason. + /// + /// Rejects, leaving the stage untouched, if a check fails. Records + /// [`MutationEvent::InsertedStatement`], then for a terminator + /// [`MutationEvent::ChangedTerminator`] and for a graph member + /// [`MutationEvent::ChangedDiGraphTopology`] or + /// [`MutationEvent::ChangedUnGraphTopology`]. + pub fn create( + &mut self, + insertion_pt: InsertionPoint, + definition: L, + ) -> Result { + let placement = self.check_insertion_pt(insertion_pt, &definition)?; + let stmt = self.build_statement(definition)?; + self.place(stmt, placement); + Ok(stmt) + } + + /// Insert the detached `stmt` at `insertion_pt`. + /// + /// A detached statement is one this `Rewriter` made but has not placed, + /// such as a copy; it has no parent yet. It must fit `insertion_pt` as for + /// [`Rewriter::create`]. + /// + /// A detached statement's results can be read before it is placed, but + /// one never placed is tombstoned when the `Rewriter` is dropped, leaving + /// the statements reading its results with dangling operands. + /// [`verify_derived`](crate::verify_derived) reports them, so + /// [`Pipeline::run_pass`](crate::Pipeline::run_pass) quarantines the + /// stage. + /// + /// Rejects, leaving the stage untouched, if `stmt` is unknown, is not a + /// detached statement of this `Rewriter` ([`RewriteError::NotDetached`]), + /// or does not fit `insertion_pt`. `insertion_pt` must not be nested in + /// `stmt`'s own bodies ([`RewriteError::InsertedIntoItself`], wrapped in + /// [`RewriteError::CannotInsert`]). Records the events of + /// [`Rewriter::create`]. + pub fn insert( + &mut self, + insertion_pt: InsertionPoint, + stmt: Statement, + ) -> Result<(), RewriteError> { + let item = self.live_statement(stmt)?; + if !self.detached.contains(&stmt) { + return Err(RewriteError::NotDetached(stmt)); + } + let placement = self.check_insertion_pt(insertion_pt, &item.definition)?; + if self.encloses(stmt, self.insertion_pt_owner(insertion_pt)) { + return Err(RewriteError::CannotInsert { + insertion_pt, + reason: Box::new(RewriteError::InsertedIntoItself(stmt)), + }); + } + self.place(stmt, placement); + Ok(()) + } + + /// The statement whose body holds `insertion_pt`, or `None` at the top + /// level. `insertion_pt` has passed [`Rewriter::check_insertion_pt`]. + fn insertion_pt_owner(&self, insertion_pt: InsertionPoint) -> Option { + match insertion_pt { + InsertionPoint::Before(anchor) | InsertionPoint::After(anchor) => { + tree::parent(self.stage, anchor) + } + InsertionPoint::Terminator(block) => match block.get_info(self.stage)?.parent()? { + BlockParent::CFG(cfg) => cfg.get_info(self.stage)?.parent, + BlockParent::Statement(owner) => Some(owner), + }, + InsertionPoint::DiGraph(graph) => graph.get_info(self.stage)?.parent(), + InsertionPoint::UnGraph(graph) => graph.get_info(self.stage)?.parent(), + } + } + + /// Check that a statement with `definition` fits `insertion_pt`. A + /// rejection is wrapped in [`RewriteError::CannotInsert`], naming + /// `insertion_pt`. + fn check_insertion_pt( + &self, + insertion_pt: InsertionPoint, + definition: &L, + ) -> Result { + self.placement_at(insertion_pt, definition) + .map_err(|reason| RewriteError::CannotInsert { + insertion_pt, + reason: Box::new(reason), + }) + } + + /// Where a statement with `definition` goes at `insertion_pt`, or why it + /// does not fit there. + fn placement_at( + &self, + insertion_pt: InsertionPoint, + definition: &L, + ) -> Result { + // Block chains and graphs take only non-terminators, which never carry + // successors. + let check_non_terminator = || { + if definition.is_terminator() { + Err(RewriteError::UnexpectedTerminator) + } else if definition.successors().next().is_some() { + Err(RewriteError::NonTerminatorWithSuccessors) + } else { + Ok(()) + } + }; + match insertion_pt { + InsertionPoint::Before(anchor) | InsertionPoint::After(anchor) => { + let side = match insertion_pt { + InsertionPoint::After(_) => Side::After, + _ => Side::Before, + }; + let (block, prev, next) = self.slot_at(anchor, side)?; + check_non_terminator()?; + Ok(Placement::Chain { block, prev, next }) + } + InsertionPoint::Terminator(block) => { + if self.live_block(block)?.terminator.is_some() { + return Err(RewriteError::BlockHasTerminator(block)); + } + if !definition.is_terminator() { + return Err(RewriteError::ExpectedTerminator); + } + if definition.results().next().is_some() { + return Err(RewriteError::CannotInsertTerminatorWithResults); + } + let targets: HashSet = definition.successors().map(|s| s.target()).collect(); + for &target in &targets { + self.live_block(target)?; + } + Ok(Placement::Terminator { block, targets }) + } + InsertionPoint::DiGraph(graph) => { + self.live_digraph(graph)?; + check_non_terminator()?; + Ok(Placement::DiGraph(graph)) + } + InsertionPoint::UnGraph(graph) => { + self.live_ungraph(graph)?; + check_non_terminator()?; + if !definition.is_edge() { + return Ok(Placement::UnGraphNode(graph)); + } + if definition.arguments().next().is_some() { + return Err(RewriteError::EdgeStatementWithOperands); + } + Ok(Placement::UnGraphEdge(graph)) + } + } + } + + /// Place the detached `stmt` as `placement` says, and record the events. + fn place(&mut self, stmt: Statement, placement: Placement) { + self.detached.remove(&stmt); + let parent = match placement { + Placement::Chain { block, .. } | Placement::Terminator { block, .. } => { + StatementParent::Block(block) + } + Placement::DiGraph(graph) => StatementParent::DiGraph(graph), + Placement::UnGraphNode(graph) | Placement::UnGraphEdge(graph) => { + StatementParent::UnGraph(graph) + } + }; + if let Some(info) = stmt.get_info_mut(self.stage) { + info.parent = Some(parent); + } + self.events.push(MutationEvent::InsertedStatement { stmt }); + + match placement { + Placement::Chain { block, prev, next } => { + self.link(LinkedList::single(stmt), block, prev, next); + } + Placement::Terminator { block, targets } => { + // Only terminators carry successors and the block had none, so + // every target gains exactly one entry for `block`. + for target in targets { + add_predecessor(self.stage, target, block); + } + if let Some(info) = block.get_info_mut(self.stage) { + info.terminator = Some(stmt); + } + self.events.push(MutationEvent::ChangedTerminator { block }); + } + Placement::DiGraph(graph) => { + graph + .get_info_mut(self.stage) + .expect("graph is live in this stage") + .add_node(stmt); + for operand in self.operands_of(stmt) { + self.add_operand_edge(graph, stmt, operand); + } + // A detached statement's results are usable before it is + // placed, so members may already read them: add an edge to + // each of their slots. `stmt`'s own slots got theirs above. + for result in self.results_of(stmt) { + let consumers: Vec = result + .expect_info(self.stage) + .uses() + .iter() + .filter_map(|site| match *site { + Use::StatementOperand { stmt: consumer, .. } if consumer != stmt => { + Some(consumer) + } + _ => None, + }) + .collect(); + let info = graph + .get_info_mut(self.stage) + .expect("graph is live in this stage"); + for consumer in consumers { + if info.node_idx_of(consumer).is_some() { + info.add_edge(stmt, consumer, result); + } + } + } + self.events + .push(MutationEvent::ChangedDiGraphTopology { graph }); + } + Placement::UnGraphNode(graph) => { + // `stmt` already uses its operands, but endpoints only count + // graph nodes, so the snapshot does not see it until it is + // added. + let wires = self.snapshot_wires(graph, self.operands_of(stmt)); + graph + .get_info_mut(self.stage) + .expect("graph is live in this stage") + .add_node(stmt); + self.resync_wires(graph, wires); + self.events + .push(MutationEvent::ChangedUnGraphTopology { graph }); + } + Placement::UnGraphEdge(graph) => { + graph + .get_info_mut(self.stage) + .expect("graph is live in this stage") + .add_edge_member(stmt); + // A detached statement's results are usable before it is + // placed, so nodes may already read its wires: resync each + // from no endpoints, making a wire with two an edge. + let wires = self + .results_of(stmt) + .into_iter() + .map(|wire| (wire, Default::default())) + .collect(); + self.resync_wires(graph, wires); + self.events + .push(MutationEvent::ChangedUnGraphTopology { graph }); + } + } + } + + /// `stmt`'s operands, in order. + fn operands_of(&self, stmt: Statement) -> Vec { + stmt.expect_info(self.stage) + .definition + .arguments() + .copied() + .collect() + } + + /// `stmt`'s results, in order. + fn results_of(&self, stmt: Statement) -> Vec { + stmt.expect_info(self.stage) + .definition + .results() + .map(|&result| result.into()) + .collect() + } + + /// Build `definition` as a detached statement: it has no parent and no + /// place yet. Its results are bound and its operand uses recorded. A + /// statement built but never inserted is tombstoned when the `Rewriter` is + /// dropped. + /// + /// Rejects, allocating nothing, if an operand is not live + /// ([`RewriteError::UnknownValue`]) or a result is not a fresh + /// [`Rewriter::new_result`] value ([`RewriteError::ResultNotFresh`]). + pub(super) fn build_statement(&mut self, definition: L) -> Result { + let results = self.fresh_results(&definition)?; + let operands: Vec = definition.arguments().copied().collect(); + for &operand in &operands { + if operand.get_info(self.stage).is_none() { + return Err(RewriteError::UnknownValue(operand)); + } + } + + let stmt = self + .stage + .statement_arena_mut() + .alloc_with_id(|id| StatementInfo { + node: LinkedListNode::new(id), + parent: None, + definition, + }); + self.bind_results(stmt, results); + for (index, &operand) in operands.iter().enumerate() { + add_use(self.stage, operand, Use::StatementOperand { stmt, index }); + } + self.detached.insert(stmt); + Ok(stmt) + } +} diff --git a/crates/kirin-ir/src/rewrite/insert_tests.rs b/crates/kirin-ir/src/rewrite/insert_tests.rs new file mode 100644 index 0000000000..51e75d0276 --- /dev/null +++ b/crates/kirin-ir/src/rewrite/insert_tests.rs @@ -0,0 +1,45 @@ +//! [`Rewriter::insert`] rejects a place nested in the statement's own bodies. +//! +//! In-crate because it needs a detached statement that owns a body, which no +//! public API makes yet. + +use crate::arena::GetInfo; +use crate::testing::dialect::{TestLang, new_stage}; +use crate::{Body, InsertionPoint, RewriteError, Rewriter, StatementParent}; + +#[test] +fn insert_rejects_a_place_nested_in_its_own_body() { + // `top` is outside everything. `body` is an unowned block holding `mid`, + // which owns `inner`, which holds `deep`. + let mut stage = new_stage(); + let top = stage.statement().definition(TestLang::Nop).new(); + let top_block = stage.block().stmt(top).new(); + let deep = stage.statement().definition(TestLang::Nop).new(); + let inner = stage.block().stmt(deep).new(); + let mid = stage.statement().definition(TestLang::Region(inner)).new(); + let body = stage.block().stmt(mid).new(); + let mut stage = stage.finalize().unwrap(); + + let mut rw = Rewriter::new(&mut stage); + // A detached statement owning `body`, as a copy of a region-holding + // statement would be. + let copy = rw.build_statement(TestLang::Region(body)).unwrap(); + rw.adopt_body(Body::Block(body), copy); + + let into_itself = |insertion_pt| RewriteError::CannotInsert { + insertion_pt, + reason: Box::new(RewriteError::InsertedIntoItself(copy)), + }; + // One level down, and two. + let after_mid = InsertionPoint::After(mid); + assert_eq!(rw.insert(after_mid, copy), Err(into_itself(after_mid))); + let before_deep = InsertionPoint::Before(deep); + assert_eq!(rw.insert(before_deep, copy), Err(into_itself(before_deep))); + + rw.insert(InsertionPoint::Before(top), copy).unwrap(); + drop(rw); + assert_eq!( + copy.expect_info(&stage).parent, + Some(StatementParent::Block(top_block)) + ); +} diff --git a/crates/kirin-ir/src/rewrite/mod.rs b/crates/kirin-ir/src/rewrite/mod.rs index ea8ab6c2e7..12a1eea7cc 100644 --- a/crates/kirin-ir/src/rewrite/mod.rs +++ b/crates/kirin-ir/src/rewrite/mod.rs @@ -19,10 +19,12 @@ //! See the [`rewriter`] module docs for what a single edit does and does not //! guarantee, and [`pass`] for the boundary's failure routes. -mod alloc; mod bodies; pub(crate) mod cfg; pub(crate) mod digraph; +mod insert; +#[cfg(test)] +mod insert_tests; mod list; mod moves; pub(crate) mod pass; @@ -34,6 +36,7 @@ pub(crate) mod rewriter; pub(crate) mod ungraph; pub mod walk; +pub use insert::InsertionPoint; pub use quarantine::{QuarantineCause, Quarantined}; pub use rewriter::{MutationEvent, RewriteError, Rewriter}; diff --git a/crates/kirin-ir/src/rewrite/moves.rs b/crates/kirin-ir/src/rewrite/moves.rs index 951158b785..63775b859b 100644 --- a/crates/kirin-ir/src/rewrite/moves.rs +++ b/crates/kirin-ir/src/rewrite/moves.rs @@ -9,14 +9,15 @@ impl<'a, L: Dialect> Rewriter<'a, L> { /// body. /// /// The statement keeps its id, results, uses and nested bodies; only its - /// place changes. As with [`Rewriter::insert_before`], `anchor` may be a - /// block's terminator: `stmt` then goes to the end of that block's + /// place changes. As with + /// [`InsertionPoint::Before`](crate::InsertionPoint::Before), `anchor` may + /// be a block's terminator: `stmt` then goes to the end of that block's /// statement chain. Moving a statement to where it already is (before /// itself, or before the statement that already follows it) does nothing /// and records no event. /// - /// Not checked, as with [`Rewriter::insert_before`]: that every use of - /// `stmt`'s results still comes after it. + /// Not checked, as with [`Rewriter::create`]: that every use of `stmt`'s + /// results still comes after it. /// /// Rejects, leaving the stage untouched, if `stmt` or `anchor` is unknown /// ([`RewriteError::UnknownStatement`]) or not in a block body @@ -63,14 +64,10 @@ impl<'a, L: Dialect> Rewriter<'a, L> { if prev == Some(stmt) || next == Some(stmt) { return Ok(()); } - // Walk up the statements whose bodies enclose `anchor`. If `stmt` is - // one of them, the move would put `stmt` inside its own body. - let mut owner = tree::parent(self.stage, anchor); - while let Some(current) = owner { - if current == stmt { - return Err(RewriteError::MovedIntoItself { stmt, anchor }); - } - owner = tree::parent(self.stage, current); + // If `stmt` encloses `anchor`, the move would put `stmt` inside its + // own body. + if self.encloses(stmt, tree::parent(self.stage, anchor)) { + return Err(RewriteError::MovedIntoItself { stmt, anchor }); } self.unlink(stmt, block); diff --git a/crates/kirin-ir/src/rewrite/results.rs b/crates/kirin-ir/src/rewrite/results.rs index 63ffb823f1..254c0f21bb 100644 --- a/crates/kirin-ir/src/rewrite/results.rs +++ b/crates/kirin-ir/src/rewrite/results.rs @@ -1,25 +1,26 @@ -//! Fresh result values for inserted statements. +//! Fresh result values for built statements. //! //! A definition names its results by id, so the ids must exist before the -//! definition can be built, while the statement that defines them does not -//! exist until it is inserted. [`Rewriter::new_result`] bridges the gap: it -//! allocates an id whose arena slot stays empty (so the value reads as not -//! live and cannot be used yet) and keeps its type until an insert binds it. -//! Dropping the `Rewriter` tombstones every value it never bound, so once it -//! is gone each result slot is either bound or a real tombstone. +//! definition can be written, while the statement that defines them does not +//! exist until it is built. [`Rewriter::new_result`] bridges the gap: it +//! allocates an id whose arena slot stays empty (so the value reads as not live +//! and cannot be used yet) and keeps its type until [`Rewriter::create`] binds +//! it. Dropping the `Rewriter` tombstones every value it never bound, so once +//! it is gone each result slot is either bound or a real tombstone. It also +//! tombstones every detached statement it never inserted. use crate::{Dialect, ResultValue, SSAInfo, SSAKind, SSAValue, Statement}; use crate::rewrite::rewriter::{RewriteError, Rewriter}; impl<'a, L: Dialect> Rewriter<'a, L> { - /// Allocate a result value of type `ty` for a statement not inserted yet. + /// Allocate a result value of type `ty` for a statement not built yet. /// - /// Put it in a result slot of the definition passed to an insert; the - /// insert makes it a result of the new statement. Until then the value is - /// not live: using it as an operand is rejected with - /// [`RewriteError::UnknownValue`]. A value this `Rewriter` never binds is - /// tombstoned when the `Rewriter` is dropped. + /// Put it in a result slot of the definition passed to + /// [`Rewriter::create`], which makes it a result of the new statement. + /// Until then the value is not live: using it as an operand is rejected + /// with [`RewriteError::UnknownValue`]. A value this `Rewriter` never binds + /// is tombstoned when the `Rewriter` is dropped. pub fn new_result(&mut self, ty: L::Type) -> ResultValue { let value = self.stage.ssas.alloc(None); self.unbound_ssa_results.insert(value, ty); @@ -60,9 +61,14 @@ impl<'a, L: Dialect> Rewriter<'a, L> { } impl<'a, L: Dialect> Drop for Rewriter<'a, L> { - /// Tombstone every [`new_result`](Rewriter::new_result) value no insert - /// bound, so no empty, live-looking slot outlives the `Rewriter`. + /// Tombstone every detached statement no [`insert`](Rewriter::insert) + /// placed, with its results and operand uses, and every + /// [`new_result`](Rewriter::new_result) value no statement bound, so + /// nothing detached or empty outlives the `Rewriter`. fn drop(&mut self) { + for stmt in std::mem::take(&mut self.detached) { + self.tombstone_statement(stmt, None); + } for (value, _) in self.unbound_ssa_results.drain() { let _ = self.stage.ssas.delete(value); } diff --git a/crates/kirin-ir/src/rewrite/rewriter.rs b/crates/kirin-ir/src/rewrite/rewriter.rs index b59dbbc204..2decca3fd6 100644 --- a/crates/kirin-ir/src/rewrite/rewriter.rs +++ b/crates/kirin-ir/src/rewrite/rewriter.rs @@ -25,39 +25,38 @@ //! Scope is operand/yield rewriting (including bulk result redirection through //! [`Rewriter::replace_results`], and selective redirection through //! [`Rewriter::replace_uses_if`] and -//! [`Rewriter::replace_all_uses_except_stmt`]), block-body statement surgery -//! ([`Rewriter::erase_statement`], [`Rewriter::insert_before`], -//! [`Rewriter::insert_after`], [`Rewriter::replace_statement`], -//! [`Rewriter::insert_terminator`], [`Rewriter::move_before`], -//! [`Rewriter::move_after`]), `CFG` -//! block-list surgery in the sibling `cfg` module (append/insert/erase/split/ -//! merge blocks, [`Rewriter::retarget_successor`], and block-argument insertion -//! and erasure), `DiGraph` node surgery in the sibling `digraph` module -//! ([`Rewriter::insert_node`], [`Rewriter::replace_yield`]), and `UnGraph` -//! surgery in the sibling `ungraph` module ([`Rewriter::insert_ungraph_node`], -//! [`Rewriter::insert_ungraph_edge`]); graph members are erased through -//! [`Rewriter::erase_statement`]. The rewriter **maintains** all four derived mirrors -//! incrementally on every edit — [`SSAInfo::uses`](crate::SSAInfo), +//! [`Rewriter::replace_all_uses_except_stmt`]), creating statements at an +//! [`InsertionPoint`](crate::InsertionPoint) in a block body or a graph +//! ([`Rewriter::create`], [`Rewriter::insert`]), block-body statement surgery +//! ([`Rewriter::erase_statement`], [`Rewriter::replace_statement`], +//! [`Rewriter::move_before`], [`Rewriter::move_after`]), `CFG` block-list +//! surgery in the sibling `cfg` module (append/insert/erase/split/merge +//! blocks, [`Rewriter::retarget_successor`], and block-argument insertion and +//! erasure), `DiGraph` yield editing in the sibling `digraph` module +//! ([`Rewriter::replace_yield`]), and `UnGraph` topology in the sibling +//! `ungraph` module; graph members are erased through +//! [`Rewriter::erase_statement`]. The rewriter **maintains** all four derived +//! mirrors incrementally on every edit — [`SSAInfo::uses`](crate::SSAInfo), //! [`BlockInfo::predecessors`](crate::BlockInfo), the block-body summary //! (`BlockInfo::statements`/`terminator`), and the CFG chain summary //! (`CFGInfo::blocks`) — so they stay valid without a rebuild — and keeps each -//! graph's petgraph topology in step with member insertion and erasure and -//! with operand edits on its members. Deferred to later slices: full -//! cross-block dominance/visibility preflight. Inserted -//! statements define results through values pre-allocated with -//! [`Rewriter::new_result`]. +//! graph's petgraph topology in step with member insertion and erasure and with +//! operand edits on its members. Deferred to later slices: full cross-block +//! dominance/visibility preflight. Inserted statements define results through +//! values pre-allocated with [`Rewriter::new_result`]. //! //! # Graph bodies //! -//! Operand edits keep each graph's petgraph in step: on a `DiGraph` member -//! they move the slot's edge, and on an `UnGraph` node they resync the wires -//! the slot leaves and joins. An `UnGraph` wire may be read by more than two -//! node slots mid-pass; such a wire has no edge, and +//! Operand edits keep each graph's petgraph in step: on a `DiGraph` member they +//! move the slot's edge, and on an `UnGraph` node they resync the wires the +//! slot leaves and joins. An `UnGraph` wire may be read by more than two node +//! slots mid-pass; such a wire has no edge, and //! [`verify_derived`](crate::verify_derived) reports it as -//! [`Finding::UnGraphDegreeExceeded`](crate::Finding). The block `insert_*` -//! methods reject graph members with [`RewriteError::NotInBlockBody`]; graph -//! members are added with [`Rewriter::insert_node`], -//! [`Rewriter::insert_ungraph_node`], and [`Rewriter::insert_ungraph_edge`]. +//! [`Finding::UnGraphDegreeExceeded`](crate::Finding). A graph member cannot be +//! an anchor for a block-body insertion point +//! ([`RewriteError::NotInBlockBody`]); graph members are added with +//! [`InsertionPoint::DiGraph`](crate::InsertionPoint::DiGraph) and +//! [`InsertionPoint::UnGraph`](crate::InsertionPoint::UnGraph). //! //! Because value rewrites are index-driven (they consult and update //! `SSAInfo::uses` rather than scanning), correctness depends on the use index @@ -81,10 +80,10 @@ use std::collections::HashSet; use std::fmt; use indexmap::{IndexMap, IndexSet}; -use rustc_hash::{FxBuildHasher, FxHashMap}; +use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; +use crate::InsertionPoint; use crate::arena::{GetInfo, GetInfoMut}; -use crate::node::linked_list::LinkedList; use smallvec::SmallVec; use super::bodies::bodies_of; @@ -154,8 +153,15 @@ pub enum MutationEvent { } /// An illegal mutation rejected by a [`Rewriter`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum RewriteError { + /// [`Rewriter::create`] or [`Rewriter::insert`] could not place a + /// statement at `insertion_pt`, for `reason`: the anchor, block or graph + /// it names is not a valid place, or the statement does not fit there. + CannotInsert { + insertion_pt: InsertionPoint, + reason: Box, + }, /// The statement ID does not resolve to a live statement in this stage. UnknownStatement(Statement), /// The SSA value ID does not resolve to a live value in this stage. @@ -171,28 +177,37 @@ pub enum RewriteError { /// The statement defines results that are still used elsewhere, so erasing it /// would leave dangling uses. Replace those uses first. StatementResultsInUse(Statement), - /// Nothing can follow a block's terminator, so `insert_after` cannot anchor - /// on one. (`insert_before` can: it appends to the end of the chain.) + /// Nothing can follow a block's terminator, so + /// [`InsertionPoint::After`](crate::InsertionPoint::After) and `move_after` + /// cannot anchor on one. (`Before` can: it means the end of the chain.) AnchorIsTerminator(Statement), - /// The inserted definition, or the moved statement, is a terminator where - /// a non-terminator is required: `insert_before`/`insert_after` and - /// `move_before`/`move_after` do not splice terminators into a block's - /// statement list (use [`Rewriter::insert_terminator`] on a block that has - /// none), and a `DiGraph` has no terminators for `insert_node` to add. The - /// counterpart of [`RewriteError::ExpectedTerminator`]. + /// The inserted or moved statement is a terminator where a non-terminator + /// is required: [`InsertionPoint::Before`](crate::InsertionPoint::Before) / + /// [`After`](crate::InsertionPoint::After) and `move_before`/`move_after` + /// do not splice terminators into a block's statement list (use + /// [`InsertionPoint::Terminator`](crate::InsertionPoint::Terminator) on a + /// block that has none), and graphs have no terminators. The counterpart of + /// [`RewriteError::ExpectedTerminator`]. UnexpectedTerminator, + /// [`Rewriter::insert`] was given a statement that is not a detached + /// statement of this `Rewriter`: one already in the IR, or already + /// inserted. + NotDetached(Statement), /// The inserted or replacement definition is a non-terminator that carries /// successors. Only a block's terminator may transfer control, so such a /// statement would add edges the predecessor mirror does not count. NonTerminatorWithSuccessors, - /// `insert_terminator` was given a block that already has a terminator. + /// [`InsertionPoint::Terminator`](crate::InsertionPoint::Terminator) names + /// a block that already has a terminator. BlockHasTerminator(Block), - /// The definition passed to `insert_terminator` declares results; a - /// terminator ends its block, so nothing could use them. + /// A statement for + /// [`InsertionPoint::Terminator`](crate::InsertionPoint::Terminator) + /// declares results; a terminator ends its block, so nothing could use + /// them. CannotInsertTerminatorWithResults, - /// A result slot of the inserted definition holds a value that is not a - /// fresh [`Rewriter::new_result`] of this `Rewriter`: it is an existing or - /// already bound value, or it fills two slots. + /// A result slot of the definition passed to [`Rewriter::create`] holds a + /// value that is not a fresh [`Rewriter::new_result`] of this `Rewriter`: + /// it is an existing or already bound value, or it fills two slots. ResultNotFresh(SSAValue), /// A result list did not line up one-for-one with the statement's results: /// a `replace_statement` definition changed the result count (which would @@ -214,12 +229,6 @@ pub enum RewriteError { YieldIndexOutOfRange { graph: DiGraph, index: usize }, /// The `UnGraph` ID does not resolve to a live `UnGraph` in this stage. UnknownUnGraph(UnGraph), - /// `insert_ungraph_node` was given an edge statement; add those with - /// [`Rewriter::insert_ungraph_edge`]. - UnexpectedEdgeStatement, - /// `insert_ungraph_edge` was given a statement that is not an edge - /// statement; add nodes with [`Rewriter::insert_ungraph_node`]. - ExpectedEdgeStatement, /// An inserted edge statement has operands. A wire only connects the nodes /// that read it; it consumes no values. EdgeStatementWithOperands, @@ -243,6 +252,10 @@ pub enum RewriteError { /// statement's own bodies, which would make the statement its own /// ancestor. MovedIntoItself { stmt: Statement, anchor: Statement }, + /// [`Rewriter::insert`] was given an insertion point nested in the + /// statement's own bodies, which would make the statement its own + /// ancestor. + InsertedIntoItself(Statement), /// `splice_block_before`/`splice_block_after` was given an anchor in /// `source` or nested in one of its statements, which would place a /// spliced statement next to or inside itself. @@ -258,7 +271,9 @@ pub enum RewriteError { CfgMismatch { block: Block, expected: CFG }, /// `merge_blocks` was asked to merge a block into itself. CannotMergeBlockIntoItself(Block), - /// The definition passed to `insert_terminator` is not a terminator. + /// A statement for + /// [`InsertionPoint::Terminator`](crate::InsertionPoint::Terminator) is not + /// a terminator. ExpectedTerminator, /// A list of values did not line up one-for-one with a block's arguments. BlockArgumentArityMismatch { @@ -299,6 +314,12 @@ impl fmt::Display for RewriteError { "statement {stmt:?} is not a live statement in this stage" ) } + RewriteError::CannotInsert { + insertion_pt, + reason, + } => { + write!(f, "cannot insert at {insertion_pt:?}: {reason}") + } RewriteError::UnknownValue(value) => { write!(f, "SSA value {value} is not a live value in this stage") } @@ -323,10 +344,16 @@ impl fmt::Display for RewriteError { RewriteError::AnchorIsTerminator(stmt) => { write!(f, "cannot insert after terminator {stmt:?}") } + RewriteError::NotDetached(stmt) => { + write!( + f, + "{stmt:?} is not a detached statement built by this rewriter" + ) + } RewriteError::UnexpectedTerminator => { write!( f, - "expected a non-terminator: terminators are only inserted with insert_terminator, and are not moved" + "expected a non-terminator: terminators are only inserted as a block's terminator, and are not moved" ) } RewriteError::BlockHasTerminator(block) => { @@ -366,18 +393,6 @@ impl fmt::Display for RewriteError { RewriteError::UnknownUnGraph(graph) => { write!(f, "UnGraph {graph} is not a live UnGraph in this stage") } - RewriteError::UnexpectedEdgeStatement => { - write!( - f, - "expected a node: edge statements are inserted with insert_ungraph_edge" - ) - } - RewriteError::ExpectedEdgeStatement => { - write!( - f, - "expected an edge statement: nodes are inserted with insert_ungraph_node" - ) - } RewriteError::EdgeStatementWithOperands => { write!(f, "an edge statement cannot have operands") } @@ -408,6 +423,9 @@ impl fmt::Display for RewriteError { "cannot move {stmt:?} next to {anchor:?}, which is nested in it" ) } + RewriteError::InsertedIntoItself(stmt) => { + write!(f, "{stmt:?} would be inserted into its own body") + } RewriteError::SplicedIntoItself { source, anchor } => { write!( f, @@ -505,6 +523,9 @@ pub struct Rewriter<'a, L: Dialect> { /// Values from [`Rewriter::new_result`] not yet bound to a statement, with /// their types. Their arena slots stay empty until bound. pub(super) unbound_ssa_results: FxHashMap, + /// Detached statements, with no parent, not inserted yet. They are + /// tombstoned if never inserted. + pub(super) detached: FxHashSet, } impl<'a, L: Dialect> Rewriter<'a, L> { @@ -513,6 +534,7 @@ impl<'a, L: Dialect> Rewriter<'a, L> { stage, events: Vec::new(), unbound_ssa_results: FxHashMap::default(), + detached: FxHashSet::default(), } } @@ -945,9 +967,9 @@ impl<'a, L: Dialect> Rewriter<'a, L> { /// Erase `stmt`: tombstone it, remove it from its block, drop its operand /// uses from the def-use index, and tombstone its result values. Erasing a - /// block's terminator clears the block's terminator, - /// removes its outgoing edges from the predecessor mirror, and leaves the - /// block unterminated (see [`Rewriter::insert_terminator`]). + /// block's terminator clears the block's terminator, removes its outgoing + /// edges from the predecessor mirror, and leaves the block unterminated + /// (see [`InsertionPoint::Terminator`](crate::InsertionPoint::Terminator)). /// Statement-owned blocks, `CFG`s, `DiGraph`s, and `UnGraph`s nested inside /// `stmt` are tombstoned with it, recursively. /// @@ -1131,35 +1153,6 @@ impl<'a, L: Dialect> Rewriter<'a, L> { } } - /// Insert `definition` as a new statement immediately before `anchor`. - /// - /// Shares [`Rewriter::insert_after`]'s preconditions, except that `anchor` - /// may be the block's terminator: the new statement is then appended to the - /// end of the block's statement chain. - pub fn insert_before( - &mut self, - anchor: Statement, - definition: L, - ) -> Result { - self.insert_relative(anchor, definition, Side::Before) - } - - /// Insert `definition` as a new statement immediately after `anchor`. - /// - /// `anchor` must be a live, non-terminator statement owned by a block body, - /// and `definition` must be a non-terminator with no successors. Every - /// operand must be live, and every result a fresh [`Rewriter::new_result`] - /// value, which becomes a result of the new statement. On success the new - /// statement is spliced into the block, its operand uses are recorded, and - /// [`MutationEvent::InsertedStatement`] is emitted; the new id is returned. - pub fn insert_after( - &mut self, - anchor: Statement, - definition: L, - ) -> Result { - self.insert_relative(anchor, definition, Side::After) - } - /// Where a statement goes when placed on `side` of `anchor`: its block, /// and the chain statements that will sit directly before and after it /// (`None` at either end of the chain). @@ -1193,97 +1186,6 @@ impl<'a, L: Dialect> Rewriter<'a, L> { Ok((block, prev, next)) } - fn insert_relative( - &mut self, - anchor: Statement, - definition: L, - side: Side, - ) -> Result { - let (block, prev, next) = self.slot_at(anchor, side)?; - - if definition.is_terminator() { - return Err(RewriteError::UnexpectedTerminator); - } - if definition.successors().next().is_some() { - return Err(RewriteError::NonTerminatorWithSuccessors); - } - let (results, operands) = self.prepare_insert(&definition)?; - let new_stmt = self.alloc_statement( - definition, - StatementParent::Block(block), - prev, - next, - results, - &operands, - ); - - self.link(LinkedList::single(new_stmt), block, prev, next); - - // Predecessor mirror does not need to be updated since we rejected terminators earlier. - self.events - .push(MutationEvent::InsertedStatement { stmt: new_stmt }); - Ok(new_stmt) - } - - /// Insert `definition` as the terminator of `block`, which must have none - /// (for example the first half left by [`Rewriter::split_block`]). - /// - /// The block may be in a `CFG` or a statement-owned body. Operand uses and - /// the predecessor mirror are updated for the new statement's operands and - /// successors, and the block's terminator pointer is set. - /// - /// Rejects, leaving the stage untouched, if `block` is unknown or already - /// has a terminator, or if `definition` is not a terminator, declares - /// results, or names a dead operand or successor. - /// - /// Records [`MutationEvent::InsertedStatement`] then - /// [`MutationEvent::ChangedTerminator`]. - pub fn insert_terminator( - &mut self, - block: Block, - definition: L, - ) -> Result { - { - let info = self.live_block(block)?; - if info.terminator.is_some() { - return Err(RewriteError::BlockHasTerminator(block)); - } - } - if !definition.is_terminator() { - return Err(RewriteError::ExpectedTerminator); - } - if definition.results().next().is_some() { - return Err(RewriteError::CannotInsertTerminatorWithResults); - } - let (results, operands) = self.prepare_insert(&definition)?; - let targets: HashSet = definition.successors().map(|s| s.target()).collect(); - for &target in &targets { - self.live_block(target)?; - } - - let new_stmt = self.alloc_statement( - definition, - StatementParent::Block(block), - None, - None, - results, - &operands, - ); - // Only terminators carry successors and the block had none, so every - // target gains exactly one entry for `block`. - for target in targets { - add_predecessor(self.stage, target, block); - } - if let Some(info) = block.get_info_mut(self.stage) { - info.terminator = Some(new_stmt); - } - - self.events - .push(MutationEvent::InsertedStatement { stmt: new_stmt }); - self.events.push(MutationEvent::ChangedTerminator { block }); - Ok(new_stmt) - } - /// Swap `stmt`'s definition for `definition` in place, keeping the same /// statement id, position (block slot, graph node, or `UnGraph` edge /// statement), and result values. diff --git a/crates/kirin-ir/src/rewrite/ungraph/surgery.rs b/crates/kirin-ir/src/rewrite/ungraph/surgery.rs index b82f49a410..b634467070 100644 --- a/crates/kirin-ir/src/rewrite/ungraph/surgery.rs +++ b/crates/kirin-ir/src/rewrite/ungraph/surgery.rs @@ -1,103 +1,10 @@ use crate::arena::{GetInfo, GetInfoMut}; -use crate::builder::ungraph::UnGraphRole; -use crate::{Dialect, SSAValue, Statement, StatementParent, UnGraph, UnGraphInfo}; +use crate::{Dialect, SSAValue, Statement, UnGraph, UnGraphInfo}; use crate::rewrite::rewriter::{MutationEvent, RewriteError, Rewriter}; -/// Adding `UnGraph` members. +/// Erasing and tombstoning `UnGraph` members. impl<'a, L: Dialect> Rewriter<'a, L> { - /// Add `definition` to `graph` as a new node. - /// - /// `definition` must not be an edge statement (see - /// [`Rewriter::insert_ungraph_edge`]), and must be a non-terminator with no - /// successors. Every operand must be live, and every result a fresh - /// [`Rewriter::new_result`] value. Each operand that is a wire of `graph` - /// (an edge statement's result or a boundary port) becomes an endpoint of - /// that wire; a wire read by two node slots is an edge between those - /// nodes. Other operands are ordinary uses. - /// - /// A wire may end up with more than two endpoints. That is invalid IR, - /// left for [`verify_derived`](crate::verify_derived) to report; such a - /// wire has no edge. - /// - /// Rejects, leaving the stage untouched, if `graph` is unknown or a check - /// above fails. Records - /// [`MutationEvent::InsertedStatement`] then - /// [`MutationEvent::ChangedUnGraphTopology`]. - pub fn insert_ungraph_node( - &mut self, - graph: UnGraph, - definition: L, - ) -> Result { - self.live_ungraph(graph)?; - Self::check_ungraph_member(&definition, UnGraphRole::Node)?; - let (results, operands) = self.prepare_insert(&definition)?; - - let wires = self.snapshot_wires(graph, operands.iter().copied()); - - let stmt = self.alloc_statement( - definition, - StatementParent::UnGraph(graph), - None, - None, - results, - &operands, - ); - graph - .get_info_mut(self.stage) - .expect("graph is live in this stage") - .add_node(stmt); - self.resync_wires(graph, wires); - - self.events.push(MutationEvent::InsertedStatement { stmt }); - self.events - .push(MutationEvent::ChangedUnGraphTopology { graph }); - Ok(stmt) - } - - /// Add `definition` to `graph` as a new edge statement, whose results are - /// new wires. - /// - /// `definition` must be an edge statement, not a terminator, with no - /// successors and no operands (a wire only connects the nodes that read - /// it); every result must be a fresh [`Rewriter::new_result`] - /// value. The new wires have no endpoints yet, so the petgraph gains no - /// edge until nodes read them. - /// - /// Rejects, leaving the stage untouched, if `graph` is unknown or a check - /// above fails. Records [`MutationEvent::InsertedStatement`] then - /// [`MutationEvent::ChangedUnGraphTopology`]. - pub fn insert_ungraph_edge( - &mut self, - graph: UnGraph, - definition: L, - ) -> Result { - self.live_ungraph(graph)?; - Self::check_ungraph_member(&definition, UnGraphRole::Edge)?; - if definition.arguments().next().is_some() { - return Err(RewriteError::EdgeStatementWithOperands); - } - let (results, operands) = self.prepare_insert(&definition)?; - - let stmt = self.alloc_statement( - definition, - StatementParent::UnGraph(graph), - None, - None, - results, - &operands, - ); - graph - .get_info_mut(self.stage) - .expect("graph is live in this stage") - .add_edge_member(stmt); - - self.events.push(MutationEvent::InsertedStatement { stmt }); - self.events - .push(MutationEvent::ChangedUnGraphTopology { graph }); - Ok(stmt) - } - /// The `UnGraph` arm of [`Rewriter::erase_statement`]: `stmt` is a live /// node or edge statement of `graph`. pub(in crate::rewrite) fn erase_ungraph_member( @@ -174,22 +81,4 @@ impl<'a, L: Dialect> Rewriter<'a, L> { .map(|item| &**item) .ok_or(RewriteError::UnknownUnGraph(graph)) } - - /// Check that `definition` can be an `UnGraph` member in `role`: an edge - /// statement exactly when `role` is [`UnGraphRole::Edge`], and a - /// non-terminator with no successors. - fn check_ungraph_member(definition: &L, role: UnGraphRole) -> Result<(), RewriteError> { - match (role, definition.is_edge()) { - (UnGraphRole::Node, true) => return Err(RewriteError::UnexpectedEdgeStatement), - (UnGraphRole::Edge, false) => return Err(RewriteError::ExpectedEdgeStatement), - _ => {} - } - if definition.is_terminator() { - return Err(RewriteError::UnexpectedTerminator); - } - if definition.successors().next().is_some() { - return Err(RewriteError::NonTerminatorWithSuccessors); - } - Ok(()) - } } diff --git a/crates/kirin-ir/src/rewrite/walk/tree.rs b/crates/kirin-ir/src/rewrite/walk/tree.rs index ca573b6040..5b14145eac 100644 --- a/crates/kirin-ir/src/rewrite/walk/tree.rs +++ b/crates/kirin-ir/src/rewrite/walk/tree.rs @@ -81,7 +81,7 @@ pub(crate) fn parent(stage: &StageInfo, stmt: Statement) -> Optio } /// Whether `stmt` is live and owns any body, empty or not. -pub(super) fn owns_bodies(stage: &StageInfo, stmt: Statement) -> bool { +pub(crate) fn owns_bodies(stage: &StageInfo, stmt: Statement) -> bool { stmt.get_info(stage).is_some_and(|info| { let definition = info.definition(); definition.blocks().next().is_some() diff --git a/crates/kirin-ir/src/testing/dialect.rs b/crates/kirin-ir/src/testing/dialect.rs index a71a23c18f..76b58f3bf9 100644 --- a/crates/kirin-ir/src/testing/dialect.rs +++ b/crates/kirin-ir/src/testing/dialect.rs @@ -3,7 +3,8 @@ //! Deliberately smaller than `tests/common.rs`'s `BuilderDialect`: it carries //! only what the in-crate tests need — operands, so values have uses, and a //! terminator, so blocks have a body to summarize, and a result-producing op -//! and an edge op, so graph mirrors have edges to forge. Successors and bodies +//! and an edge op, so graph mirrors have edges to forge, and one op owning a +//! block, so a statement can enclose others. Successors and the other bodies //! are covered by the integration tests, which reach the same code through the //! public API. @@ -42,6 +43,7 @@ impl Placeholder for TestType { /// - `Return`: a terminator /// - `Def(r)`: one result, so a `DiGraph` member can feed another /// - `Wire(r)`: an edge statement, whose result is an `UnGraph` wire +/// - `Region(b)`: owns the block `b`, so it encloses `b`'s statements #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) enum TestLang { Nop, @@ -50,6 +52,7 @@ pub(crate) enum TestLang { Return, Def(ResultValue), Wire(ResultValue), + Region(Block), } impl<'a> HasArguments<'a> for TestLang { @@ -120,21 +123,29 @@ impl<'a> HasResultsMut<'a> for TestLang { } } +impl<'a> HasBlocks<'a> for TestLang { + type Iter = std::option::IntoIter<&'a Block>; + fn blocks(&'a self) -> Self::Iter { + match self { + TestLang::Region(b) => Some(b).into_iter(), + _ => None.into_iter(), + } + } +} + +impl<'a> HasBlocksMut<'a> for TestLang { + type IterMut = std::option::IntoIter<&'a mut Block>; + fn blocks_mut(&'a mut self) -> Self::IterMut { + match self { + TestLang::Region(b) => Some(b).into_iter(), + _ => None.into_iter(), + } + } +} + /// The remaining `Dialect` supertraits, none of which this dialect exercises. macro_rules! empty_impls { ($ty:ty) => { - impl<'a> HasBlocks<'a> for $ty { - type Iter = std::iter::Empty<&'a Block>; - fn blocks(&'a self) -> Self::Iter { - std::iter::empty() - } - } - impl<'a> HasBlocksMut<'a> for $ty { - type IterMut = std::iter::Empty<&'a mut Block>; - fn blocks_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() - } - } impl<'a> HasCFG<'a> for $ty { type Iter = std::iter::Empty<&'a CFG>; fn cfgs(&'a self) -> Self::Iter { diff --git a/crates/kirin-ir/tests/common.rs b/crates/kirin-ir/tests/common.rs index db2ed238fa..f4335cdb3b 100644 --- a/crates/kirin-ir/tests/common.rs +++ b/crates/kirin-ir/tests/common.rs @@ -49,8 +49,8 @@ impl Placeholder for TestType { /// - `Isolated`: no operands, no results (ungraph isolated node) /// - `Branch(target)`: one-successor control-flow terminator /// - `CondBranch(a, b)`: two-successor control-flow terminator -/// - `Invoke(r)`: a terminator defining a result, which `insert_terminator` -/// rejects +/// - `Invoke(r)`: a terminator defining a result, which an +/// `InsertionPoint::Terminator` rejects /// - `Fork(target)`: a non-terminator carrying a successor, which is invalid /// IR; exists to test that verification reports it /// - `Jump(target, args)`: one-successor terminator forwarding `args` to the @@ -417,3 +417,13 @@ impl EventCursor { events } } + +/// The error [`Rewriter::create`] and [`Rewriter::insert`] return when a +/// statement cannot go at `insertion_pt`, for `reason`. +#[allow(dead_code)] +pub fn cannot_insert(insertion_pt: InsertionPoint, reason: RewriteError) -> RewriteError { + RewriteError::CannotInsert { + insertion_pt, + reason: Box::new(reason), + } +} diff --git a/crates/kirin-ir/tests/derived.rs b/crates/kirin-ir/tests/derived.rs index 1baffda5ad..4f36e50a65 100644 --- a/crates/kirin-ir/tests/derived.rs +++ b/crates/kirin-ir/tests/derived.rs @@ -20,7 +20,7 @@ mod common; use std::collections::HashSet; -use common::{BuilderDialect, TestType, make_split, new_stage}; +use common::{BuilderDialect, TestType, cannot_insert, make_split, new_stage}; use kirin_ir::*; /// The def-use set of `value` (order-agnostic; `SSAInfo::uses` is a bag). @@ -360,7 +360,7 @@ fn insert_before_keeps_the_use_index_in_step() { let inserted = { let mut rewriter = Rewriter::new(&mut f.stage); rewriter - .insert_before(f.consumer, BuilderDialect::Use(y)) + .create(InsertionPoint::Before(f.consumer), BuilderDialect::Use(y)) .unwrap() }; @@ -380,7 +380,7 @@ fn insert_after_keeps_the_use_index_in_step() { let inserted = { let mut rewriter = Rewriter::new(&mut f.stage); rewriter - .insert_after(f.add, BuilderDialect::Use(y)) + .create(InsertionPoint::After(f.add), BuilderDialect::Use(y)) .unwrap() }; @@ -415,10 +415,11 @@ fn replace_statement_keeps_the_use_index_in_step() { // --------------------------------------------------------------------------- // Predecessors mirror // -// `replace_statement` is the only method that can change a control-flow edge. -// The operand-rewriting methods write through `arguments_mut()`, which yields -// `&mut SSAValue` and so cannot reach a `Successor`; `erase_statement` rejects -// terminators, and the `insert_*` methods never insert one. +// These tests cover `replace_statement`, which rewrites a terminator's edges in +// place. The operand-rewriting methods write through `arguments_mut()`, which +// yields `&mut SSAValue` and so cannot reach a `Successor`. The other edge +// edits (erasing a terminator, `InsertionPoint::Terminator`, +// `retarget_successor`, merging blocks) are tested in `rewriter_cfg.rs`. // --------------------------------------------------------------------------- #[test] @@ -680,7 +681,7 @@ fn insert_before_keeps_the_block_body_in_step() { let inserted = { let mut rewriter = Rewriter::new(&mut f.stage); rewriter - .insert_before(f.first, BuilderDialect::Nop) + .create(InsertionPoint::Before(f.first), BuilderDialect::Nop) .unwrap() }; @@ -697,7 +698,9 @@ fn insert_after_keeps_the_block_body_in_step() { let inserted = { let mut rewriter = Rewriter::new(&mut f.stage); - rewriter.insert_after(f.last, BuilderDialect::Nop).unwrap() + rewriter + .create(InsertionPoint::After(f.last), BuilderDialect::Nop) + .unwrap() }; assert_eq!( @@ -714,7 +717,7 @@ fn insert_before_the_terminator_keeps_the_block_body_in_step() { let inserted = { let mut rewriter = Rewriter::new(&mut f.stage); rewriter - .insert_before(f.terminator, BuilderDialect::Nop) + .create(InsertionPoint::Before(f.terminator), BuilderDialect::Nop) .unwrap() }; @@ -736,8 +739,11 @@ fn insert_after_the_terminator_leaves_the_block_body_intact() { let mut rewriter = Rewriter::new(&mut f.stage); // Nothing can follow a terminator. assert_eq!( - rewriter.insert_after(f.terminator, BuilderDialect::Nop), - Err(RewriteError::AnchorIsTerminator(f.terminator)) + rewriter.create(InsertionPoint::After(f.terminator), BuilderDialect::Nop), + Err(cannot_insert( + InsertionPoint::After(f.terminator), + RewriteError::AnchorIsTerminator(f.terminator) + )) ); } diff --git a/crates/kirin-ir/tests/rewriter.rs b/crates/kirin-ir/tests/rewriter.rs index 5777d9024b..37a4b0772a 100644 --- a/crates/kirin-ir/tests/rewriter.rs +++ b/crates/kirin-ir/tests/rewriter.rs @@ -5,7 +5,9 @@ mod common; use std::collections::HashSet; -use common::{BuilderDialect, EventCursor, TestType, make_split, make_wire, new_stage}; +use common::{ + BuilderDialect, EventCursor, TestType, cannot_insert, make_split, make_wire, new_stage, +}; use kirin_ir::*; /// The def-use set of `value` (order-agnostic; `SSAInfo::uses` is a bag). @@ -419,7 +421,7 @@ fn erase_statement_erases_a_terminator_and_its_edges() { // The block can be closed again. Rewriter::new(&mut stage) - .insert_terminator(entry, BuilderDialect::Return) + .create(InsertionPoint::Terminator(entry), BuilderDialect::Return) .unwrap(); verify_derived(&stage).unwrap(); } @@ -477,8 +479,12 @@ fn insert_before_and_after_splice_and_maintain_index() { let (n0, n1, events) = { let mut rw = Rewriter::new(&mut stage); - let n0 = rw.insert_before(s0, BuilderDialect::Use(real_y)).unwrap(); - let n1 = rw.insert_after(s0, BuilderDialect::Use(real_x)).unwrap(); + let n0 = rw + .create(InsertionPoint::Before(s0), BuilderDialect::Use(real_y)) + .unwrap(); + let n1 = rw + .create(InsertionPoint::After(s0), BuilderDialect::Use(real_x)) + .unwrap(); (n0, n1, rw.events().to_vec()) }; @@ -503,28 +509,55 @@ fn insert_before_and_after_splice_and_maintain_index() { // Rejections, none of which mutate the stage. let mut rw = Rewriter::new(&mut stage); assert_eq!( - rw.insert_after(ret, BuilderDialect::Nop), - Err(RewriteError::AnchorIsTerminator(ret)) + rw.create(InsertionPoint::After(ret), BuilderDialect::Nop), + Err(cannot_insert( + InsertionPoint::After(ret), + RewriteError::AnchorIsTerminator(ret) + )) ); let not_fresh = ResultValue::from(TestSSAValue(0)); assert_eq!( - rw.insert_before(s0, BuilderDialect::Wire(not_fresh)), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Wire(not_fresh)), Err(RewriteError::ResultNotFresh(not_fresh.into())) ); assert_eq!( - rw.insert_before(s0, BuilderDialect::Return), - Err(RewriteError::UnexpectedTerminator) + rw.create(InsertionPoint::Before(s0), BuilderDialect::Return), + Err(cannot_insert( + InsertionPoint::Before(s0), + RewriteError::UnexpectedTerminator + )) ); // Only a terminator may carry successors. assert_eq!( - rw.insert_after(s0, BuilderDialect::Fork(Successor::from_block(block))), - Err(RewriteError::NonTerminatorWithSuccessors) + rw.create( + InsertionPoint::After(s0), + BuilderDialect::Fork(Successor::from_block(block)) + ), + Err(cannot_insert( + InsertionPoint::After(s0), + RewriteError::NonTerminatorWithSuccessors + )) ); let bogus = SSAValue::from(TestSSAValue(9999)); assert_eq!( - rw.insert_before(s0, BuilderDialect::Use(bogus)), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Use(bogus)), Err(RewriteError::UnknownValue(bogus)) ); + // `insert` places only a statement this rewriter holds detached: not one + // already in the IR, nor an unknown id. + assert_eq!( + rw.insert(InsertionPoint::Before(s0), s0), + Err(RewriteError::NotDetached(s0)) + ); + assert_eq!( + rw.insert(InsertionPoint::After(s0), n0), + Err(RewriteError::NotDetached(n0)) + ); + let unknown = rw.stage().statement_arena().next_id(); + assert_eq!( + rw.insert(InsertionPoint::Before(s0), unknown), + Err(RewriteError::UnknownStatement(unknown)) + ); assert!(rw.events().is_empty()); drop(rw); @@ -561,36 +594,41 @@ fn insert_binds_fresh_results() { // Unbound values are not live yet, and not visible to verification. assert_eq!( - rw.insert_before(s0, BuilderDialect::Use(a.into())), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Use(a.into())), Err(RewriteError::UnknownValue(a.into())) ); verify_derived(rw.stage()).unwrap(); // Rejected: the same value twice, an existing value, a stale value. assert_eq!( - rw.insert_before(s0, BuilderDialect::Split(a, a)), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Split(a, a)), Err(RewriteError::ResultNotFresh(a.into())) ); assert_eq!( - rw.insert_before(s0, BuilderDialect::Wire(ResultValue::from(real_x))), + rw.create( + InsertionPoint::Before(s0), + BuilderDialect::Wire(ResultValue::from(real_x)) + ), Err(RewriteError::ResultNotFresh(real_x)) ); assert_eq!( - rw.insert_before(s0, BuilderDialect::Wire(stale)), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Wire(stale)), Err(RewriteError::ResultNotFresh(stale.into())) ); // A rejection after the freshness check leaves `a` unbound: the `Split` // below still binds it. let dead = SSAValue::from(TestSSAValue(9_999)); assert_eq!( - rw.insert_before(s0, BuilderDialect::Compute(dead, a)), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Compute(dead, a)), Err(RewriteError::UnknownValue(dead)) ); assert!(a.get_info(rw.stage()).is_none()); assert!(rw.events().is_empty()); // Binding makes them results of the new statement, in slot order. - let split = rw.insert_before(s0, BuilderDialect::Split(a, b)).unwrap(); + let split = rw + .create(InsertionPoint::Before(s0), BuilderDialect::Split(a, b)) + .unwrap(); let info = a.get_info(rw.stage()).unwrap(); assert_eq!(*info.kind(), SSAKind::Result(split, 0)); assert_eq!(*info.ty(), TestType::I32); @@ -600,16 +638,18 @@ fn insert_binds_fresh_results() { // Bound values are usable, and cannot be bound again. let user = rw - .insert_after(split, BuilderDialect::Use(a.into())) + .create(InsertionPoint::After(split), BuilderDialect::Use(a.into())) .unwrap(); assert_eq!( - rw.insert_after(split, BuilderDialect::Wire(b)), + rw.create(InsertionPoint::After(split), BuilderDialect::Wire(b)), Err(RewriteError::ResultNotFresh(b.into())) ); - // `insert_after` binds results too. + // An `After` point binds results too. let c = rw.new_result(TestType::I32); - let wire = rw.insert_after(split, BuilderDialect::Wire(c)).unwrap(); + let wire = rw + .create(InsertionPoint::After(split), BuilderDialect::Wire(c)) + .unwrap(); let info = c.get_info(rw.stage()).unwrap(); assert_eq!(*info.kind(), SSAKind::Result(wire, 0)); assert_eq!(*info.ty(), TestType::I32); @@ -644,7 +684,9 @@ fn insert_before_terminator_fills_a_terminator_only_block() { // With no non-terminator to anchor on, the terminator is the only way in. let (n0, events) = { let mut rw = Rewriter::new(&mut stage); - let n0 = rw.insert_before(ret, BuilderDialect::Nop).unwrap(); + let n0 = rw + .create(InsertionPoint::Before(ret), BuilderDialect::Nop) + .unwrap(); (n0, rw.events().to_vec()) }; diff --git a/crates/kirin-ir/tests/rewriter_cfg.rs b/crates/kirin-ir/tests/rewriter_cfg.rs index 1940c326a8..c3ecf2123e 100644 --- a/crates/kirin-ir/tests/rewriter_cfg.rs +++ b/crates/kirin-ir/tests/rewriter_cfg.rs @@ -6,7 +6,7 @@ mod common; use std::collections::HashSet; -use common::{BuilderDialect, EventCursor, TestType, make_split, new_stage}; +use common::{BuilderDialect, EventCursor, TestType, cannot_insert, make_split, new_stage}; use kirin_ir::*; /// The def-use set of `value` (order-agnostic; `SSAInfo::uses` is a bag). @@ -241,7 +241,7 @@ fn erase_block_allows_self_loop() { let mut stage = stage.finalize().unwrap(); // The builder cannot name `lp` before creating it, so close the loop here. Rewriter::new(&mut stage) - .insert_terminator(lp, br(lp)) + .create(InsertionPoint::Terminator(lp), br(lp)) .unwrap(); assert_eq!(preds(&stage, lp), HashSet::from([lp])); @@ -453,7 +453,7 @@ fn split_block_moves_suffix_and_leaves_original_unterminated() { // Closing the first half with a branch, as a caller would, wires the edge. Rewriter::new(&mut stage) - .insert_terminator(entry, br(tail)) + .create(InsertionPoint::Terminator(entry), br(tail)) .unwrap(); assert_eq!(preds(&stage, tail), HashSet::from([entry])); verify_derived(&stage).unwrap(); @@ -555,8 +555,8 @@ fn insert_terminator_wires_operands_and_edges() { let (jump, events) = { let mut rw = Rewriter::new(&mut stage); let jump = rw - .insert_terminator( - entry, + .create( + InsertionPoint::Terminator(entry), BuilderDialect::Jump(Successor::from_block(target), vec![a]), ) .unwrap(); @@ -596,7 +596,7 @@ fn insert_terminator_works_in_statement_owned_body() { let mut stage = stage.finalize().unwrap(); let ret = Rewriter::new(&mut stage) - .insert_terminator(body, BuilderDialect::Return) + .create(InsertionPoint::Terminator(body), BuilderDialect::Return) .unwrap(); assert_eq!(body.terminator(&stage), Some(ret)); @@ -619,33 +619,57 @@ fn insert_terminator_rejects_without_mutating() { let mut rw = Rewriter::new(&mut stage); assert_eq!( - rw.insert_terminator(terminated, BuilderDialect::Return), - Err(RewriteError::BlockHasTerminator(terminated)) + rw.create( + InsertionPoint::Terminator(terminated), + BuilderDialect::Return + ), + Err(cannot_insert( + InsertionPoint::Terminator(terminated), + RewriteError::BlockHasTerminator(terminated) + )) ); assert_eq!( - rw.insert_terminator(open, BuilderDialect::Nop), - Err(RewriteError::ExpectedTerminator) + rw.create(InsertionPoint::Terminator(open), BuilderDialect::Nop), + Err(cannot_insert( + InsertionPoint::Terminator(open), + RewriteError::ExpectedTerminator + )) ); // Even a fresh result is rejected: nothing can follow a terminator to use it. let result = rw.new_result(TestType::I32); assert_eq!( - rw.insert_terminator(open, BuilderDialect::Invoke(result)), - Err(RewriteError::CannotInsertTerminatorWithResults) + rw.create( + InsertionPoint::Terminator(open), + BuilderDialect::Invoke(result) + ), + Err(cannot_insert( + InsertionPoint::Terminator(open), + RewriteError::CannotInsertTerminatorWithResults + )) ); assert_eq!( - rw.insert_terminator(open, br(dead_block)), - Err(RewriteError::UnknownBlock(dead_block)) + rw.create(InsertionPoint::Terminator(open), br(dead_block)), + Err(cannot_insert( + InsertionPoint::Terminator(open), + RewriteError::UnknownBlock(dead_block) + )) ); assert_eq!( - rw.insert_terminator( - open, + rw.create( + InsertionPoint::Terminator(open), BuilderDialect::Jump(Successor::from_block(terminated), vec![dead_value]), ), Err(RewriteError::UnknownValue(dead_value)) ); assert_eq!( - rw.insert_terminator(dead_block, BuilderDialect::Return), - Err(RewriteError::UnknownBlock(dead_block)) + rw.create( + InsertionPoint::Terminator(dead_block), + BuilderDialect::Return + ), + Err(cannot_insert( + InsertionPoint::Terminator(dead_block), + RewriteError::UnknownBlock(dead_block) + )) ); assert!(rw.events().is_empty()); @@ -893,7 +917,7 @@ fn merge_blocks_rejects_self_looping_succ() { let mut stage = stage.finalize().unwrap(); // The builder cannot name `succ` before creating it, so close the loop here. Rewriter::new(&mut stage) - .insert_terminator(succ, br(succ)) + .create(InsertionPoint::Terminator(succ), br(succ)) .unwrap(); let succ_br = succ.terminator(&stage).unwrap(); diff --git a/crates/kirin-ir/tests/rewriter_digraph.rs b/crates/kirin-ir/tests/rewriter_digraph.rs index e5bc6129d3..d4bf7fdbe8 100644 --- a/crates/kirin-ir/tests/rewriter_digraph.rs +++ b/crates/kirin-ir/tests/rewriter_digraph.rs @@ -6,7 +6,9 @@ mod common; use std::collections::{HashMap, HashSet}; -use common::{BuilderDialect, EventCursor, TestType, make_compute, make_split, new_stage}; +use common::{ + BuilderDialect, EventCursor, TestType, cannot_insert, make_compute, make_split, new_stage, +}; use kirin_ir::*; use petgraph::visit::{EdgeRef, IntoEdgeReferences}; @@ -77,13 +79,20 @@ fn insert_node_adds_operand_edges_and_binds_results() { let mut rw = Rewriter::new(&mut stage); // A member operand gets an edge from its producer. let r1 = rw.new_result(TestType::I32); - let n1 = rw.insert_node(dg, BuilderDialect::Compute(rp, r1)).unwrap(); + let n1 = rw + .create(InsertionPoint::DiGraph(dg), BuilderDialect::Compute(rp, r1)) + .unwrap(); // An outside operand gets no edge. let n2 = rw - .insert_node(dg, BuilderDialect::Add(r1.into(), x)) + .create( + InsertionPoint::DiGraph(dg), + BuilderDialect::Add(r1.into(), x), + ) .unwrap(); // Two slots reading the same member result get two parallel edges. - let n3 = rw.insert_node(dg, BuilderDialect::Add(rp, rp)).unwrap(); + let n3 = rw + .create(InsertionPoint::DiGraph(dg), BuilderDialect::Add(rp, rp)) + .unwrap(); let events = rw.events().to_vec(); drop(rw); @@ -134,28 +143,49 @@ fn insert_node_rejects_without_mutating() { let mut rw = Rewriter::new(&mut stage); let fresh = rw.new_result(TestType::I32); assert_eq!( - rw.insert_node(dead_graph, BuilderDialect::Nop), - Err(RewriteError::UnknownDiGraph(dead_graph)) + rw.create(InsertionPoint::DiGraph(dead_graph), BuilderDialect::Nop), + Err(cannot_insert( + InsertionPoint::DiGraph(dead_graph), + RewriteError::UnknownDiGraph(dead_graph) + )) ); assert_eq!( - rw.insert_node(dg, BuilderDialect::Return), - Err(RewriteError::UnexpectedTerminator) + rw.create(InsertionPoint::DiGraph(dg), BuilderDialect::Return), + Err(cannot_insert( + InsertionPoint::DiGraph(dg), + RewriteError::UnexpectedTerminator + )) ); assert_eq!( - rw.insert_node(dg, BuilderDialect::Fork(Successor::from_block(outer))), - Err(RewriteError::NonTerminatorWithSuccessors) + rw.create( + InsertionPoint::DiGraph(dg), + BuilderDialect::Fork(Successor::from_block(outer)) + ), + Err(cannot_insert( + InsertionPoint::DiGraph(dg), + RewriteError::NonTerminatorWithSuccessors + )) ); assert_eq!( - rw.insert_node(dg, BuilderDialect::Compute(x, ResultValue::from(rp))), + rw.create( + InsertionPoint::DiGraph(dg), + BuilderDialect::Compute(x, ResultValue::from(rp)) + ), Err(RewriteError::ResultNotFresh(rp)) ); assert_eq!( - rw.insert_node(dg, BuilderDialect::Use(dead_value())), + rw.create( + InsertionPoint::DiGraph(dg), + BuilderDialect::Use(dead_value()) + ), Err(RewriteError::UnknownValue(dead_value())) ); // An unbound result is not live, so it cannot be an operand. assert_eq!( - rw.insert_node(dg, BuilderDialect::Use(fresh.into())), + rw.create( + InsertionPoint::DiGraph(dg), + BuilderDialect::Use(fresh.into()) + ), Err(RewriteError::UnknownValue(fresh.into())) ); assert!(rw.events().is_empty()); diff --git a/crates/kirin-ir/tests/rewriter_ungraph.rs b/crates/kirin-ir/tests/rewriter_ungraph.rs index 2d99f750e1..e6aa973ccf 100644 --- a/crates/kirin-ir/tests/rewriter_ungraph.rs +++ b/crates/kirin-ir/tests/rewriter_ungraph.rs @@ -6,7 +6,9 @@ mod common; use std::collections::HashMap; -use common::{BuilderDialect, EventCursor, TestType, make_split, make_wire, new_stage}; +use common::{ + BuilderDialect, EventCursor, TestType, cannot_insert, make_split, make_wire, new_stage, +}; use kirin_ir::*; use petgraph::visit::{EdgeRef, IntoEdgeReferences}; @@ -136,16 +138,23 @@ fn insert_ungraph_node_connects_the_readers_of_a_wire() { let mut rw = Rewriter::new(&mut stage); // One reader: an endpoint, but no edge yet. - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); assert!(edges(rw.stage(), ug).is_empty()); // A second reader completes the edge; the boundary port gets one reader. let b = rw - .insert_ungraph_node(ug, BuilderDialect::Gate(w, boundary)) + .create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Gate(w, boundary), + ) .unwrap(); assert_eq!(edges(rw.stage(), ug), HashMap::from([(w, pair(a, b))])); // A third reader makes `w` a hyperedge: accepted, but `w` loses its edge // and verification reports it. - let c = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let c = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); assert!(edges(rw.stage(), ug).is_empty()); assert_eq!( degree_findings(rw.stage()), @@ -160,7 +169,10 @@ fn insert_ungraph_node_connects_the_readers_of_a_wire() { assert_eq!(edges(rw.stage(), ug), HashMap::from([(w, pair(a, b))])); // A boundary port is a wire too; a capture is an ordinary use. let d = rw - .insert_ungraph_node(ug, BuilderDialect::Gate(boundary, capture)) + .create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Gate(boundary, capture), + ) .unwrap(); let events = rw.events().to_vec(); drop(rw); @@ -202,11 +214,13 @@ fn insert_ungraph_node_reading_a_wire_twice_is_a_self_loop() { let mut rw = Rewriter::new(&mut stage); let s = rw - .insert_ungraph_node(ug, BuilderDialect::Gate(v, v)) + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Gate(v, v)) .unwrap(); assert_eq!(edges(rw.stage(), ug), HashMap::from([(v, pair(s, s))])); // Both of `v`'s endpoints are taken: a third reader leaves no edge. - let t = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); + let t = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); drop(rw); assert!(edges(&stage, ug).is_empty()); @@ -227,14 +241,16 @@ fn insert_ungraph_edge_adds_a_wire_nodes_can_read() { let mut rw = Rewriter::new(&mut stage); let r = rw.new_result(TestType::Qubit); - let e = rw.insert_ungraph_edge(ug, BuilderDialect::Wire(r)).unwrap(); + let e = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Wire(r)) + .unwrap(); // The new wire has no readers, so no edge. assert!(edges(rw.stage(), ug).is_empty()); let n1 = rw - .insert_ungraph_node(ug, BuilderDialect::Use(r.into())) + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(r.into())) .unwrap(); let n2 = rw - .insert_ungraph_node(ug, BuilderDialect::Use(r.into())) + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(r.into())) .unwrap(); let events = rw.events().to_vec(); drop(rw); @@ -276,39 +292,64 @@ fn ungraph_inserts_reject_without_mutating() { let fresh = rw.new_result(TestType::Qubit); let rejections = [ ( - rw.insert_ungraph_node(dead_graph, BuilderDialect::Isolated), - RewriteError::UnknownUnGraph(dead_graph), - ), - ( - rw.insert_ungraph_edge(dead_graph, BuilderDialect::Wire(fresh)), - RewriteError::UnknownUnGraph(dead_graph), + rw.create( + InsertionPoint::UnGraph(dead_graph), + BuilderDialect::Isolated, + ), + cannot_insert( + InsertionPoint::UnGraph(dead_graph), + RewriteError::UnknownUnGraph(dead_graph), + ), ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Return), - RewriteError::UnexpectedTerminator, + rw.create( + InsertionPoint::UnGraph(dead_graph), + BuilderDialect::Wire(fresh), + ), + cannot_insert( + InsertionPoint::UnGraph(dead_graph), + RewriteError::UnknownUnGraph(dead_graph), + ), ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Fork(Successor::from_block(dead_block))), - RewriteError::NonTerminatorWithSuccessors, + rw.create(InsertionPoint::UnGraph(ug), BuilderDialect::Return), + cannot_insert( + InsertionPoint::UnGraph(ug), + RewriteError::UnexpectedTerminator, + ), ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Wire(fresh)), - RewriteError::UnexpectedEdgeStatement, + rw.create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Fork(Successor::from_block(dead_block)), + ), + cannot_insert( + InsertionPoint::UnGraph(ug), + RewriteError::NonTerminatorWithSuccessors, + ), ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Use(dead_value())), + rw.create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Use(dead_value()), + ), RewriteError::UnknownValue(dead_value()), ), ( - rw.insert_ungraph_edge(ug, BuilderDialect::Use(w)), - RewriteError::ExpectedEdgeStatement, + rw.create( + InsertionPoint::UnGraph(ug), + BuilderDialect::ParamWire(w, fresh), + ), + cannot_insert( + InsertionPoint::UnGraph(ug), + RewriteError::EdgeStatementWithOperands, + ), ), ( - rw.insert_ungraph_edge(ug, BuilderDialect::ParamWire(w, fresh)), - RewriteError::EdgeStatementWithOperands, - ), - ( - rw.insert_ungraph_edge(ug, BuilderDialect::Wire(ResultValue::from(w))), + rw.create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Wire(ResultValue::from(w)), + ), RewriteError::ResultNotFresh(w), ), ]; @@ -336,9 +377,15 @@ fn erase_ungraph_node_drops_its_edges_and_frees_the_wire() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let c = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let c = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); log.advance(&rw); // `a` is not the last node, so the last one moves into its slot. @@ -352,7 +399,9 @@ fn erase_ungraph_node_drops_its_edges_and_frees_the_wire() { ); // `w` is left with one reader, so its edge is gone and it can take another. assert!(edges(rw.stage(), ug).is_empty()); - let d = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let d = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); drop(rw); assert!(a.get_info(&stage).is_none()); @@ -378,10 +427,13 @@ fn erase_ungraph_node_with_used_results_is_rejected() { let mut rw = Rewriter::new(&mut stage); let r = rw.new_result(TestType::I32); let n = rw - .insert_ungraph_node(ug, BuilderDialect::Compute(capture, r)) + .create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Compute(capture, r), + ) .unwrap(); let _reader = rw - .insert_ungraph_node(ug, BuilderDialect::Use(r.into())) + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(r.into())) .unwrap(); log.advance(&rw); @@ -408,7 +460,9 @@ fn erase_edge_statement_once_no_node_reads_its_wire() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); log.advance(&rw); // A node still reads the wire. @@ -505,10 +559,18 @@ fn replace_operand_swaps_endpoints_through_an_over_full_wire() { } = fixture(); let mut rw = Rewriter::new(&mut stage); // w joins a — b, v joins c — d; both wires are full. - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let c = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); - let d = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let c = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); + let d = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); log.advance(&rw); let topology = MutationEvent::ChangedUnGraphTopology { graph: ug }; @@ -557,8 +619,12 @@ fn replace_operand_off_a_wire_drops_its_edge() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let _a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let _a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); log.advance(&rw); // A capture is not a wire: b leaves w, so w's edge goes. @@ -575,8 +641,11 @@ fn replace_operand_off_a_wire_drops_its_edge() { // Neither the capture nor a node's result is a wire: moving between them // changes no topology, so only the operand event is recorded. let r = rw.new_result(TestType::I32); - rw.insert_ungraph_node(ug, BuilderDialect::Compute(capture, r)) - .unwrap(); + rw.create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Compute(capture, r), + ) + .unwrap(); log.advance(&rw); rw.replace_operand(b, 0, r).unwrap(); assert_eq!( @@ -600,8 +669,12 @@ fn replace_all_uses_moves_every_reader_to_the_new_wire() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let c = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let c = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); log.advance(&rw); // One reader each: merging w into v gives v exactly two. @@ -617,7 +690,9 @@ fn replace_all_uses_moves_every_reader_to_the_new_wire() { ); // A third reader merged in leaves v over full, with no edge. - let e = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let e = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); rw.replace_all_uses(w, v).unwrap(); drop(rw); @@ -646,8 +721,12 @@ fn replace_results_redirects_an_edge_statements_readers() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); log.advance(&rw); // w's readers move to v, so the edge now carries v, and w_stmt is free @@ -708,9 +787,15 @@ fn replace_statement_on_ungraph_node_moves_its_wires() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let c = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let c = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); log.advance(&rw); // b now reads v instead of w: w loses its edge, v gains one. @@ -749,8 +834,12 @@ fn replace_statement_on_edge_statement_keeps_its_wire() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); log.advance(&rw); // The result slot is overwritten with the statement's own result, so `w` @@ -790,7 +879,10 @@ fn replace_statement_on_ungraph_member_rejects_role_changes() { let mut rw = Rewriter::new(&mut stage); let r = rw.new_result(TestType::I32); let node = rw - .insert_ungraph_node(ug, BuilderDialect::Compute(capture, r)) + .create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Compute(capture, r), + ) .unwrap(); log.advance(&rw); let placeholder = ResultValue::from(TestSSAValue(0)); @@ -836,9 +928,15 @@ fn replace_uses_if_moves_only_the_selected_reader_to_the_new_wire() { .. } = fixture(); let mut rw = Rewriter::new(&mut stage); - let a = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let b = rw.insert_ungraph_node(ug, BuilderDialect::Use(w)).unwrap(); - let c = rw.insert_ungraph_node(ug, BuilderDialect::Use(v)).unwrap(); + let a = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let b = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(w)) + .unwrap(); + let c = rw + .create(InsertionPoint::UnGraph(ug), BuilderDialect::Use(v)) + .unwrap(); assert_eq!(edges(rw.stage(), ug), HashMap::from([(w, pair(a, b))])); log.advance(&rw); diff --git a/crates/kirin-ir/tests/walk.rs b/crates/kirin-ir/tests/walk.rs index d6d506b070..e58d92e0ff 100644 --- a/crates/kirin-ir/tests/walk.rs +++ b/crates/kirin-ir/tests/walk.rs @@ -245,7 +245,7 @@ fn pre_order_walks_children_inserted_while_visiting() { if stmt == owner { inserted = Some( rewriter - .insert_before(then_ret, BuilderDialect::Nop) + .create(InsertionPoint::Before(then_ret), BuilderDialect::Nop) .unwrap(), ); } @@ -267,7 +267,11 @@ fn post_order_walks_siblings_inserted_after_the_visited_statement() { let mut inserted = None; let visited = walk_editing::(&mut t.stage, t.root, |rewriter, stmt| { if stmt == a { - inserted = Some(rewriter.insert_after(a, BuilderDialect::Nop).unwrap()); + inserted = Some( + rewriter + .create(InsertionPoint::After(a), BuilderDialect::Nop) + .unwrap(), + ); } }) .unwrap(); @@ -281,7 +285,9 @@ fn visits_with_insert_after_a() -> usize { let a = t.a; walk_editing::(&mut t.stage, t.root, |rewriter, stmt| { if stmt == a { - rewriter.insert_after(a, BuilderDialect::Nop).unwrap(); + rewriter + .create(InsertionPoint::After(a), BuilderDialect::Nop) + .unwrap(); } }) .unwrap() @@ -329,12 +335,24 @@ fn bfs_walks_statements_inserted_at_the_current_level_and_below() { let visited = walk_editing::(&mut stage, root, |rewriter, stmt| { if stmt == a { // Ahead of the cursor at the current level, and one level down. - n = Some(rewriter.insert_after(b, BuilderDialect::Nop).unwrap()); - z = Some(rewriter.insert_after(x, BuilderDialect::Nop).unwrap()); + n = Some( + rewriter + .create(InsertionPoint::After(b), BuilderDialect::Nop) + .unwrap(), + ); + z = Some( + rewriter + .create(InsertionPoint::After(x), BuilderDialect::Nop) + .unwrap(), + ); } if stmt == y { // At a level that is already finished. - h = Some(rewriter.insert_after(a, BuilderDialect::Nop).unwrap()); + h = Some( + rewriter + .create(InsertionPoint::After(a), BuilderDialect::Nop) + .unwrap(), + ); } }) .unwrap(); @@ -606,7 +624,7 @@ fn skip_after_inserting() -> (Tree, Statement, Vec) { } inserted = Some( rewriter - .insert_before(then_ret, BuilderDialect::Nop) + .create(InsertionPoint::Before(then_ret), BuilderDialect::Nop) .unwrap(), ); WalkResult::Skip diff --git a/docs/design/rewrite-engine.md b/docs/design/rewrite-engine.md index c4fdf3c60c..e9ec8a64d6 100644 --- a/docs/design/rewrite-engine.md +++ b/docs/design/rewrite-engine.md @@ -1410,7 +1410,7 @@ derived metadata that each mutation must synchronize. The reverse derivation and a finalize-only installer; the public mutating `rebuild_use_index` is gone. Every current `Rewriter` edit (`replace_operand`, `replace_all_uses`, `replace_results`, `erase_statement`, -`insert_before`/`insert_after`, `replace_statement`) maintains it +`create`/`insert`, `replace_statement`) maintains it incrementally. The index is derived metadata over authoritative statement/yield slots, not a @@ -1707,17 +1707,19 @@ impl<'a, L: Dialect> Rewriter<'a, L> { stmt: Statement, ) -> Result<(), RewriteError>; - pub fn insert_before( + // `InsertionPoint`: Before(Statement), After(Statement), Terminator(Block), + // DiGraph(DiGraph), UnGraph(UnGraph). + pub fn create( &mut self, - anchor: Statement, + insertion_pt: InsertionPoint, definition: L, ) -> Result; - pub fn insert_after( + pub fn insert( &mut self, - anchor: Statement, - definition: L, - ) -> Result; + insertion_pt: InsertionPoint, + stmt: Statement, + ) -> Result<(), RewriteError>; pub fn replace_statement( &mut self, @@ -1725,11 +1727,6 @@ impl<'a, L: Dialect> Rewriter<'a, L> { definition: L, ) -> Result<(), RewriteError>; - pub fn insert_terminator( - &mut self, - block: Block, - definition: L, - ) -> Result; // --- `Cfg` block-list surgery (rewrite/cfg) --- pub fn append_block( @@ -1814,20 +1811,17 @@ stage remains valid. | `replace_uses_if` | `replace_all_uses` restricted to the use sites a predicate accepts; the predicate gets the stage and each site, all before any write, and rejected sites stay uses of `old`. | as `replace_all_uses` for the rewritten slots; `ReplacedUses` only if every use was selected | returns count of slots rewritten; self-replace is `Ok(0)`; `old` and `new` must be live | | `replace_all_uses_except_stmt` / `_stmts` | `replace_uses_if` keeping the operands of the given statement(s); uses nested in their bodies are still replaced. | as `replace_uses_if` | the excepted statements must be live | | `erase_statement` | Tombstone the statement, unlink it from its block list (or remove its graph node and the edges touching it, or drop an `UnGraph` edge statement from the member map), drop its operand uses, tombstone its (unused) result values. Statement-owned blocks, `Cfg`s, `DiGraph`s, and `UnGraph`s nested in it are tombstoned with it. Erasing a terminator clears the block's terminator and removes its outgoing edges, leaving the block unterminated. | `ErasedStatement` (covers the nested bodies; no CFG or graph events), then `ChangedTerminator` for a terminator, `ChangedDiGraphTopology` for a `DiGraph` member, or `ChangedUnGraphTopology` for an `UnGraph` member | parented by a block or graph; no live result uses (a `DiGraph` yield is a use, and so is a node reading an edge statement's wire) | -| `new_result` | Allocate a typed result value for a statement not inserted yet; its arena slot stays empty (not live) until an insert binds it. | none | a value its `Rewriter` never binds is tombstoned when the `Rewriter` is dropped (`run_pass` drops it before verifying) | -| `insert_before` / `insert_after` | Allocate a statement, splice it adjacent to `anchor`, bind its results, add operand uses; return the new id. | `InsertedStatement` | anchor is block-parented; only `insert_before` may anchor on the terminator, which appends to the end of the chain; `definition` is a non-terminator with no successors; operands must be live; results must be distinct, unbound `new_result` values | +| `new_result` | Allocate a typed result value for a statement not created yet; its arena slot stays empty (not live) until `create` binds it. | none | a value its `Rewriter` never binds is tombstoned when the `Rewriter` is dropped (`run_pass` drops it before verifying) | +| `create` | Allocate `definition` as a statement at an `InsertionPoint`, bind its results, add operand uses; return the new id. `Before`/`After` splice it adjacent to an anchor in a block body (`Before` a terminator appends to the end of the chain). `Terminator` makes it the terminator of a block that has none and adds predecessor entries. `DiGraph` adds a member and node with an edge from each member producer of an operand. `UnGraph` adds an edge statement if the definition is one, otherwise a node whose wire operands become endpoints (a wire with two endpoints is an edge). | `InsertedStatement`, then `ChangedTerminator`, `ChangedDiGraphTopology` or `ChangedUnGraphTopology` for those points | operands live; results distinct, unbound `new_result` values; for `Before`/`After` and graphs a non-terminator with no successors; for `Terminator` a result-less terminator with live successors; an `UnGraph` edge statement has no operands; a wire may exceed two endpoints (reported by `verify_derived`; it then has no edge) | +| `insert` | Place a detached statement (one this `Rewriter` made but has not placed, such as a copy) at an `InsertionPoint`, as `create` does. | as `create` | the statement is detached and belongs to this `Rewriter`; it fits the point as for `create`. Detached statements never inserted are tombstoned when the `Rewriter` is dropped | | `move_before` / `move_after` | Unlink a statement and link it adjacent to `anchor`, in any block body; id, results, uses and nested bodies are unchanged. | `MovedStatement` | both are block-parented; the moved statement is not a terminator; only `move_before` may anchor on the terminator, which appends to the end of the chain; the anchor is not nested in the moved statement; moving next to itself is a no-op | | `splice_block_before` / `splice_block_after` | Move every statement of `source` but its terminator, in order, adjacent to `anchor` in any block body; redirect `source`'s arguments to `arg_replacements`. `source` keeps its arguments and terminator and is not erased. | the `replace_all_uses` events, `SplicedBlock` | the anchor is block-parented, not in `source` and not nested in its statements; only `splice_block_before` may anchor on the terminator, which appends to the end of the chain; exact argument arity; replacements must be live and may be `source`'s own arguments | -| `insert_terminator` | Allocate `definition` as the terminator of a block that has none; add operand uses and predecessor entries. | `InsertedStatement`, `ChangedTerminator` | any block (`Cfg` member or statement-owned body); `definition` is a result-less terminator with live operands and successors | -| `insert_node` | Allocate `definition` as a new `DiGraph` member and node; bind its results, add operand uses and an edge from each member producer of an operand. | `InsertedStatement`, `ChangedDiGraphTopology` | live graph; non-terminator with no successors; live operands (outside ones get no edge); results must be `new_result` values | | `replace_yield` | Rewrite one `DiGraph` yield slot; move its yield use `old → new`. Topology is unchanged. | `ChangedDiGraphYields` (none if slot unchanged) | live graph and value, index in range | -| `insert_ungraph_node` | Allocate `definition` as a new `UnGraph` node; bind its results, add operand uses; each operand that is a wire of the graph becomes an endpoint, and a wire with two endpoints is an edge. | `InsertedStatement`, `ChangedUnGraphTopology` | live graph; non-terminator with no successors, not an edge statement; live operands; `new_result` results; a wire may exceed two endpoints (reported by `verify_derived`; it then has no edge) | -| `insert_ungraph_edge` | Allocate `definition` as a new `UnGraph` edge statement and append it to the member map; its results are new wires with no endpoints. | `InsertedStatement`, `ChangedUnGraphTopology` | live graph; an edge statement with no operands, not a terminator, no successors; `new_result` results | | `replace_statement` | Swap the definition in place, keeping id, position, and result SSA identity; fix operand uses and, for a terminator, the predecessor mirror, for a `DiGraph` member its incoming edges, or for an `UnGraph` node the wires it leaves and joins. Keeps body parents in step with the bodies the new definition names: bodies it drops are tombstoned with their contents, bodies it gains get the statement as parent. | `ReplacedStatement`, then `ChangedTerminator` for a terminator or `ChangedDiGraphTopology` / `ChangedUnGraphTopology` for a member whose edges changed; `ChangedBodyNesting` after `ReplacedStatement` when the bodies changed | parented by a block or graph; same result arity and terminator-ness as the original; a non-terminator replacement carries no successors; an `UnGraph` member keeps its edge role, and an edge statement has no operands; every gained body is live, owned by nothing else, named once, and does not enclose the statement | | `replace_results` | Redirect every result of a statement at once, one replacement per result. | one `ChangedOperands` per affected statement, then one `ReplacedUses` per redirected result | exact arity; rewrites uses only, never the result list | | `append_block` / `insert_block_before` / `insert_block_after` | Allocate an empty block (no terminator) with fresh typed arguments and link it into the `Cfg` block list. | `ChangedBlockList` | anchor must be a live block of a `Cfg` list (not a statement-owned body) | | `erase_block` | Unlink the block and tombstone it with its arguments, statements, results, and nested blocks/`Cfg`s; drop their uses and outgoing edges. | `ChangedBlockList`, `ChangedCfg` | rejects if another block still branches to it (a self-loop is fine) or an argument or top-level result is used outside the block; retarget those edges and replace those uses first | -| `split_block` | Move `at`, its successors in the chain, and the terminator into a new block placed after the original; the original is left without a terminator, for the caller to close with `insert_terminator`. | `ChangedBlockList`, `ChangedCfg` | new block has no arguments | +| `split_block` | Move `at`, its successors in the chain, and the terminator into a new block placed after the original; the original is left without a terminator, for the caller to close with `create` at `InsertionPoint::Terminator`. | `ChangedBlockList`, `ChangedCfg` | new block has no arguments | | `splice_cfg_after` | Move every block of `source`, in order, into `anchor`'s `Cfg` right after `anchor`; ids, arguments, statements, terminators and the edges between them are unchanged. `source` is left empty and not erased. | `ChangedBlockList` for `source`, then for the destination, then `SplicedCfg` | `source` live; `anchor` a live block of a `Cfg` list, not in `source` and not nested in its blocks; an empty `source` moves nothing | | `merge_blocks` | Erase `pred`'s terminator, redirect `succ`'s arguments to `arg_replacements`, splice `succ`'s statements and terminator onto `pred`, tombstone `succ`. | the `erase_statement` events, the `replace_all_uses` events, `ChangedBlockList`, `ChangedCfg` | same `Cfg`, distinct blocks, exact argument arity; replacements must be live and not `succ`'s own arguments; rejects if a block other than `pred` still branches to `succ`; `pred` need not branch to `succ` | | `retarget_successor` | Point every successor slot of `stmt` naming `old` at `new`. | `ChangedCfg` (none if nothing changed) | returns the count rewritten; `old` need not be live, `new` must be live | @@ -1992,20 +1986,18 @@ axis — hence the column. | Variant | Category | Meaning | Raised by | |---|---|---|---| | `UnknownStatement(stmt)` | liveness | id does not resolve to a live (non-tombstoned) statement | all statement methods | -| `UnknownValue(value)` | liveness | SSA id does not resolve to a live value | `replace_operand`, `replace_all_uses`, `insert_*`, `replace_statement`, `splice_block_*` | +| `UnknownValue(value)` | liveness | SSA id does not resolve to a live value | `replace_operand`, `replace_all_uses`, `create`, `replace_statement`, `splice_block_*` | | `UnknownBlock(block)` | liveness | a block id (anchor, operand block, or successor target) does not resolve to a live block | `replace_statement`, CFG block-list methods | | `UnknownCfg(cfg)` | liveness | the `Cfg` id does not resolve to a live `Cfg` | `append_block`, `splice_cfg_after` | -| `UnknownDiGraph(graph)` | liveness | the `DiGraph` id does not resolve to a live `DiGraph` | `insert_node`, `replace_yield` | +| `UnknownDiGraph(graph)` | liveness | the `DiGraph` id does not resolve to a live `DiGraph` | `create`/`insert` at `DiGraph`, `replace_yield` | | `YieldIndexOutOfRange { graph, index }` | liveness | yield index past the graph's yield list | `replace_yield` | -| `UnknownUnGraph(graph)` | liveness | the `UnGraph` id does not resolve to a live `UnGraph` | `insert_ungraph_node`, `insert_ungraph_edge` | -| `UnexpectedEdgeStatement` | guard | `insert_ungraph_node` was given an edge statement | `insert_ungraph_node` | -| `ExpectedEdgeStatement` | guard | `insert_ungraph_edge` was given a node | `insert_ungraph_edge` | -| `EdgeStatementWithOperands` | guard | an edge statement has operands; a wire consumes no values | `insert_ungraph_edge`, `replace_statement` | +| `UnknownUnGraph(graph)` | liveness | the `UnGraph` id does not resolve to a live `UnGraph` | `create`/`insert` at `UnGraph` | +| `EdgeStatementWithOperands` | guard | an edge statement has operands; a wire consumes no values | `create`/`insert` at `UnGraph`, `replace_statement` | | `NotInCfgBody(block)` | guard | the block is a statement-owned single-block body (or detached), not a member of a `Cfg` list | CFG block-list methods | | `CfgMismatch { block, expected }` | guard | the block belongs to a different `Cfg` than required | `merge_blocks` | | `CannotMergeBlockIntoItself(block)` | guard | `merge_blocks` was given the same block twice | `merge_blocks` | -| `ExpectedTerminator` | guard | the definition is not a terminator | `insert_terminator` | -| `BlockHasTerminator(block)` | guard | the block already has a terminator | `insert_terminator` | +| `ExpectedTerminator` | guard | the inserted statement is not a terminator | `create`/`insert` at `Terminator` | +| `BlockHasTerminator(block)` | guard | the block already has a terminator | `create`/`insert` at `Terminator` | | `BlockArgumentArityMismatch { block, expected, found }` | guard | values do not line up one-for-one with the block's arguments | `merge_blocks`, `splice_block_*` | | `BlockArgumentIndexOutOfRange { block, index }` | liveness | argument index past the block's argument list (for insertion, past its end) | `insert_block_arg`, `erase_block_arg` | | `ReplacementIsMergedArgument { block, value }` | guard | a replacement is one of the merged block's own arguments, which the merge deletes | `merge_blocks` | @@ -2014,13 +2006,14 @@ axis — hence the column. | `NoSuccessorOperands { stmt, successor }` | guard | an incoming edge's `BranchOp` gives no editable operand list for that successor | `insert_block_arg`, `erase_block_arg` | | `SuccessorOperandIndexOutOfRange { stmt, successor, index }` | guard | an incoming edge forwards too few operands for `index` (its arity disagrees with the block's) | `insert_block_arg`, `erase_block_arg` | | `OperandIndexOutOfRange { stmt, index }` | liveness | operand index past the statement's operand list | `replace_operand` | -| `NotInBlockBody(stmt)` | guard | for `insert_*`, the anchor is owned by a graph (or nothing), not a block; for `erase_statement` and `replace_statement`, the statement is owned by nothing | `erase_statement`, `insert_*`, `replace_statement` | +| `NotInBlockBody(stmt)` | guard | for `create`/`insert` at `Before`/`After`, the anchor is owned by a graph (or nothing), not a block; for `erase_statement` and `replace_statement`, the statement is owned by nothing | `erase_statement`, `create`/`insert`, `replace_statement` | | `StatementResultsInUse(stmt)` | guard | a result of the statement is still used elsewhere (for `erase_block`, outside the block); replace those uses first | `erase_statement`, `erase_block` | -| `AnchorIsTerminator(stmt)` | guard | nothing can follow a terminator, so `insert_after` cannot anchor on one | `insert_after` | -| `UnexpectedTerminator` | guard | the inserted definition is a terminator where a non-terminator is required (not spliced mid-block, use `insert_terminator`; a `DiGraph` has none) | `insert_*`, `insert_node` | -| `NonTerminatorWithSuccessors` | guard | the definition is a non-terminator carrying successors; only a terminator may transfer control | `insert_*`, `insert_node`, `replace_statement` | -| `CannotInsertTerminatorWithResults` | guard | the terminator declares results, which nothing could use | `insert_terminator` | -| `ResultNotFresh(value)` | guard | a result slot of the inserted definition is not a distinct, unbound `new_result` value of this `Rewriter` | `insert_*` | +| `AnchorIsTerminator(stmt)` | guard | nothing can follow a terminator, so `After` cannot anchor on one | `create`/`insert` at `After`, `move_after`, `splice_block_after` | +| `UnexpectedTerminator` | guard | the inserted statement is a terminator where a non-terminator is required (not spliced mid-block, use `InsertionPoint::Terminator`; graphs have none) | `create`/`insert` at `Before`/`After`/graphs, `move_*` | +| `NonTerminatorWithSuccessors` | guard | the definition is a non-terminator carrying successors; only a terminator may transfer control | `create`/`insert` at `Before`/`After`/graphs, `replace_statement` | +| `CannotInsertTerminatorWithResults` | guard | the terminator declares results, which nothing could use | `create`/`insert` at `Terminator` | +| `ResultNotFresh(value)` | guard | a result slot of the definition is not a distinct, unbound `new_result` value of this `Rewriter` | `create` | +| `NotDetached(stmt)` | guard | the statement is not a detached statement of this `Rewriter`: it is already in the IR, or already inserted | `insert` | | `ResultArityMismatch { stmt, expected, found }` | guard | replacement changes the result count, orphaning/inventing result SSA values | `replace_statement` | | `TerminatorKindMismatch(stmt)` | guard | replacement changes terminator-ness, desyncing the block's terminator cache | `replace_statement` | | `EdgeRoleMismatch(stmt)` | guard | the replacement of an `UnGraph` member changes `is_edge()`; nodes and edge statements are stored differently | `replace_statement` | @@ -2753,8 +2746,8 @@ covers `Block` **and** `Cfg` bodies, because a block inside a `Cfg` is still a - [x] Terminator surgery: erase/replace a terminator, maintaining `BlockInfo::terminator` and successor edges. Emits `ChangedTerminator`. `erase_statement` erases a terminator (leaving the block unterminated), - `replace_statement` replaces one, and `insert_terminator` closes a block - that has none. + `replace_statement` replaces one, and `create` at + `InsertionPoint::Terminator` closes a block that has none. - [x] Add the grouped operation-owned `HasCfgEdges` view that preserves each successor together with its forwarded arguments. Landed as the `BranchOp` trait (after MLIR's `BranchOpInterface`): a `Dialect` supertrait whose @@ -2767,22 +2760,23 @@ covers `Block` **and** `Cfg` bodies, because a block inside a `Cfg` is still a Emits `ChangedBlockList` / `ChangedCfg`. This is the capability [Level 5](#level-5-cfg-fragment) assumes and the one no earlier milestone named. Edge operands are edited through `BranchOp`. `split_block` leaves the - original block unterminated, and the caller closes it with - `insert_terminator`. + original block unterminated, and the caller closes it with `create` at + `InsertionPoint::Terminator`. - [x] `DiGraph` body surgery: insert/erase nodes with petgraph topology and - `yields` maintenance. Emits `ChangedDiGraphTopology`. Landed as - `insert_node`, member erasure through `erase_statement`, `replace_yield` - (`ChangedDiGraphYields`), tombstoning owned `DiGraph`s with their owner, and - edge maintenance in `replace_operand`, `replace_all_uses`/`replace_results`, - and `replace_statement` on members. The `StableGraph` swap and the derived - graph mirror landed afterwards (see the graph-body prerequisites). -- [x] `UnGraph` body surgery: insert/erase nodes and edge statements, maintaining - undirected topology, member order, and degree constraints. Emits - `ChangedUnGraphTopology`. Landed as `insert_ungraph_node`, - `insert_ungraph_edge`, member erasure through `erase_statement`, tombstoning - owned `UnGraph`s with their owner, wire resyncing in `replace_operand`, - `replace_all_uses`/`replace_results`, and `replace_statement`, and builder - member checks. The degree limit is not a + `yields` maintenance. Emits `ChangedDiGraphTopology`. Landed as `create` at + `InsertionPoint::DiGraph`, member erasure through `erase_statement`, + `replace_yield` (`ChangedDiGraphYields`), tombstoning owned `DiGraph`s with + their owner, and edge maintenance in `replace_operand`, + `replace_all_uses`/`replace_results`, and `replace_statement` on members. The + `StableGraph` swap and the derived graph mirror landed afterwards (see the + graph-body prerequisites). +- [x] `UnGraph` body surgery: insert/erase nodes and edge statements, + maintaining undirected topology, member order, and degree constraints. Emits + `ChangedUnGraphTopology`. Landed as `create` at `InsertionPoint::UnGraph` (a + node or an edge statement, as the definition says), member erasure through + `erase_statement`, tombstoning owned `UnGraph`s with their owner, wire + resyncing in `replace_operand`, `replace_all_uses`/`replace_results`, and + `replace_statement`, and builder member checks. The degree limit is not a per-edit guard: a wire may exceed two readers mid-pass (it then has no edge), and `verify_derived` reports `Finding::UnGraphDegreeExceeded`. @@ -2840,14 +2834,15 @@ policy. **Deferred out of M1:** - [x] Result-defining insertion — callers pre-allocate typed results with - `Rewriter::new_result` (an empty arena slot plus a type held by the - `Rewriter` until an insert binds it); inserts reject anything else with + `Rewriter::new_result` (an empty arena slot plus a type held by the `Rewriter` + until `create` binds it); `create` rejects anything else with `ResultNotFresh`. Dropping the `Rewriter` tombstones every value it never - bound, so afterwards each result slot is either bound or a real tombstone. Replacement-arena result SSA records already - carry the types to copy. `Rewriter` allocates fresh ids and checks mechanical - arity but makes no generic type-compatibility judgment; that belongs to rule - authoring or later verification. Generated field-wise remapping in M4 must - solve result allocation anyway. + bound and every detached statement it never inserted, so afterwards each + result slot is either bound or a real tombstone. Replacement-arena result SSA + records already carry the types to copy. `Rewriter` allocates fresh ids and + checks mechanical arity but makes no generic type-compatibility judgment; that + belongs to rule authoring or later verification. Generated field-wise + remapping in M4 must solve result allocation anyway. - [ ] `remove_result` — see [`remove_result` (speculative)](#remove_result-speculative--deferred-past-m1). - [ ] Independent non-panicking verifier — deferred outside the rewrite-engine diff --git a/docs/plans/kirin-ir/2026-11-04-remaining-ir-operations.md b/docs/plans/kirin-ir/2026-11-04-remaining-ir-operations.md deleted file mode 100644 index 981b050170..0000000000 --- a/docs/plans/kirin-ir/2026-11-04-remaining-ir-operations.md +++ /dev/null @@ -1,259 +0,0 @@ -# Remaining IR operations in the `kirin-ir` `Rewriter` - -Branch: `alex/remaining-ir-operations`, off `rust`. - -Adds the `Rewriter` operations needed for code motion and inlining: moving -statements and blocks, splicing whole bodies, copying bodies, and (as an -attempt) inlining a call. - -## Background - -### What exists - -| Body | Operations | -|---|---| -| block body | `erase_statement`, `insert_before` / `insert_after` (new statements only), `replace_statement`, `insert_terminator` | -| CFG | `append_block`, `insert_block_before` / `_after` (new empty blocks only), `erase_block`, `split_block`, `merge_blocks`, `retarget_successor`, `insert_block_arg`, `erase_block_arg` | -| values | `replace_operand`, `replace_all_uses`, `replace_results`, `new_result` | -| `DiGraph` | `insert_node`, `replace_yield` (removal through `erase_statement`) | -| `UnGraph` | `insert_ungraph_node`, `insert_ungraph_edge` (removal through `erase_statement`) | - -Nothing moves an existing statement or block, and nothing copies IR. The one -operation that moves IR is `merge_blocks`, which appends one block's -statements to the end of another block of the same CFG. - -### Cost model - -Each statement stores its parent block (`StatementParent::Block`), and each -block stores its parent CFG (`BlockParent::CFG`). Values store their defining -statement or block, and uses store the using statement. So: - -- moving a **statement** costs O(1): relink it, change its parent; -- moving a **block** costs O(1), and its statements don't change; -- moving the **statements of a block** into another block costs O(number - moved), one parent write each (this is why `split_block` costs O(statements - after the split point)); -- moving a **whole CFG's blocks** costs O(number of blocks). - -Ids never change when IR moves, so uses stay valid for free. A copy creates new -ids and must map every operand, which costs O(size of what is copied). - -### Comparison with MLIR - -| MLIR (`RewriterBase`, `Region`, `Operation`) | Kirin today | This plan | -|---|---|---| -| `moveOpBefore` / `moveOpAfter` | none | step 2 | -| `Block::moveBefore` | none | not in this plan (step 3, dropped) | -| `inlineBlockBefore(source, dest, before, args)` | `merge_blocks` (end of block, same CFG only) | step 4 | -| `inlineRegionBefore(region, parent, before)` | none | step 5 | -| `Operation::clone`, `Region::cloneInto` with `IRMapping` | none | step 6 | -| `inlineCall` + `DialectInlinerInterface` | none | step 7 (attempt) | -| `replaceUsesWithIf`, `replaceAllUsesExcept` | `replace_all_uses` only | step 1 | -| `Operation::isBeforeInBlock` | none | not in this plan (dominance comes later) | -| `Value::setType` | none | not in this plan | -| `replaceOpWithNewOp` | `replace_statement` (same result count) | not needed | -| `eraseOpResults` | `remove_result` deferred | not in this plan | - -## Steps (pause for review after each) - -Each step adds the operation to `Rewriter`, its rejections as `RewriteError` -variants where needed, its `MutationEvent`, tests in `kirin-ir`, and its row in -the operation table of `docs/design/rewrite-engine.md`. - -### 1. Replace only some uses - -```rust -rewriter.replace_uses_if(old, new, |stage: &StageInfo, site: Use| ...)?; -rewriter.replace_all_uses_except(old, new, stmt)?; -``` - -MLIR's `replaceUsesWithIf` / `replaceAllUsesExcept`. Like `replace_all_uses`, -but only the use sites the predicate accepts, or all but those of `stmt`. The -predicate gets the stage, so it can look up the using statement's parent or -neighbors; it runs on every use before anything is changed. CSE -and code motion need this when only some uses may change, e.g. only the uses -after a point. Same events as `replace_all_uses`, counting only the changed -slots. - -Cost: O(uses of `old`). - -### 2. Move a statement - -```rust -rewriter.move_before(stmt, anchor)?; -rewriter.move_after(stmt, anchor)?; -``` - -Same anchor rules as `insert_before` / `insert_after`: `move_before` a -terminator appends to the end of that block's statements; nothing can go after -a terminator. The anchor may be in any block, of any CFG or statement-owned -body. - -Example, hoisting out of an `scf.if` arm: - -``` -^entry: ^entry: - %r = if %c then { %t = neg %x // moved - %t = neg %x ==> %r = if %c then { - yield %t yield %t - } ... } ... -``` - -Rejects: -- `stmt` or `anchor` unknown; -- `stmt` is a terminator (terminators are not in the statement chain); -- `stmt` or `anchor` is a graph member; -- the anchor is inside `stmt`'s own nested bodies (moving an `scf.if` into its - own arm would make it contain itself). - -Not checked, like `insert_before`: that every use of `stmt`'s results still -comes after it. That is the caller's job. - -Records a new `MutationEvent::MovedStatement { stmt }`. `ErasedStatement` + -`InsertedStatement` would wrongly say the statement was tombstoned. - -**Walk check.** Moving the visited statement breaks pre-order, post-order and -BFS, which find the next statement through the visited one. Today the walk -only notices the `merge_blocks` case (the visited statement's old block was -erased). This step adds a check for moves, read from the `Rewriter`'s event -log, so nothing new is stored in the IR: - -- Every operation that moves IR records an event naming what moved: - `MovedStatement { stmt }` here, and the events of steps 3–5. -- `WalkCursor` gets a variant of `next` that takes the `Rewriter` instead of - the stage. At each visit the cursor remembers the visited statement, its - block, and the length of the event log. On its next step it scans only the - events recorded since, and reports `WalkError::VisitedMoved` if one moved the - visited statement or its block. When nothing was recorded, that is one - length comparison. -- `walk_mut` uses the new variant. The read-only `walk` and `schedule` keep - `next(stage)`, since nothing moves during them. -- The reverse BFS orders allow moves (their order is built at the start), so - they skip the check. -- `split_block` records no move event, so the split the contract allows still - passes. Moving an ancestor of the visited statement is not checked. - -Cost: O(1), plus O(nesting depth) for the cycle check. - -### 3. Move a block within its CFG (dropped) - -Not added. A CFG's block order has one semantic effect, choosing the entry -block (the first in the list); otherwise it only changes printing and -iteration order. A new entry block can already be made with -`insert_block_before` plus a branch, and steps 5 and 7 place blocks while -splicing, so nothing needs a separate move. Add it when block layout starts -to matter (e.g. a backend or an analysis seeded in reverse postorder). - -### 4. Splice a block's statements into another block - -```rust -rewriter.splice_block_before(source, anchor, &arg_replacements)?; -rewriter.splice_block_after(source, anchor, &arg_replacements)?; -``` - -MLIR's `inlineBlockBefore`. Moves `source`'s statements (not its terminator) -next to `anchor`, and redirects uses of `source`'s arguments to -`arg_replacements`. The anchor rules are those of `move_before` / -`move_after`. `splice_block_after` is for step 7, where the callee's entry is -spliced after the call, the last statement of an unterminated block. `source` is left empty, keeping its arguments and -terminator, and is not erased: the caller decides what to do with it. -Generalizes `merge_blocks`: any position, in any block of any CFG or -statement-owned body. Records a new `SplicedBlock { source, dest }`, which the -walk check reads as "every statement of `source` moved". Afterwards a spliced -statement's parent is `dest`, so the check also takes the visited statement's -parent from before the visit, and compares it with `source`. - -Rejects an anchor in `source` or nested in one of its statements -(`SplicedIntoItself`). - -Example, folding `scf.if` with a known condition: splice the `then` arm in -front of the `if`, replace the `if`'s results with the arm's `yield` values, -erase the `if`. - -Cost: O(statements moved). - -### 5. Splice a whole CFG into another - -```rust -rewriter.splice_cfg_after(source, anchor_block)?; -``` - -MLIR's `inlineRegionBefore`. Moves every block of `source` into `anchor_block`'s -CFG, right after `anchor_block`. Branches between the moved blocks stay valid, -since they all move together. `source` is left empty and not erased; its -owner (e.g. the callee function) still names it, and the caller decides -whether to erase the owner. Records `ChangedBlockList` for both CFGs and a new -`SplicedCfg { source, dest }`, which the walk check reads as "every block of -`source` moved". Afterwards a moved block's parent is `dest`, so, as in step 4, -the check compares against where the visited statement was before the visit: -a `VisitedPlace` holding its block and that block's CFG. -`ChangedBlockList` alone could not drive the check, since `split_block`, which -a walk allows, records it too. - -Rejects an anchor block in `source` or nested in one of its blocks -(`CfgSplicedIntoItself`). There is no `_before` variant and no way to splice -into an empty CFG, which has no anchor; inlining needs neither. - -Cost: O(blocks moved). This is the move-instead-of-copy path for inlining a -callee's last use. - -### 6. Copy a statement or a CFG - -```rust -let mut map = IrMap::default(); // MLIR's IRMapping -let copy = rewriter.clone_before(stmt, anchor, &mut map)?; -let blocks = rewriter.clone_cfg_after(source, anchor_block, &mut map)?; -``` - -A deep copy with fresh ids: statements, results, blocks, block arguments and -nested bodies, including `DiGraph` and `UnGraph` bodies. A copied graph gets -new ports, copied members and yields, and its topology is rebuilt from the -copied operands with the helpers `insert_node` / `insert_ungraph_node` -already use. Every operand and branch target inside the copy is mapped -through `map`. A value from outside the copied IR keeps pointing at the -original. The caller can pre-seed `map`, e.g. to map the callee's entry -arguments to the call's operands. `map` afterwards holds old → new for every -value and block copied, so the caller can find the copied `ret`s. - -Cost: O(size of what is copied). - -### 7. Inline a call (attempt) - -An inliner in `kirin-function`, which knows its own call and `ret` ops, built -from steps 4–6 and the existing `kirin-ir` operations. It takes a closure that -builds the branch op, since `kirin-function` doesn't know which branch op a -language uses. It inlines `CallSpecialized` calls within the same stage; the -other call kinds first need resolving to a specialization. For a call `c` in block `B`: - -1. `split_block` after `c`. The continuation block `C` gets the rest of `B`. -2. Copy the callee's CFG between `B` and `C`, mapping its entry arguments to - `c`'s operands (step 6). For the callee's last use, splice instead (step 5). -3. Splice the copied entry block into `B` (step 4), or branch to it. -4. Replace each copied `ret v` with a branch to `C` passing `v`. -5. Give `C` one argument per result of `c`, redirect `c`'s results to them, and - erase `c`. - -Cost per call: O(statements after `c` in `B`) for the split, plus O(callee -size) for the copy. Inlining a block's calls from last to first makes the -splits cost O(block size) in total, since each statement moves at most once. - -### 8. Docs - -`Rewriter` module docs, the design doc's operation table, and the -`MutationEvent` count in Gap 3. The `kirin_ir::walk` contract gets a line on -the new moves. - -## Decisions - -1. Moves record new events naming what moved: `MovedStatement`, - `SplicedBlock`, `SplicedCfg`. -2. The walk checks for a moved visited statement or block by reading the - `Rewriter`'s event log (step 2). Ancestors are not checked. -3. Moving a block within its CFG is not added (step 3). -4. A block's statements can be spliced into any block. -5. Splicing leaves the source block or CFG empty and does not erase it. -6. Copying includes graph bodies. -7. Inlining lives in `kirin-function`, with a closure building the branch. -8. Only `CallSpecialized` within the same stage is inlined. -9. Of the extra MLIR operations, only `replace_uses_if` / - `replace_all_uses_except` (step 1). Their predicate gets the stage. diff --git a/tests/roundtrip/ungraph.rs b/tests/roundtrip/ungraph.rs index 6ad27a3147..5949f1a89d 100644 --- a/tests/roundtrip/ungraph.rs +++ b/tests/roundtrip/ungraph.rs @@ -214,7 +214,10 @@ fn test_ungraph_wire_inserted_after_its_reader_roundtrips() { let mut rw = kirin::ir::Rewriter::new(stage); let res = rw.new_result(SimpleType::Any); let wire = rw - .insert_ungraph_edge(ug, UngraphTestLanguage::Wire(Wire { res })) + .create( + InsertionPoint::UnGraph(ug), + UngraphTestLanguage::Wire(Wire { res }), + ) .unwrap(); rw.replace_operand(reader, 1, res).unwrap(); drop(rw);