diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index ece86535c..49aeceafe 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 fe2fad9e2..000000000 --- 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/cfg/split.rs b/crates/kirin-ir/src/rewrite/cfg/split.rs index feb667694..89d66f5e5 100644 --- a/crates/kirin-ir/src/rewrite/cfg/split.rs +++ b/crates/kirin-ir/src/rewrite/cfg/split.rs @@ -11,9 +11,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 0a18864ba..e21bb83d4 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 16ed9864c..07ea97588 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 000000000..4d8807379 --- /dev/null +++ b/crates/kirin-ir/src/rewrite/insert.rs @@ -0,0 +1,335 @@ +//! 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::LinkedListNode; +use crate::{ + Block, DiGraph, Dialect, SSAValue, Statement, StatementInfo, StatementParent, UnGraph, Use, +}; + +use crate::rewrite::rewriter::{MutationEvent, RewriteError, Rewriter, add_predecessor, add_use}; + +/// 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`]). + /// + /// 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`. 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)?; + self.place(stmt, placement); + Ok(()) + } + + /// Check that a statement with `definition` fits `insertion_pt`. + fn check_insertion_pt( + &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 after = matches!(insertion_pt, InsertionPoint::After(_)); + let (block, prev, next) = self.slot_at(anchor, after)?; + 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 operands: Vec = stmt + .expect_info(self.stage) + .definition + .arguments() + .copied() + .collect(); + 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_into_block(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 operands { + 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, 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::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 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/mod.rs b/crates/kirin-ir/src/rewrite/mod.rs index 9973e5405..cf225c6bf 100644 --- a/crates/kirin-ir/src/rewrite/mod.rs +++ b/crates/kirin-ir/src/rewrite/mod.rs @@ -19,10 +19,10 @@ //! 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; mod moves; pub(crate) mod pass; #[cfg(test)] @@ -33,6 +33,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 5eced7ea9..2705feee6 100644 --- a/crates/kirin-ir/src/rewrite/moves.rs +++ b/crates/kirin-ir/src/rewrite/moves.rs @@ -9,12 +9,13 @@ 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 before itself does nothing. /// - /// 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 /// or not in a block body ([`RewriteError::NotInBlockBody`]), if `stmt` is diff --git a/crates/kirin-ir/src/rewrite/results.rs b/crates/kirin-ir/src/rewrite/results.rs index 63ffb823f..254c0f21b 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 8114b995f..b642797e8 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,7 +80,7 @@ use std::collections::HashSet; use std::fmt; use indexmap::{IndexMap, IndexSet}; -use rustc_hash::{FxBuildHasher, FxHashMap}; +use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use crate::arena::{GetInfo, GetInfoMut}; use smallvec::SmallVec; @@ -170,28 +169,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 @@ -213,12 +221,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, @@ -257,7 +259,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 { @@ -322,10 +326,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) => { @@ -365,18 +375,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") } @@ -504,6 +502,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> { @@ -512,6 +513,7 @@ impl<'a, L: Dialect> Rewriter<'a, L> { stage, events: Vec::new(), unbound_ssa_results: FxHashMap::default(), + detached: FxHashSet::default(), } } @@ -943,9 +945,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. /// @@ -1129,35 +1131,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, false) - } - - /// 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, true) - } - /// Where a statement goes when placed before (`after == false`) or after /// `anchor`: its block, and the chain statements that will sit directly /// before and after it (`None` at either end of the chain). @@ -1277,97 +1250,6 @@ impl<'a, L: Dialect> Rewriter<'a, L> { } } - fn insert_relative( - &mut self, - anchor: Statement, - definition: L, - after: bool, - ) -> Result { - let (block, prev, next) = self.slot_at(anchor, after)?; - - 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_into_block(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 b82f49a41..b63446707 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/tests/common.rs b/crates/kirin-ir/tests/common.rs index db2ed238f..cbcea1a60 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 diff --git a/crates/kirin-ir/tests/derived.rs b/crates/kirin-ir/tests/derived.rs index 1baffda5a..8fee0a76e 100644 --- a/crates/kirin-ir/tests/derived.rs +++ b/crates/kirin-ir/tests/derived.rs @@ -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,7 +739,7 @@ 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), + rewriter.create(InsertionPoint::After(f.terminator), BuilderDialect::Nop), Err(RewriteError::AnchorIsTerminator(f.terminator)) ); } diff --git a/crates/kirin-ir/tests/rewriter.rs b/crates/kirin-ir/tests/rewriter.rs index 1917971a1..6a7f3ecae 100644 --- a/crates/kirin-ir/tests/rewriter.rs +++ b/crates/kirin-ir/tests/rewriter.rs @@ -419,7 +419,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 +477,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 +507,46 @@ 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), + rw.create(InsertionPoint::After(ret), BuilderDialect::Nop), Err(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), + rw.create(InsertionPoint::Before(s0), BuilderDialect::Return), Err(RewriteError::UnexpectedTerminator) ); // Only a terminator may carry successors. assert_eq!( - rw.insert_after(s0, BuilderDialect::Fork(Successor::from_block(block))), + rw.create( + InsertionPoint::After(s0), + BuilderDialect::Fork(Successor::from_block(block)) + ), Err(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 +583,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 +627,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 +673,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 1940c326a..329725ef0 100644 --- a/crates/kirin-ir/tests/rewriter_cfg.rs +++ b/crates/kirin-ir/tests/rewriter_cfg.rs @@ -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,32 +619,41 @@ fn insert_terminator_rejects_without_mutating() { let mut rw = Rewriter::new(&mut stage); assert_eq!( - rw.insert_terminator(terminated, BuilderDialect::Return), + rw.create( + InsertionPoint::Terminator(terminated), + BuilderDialect::Return + ), Err(RewriteError::BlockHasTerminator(terminated)) ); assert_eq!( - rw.insert_terminator(open, BuilderDialect::Nop), + rw.create(InsertionPoint::Terminator(open), BuilderDialect::Nop), Err(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)), + rw.create( + InsertionPoint::Terminator(open), + BuilderDialect::Invoke(result) + ), Err(RewriteError::CannotInsertTerminatorWithResults) ); assert_eq!( - rw.insert_terminator(open, br(dead_block)), + rw.create(InsertionPoint::Terminator(open), br(dead_block)), Err(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), + rw.create( + InsertionPoint::Terminator(dead_block), + BuilderDialect::Return + ), Err(RewriteError::UnknownBlock(dead_block)) ); @@ -893,7 +902,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 e5bc6129d..c7b8d4614 100644 --- a/crates/kirin-ir/tests/rewriter_digraph.rs +++ b/crates/kirin-ir/tests/rewriter_digraph.rs @@ -77,13 +77,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 +141,40 @@ 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), + rw.create(InsertionPoint::DiGraph(dead_graph), BuilderDialect::Nop), Err(RewriteError::UnknownDiGraph(dead_graph)) ); assert_eq!( - rw.insert_node(dg, BuilderDialect::Return), + rw.create(InsertionPoint::DiGraph(dg), BuilderDialect::Return), Err(RewriteError::UnexpectedTerminator) ); assert_eq!( - rw.insert_node(dg, BuilderDialect::Fork(Successor::from_block(outer))), + rw.create( + InsertionPoint::DiGraph(dg), + BuilderDialect::Fork(Successor::from_block(outer)) + ), Err(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 2d99f750e..a5e345fca 100644 --- a/crates/kirin-ir/tests/rewriter_ungraph.rs +++ b/crates/kirin-ir/tests/rewriter_ungraph.rs @@ -136,16 +136,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 +167,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 +212,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 +239,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 +290,49 @@ fn ungraph_inserts_reject_without_mutating() { let fresh = rw.new_result(TestType::Qubit); let rejections = [ ( - rw.insert_ungraph_node(dead_graph, BuilderDialect::Isolated), + rw.create( + InsertionPoint::UnGraph(dead_graph), + BuilderDialect::Isolated, + ), RewriteError::UnknownUnGraph(dead_graph), ), ( - rw.insert_ungraph_edge(dead_graph, BuilderDialect::Wire(fresh)), + rw.create( + InsertionPoint::UnGraph(dead_graph), + BuilderDialect::Wire(fresh), + ), RewriteError::UnknownUnGraph(dead_graph), ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Return), + rw.create(InsertionPoint::UnGraph(ug), BuilderDialect::Return), RewriteError::UnexpectedTerminator, ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Fork(Successor::from_block(dead_block))), + rw.create( + InsertionPoint::UnGraph(ug), + BuilderDialect::Fork(Successor::from_block(dead_block)), + ), RewriteError::NonTerminatorWithSuccessors, ), ( - rw.insert_ungraph_node(ug, BuilderDialect::Wire(fresh)), - RewriteError::UnexpectedEdgeStatement, - ), - ( - 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.insert_ungraph_edge(ug, BuilderDialect::ParamWire(w, fresh)), + rw.create( + InsertionPoint::UnGraph(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 +360,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 +382,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 +410,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 +443,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 +542,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 +602,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 +624,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 +652,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 +673,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 +704,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 +770,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 +817,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 +862,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 +911,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 da0d84be6..32a041c2a 100644 --- a/crates/kirin-ir/tests/walk.rs +++ b/crates/kirin-ir/tests/walk.rs @@ -244,7 +244,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(), ); } @@ -266,7 +266,11 @@ fn post_order_walks_siblings_inserted_after_the_visited_statement() { let mut inserted = None; let visited = walk_editing::(&mut t, |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(); @@ -280,7 +284,9 @@ fn visits_with_insert_after_a() -> usize { let a = t.a; walk_editing::(&mut t, |rewriter, stmt| { if stmt == a { - rewriter.insert_after(a, BuilderDialect::Nop).unwrap(); + rewriter + .create(InsertionPoint::After(a), BuilderDialect::Nop) + .unwrap(); } }) .unwrap() @@ -332,12 +338,24 @@ fn bfs_walks_statements_inserted_at_the_current_level_and_below() { visited.push(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(), + ); } } let (n, z, h) = (n.unwrap(), z.unwrap(), h.unwrap()); @@ -620,7 +638,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 83162f757..1a155efae 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 index 981b05017..49fb75b10 100644 --- a/docs/plans/kirin-ir/2026-11-04-remaining-ir-operations.md +++ b/docs/plans/kirin-ir/2026-11-04-remaining-ir-operations.md @@ -12,11 +12,11 @@ attempt) inlining a call. | Body | Operations | |---|---| -| block body | `erase_statement`, `insert_before` / `insert_after` (new statements only), `replace_statement`, `insert_terminator` | +| block body | `erase_statement`, `create` at `Before` / `After` (new statements only), `replace_statement`, `create` at `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`) | +| `DiGraph` | `create` at `DiGraph`, `replace_yield` (removal through `erase_statement`) | +| `UnGraph` | `create` at `UnGraph`, for nodes and edge statements (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 @@ -84,7 +84,7 @@ rewriter.move_before(stmt, anchor)?; rewriter.move_after(stmt, anchor)?; ``` -Same anchor rules as `insert_before` / `insert_after`: `move_before` a +Same anchor rules as `InsertionPoint::Before` / `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. @@ -106,7 +106,7 @@ Rejects: - 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 +Not checked, like `create`: that every use of `stmt`'s results still comes after it. That is the caller's job. Records a new `MutationEvent::MovedStatement { stmt }`. `ErasedStatement` + @@ -199,21 +199,56 @@ callee's last use. ### 6. Copy a statement or a CFG +As in MLIR, copying is separate from inserting, and creating a new statement +is one call at an insertion point: + ```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)?; +let s = rewriter.create(InsertionPoint::Before(anchor), definition)?; // build + place +let mut map = IrMap::default(); // MLIR's IRMapping +let copy = rewriter.clone_statement(stmt, &mut map)?; // detached deep copy +rewriter.insert(InsertionPoint::After(anchor), copy)?; // place a detached statement +let blocks = rewriter.clone_cfg_after(source, anchor_block, &mut map)?; // like Region::cloneInto ``` -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. +- **`InsertionPoint`** says where a statement goes: `Before(stmt)`, + `After(stmt)`, `Terminator(block)`, `DiGraph(graph)`, `UnGraph(graph)`. An + `UnGraph` member is an edge statement or a node as its definition says + (`IsEdge`). +- **`create`** checks the point against the definition, then builds and places + the statement; a rejection leaves the stage untouched. +- **`insert`** places a detached statement of this `Rewriter`, one it made + but has not placed, such as a copy (`NotDetached` otherwise). Detached + statements never inserted are tombstoned when the `Rewriter` is dropped. + Building a detached statement from a definition is internal to `create`. + `move_before` / `move_after` still require a statement that is already in a + block. +- **`clone_statement`** makes a detached deep copy with fresh ids: + statements, results, blocks, block arguments and nested bodies. Every + operand and branch target inside the copy is mapped through `map`; a value + from outside keeps pointing at the original unless `map` was pre-seeded. + Terminators are not cloned, since a detached terminator could not be + inserted into a statement chain. Records no event. +- **`clone_cfg_after`** copies a CFG's blocks into place after + `anchor_block`, as MLIR's `Region::cloneInto` does (a region cannot be + detached). Rejects an `anchor_block` inside `source`. Records + `ChangedBlockList` for the destination. +- **A block argument already in `map` is not copied.** Its uses inside the copy + read the mapped value. Pre-seeding the callee entry block's arguments with a + call's operands gives a copied entry block with no arguments, ready to + splice into the call's block (step 7). +- Names are not copied; symbols are. +- `map` afterwards holds old → new for every value and block copied, so the + caller can find the copied `ret`s. + +Split in two: + +- **6a**: `InsertionPoint` with `create` / `insert`, then `IrMap`, + `clone_statement` and `clone_cfg_after` over block and CFG bodies. A + statement owning a `DiGraph` or `UnGraph` body is rejected + (`CopyOfGraphBody`). +- **6b**: graph bodies. A copied graph gets new ports, copied members and + yields, and its topology is rebuilt from the copied operands with the + helpers `create` uses at `DiGraph` / `UnGraph` points. Cost: O(size of what is copied). diff --git a/tests/roundtrip/ungraph.rs b/tests/roundtrip/ungraph.rs index 6ad27a314..5949f1a89 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);