diff --git a/AGENTS.md b/AGENTS.md index 0d02fab560..15104ea739 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - `kirin-derive-chumsky` — `#[derive(HasParser, PrettyPrint)]` (proc-macro + code generation) **Interpreter:** -- `kirin-interpreter` — interpreter framework. Shared pieces: `Interp`, `Interpretable`, `Frame`/`drive_frames`, and the owner-summary fixpoint driver (`StandardFixpointInterpreter`). **Semantics vs shape**: dialect rules dispatch on a `SemanticKey` (`ForwardEval`, `StrongDemand`, `ClassicLiveness`, or a downstream key — the Rust analogue of Kirin 1.0's string keys like `"main"`/`"typeinfer"`/`"constprop"`/`"qubit.address"`); each key declares the `AnalysisShape` its solver runs on (`SparseForwardShape`/`SparseBackwardShape`/`DenseForwardShape`/`DenseBackwardShape` — mechanics only, never dispatch tags). Two keys may share one shape. Each key joins its shape's *family* (`SparseForwardSemantic`/`SparseBackwardSemantic`/`DenseForwardSemantic`/`DenseBackwardSemantic`), and the engines/transfers are generic over the key with the canonical default (`SparseForwardTransfer<..., Sem = ForwardEval>`, `SparseBackwardInterpreter<..., Sem = StrongDemand>`, `DenseBackwardInterpreter<..., Sem = ClassicLiveness>`), so a downstream key reuses an engine by instantiating `Sem`. Engine traits are shape-generic mechanics (`SparseForwardInterp` read/write; `SparseBackwardInterp` fact/`raise_fact`/effect/topology; `DenseBackwardInterp` insert/remove point facts); semantics-specific helper vocabulary lives in key-pinned helper traits (`DemandInterp`: `demand`/`is_demanded`/`demand_uses_if_observable`; `ClassicLivenessInterp`: `gen_live`/`kill_def`/`gen_uses_kill_defs`) — demand rules bind `DemandInterp`, classic-liveness rules bind `ClassicLivenessInterp`. Effects per shape: `SparseForwardEffect`, `SparseBackwardEffect`, `DenseBackwardEffect`; engines: `ConcreteInterpreter`, `SparseForwardInterpreter`, `SparseBackwardInterpreter`, `DenseBackwardInterpreter`. `InterpDispatch` is keyed on the engine alone — the dispatched key is always `I::Semantics`, so a stage can never be paired with a foreign key's rules. `DenseForwardShape` (typestate) has no key yet. `AbstractInterpreter` is the marker trait for lattice-valued engines. **Source layout** (public API is unchanged — everything re-exports through `lib.rs` plus the `dialect`/`engine` preludes): `core/` (chassis: `Interp`/dispatch/effects/frame protocol/env/error/linker/queries), `semantics/` (`keys.rs` + `shape.rs`), `facts/` (`anchor.rs`/`store.rs`/`topology.rs`), `fixpoint/` (convergence driver), `engines/` (`concrete/`, `sparse_forward/`, `sparse_backward/`, `dense_backward/`, each `interp.rs` + optional `frames.rs`). +- `kirin-interpreter` — interpreter framework. Shared pieces: `Interp`, `Interpretable`, `Frame`/`drive_frames`, and the owner-summary fixpoint driver (`StandardFixpointInterpreter`). **Semantics vs shape**: dialect rules dispatch on a `SemanticKey` (`ForwardEval`, `StrongDemand`, `ClassicLiveness`, or a downstream key — the Rust analogue of Kirin 1.0's string keys like `"main"`/`"typeinfer"`/`"constprop"`/`"qubit.address"`); each key declares the `AnalysisShape` its solver runs on (`SparseForwardShape`/`SparseBackwardShape`/`DenseForwardShape`/`DenseBackwardShape` — mechanics only, never dispatch tags). Two keys may share one shape. Each key joins its shape's *family* (`SparseForwardSemantic`/`SparseBackwardSemantic`/`DenseForwardSemantic`/`DenseBackwardSemantic`), and the engines/transfers are generic over the key with the canonical default (`SparseForwardTransfer<..., Sem = ForwardEval>`, `SparseBackwardInterpreter<..., Sem = StrongDemand>`, `DenseBackwardInterpreter<..., Sem = ClassicLiveness>`), so a downstream key reuses an engine by instantiating `Sem`. Engine traits are shape-generic mechanics (`SparseForwardInterp` read/write; `SparseBackwardInterp` fact/`raise_fact`/effect/topology; `DenseBackwardInterp` opaque `point_state`/`point_state_mut`); semantics-specific helper vocabulary lives in key-pinned helper traits (`DemandInterp`: `demand`/`is_demanded`/`demand_uses_if_observable`; `ClassicLivenessInterp`: `gen_live`/`kill_def`/`gen_uses_kill_defs`) — demand rules bind `DemandInterp`, classic-liveness rules bind `ClassicLivenessInterp`. **The shape layer never says what a fact is**: `raise_fact` takes the lattice element to merge, the dense point state is opaque, and the only state contracts the engines and dialect frames name are `Lattice` (merges) plus `DenseBackwardState` (`rename`/`forget`, for CFG edges and `scf.for`'s back-edge). Fact-shaped contracts are key-pinned instead — `HasTop` on `DemandInterp`, `PointFacts` on `ClassicLivenessInterp` — carried as associated-type bounds in the supertrait so elaboration keeps dialect rules from spelling them. Effects per shape: `SparseForwardEffect`, `SparseBackwardEffect`, `DenseBackwardEffect`; engines: `ConcreteInterpreter`, `SparseForwardInterpreter`, `SparseBackwardInterpreter`, `DenseBackwardInterpreter`. `InterpDispatch` is keyed on the engine alone — the dispatched key is always `I::Semantics`, so a stage can never be paired with a foreign key's rules. `DenseForwardShape` (typestate) has no key yet. `AbstractInterpreter` is the marker trait for lattice-valued engines. **Source layout** (public API is unchanged — everything re-exports through `lib.rs` plus the `dialect`/`engine` preludes): `core/` (chassis: `Interp`/dispatch/effects/frame protocol/env/error/linker/queries), `semantics/` (`keys.rs` + `shape.rs`), `facts/` (`anchor.rs`/`store.rs`/`topology.rs`), `fixpoint/` (convergence driver), `engines/` (`concrete/`, `sparse_forward/`, `sparse_backward/`, `dense_backward/`, each `interp.rs` + optional `frames.rs`). **Dialects:** - `kirin-cf`, `kirin-scf`, `kirin-constant`, `kirin-arith`, `kirin-bitwise`, `kirin-cmp`, `kirin-function` @@ -149,15 +149,23 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Dialects are engine-blind**: one `Interpretable` impl serves concrete execution and abstract interpretation; the value domain decides. Undecided conditions (`BranchCondition::is_truthy` / `ForLoopValue::loop_condition` returning `None`) are read in the rule and handed to the dialect's own frame, which rejects them under concrete execution and explores+joins under abstract. (`Branch` is the cf CFG analogue, driven by the engine's CFG frame.) Never write per-engine dialect impls — but a control dialect's *frame* may have distinct concrete/abstract forms, built per-engine through a dialect dispatch trait. -- **Ordinary vs control dialects (frame ownership)**: Ordinary dialects (arith, cmp, constant, bitwise, tuple, ordinary cf branch ops) implement statement-local semantics with the `SparseForwardInterp` helpers and **never see frames**. A dialect whose operations own *structured traversal* defines **dialect-owned frames** and pushes them with `SparseForwardEffect::Push`. The framework's `BodyFrame` / `AbstractBlockFrame` (single-block body walkers) are reusable **building blocks**, not framework-owned structured semantics — a dialect frame may build one to walk a chosen body, but the structured *decision* and result binding stay in the dialect frame. +- **Ordinary vs control dialects (frame ownership)**: Ordinary dialects (arith, cmp, constant, bitwise, tuple, ordinary cf branch ops) implement statement-local semantics with the `SparseForwardInterp` helpers and **never see frames**. A dialect whose operations own *structured traversal* defines **dialect-owned frames** and pushes them with `SparseForwardEffect::Push`. The framework's `BlockFrame` / `AbstractBlockFrame` (single-block body walkers) are reusable **building blocks**, not framework-owned structured semantics — a dialect frame may build one to walk a chosen body, but the structured *decision* and result binding stay in the dialect frame. - **SCF is the example**: `scf.if` → `kirin_scf::ScfIfFrame` (concrete) / `AbstractScfIfFrame` (abstract); `scf.for` → `ScfForFrame` / `AbstractScfForFrame`. Each is built per-engine through a dialect dispatch trait (`ScfIfDispatch`/`ScfForDispatch`) and returned as `SparseForwardEffect::Push`. The if frame owns picking the arm (concrete) or exploring both arms + joining (abstract); the for frame owns the loop-carried join/widen fixpoint. A language that uses SCF composes a total frame type embedding the standard frames plus `ScfIfFrame`/`ScfForFrame` (via `BuildScfIf`/`BuildScfFor` and the abstract equivalents); see `example/toy-lang`'s `ToyFrame`/`ToyAbstractFrame`. (Future structured dialects would follow the same pattern; only the existing SCF ops are implemented.) - **Calling conventions are linkers**: `Linker` resolves `Callee` to a `(stage, specialization, body)` target and is passed to engines by value (`.with_linker(..)`). `SameStageLinker` is the default; `CrossStageLinker` routes calls to whichever stage has a live specialization, which is all that cross-language execution *and* cross-language analysis require. Policy must be a component (field), never a trait impl on an engine type. -- **Engines run frames; traversal lives in frames**: both engines share one driver loop, `drive_frames` (`core/frame.rs`), over the direction-neutral `Frame` protocol — pop the top frame, `step`, apply the returned `FrameEffect`, owning no traversal logic. `FrameEngine` is the minimal anchor (just a total `Error`); every `Interp` is a `FrameEngine` by blanket impl, so frames are decoupled from the forward eval engine and stay reusable. `ConcreteInterpreter<'ir, S, V, E, Lk, F = StandardFrame>` uses the concrete standard frames (`engines/concrete/frames.rs`: `BodyFrame`/`CallFrame`, single-path). `SparseForwardInterpreter<'ir, S, V, E, Lk, P = ContextInsensitive, F = StandardAbstractFrame>` (the forward abstract engine, implementing the `AbstractInterpreter` trait) uses the forward abstract standard frames (`engines/sparse_forward/frames.rs`: `AbstractBlockFrame`/`AbstractCallFrame`) over the shared owner-summary fixpoint driver — block owners with widening, `Branch` exploration, and per-key interprocedural summaries (caller re-enqueueing incl. same-key self-recursion). The backward engines (`SparseBackwardInterpreter`, `DenseBackwardInterpreter`) implement the same `AbstractInterpreter` trait with their own fact stores/effects/frames, wrapping the same fixpoint driver around their own summary-free `*Transfer` inner `Interp`s. The default `StandardFrame`/`StandardAbstractFrame` are structured-control-free; a language with a structured dialect supplies a custom `F` embedding the standard frames (via `FrameBuild`/`AbstractFrameBuild`) plus that dialect's frames. Analysis crates are a lattice + a policy/frame choice + an engine type alias (see `kirin-constprop` for the forward shape and `kirin-liveness` for the two backward shapes). +- **Engines run frames; traversal lives in frames**: both engines share one driver loop, `drive_frames` (`core/frame.rs`), over the direction-neutral `Frame` protocol — pop the top frame, `step_into`, apply the returned `FrameEffect`, owning no traversal logic. **One trait covers both roles**: a *member* (an individual walker) is generic over the total frame type `F` it composes into and names its successors in `F`; a *universe* (a language's total frame enum) implements `Frame` when it is the stack's element type but stays generic over `F`, so it can itself be embedded in a larger enum — `drive_frames` bounds on `F: Frame`. That is how `toy-lang`'s `TracingFrame` is a newtype wrapping `ToyFrame` whole rather than a copy of its variants. `FrameEngine` is the minimal anchor (just a total `Error`); every `Interp` is a `FrameEngine` by blanket impl, so frames are decoupled from the forward eval engine and stay reusable. `ConcreteInterpreter<'ir, S, V, E, Lk, F = StandardFrame>` uses the concrete standard frames (`engines/concrete/frames/`: the representation walkers `BlockFrame`/`CFGFrame`/`DiGraphFrame` plus the `CallFrame` call boundary, single-path; `UnGraph` traversal has no framework default — a compiler supplies it via `FrameBuild::from_ungraph_entry`). `SparseForwardInterpreter<'ir, S, V, E, Lk, P = ContextInsensitive, F = StandardAbstractFrame>` (the forward abstract engine, implementing the `AbstractInterpreter` trait) uses the forward abstract standard frames (`engines/sparse_forward/frames.rs`: `AbstractBlockFrame`/`AbstractCallFrame`) over the shared owner-summary fixpoint driver — block owners with widening, `Branch` exploration, and per-key interprocedural summaries (caller re-enqueueing incl. same-key self-recursion). The backward engines (`SparseBackwardInterpreter`, `DenseBackwardInterpreter`) implement the same `AbstractInterpreter` trait with their own fact stores/effects/frames, wrapping the same fixpoint driver around their own summary-free `*Transfer` inner `Interp`s. The default `StandardFrame`/`StandardAbstractFrame` are structured-control-free; a language with a structured dialect supplies a custom `F` embedding the standard frames (via `FrameBuild`/`AbstractFrameBuild`) plus that dialect's frames. Analysis crates are a lattice + a policy/frame choice + an engine type alias (see `kirin-constprop` for the forward shape and `kirin-liveness` for the two backward shapes). -- **Customizing traversal**: `core/frame.rs` defines the shared `Frame`/`FrameEffect`/`drive_frames` protocol plus forward driver traits. Concrete custom frames embed `BodyFrame`/`CallFrame` via `FrameBuild`; forward abstract custom frames embed `Abstract*Frame`s via `AbstractFrameBuild`. Structured dialects may push dialect-owned frames with `SparseForwardEffect::Push`; ordinary dialects never name frame types. Abstract summary keying and join/widen policy stay in `CallContext`/`WideningStrategy`. +- **Callable-body walkers are a concrete policy**: `CallFrame` owns the call convention (resolve, allocate the activation, enter, suspend, validate the completion, free exactly once, bind results) and delegates *only* which walker enters the callee body to `CallBodyFramePolicy`, selected via `FrameBuild::BodyFrames` (default `DefaultBodyFrames`: `CFG`→`CFGFrame`, `Block`→`BlockFrame`, `DiGraph`→`DiGraphFrame`, `UnGraph`→`FrameBuild::from_ungraph_entry`). `CallFrame` still means `CallFrame`, so no existing language changes. `#[derive(FrameBuild)]` emits the default; `#[interpret(body_frames = MyBodyFrames)]` overrides it. **Concrete execution only** — forward abstract interpretation summarizes calls (`AbstractCallFrame`) and maps a callable body to an `Owner` in `seed_entry_block` instead of descending, and the backward engines never walk callable bodies through a call frame; customizing those would need engine-family-specific policies, and IR owners must never supply walkers. This policy is *not* consulted for nested bodies: `scf.if`/`scf.for` and other structured operations keep choosing their own dialect frames through their dispatch traits. + +- **Engine capabilities are per-frame, not per-engine**: `core/frame.rs` splits the forward engine surface into component traits named after the *capability* they supply — `StatementDispatch: Interp` (dispatch a statement), `BlockQueries: Interp` (read-only block queries), `CFGQueries: BlockQueries` (`cfg_entry`), `DiGraphQueries: Interp` (`digraph_walk_plan`), `CallServices: Env` (activation storage, linking, callable-entry dispatch; kept whole because `CallFrame` consumes all four and their pairing is a safety property — `CallFrame` still owns the *convention*). **A member frame bounds only what it consumes** (`ScfIfFrame` needs just `FrameEngine`; `ScfForFrame` just `Env`); a *universe* — a total frame enum — keeps an umbrella (`ForwardFrameEngine + SparseForwardInterp` concrete, `ForwardDataflowFrameEngine + SparseForwardInterp` abstract), because its engine must support the union of all its variants. Do not mechanically narrow universe bounds. `ForwardDataflowFrameEngine` extends only `Env + StatementDispatch + BlockQueries + DiGraphQueries` — an abstract engine summarizes calls and seeds owners, so it must **not** be made to inherit `CallServices` or `CFGQueries`. `tests/frame_engine_capabilities.rs` holds mock engines whose ability to compile is the regression test; widening a member frame's bound breaks it. + +- **Naming rule for these traits**: `drive_frames` is the only *driver* at this layer (the frame-stack loop); `ForwardDriver`/`DenseBackwardDriver` are fixpoint-driver structs. Capability traits are named for what they supply, never `*FrameDriver` — do not reintroduce that suffix, and do not add compatibility aliases for it. `FrameEngine` = minimal contract for the generic frame stack; `ForwardFrameEngine`/`ForwardDataflowFrameEngine`/`DenseBackwardFrameEngine` = whole-universe capability sets. The `*Queries` traits must stay **read-only** and require only `Interp`: block-entry binding lives on the crate-private `BlockBinding: Env + BlockQueries` so no query trait's name hides a store mutation. Binding into an explicitly named activation is `Env::bind_values(index, slots, values)`; `SparseForwardInterp::write_results` is the dialect-facing current-activation helper. Names describe the operation, not the `Product` container. + +- **`StatementDispatch` vs `InterpDispatch`**: opposite directions. `InterpDispatch` is implemented by a *stage/language* to route a statement to its dialect rule. `StatementDispatch` is implemented by the *engine* and is what a frame calls: it stashes the current location (`stage`/`statement`/`index`) for the rule to read back through `Interp`, then delegates to `InterpDispatch`. + +- **Customizing traversal**: Every frame — walker or enum — implements the same three methods (`step_into`/`resume_done_into`/`resume_into`), all returning `Result, I::Error>`, so a total enum's match arms are uniform across variants. Concrete custom frames embed `BlockFrame`/`CFGFrame`/`DiGraphFrame`/`CallFrame` via `FrameBuild` (whose `from_ungraph_entry` hook supplies a callable-UnGraph policy); forward abstract custom frames embed `Abstract*Frame`s via `AbstractFrameBuild`. Structured dialects may push dialect-owned frames with `SparseForwardEffect::Push`; ordinary dialects never name frame types. Abstract summary keying and join/widen policy stay in `CallContext`/`WideningStrategy`. - **Stage dispatch**: stage enums add `#[derive(InterpDispatch)]` next to `StageMeta`/`ParseDispatch`; single-language pipelines get a blanket impl. `InterpDispatch` is keyed on the engine alone; the dispatched key is always `I::Semantics`. The engine sets its current location then passes itself to dispatch, which forwards to the matching `Interpretable`/`FunctionEntry` rule. Engine-internal IR queries go through `StageQuery`. diff --git a/Cargo.lock b/Cargo.lock index e4e149d90c..c8c281304f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,7 +710,9 @@ dependencies = [ "kirin-chumsky", "kirin-cmp", "kirin-constant", + "kirin-constprop", "kirin-function", + "kirin-interpreter", "kirin-ir", "kirin-lexer", "kirin-prettyless", @@ -884,6 +886,7 @@ version = "0.1.0" dependencies = [ "kirin-derive-interpreter", "kirin-ir", + "petgraph", "smallvec", "thiserror 2.0.18", ] diff --git a/Cargo.toml b/Cargo.toml index e17ca838c1..3b471d120d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,10 +115,14 @@ kirin-bitwise = { workspace = true } kirin-cf = { workspace = true } kirin-cmp = { workspace = true } kirin-constant = { workspace = true } +kirin-constprop = { workspace = true } kirin-function = { workspace = true } kirin-scf = { workspace = true } +kirin-interpreter = { workspace = true, features = ["derive"] } kirin-test-languages = { workspace = true, features = [ "arith-function-language", + "graph-function-language", + "interpreter", "bitwise-function-language", "callable-language", "namespaced-language", diff --git a/crates/kirin-constprop/src/context.rs b/crates/kirin-constprop/src/context.rs index b62b6d42dc..1483488cbc 100644 --- a/crates/kirin-constprop/src/context.rs +++ b/crates/kirin-constprop/src/context.rs @@ -21,7 +21,9 @@ use std::collections::{HashMap, HashSet}; -use kirin_interpreter::{CallContext, ContextInsensitive, InterpreterError, WideningStrategy}; +use kirin_interpreter::{ + CallContext, ContextInsensitive, FunctionTarget, InterpreterError, WideningStrategy, +}; use kirin_ir::{CompileStage, Product, SpecializedFunction}; use crate::ConstPropValue; @@ -67,12 +69,9 @@ impl Default for ConstPropContext { impl CallContext for ConstPropContext { type Key = (CompileStage, SpecializedFunction, CallCtx); - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - args: &Product, - ) -> Self::Key { + fn key(&mut self, target: &FunctionTarget, args: &Product) -> Self::Key { + let stage = target.stage; + let function = target.function; let ctx = match all_const(args) { Some(consts) => { let admitted = self.admitted.entry((stage, function)).or_default(); diff --git a/crates/kirin-derive-interpreter/src/frame_build.rs b/crates/kirin-derive-interpreter/src/frame_build.rs new file mode 100644 index 0000000000..832e36ab3e --- /dev/null +++ b/crates/kirin-derive-interpreter/src/frame_build.rs @@ -0,0 +1,467 @@ +//! Code generation for the frame-injection derives: `#[derive(FrameBuild)]`, +//! `#[derive(AbstractFrameBuild)]`, `#[derive(DenseFrameBuild)]`. +//! +//! A language that runs the interpreter declares a **total frame enum** — the +//! one Rust type the driver's `Vec` stack holds. Each framework walker it +//! carries must be injectable into that enum, which is what the `*FrameBuild` +//! traits are for. The impls are pure transcription: one constructor per +//! framework frame, each body `Self::Variant(frame)`. +//! +//! These derives write that transcription. The **derive name selects the +//! family** — `FrameBuild` (concrete), `AbstractFrameBuild` (sparse forward), +//! `DenseFrameBuild` (dense backward) — so no attribute is needed, and the name +//! matches the trait it implements as the other interpreter derives do. +//! +//! Variants are matched to constructors by their **field type**, not their +//! variant name, so renaming a variant cannot silently change what is +//! generated. Variants holding a *dialect* frame (`ScfIfFrame`, …) are ignored: +//! those are injected through the dialect's own `Build*` trait, declared by the +//! dialect and implemented by hand. +//! +//! Not derivable, by design: an enum that supplies a callable-`UnGraph` policy. +//! `FrameBuild::from_ungraph_entry` is a defaulted method and a derive emits the +//! whole impl block, so such an enum keeps its hand-written impl (see +//! `UnPolicyFrame` in the workspace `body_kinds` test). + +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +const DEFAULT_INTERP_CRATE: &str = "::kirin_interpreter"; + +/// One constructor of an injection trait. +struct Ctor { + /// The framework frame type this constructor accepts, matched against a + /// variant's field type by its final path segment. + frame: &'static str, + /// The trait method to generate. + method: &'static str, + /// Whether the trait requires it. Optional constructors have a defaulted + /// implementation in the trait and are simply omitted when no variant + /// carries the frame. + required: bool, + /// Whether the method returns `Result` rather than `Self` — the + /// trait lets these refuse, so the generated body wraps in `Ok`. + fallible: bool, +} + +/// A frame family: its injection trait, that trait's arity, and its +/// constructors. +pub struct Family { + trait_name: &'static str, + /// Number of type parameters the trait takes, which must equal the number + /// the deriving enum declares. + arity: usize, + ctors: &'static [Ctor], + /// `true` for the concrete family, whose `FrameBuild` carries a + /// `BodyFrames` associated type selecting the callable-body walkers. The + /// abstract and dense families have no such policy — forward abstract + /// interpretation summarizes calls rather than descending into them, and the + /// backward engines do not walk callable bodies through a call frame. + body_frames: bool, +} + +pub const CONCRETE: Family = Family { + trait_name: "FrameBuild", + arity: 2, + ctors: &[ + Ctor { + frame: "BlockFrame", + method: "from_block", + required: true, + fallible: false, + }, + Ctor { + frame: "CFGFrame", + method: "from_cfg", + required: true, + fallible: false, + }, + Ctor { + frame: "CallFrame", + method: "from_call", + required: true, + fallible: false, + }, + Ctor { + frame: "DiGraphFrame", + method: "from_digraph", + required: true, + fallible: false, + }, + ], + body_frames: true, +}; + +pub const SPARSE_FORWARD: Family = Family { + trait_name: "AbstractFrameBuild", + arity: 3, + ctors: &[ + Ctor { + frame: "AbstractBlockFrame", + method: "from_block", + required: true, + fallible: false, + }, + Ctor { + frame: "AbstractCallFrame", + method: "from_call", + required: true, + fallible: false, + }, + // Graph bodies are opt-in: the trait's default refuses, so an enum + // without this variant simply inherits the refusal. + Ctor { + frame: "AbstractDiGraphFrame", + method: "from_digraph", + required: false, + fallible: true, + }, + ], + body_frames: false, +}; + +pub const DENSE_BACKWARD: Family = Family { + trait_name: "DenseFrameBuild", + arity: 2, + ctors: &[Ctor { + frame: "DenseBlockFrame", + method: "from_block", + required: true, + fallible: false, + }], + body_frames: false, +}; + +/// The `#[interpret(..)]` options these derives read, reusing the namespace the +/// other interpreter derives already use. +struct Options { + crate_path: syn::Path, + /// `#[interpret(body_frames = MyBodyFrames)]` — the callable-body walker + /// policy for the concrete family. `None` means [`DefaultBodyFrames`]. + body_frames: Option, +} + +fn parse_options(input: &DeriveInput) -> syn::Result { + let mut crate_path = None; + let mut body_frames = None; + for attr in &input.attrs { + if !attr.path().is_ident("interpret") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("crate") { + crate_path = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("body_frames") { + body_frames = Some(meta.value()?.parse()?); + Ok(()) + } else { + Err(meta.error("unsupported attribute for #[interpret(...)]")) + } + })?; + } + Ok(Options { + crate_path: match crate_path { + Some(path) => path, + None => syn::parse_str(DEFAULT_INTERP_CRATE)?, + }, + body_frames, + }) +} + +/// The final path segment of a type, used to recognize a framework frame. +fn type_head(ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(path) => path.path.segments.last().map(|s| s.ident.to_string()), + _ => None, + } +} + +pub fn generate(input: &DeriveInput, family: &Family) -> syn::Result { + let trait_ident: syn::Ident = syn::parse_str(family.trait_name)?; + let options = parse_options(input)?; + let interp_crate = &options.crate_path; + + if !family.body_frames && options.body_frames.is_some() { + return Err(syn::Error::new_spanned( + input, + format!( + "`body_frames` applies only to the concrete `FrameBuild` family; `{}` has no callable-body policy", + family.trait_name + ), + )); + } + + let syn::Data::Enum(data) = &input.data else { + return Err(syn::Error::new_spanned( + input, + format!( + "`{}` can only be derived for a total frame enum", + family.trait_name + ), + )); + }; + + // The trait's type arguments are the enum's own type parameters, in order — + // `enum ToyFrame` implements `FrameBuild`. Reject a mismatch + // here rather than emitting an impl that fails to resolve later. + let type_params: Vec<&syn::Ident> = input.generics.type_params().map(|p| &p.ident).collect(); + if type_params.len() != family.arity { + return Err(syn::Error::new_spanned( + input, + format!( + "`{}` expects an enum with {} type parameter(s) to match `{}<{}>`, found {}. \ + An enum whose trait arguments are not its own type parameters must implement the trait by hand.", + family.trait_name, + family.arity, + family.trait_name, + vec!["_"; family.arity].join(", "), + type_params.len(), + ), + )); + } + + // Match each variant to a constructor by its field type. + let mut methods = Vec::new(); + for ctor in family.ctors { + let mut matched = None; + for variant in &data.variants { + let syn::Fields::Unnamed(fields) = &variant.fields else { + continue; + }; + if fields.unnamed.len() != 1 { + continue; + } + let field_ty = &fields.unnamed[0].ty; + if type_head(field_ty).as_deref() == Some(ctor.frame) { + if matched.is_some() { + return Err(syn::Error::new_spanned( + variant, + format!( + "two variants hold a `{}`; `{}::{}` would be ambiguous", + ctor.frame, family.trait_name, ctor.method + ), + )); + } + matched = Some((&variant.ident, field_ty)); + } + } + + let Some((variant_ident, field_ty)) = matched else { + if ctor.required { + return Err(syn::Error::new_spanned( + input, + format!( + "no variant holds a `{}`, which `{}` requires for `{}`. \ + Add such a variant, or implement the trait by hand.", + ctor.frame, family.trait_name, ctor.method + ), + )); + } + continue; + }; + + let method: syn::Ident = syn::parse_str(ctor.method)?; + // The parameter type is copied verbatim from the variant, so the frame + // type's own generic arity never has to be reconstructed here. + methods.push(if ctor.fallible { + let err = type_params[1]; + quote! { + fn #method(frame: #field_ty) -> ::core::result::Result { + ::core::result::Result::Ok(Self::#variant_ident(frame)) + } + } + } else { + quote! { + fn #method(frame: #field_ty) -> Self { + Self::#variant_ident(frame) + } + } + }); + } + + let enum_ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let assoc_body_frames = if family.body_frames { + let policy = match &options.body_frames { + Some(path) => quote! { #path }, + None => quote! { #interp_crate::DefaultBodyFrames }, + }; + quote! { type BodyFrames = #policy; } + } else { + quote! {} + }; + + Ok(quote! { + #[automatically_derived] + impl #impl_generics #interp_crate::#trait_ident<#(#type_params),*> + for #enum_ident #ty_generics #where_clause + { + #assoc_body_frames + #(#methods)* + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use kirin_test_utils::rustfmt; + + fn emit(input: syn::DeriveInput, family: &Family) -> String { + rustfmt( + generate(&input, family) + .expect("codegen failed") + .to_string(), + ) + } + + #[test] + fn concrete_frame_enum_with_dialect_variants() { + // The two scf variants are ignored — they are injected through + // `BuildScfIf`/`BuildScfFor`, which the dialect declares. + let input: syn::DeriveInput = syn::parse_quote! { + enum ToyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + ScfIf(ScfIfFrame), + ScfFor(ScfForFrame), + } + }; + insta::assert_snapshot!(emit(input, &CONCRETE)); + } + + #[test] + fn sparse_forward_omits_optional_digraph_when_absent() { + let input: syn::DeriveInput = syn::parse_quote! { + enum ToyAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + ScfIf(AbstractScfIfFrame), + } + }; + insta::assert_snapshot!(emit(input, &SPARSE_FORWARD)); + } + + #[test] + fn sparse_forward_digraph_is_fallible_when_present() { + let input: syn::DeriveInput = syn::parse_quote! { + enum StandardAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), + } + }; + insta::assert_snapshot!(emit(input, &SPARSE_FORWARD)); + } + + #[test] + fn dense_backward_single_constructor() { + let input: syn::DeriveInput = syn::parse_quote! { + enum ToyDenseBackwardFrame { + Block(DenseBlockFrame), + ScfIf(DenseScfIfFrame), + } + }; + insta::assert_snapshot!(emit(input, &DENSE_BACKWARD)); + } + + #[test] + fn crate_path_override_is_honoured() { + let input: syn::DeriveInput = syn::parse_quote! { + #[interpret(crate = crate)] + enum StandardFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + insta::assert_snapshot!(emit(input, &CONCRETE)); + } + + #[test] + fn body_frames_override_is_honoured() { + let input: syn::DeriveInput = syn::parse_quote! { + #[interpret(body_frames = MyBodyFrames)] + enum MyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + insta::assert_snapshot!(emit(input, &CONCRETE)); + } + + #[test] + fn rejects_body_frames_on_a_family_without_a_call_policy() { + // Only the concrete family descends into a callee, so only it has a + // callable-body policy to configure. + let input: syn::DeriveInput = syn::parse_quote! { + #[interpret(body_frames = MyBodyFrames)] + enum MyAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + } + }; + let err = generate(&input, &SPARSE_FORWARD).unwrap_err().to_string(); + assert!(err.contains("applies only to the concrete"), "{err}"); + } + + #[test] + fn rejects_missing_required_frame() { + let input: syn::DeriveInput = syn::parse_quote! { + enum Incomplete { + Block(BlockFrame), + } + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!(err.contains("no variant holds a `CFGFrame`"), "{err}"); + } + + #[test] + fn rejects_wrong_type_param_count() { + let input: syn::DeriveInput = syn::parse_quote! { + enum Weird { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!( + err.contains("expects an enum with 2 type parameter(s)"), + "{err}" + ); + } + + #[test] + fn rejects_ambiguous_duplicate_frame() { + let input: syn::DeriveInput = syn::parse_quote! { + enum Dup { + Block(BlockFrame), + AlsoBlock(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!(err.contains("would be ambiguous"), "{err}"); + } + + #[test] + fn rejects_non_enum() { + let input: syn::DeriveInput = syn::parse_quote! { + struct NotAnEnum(BlockFrame); + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!(err.contains("total frame enum"), "{err}"); + } +} diff --git a/crates/kirin-derive-interpreter/src/function_entry.rs b/crates/kirin-derive-interpreter/src/function_entry.rs index 94f11abcae..b1ad6e3bea 100644 --- a/crates/kirin-derive-interpreter/src/function_entry.rs +++ b/crates/kirin-derive-interpreter/src/function_entry.rs @@ -114,7 +114,7 @@ fn emit_function_entry( args: #ir_crate::Product<<__EntryI as #interp_crate::Interp>::Value>, interp: &mut __EntryI, ) -> Result< - #interp_crate::FunctionBody<<__EntryI as #interp_crate::Interp>::Value>, + #interp_crate::CallableBody<<__EntryI as #interp_crate::Interp>::Value>, <__EntryI as #interp_crate::Interp>::Error, > { #body diff --git a/crates/kirin-derive-interpreter/src/interp_dispatch.rs b/crates/kirin-derive-interpreter/src/interp_dispatch.rs index 8a2149615e..7eb02631da 100644 --- a/crates/kirin-derive-interpreter/src/interp_dispatch.rs +++ b/crates/kirin-derive-interpreter/src/interp_dispatch.rs @@ -116,7 +116,7 @@ pub fn generate(input: &DeriveInput) -> Result { args: #ir_crate::Product<<__InterpI as #interp_crate::Interp>::Value>, interp: &mut __InterpI, ) -> Result< - #interp_crate::FunctionBody<<__InterpI as #interp_crate::Interp>::Value>, + #interp_crate::CallableBody<<__InterpI as #interp_crate::Interp>::Value>, <__InterpI as #interp_crate::Interp>::Error, > { match self { diff --git a/crates/kirin-derive-interpreter/src/lib.rs b/crates/kirin-derive-interpreter/src/lib.rs index 1c80fb6178..07f92859dc 100644 --- a/crates/kirin-derive-interpreter/src/lib.rs +++ b/crates/kirin-derive-interpreter/src/lib.rs @@ -1,5 +1,6 @@ extern crate proc_macro; +mod frame_build; mod function_entry; mod interp_dispatch; mod interpretable; @@ -41,3 +42,38 @@ pub fn derive_interp_dispatch(input: TokenStream) -> TokenStream { Err(e) => e.into_compile_error().into(), } } + +/// Derive `FrameBuild` for a **concrete** total frame enum: one +/// constructor per framework walker it carries, matched by field type. Variants +/// holding dialect frames are ignored — those are injected through the dialect's +/// own `Build*` trait. +#[proc_macro_derive(FrameBuild, attributes(interpret))] +pub fn derive_frame_build(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + match frame_build::generate(&ast, &frame_build::CONCRETE) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Derive `AbstractFrameBuild` for a **sparse forward** total frame +/// enum. `from_digraph` is emitted only when a variant carries an +/// `AbstractDiGraphFrame`; otherwise the trait's refusing default is inherited. +#[proc_macro_derive(AbstractFrameBuild, attributes(interpret))] +pub fn derive_abstract_frame_build(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + match frame_build::generate(&ast, &frame_build::SPARSE_FORWARD) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Derive `DenseFrameBuild` for a **dense backward** total frame enum. +#[proc_macro_derive(DenseFrameBuild, attributes(interpret))] +pub fn derive_dense_frame_build(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + match frame_build::generate(&ast, &frame_build::DENSE_BACKWARD) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap new file mode 100644 index 0000000000..1482c724da --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap @@ -0,0 +1,20 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +expression: "emit(input, &CONCRETE)" +--- +#[automatically_derived] +impl ::kirin_interpreter::FrameBuild for MyFrame { + type BodyFrames = MyBodyFrames; + fn from_block(frame: BlockFrame) -> Self { + Self::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self::DiGraph(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap new file mode 100644 index 0000000000..b5c0e4600d --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap @@ -0,0 +1,21 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 335 +expression: "emit(input, &CONCRETE)" +--- +#[automatically_derived] +impl ::kirin_interpreter::FrameBuild for ToyFrame { + type BodyFrames = ::kirin_interpreter::DefaultBodyFrames; + fn from_block(frame: BlockFrame) -> Self { + Self::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self::DiGraph(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap new file mode 100644 index 0000000000..0f9e5de746 --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap @@ -0,0 +1,21 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 384 +expression: "emit(input, &CONCRETE)" +--- +#[automatically_derived] +impl crate::FrameBuild for StandardFrame { + type BodyFrames = crate::DefaultBodyFrames; + fn from_block(frame: BlockFrame) -> Self { + Self::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self::DiGraph(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap new file mode 100644 index 0000000000..f0bb0b5f48 --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap @@ -0,0 +1,11 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 291 +expression: "emit(input, &DENSE_BACKWARD)" +--- +#[automatically_derived] +impl ::kirin_interpreter::DenseFrameBuild for ToyDenseBackwardFrame { + fn from_block(frame: DenseBlockFrame) -> Self { + Self::Block(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap new file mode 100644 index 0000000000..2fe6d0bcd1 --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap @@ -0,0 +1,17 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 280 +expression: "emit(input, &SPARSE_FORWARD)" +--- +#[automatically_derived] +impl ::kirin_interpreter::AbstractFrameBuild for StandardAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + Self::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: AbstractDiGraphFrame) -> ::core::result::Result { + ::core::result::Result::Ok(Self::DiGraph(frame)) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap new file mode 100644 index 0000000000..21aa450e0e --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap @@ -0,0 +1,14 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 268 +expression: "emit(input, &SPARSE_FORWARD)" +--- +#[automatically_derived] +impl ::kirin_interpreter::AbstractFrameBuild for ToyAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + Self::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + Self::Call(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap index 1da0bb6b7e..8524f2e6d5 100644 --- a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap @@ -14,7 +14,7 @@ where args: ::kirin::ir::Product<<__EntryI as ::kirin_interpreter::Interp>::Value>, interp: &mut __EntryI, ) -> Result< - ::kirin_interpreter::FunctionBody<<__EntryI as ::kirin_interpreter::Interp>::Value>, + ::kirin_interpreter::CallableBody<<__EntryI as ::kirin_interpreter::Interp>::Value>, <__EntryI as ::kirin_interpreter::Interp>::Error, > { match self { diff --git a/crates/kirin-function/src/interpreter.rs b/crates/kirin-function/src/interpreter.rs index 2652c242b1..8cc8c3c80d 100644 --- a/crates/kirin-function/src/interpreter.rs +++ b/crates/kirin-function/src/interpreter.rs @@ -1,7 +1,7 @@ use kirin::prelude::{CompileTimeValue, HasBottom, HasCFGBody, Product, SSAValue}; use kirin_interpreter::dialect::{ - CallEffect, Callee, ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, - ForwardEval, FunctionBody, FunctionEntry, Interp, Interpretable, InterpreterError, + CallEffect, CallableBody, Callee, ClassicLiveness, ClassicLivenessInterp, DemandInterp, + DenseBackwardEffect, ForwardEval, FunctionEntry, Interp, Interpretable, InterpreterError, SparseForwardEffect, SparseForwardInterp, StrongDemand, }; @@ -98,8 +98,8 @@ where &self, args: Product, _interp: &mut I, - ) -> Result, I::Error> { - Ok(FunctionBody::new(*self.cfg()).args(args)) + ) -> Result, I::Error> { + Ok(CallableBody::new(*self.cfg()).args(args)) } } @@ -112,8 +112,8 @@ where &self, args: Product, _interp: &mut I, - ) -> Result, I::Error> { - Ok(FunctionBody::new(*self.cfg()).args(args)) + ) -> Result, I::Error> { + Ok(CallableBody::new(*self.cfg()).args(args)) } } diff --git a/crates/kirin-interpreter/Cargo.toml b/crates/kirin-interpreter/Cargo.toml index 6ad2250608..5fca996fca 100644 --- a/crates/kirin-interpreter/Cargo.toml +++ b/crates/kirin-interpreter/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] kirin-ir = { workspace = true } +petgraph = { workspace = true } kirin-derive-interpreter = { workspace = true, optional = true } smallvec = { workspace = true } thiserror = { workspace = true } diff --git a/crates/kirin-interpreter/src/core/dispatch.rs b/crates/kirin-interpreter/src/core/dispatch.rs index 64cd2e7d5e..19f6428f87 100644 --- a/crates/kirin-interpreter/src/core/dispatch.rs +++ b/crates/kirin-interpreter/src/core/dispatch.rs @@ -1,6 +1,6 @@ use kirin_ir::{Dialect, Product, StageInfo, StageMeta, Statement}; -use crate::{FunctionBody, Interp}; +use crate::{CallableBody, Interp}; /// Statement semantics. The single trait dialect authors implement. /// @@ -18,7 +18,7 @@ pub trait Interpretable: Dialect { /// Function-entry semantics for callable statements. /// /// Implemented by statements that define function bodies (e.g. -/// `kirin_function::Function`); describes the [`FunctionBody`] an engine enters +/// `kirin_function::Function`); describes the [`CallableBody`] an engine enters /// when the function is invoked. Derived on language enums with /// `#[derive(FunctionEntry)]` where `#[callable]` marks the variants that wrap /// callable statements. @@ -27,7 +27,7 @@ pub trait FunctionEntry: Dialect { &self, args: Product, interp: &mut I, - ) -> Result, I::Error>; + ) -> Result, I::Error>; } /// Monomorphic statement dispatch over a stage enum. @@ -54,7 +54,7 @@ pub trait InterpDispatch: StageMeta { body: Statement, args: Product, interp: &mut I, - ) -> Result, I::Error>; + ) -> Result, I::Error>; } impl InterpDispatch for StageInfo @@ -76,7 +76,7 @@ where body: Statement, args: Product, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result, I::Error> { let definition = body.definition(self).clone(); definition.function_entry(args, interp) } diff --git a/crates/kirin-interpreter/src/core/effect.rs b/crates/kirin-interpreter/src/core/effect.rs index cfa35c6a4e..87a1429454 100644 --- a/crates/kirin-interpreter/src/core/effect.rs +++ b/crates/kirin-interpreter/src/core/effect.rs @@ -1,8 +1,47 @@ use kirin_ir::{ - Block, CFG, CompileStage, Function, Product, SSAValue, SpecializedFunction, StagedFunction, - Symbol, + Block, CFG, CompileStage, DiGraph, Function, Product, SSAValue, SpecializedFunction, + StagedFunction, Symbol, UnGraph, }; +/// A traversal descriptor: which body was the engine handed? +/// +/// Interpreter vocabulary, not an IR concept — dialect ops keep their precise +/// field types (`Block`, `CFG`, `DiGraph`, `UnGraph`); a `Body` appears only at +/// the moment a body is handed to the interpreter (callable entry, +/// body-containment queries, analysis scopes). Bodies carry no semantics of their own: the +/// statement that owns a body defines what entering and exiting it means. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Body { + Block(Block), + CFG(CFG), + DiGraph(DiGraph), + UnGraph(UnGraph), +} + +impl From for Body { + fn from(block: Block) -> Self { + Self::Block(block) + } +} + +impl From for Body { + fn from(cfg: CFG) -> Self { + Self::CFG(cfg) + } +} + +impl From for Body { + fn from(graph: DiGraph) -> Self { + Self::DiGraph(graph) + } +} + +impl From for Body { + fn from(graph: UnGraph) -> Self { + Self::UnGraph(graph) + } +} + /// The closed forward control algebra a statement produces. /// /// Atomic statements read operands, write results, and return [`SparseForwardEffect::Next`]. @@ -86,27 +125,34 @@ pub enum Callee { Specialized(SpecializedFunction), } -/// The body a callable statement enters when invoked: a CFG plus the -/// entry arguments bound to its entry block. +/// The body a callable statement enters when invoked, plus the entry +/// arguments bound to its boundary (block parameters / graph ports). /// /// This is the function-call entry descriptor — the call mechanism, not a /// structured-control abstraction. A [`FunctionEntry`](crate::FunctionEntry) -/// rule returns one; the engine builds the body frame that walks the CFG. -pub struct FunctionBody { - pub cfg: CFG, +/// rule returns one; the call boundary picks the walker that matches the +/// body kind. Any body kind may be callable — the statement declaring itself +/// callable defines the semantics; the framework supplies default walkers +/// for `CFG`, `Block`, and `DiGraph`, while `UnGraph` traversal is a +/// dialect/compiler-supplied policy (the concrete engine's +/// `FrameBuild::from_ungraph_entry` hook), rejected with +/// [`InterpreterError::NoDefaultWalker`](crate::InterpreterError) when no +/// policy is provided. +pub struct CallableBody { + pub body: Body, pub args: Product, } -impl FunctionBody { - /// A function body over `cfg`, with no entry arguments yet. - pub fn new(cfg: CFG) -> Self { +impl CallableBody { + /// A callable body, with no entry arguments yet. + pub fn new(body: impl Into) -> Self { Self { - cfg, + body: body.into(), args: Product::new(), } } - /// Entry arguments bound to the CFG entry block's parameters. + /// Entry arguments bound to the body's boundary parameters. pub fn args(mut self, args: impl IntoIterator) -> Self { self.args = args.into_iter().collect(); self diff --git a/crates/kirin-interpreter/src/core/error.rs b/crates/kirin-interpreter/src/core/error.rs index c200188c9e..74a886bd86 100644 --- a/crates/kirin-interpreter/src/core/error.rs +++ b/crates/kirin-interpreter/src/core/error.rs @@ -41,10 +41,14 @@ pub enum InterpreterError { MissingCallTarget(Symbol), #[error("cfg has no entry block")] EmptyCFG, + #[error("body {0:?} has no default walker in this engine")] + NoDefaultWalker(crate::Body), + #[error("digraph {0:?} has a cycle; the default walker only runs DAGs")] + GraphHasCycle(kirin_ir::DiGraph), + #[error("CFG control flow (jump/branch) inside a single-block or graph body")] + CFGControlFlowInStructuredBody, #[error("block {0:?} fell through without a terminator effect")] BlockFellThrough(Block), - #[error("function body fell through without returning")] - FunctionBodyFellThrough, #[error("yield outside of an enclosing scope at {0:?}")] UnexpectedYield(Statement), #[error("statement {0:?} is not callable")] diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 3c26a3c726..2f2b1cfa11 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -1,16 +1,70 @@ -//! Shared frame protocol plus forward frame-driver capabilities. +//! Shared frame protocol plus the engine capabilities frames require. //! //! [`Frame`], [`FrameEngine`], [`FrameEffect`], and [`drive_frames`] are -//! direction-neutral. Forward engines add [`ForwardFrameDriver`] and -//! [`ForwardDataflowFrameDriver`] for env access, IR queries, calls, and abstract -//! merge/summarization. +//! direction-neutral. Forward engines add the capability traits below. +//! +//! # Three levels of "engine" +//! +//! The word means something different at each level, so the names are kept +//! distinct: +//! +//! - **[`drive_frames`]** is the *frame-stack driver* — the loop. Nothing else +//! is a "driver"; the concrete objects named `ForwardDriver` / +//! `DenseBackwardDriver` are fixpoint-driver structs, not capability traits. +//! - **[`FrameEngine`]** is the minimal engine contract the generic frame stack +//! needs: a total `Error` type, and nothing more. +//! - the **component traits** below are narrowly scoped services an interpreter +//! engine supplies *to individual frames*, and the two **umbrellas** +//! ([`ForwardFrameEngine`], [`ForwardDataflowFrameEngine`]) name the full +//! capability set for a whole standard frame universe. +//! +//! # The capability model +//! +//! Capabilities are split by **what one frame needs**, not by what one engine +//! happens to provide. Each trait is the requirement of a specific kind of +//! traversal, so a frame's bound documents exactly which engine operations it +//! can reach — and an engine that implements only some of them still runs the +//! frames it can support. +//! +//! | trait | capability | consumed by | +//! |---|---|---| +//! | [`StatementDispatch`] | dispatch a statement to its dialect rule | every executing frame | +//! | [`BlockQueries`] | read-only structural queries for walking one block | [`BlockFrame`](crate::BlockFrame), [`AbstractBlockFrame`](crate::AbstractBlockFrame), dialect block walkers | +//! | [`CFGQueries`] | find a CFG's entry block (`: BlockQueries`) | [`CFGFrame`](crate::CFGFrame) | +//! | [`DiGraphQueries`] | schedule a digraph body | [`DiGraphFrame`](crate::DiGraphFrame) | +//! | [`CallServices`] | activation storage, linking, callable-entry dispatch | [`CallFrame`](crate::CallFrame) | +//! +//! The `*Queries` traits are exactly that: **read-only**. The one operation that +//! needs both a query and a write — binding a block's parameters to incoming +//! actuals — lives on the crate-private `BlockBinding` extension instead of +//! hiding inside [`BlockQueries`], so no query trait's name conceals a store +//! mutation. +//! +//! [`StatementDispatch`] is the engine side of dialect dispatch, and is easy to +//! confuse with [`InterpDispatch`](crate::InterpDispatch) — they face opposite +//! directions. `InterpDispatch` is implemented by a **stage/language** to +//! route a statement to the right dialect rule. `StatementDispatch` is +//! implemented by the **engine** and is what a *frame* calls: it stashes the +//! current location (`stage`/`statement`/`index`) so the rule can read it back +//! through [`Interp`], then delegates to `InterpDispatch`. +//! +//! Two umbrellas compose the components for the two *engine families*. A total +//! frame enum belongs on an umbrella — a universe's engine must support the +//! union of all its variants — while a member frame names only its components: +//! +//! - [`ForwardFrameEngine`] — the full concrete surface: all four components, +//! blanket-implemented. +//! - [`ForwardDataflowFrameEngine`] — abstract dataflow: the traversal +//! components abstract execution *shares*, plus merge/summarization. It does +//! **not** inherit [`CallServices`] or [`CFGQueries`], because an abstract +//! engine summarizes calls rather than entering them. use std::hash::Hash; use kirin_ir::{Block, CFG, CompileStage, Product, SSAValue, Statement}; use crate::{ - CallEffect, Callee, Env, EnvIndex, FunctionBody, FunctionTarget, Interp, InterpreterError, + Body, CallEffect, CallableBody, Callee, Env, EnvIndex, FunctionTarget, Interp, InterpreterError, }; /// Structural effect a [`Frame`] returns to the engine driver loop. @@ -20,10 +74,11 @@ pub enum FrameEffect { /// Push `parent` then `child`; `child` runs next, `parent` resumes after. Push { parent: F, child: F }, /// This frame finished with no payload; its parent's - /// [`Frame::resume_done`] is called. + /// [`Frame::resume_done_into`] is called. Done, - /// This frame produced a completion `C`; its parent's [`Frame::resume`] is - /// called (or, at the root, the run finishes with `C`). + /// This frame produced a completion `C`; its parent's + /// [`Frame::resume_into`] is called (or, at the root, the run finishes with + /// `C`). Complete(C), } @@ -37,21 +92,43 @@ impl FrameEngine for T { type Error = ::Error; } -/// A continuation frame anchored in an IR traversal. +/// A continuation frame anchored in an IR traversal, expressed over the total +/// frame type `F` it composes into. /// -/// Implemented by the total frame enum `F`. Each method consumes `self` and -/// returns the next structural move as a [`FrameEffect`]. -pub trait Frame: Sized { +/// Every method consumes `self` and returns the next structural move as a +/// [`FrameEffect`] **over `F`** — never over `Self`. That single choice is what +/// lets one trait serve both roles a frame stack needs: +/// +/// - a **member** — an individual walker ([`BlockFrame`](crate::BlockFrame), +/// [`CallFrame`](crate::CallFrame), a dialect's own frame). It is one variant +/// of `F` and names its successors in `F`, re-wrapping itself through the +/// relevant `*FrameBuild` hook. Members are generic over `F`, so the same +/// walker composes into any language's frame type. +/// - a **universe** — a language's total frame enum. It implements +/// `Frame` when it is the stack's element type, and stays generic +/// over `F` so that it can *also* be embedded in a larger enum (an +/// instrumenting wrapper, or another language's frame type) without +/// re-enumerating its variants. +/// +/// [`drive_frames`] bounds on `F: Frame`: the stack's element type must be +/// a universe — a type able to represent every frame that can appear on it. +pub trait Frame: Sized { /// The completion payload this frame family bubbles to parents/root. type Completion; - fn step(self, interp: &mut I) -> Result, I::Error>; - fn resume_done(self, interp: &mut I) -> Result, I::Error>; - fn resume( + /// Do this frame's next unit of work. + fn step_into(self, interp: &mut I) -> Result, I::Error>; + + /// A pushed child finished with no payload. + fn resume_done_into(self, interp: &mut I) + -> Result, I::Error>; + + /// A pushed child finished with a completion payload. + fn resume_into( self, completion: Self::Completion, interp: &mut I, - ) -> Result, I::Error>; + ) -> Result, I::Error>; } /// Shared frame-stepping loop. @@ -59,13 +136,13 @@ pub fn drive_frames(engine: &mut I, frames: &mut Vec) -> Result, - F: Frame, + F: Frame, { loop { let frame = frames .pop() .ok_or_else(|| I::Error::from(InterpreterError::EmptyFrameStack))?; - let mut effect = frame.step(engine)?; + let mut effect = frame.step_into(engine)?; loop { match effect { FrameEffect::Continue(frame) => { @@ -81,11 +158,11 @@ where let parent = frames .pop() .ok_or_else(|| I::Error::from(InterpreterError::EmptyFrameStack))?; - effect = parent.resume_done(engine)?; + effect = parent.resume_done_into(engine)?; } FrameEffect::Complete(completion) => match frames.pop() { Some(parent) => { - effect = parent.resume(completion, engine)?; + effect = parent.resume_into(completion, engine)?; } None => return Ok(completion), }, @@ -94,38 +171,43 @@ where } } -/// Capabilities required by forward frames. +/// Engine capability for dispatching a statement to its dialect rule. /// -/// Re-exported as [`FrameDriver`](crate::FrameDriver). -pub trait ForwardFrameDriver: Env { - /// Allocate a fresh SSA activation record. - fn alloc_env(&mut self) -> EnvIndex; - /// Free an activation record. - fn free_env(&mut self, index: EnvIndex) -> Result<(), Self::Error>; - /// Resolve a callee to a concrete function target via the engine's linker. - fn resolve_call( - &self, - stage: CompileStage, - callee: &Callee, - ) -> Result; +/// The one capability *every* frame that executes statements needs, and the only +/// one shared by concrete execution and abstract dataflow. +/// +/// Not to be confused with [`InterpDispatch`](crate::InterpDispatch), which +/// faces the other way: a *stage/language* implements `InterpDispatch` to route +/// a statement to its dialect rule, while an *engine* implements +/// `StatementDispatch` to expose location-aware dispatch to frames. +pub trait StatementDispatch: Interp { /// Dispatch one statement to its dialect [`Interpretable`](crate::Interpretable) /// rule, producing this engine's [`Effect`](Interp::Effect) (a /// [`SparseForwardEffect`](crate::SparseForwardEffect) for the value engines). + /// + /// The engine stashes `stage`/`statement`/`index` as its current location + /// first, so the rule can read it back through [`Interp`]. fn run_statement( &mut self, stage: CompileStage, statement: Statement, index: EnvIndex, ) -> Result; - /// Build the [`FunctionBody`] a callable statement enters on invocation. - fn enter_function( - &mut self, - stage: CompileStage, - body: Statement, - args: Product, - index: EnvIndex, - ) -> Result, Self::Error>; +} +/// Read-only structural queries needed to traverse a single [`Block`]. +/// +/// The requirement of [`BlockFrame`](crate::BlockFrame), its internal +/// `BlockCursor`, and every frame that steps through a block's statements. +/// +/// **Read-only by construction**: only [`Interp`] is required, not [`Env`], so +/// nothing on this trait can touch SSA storage. Entering a block also *binds* +/// its parameters, which needs a write — that operation lives on the +/// crate-private `BlockBinding` extension (bounded `Env + BlockQueries`) +/// rather than here, so this name cannot hide a store mutation. Engine-internal +/// callers wanting the same queries outside a frame use +/// [`StageQuery`](crate::StageQuery). +pub trait BlockQueries: Interp { fn block_params(&self, stage: CompileStage, block: Block) -> Result, Self::Error>; fn first_statement( @@ -139,9 +221,26 @@ pub trait ForwardFrameDriver: Env { block: Block, after: Statement, ) -> Result, Self::Error>; +} + +/// Structural queries needed to enter and traverse a [`CFG`]. +/// +/// Extends [`BlockQueries`] because walking a CFG *is* walking its blocks and +/// following jumps between them; `cfg_entry` only adds finding where to start. +pub trait CFGQueries: BlockQueries { fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, Self::Error>; +} - /// Bind a block's parameters to incoming actuals in `env` (arity-checked). +/// Crate-private block-entry binding: the one operation that needs a +/// [`BlockQueries`] read *and* an [`Env`] write. +/// +/// Deliberately not on [`BlockQueries`] (whose name promises read-only) and +/// deliberately not public: it is frame-internal mechanics, blanket-implemented +/// for every engine with both capabilities, so a frame that binds a block entry +/// spells its requirement honestly as `Env + BlockQueries`. +pub(crate) trait BlockBinding: Env + BlockQueries { + /// Positionally bind a block's parameters to incoming actuals in `index`, + /// checking arity. fn bind_block_args( &mut self, stage: CompileStage, @@ -162,35 +261,112 @@ pub trait ForwardFrameDriver: Env { } Ok(()) } +} + +impl BlockBinding for T {} + +/// Structural/scheduling queries needed to traverse a +/// [`DiGraph`](kirin_ir::DiGraph) body. +/// +/// Split out from the block/CFG queries because a digraph walk shares none of +/// their mechanics: there are no blocks, no jumps, and no entry block — only a +/// dependency order. +pub trait DiGraphQueries: Interp { + /// The default walk plan of a digraph body (ports, toposorted nodes, + /// yields). Errors on cyclic digraphs. + /// + /// Digraph bodies are opt-in: an engine that never walks one inherits this + /// rejection rather than inventing a schedule, the same way + /// [`FrameBuild::from_ungraph_entry`](crate::FrameBuild::from_ungraph_entry) + /// rejects a callable `UnGraph` without a compiler-supplied policy. + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + let _ = stage; + Err(Self::Error::from(InterpreterError::NoDefaultWalker( + Body::DiGraph(graph), + ))) + } +} - /// Destructure `values` into `results` slots in `env` (arity-checked). - fn write_results( +/// Engine services used by [`CallFrame`](crate::CallFrame): activation storage, +/// linking, and callable-entry dispatch. +/// +/// **[`CallFrame`](crate::CallFrame) still owns the calling convention** — the +/// order of operations, which completions are legal, and freeing the activation +/// exactly once. This trait only supplies the primitives it calls. +/// +/// Kept whole on purpose: the standard `CallFrame` consumes all four together, +/// and their pairing is a safety property — an `alloc_env` without its matching +/// `free_env` is a leak, a second `free_env` a double free. Splitting them into +/// separate capabilities would let an engine offer half a call convention. +/// +/// Notably *not* required by abstract dataflow: forward abstract interpretation +/// summarizes a call instead of descending into it, so +/// [`ForwardDataflowFrameEngine`] does not extend this trait. +pub trait CallServices: Env { + /// Allocate a fresh SSA activation record. + fn alloc_env(&mut self) -> EnvIndex; + /// Free an activation record. + fn free_env(&mut self, index: EnvIndex) -> Result<(), Self::Error>; + /// Resolve a callee to a concrete function target via the engine's linker. + fn resolve_call( + &self, + stage: CompileStage, + callee: &Callee, + ) -> Result; + /// Build the [`CallableBody`] a callable statement enters on invocation. + fn enter_function( &mut self, + stage: CompileStage, + body: Statement, + args: Product, index: EnvIndex, - results: &Product, - values: Product, - ) -> Result<(), Self::Error> { - if results.len() != values.len() { - return Err(Self::Error::from(InterpreterError::ProductArityMismatch { - expected: results.len(), - actual: values.len(), - })); - } - for (slot, value) in results.iter().copied().zip(values) { - self.env_write(index, slot, value)?; - } - Ok(()) - } + ) -> Result, Self::Error>; } -/// The **forward dataflow** frame-driver capability surface: what the forward -/// abstract frames need from the engine, beyond the [`ForwardFrameDriver`] IR -/// queries. +/// An interpreter engine capable of running the complete standard **concrete** +/// forward-frame universe. +/// +/// This is an umbrella, not a definition — it adds no methods and is +/// [blanket-implemented](#impl-ForwardFrameEngine-for-T) for any engine +/// providing the four components. Use it at the *universe* level, where a total +/// frame enum's engine must support the union of all its variants +/// ([`StandardFrame`](crate::StandardFrame) and downstream frame enums do). +/// Individual member frames should bound only the components they use, so a +/// partial engine can still run them. +pub trait ForwardFrameEngine: + StatementDispatch + CFGQueries + DiGraphQueries + CallServices +{ +} + +impl ForwardFrameEngine for T where + T: StatementDispatch + CFGQueries + DiGraphQueries + CallServices +{ +} + +/// An interpreter engine capable of running the standard **forward abstract** +/// frame universe: the traversal capabilities it shares with concrete execution, +/// plus merge/summarization. +/// +/// It extends [`Env`] + [`StatementDispatch`] + [`BlockQueries`] + +/// [`DiGraphQueries`] — the traversal it genuinely shares — and **deliberately +/// not** [`CallServices`] or [`CFGQueries`]. An abstract engine does not descend +/// into a callee (it [summarizes](Self::summarize_call) the call), so requiring +/// it to expose concrete activation allocation, activation cleanup, +/// `resolve_call`, and `enter_function` would be demanding a call convention it +/// never performs. `cfg_entry` is likewise absent: the forward abstract engine +/// reaches a callable body's entry block through [`Owner`](crate::Owner) seeding +/// in the fixpoint driver, not by asking a frame to enter a CFG. A frame that +/// *does* want either capability can name it in addition — see the +/// abstract-body-traversal follow-up. /// /// Implemented by [`SparseForwardInterpreter`](crate::SparseForwardInterpreter). -/// The standard abstract frames are generic over `I: ForwardDataflowFrameDriver`, -/// so a custom forward-dataflow frame can drive any engine providing these -/// capabilities. +/// The standard abstract frames are generic over +/// `I: ForwardDataflowFrameEngine`, so a custom forward-dataflow frame can drive +/// any engine providing these capabilities. /// /// The interprocedural protocol stays **atomic in the engine**: `summarize_call` /// performs the whole call-summarization step (resolve, key, join arguments into @@ -198,10 +374,9 @@ pub trait ForwardFrameDriver: Env { /// self-recursion* — and read the current return summary or `bottom`), so a /// custom frame cannot reorder it and break soundness. Frames only decide /// *traversal*: which frame to step next. -/// -/// Re-exported as [`AbstractFrameDriver`](crate::AbstractFrameDriver) for backward -/// compatibility. -pub trait ForwardDataflowFrameDriver: ForwardFrameDriver { +pub trait ForwardDataflowFrameEngine: + Env + StatementDispatch + BlockQueries + DiGraphQueries +{ /// The key under which function entry/return summaries are tracked /// (the analysis [`CallContext::Key`](crate::CallContext::Key)). type SummaryKey: Clone + Eq + Hash; diff --git a/crates/kirin-interpreter/src/core/interp.rs b/crates/kirin-interpreter/src/core/interp.rs index 2916cfc4e2..a28136ba4d 100644 --- a/crates/kirin-interpreter/src/core/interp.rs +++ b/crates/kirin-interpreter/src/core/interp.rs @@ -69,6 +69,34 @@ pub trait Env: Interp { value: SSAValue, data: Self::Value, ) -> Result<(), Self::Error>; + + /// Positionally bind runtime values to SSA slots in an **explicitly + /// selected** activation, checking arity. + /// + /// The explicitly-addressed counterpart of + /// [`SparseForwardInterp::write_results`], which always binds into the + /// engine's *current* activation ([`Interp::index`]). Frames need this one: + /// a frame binds results into the activation it owns, which is not + /// necessarily the one a dialect rule is executing in. The two differ by + /// *which activation*, not by what they do — hence neither name mentions the + /// [`Product`] container. + fn bind_values( + &mut self, + index: EnvIndex, + slots: &[SSAValue], + values: Product, + ) -> Result<(), Self::Error> { + if slots.len() != values.len() { + return Err(Self::Error::from(InterpreterError::ProductArityMismatch { + expected: slots.len(), + actual: values.len(), + })); + } + for (slot, value) in slots.iter().copied().zip(values) { + self.env_write(index, slot, value)?; + } + Ok(()) + } } /// [`SparseForwardShape`](crate::SparseForwardShape)-engine flavor: env diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index cdcd5f9b0b..b05588c4cf 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -1,6 +1,7 @@ //! The shared interpreter chassis: the engine trait ([`Interp`]) and dialect //! dispatch ([`Interpretable`]), effect types, the direction-neutral frame -//! protocol, activation storage, calling conventions, errors, and IR queries. +//! protocol, activation storage, calling conventions, errors, and the IR +//! queries ([`query`]) engines run against a stage. //! Everything here is engine-agnostic; the engines compose these pieces. pub(crate) mod dispatch; @@ -14,13 +15,14 @@ pub(crate) mod query; pub(crate) mod value; pub use dispatch::{FunctionEntry, InterpDispatch, Interpretable}; -pub use effect::{CallEffect, Callee, Edge, FunctionBody, SparseForwardEffect}; +pub use effect::{Body, CallEffect, CallableBody, Callee, Edge, SparseForwardEffect}; pub use env::{EnvIndex, EnvStackStore, Store}; pub use error::InterpreterError; pub use frame::{ - ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameEffect, FrameEngine, drive_frames, + BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, }; pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; -pub use query::StageQuery; +pub use query::{GraphWalkPlan, StageQuery, TerminatorArgs}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index 3204296027..565e5ba848 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -6,15 +6,26 @@ //! kirin-ir's `StageDispatch` machinery; [`StageQuery`] bundles them into one //! bound that any well-formed stage enum satisfies automatically. +use crate::Body; +use crate::InterpreterError; use kirin_ir::{ - Block, CFG, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasCFG, HasStageInfo, - HasSuccessors, Pipeline, SSAKind, SSAValue, SpecializedFunction, StageAction, StageInfo, - StageMeta, StagedFunction, Statement, SupportsStageDispatch, Symbol, - UniqueLiveSpecializationError, + Block, BlockParent, CFG, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasCFG, + HasDigraphs, HasStageInfo, HasUngraphs, Pipeline, PortParent, SSAKind, SSAValue, + SpecializedFunction, StageAction, StageInfo, StageMeta, StagedFunction, Statement, + SupportsStageDispatch, Symbol, UniqueLiveSpecializationError, }; +use smallvec::{SmallVec, smallvec}; -use crate::InterpreterError; -use crate::facts::topology::{self, CFGTopology}; +/// Operands yielded by a structured body's terminator. +/// +/// Most structured operations yield zero, one, or two values. Wider products +/// remain supported by spilling to the heap. +pub type TerminatorArgs = SmallVec<[SSAValue; 2]>; + +/// Statements that can translate demand on a block argument. +/// +/// Capacity matches [`kirin_ir::BlockInfo`]'s inline predecessor capacity. +pub(crate) type BlockArgumentPredecessorStatements = SmallVec<[Statement; 4]>; /// Block parameters as SSA values. pub struct BlockParams(pub Block); @@ -95,6 +106,61 @@ where } } +/// Last statement of a block (the cached terminator when present, otherwise +/// the tail of the statement list). +pub struct LastStatement(pub Block); + +impl StageAction for LastStatement +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Option; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + self.0 + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.0))?; + Ok(self.0.last_statement(info)) + } +} + +/// Statement before `before` within `block`, starting after the cached +/// terminator and then walking the statement list backwards. +pub struct PreviousStatement { + pub block: Block, + pub before: Statement, +} + +impl StageAction for PreviousStatement +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Option; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + self.block + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.block))?; + if self.block.terminator(info) == Some(self.before) { + Ok(self.block.statements(info).next_back()) + } else { + Ok(*self.before.prev(info)) + } + } +} + /// Entry block of a CFG. pub struct CFGEntry(pub CFG); @@ -115,6 +181,52 @@ where } } +/// Everything the default digraph walker needs: the boundary ports, the +/// node statements in topological order, and the graph's yields. +#[derive(Clone, Debug)] +pub struct GraphWalkPlan { + pub ports: Vec, + pub schedule: Vec, + pub yields: Vec, +} + +/// The walk plan of a digraph body (ports, toposorted nodes, yields). +/// +/// Fails with [`InterpreterError::GraphHasCycle`] on cyclic digraphs: they +/// are structurally legal IR but have no single-pass execution order. +pub struct DiGraphWalkQuery(pub kirin_ir::DiGraph); + +impl StageAction for DiGraphWalkQuery +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = GraphWalkPlan; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + let graph_info = self + .0 + .get_info(info) + .ok_or(InterpreterError::GraphHasCycle(self.0))?; + let order = petgraph::algo::toposort(graph_info.graph(), None) + .map_err(|_| InterpreterError::GraphHasCycle(self.0))?; + let schedule = order + .into_iter() + .map(|node| graph_info.graph()[node]) + .collect(); + Ok(GraphWalkPlan { + ports: graph_info.ports().to_vec(), + schedule, + yields: graph_info.yields().to_vec(), + }) + } +} + /// The unique live specialization of a staged function. pub struct UniqueSpecialization(pub StagedFunction); @@ -208,7 +320,7 @@ where L: Dialect, for<'a> L: HasArguments<'a>, { - type Output = Vec; + type Output = TerminatorArgs; type Error = InterpreterError; fn run( @@ -224,23 +336,25 @@ where .definition(info) .arguments() .copied() - .collect::>() + .collect::() }) .unwrap_or_default()) } } -/// The topology of a CFG: blocks (including nested structured bodies), -/// statements per block, CFG successors, and block feeders. -pub struct CFGTopologyQuery(pub CFG); +/// Statements whose backward rules translate demand on a block argument. +/// +/// A directly owned single-block body is translated by its structural owner. +/// A CFG block is translated by the terminator of each block in its finalized +/// predecessor index. +pub struct BlockArgumentPredecessors(pub Block); -impl StageAction for CFGTopologyQuery +impl StageAction for BlockArgumentPredecessors where S: StageMeta + HasStageInfo, L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a>, { - type Output = CFGTopology; + type Output = BlockArgumentPredecessorStatements; type Error = InterpreterError; fn run( @@ -248,7 +362,151 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - Ok(topology::cfg_topology(info, &self.0)) + let block = self + .0 + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.0))?; + + match block.parent { + Some(BlockParent::Statement(owner)) => Ok(smallvec![owner]), + Some(BlockParent::CFG(_)) => block + .predecessors + .iter() + .map(|predecessor| { + predecessor.terminator(info).ok_or(InterpreterError::Custom( + "CFG predecessor block has no terminator", + )) + }) + .collect(), + None => Ok(SmallVec::new()), + } + } +} + +/// The statement structurally owning a graph port's parent graph. +/// +/// The port's [`PortParent`] identifies the authoritative graph, whose +/// `GraphInfo::parent` field identifies the statement whose dialect rule +/// translates values and demand across the graph boundary. +pub struct GraphPortOwner(pub PortParent); + +impl StageAction for GraphPortOwner +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Statement; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + let owner = match self.0 { + PortParent::DiGraph(graph) => graph.get_info(info).and_then(|graph| graph.parent()), + PortParent::UnGraph(graph) => graph.get_info(info).and_then(|graph| graph.parent()), + }; + owner.ok_or(InterpreterError::Custom( + "graph port has no owning statement", + )) + } +} + +/// Blocks directly selected as dense fixpoint owners by an analysis root. +pub struct DirectBodyBlocks(pub Body); + +impl StageAction for DirectBodyBlocks +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Vec; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + Ok(match self.0 { + Body::CFG(cfg) => cfg.blocks(info).collect(), + Body::Block(block) => vec![block], + Body::DiGraph(_) | Body::UnGraph(_) => Vec::new(), + }) + } +} + +/// One step of a body-containment walk: the statements directly in this body +/// part and the child body parts reached from it. +pub struct BodyContents { + pub statements: Vec, + pub children: Vec, +} + +pub struct BodyContentsQuery(pub Body); + +impl StageAction for BodyContentsQuery +where + S: StageMeta + HasStageInfo, + L: Dialect, + for<'a> L: HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, +{ + type Output = BodyContents; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + let (statements, mut children) = match self.0 { + Body::CFG(cfg) => ( + Vec::new(), + cfg.blocks(info).map(Body::Block).collect::>(), + ), + Body::Block(block) => { + block + .get_info(info) + .ok_or(InterpreterError::MissingBlock(block))?; + let mut statements: Vec = block.statements(info).collect(); + if let Some(terminator) = block.terminator(info) { + statements.push(terminator); + } + (statements, Vec::new()) + } + Body::DiGraph(graph) => ( + graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + Vec::new(), + ), + Body::UnGraph(graph) => ( + graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + Vec::new(), + ), + }; + + for &statement in &statements { + let definition = statement.definition(info); + children.extend(definition.blocks().copied().map(Body::Block)); + children.extend(definition.cfgs().copied().map(Body::CFG)); + children.extend(definition.digraphs().copied().map(Body::DiGraph)); + children.extend(definition.ungraphs().copied().map(Body::UnGraph)); + } + + Ok(BodyContents { + statements, + children, + }) } } @@ -282,6 +540,8 @@ pub trait StageQuery: + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch< UniqueSpecialization, @@ -290,8 +550,15 @@ pub trait StageQuery: > + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch - + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch + + SupportsStageDispatch< + BlockArgumentPredecessors, + BlockArgumentPredecessorStatements, + InterpreterError, + > + SupportsStageDispatch + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch + + SupportsStageDispatch { } @@ -300,6 +567,8 @@ impl StageQuery for S where + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch< UniqueSpecialization, @@ -308,12 +577,19 @@ impl StageQuery for S where > + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch - + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch + + SupportsStageDispatch< + BlockArgumentPredecessors, + BlockArgumentPredecessorStatements, + InterpreterError, + > + SupportsStageDispatch + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch + + SupportsStageDispatch { } - -/// Run a stage action against the stage with id `stage`. +// TODO: add caching (with red-green tree) to avoid repeated queries for the same stage and block/statement. +// Run a stage action against the stage with id `stage`. pub(crate) fn dispatch( pipeline: &Pipeline, stage: CompileStage, @@ -354,6 +630,23 @@ pub(crate) fn next_statement( dispatch(pipeline, stage, NextStatement { block, after }) } +pub(crate) fn last_statement( + pipeline: &Pipeline, + stage: CompileStage, + block: Block, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, LastStatement(block)) +} + +pub(crate) fn previous_statement( + pipeline: &Pipeline, + stage: CompileStage, + block: Block, + before: Statement, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, PreviousStatement { block, before }) +} + pub(crate) fn cfg_entry( pipeline: &Pipeline, stage: CompileStage, @@ -398,14 +691,46 @@ pub(crate) fn terminator_arguments( pipeline: &Pipeline, stage: CompileStage, block: Block, -) -> Result, InterpreterError> { +) -> Result { dispatch(pipeline, stage, TerminatorArguments(block)) } -pub(crate) fn cfg_topology( +pub(crate) fn block_argument_predecessors( pipeline: &Pipeline, stage: CompileStage, - cfg: CFG, -) -> Result { - dispatch(pipeline, stage, CFGTopologyQuery(cfg)) + block: Block, +) -> Result { + dispatch(pipeline, stage, BlockArgumentPredecessors(block)) +} + +pub(crate) fn graph_port_owner( + pipeline: &Pipeline, + stage: CompileStage, + parent: PortParent, +) -> Result { + dispatch(pipeline, stage, GraphPortOwner(parent)) +} + +pub(crate) fn digraph_walk_plan( + pipeline: &Pipeline, + stage: CompileStage, + graph: kirin_ir::DiGraph, +) -> Result { + dispatch(pipeline, stage, DiGraphWalkQuery(graph)) +} + +pub(crate) fn direct_body_blocks( + pipeline: &Pipeline, + stage: CompileStage, + body: Body, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, DirectBodyBlocks(body)) +} + +pub(crate) fn body_contents( + pipeline: &Pipeline, + stage: CompileStage, + body: Body, +) -> Result { + dispatch(pipeline, stage, BodyContentsQuery(body)) } diff --git a/crates/kirin-interpreter/src/engines/concrete/frames.rs b/crates/kirin-interpreter/src/engines/concrete/frames.rs deleted file mode 100644 index 92c3c16d83..0000000000 --- a/crates/kirin-interpreter/src/engines/concrete/frames.rs +++ /dev/null @@ -1,392 +0,0 @@ -//! The **concrete** implementation of the shared [`frame`](crate::core::frame) -//! protocol. -//! -//! These are the default total frames for [`ConcreteInterpreter`](crate::ConcreteInterpreter): -//! [`BodyFrame`] (walks a function-body CFG or a single body block) and -//! [`CallFrame`] (call/return). They implement the shared [`Frame`] trait by -//! consuming the dialect [`SparseForwardEffect`] and driving a single deterministic -//! path. Structured-control dialects do not get a framework "scope": they push -//! a frame **they own** through [`SparseForwardEffect::Push`] (that frame may build a -//! [`BodyFrame`] to walk a chosen body — a reusable building block, not -//! framework-owned structured semantics). A language that combines such a -//! dialect defines its own total frame enum embedding [`BodyFrame`]/[`CallFrame`] -//! via [`FrameBuild`] plus its dialect frames. The forward abstract analogue -//! lives in [`sparse_forward::frames`](crate::engines::sparse_forward::frames). - -use kirin_ir::{Block, CFG, CompileStage, Product, SSAValue, Statement}; - -use crate::{ - CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, - SparseForwardEffect, SparseForwardInterp, -}; - -/// Completion payloads produced by the standard concrete frames. -/// -/// `Returned` bubbles a function return across frames to the enclosing -/// [`CallFrame`]; `Finished` carries the values a pushed body frame yielded back -/// to whoever pushed it (written into that push's result slots). -pub enum Completion { - /// A function returned these values; bubbles to the enclosing - /// [`CallFrame`], or finishes the run at the root. - Returned(Product), - /// A pushed body frame yielded these values to its pusher. - Finished(Product), -} - -/// Construction trait letting any total frame enum embed the standard concrete -/// frames. -/// -/// The default [`StandardFrame`] implements it trivially; a language that adds -/// structured-control dialects implements it on its own enum to reuse -/// [`BodyFrame`]/[`CallFrame`] traversal while adding its own dialect frames. -pub trait FrameBuild: Sized { - fn from_body(frame: BodyFrame) -> Self; - fn from_call(frame: CallFrame) -> Self; -} - -/// Traversal of one body: a function-body CFG (multi-block, with jumps) -/// or a single body block (scf-style, terminated by a yield). -pub struct BodyFrame { - stage: CompileStage, - index: EnvIndex, - owns_env: bool, - function_boundary: bool, - block: Block, - cursor: Option, - /// Entry arguments not yet bound. A body frame built by a dialect frame is - /// constructed without engine access — it binds on its first `step`, so - /// building it requires no [`FrameDriver`] (a dialect frame builds these as - /// plain values, no engine capability or trait-resolution cycle). - pending: Option>, - /// Result slots awaiting a pushed body frame's `Finished` completion. - resume_slots: Option>, - _marker: std::marker::PhantomData (V, E)>, -} - -impl BodyFrame -where - V: Clone, - E: From, -{ - /// Walk a function body: start at the entry block of `cfg`, binding - /// `args` to its parameters. Owns the activation and is the return boundary. - pub fn function( - interp: &mut I, - stage: CompileStage, - index: EnvIndex, - cfg: CFG, - args: Product, - ) -> Result - where - I: FrameDriver, - { - let entry = interp - .cfg_entry(stage, cfg)? - .ok_or_else(|| E::from(InterpreterError::EmptyCFG))?; - Self::start(interp, stage, index, entry, args, true, true) - } - - /// A single body block (scf-style), to bind `args` to its parameters on the - /// first step. Borrows the caller's activation and is not a return boundary. - /// Pure construction — needs no engine access. - pub fn block(stage: CompileStage, index: EnvIndex, block: Block, args: Product) -> Self { - Self { - stage, - index, - owns_env: false, - function_boundary: false, - block, - cursor: None, - pending: Some(args), - resume_slots: None, - _marker: std::marker::PhantomData, - } - } - - fn start( - interp: &mut I, - stage: CompileStage, - index: EnvIndex, - block: Block, - args: Product, - owns_env: bool, - function_boundary: bool, - ) -> Result - where - I: FrameDriver, - { - interp.bind_block_args(stage, index, block, &args)?; - let cursor = interp.first_statement(stage, block)?; - Ok(Self { - stage, - index, - owns_env, - function_boundary, - block, - cursor, - pending: None, - resume_slots: None, - _marker: std::marker::PhantomData, - }) - } - - /// Execute the next statement and translate its [`SparseForwardEffect`] into a - /// [`FrameEffect`] over the total frame type `F`. - pub fn step_into(mut self, interp: &mut I) -> Result>, E> - where - I: FrameDriver + SparseForwardInterp, - F: FrameBuild, - { - // Bind entry arguments lazily on the first step (a dialect-built body - // frame carries them unbound). - if let Some(args) = self.pending.take() { - interp.bind_block_args(self.stage, self.index, self.block, &args)?; - self.cursor = interp.first_statement(self.stage, self.block)?; - return Ok(FrameEffect::Continue(F::from_body(self))); - } - let Some(statement) = self.cursor else { - return Err(E::from(if self.function_boundary { - InterpreterError::FunctionBodyFellThrough - } else { - InterpreterError::BlockFellThrough(self.block) - })); - }; - self.cursor = interp.next_statement(self.stage, self.block, statement)?; - - match interp.run_statement(self.stage, statement, self.index)? { - SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_body(self))), - SparseForwardEffect::Jump(edge) => { - interp.bind_block_args(self.stage, self.index, edge.target, &edge.args)?; - self.cursor = interp.first_statement(self.stage, edge.target)?; - self.block = edge.target; - Ok(FrameEffect::Continue(F::from_body(self))) - } - SparseForwardEffect::Branch(_) => Err(E::from(InterpreterError::IndeterminateBranch)), - SparseForwardEffect::Push { frame, results } => { - self.resume_slots = Some(results); - Ok(FrameEffect::Push { - parent: F::from_body(self), - child: frame, - }) - } - SparseForwardEffect::Call(call) => { - let pending = CallFrame::pending(self.stage, self.index, call); - Ok(FrameEffect::Push { - parent: F::from_body(self), - child: F::from_call(pending), - }) - } - SparseForwardEffect::Yield(values) => { - if self.function_boundary { - return Err(E::from(InterpreterError::Custom( - "yield reached a function boundary", - ))); - } - Ok(FrameEffect::Complete(Completion::Finished(values))) - } - SparseForwardEffect::Return(values) => self.finish_return::(interp, values), - } - } - - /// A child finished without a payload (its results are already in the - /// shared index, e.g. a returned call): resume at the advanced cursor. - pub fn resume_done_into(self) -> FrameEffect> - where - F: FrameBuild, - { - FrameEffect::Continue(F::from_body(self)) - } - - /// A child bubbled a completion: a pushed body frame `Finished` (write its - /// values into the pending slots and continue) or a `Returned` (a return - /// happened in the child — keep bubbling). - pub fn resume_into( - mut self, - completion: Completion, - interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - match completion { - Completion::Finished(values) => { - let slots = self.resume_slots.take().ok_or_else(|| { - E::from(InterpreterError::Custom("body resume without result slots")) - })?; - interp.write_results(self.index, &slots, values)?; - Ok(FrameEffect::Continue(F::from_body(self))) - } - Completion::Returned(values) => self.finish_return::(interp, values), - } - } - - /// Produce a `Returned` completion, freeing the activation record when this - /// frame is the owning function boundary. - fn finish_return( - self, - interp: &mut I, - values: Product, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - if self.function_boundary && self.owns_env { - interp.free_env(self.index)?; - } - Ok(FrameEffect::Complete(Completion::Returned(values))) - } -} - -/// Call/return bookkeeping: dispatch a function invocation, then await its -/// return and land the results in the caller's activation. -pub enum CallFrame { - /// Not yet dispatched: resolve the callee, enter its body. - Pending { - resolve_stage: CompileStage, - callee: Callee, - args: Product, - caller_env: EnvIndex, - results: Product, - }, - /// Dispatched: awaiting the callee's `Returned` completion. - Awaiting { - caller_env: EnvIndex, - results: Product, - }, -} - -impl CallFrame -where - V: Clone, -{ - /// Build a pending call frame from a [`CallEffect`]. - pub fn pending(scope_stage: CompileStage, caller_env: EnvIndex, call: CallEffect) -> Self { - CallFrame::Pending { - resolve_stage: call.stage.unwrap_or(scope_stage), - callee: call.callee, - args: call.args, - caller_env, - results: call.results, - } - } - - pub fn step_into(self, interp: &mut I) -> Result>, I::Error> - where - I: FrameDriver, - I::Error: From, - F: FrameBuild, - { - match self { - CallFrame::Pending { - resolve_stage, - callee, - args, - caller_env, - results, - } => { - let target = interp.resolve_call(resolve_stage, &callee)?; - let index = interp.alloc_env(); - let body = interp.enter_function(target.stage, target.body, args, index)?; - let frame = BodyFrame::function(interp, target.stage, index, body.cfg, body.args)?; - Ok(FrameEffect::Push { - parent: F::from_call(CallFrame::Awaiting { - caller_env, - results, - }), - child: F::from_body(frame), - }) - } - CallFrame::Awaiting { .. } => Err(I::Error::from(InterpreterError::Custom( - "call frame stepped while awaiting a return", - ))), - } - } - - pub fn resume_done_into(self) -> Result>, InterpreterError> { - Err(InterpreterError::Custom( - "call frame resumed without a return", - )) - } - - pub fn resume_into( - self, - completion: Completion, - interp: &mut I, - ) -> Result>, I::Error> - where - I: FrameDriver, - I::Error: From, - F: FrameBuild, - { - match (self, completion) { - ( - CallFrame::Awaiting { - caller_env, - results, - }, - Completion::Returned(values), - ) => { - interp.write_results(caller_env, &results, values)?; - Ok(FrameEffect::Done) - } - (CallFrame::Awaiting { .. }, Completion::Finished(_)) => Err(I::Error::from( - InterpreterError::Custom("call frame resumed with a body completion"), - )), - (CallFrame::Pending { .. }, _) => Err(I::Error::from(InterpreterError::Custom( - "call frame resumed before dispatch", - ))), - } - } -} - -/// The default total concrete frame enum: standard concrete traversal (no -/// structured-control dialect frames). -pub enum StandardFrame { - Body(BodyFrame), - Call(CallFrame), -} - -impl FrameBuild for StandardFrame { - fn from_body(frame: BodyFrame) -> Self { - StandardFrame::Body(frame) - } - fn from_call(frame: CallFrame) -> Self { - StandardFrame::Call(frame) - } -} - -impl Frame for StandardFrame -where - I: FrameDriver + SparseForwardInterp>, - V: Clone, - E: From, -{ - type Completion = Completion; - - fn step(self, interp: &mut I) -> Result, I::Error> { - match self { - StandardFrame::Body(frame) => frame.step_into::(interp), - StandardFrame::Call(frame) => frame.step_into::(interp), - } - } - - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { - match self { - StandardFrame::Body(frame) => Ok(frame.resume_done_into::()), - StandardFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), - } - } - - fn resume( - self, - completion: Self::Completion, - interp: &mut I, - ) -> Result, I::Error> { - match self { - StandardFrame::Body(frame) => frame.resume_into::(completion, interp), - StandardFrame::Call(frame) => frame.resume_into::(completion, interp), - } - } -} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs new file mode 100644 index 0000000000..d220ff3270 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs @@ -0,0 +1,108 @@ +use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; + +use crate::core::frame::BlockBinding; +use crate::{BlockQueries, Env, EnvIndex, InterpreterError}; + +/// Block-cursor mechanics shared by the block-shaped walkers +/// ([`BlockFrame`](super::BlockFrame) and [`CFGFrame`](super::CFGFrame)): +/// the current block, the statement cursor, lazily bound entry arguments, +/// and the result slots awaiting a pushed child's completion values. +/// +/// Traversal state only — no environment ownership, no invocation role. +pub(super) struct BlockCursor { + pub(super) stage: CompileStage, + pub(super) index: EnvIndex, + pub(super) block: Block, + cursor: Option, + /// Entry arguments not yet bound. A frame built by a dialect frame is + /// constructed without engine access — it binds on its first `step`, so + /// construction needs no engine access. + pending: Option>, + /// Result slots awaiting a pushed child frame's completion values. + resume_slots: Option>, +} + +impl BlockCursor { + pub(super) fn new( + stage: CompileStage, + index: EnvIndex, + block: Block, + args: Product, + ) -> Self { + Self { + stage, + index, + block, + cursor: None, + pending: Some(args), + resume_slots: None, + } + } + + /// Bind pending entry arguments to the block's parameters and position + /// the cursor at its first statement. Returns `true` if binding happened + /// on this call (the frame should `Continue` and step again). + pub(super) fn bind_entry(&mut self, interp: &mut I) -> Result + where + I: Env + BlockQueries, + { + match self.pending.take() { + Some(args) => { + interp.bind_block_args(self.stage, self.index, self.block, &args)?; + self.cursor = interp.first_statement(self.stage, self.block)?; + Ok(true) + } + None => Ok(false), + } + } + + /// Take the current statement, advancing the cursor past it. + pub(super) fn advance(&mut self, interp: &I) -> Result, I::Error> + where + I: BlockQueries, + { + let Some(statement) = self.cursor else { + return Ok(None); + }; + self.cursor = interp.next_statement(self.stage, self.block, statement)?; + Ok(Some(statement)) + } + + /// Move to `target` (a CFG jump): bind its parameters and reset the + /// cursor to its first statement. + pub(super) fn enter_block( + &mut self, + interp: &mut I, + target: Block, + args: &Product, + ) -> Result<(), I::Error> + where + I: Env + BlockQueries, + { + interp.bind_block_args(self.stage, self.index, target, args)?; + self.cursor = interp.first_statement(self.stage, target)?; + self.block = target; + Ok(()) + } + + /// Stash the result slots of a `Push` until the child completes. + pub(super) fn expect_results(&mut self, results: Product) { + self.resume_slots = Some(results); + } + + /// Write a completed child's values into the stashed result slots. + pub(super) fn write_child_results( + &mut self, + interp: &mut I, + values: Product, + ) -> Result<(), I::Error> + where + I: Env, + I::Error: From, + { + let slots = self.resume_slots.take().ok_or_else(|| { + I::Error::from(InterpreterError::Custom("body resume without result slots")) + })?; + interp.bind_values(self.index, slots.as_slice(), values) + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs new file mode 100644 index 0000000000..01d182fdee --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs @@ -0,0 +1,117 @@ +use kirin_ir::{Block, CompileStage, Product}; + +use crate::{ + BlockQueries, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, StatementDispatch, +}; + +use super::block_cursor::BlockCursor; +use super::{CallFrame, Completion, FrameBuild}; + +/// Representation walker for exactly one [`Block`]: bind its parameters, +/// run its statements in order, and surface the exit through the +/// [`Completion`] protocol — `Return` as +/// [`Returned`](Completion::Returned), `Yield` as +/// [`Yielded`](Completion::Yielded). +/// +/// Traversal mechanics only. A `BlockFrame` does not own an activation, is +/// not a call boundary, and does not know whether it is a callable function +/// body ([`CallFrame`] → `BlockFrame`) or a nested structured-operation body +/// (dialect frame → `BlockFrame`): the parent frame defines the role and +/// interprets the completion. CFG transitions (`Jump`/`Branch`) are rejected +/// — a single block owns no CFG edges; multi-block traversal is +/// [`CFGFrame`](super::CFGFrame)'s job. +pub struct BlockFrame { + cursor: BlockCursor, + _marker: std::marker::PhantomData E>, +} + +impl BlockFrame +where + V: Clone, + E: From, +{ + /// Walk `block`, binding `args` to its parameters on the first step. + /// Pure construction — needs no engine access, so a dialect frame can + /// build one as plain values. + pub fn new(stage: CompileStage, index: EnvIndex, block: Block, args: Product) -> Self { + Self { + cursor: BlockCursor::new(stage, index, block, args), + _marker: std::marker::PhantomData, + } + } +} + +impl Frame for BlockFrame +where + I: BlockQueries + StatementDispatch + SparseForwardInterp, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; + + /// Execute the next statement and translate its [`SparseForwardEffect`] + /// into a [`FrameEffect`] over the total frame type `F`. + fn step_into(mut self, interp: &mut I) -> Result>, E> { + if self.cursor.bind_entry(interp)? { + return Ok(FrameEffect::Continue(F::from_block(self))); + } + let Some(statement) = self.cursor.advance(interp)? else { + return Err(E::from(InterpreterError::BlockFellThrough( + self.cursor.block, + ))); + }; + + match interp.run_statement(self.cursor.stage, statement, self.cursor.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_block(self))), + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CFGControlFlowInStructuredBody)) + } + SparseForwardEffect::Push { frame, results } => { + self.cursor.expect_results(results); + Ok(FrameEffect::Push { + parent: F::from_block(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.cursor.stage, self.cursor.index, call); + Ok(FrameEffect::Push { + parent: F::from_block(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Yield(values) => { + Ok(FrameEffect::Complete(Completion::Yielded(values))) + } + SparseForwardEffect::Return(values) => { + Ok(FrameEffect::Complete(Completion::Returned(values))) + } + } + } + + /// A child finished without a payload (its results are already in the + /// shared activation, e.g. a returned call): resume at the advanced + /// cursor. + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_block(self))) + } + + /// A child bubbled a completion: a pushed frame's values land in the + /// push's result slots; a `Returned` keeps bubbling toward the nearest + /// [`CallFrame`] (this frame owns no activation to free). + fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match completion { + Completion::Finished(values) | Completion::Yielded(values) => { + self.cursor.write_child_results(interp, values)?; + Ok(FrameEffect::Continue(F::from_block(self))) + } + Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs new file mode 100644 index 0000000000..5d63080b50 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -0,0 +1,214 @@ +use kirin_ir::{CompileStage, Product, SSAValue}; + +use crate::{ + Body, CallEffect, CallServices, Callee, EnvIndex, Frame, FrameEffect, InterpreterError, +}; + +use super::{BodyFrameEntry, CallBodyFramePolicy, Completion, DefaultBodyFrames, FrameBuild}; + +/// The function-call boundary frame: interpreter runtime bookkeeping, not a +/// function dialect operation and not the callable itself. +/// +/// A `CallFrame` owns the whole activation lifecycle that representation +/// walkers deliberately don't: +/// +/// 1. resolve the callee through the engine's [`Linker`](crate::Linker); +/// 2. allocate the callee activation; +/// 3. ask [`FunctionEntry`](crate::FunctionEntry) for the callable body +/// descriptor ([`CallableBody`](crate::CallableBody)); +/// 4. select the entry frame for the closed [`Body`] variant — **delegated to +/// the `P` policy** ([`CallBodyFramePolicy`]), which defaults to +/// [`DefaultBodyFrames`]: `CFG` → `CFGFrame`, `Block` → `BlockFrame`, +/// `DiGraph` → `DiGraphFrame`, `UnGraph` → the dialect/compiler hook +/// ([`FrameBuild::from_ungraph_entry`]). Everything else in this list is +/// fixed: a custom policy chooses walkers, never the lifecycle; +/// 5. suspend while the callee frame runs; +/// 6. validate the callee's completion kind ([`Returned`](Completion::Returned) +/// or a graph's natural [`Finished`](Completion::Finished) are returns; a +/// structured [`Yielded`](Completion::Yielded) is an error); +/// 7. free the callee activation exactly once; +/// 8. deliver the returned values — into the caller's result slots for a +/// nested call, or as the run's completion for a root call +/// ([`ConcreteInterpreter::call`](crate::ConcreteInterpreter::call) pushes +/// a [`CallFrame::root`], so root and nested calls share this one +/// boundary implementation). +pub struct CallFrame { + state: CallState, + /// Which walkers enter the callee body. Selected by the total frame type + /// via [`FrameBuild::BodyFrames`]; the lifecycle above is unaffected. + _policy: std::marker::PhantomData P>, +} + +enum CallState { + /// Not yet dispatched: resolve the callee and enter its body. + Pending { + resolve_stage: CompileStage, + callee: Callee, + args: Product, + dest: CallDest, + }, + /// Dispatched: the callee frame is running. Holds the callee activation + /// so the boundary frees it exactly once on completion. + Awaiting { + callee_env: EnvIndex, + dest: CallDest, + }, +} + +/// Where a finished call delivers its returned values. +enum CallDest { + /// Write into result slots of the calling activation and resume the + /// caller. + Caller { + env: EnvIndex, + results: Product, + }, + /// A root call: complete the frame stack with the values. + Root, +} + +impl CallFrame +where + V: Clone, +{ + /// A call issued by a statement ([`SparseForwardEffect::Call`](crate::SparseForwardEffect::Call)): + /// returned values land in `call.results` of the caller's activation. + pub fn pending(scope_stage: CompileStage, caller_env: EnvIndex, call: CallEffect) -> Self { + CallFrame { + state: CallState::Pending { + resolve_stage: call.stage.unwrap_or(scope_stage), + callee: call.callee, + args: call.args, + dest: CallDest::Caller { + env: caller_env, + results: call.results, + }, + }, + _policy: std::marker::PhantomData, + } + } + + /// A root call (no calling activation): the returned values complete the + /// frame stack. + pub fn root(stage: CompileStage, callee: Callee, args: Product) -> Self { + CallFrame { + state: CallState::Pending { + resolve_stage: stage, + callee, + args, + dest: CallDest::Root, + }, + _policy: std::marker::PhantomData, + } + } +} + +impl Frame for CallFrame +where + I: CallServices, + F: FrameBuild, + P: CallBodyFramePolicy, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { + match self.state { + CallState::Pending { + resolve_stage, + callee, + args, + dest, + } => { + let target = interp.resolve_call(resolve_stage, &callee)?; + let index = interp.alloc_env(); + let entry = interp.enter_function(target.stage, target.body, args, index)?; + // The closed `Body` enum is the framework's supported body + // vocabulary, so this match is intentionally exhaustive; + // only the `UnGraph` arm delegates to a language policy. + // `Body` is a closed vocabulary, so this match stays + // exhaustive; only *which frame* each arm builds is + // configurable, via the `P` policy. Activation ownership and + // completion handling deliberately stay out of the policy. + let child = match entry.body { + Body::CFG(cfg) => P::from_cfg(BodyFrameEntry { + stage: target.stage, + index, + body: cfg, + args: entry.args, + })?, + Body::Block(block) => P::from_block(BodyFrameEntry { + stage: target.stage, + index, + body: block, + args: entry.args, + })?, + Body::DiGraph(graph) => P::from_digraph(BodyFrameEntry { + stage: target.stage, + index, + body: graph, + args: entry.args, + })?, + Body::UnGraph(graph) => P::from_ungraph(BodyFrameEntry { + stage: target.stage, + index, + body: graph, + args: entry.args, + })?, + }; + Ok(FrameEffect::Push { + parent: F::from_call(CallFrame { + state: CallState::Awaiting { + callee_env: index, + dest, + }, + _policy: std::marker::PhantomData, + }), + child, + }) + } + CallState::Awaiting { .. } => Err(E::from(InterpreterError::Custom( + "call frame stepped while awaiting a return", + ))), + } + } + + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Err(E::from(InterpreterError::Custom( + "call frame resumed without a return", + ))) + } + + /// The callee completed: validate the completion kind, free the callee + /// activation exactly once, and deliver the returned values. + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + let CallState::Awaiting { callee_env, dest } = self.state else { + return Err(E::from(InterpreterError::Custom( + "call frame resumed before dispatch", + ))); + }; + let values = match completion { + // An explicit `Return`, or a graph body's natural completion + // (a callable DiGraph's outputs are the call's returned values). + Completion::Returned(values) | Completion::Finished(values) => values, + Completion::Yielded(_) => { + return Err(E::from(InterpreterError::Custom( + "structured yield reached a function-call boundary (a callable body must exit with return)", + ))); + } + }; + interp.free_env(callee_env)?; + match dest { + CallDest::Caller { env, results } => { + interp.bind_values(env, results.as_slice(), values)?; + Ok(FrameEffect::Done) + } + CallDest::Root => Ok(FrameEffect::Complete(Completion::Returned(values))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs new file mode 100644 index 0000000000..fb3bb4d9df --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs @@ -0,0 +1,140 @@ +use kirin_ir::{CFG, CompileStage, Product}; + +use crate::{ + CFGQueries, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, StatementDispatch, +}; + +use super::block_cursor::BlockCursor; +use super::{CallFrame, Completion, FrameBuild}; + +/// Representation walker for a [`CFG`]: enter the entry block, run +/// statements, follow `Jump` edges between blocks (binding successor +/// arguments and resetting the cursor), and surface the exit through the +/// [`Completion`] protocol. +/// +/// Traversal mechanics only. A `CFGFrame` does not own an activation, is not +/// a call boundary, and does not decide whether a `Return` belongs to a +/// function call — it completes [`Returned`](Completion::Returned) and lets +/// the completion bubble to the nearest [`CallFrame`]. An undecided concrete +/// `Branch` is an error (single-path execution); exploring branch +/// alternatives is the abstract engine's business. +pub struct CFGFrame { + stage: CompileStage, + index: EnvIndex, + cfg: CFG, + /// Entry arguments awaiting the first step (the entry block is resolved + /// lazily, so construction needs no engine access). + pending: Option>, + /// The active block cursor; `None` until the entry block is resolved. + cursor: Option>, + _marker: std::marker::PhantomData E>, +} + +impl CFGFrame +where + V: Clone, + E: From, +{ + /// Walk `cfg` from its entry block, binding `args` to the entry block's + /// parameters on the first step. Pure construction — needs no engine + /// access. + pub fn new(stage: CompileStage, index: EnvIndex, cfg: CFG, args: Product) -> Self { + Self { + stage, + index, + cfg, + pending: Some(args), + cursor: None, + _marker: std::marker::PhantomData, + } + } +} + +impl Frame for CFGFrame +where + I: CFGQueries + StatementDispatch + SparseForwardInterp, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; + + /// Execute the next statement and translate its [`SparseForwardEffect`] + /// into a [`FrameEffect`] over the total frame type `F`. + fn step_into(mut self, interp: &mut I) -> Result>, E> { + // First step: find the entry block and bind the entry arguments. + if let Some(args) = self.pending.take() { + let entry = interp + .cfg_entry(self.stage, self.cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCFG))?; + let mut cursor = BlockCursor::new(self.stage, self.index, entry, args); + cursor.bind_entry(interp)?; + self.cursor = Some(cursor); + return Ok(FrameEffect::Continue(F::from_cfg(self))); + } + let cursor = self + .cursor + .as_mut() + .ok_or_else(|| E::from(InterpreterError::Custom("cfg frame stepped before entry")))?; + let Some(statement) = cursor.advance(interp)? else { + return Err(E::from(InterpreterError::BlockFellThrough(cursor.block))); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_cfg(self))), + SparseForwardEffect::Jump(edge) => { + cursor.enter_block(interp, edge.target, &edge.args)?; + Ok(FrameEffect::Continue(F::from_cfg(self))) + } + SparseForwardEffect::Branch(_) => Err(E::from(InterpreterError::IndeterminateBranch)), + SparseForwardEffect::Push { frame, results } => { + cursor.expect_results(results); + Ok(FrameEffect::Push { + parent: F::from_cfg(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.stage, self.index, call); + Ok(FrameEffect::Push { + parent: F::from_cfg(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Yield(values) => { + Ok(FrameEffect::Complete(Completion::Yielded(values))) + } + SparseForwardEffect::Return(values) => { + Ok(FrameEffect::Complete(Completion::Returned(values))) + } + } + } + + /// A child finished without a payload (its results are already in the + /// shared activation, e.g. a returned call): resume at the advanced + /// cursor. + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_cfg(self))) + } + + /// A child bubbled a completion: a pushed frame's values land in the + /// push's result slots; a `Returned` keeps bubbling toward the nearest + /// [`CallFrame`] (this frame owns no activation to free). + fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match completion { + Completion::Finished(values) | Completion::Yielded(values) => { + let cursor = self.cursor.as_mut().ok_or_else(|| { + E::from(InterpreterError::Custom("cfg frame resumed before entry")) + })?; + cursor.write_child_results(interp, values)?; + Ok(FrameEffect::Continue(F::from_cfg(self))) + } + Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs new file mode 100644 index 0000000000..147de26d3a --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs @@ -0,0 +1,181 @@ +use kirin_ir::{CompileStage, Product, SSAValue, Statement}; + +use crate::{ + DiGraphQueries, Env, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, StatementDispatch, +}; + +use super::{CallFrame, Completion, FrameBuild}; + +/// Representation-specific walker for a [`DiGraph`](kirin_ir::DiGraph) body: bind +/// entry arguments to the graph's boundary ports, run the node statements in +/// topological (dependency) order, and complete +/// [`Finished`](Completion::Finished) with the graph's declared yields. +/// +/// Traversal mechanics only. A `DiGraphFrame` does not own an activation and +/// does not know whether it is a callable graph-function body +/// ([`CallFrame`] → `DiGraphFrame`, where the yields become the call's +/// returned values) or a graph nested inside another body (dialect frame → +/// `DiGraphFrame`, where the yields return to the pushing operation): the +/// parent interprets the completion. +/// +/// The concrete execution policy requires a DAG: directed cycles are +/// rejected when the walk plan is built +/// ([`GraphHasCycle`](InterpreterError::GraphHasCycle)). This is a property +/// of this walker, not of the IR — a `DiGraph` may represent cycles. +/// +/// Pure construction — the walk plan is fetched and the ports are bound on +/// the first `step`, so a dialect frame can build one without engine access +/// (the same lazy pattern as [`BlockFrame`](super::BlockFrame)). CFG control +/// flow (`Jump`/`Branch`) and `Yield`/`Return` are errors inside a graph: a +/// digraph's outputs are its declared yields, not a statement effect. +pub struct DiGraphFrame { + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + /// Entry arguments not yet bound (bound on the first `step`). + pending: Option>, + /// Remaining schedule, in topological order; `None` until the first step. + schedule: Option>, + yields: Vec, + /// Result slots awaiting a pushed child frame's completion values. + resume_slots: Option>, + _marker: std::marker::PhantomData (V, E)>, +} + +impl DiGraphFrame +where + V: Clone, + E: From, +{ + /// Walk `graph`, binding `args` to its boundary ports on the first step. + pub fn new( + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + args: Product, + ) -> Self { + Self { + stage, + index, + graph, + pending: Some(args), + schedule: None, + yields: Vec::new(), + resume_slots: None, + _marker: std::marker::PhantomData, + } + } + + /// Schedule exhausted: read the declared yields from the activation and + /// complete `Finished` — the graph's natural completion. The parent + /// decides what the values mean (call returns or push results). + /// + /// Reading the yields is all this step needs, so it asks for [`Env`] alone — + /// not [`DiGraphQueries`], whose schedule was already consumed. + fn finish(self, interp: &mut I) -> Result>, E> + where + I: Env, + F: FrameBuild, + { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(Completion::Finished(values))) + } +} + +impl Frame for DiGraphFrame +where + I: DiGraphQueries + StatementDispatch + SparseForwardInterp, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; + + /// Execute the next scheduled node and translate its + /// [`SparseForwardEffect`] into a [`FrameEffect`] over the total frame + /// type `F`. + fn step_into(mut self, interp: &mut I) -> Result>, E> { + // First step: fetch the walk plan and bind the boundary ports. + if let Some(args) = self.pending.take() { + let plan = interp.digraph_walk_plan(self.stage, self.graph)?; + if plan.ports.len() != args.len() { + return Err(E::from(InterpreterError::ProductArityMismatch { + expected: plan.ports.len(), + actual: args.len(), + })); + } + for (port, value) in plan.ports.iter().copied().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + self.schedule = Some(plan.schedule.into()); + self.yields = plan.yields; + return Ok(FrameEffect::Continue(F::from_digraph(self))); + } + + let Some(statement) = self.schedule.as_mut().and_then(|s| s.pop_front()) else { + return self.finish::(interp); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_digraph(self))), + SparseForwardEffect::Push { frame, results } => { + self.resume_slots = Some(results); + Ok(FrameEffect::Push { + parent: F::from_digraph(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.stage, self.index, call); + Ok(FrameEffect::Push { + parent: F::from_digraph(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CFGControlFlowInStructuredBody)) + } + SparseForwardEffect::Yield(_) => Err(E::from(InterpreterError::Custom( + "yield inside a digraph body (a digraph's outputs are its declared yields)", + ))), + SparseForwardEffect::Return(_) => Err(E::from(InterpreterError::Custom( + "return inside a digraph body", + ))), + } + } + + /// A child finished without a payload (e.g. a returned call whose results + /// are already written): resume the schedule. + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_digraph(self))) + } + + /// A child bubbled a completion: a pushed frame's values land in the + /// push's result slots. A `Returned` cannot bubble out of a graph node — + /// a digraph has no function-return convention. + fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match completion { + Completion::Finished(values) | Completion::Yielded(values) => { + let slots = self.resume_slots.take().ok_or_else(|| { + E::from(InterpreterError::Custom( + "digraph resume without result slots", + )) + })?; + interp.bind_values(self.index, slots.as_slice(), values)?; + Ok(FrameEffect::Continue(F::from_digraph(self))) + } + Completion::Returned(_) => Err(E::from(InterpreterError::Custom( + "return bubbled into a digraph body", + ))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs b/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs new file mode 100644 index 0000000000..3b438037f5 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs @@ -0,0 +1,65 @@ +//! The **concrete** implementation of the shared [`frame`](crate::core::frame) +//! protocol. +//! +//! Two independent axes organize these frames: +//! +//! - **Body representation** — the closed [`Body`](crate::Body) vocabulary +//! (`CFG` / `Block` / `DiGraph` / `UnGraph`), an intentional IR design +//! decision. Each representation the framework can walk has one +//! representation frame implementing *traversal mechanics only*: +//! [`CFGFrame`] (multi-block, follows jumps), [`BlockFrame`] (one linear +//! block), and [`DiGraphFrame`] (dependency-ordered DAG walk). `UnGraph` +//! has **no** default walker — an undirected graph has no inherent +//! execution order, so traversal is a dialect/compiler-supplied policy +//! ([`FrameBuild::from_ungraph_entry`]). +//! +//! - **Entry context** — *why* a body is being walked: as a callable function +//! body (entered through [`CallFrame`], the call boundary that owns the +//! callee activation and return bookkeeping), as a nested structured +//! operation body (entered through a dialect frame such as `kirin-scf`'s +//! `ScfIfFrame`/`ScfForFrame`, pushed with +//! [`SparseForwardEffect::Push`]), or as an analysis owner (the abstract +//! engines' concern, not this module's). +//! +//! Representation frames never know their entry context: the same +//! [`BlockFrame`] walks a callable Block body and an `scf.if` arm; the parent +//! frame ([`CallFrame`] or the dialect frame) interprets the walker's +//! [`Completion`] and owns activation lifetime. Roles compose instead of +//! multiplying frame types: +//! +//! ```text +//! linear function = CallFrame → BlockFrame +//! CFG function = CallFrame → CFGFrame +//! graph function = CallFrame → DiGraphFrame +//! nested scf block = ScfIfFrame → BlockFrame +//! ``` +//! +//! These are the default total frames for +//! [`ConcreteInterpreter`](crate::ConcreteInterpreter) (bundled as +//! [`StandardFrame`]). Structured-control dialects do not get a framework +//! "scope": they push a frame **they own** through +//! [`SparseForwardEffect::Push`] (that frame may build a [`BlockFrame`] to +//! walk a chosen body — a reusable building block, not framework-owned +//! structured semantics). A language that combines such a dialect defines its +//! own total frame enum embedding these frames via [`FrameBuild`] plus its +//! dialect frames. The forward abstract analogue lives in +//! [`sparse_forward::frames`](crate::engines::sparse_forward::frames). +//! +//! [`SparseForwardEffect::Push`]: crate::SparseForwardEffect::Push + +mod block_cursor; +mod block_frame; +mod call_frame; +mod cfg_frame; +mod digraph_frame; +mod protocol; +mod standard_frame; + +pub use block_frame::BlockFrame; +pub use call_frame::CallFrame; +pub use cfg_frame::CFGFrame; +pub use digraph_frame::DiGraphFrame; +pub use protocol::{ + BodyFrameEntry, CallBodyFramePolicy, Completion, DefaultBodyFrames, FrameBuild, UnGraphEntry, +}; +pub use standard_frame::StandardFrame; diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs b/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs new file mode 100644 index 0000000000..62bf897bd4 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs @@ -0,0 +1,222 @@ +use kirin_ir::{Block, CFG, CompileStage, DiGraph, Product, UnGraph}; + +use crate::{Body, EnvIndex, InterpreterError}; + +use super::{BlockFrame, CFGFrame, CallFrame, DiGraphFrame}; + +/// Completion payloads produced by the standard concrete frames. +/// +/// Representation walkers report *what happened*; the parent frame decides +/// what it means. The protocol distinguishes the three relevant exits: +/// +/// - [`Returned`](Completion::Returned): an explicit function `Return` was +/// executed. Every frame between the walker and the call boundary relays it +/// unchanged (dialect frames included), so it bubbles to the nearest +/// [`CallFrame`] — the only frame that frees the callee activation and +/// writes the caller's result slots. +/// - [`Yielded`](Completion::Yielded): a structured `Yield` terminated a +/// block body. The structured-operation frame that pushed the block (e.g. +/// `scf.if`/`scf.for`) consumes the carried values. A [`CallFrame`] +/// rejects it: a callable Block or CFG must exit with `Return`, never a +/// structured yield. +/// - [`Finished`](Completion::Finished): the body ran to its natural end +/// with these values — a digraph's declared output yields, or a dialect +/// frame's finished sub-computation. A frame that pushed the child writes +/// them into the push's result slots; a [`CallFrame`] accepts them as the +/// call's returned values (a callable DiGraph's outputs are its returns). +pub enum Completion { + /// An explicit function `Return` with these values; bubbles to the + /// enclosing [`CallFrame`]. + Returned(Product), + /// A structured `Yield` with these carried values; consumed by the + /// dialect frame that pushed the block body. + Yielded(Product), + /// Natural completion of a body/sub-computation with these values; + /// delivered to whoever entered it (pusher or [`CallFrame`]). + Finished(Product), +} + +/// The entry context handed to a dialect/compiler-supplied callable-UnGraph +/// policy: the callee stage, the callee activation (owned by the awaiting +/// [`CallFrame`], never by the policy frame), the graph, and the entry +/// arguments for its boundary ports. +pub struct UnGraphEntry { + pub stage: CompileStage, + pub index: EnvIndex, + pub graph: UnGraph, + pub args: Product, +} + +/// The entry context for **one** callable body representation `B`, handed to a +/// [`CallBodyFramePolicy`]. +/// +/// Generalizes [`UnGraphEntry`], which is the `B = UnGraph` case retained for +/// the existing escape hatch. Note what is *not* here: the callee activation is +/// only *borrowed* — `index` names the activation the awaiting [`CallFrame`] +/// allocated and will free exactly once. A policy builds a walker over it; it +/// never owns its lifetime. +pub struct BodyFrameEntry { + pub stage: CompileStage, + pub index: EnvIndex, + pub body: B, + pub args: Product, +} + +impl From> for UnGraphEntry { + fn from(entry: BodyFrameEntry) -> Self { + UnGraphEntry { + stage: entry.stage, + index: entry.index, + graph: entry.body, + args: entry.args, + } + } +} + +/// Which walker enters a **callable** body of each representation. +/// +/// This is the *body-entry* half of [`CallFrame`], split out so the two +/// concerns are separately replaceable: +/// +/// - the **call convention** — resolve the callee, allocate its activation, +/// ask [`FunctionEntry`](crate::FunctionEntry) for the body, suspend, +/// validate the completion kind, free the activation exactly once, bind the +/// results — stays in [`CallFrame`] and is *not* configurable. It is where +/// double-frees would live. +/// - the **walker choice** — which frame traverses that body — is this trait. +/// +/// So a language can say "walk my CFGs with my own scheduler" without forking +/// the lifecycle. [`Body`](crate::Body) stays a closed vocabulary; only the +/// frame chosen per variant becomes configurable. +/// +/// **Concrete execution only.** Forward abstract interpretation does not +/// descend into a callee — `AbstractCallFrame` *summarizes* the call and the +/// fixpoint engine separately maps a callable body to an +/// [`Owner`](crate::Owner). Customizing that would be an abstract +/// body-entry/owner policy, not this one. The backward engines don't walk +/// callable bodies through a call frame at all. +/// +/// Selected by the compiler/language author through the concrete total frame +/// type's [`FrameBuild::BodyFrames`]. A dialect crate may *offer* reusable +/// walkers or policies, but a callable dialect should not permanently fix one +/// traversal for every engine. +pub trait CallBodyFramePolicy { + fn from_cfg(entry: BodyFrameEntry) -> Result; + fn from_block(entry: BodyFrameEntry) -> Result; + fn from_digraph(entry: BodyFrameEntry) -> Result; + fn from_ungraph(entry: BodyFrameEntry) -> Result; +} + +/// The framework's default callable-body walkers — today's exact behaviour: +/// +/// | body | walker | +/// |---|---| +/// | `CFG` | [`CFGFrame`] | +/// | `Block` | [`BlockFrame`] | +/// | `DiGraph` | [`DiGraphFrame`] | +/// | `UnGraph` | [`FrameBuild::from_ungraph_entry`] — `NoDefaultWalker` unless overridden | +/// +/// `CallFrame` means `CallFrame`, so nothing changes +/// for a language that does not opt in. +pub struct DefaultBodyFrames; + +impl CallBodyFramePolicy for DefaultBodyFrames +where + V: Clone, + E: From, + F: FrameBuild, +{ + fn from_cfg(entry: BodyFrameEntry) -> Result { + Ok(F::from_cfg(CFGFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_block(entry: BodyFrameEntry) -> Result { + Ok(F::from_block(BlockFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_digraph(entry: BodyFrameEntry) -> Result { + Ok(F::from_digraph(DiGraphFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + /// Delegates to the pre-existing escape hatch, so a language that already + /// supplies a callable-UnGraph walker keeps working untouched. + fn from_ungraph(entry: BodyFrameEntry) -> Result { + F::from_ungraph_entry(entry.into()) + } +} + +/// Construction trait letting any total frame enum embed the standard +/// concrete frames. +/// +/// The default [`StandardFrame`](super::StandardFrame) implements it +/// trivially; a language that adds structured-control dialects implements it +/// on its own enum to reuse the representation walkers and [`CallFrame`] +/// while adding its own dialect frames. +/// +/// Two traits sit next to each other here; they answer different questions: +/// +/// - **`FrameBuild`** — *injection*: how is an already-built frame wrapped into +/// the total frame type `F`? One constructor per framework frame, so a member +/// frame can re-wrap itself without knowing what `F` is. +/// - **[`CallBodyFramePolicy`]** — *selection*: which walker enters a **callable** +/// body of a given representation? Chosen per language via +/// [`BodyFrames`](Self::BodyFrames). +/// +/// [`Body`](crate::Body) is a deliberately closed enum, so [`CallFrame`] +/// matches it exhaustively; the policy decides only *which frame* each arm +/// builds. `UnGraph` keeps its dedicated +/// [`from_ungraph_entry`](Self::from_ungraph_entry) hook, which +/// [`DefaultBodyFrames`] delegates to, so languages that already supply a +/// callable-UnGraph walker are unaffected. +pub trait FrameBuild: Sized { + /// Which walkers this frame type uses to enter a **callable** body. + /// + /// Defaults to [`DefaultBodyFrames`] for every enum that does not opt in; + /// see [`CallBodyFramePolicy`]. Deliberately *unbounded* here: bounding it + /// as `CallBodyFramePolicy` makes checking + /// `type BodyFrames = DefaultBodyFrames` require `Self: FrameBuild`, + /// i.e. the very impl being checked. The obligation is instead attached + /// where the policy is *used*, in `CallFrame`'s [`Frame`](crate::Frame) + /// impl. + type BodyFrames; + + fn from_block(frame: BlockFrame) -> Self; + fn from_cfg(frame: CFGFrame) -> Self; + fn from_call(frame: CallFrame) -> Self; + fn from_digraph(frame: DiGraphFrame) -> Self; + + /// Build the entry frame for a **callable** `UnGraph` body. + /// + /// There is no framework default: an undirected graph has no inherent + /// producer/consumer direction, control-flow successor, or topological + /// execution order — its semantics (graph rewriting, circuits, constraint + /// propagation, …) belong to the dialect/compiler. A language with such + /// semantics overrides this to construct its own policy frame; everyone + /// else inherits this rejection, so total frame enums carry no meaningless + /// UnGraph boilerplate. (Nested, uncallable UnGraph operations don't come + /// through here — a dialect frame enters them via + /// [`SparseForwardEffect::Push`](crate::SparseForwardEffect::Push).) + fn from_ungraph_entry(entry: UnGraphEntry) -> Result + where + E: From, + { + Err(E::from(InterpreterError::NoDefaultWalker(Body::UnGraph( + entry.graph, + )))) + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs new file mode 100644 index 0000000000..66ae05e39b --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs @@ -0,0 +1,80 @@ +use crate::{ForwardFrameEngine, Frame, FrameEffect, InterpreterError, SparseForwardInterp}; + +use super::{ + BlockFrame, CFGFrame, CallFrame, Completion, DefaultBodyFrames, DiGraphFrame, FrameBuild, +}; + +/// The standard total concrete frame enum: the representation walkers plus +/// the call boundary, no structured-control dialect frames and no +/// callable-UnGraph policy (so a call into an `UnGraph` body reports +/// [`NoDefaultWalker`](InterpreterError::NoDefaultWalker)). +pub enum StandardFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), +} + +impl FrameBuild for StandardFrame { + type BodyFrames = DefaultBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + StandardFrame::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + StandardFrame::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + StandardFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + StandardFrame::DiGraph(frame) + } +} + +/// A *universe* impl: generic over the outer total frame type `F` so that +/// `StandardFrame` can be the stack's element type (`F = Self`, the usual case) +/// **or** be embedded in a larger enum — an instrumenting wrapper, say — without +/// re-enumerating its variants. +impl Frame for StandardFrame +where + I: ForwardFrameEngine + SparseForwardInterp, + // The `Call` variant is spelled `CallFrame`, i.e. the default policy, so + // an outer universe embedding `StandardFrame` must use that policy too. + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { + match self { + StandardFrame::Block(frame) => frame.step_into(interp), + StandardFrame::CFG(frame) => frame.step_into(interp), + StandardFrame::Call(frame) => frame.step_into(interp), + StandardFrame::DiGraph(frame) => frame.step_into(interp), + } + } + + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + match self { + StandardFrame::Block(frame) => frame.resume_done_into(interp), + StandardFrame::CFG(frame) => frame.resume_done_into(interp), + StandardFrame::Call(frame) => frame.resume_done_into(interp), + StandardFrame::DiGraph(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match self { + StandardFrame::Block(frame) => frame.resume_into(completion, interp), + StandardFrame::CFG(frame) => frame.resume_into(completion, interp), + StandardFrame::Call(frame) => frame.resume_into(completion, interp), + StandardFrame::DiGraph(frame) => frame.resume_into(completion, interp), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index 68650c49df..be2437aeda 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -4,10 +4,10 @@ use kirin_ir::{Block, CFG, CompileStage, Pipeline, Product, SSAValue, StageMeta, use crate::core::query; use crate::{ - BodyFrame, Callee, Completion, Env, EnvIndex, EnvStackStore, ForwardEval, Frame, FrameBuild, - FrameDriver, FunctionBody, FunctionTarget, Interp, InterpDispatch, InterpLocation, - InterpreterError, Linker, SameStageLinker, SparseForwardEffect, StageQuery, StandardFrame, - Store, drive_frames, + BlockQueries, CFGQueries, CallFrame, CallServices, CallableBody, Callee, Completion, + DiGraphQueries, Env, EnvIndex, EnvStackStore, ForwardEval, Frame, FrameBuild, FunctionTarget, + Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, SameStageLinker, + SparseForwardEffect, StageQuery, StandardFrame, StatementDispatch, Store, drive_frames, }; /// Concrete executor: runs IR over a concrete value domain with an explicit @@ -112,7 +112,11 @@ where } } -impl<'ir, S, V, E, Lk, F> FrameDriver for ConcreteInterpreter<'ir, S, V, E, Lk, F> +// The concrete engine provides the whole forward capability surface; it is split +// into one impl block per capability so the components stay individually +// nameable, and the blanket impl gives it `ForwardFrameEngine`/`ForwardFrameEngine`. + +impl<'ir, S, V, E, Lk, F> CallServices for ConcreteInterpreter<'ir, S, V, E, Lk, F> where S: StageQuery + InterpDispatch, V: Clone, @@ -133,47 +137,63 @@ where .map_err(E::from) } - fn run_statement( + fn enter_function( &mut self, stage: CompileStage, - statement: Statement, + body: Statement, + args: Product, index: EnvIndex, - ) -> Result { + ) -> Result, E> { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement, + statement: body, index, }); - let result = info.dispatch_statement(statement, self); + let result = info.dispatch_function_entry(body, args, self); self.location = previous; result } +} - fn enter_function( +impl<'ir, S, V, E, Lk, F> StatementDispatch for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ + fn run_statement( &mut self, stage: CompileStage, - body: Statement, - args: Product, + statement: Statement, index: EnvIndex, - ) -> Result, E> { + ) -> Result { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement: body, + statement, index, }); - let result = info.dispatch_function_entry(body, args, self); + let result = info.dispatch_statement(statement, self); self.location = previous; result } +} +impl<'ir, S, V, E, Lk, F> BlockQueries for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { query::block_params(self.pipeline, stage, block).map_err(E::from) } @@ -190,19 +210,43 @@ where ) -> Result, E> { query::next_statement(self.pipeline, stage, block, after).map_err(E::from) } +} +impl<'ir, S, V, E, Lk, F> CFGQueries for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, E> { query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } } +impl<'ir, S, V, E, Lk, F> DiGraphQueries for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + query::digraph_walk_plan(self.pipeline, stage, graph).map_err(E::from) + } +} + impl<'ir, S, V, E, Lk, F> ConcreteInterpreter<'ir, S, V, E, Lk, F> where S: StageQuery + InterpDispatch, V: Clone, E: From, Lk: Linker, - F: Frame> + FrameBuild, + F: Frame> + FrameBuild, { /// Resolve `stage`/`function` by name and execute it to completion. pub fn call_by_name( @@ -223,18 +267,20 @@ where } /// Execute a function to completion and return its return product. + /// + /// The root call is an ordinary [`CallFrame`]: the same call boundary + /// that nested `Call` effects go through owns callee resolution, the + /// callee activation, body-kind selection, and completion validation — + /// there is exactly one implementation of that behavior. pub fn call( &mut self, stage: CompileStage, callee: Callee, args: impl IntoIterator, ) -> Result, E> { - let target = self.resolve_call(stage, &callee)?; - let index = self.alloc_env(); let args: Product = args.into_iter().collect(); - let body = self.enter_function(target.stage, target.body, args, index)?; - let frame = BodyFrame::function(self, target.stage, index, body.cfg, body.args)?; - self.frames.push(F::from_body(frame)); + self.frames + .push(F::from_call(CallFrame::root(stage, callee, args))); self.run() } @@ -247,9 +293,9 @@ where self.frames = frames; match completion? { Completion::Returned(values) => Ok(values), - Completion::Finished(_) => Err(E::from(InterpreterError::Custom( - "body completion reached the frame-stack root", - ))), + Completion::Yielded(_) | Completion::Finished(_) => Err(E::from( + InterpreterError::Custom("body completion reached the frame-stack root"), + )), } } } diff --git a/crates/kirin-interpreter/src/engines/concrete/mod.rs b/crates/kirin-interpreter/src/engines/concrete/mod.rs index 968305c9ff..16290e0e78 100644 --- a/crates/kirin-interpreter/src/engines/concrete/mod.rs +++ b/crates/kirin-interpreter/src/engines/concrete/mod.rs @@ -3,5 +3,8 @@ pub(crate) mod frames; pub(crate) mod interp; -pub use frames::{BodyFrame, CallFrame, Completion, FrameBuild, StandardFrame}; +pub use frames::{ + BlockFrame, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, CallFrame, Completion, + DefaultBodyFrames, DiGraphFrame, FrameBuild, StandardFrame, UnGraphEntry, +}; pub use interp::ConcreteInterpreter; diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index 6d26b60ed3..f2ef4c4502 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -15,15 +15,15 @@ use std::marker::PhantomData; use kirin_ir::{Block, CompileStage, Statement}; use crate::{ - DenseBackwardCompletion, DenseBackwardEffect, DenseBackwardFrameDriver, Frame, FrameEffect, - InterpreterError, + DenseBackwardCompletion, DenseBackwardEffect, DenseBackwardFrameEngine, Frame, FrameEffect, + InterpreterError, ProgramPoint, }; /// How a [`DenseBlockFrame`] treats its block. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DenseBlockMode { /// A CFG block owner: the terminator's [`Edges`](DenseBackwardEffect::Edges) - /// are absorbed (seeding the state and recording `live_out`); completes + /// are absorbed (seeding the state and producing `live_out`); completes /// with [`DenseBackwardCompletion::Block`]. CFGOwner, /// A structured body walked by a dialect frame against the current point @@ -37,10 +37,14 @@ pub enum DenseBlockMode { pub struct DenseBlockFrame { stage: CompileStage, block: Block, - /// Materialized on the first step (needs the driver). - statements: Option>, - /// Number of statements not yet walked (walks from the end). - remaining: usize, + /// Current position in the reverse statement walk. + cursor: Option, + /// `false` until the cursor has been positioned at the block's last + /// logical statement. + initialized: bool, + /// `true` only while visiting the first reverse position (the block's + /// logical terminator position). + at_block_exit: bool, mode: DenseBlockMode, /// The absorbed edge mapping (`CFGOwner` only). live_out: Option, @@ -59,8 +63,9 @@ where Self { stage, block, - statements: None, - remaining: 0, + cursor: None, + initialized: false, + at_block_exit: false, mode, live_out: None, pending_point: None, @@ -77,44 +82,55 @@ where pub fn structured_body(stage: CompileStage, block: Block) -> Self { Self::with_mode(stage, block, DenseBlockMode::StructuredBody) } +} + +impl Frame for DenseBlockFrame +where + I: DenseBackwardFrameEngine, + F: DenseFrameBuild, + V: Clone, + E: From, +{ + type Completion = DenseBackwardCompletion; - pub fn step_into( + fn step_into( mut self, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild, - { - let statements = match self.statements.as_ref() { - Some(statements) => statements, - None => { - let statements = interp.block_statements(self.block)?; - self.remaining = statements.len(); - self.statements.insert(statements) + ) -> Result>, E> { + if !self.initialized { + if self.mode == DenseBlockMode::StructuredBody { + let facts = interp.state(); + interp.record_point(ProgramPoint::BlockExit(self.block), facts); } - }; - let total = statements.len(); + self.cursor = interp.last_statement(self.stage, self.block)?; + self.initialized = true; + self.at_block_exit = true; + } - if self.remaining == 0 { + let Some(statement) = self.cursor else { + let live_in = interp.state(); + if self.mode == DenseBlockMode::StructuredBody { + interp.record_point(ProgramPoint::BlockEntry(self.block), live_in.clone()); + } return Ok(FrameEffect::Complete(match self.mode { DenseBlockMode::CFGOwner => DenseBackwardCompletion::Block { - live_in: interp.state(), - live_out: self.live_out.take().unwrap_or_else(|| interp.state()), + live_in: live_in.clone(), + live_out: self.live_out.take().unwrap_or(live_in), }, DenseBlockMode::StructuredBody => DenseBackwardCompletion::Structured, })); - } + }; - let index = self.remaining - 1; - let is_terminator_position = self.remaining == total; - let statement = self.statements.as_ref().expect("materialized")[index]; - self.remaining = index; + let is_terminator_position = self.at_block_exit; + self.cursor = interp.previous_statement(self.stage, self.block, statement)?; + self.at_block_exit = false; - interp.record_after(statement); + let after = interp.state(); + interp.record_point(ProgramPoint::After(statement), after); match interp.run_statement(self.stage, statement)? { DenseBackwardEffect::Next => { - interp.record_before(statement); + let before = interp.state(); + interp.record_point(ProgramPoint::Before(statement), before); Ok(FrameEffect::Continue(F::from_block(self))) } DenseBackwardEffect::Edges(edges) => { @@ -127,7 +143,8 @@ where DenseBlockMode::CFGOwner => { let out = interp.absorb_edges(self.stage, &edges)?; self.live_out = Some(out); - interp.record_before(statement); + let before = interp.state(); + interp.record_point(ProgramPoint::Before(statement), before); Ok(FrameEffect::Continue(F::from_block(self))) } DenseBlockMode::StructuredBody => Err(E::from(InterpreterError::Custom( @@ -147,25 +164,25 @@ where } } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into( + self, + _interp: &mut I, + ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "dense block frames resume only with completions", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild, - { + ) -> Result>, E> { match completion { DenseBackwardCompletion::Structured => { if let Some(statement) = self.pending_point.take() { - interp.record_before(statement); + let before = interp.state(); + interp.record_point(ProgramPoint::Before(statement), before); } Ok(FrameEffect::Continue(F::from_block(self))) } @@ -197,33 +214,39 @@ impl DenseFrameBuild for StandardDenseBackwardFrame { } } -impl Frame for StandardDenseBackwardFrame +/// A *universe* impl, generic over the outer total frame type `F` — see +/// [`Frame`] for what that buys. +impl Frame for StandardDenseBackwardFrame where - I: DenseBackwardFrameDriver>, + I: DenseBackwardFrameEngine, + F: DenseFrameBuild, V: Clone, E: From, { type Completion = DenseBackwardCompletion; - fn step(self, interp: &mut I) -> Result, E> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - Self::Block(frame) => frame.step_into::(interp), + Self::Block(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, E> { + fn resume_done_into( + self, + interp: &mut I, + ) -> Result>, E> { match self { - Self::Block(frame) => frame.resume_done_into::(), + Self::Block(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result, E> { + ) -> Result>, E> { match self { - Self::Block(frame) => frame.resume_into::(completion, interp), + Self::Block(frame) => frame.resume_into(completion, interp), } } } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 2fb66aa0f3..3a59968e36 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -19,14 +19,14 @@ //! - **[`DenseBackwardTransfer`]** is the [`Interp`] delegate: pipeline access, //! the real dispatch location, and the *current point state* the dialect //! rules transform through the shape-generic -//! [`DenseBackwardInterp::insert_fact`] / [`DenseBackwardInterp::remove_fact`] -//! (liveness rules use the [`ClassicLivenessInterp`] `gen_live`/`kill_def` -//! spellings). +//! [`DenseBackwardInterp::point_state_mut`] (liveness rules use the +//! [`ClassicLivenessInterp`] `gen_live`/`kill_def` spellings, which is where +//! "a state is a set of values" lives — the engine never assumes it). //! - the **[`StandardFixpointInterpreter`]** driver owns the block-boundary //! summaries ([`BlockLiveness`], keyed by [`Scoped`] blocks), the block //! worklist, and [`BackwardSummaryDeps`] (successor changed → reanalyse //! predecessor, registered self-discoveringly by -//! [`absorb_edges`](DenseBackwardFrameDriver::absorb_edges)). +//! [`absorb_edges`](DenseBackwardFrameEngine::absorb_edges)). //! //! # Owners are blocks; one owner analysis is one backward walk //! @@ -39,25 +39,28 @@ //! argument, pass-through for non-parameters) — which both seeds the walk //! state and records the block's `live_out`. Structured dialects push //! dialect-owned frames ([`DenseBackwardEffect::Push`]) that walk their bodies -//! against the same point state. Per-statement states are not persisted: -//! reconstruct them on demand with -//! [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). +//! against the same point state. Each block walk records statement points and +//! nested structured-block boundaries; later fixpoint iterations overwrite +//! earlier approximations. CFG-owner boundaries remain canonical in their +//! converged summaries. The public fact view merges those disjoint sources +//! into one scope-qualified program-point store. use std::marker::PhantomData; use kirin_ir::{ - Block, CFG, CompileStage, HasArguments, HasBottom, HasResults, Lattice, Pipeline, SSAValue, + Block, CompileStage, HasArguments, HasBottom, HasResults, Lattice, Pipeline, SSAValue, StageMeta, Statement, }; use super::frames::{DenseBlockFrame, DenseFrameBuild}; +use crate::Body; use crate::core::query; -use crate::engines::sparse_backward::CFGScope; +use crate::engines::sparse_backward::BodyScope; use crate::{ - AbstractInterpreter, BackwardSummaryDeps, CFGTopology, ClassicLiveness, DenseBackwardSemantic, - DensePointStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, - InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, - StandardFixpointInterpreter, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, + AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, EnvIndex, + FactStore, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, + OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, + SummaryDependency, SummaryDependencyIndex, SummaryEffect, TerminatorArgs, }; // =========================================================================== @@ -86,10 +89,35 @@ pub enum DenseBackwardEffect { Push { frame: F }, } -/// The point-state contract: a dense backward state is a set of SSA values. +/// How a dense backward state moves across a control edge or out of a scope. /// -/// Implemented by the analysis's state type (e.g. `kirin_liveness::LiveSet`); -/// the join for merge points comes from [`Lattice`] (set union for liveness). +/// A dense backward state names its facts by [`SSAValue`]. Crossing an edge +/// renames them — the target's parameters become the edge's arguments; leaving +/// a scope drops them. Neither operation says what a fact *is*, which is why +/// this is the only state contract the engine and the dialect frames need: +/// [`Lattice`] merges at join points, this moves facts between vocabularies. +/// +/// Pass-through renaming (keep facts the rename does not cover) is the caller's +/// choice, spelled `state.rename(p, a).join(&state.forget(p))` — the CFG edge +/// transfer wants it, a loop back-edge does not. +pub trait DenseBackwardState: Lattice + Sized { + /// Only the facts named by `params`, renamed to the matching `args`. + /// + /// A `params[i]` with no `args[i]` contributes nothing. + fn rename(&self, params: &[SSAValue], args: &[SSAValue]) -> Self; + + /// Everything except the facts named by `values`. + fn forget(&self, values: &[SSAValue]) -> Self; +} + +/// The point-state contract of [`ClassicLiveness`]: its state is a set of live +/// SSA values. +/// +/// This is *semantics*, not shape — it backs +/// [`gen_live`](ClassicLivenessInterp::gen_live) / +/// [`kill_def`](ClassicLivenessInterp::kill_def) and is required by nothing in +/// the engine or the frames. A different dense-backward key brings its own +/// state contract and implements only [`DenseBackwardState`]. pub trait PointFacts { /// Insert `value`; `true` if newly added. fn insert(&mut self, value: SSAValue) -> bool; @@ -115,11 +143,15 @@ pub trait DenseBackwardInterp: /// [`DenseBackwardEffect::Push`]. Ordinary dialects never name it. type Frame; - /// Insert `value` into the current point state. - fn insert_fact(&mut self, value: impl Into) -> Result<(), Self::Error>; + /// The point state being transformed: the state *after* the statement on + /// entry to a rule, *before* it on exit. + /// + /// The engine hands the state over opaquely; how a rule transforms it is + /// the semantics' business. + fn point_state(&self) -> &Self::Value; - /// Remove `value` from the current point state. - fn remove_fact(&mut self, value: impl Into) -> Result<(), Self::Error>; + /// The point state being transformed, mutably. + fn point_state_mut(&mut self) -> &mut Self::Value; } /// [`ClassicLiveness`]'s helper vocabulary on top of the shape-generic @@ -131,16 +163,22 @@ pub trait DenseBackwardInterp: /// Pinned to `Semantics = ClassicLiveness` via the supertrait (rustc /// elaborates supertraits, so rules bounding `I: ClassicLivenessInterp` need /// no extra clauses), and blanket-implemented for every classic-liveness -/// dense-backward engine. -pub trait ClassicLivenessInterp: DenseBackwardInterp + Interp { +/// dense-backward engine. [`PointFacts`] rides in the same supertrait as an +/// associated-type bound for the same reason: elaboration means liveness rules +/// inherit it and never spell it. +pub trait ClassicLivenessInterp: + DenseBackwardInterp + Interp +{ /// Gen: mark `value` live at the current point (a use). fn gen_live(&mut self, value: impl Into) -> Result<(), Self::Error> { - self.insert_fact(value) + self.point_state_mut().insert(value.into()); + Ok(()) } /// Kill: remove `value` (a definition) from the current point state. fn kill_def(&mut self, value: impl Into) -> Result<(), Self::Error> { - self.remove_fact(value) + self.point_state_mut().remove(value.into()); + Ok(()) } /// The classic (weak) liveness transfer for an ordinary statement: kill @@ -160,8 +198,10 @@ pub trait ClassicLivenessInterp: DenseBackwardInterp + Interp ClassicLivenessInterp for I where - I: DenseBackwardInterp + Interp +impl ClassicLivenessInterp for I +where + I: DenseBackwardInterp + Interp, + I::Value: PointFacts, { } @@ -240,20 +280,18 @@ where impl<'ir, S, V, E, F, Sem> DenseBackwardInterp for DenseBackwardTransfer<'ir, S, V, E, F, Sem> where S: StageMeta, - V: Clone + PointFacts, + V: Clone, E: From, Sem: DenseBackwardSemantic, { type Frame = F; - fn insert_fact(&mut self, value: impl Into) -> Result<(), E> { - self.state.insert(value.into()); - Ok(()) + fn point_state(&self) -> &V { + &self.state } - fn remove_fact(&mut self, value: impl Into) -> Result<(), E> { - self.state.remove(value.into()); - Ok(()) + fn point_state_mut(&mut self) -> &mut V { + &mut self.state } } @@ -321,7 +359,7 @@ where E: From, Sem: DenseBackwardSemantic, { - type SummaryKey = Scoped; + type SummaryKey = Scoped; type Summary = BlockLiveness; type Frame = F; type Completion = DenseBackwardCompletion; @@ -336,42 +374,23 @@ pub enum DenseBackwardCompletion { Structured, } -/// Analysis-local state carried in the driver's `store` slot: the scope, -/// the CFG topology, and an optional per-point recorder filled by the -/// block frames during [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). -pub struct DenseAnalysisState { - scope: Option, - topology: CFGTopology, - recorder: Option>, -} - -impl Default for DenseAnalysisState { - fn default() -> Self { - Self { - scope: None, - topology: CFGTopology::default(), - recorder: None, - } - } -} - /// The dense backward driver: a [`StandardFixpointInterpreter`] over /// [`DenseBackwardTransfer`] with scope-qualified block owners and /// successor→predecessor dependencies. pub type DenseBackwardDriver<'ir, S, V, E, F, Sem = ClassicLiveness> = StandardFixpointInterpreter< DenseBackwardTransfer<'ir, S, V, E, F, Sem>, DenseBackwardProfile, - DenseAnalysisState, - BackwardSummaryDeps>, + FactStore, V>, + BackwardSummaryDeps>, >; // =========================================================================== // Driver capabilities (frames run on the driver) // =========================================================================== -/// The dense-backward frame-driver capability surface: what the dense frames +/// The dense-backward engine-capability surface: what the dense frames /// need from the engine. Implemented on the driver (it needs the summaries). -pub trait DenseBackwardFrameDriver: Interp> { +pub trait DenseBackwardFrameEngine: Interp> { /// The engine's total backward frame type. type Frame; @@ -382,8 +401,21 @@ pub trait DenseBackwardFrameDriver: Interp Result; - /// A block's statements in program order (terminator, if any, last). - fn block_statements(&self, block: Block) -> Result, Self::Error>; + /// The last logical statement of a block (the terminator when present). + fn last_statement( + &self, + stage: CompileStage, + block: Block, + ) -> Result, Self::Error>; + + /// The statement immediately before `before` in the block's logical + /// statement order. + fn previous_statement( + &self, + stage: CompileStage, + block: Block, + before: Statement, + ) -> Result, Self::Error>; /// The parameters of `block` (structured frames map carried demand). fn block_params(&self, stage: CompileStage, block: Block) @@ -394,7 +426,7 @@ pub trait DenseBackwardFrameDriver: Interp Result, Self::Error>; + ) -> Result; /// The current point state (cloned). fn state(&self) -> Self::Value; @@ -403,13 +435,9 @@ pub trait DenseBackwardFrameDriver: Interp Self::Value; - /// Record the current state as the point *before* `statement` (no-op - /// unless a per-point reconstruction is running). - fn record_before(&mut self, statement: Statement); - - /// Record the current state as the point *after* `statement` (no-op - /// unless a per-point reconstruction is running). - fn record_after(&mut self, statement: Statement); + /// Store `facts` at a block or statement program point, overwriting the + /// approximation recorded by any earlier fixpoint iteration. + fn record_point(&mut self, point: ProgramPoint, facts: Self::Value); /// Absorb a CFG terminator's edges atomically: for each successor, map /// its converged live-in across the edge (parameter → matching edge @@ -425,10 +453,10 @@ pub trait DenseBackwardFrameDriver: Interp Result; } -impl<'ir, S, V, E, F, Sem> DenseBackwardFrameDriver for DenseBackwardDriver<'ir, S, V, E, F, Sem> +impl<'ir, S, V, E, F, Sem> DenseBackwardFrameEngine for DenseBackwardDriver<'ir, S, V, E, F, Sem> where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + PointFacts, + V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, { @@ -454,21 +482,24 @@ where result } - fn block_statements(&self, block: Block) -> Result, E> { - self.store() - .topology - .blocks - .iter() - .find(|candidate| candidate.block == block) - .map(|candidate| candidate.stmts.clone()) - .ok_or_else(|| E::from(InterpreterError::MissingBlock(block))) + fn last_statement(&self, stage: CompileStage, block: Block) -> Result, E> { + query::last_statement(self.inner().pipeline(), stage, block).map_err(E::from) + } + + fn previous_statement( + &self, + stage: CompileStage, + block: Block, + before: Statement, + ) -> Result, E> { + query::previous_statement(self.inner().pipeline(), stage, block, before).map_err(E::from) } fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { query::block_params(self.inner().pipeline(), stage, block).map_err(E::from) } - fn terminator_args(&self, stage: CompileStage, block: Block) -> Result, E> { + fn terminator_args(&self, stage: CompileStage, block: Block) -> Result { query::terminator_arguments(self.inner().pipeline(), stage, block).map_err(E::from) } @@ -480,56 +511,43 @@ where std::mem::replace(&mut self.inner_mut().state, state) } - fn record_before(&mut self, statement: Statement) { - let state = self.inner().state.clone(); - if let Some(recorder) = self.store_mut().recorder.as_mut() { - recorder.set(ProgramPoint::Before(statement), state); - } - } - - fn record_after(&mut self, statement: Statement) { - let state = self.inner().state.clone(); - if let Some(recorder) = self.store_mut().recorder.as_mut() { - recorder.set(ProgramPoint::After(statement), state); - } + fn record_point(&mut self, point: ProgramPoint, facts: V) { + let scope = self + .current_owner() + .map(|owner| owner.scope) + .expect("dense frames only run while analyzing an owner"); + self.store_mut().set(Scoped::new(scope, point), facts); } fn absorb_edges(&mut self, stage: CompileStage, edges: &[SuccessorEdge]) -> Result { - let scope = self - .store() - .scope - .ok_or_else(|| E::from(InterpreterError::Custom("no active backward analysis")))?; + let current = self + .current_owner() + .cloned() + .ok_or_else(|| E::from(InterpreterError::Custom("no active backward owner")))?; + let scope = current.scope; let mut out = V::bottom(); for edge in edges { let owner = Scoped::new(scope, edge.target); // Successor changed → reanalyse the current block. - if let Some(current) = self.current_owner().cloned() { - self.dependency_index_mut() - .register(&owner, SummaryDependency::Reanalyze(current)) - .expect("backward dependency index is infallible"); - } + self.dependency_index_mut() + .register(&owner, SummaryDependency::Reanalyze(current.clone())) + .expect("backward dependency index is infallible"); let Some(summary) = self.summary(&owner) else { continue; }; let params = query::block_params(self.inner().pipeline(), stage, edge.target)?; - let mut mapped = V::bottom(); - for value in summary.live_in.values() { - match params.iter().position(|param| *param == value) { - Some(index) => { - if let Some(arg) = edge.args.get(index) { - mapped.insert(*arg); - } - } - // A live-in that is not a parameter of the successor is a - // dominated direct cross-block use: pass it through. - None => { - mapped.insert(value); - } - } - } + // The successor's entry state in this block's vocabulary: its + // parameters renamed to the edge's arguments, joined with the + // facts the rename does not cover — live-ins that are not + // parameters are dominated direct cross-block uses and pass + // through unchanged. + let entry = &summary.live_in; + let mapped = entry + .rename(¶ms, &edge.args) + .join(&entry.forget(¶ms)); out = out.join(&mapped); } @@ -548,7 +566,7 @@ struct DenseBackwardSemantics; impl<'ir, S, V, E, F, Sem> OwnerSemantics< DenseBackwardDriver<'ir, S, V, E, F, Sem>, - Scoped, + Scoped, BlockLiveness, F, DenseBackwardCompletion, @@ -556,7 +574,7 @@ impl<'ir, S, V, E, F, Sem> > for DenseBackwardSemantics where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + PointFacts, + V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, F: DenseFrameBuild, @@ -564,7 +582,7 @@ where fn bottom_summary( &mut self, _interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - _owner: &Scoped, + _owner: &Scoped, ) -> Result, E> { Ok(BlockLiveness::bottom()) } @@ -572,7 +590,7 @@ where fn entry_frame( &mut self, interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - owner: &Scoped, + owner: &Scoped, _summary: &BlockLiveness, ) -> Result { let (stage, _cfg) = owner.scope; @@ -585,9 +603,9 @@ where fn complete_owner( &mut self, _interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - owner: Scoped, + owner: Scoped, completion: DenseBackwardCompletion, - ) -> Result, BlockLiveness>, E> { + ) -> Result, BlockLiveness>, E> { match completion { DenseBackwardCompletion::Block { live_in, live_out } => Ok(SummaryEffect::Update { owner, @@ -609,7 +627,8 @@ where /// ```ignore /// let mut analysis = DenseBackwardInterpreter::::new(&pipeline); /// analysis.analyze(stage, cfg)?; -/// let boundary = analysis.block_summary(stage, cfg, block); +/// let point = Scoped::new((stage, Body::CFG(cfg)), ProgramPoint::BlockEntry(block)); +/// let live_in = analysis.point_facts(point); /// ``` pub struct DenseBackwardInterpreter< 'ir, @@ -638,7 +657,7 @@ where Self { driver: StandardFixpointInterpreter::with_dependency_index( DenseBackwardTransfer::new(pipeline), - DenseAnalysisState::default(), + FactStore::new(), (), BackwardSummaryDeps::new(), ), @@ -649,91 +668,86 @@ where self.driver.inner().pipeline() } - /// The converged boundary states of `block` under the `(stage, cfg)` - /// scope. - pub fn block_summary( - &self, - stage: CompileStage, - cfg: CFG, - block: Block, - ) -> Option<&BlockLiveness> { - self.driver.summary(&Scoped::new((stage, cfg), block)) + /// The converged fact at a scope-qualified program point. + /// + /// CFG-owner boundaries come directly from their fixpoint summaries; + /// statement and nested structured-block points come from the point store. + /// Each fact therefore has one mutable source during solving. + pub fn point_facts(&self, point: Scoped) -> Option<&V> { + let summary_fact = match point.item { + ProgramPoint::BlockEntry(block) => self + .driver + .summary(&Scoped::new(point.scope, block)) + .map(|summary| &summary.live_in), + ProgramPoint::BlockExit(block) => self + .driver + .summary(&Scoped::new(point.scope, block)) + .map(|summary| &summary.live_out), + ProgramPoint::Before(_) | ProgramPoint::After(_) => None, + }; + summary_fact.or_else(|| self.driver.store().get(point)) } - /// The analyzed CFG's own top-level blocks (post-`analyze`). - pub fn cfg_blocks(&self) -> Vec { - self.driver - .store() - .topology - .cfg_blocks() - .map(|block| block.block) - .collect() + /// Snapshot the active analysis as one scope-qualified program-point fact + /// store. + /// + /// The solver keeps CFG-owner boundaries in summaries because they drive + /// convergence. This copies each final boundary into the returned result; + /// it does not create a second mutable representation inside the engine. + pub fn facts(&self) -> FactStore, V> { + let mut facts = self.driver.store().clone(); + + for (owner, summary) in self.driver.summaries() { + facts.set( + Scoped::new(owner.scope, ProgramPoint::BlockEntry(owner.item)), + summary.live_in.clone(), + ); + facts.set( + Scoped::new(owner.scope, ProgramPoint::BlockExit(owner.item)), + summary.live_out.clone(), + ); + } + facts } } impl<'ir, S, V, E, F, Sem> DenseBackwardInterpreter<'ir, S, V, E, F, Sem> where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + PointFacts, + V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, - F: Frame, Completion = DenseBackwardCompletion> + F: Frame, F, Completion = DenseBackwardCompletion> + DenseFrameBuild, { + /// The blocks directly selected as fixpoint owners for `body`. + /// + /// This reads the current IR rather than returning cached analysis state. + fn direct_body_blocks( + &self, + stage: CompileStage, + body: impl Into, + ) -> Result, E> { + query::direct_body_blocks(self.driver.inner().pipeline(), stage, body.into()) + .map_err(E::from) + } + /// Run the block-boundary fixpoint over `cfg` in `stage`: seed every /// CFG block (a backward analysis must visit them all) and drain the /// worklist; dependencies are discovered from the terminators' edges. - pub fn analyze(&mut self, stage: CompileStage, cfg: CFG) -> Result<(), E> { - let scope = (stage, cfg); - let topology = query::cfg_topology(self.driver.inner().pipeline(), stage, cfg)?; - let owners: Vec> = topology - .cfg_blocks() - .map(|block| Scoped::new(scope, block.block)) + pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { + let body = body.into(); + let scope = (stage, body); + let blocks = self.direct_body_blocks(stage, body)?; + let owners: Vec> = blocks + .iter() + .copied() + .map(|block| Scoped::new(scope, block)) .collect(); - *self.driver.store_mut() = DenseAnalysisState { - scope: Some(scope), - topology, - recorder: None, - }; + let pipeline = self.driver.inner().pipeline(); + self.driver = Self::new(pipeline).driver; let mut semantics = DenseBackwardSemantics; self.driver.solve_many(&mut semantics, owners) } - - /// Reconstruct every per-statement state — including statements inside - /// structured bodies, at any nesting depth — by re-walking each converged - /// CFG block with the recorder enabled. Per-point states are never - /// persisted by the fixpoint itself; loop bodies record their final - /// (stable) iteration. - pub fn reconstruct_points( - &mut self, - stage: CompileStage, - cfg: CFG, - ) -> Result, E> { - let scope = (stage, cfg); - self.driver.store_mut().recorder = Some(DensePointStore::new()); - for block in self.cfg_blocks() { - // The CFGOwner walk re-absorbs the converged successor summaries, - // so it replays exactly the fixpoint's final states. - let _ = scope; - self.driver.replace_state(V::bottom()); - match self - .driver - .run_frame(F::from_block(DenseBlockFrame::cfg_owner(stage, block)))? - { - DenseBackwardCompletion::Block { .. } => {} - DenseBackwardCompletion::Structured => { - return Err(E::from(InterpreterError::Custom( - "a CFG block walk completed as a structured frame", - ))); - } - } - } - Ok(self - .driver - .store_mut() - .recorder - .take() - .expect("recorder installed above")) - } } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs index e440ba0b94..a15ace34f2 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs @@ -7,8 +7,7 @@ pub(crate) mod interp; pub use frames::{DenseBlockFrame, DenseBlockMode, DenseFrameBuild, StandardDenseBackwardFrame}; pub use interp::{ - BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, - DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardTransfer, PointFacts, - SuccessorEdge, + BlockLiveness, ClassicLivenessInterp, DenseBackwardCompletion, DenseBackwardDriver, + DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, + DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, PointFacts, SuccessorEdge, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index 54c08b5c04..961e7feca3 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -20,52 +20,52 @@ //! the real dispatch location, and the per-rule demand buffer. //! - the **[`StandardFixpointInterpreter`]** driver owns the demand facts //! (summaries keyed by [`Scoped`] SSA values — never bare values), the value -//! worklist, and the analysis state (scope + CFG topology). +//! worklist, and the analysis scope. //! //! # Owners are values; scheduling is demand propagation //! -//! `SummaryKey = Scoped<(CompileStage, CFG), SSAValue>`: the fact anchor is +//! `SummaryKey = Scoped`: the fact anchor is //! the owner. The driver's *default* self-dependent index //! ([`OwnerSummaryDeps`]) is exactly demand propagation — a value whose fact //! rises is rescheduled, and analyzing a value means dispatching the rules //! that can translate its demand: //! //! - a statement **result** → the defining statement's backward rule; -//! - a **block argument** → each of the block's *feeders* (terminators -//! targeting the block, statements owning it as a structured body) from the -//! [`CFGTopology`]; -//! - a graph **port** → unsupported (loud error). +//! - a **block argument** → its directly owning structured statement, or each +//! indexed CFG predecessor block's terminator; +//! - a graph **port** → the statement owning the graph boundary. //! //! Rules read converged facts ([`DemandInterp::is_demanded`]) and raise new //! demands ([`DemandInterp::demand`], strong liveness's spelling of the //! shape-generic [`SparseBackwardInterp::raise_fact`]); each rule returns the //! facts it raised as its [`SparseBackwardEffect`]. All fact mutation flows //! through the driver's single merge path. Facts only rise in a finite-height -//! lattice, so the fixpoint terminates with O(feeders) rule runs per rise — +//! lattice, so the fixpoint terminates with O(predecessors) rule runs per rise — //! no block re-walks, no widening, and no frames for structured control: //! loop-carried demand (e.g. `scf.for`) converges through the value worklist. +use std::collections::{HashSet, VecDeque}; use std::marker::PhantomData; use std::mem; use kirin_ir::{ - Block, CFG, CompileStage, HasArguments, HasBottom, HasResults, HasTop, IsPure, Lattice, - Pipeline, SSAKind, SSAValue, StageMeta, Statement, + Block, CompileStage, HasArguments, HasBottom, HasResults, HasTop, IsPure, Lattice, Pipeline, + SSAKind, SSAValue, StageMeta, Statement, }; use crate::core::query; use crate::{ - AbstractInterpreter, CFGTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, + AbstractInterpreter, Body, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, Scoped, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, StrongDemand, - Summary, SummaryEffect, + Summary, SummaryEffect, TerminatorArgs, }; -/// The scope a CFG-level backward analysis qualifies its facts with. +/// The scope a body-level backward analysis qualifies its facts with. /// /// Arena ids are per-stage, so the stage is part of the scope; analyzing two -/// cfgs in one engine keeps their facts distinct. -pub type CFGScope = (CompileStage, CFG); +/// bodies in one engine keeps their facts distinct. +pub type BodyScope = (CompileStage, Body); // =========================================================================== // Effect + dialect-facing trait @@ -88,7 +88,7 @@ pub enum SparseBackwardEffect { /// sparse-backward semantics ([`StrongDemand`] today, downstream keys /// tomorrow) shares this surface: read a converged per-value fact, raise a /// fact, drain the raised facts into the rule's effect, and query the block -/// topology facts terminator/structured rules map across. Rules are +/// boundary facts terminator/structured rules map across. Rules are /// scope-blind: they name bare SSA values, and the engine qualifies them with /// the current analysis scope. /// @@ -100,8 +100,16 @@ pub trait SparseBackwardInterp: /// The converged fact for `value` (bottom if absent). fn fact(&self, value: impl Into) -> Result; - /// Raise `value`'s fact to ⊤ (buffered; returned by [`effect`](Self::effect)). - fn raise_fact(&mut self, value: impl Into) -> Result<(), Self::Error>; + /// Merge `fact` into `value`'s fact (buffered; returned by + /// [`effect`](Self::effect)). + /// + /// The engine moves the fact and never inspects it — which element of the + /// lattice a rule raises is the semantics' business, not the shape's. + fn raise_fact( + &mut self, + value: impl Into, + fact: Self::Value, + ) -> Result<(), Self::Error>; /// Drain the raised-fact buffer into this rule's effect. fn effect(&mut self) -> Self::Effect; @@ -111,7 +119,7 @@ pub trait SparseBackwardInterp: fn block_params(&self, block: Block) -> Result, Self::Error>; /// The operands of `block`'s terminator — a structured body's yield slots. - fn terminator_args(&self, block: Block) -> Result, Self::Error>; + fn terminator_args(&self, block: Block) -> Result; } /// [`StrongDemand`]'s helper vocabulary on top of the shape-generic @@ -122,10 +130,17 @@ pub trait SparseBackwardInterp: /// Pinned to `Semantics = StrongDemand` via the supertrait (rustc elaborates /// supertraits, so rules bounding `I: DemandInterp` need no extra clauses), /// and blanket-implemented for every strong-demand sparse-backward engine. -pub trait DemandInterp: SparseBackwardInterp + Interp { +/// [`HasTop`] rides in the same supertrait as an associated-type bound rather +/// than in a `where` clause, for the same reason: elaboration means rules +/// bounding `I: DemandInterp` inherit it and never spell it. (A +/// `where Self::Value: HasTop` on the trait would *not* be elaborated — every +/// rule would have to repeat it.) +pub trait DemandInterp: + SparseBackwardInterp + Interp +{ /// Raise `value`'s demand (a demanded value carries the ⊤ fact). fn demand(&mut self, value: impl Into) -> Result<(), Self::Error> { - self.raise_fact(value) + self.raise_fact(value, Self::Value::top()) } /// `true` iff `value` carries a non-bottom demand fact. @@ -161,7 +176,12 @@ pub trait DemandInterp: SparseBackwardInterp + Interp } } -impl DemandInterp for I where I: SparseBackwardInterp + Interp {} +impl DemandInterp for I +where + I: SparseBackwardInterp + Interp, + I::Value: HasTop, +{ +} // =========================================================================== // SparseBackwardTransfer — the summary-free Interp delegate @@ -275,18 +295,17 @@ where E: From, Sem: SparseBackwardSemantic, { - type SummaryKey = Scoped; + type SummaryKey = Scoped; type Summary = DemandSummary; type Frame = DemandFrame; type Completion = Vec<(SSAValue, V)>; } /// Analysis-local state carried in the driver's `store` slot: the scope facts -/// are qualified with, and the CFG topology (feeders for block arguments). +/// are qualified with. #[derive(Default)] pub struct BackwardAnalysisState { - scope: Option, - topology: CFGTopology, + scope: Option, } /// The sparse backward driver: a [`StandardFixpointInterpreter`] over @@ -296,7 +315,7 @@ pub type SparseBackwardDriver<'ir, S, V, E, Sem = StrongDemand> = StandardFixpoi SparseBackwardTransfer<'ir, S, V, E, Sem>, SparseBackwardProfile, BackwardAnalysisState, - OwnerSummaryDeps>, + OwnerSummaryDeps>, >; // =========================================================================== @@ -321,16 +340,22 @@ impl DemandFrame { } } -impl<'ir, S, V, E, Sem> Frame> for DemandFrame +// A leaf *universe*, deliberately pinned to `F = Self` rather than generic like +// the other frames. `step_into` returns `FrameEffect::Continue(self)`, so being +// generic over `F` would need a conversion `DemandFrame -> F` — i.e. a public +// `DemandFrameBuild` hook that nothing currently calls. Add it if the sparse +// backward engine ever needs a wrapping frame (an instrumenting layer, say); +// until then the pinned signature states the fact that it cannot be embedded. +impl<'ir, S, V, E, Sem> Frame, Self> for DemandFrame where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { type Completion = Vec<(SSAValue, V)>; - fn step( + fn step_into( mut self, interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, ) -> Result, E> { @@ -345,7 +370,7 @@ where } } - fn resume_done( + fn resume_done_into( self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, ) -> Result, E> { @@ -354,7 +379,7 @@ where ))) } - fn resume( + fn resume_into( self, _completion: Self::Completion, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, @@ -401,7 +426,7 @@ where impl<'ir, S, V, E, Sem> SparseBackwardInterp for SparseBackwardDriver<'ir, S, V, E, Sem> where S: StageMeta + StageQuery, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { @@ -416,9 +441,9 @@ where .unwrap_or_else(V::bottom)) } - fn raise_fact(&mut self, value: impl Into) -> Result<(), E> { + fn raise_fact(&mut self, value: impl Into, fact: V) -> Result<(), E> { let value = value.into(); - self.inner_mut().demands.push((value, V::top())); + self.inner_mut().demands.push((value, fact)); Ok(()) } @@ -430,7 +455,7 @@ where query::block_params(self.inner().pipeline(), self.stage(), block).map_err(E::from) } - fn terminator_args(&self, block: Block) -> Result, E> { + fn terminator_args(&self, block: Block) -> Result { query::terminator_arguments(self.inner().pipeline(), self.stage(), block).map_err(E::from) } } @@ -444,7 +469,7 @@ struct SparseBackwardSemantics; impl<'ir, S, V, E, Sem> OwnerSemantics< SparseBackwardDriver<'ir, S, V, E, Sem>, - Scoped, + Scoped, DemandSummary, DemandFrame, Vec<(SSAValue, V)>, @@ -452,14 +477,14 @@ impl<'ir, S, V, E, Sem> > for SparseBackwardSemantics where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { fn bottom_summary( &mut self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - _owner: &Scoped, + _owner: &Scoped, ) -> Result, E> { Ok(DemandSummary(V::bottom())) } @@ -467,19 +492,22 @@ where fn entry_frame( &mut self, interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - owner: &Scoped, + owner: &Scoped, _summary: &DemandSummary, ) -> Result, E> { let (stage, _cfg) = owner.scope; let kind = query::value_kind(interp.inner().pipeline(), stage, owner.item)?; let work = match kind { SSAKind::Result(statement, _) => vec![statement], - SSAKind::BlockArgument(block, _) => interp.store().topology.feeders(block).to_vec(), - SSAKind::Port(..) => { - return Err(E::from(InterpreterError::Custom( - "graph ports are not supported by sparse backward demand", - ))); + SSAKind::BlockArgument(block, _) => { + query::block_argument_predecessors(interp.inner().pipeline(), stage, block)? + .into_vec() } + SSAKind::Port(parent, _) => vec![query::graph_port_owner( + interp.inner().pipeline(), + stage, + parent, + )?], }; Ok(DemandFrame::new(stage, work)) } @@ -487,9 +515,9 @@ where fn complete_owner( &mut self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - owner: Scoped, + owner: Scoped, completion: Vec<(SSAValue, V)>, - ) -> Result, DemandSummary>, E> { + ) -> Result, DemandSummary>, E> { // Scope-qualify the bare values the rules demanded. Ok(SummaryEffect::Many( completion @@ -542,16 +570,25 @@ where self.driver.inner().pipeline() } - /// The converged demand fact for `value` under the `(stage, cfg)` scope. - pub fn fact(&self, stage: CompileStage, cfg: CFG, value: impl Into) -> Option<&V> { + /// The converged demand fact for `value` under the `(stage, body)` scope. + pub fn fact( + &self, + stage: CompileStage, + body: impl Into, + value: impl Into, + ) -> Option<&V> { self.driver - .summary(&Scoped::new((stage, cfg), value.into())) + .summary(&Scoped::new((stage, body.into()), value.into())) .map(|summary| &summary.0) } - /// All converged `(value, fact)` pairs under the `(stage, cfg)` scope. - pub fn facts(&self, stage: CompileStage, cfg: CFG) -> impl Iterator { - let scope = (stage, cfg); + /// All converged `(value, fact)` pairs under the `(stage, body)` scope. + pub fn facts( + &self, + stage: CompileStage, + body: impl Into, + ) -> impl Iterator { + let scope = (stage, body.into()); self.driver .summaries() .iter() @@ -559,11 +596,11 @@ where .map(|(owner, summary)| (owner.item, &summary.0)) } - /// The converged facts under the `(stage, cfg)` scope as a + /// The converged facts under the `(stage, body)` scope as a /// [`SparseStore`] (the sparse per-SSA-value fact view; absent = bottom). - pub fn fact_store(&self, stage: CompileStage, cfg: CFG) -> SparseStore { + pub fn fact_store(&self, stage: CompileStage, body: impl Into) -> SparseStore { let mut store = SparseStore::new(); - for (value, fact) in self.facts(stage, cfg) { + for (value, fact) in self.facts(stage, body) { store.set(value, fact.clone()); } store @@ -573,38 +610,40 @@ where impl<'ir, S, V, E, Sem> SparseBackwardInterpreter<'ir, S, V, E, Sem> where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { - /// Run the demand fixpoint over `cfg` in `stage`. + /// Run the demand fixpoint over `body` in `stage`. /// - /// **Prepass**: enumerate the CFG topology (blocks including structured - /// bodies, statements, feeders), then run every statement's rule once with - /// nothing demanded — impure statements and terminators contribute the - /// demand roots. **Propagation**: drain the value worklist; each risen - /// value dispatches the rules that translate its demand. - pub fn analyze(&mut self, stage: CompileStage, cfg: CFG) -> Result<(), E> { - let scope = (stage, cfg); - let topology = query::cfg_topology(self.driver.inner().pipeline(), stage, cfg)?; - let statements: Vec = topology - .blocks - .iter() - .flat_map(|block| block.stmts.iter().copied()) - .collect(); - *self.driver.store_mut() = BackwardAnalysisState { - scope: Some(scope), - topology, - }; + /// **Prepass**: walk the body's containment hierarchy, running every + /// statement's rule once with nothing demanded — impure statements and + /// terminators contribute the demand roots. + /// **Propagation**: drain the value worklist; each risen value dispatches + /// the rules that translate its demand. + pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { + let body = body.into(); + let scope = (stage, body); + *self.driver.store_mut() = BackwardAnalysisState { scope: Some(scope) }; let mut semantics = SparseBackwardSemantics; - // Prepass: collect the demand roots. + // Prepass: visit each contained body part once and collect all demand + // roots before merging any of them, so every rule observes bottom. + let mut bodies = VecDeque::from([body]); + let mut visited = HashSet::new(); let mut seeds: Vec<(SSAValue, V)> = Vec::new(); - for statement in statements { - let SparseBackwardEffect::Demands(demands) = - self.driver.run_statement(stage, statement)?; - seeds.extend(demands); + while let Some(body) = bodies.pop_front() { + if !visited.insert(body) { + continue; + } + let contents = query::body_contents(self.driver.inner().pipeline(), stage, body)?; + bodies.extend(contents.children); + for statement in contents.statements { + let SparseBackwardEffect::Demands(demands) = + self.driver.run_statement(stage, statement)?; + seeds.extend(demands); + } } // Propagate to the fixpoint. @@ -619,11 +658,16 @@ where } /// `true` iff `value` carries a non-bottom demand fact under the scope. - pub fn is_demanded(&self, stage: CompileStage, cfg: CFG, value: impl Into) -> bool + pub fn is_demanded( + &self, + stage: CompileStage, + body: impl Into, + value: impl Into, + ) -> bool where V: HasBottom, { - self.fact(stage, cfg, value) + self.fact(stage, body, value) .is_some_and(|fact| *fact != V::bottom()) } } diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs index 6fba1b42ad..aee1ad363f 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod interp; pub use interp::{ - BackwardAnalysisState, CFGScope, DemandFrame, DemandInterp, DemandSummary, + BackwardAnalysisState, BodyScope, DemandFrame, DemandInterp, DemandSummary, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs index 85c5a3302c..8b6013e187 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs @@ -15,17 +15,19 @@ //! policy lives in that dialect frame (it may reuse [`AbstractBlockFrame`] to //! walk a chosen body). The interprocedural //! *policy* (summary keying, join/widen, caller recording — including same-key -//! recursion) stays atomic in the engine behind [`AbstractFrameDriver`]; frames +//! recursion) stays atomic in the engine behind [`ForwardDataflowFrameEngine`]; frames //! only choose what to step next. +use std::collections::VecDeque; use std::hash::Hash; use std::marker::PhantomData; -use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; +use kirin_ir::{Block, CompileStage, DiGraph, Product, SSAValue, Statement}; +use crate::core::frame::BlockBinding; use crate::{ - AbstractFrameDriver, CallEffect, Edge, EnvIndex, Frame, FrameEffect, InterpreterError, - SparseForwardEffect, SparseForwardInterp, + Body, CallEffect, Edge, Env, EnvIndex, ForwardDataflowFrameEngine, Frame, FrameEffect, + InterpreterError, SparseForwardEffect, SparseForwardInterp, }; /// Completion payloads produced by the standard abstract frames. @@ -47,6 +49,22 @@ pub enum AbstractCompletion { pub trait AbstractFrameBuild: Sized { fn from_block(frame: AbstractBlockFrame) -> Self; fn from_call(frame: AbstractCallFrame) -> Self; + + /// Embed the standard abstract digraph walker. + /// + /// Graph bodies are opt-in: a total abstract frame enum that carries no + /// [`AbstractDiGraphFrame`] inherits this rejection rather than pretending + /// to analyze one, the same way + /// [`FrameBuild::from_ungraph_entry`](crate::FrameBuild::from_ungraph_entry) + /// rejects a callable `UnGraph` without a compiler-supplied policy. + fn from_digraph(frame: AbstractDiGraphFrame) -> Result + where + E: From, + { + Err(E::from(InterpreterError::NoDefaultWalker(Body::DiGraph( + frame.graph(), + )))) + } } // =========================================================================== @@ -75,7 +93,7 @@ pub struct AbstractBlockFrame { cursor: Option, mode: BlockMode, /// Entry arguments not yet bound — bound on the first step, so building the - /// frame needs no engine access (see [`BodyFrame`](crate::BodyFrame)). + /// frame needs no engine access (see [`BlockFrame`](crate::BlockFrame)). pending: Option>, resume_slots: Option>, _marker: PhantomData (E, K)>, @@ -121,16 +139,20 @@ where _marker: PhantomData, } } +} - pub fn step_into( - mut self, - interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver - + SparseForwardInterp, - F: AbstractFrameBuild, - { +impl Frame for AbstractBlockFrame +where + I: ForwardDataflowFrameEngine + + SparseForwardInterp, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, interp: &mut I) -> Result>, E> { // Bind entry arguments lazily on the first step. if let Some(args) = self.pending.take() { interp.bind_block_args(self.stage, self.index, self.block, &args)?; @@ -201,22 +223,15 @@ where } /// A pushed call frame finished: continue walking the body. - pub fn resume_done_into(self) -> FrameEffect> - where - F: AbstractFrameBuild, - { - FrameEffect::Continue(F::from_block(self)) + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_block(self))) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild, - { + ) -> Result>, E> { match completion { AbstractCompletion::Finished(Some(values)) => { let slots = self.resume_slots.take().ok_or_else(|| { @@ -224,7 +239,7 @@ where "block resume without result slots", )) })?; - interp.write_results(self.index, &slots, values)?; + interp.bind_values(self.index, slots.as_slice(), values)?; Ok(FrameEffect::Continue(F::from_block(self))) } // A nested push returned without finishing: this pass left via return. @@ -248,6 +263,192 @@ where } } +// =========================================================================== +// DiGraph frame: one dependency-ordered pass over a graph body +// =========================================================================== + +/// Abstract walker for a [`DiGraph`] body: bind the boundary ports, run the +/// node statements in dependency (topological) order, and complete +/// [`Finished`](AbstractCompletion::Finished) with the graph's declared yields. +/// +/// A single pass is **exact** for a DAG — there is no loop inside the graph, so +/// no widening happens here. Convergence pressure comes only from *outside*: +/// the owner's entry product is widened at +/// [`Owner`](crate::Owner) entry when a new call site raises it, and the whole +/// pass is re-run. +/// +/// The one substantive difference from the concrete +/// [`DiGraphFrame`](crate::DiGraphFrame) is call handling: a `Call` effect +/// pushes an [`AbstractCallFrame`], so the call goes through the engine's +/// interprocedural summarization protocol (`summarize_call`) instead of +/// descending into the callee. Descending would neither widen nor terminate on +/// recursion. +/// +/// Like the other frames, construction is pure — the walk plan is fetched and +/// the ports are bound on the first `step`, so a dialect frame can build one +/// without engine access. +pub struct AbstractDiGraphFrame { + stage: CompileStage, + index: EnvIndex, + graph: DiGraph, + /// Entry arguments not yet bound (bound on the first `step`). + pending: Option>, + /// Remaining schedule in dependency order; `None` until the first step. + schedule: Option>, + yields: Vec, + /// Result slots awaiting a pushed child frame's completion values. + resume_slots: Option>, + _marker: PhantomData (E, K)>, +} + +impl AbstractDiGraphFrame { + /// The graph body this frame walks. Available without the walking bounds so + /// [`AbstractFrameBuild::from_digraph`]'s rejecting default can name it. + pub fn graph(&self) -> DiGraph { + self.graph + } +} + +impl AbstractDiGraphFrame +where + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + /// Walk `graph`, binding `args` to its boundary ports on the first step. + pub fn new(stage: CompileStage, index: EnvIndex, graph: DiGraph, args: Product) -> Self { + Self { + stage, + index, + graph, + pending: Some(args), + schedule: None, + yields: Vec::new(), + resume_slots: None, + _marker: PhantomData, + } + } + + /// Schedule exhausted: read the declared yields out of the activation and + /// complete. The parent decides what the values mean — a graph **owner** + /// turns them into the function's return, a pushing statement binds them + /// into its result slots. + /// Reading the yields needs [`Env`] alone, not the whole dataflow surface. + fn finish(self, interp: &mut I) -> Result>, E> + where + I: Env, + F: AbstractFrameBuild, + { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(AbstractCompletion::Finished(Some( + values, + )))) + } +} + +impl Frame for AbstractDiGraphFrame +where + I: ForwardDataflowFrameEngine + + SparseForwardInterp, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, interp: &mut I) -> Result>, E> { + // First step: fetch the walk plan and bind the boundary ports. + if let Some(args) = self.pending.take() { + let plan = interp.digraph_walk_plan(self.stage, self.graph)?; + if plan.ports.len() != args.len() { + return Err(E::from(InterpreterError::ProductArityMismatch { + expected: plan.ports.len(), + actual: args.len(), + })); + } + for (port, value) in plan.ports.iter().copied().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + self.schedule = Some(plan.schedule.into()); + self.yields = plan.yields; + return Ok(FrameEffect::Continue(F::from_digraph(self)?)); + } + + let Some(statement) = self.schedule.as_mut().and_then(|s| s.pop_front()) else { + return self.finish::(interp); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_digraph(self)?)), + SparseForwardEffect::Push { frame, results } => { + self.resume_slots = Some(results); + Ok(FrameEffect::Push { + parent: F::from_digraph(self)?, + child: frame, + }) + } + // Summarize, don't descend: the interprocedural fixpoint + // re-evaluates the callee under its own key. + SparseForwardEffect::Call(call) => { + let call_frame = AbstractCallFrame::new(self.stage, call, self.index); + Ok(FrameEffect::Push { + parent: F::from_digraph(self)?, + child: F::from_call(call_frame), + }) + } + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CFGControlFlowInStructuredBody)) + } + SparseForwardEffect::Yield(_) => Err(E::from(InterpreterError::Custom( + "yield inside a digraph body (a digraph's outputs are its declared yields)", + ))), + SparseForwardEffect::Return(_) => Err(E::from(InterpreterError::Custom( + "return inside a digraph body", + ))), + } + } + + /// A pushed child finished without a payload (e.g. a summarized call whose + /// results are already written): resume the schedule. + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_digraph(self)?)) + } + + fn resume_into( + mut self, + completion: AbstractCompletion, + interp: &mut I, + ) -> Result>, E> { + match completion { + AbstractCompletion::Finished(Some(values)) => { + let slots = self.resume_slots.take().ok_or_else(|| { + E::from(InterpreterError::Custom( + "digraph resume without result slots", + )) + })?; + interp.bind_values(self.index, slots.as_slice(), values)?; + Ok(FrameEffect::Continue(F::from_digraph(self)?)) + } + // A nested push left via `return`. A digraph has no function-return + // convention, so this cannot be relayed. + AbstractCompletion::Finished(None) => Err(E::from(InterpreterError::Custom( + "return bubbled into a digraph body", + ))), + AbstractCompletion::FunctionDone => Err(E::from(InterpreterError::Custom( + "digraph frame resumed with a function completion", + ))), + AbstractCompletion::CFGBlock { .. } => Err(E::from(InterpreterError::Custom( + "digraph frame resumed with a CFG-block completion", + ))), + } + } +} + // =========================================================================== // Call frame: summarize a call (no descent — the interprocedural fixpoint // re-evaluates the callee). @@ -276,25 +477,33 @@ where _marker: PhantomData, } } +} - pub fn step_into(self, interp: &mut I) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild, - { +impl Frame for AbstractCallFrame +where + I: ForwardDataflowFrameEngine, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(self, interp: &mut I) -> Result>, E> { interp.summarize_call(self.stage, self.call, self.index)?; Ok(FrameEffect::Done) } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "call frame resumed without a return", ))) } - pub fn resume_into( + fn resume_into( self, _completion: AbstractCompletion, + _interp: &mut I, ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "call frame resumed with a completion", @@ -312,6 +521,7 @@ where pub enum StandardAbstractFrame { Block(AbstractBlockFrame), Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), } impl AbstractFrameBuild for StandardAbstractFrame { @@ -321,40 +531,49 @@ impl AbstractFrameBuild for StandardAbstractFrame { fn from_call(frame: AbstractCallFrame) -> Self { StandardAbstractFrame::Call(frame) } + fn from_digraph(frame: AbstractDiGraphFrame) -> Result { + Ok(StandardAbstractFrame::DiGraph(frame)) + } } -impl Frame for StandardAbstractFrame +/// A *universe* impl, generic over the outer total frame type `F` — see +/// [`Frame`] for what that buys. +impl Frame for StandardAbstractFrame where - I: AbstractFrameDriver - + SparseForwardInterp>, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, + F: AbstractFrameBuild, V: Clone + PartialEq, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - StandardAbstractFrame::Block(frame) => frame.step_into::(interp), - StandardAbstractFrame::Call(frame) => frame.step_into::(interp), + StandardAbstractFrame::Block(frame) => frame.step_into(interp), + StandardAbstractFrame::Call(frame) => frame.step_into(interp), + StandardAbstractFrame::DiGraph(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - StandardAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - StandardAbstractFrame::Call(frame) => frame.resume_done_into::(), + StandardAbstractFrame::Block(frame) => frame.resume_done_into(interp), + StandardAbstractFrame::Call(frame) => frame.resume_done_into(interp), + StandardAbstractFrame::DiGraph(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - StandardAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), - StandardAbstractFrame::Call(frame) => frame.resume_into::(completion), + StandardAbstractFrame::Block(frame) => frame.resume_into(completion, interp), + StandardAbstractFrame::Call(frame) => frame.resume_into(completion, interp), + StandardAbstractFrame::DiGraph(frame) => frame.resume_into(completion, interp), } } } diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 9fb118ae1a..6808fe7dab 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -8,7 +8,9 @@ //! //! - **[`SparseForwardTransfer`]** is the [`Interp`] delegate: pipeline, linker, SSA //! env, analysis policy, per-function return accumulator, and read/write logging; -//! it provides the dialect-dispatch / IR-query surface ([`ForwardFrameDriver`]). +//! it provides the dialect-dispatch / IR-query surface ([`StatementDispatch`], +//! [`BlockQueries`], [`CFGQueries`], [`DiGraphQueries`], and — for +//! concrete-shaped callers — [`CallServices`]). //! - the **[`StandardFixpointInterpreter`]** driver owns the summaries, the //! dependency graph ([`ForwardSummaryDeps`]), the owner worklist, and the //! owner-local [`ForwardStore`] (shared envs + context-qualified value-reader @@ -17,9 +19,10 @@ //! # Owner kinds //! //! [`Owner::Function`] is a **summary/storage** owner — it is *never scheduled*; it -//! records a function context's entry/return/entry-block. [`Owner::Block`] is the -//! **executable** owner: exactly the block owners run frames (one single-pass CFG -//! walk each). CFG convergence is owner-summary convergence: a block emits its +//! records a function context's entry/return/entry-block. [`Owner::Block`] and +//! [`Owner::Graph`] are the **executable** owners: exactly those run frames (one +//! single-pass walk each — a CFG block, or a whole graph body in dependency +//! order). CFG convergence is owner-summary convergence: a block emits its //! successor block-entries, its function return, its outputs, and its external //! read dependencies through the single [`apply_update`](ForwardDriver::apply_update) //! path, which merges via the analysis policy and reschedules owners / value @@ -33,19 +36,20 @@ use std::hash::Hash; use std::marker::PhantomData; use kirin_ir::{ - Block, CFG, CompileStage, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, + Block, CFG, CompileStage, DiGraph, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, StageMeta, Statement, Widen, }; use crate::core::query; use crate::{ - AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, - AbstractInterpreter, CallEffect, Callee, Env, EnvIndex, EnvStackStore, FixpointProfile, - ForwardEval, ForwardFrameDriver, ForwardSummaryDeps, Frame, FunctionBody, FunctionTarget, - Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, OwnerSemantics, - SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, StandardAbstractFrame, - StandardFixpointInterpreter, Store, Summary, SummaryDependency, SummaryDependencyIndex, - SummaryEffect, + AbstractBlockFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractFrameBuild, + AbstractInterpreter, BlockQueries, Body, CFGQueries, CallEffect, CallServices, CallableBody, + Callee, DiGraphQueries, Env, EnvIndex, EnvStackStore, FixpointProfile, + ForwardDataflowFrameEngine, ForwardEval, ForwardSummaryDeps, Frame, FunctionTarget, Interp, + InterpDispatch, InterpLocation, InterpreterError, Linker, OwnerSemantics, SameStageLinker, + SparseForwardEffect, SparseForwardSemantic, StageQuery, StandardAbstractFrame, + StandardFixpointInterpreter, StatementDispatch, Store, Summary, SummaryDependency, + SummaryDependencyIndex, SummaryEffect, }; // =========================================================================== @@ -57,12 +61,7 @@ use crate::{ pub trait CallContext { type Key: Clone + Eq + Hash; - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - args: &Product, - ) -> Self::Key; + fn key(&mut self, target: &FunctionTarget, args: &Product) -> Self::Key; } /// Explore/join strategy: combines an `incoming` abstract state into the @@ -92,13 +91,8 @@ impl Default for ContextInsensitive { impl CallContext for ContextInsensitive { type Key = (CompileStage, SpecializedFunction); - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - _args: &Product, - ) -> Self::Key { - (stage, function) + fn key(&mut self, target: &FunctionTarget, _args: &Product) -> Self::Key { + (target.stage, target.function) } } @@ -123,7 +117,8 @@ where /// Owner of a summary in the forward fixpoint. /// /// [`Owner::Function`] is a **summary/storage** owner (never scheduled); -/// [`Owner::Block`] is the **executable** owner (frame-executed). `Owner` is a +/// [`Owner::Block`] and [`Owner::Graph`] are the **executable** owners +/// (frame-executed) — one per unit of re-analysis. `Owner` is a /// dataflow-equation identity — deliberately **not** a /// [`LatticeAnchor`](crate::LatticeAnchor). #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -132,6 +127,22 @@ pub enum Owner { Function(K), /// A CFG block executable owner within function context `K`. Block { function: K, block: Block }, + /// A graph-body executable owner within function context `K`: the whole + /// graph is one unit, re-analyzed as a single dependency-ordered pass + /// whenever its entry product rises. One pass is exact for a DAG, so there + /// is no intra-graph fixpoint to split into finer owners. + Graph { function: K, graph: DiGraph }, +} + +impl Owner { + /// The function context this owner belongs to. + pub fn function(&self) -> &K { + match self { + Owner::Function(function) + | Owner::Block { function, .. } + | Owner::Graph { function, .. } => function, + } + } } /// Per-function summary/storage record: call-site metadata, the joined entry @@ -300,18 +311,15 @@ enum ForwardUpdate { /// Merge a return contribution into a function context's return (join); on /// rise, reschedule its callers. FunctionReturn { key: K, values: Product }, - /// Merge edge args into a block owner's entry (widen by visits); on rise, - /// (re)schedule that block owner. - BlockEntry { - function: K, - block: Block, - args: Product, - }, - /// Merge a block's freshly computed outputs (join); on any value's rise, + /// Merge incoming args into an **executable** owner's entry (widen by + /// visits); on rise, (re)schedule that owner. The incoming args are a CFG + /// edge's arguments for a block owner, or the boundary-port values for a + /// graph owner. + OwnerEntry { owner: Owner, args: Product }, + /// Merge an owner's freshly computed outputs (join); on any value's rise, /// reschedule that value's readers. - BlockOutputs { - function: K, - block: Block, + OwnerOutputs { + owner: Owner, outputs: HashMap, }, } @@ -485,7 +493,7 @@ where } // Policy-driven merge + return accumulation, kept on the transfer (the analysis `P` -// lives here). The driver's `AbstractFrameDriver` impl delegates to these. +// lives here). The driver's `ForwardDataflowFrameEngine` impl delegates to these. impl<'ir, S: StageMeta, V, E, Lk, P, F, Sem> SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> where V: Clone + PartialEq + Widen, @@ -505,13 +513,8 @@ where } /// Key a resolved call target through the analysis. - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - args: &Product, - ) ->

>::Key { - self.analysis.key(stage, function, args) + fn key(&mut self, target: &FunctionTarget, args: &Product) ->

>::Key { + self.analysis.key(target, args) } fn take_ret_acc(&mut self) -> Option> { @@ -601,8 +604,12 @@ where { } -// The IR-query / dispatch capability surface. Dialect rules dispatch on the transfer. -impl<'ir, S, V, E, Lk, P, F, Sem> ForwardFrameDriver +// The IR-query / dispatch capability surface. Dialect rules dispatch on the +// transfer. The transfer implements the *concrete* call lifecycle too, even +// though the abstract frames never use it: `SparseForwardTransfer` is also the +// engine a concrete-shaped caller can drive, and keeping it whole preserves the +// existing delegation to `ForwardDriver` unchanged. +impl<'ir, S, V, E, Lk, P, F, Sem> CallServices for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> where S: StageQuery + InterpDispatch, @@ -626,47 +633,69 @@ where .map_err(E::from) } - fn run_statement( + fn enter_function( &mut self, stage: CompileStage, - statement: Statement, + body: Statement, + args: Product, index: EnvIndex, - ) -> Result { + ) -> Result, E> { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement, + statement: body, index, }); - let result = info.dispatch_statement(statement, self); + let result = info.dispatch_function_entry(body, args, self); self.location = previous; result } +} - fn enter_function( +impl<'ir, S, V, E, Lk, P, F, Sem> StatementDispatch + for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ + fn run_statement( &mut self, stage: CompileStage, - body: Statement, - args: Product, + statement: Statement, index: EnvIndex, - ) -> Result, E> { + ) -> Result { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement: body, + statement, index, }); - let result = info.dispatch_function_entry(body, args, self); + let result = info.dispatch_statement(statement, self); self.location = previous; result } +} +impl<'ir, S, V, E, Lk, P, F, Sem> BlockQueries + for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { query::block_params(self.pipeline, stage, block).map_err(E::from) } @@ -683,17 +712,47 @@ where ) -> Result, E> { query::next_statement(self.pipeline, stage, block, after).map_err(E::from) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> CFGQueries for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, E> { query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } } +impl<'ir, S, V, E, Lk, P, F, Sem> DiGraphQueries + for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + query::digraph_walk_plan(self.pipeline, stage, graph).map_err(E::from) + } +} + // =========================================================================== // Driver capability impls (frames run on the driver, which delegates to the transfer) // =========================================================================== -impl<'ir, S, V, E, Lk, P, F, Sem> ForwardFrameDriver for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +// Delegation is unchanged; only the trait each group of methods belongs to. +impl<'ir, S, V, E, Lk, P, F, Sem> CallServices for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> where S: StageQuery + InterpDispatch>, V: Clone + HasBottom, @@ -714,25 +773,45 @@ where self.inner().resolve_call(stage, callee) } - fn run_statement( + fn enter_function( &mut self, stage: CompileStage, - statement: Statement, + body: Statement, + args: Product, index: EnvIndex, - ) -> Result { - self.inner_mut().run_statement(stage, statement, index) + ) -> Result, E> { + self.inner_mut().enter_function(stage, body, args, index) } +} - fn enter_function( +impl<'ir, S, V, E, Lk, P, F, Sem> StatementDispatch for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ + fn run_statement( &mut self, stage: CompileStage, - body: Statement, - args: Product, + statement: Statement, index: EnvIndex, - ) -> Result, E> { - self.inner_mut().enter_function(stage, body, args, index) + ) -> Result { + self.inner_mut().run_statement(stage, statement, index) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> BlockQueries for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { self.inner().block_params(stage, block) } @@ -749,13 +828,42 @@ where ) -> Result, E> { self.inner().next_statement(stage, block, after) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> CFGQueries for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, E> { self.inner().cfg_entry(stage, cfg) } } -impl<'ir, S, V, E, Lk, P, F, Sem> AbstractFrameDriver for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +impl<'ir, S, V, E, Lk, P, F, Sem> DiGraphQueries for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + self.inner().digraph_walk_plan(stage, graph) + } +} + +impl<'ir, S, V, E, Lk, P, F, Sem> ForwardDataflowFrameEngine + for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> where S: StageQuery + InterpDispatch>, V: Clone + PartialEq + Widen + HasBottom, @@ -780,11 +888,7 @@ where } fn current_function_key(&self) -> Option<

>::Key> { - match self.current_owner() { - Some(Owner::Function(key)) => Some(key.clone()), - Some(Owner::Block { function, .. }) => Some(function.clone()), - None => None, - } + self.current_owner().map(|owner| owner.function().clone()) } /// Summarize a call atomically: resolve, merge the callee entry (which seeds @@ -805,7 +909,7 @@ where } = call; let resolve_stage = call_stage.unwrap_or(stage); let target = self.inner().resolve_call(resolve_stage, &callee)?; - let key = self.inner_mut().key(target.stage, target.function, &args); + let key = self.inner_mut().key(&target, &args); self.apply_update(ForwardUpdate::FunctionEntry { key: key.clone(), @@ -826,7 +930,7 @@ where .and_then(|info| info.as_function()) .and_then(|function| function.ret.clone()); match ret { - Some(values) => self.write_results(index, &results, values), + Some(values) => self.bind_values(index, results.as_slice(), values), None => { for slot in results.iter().copied() { self.env_write(index, slot, V::bottom())?; @@ -942,12 +1046,7 @@ where Ok(()) } - ForwardUpdate::BlockEntry { - function, - block, - args, - } => { - let owner = Owner::Block { function, block }; + ForwardUpdate::OwnerEntry { owner, args } => { let changed = if self.summary(&owner).is_none() { self.summaries_mut().insert( owner.clone(), @@ -987,15 +1086,8 @@ where Ok(()) } - ForwardUpdate::BlockOutputs { - function, - block, - outputs, - } => { - let owner = Owner::Block { - function: function.clone(), - block, - }; + ForwardUpdate::OwnerOutputs { owner, outputs } => { + let function = owner.function().clone(); let mut risen = Vec::new(); for (value, incoming) in outputs { let old = self @@ -1029,8 +1121,14 @@ where } } - /// Resolve the entry block of `key`'s function (allocating its shared env on - /// first use) and seed the entry block owner with the entry-block arguments. + /// Resolve the executable entry owner of `key`'s function (allocating its + /// shared env on first use) and seed it with the entry arguments. + /// + /// This is the one place a *function* becomes runnable *work*: it translates + /// the callable [`Body`] into the executable [`Owner`] the worklist can + /// hold — a `CFG`'s entry block or a `Block` body become an + /// [`Owner::Block`], a `DiGraph` body becomes an [`Owner::Graph`]. An + /// `UnGraph` has no derivable traversal order at all, so it is rejected. fn seed_entry_block( &mut self, key: &

>::Key, @@ -1051,30 +1149,53 @@ where .map(|function| function.entry.clone()) .expect("function summary present"); let body_info = self.enter_function(stage, body, entry_args, env)?; - let entry_block = self - .cfg_entry(stage, body_info.cfg)? - .ok_or_else(|| E::from(InterpreterError::EmptyCFG))?; + let owner = match body_info.body { + Body::CFG(cfg) => Owner::Block { + function: key.clone(), + block: self + .cfg_entry(stage, cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCFG))?, + }, + Body::Block(block) => Owner::Block { + function: key.clone(), + block, + }, + // A graph body has no blocks: it is its own unit of re-analysis. + Body::DiGraph(graph) => Owner::Graph { + function: key.clone(), + graph, + }, + // An undirected graph has no producer/consumer direction, so no + // traversal order can be derived from its structure. + graph @ Body::UnGraph(_) => { + return Err(E::from(InterpreterError::NoDefaultWalker(graph))); + } + }; if let Some(function) = self .summary_mut(&Owner::Function(key.clone())) .and_then(|info| info.as_function_mut()) { - function.entry_block = Some(entry_block); + function.entry_block = match &owner { + Owner::Block { block, .. } => Some(*block), + _ => None, + }; } - self.apply_update(ForwardUpdate::BlockEntry { - function: key.clone(), - block: entry_block, + self.apply_update(ForwardUpdate::OwnerEntry { + owner, args: body_info.args, }) } } // =========================================================================== -// Owner semantics: only block owners are executable. +// Owner semantics: block and graph owners are executable. // =========================================================================== -/// The forward owner semantics. Only [`Owner::Block`] owners are analyzed: bind -/// the block-entry, walk the block once, then route its outputs / successor edges / -/// return / read-deps through [`apply_update`](ForwardDriver::apply_update). +/// The forward owner semantics. [`Owner::Block`] and [`Owner::Graph`] owners are +/// analyzed: bind the entry product, walk the unit once, then route its outputs / +/// successor edges / return / read-deps through +/// [`apply_update`](ForwardDriver::apply_update). A graph owner has no successor +/// edges — its declared yields are the function's return instead. struct SparseForwardSemantics { _marker: PhantomData V>, } @@ -1114,7 +1235,11 @@ where // safe default for the dependency-index bookkeeping path. Ok(match owner { Owner::Function(_) => ForwardSummary::Function(FunctionSummary::bottom()), - Owner::Block { .. } => ForwardSummary::Block(BlockSummary::bottom()), + // Both executable owners carry the same shape of summary: a joined + // entry product plus the output facts they define. + Owner::Block { .. } | Owner::Graph { .. } => { + ForwardSummary::Block(BlockSummary::bottom()) + } }) } @@ -1124,8 +1249,8 @@ where owner: &Owner<

>::Key>, summary: &ForwardSummary, ) -> Result { - let (function, block) = match owner { - Owner::Block { function, block } => (function.clone(), *block), + let function = match owner { + Owner::Block { function, .. } | Owner::Graph { function, .. } => function.clone(), Owner::Function(_) => { return Err(E::from(InterpreterError::Custom( "function owners are storage-only and never executed", @@ -1157,12 +1282,22 @@ where )) })?; interp.inner_mut().begin_block_log(); - Ok(F::from_block(AbstractBlockFrame::new_cfg_block( - stage, - env, - block, - block_entry, - ))) + match owner { + Owner::Block { block, .. } => Ok(F::from_block(AbstractBlockFrame::new_cfg_block( + stage, + env, + *block, + block_entry, + ))), + // One dependency-ordered pass over the whole graph. Exact for a DAG, + // so the pass never needs to iterate internally. + Owner::Graph { graph, .. } => { + F::from_digraph(AbstractDiGraphFrame::new(stage, env, *graph, block_entry)) + } + Owner::Function(_) => Err(E::from(InterpreterError::Custom( + "function owners are storage-only and never executed", + ))), + } } fn complete_owner( @@ -1171,22 +1306,29 @@ where owner: Owner<

>::Key>, completion: AbstractCompletion, ) -> Result>::Key>, ForwardSummary>, E> { - let (function, block) = match &owner { - Owner::Block { function, block } => (function.clone(), *block), + let function = match &owner { + Owner::Block { function, .. } | Owner::Graph { function, .. } => function.clone(), Owner::Function(_) => { return Err(E::from(InterpreterError::Custom( "function owners are storage-only and never executed", ))); } }; - let edges = match completion { - AbstractCompletion::CFGBlock { edges } => edges, + // A block owner completes with its outgoing CFG edges. A graph owner has + // no successors at all — it completes with the graph's declared yields, + // which for a callable graph body *are* the function's return values. + let (edges, graph_yields) = match (&owner, completion) { + (Owner::Block { .. }, AbstractCompletion::CFGBlock { edges }) => (edges, None), + (Owner::Graph { .. }, AbstractCompletion::Finished(values)) => (Vec::new(), values), _ => { return Err(E::from(InterpreterError::Custom( - "block owner completed with a non-CFG-block completion", + "executable owner completed with a mismatched completion", ))); } }; + if let Some(values) = graph_yields { + interp.contribute_return(values)?; + } let (reads, writes) = interp.inner_mut().take_logs(); let env = interp.store().env(&function).ok_or_else(|| { @@ -1217,17 +1359,19 @@ where let fact = interp.inner().env_read(env, value)?; outputs.insert(value, fact); } - interp.apply_update(ForwardUpdate::BlockOutputs { - function: function.clone(), - block, + interp.apply_update(ForwardUpdate::OwnerOutputs { + owner: owner.clone(), outputs, })?; - // Propagate CFG successor edges as block-entry updates. + // Propagate CFG successor edges as block-owner entry updates. Empty for a + // returning block and for a graph owner. for edge in edges { - interp.apply_update(ForwardUpdate::BlockEntry { - function: function.clone(), - block: edge.target, + interp.apply_update(ForwardUpdate::OwnerEntry { + owner: Owner::Block { + function: function.clone(), + block: edge.target, + }, args: edge.args, })?; } @@ -1408,7 +1552,7 @@ where Lk: Linker, P: CallContext + WideningStrategy, Sem: SparseForwardSemantic, - F: Frame, Completion = AbstractCompletion> + F: Frame, F, Completion = AbstractCompletion> + AbstractFrameBuild>::Key>, { /// Resolve `stage`/`function` by name and analyze. Returns the function's @@ -1444,10 +1588,7 @@ where ) -> Result, E> { let target = self.driver.inner().resolve_call(stage, &callee)?; let args: Product = args.into_iter().collect(); - let key = self - .driver - .inner_mut() - .key(target.stage, target.function, &args); + let key = self.driver.inner_mut().key(&target, &args); self.driver.apply_update(ForwardUpdate::FunctionEntry { key: key.clone(), @@ -1456,6 +1597,9 @@ where args, })?; + // TODO: Rename this, "semantics" is a bit misleading, sounds like the + // Semantic Keys, e.g. ForwardEval. + // Alternative: Rename the keys to: SparseForwardKey / SparseBackwardKey / DenseBackwardKey. let mut semantics = SparseForwardSemantics::new(); self.driver.drain_worklist(&mut semantics)?; diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs b/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs index e307766375..04ec2ace23 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs @@ -6,8 +6,8 @@ pub(crate) mod frames; pub(crate) mod interp; pub use frames::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - StandardAbstractFrame, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, StandardAbstractFrame, }; pub use interp::{ CallContext, ContextInsensitive, Owner, SparseForwardInterpreter, SparseForwardTransfer, diff --git a/crates/kirin-interpreter/src/facts/anchor.rs b/crates/kirin-interpreter/src/facts/anchor.rs index 41dc249792..61e5422da6 100644 --- a/crates/kirin-interpreter/src/facts/anchor.rs +++ b/crates/kirin-interpreter/src/facts/anchor.rs @@ -1,10 +1,10 @@ -//! Lattice anchors: *where* dataflow facts attach — plus scope qualification -//! and change detection. +//! Locations in the IR that dataflow reasoning refers to: lattice anchors +//! (*where* facts attach), scope qualification, and change detection. //! //! Following MLIR's terminology, a lattice fact is attached to a *lattice //! anchor*: sparse analyses anchor facts to [`SSAValue`]s; dense analyses -//! anchor facts to blocks, program points, or edges. Which anchor family an -//! analysis uses is part of its solver *shape* +//! anchor facts to [`ProgramPoint`]s. Which anchor family an analysis uses is +//! part of its solver *shape* //! ([`AnalysisShape`](crate::AnalysisShape) — see //! [`semantics`](crate::semantics)); anchors themselves carry no dispatch //! meaning. What a rule *means* is a separate concern entirely: the @@ -27,20 +27,25 @@ use kirin_ir::{Block, SSAValue, Statement}; /// /// Anchors key fact stores ([`FactStore`](crate::FactStore)) and summaries, so /// they must be cheap to clone, compare, and hash. Sparse anchors are -/// [`SSAValue`]s; dense anchors are [`Block`]s, [`ProgramPoint`]s, or -/// [`DenseAnchor`]s; [`Scoped`] qualifies any anchor with its scope. +/// [`SSAValue`]s; dense anchors are [`ProgramPoint`]s; [`Scoped`] qualifies any +/// anchor with its scope. pub trait LatticeAnchor: Clone + Eq + Hash {} impl LatticeAnchor for SSAValue {} impl LatticeAnchor for Block {} -/// A program point: immediately before or after a statement. +/// A location at which a dense dataflow fact is defined. /// -/// Never anchor a fact to a raw statement without saying *before* or *after* — -/// the two carry different facts for any non-trivial analysis. +/// Blocks and statements each have two distinct boundary points. Whole CFG and +/// graph bodies are deliberately not points: unlike a block, they do not have +/// one unambiguous entry/exit fact. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ProgramPoint { + /// State on entry to a CFG-owned or statement-owned block. + BlockEntry(Block), + /// State on exit from a CFG-owned or statement-owned block. + BlockExit(Block), /// The point immediately before `stmt` executes. Before(Statement), /// The point immediately after `stmt` executes. @@ -49,21 +54,6 @@ pub enum ProgramPoint { impl LatticeAnchor for ProgramPoint {} -/// A dense lattice anchor: a block boundary, program point, or CFG edge. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum DenseAnchor { - /// State on entry to a block. - BlockEntry(Block), - /// State on exit from a block. - BlockExit(Block), - /// State at a specific [`ProgramPoint`]. - Point(ProgramPoint), - /// State on a specific CFG edge. - Edge { from: Block, to: Block }, -} - -impl LatticeAnchor for DenseAnchor {} - // =========================================================================== // Scope qualification // =========================================================================== @@ -71,11 +61,12 @@ impl LatticeAnchor for DenseAnchor {} /// An anchor or owner qualified by the scope/context it belongs to. /// /// Framework-level summary keys are never bare anchors: the same [`SSAValue`] -/// or [`Block`] under two scopes (two stages, two analyzed cfgs, two call -/// contexts) is two distinct facts, so keys carry their scope. CFG-level -/// analyses use `(CompileStage, CFG)` as the scope; interprocedural -/// analyses generalize `K` to a call-context key (the backward analogue of the -/// forward engine's context-qualified value keys). +/// or [`Block`] under two scopes (two stages, two analyzed bodies, two call +/// contexts) is two distinct facts, so keys carry their scope. Body-level +/// analyses use [`BodyScope`](crate::BodyScope) — `(CompileStage, Body)` — as +/// the scope; interprocedural analyses generalize `K` to a call-context key +/// (the backward analogue of the forward engine's context-qualified value +/// keys). #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Scoped { pub scope: K, diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index 9269a717f9..9684cdfec9 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -1,11 +1,9 @@ -//! Dataflow fact vocabulary: anchors (*where* facts attach), the polymorphic -//! fact stores, and CFG topology enumeration. Fixpoint clients use these, -//! but they are dataflow vocabulary, not the convergence driver itself. +//! Dataflow fact vocabulary: anchors (*where* facts attach) plus the +//! polymorphic fact stores. Fixpoint clients use these, but they are dataflow +//! vocabulary, not the convergence driver itself — and not IR queries either. pub(crate) mod anchor; pub(crate) mod store; -pub(crate) mod topology; -pub use anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; -pub use store::{DenseBlockStore, DensePointStore, FactStore, ScopedSparseStore, SparseStore}; -pub use topology::{BlockTopology, CFGTopology, cfg_topology}; +pub use anchor::{Change, LatticeAnchor, ProgramPoint, Scoped}; +pub use store::{FactStore, ScopedSparseStore, SparseStore}; diff --git a/crates/kirin-interpreter/src/facts/store.rs b/crates/kirin-interpreter/src/facts/store.rs index b6dc0ad809..e7699735de 100644 --- a/crates/kirin-interpreter/src/facts/store.rs +++ b/crates/kirin-interpreter/src/facts/store.rs @@ -8,14 +8,14 @@ //! this is where analyses keep dataflow facts. The familiar stores are //! instantiations picked by the analysis's anchor: sparse analyses anchor //! facts to SSA values ([`SparseStore`], scope-qualified as -//! [`ScopedSparseStore`]), dense analyses to program points -//! ([`DensePointStore`]) or block boundaries ([`DenseBlockStore`]). +//! [`ScopedSparseStore`]), while dense analyses use +//! `FactStore, F>` directly. use std::collections::HashMap; use kirin_ir::SSAValue; -use super::anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; +use super::anchor::{Change, LatticeAnchor, Scoped}; /// One dataflow fact per lattice anchor. /// @@ -105,71 +105,9 @@ pub type SparseStore = FactStore; /// under two scopes is two distinct facts. pub type ScopedSparseStore = FactStore, F>; -/// A dense store keyed by program points, for analyses that need per-point -/// state as a queryable fact (e.g. reconstructed per-statement live sets). -pub type DensePointStore = FactStore; - -/// A dense store keyed by block boundaries (entry/exit), for dense analyses. -/// -/// This is the rustc-style default: store block-boundary states and -/// reconstruct statement-local states on demand rather than persisting every -/// before/after state. A thin convenience wrapper over -/// `FactStore` using the -/// [`BlockEntry`](DenseAnchor::BlockEntry)/[`BlockExit`](DenseAnchor::BlockExit) -/// anchors. -#[derive(Clone, Debug)] -pub struct DenseBlockStore { - facts: FactStore, -} - -impl Default for DenseBlockStore { - fn default() -> Self { - Self::new() - } -} - -impl DenseBlockStore { - pub fn new() -> Self { - Self { - facts: FactStore::new(), - } - } - - pub fn entry(&self, block: kirin_ir::Block) -> Option<&F> { - self.facts.get(DenseAnchor::BlockEntry(block)) - } - - pub fn exit(&self, block: kirin_ir::Block) -> Option<&F> { - self.facts.get(DenseAnchor::BlockExit(block)) - } - - pub fn set_entry(&mut self, block: kirin_ir::Block, fact: F) { - self.facts.set(DenseAnchor::BlockEntry(block), fact); - } - - pub fn set_exit(&mut self, block: kirin_ir::Block, fact: F) { - self.facts.set(DenseAnchor::BlockExit(block), fact); - } - - /// Iterate `(block, entry fact)` pairs (order unspecified). - pub fn entries(&self) -> impl Iterator { - self.facts.iter().filter_map(|(anchor, fact)| match anchor { - DenseAnchor::BlockEntry(block) => Some((block, fact)), - _ => None, - }) - } - - /// Iterate `(block, exit fact)` pairs (order unspecified). - pub fn exits(&self) -> impl Iterator { - self.facts.iter().filter_map(|(anchor, fact)| match anchor { - DenseAnchor::BlockExit(block) => Some((block, fact)), - _ => None, - }) - } -} - #[cfg(test)] mod tests { + use crate::{Body, BodyScope, ProgramPoint}; use kirin_ir::{Block, CFG, CompileStage, Id, Statement, TestSSAValue}; use super::*; @@ -227,35 +165,39 @@ mod tests { } #[test] - fn dense_block_store_maps_entry_exit_through_dense_anchor() { + fn scoped_dense_facts_keep_block_and_statement_boundaries_distinct() { let block = Block::from(Id::from(ssa(0))); let other = Block::from(Id::from(ssa(1))); - - let mut store: DenseBlockStore<&'static str> = DenseBlockStore::new(); - store.set_entry(block, "in"); - store.set_exit(block, "out"); - - // Entry and exit of the same block are distinct anchors. - assert_eq!(store.entry(block), Some(&"in")); - assert_eq!(store.exit(block), Some(&"out")); - assert_eq!(store.entry(other), None); - - let entries: Vec<_> = store.entries().collect(); - let exits: Vec<_> = store.exits().collect(); - assert_eq!(entries, vec![(block, &"in")]); - assert_eq!(exits, vec![(block, &"out")]); - } - - #[test] - fn dense_point_store_keeps_before_and_after_distinct() { let statement = Statement::from(Id::from(ssa(3))); - let mut store: DensePointStore<&'static str> = FactStore::new(); - store.set(ProgramPoint::Before(statement), "before"); - store.set(ProgramPoint::After(statement), "after"); - - assert_eq!(store.get(ProgramPoint::Before(statement)), Some(&"before")); - assert_eq!(store.get(ProgramPoint::After(statement)), Some(&"after")); - assert_eq!(store.len(), 2); + let scope: BodyScope = ( + CompileStage::from(Id::from(ssa(10))), + Body::CFG(CFG::from(Id::from(ssa(11)))), + ); + let point = |item| Scoped::new(scope, item); + let mut store: FactStore, &'static str> = FactStore::new(); + store.set(point(ProgramPoint::BlockEntry(block)), "in"); + store.set(point(ProgramPoint::BlockExit(block)), "out"); + store.set(point(ProgramPoint::Before(statement)), "before"); + store.set(point(ProgramPoint::After(statement)), "after"); + + assert_eq!( + store.get(point(ProgramPoint::BlockEntry(block))), + Some(&"in") + ); + assert_eq!( + store.get(point(ProgramPoint::BlockExit(block))), + Some(&"out") + ); + assert_eq!(store.get(point(ProgramPoint::BlockEntry(other))), None); + assert_eq!( + store.get(point(ProgramPoint::Before(statement))), + Some(&"before") + ); + assert_eq!( + store.get(point(ProgramPoint::After(statement))), + Some(&"after") + ); + assert_eq!(store.len(), 4); } } diff --git a/crates/kirin-interpreter/src/facts/topology.rs b/crates/kirin-interpreter/src/facts/topology.rs deleted file mode 100644 index 644401bc24..0000000000 --- a/crates/kirin-interpreter/src/facts/topology.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Dialect-neutral CFG topology enumeration. -//! -//! Backward analyses need the *shape* of a CFG: which blocks exist -//! (including blocks nested inside structured statements), each block's -//! statements, the CFG successor relation, and each block's *feeders* — the -//! statements whose rules can translate demand on that block's parameters -//! (terminators targeting it, statements owning it). This is topology only — -//! uses/defs/edge-argument *semantics* stay in dialect -//! [`Interpretable`](crate::Interpretable) rules; the enumeration consumes the -//! generic [`HasSuccessors`]/[`HasBlocks`]/[`HasCFG`] contract every -//! dialect derives. - -use std::collections::{HashMap, HashSet}; - -use kirin_ir::{Block, CFG, Dialect, HasBlocks, HasCFG, HasSuccessors, StageInfo, Statement}; - -/// The shape of one block: its statements and CFG successors. -#[derive(Clone, Debug)] -pub struct BlockTopology { - pub block: Block, - /// Statements in program order; the terminator, if any, is last. - pub stmts: Vec, - /// CFG successor blocks (targets of the block's terminator). - pub successors: Vec, - /// `true` for blocks nested inside a statement (structured bodies), - /// `false` for the analyzed CFG's own top-level blocks. - pub nested: bool, -} - -/// The shape of a CFG: all blocks (the CFG's own top-level blocks and -/// structured bodies, recursively) plus the block-feeder index. -#[derive(Clone, Debug, Default)] -pub struct CFGTopology { - pub blocks: Vec, - feeders: HashMap>, -} - -impl CFGTopology { - /// The statements whose rules can translate demand on `block`'s parameters: - /// terminators with an edge into `block`, plus statements owning `block` - /// as a structured body. - pub fn feeders(&self, block: Block) -> &[Statement] { - self.feeders.get(&block).map(Vec::as_slice).unwrap_or(&[]) - } - - /// The analyzed CFG's own top-level blocks (excluding nested bodies). - pub fn cfg_blocks(&self) -> impl Iterator { - self.blocks.iter().filter(|block| !block.nested) - } -} - -/// Enumerate the topology of `cfg` in the finalized `stage`. -pub fn cfg_topology(stage: &StageInfo, cfg: &CFG) -> CFGTopology -where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a>, -{ - let mut topology = CFGTopology::default(); - let mut visited = HashSet::new(); - for block in cfg.blocks(stage) { - collect_block(stage, block, false, &mut topology, &mut visited); - } - topology -} - -fn collect_block( - stage: &StageInfo, - block: Block, - nested: bool, - topology: &mut CFGTopology, - visited: &mut HashSet, -) where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a>, -{ - if !visited.insert(block) { - return; - } - - let mut stmts: Vec = block.statements(stage).collect(); - if let Some(terminator) = block.terminator(stage) { - stmts.push(terminator); - } - - // CFG successor edges: the target's feeders include the terminator. - let mut successors = Vec::new(); - for &stmt in &stmts { - for successor in stmt.definition(stage).successors() { - let target = successor.target(); - successors.push(target); - topology.feeders.entry(target).or_default().push(stmt); - } - } - - topology.blocks.push(BlockTopology { - block, - stmts: stmts.clone(), - successors, - nested, - }); - - // Structured bodies: the owning statement feeds each owned block. - for &stmt in &stmts { - let definition = stmt.definition(stage); - let owned_blocks: Vec = definition.blocks().copied().collect(); - let owned_cfgs: Vec = definition.cfgs().copied().collect(); - for owned in owned_blocks { - topology.feeders.entry(owned).or_default().push(stmt); - collect_block(stage, owned, true, topology, visited); - } - for owned_cfg in owned_cfgs { - for owned in owned_cfg.blocks(stage) { - topology.feeders.entry(owned).or_default().push(stmt); - collect_block(stage, owned, true, topology, visited); - } - } - } -} diff --git a/crates/kirin-interpreter/src/fixpoint/runner.rs b/crates/kirin-interpreter/src/fixpoint/runner.rs index 4887df5871..36857206e7 100644 --- a/crates/kirin-interpreter/src/fixpoint/runner.rs +++ b/crates/kirin-interpreter/src/fixpoint/runner.rs @@ -21,7 +21,7 @@ where /// a fresh stack. pub fn run_frame(&mut self, root: P::Frame) -> Result where - P::Frame: Frame, + P::Frame: Frame, { if !self.frame_stack.is_empty() { return Err(I::Error::from(InterpreterError::Custom( diff --git a/crates/kirin-interpreter/src/fixpoint/solver.rs b/crates/kirin-interpreter/src/fixpoint/solver.rs index db4a1f5f53..3f852f13b8 100644 --- a/crates/kirin-interpreter/src/fixpoint/solver.rs +++ b/crates/kirin-interpreter/src/fixpoint/solver.rs @@ -45,7 +45,7 @@ where /// Analyse `entry` and everything it transitively schedules, to a fixpoint. pub fn solve(&mut self, semantics: &mut Sem, entry: P::SummaryKey) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -69,7 +69,7 @@ where entries: impl IntoIterator, ) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -90,7 +90,7 @@ where iterations: usize, ) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -113,7 +113,7 @@ where /// Pop and analyse owners until the worklist is empty. pub fn drain_worklist(&mut self, semantics: &mut Sem) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -184,7 +184,7 @@ where owner: P::SummaryKey, ) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, diff --git a/crates/kirin-interpreter/src/fixpoint/tests/counter.rs b/crates/kirin-interpreter/src/fixpoint/tests/counter.rs index a1d489fd69..d8270094b1 100644 --- a/crates/kirin-interpreter/src/fixpoint/tests/counter.rs +++ b/crates/kirin-interpreter/src/fixpoint/tests/counter.rs @@ -39,22 +39,22 @@ impl Summary for CounterSummary { #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct CounterFrame(u8); -impl> Frame for CounterFrame { +impl, F> Frame for CounterFrame { type Completion = u8; - fn step(self, _interp: &mut D) -> Result, InterpreterError> { + fn step_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Complete(self.0.saturating_add(1).min(2))) } - fn resume_done(self, _interp: &mut D) -> Result, InterpreterError> { + fn resume_done_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Done) } - fn resume( + fn resume_into( self, completion: u8, _interp: &mut D, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Complete(completion)) } } diff --git a/crates/kirin-interpreter/src/fixpoint/tests/deps.rs b/crates/kirin-interpreter/src/fixpoint/tests/deps.rs index 0275493f2c..1793421688 100644 --- a/crates/kirin-interpreter/src/fixpoint/tests/deps.rs +++ b/crates/kirin-interpreter/src/fixpoint/tests/deps.rs @@ -41,22 +41,22 @@ struct DepFrame { owner: u8, } -impl> Frame for DepFrame { +impl, F> Frame for DepFrame { type Completion = u8; - fn step(self, _interp: &mut D) -> Result, InterpreterError> { + fn step_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Complete(self.owner.saturating_add(1))) } - fn resume_done(self, _interp: &mut D) -> Result, InterpreterError> { + fn resume_done_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Done) } - fn resume( + fn resume_into( self, completion: u8, _interp: &mut D, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Complete(completion)) } } diff --git a/crates/kirin-interpreter/src/fixpoint/tests/phase.rs b/crates/kirin-interpreter/src/fixpoint/tests/phase.rs index 8115cf14ae..79966a881e 100644 --- a/crates/kirin-interpreter/src/fixpoint/tests/phase.rs +++ b/crates/kirin-interpreter/src/fixpoint/tests/phase.rs @@ -42,10 +42,10 @@ struct PhaseFrame; type PhaseInterp = StandardFixpointInterpreter>; -impl Frame for PhaseFrame { +impl Frame for PhaseFrame { type Completion = u8; - fn step(self, interp: &mut PhaseInterp) -> Result, InterpreterError> { + fn step_into(self, interp: &mut PhaseInterp) -> Result, InterpreterError> { let completion = match interp.phase() { FixpointPhase::Join => 1, FixpointPhase::Widen => 10, @@ -54,18 +54,18 @@ impl Frame for PhaseFrame { Ok(FrameEffect::Complete(completion)) } - fn resume_done( + fn resume_done_into( self, _interp: &mut PhaseInterp, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Done) } - fn resume( + fn resume_into( self, completion: u8, _interp: &mut PhaseInterp, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Complete(completion)) } } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index e3610a9323..34aa28bd52 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -40,7 +40,7 @@ //! per semantic key (and [`FunctionEntry`] for callable statements). A rule //! receives the engine `interp` directly. Shape-generic mechanics live on //! the engine traits (read/write on [`SparseForwardInterp`]; -//! fact/raise-fact on [`SparseBackwardInterp`]; insert/remove point facts on +//! fact/raise-fact on [`SparseBackwardInterp`]; opaque point-state access on //! [`DenseBackwardInterp`]); semantics-specific vocabulary lives in helper //! traits — demand rules bind [`DemandInterp`] //! (`demand`/`is_demanded`/`demand_uses_if_observable`), classic-liveness rules bind @@ -66,52 +66,61 @@ mod semantics; // The shared chassis: engine trait + dialect dispatch, effect types, // activation storage, calling conventions, errors, and IR queries. -pub use self::core::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; +pub use self::core::{ + AbstractInterpreter, Env, GraphWalkPlan, Interp, InterpLocation, SparseForwardInterp, +}; +pub use self::core::{Body, CallEffect, CallableBody, Callee, Edge, SparseForwardEffect}; pub use self::core::{BranchCondition, HasProductValue, expect_single}; -pub use self::core::{CallEffect, Callee, Edge, FunctionBody, SparseForwardEffect}; pub use self::core::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use self::core::{EnvIndex, EnvStackStore, Store}; pub use self::core::{FunctionEntry, InterpDispatch, Interpretable}; -pub use self::core::{InterpreterError, StageQuery}; -// The shared, direction-neutral frame protocol (`Frame`/`FrameEngine`/ -// `FrameEffect`/`drive_frames`) plus the forward frame-driver capability surfaces. +pub use self::core::{InterpreterError, StageQuery, TerminatorArgs}; +// The shared, direction-neutral frame protocol: `Frame`/`FrameEffect`/ +// `drive_frames` (the frame-stack driver loop) anchored on `FrameEngine`, the +// minimal engine contract. On top of it, the forward engine capabilities a frame +// can require: one narrowly scoped component trait per kind of traversal +// (`StatementDispatch`, `BlockQueries`, `CFGQueries`, `DiGraphQueries`, +// `CallServices`), so a member frame bounds only what it consumes, plus two +// whole-universe umbrellas — `ForwardFrameEngine` (full standard concrete +// surface) and `ForwardDataflowFrameEngine` (standard forward-abstract surface). pub use self::core::{ - ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameEffect, FrameEngine, drive_frames, + BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, }; -// Backward-compatible aliases for the forward frame-driver capability surfaces. -pub use self::core::ForwardDataflowFrameDriver as AbstractFrameDriver; -pub use self::core::ForwardFrameDriver as FrameDriver; -// Concrete execution engine + the concrete standard frames. +// Concrete execution engine + the concrete standard frames: the +// representation walkers (`BlockFrame`/`CFGFrame`/`DiGraphFrame` — `UnGraph` +// traversal is a dialect/compiler policy supplied through +// `FrameBuild::from_ungraph_entry`) and the `CallFrame` call boundary. pub use engines::concrete::{ - BodyFrame, CallFrame, Completion, ConcreteInterpreter, FrameBuild, StandardFrame, + BlockFrame, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, CallFrame, Completion, + ConcreteInterpreter, DefaultBodyFrames, DiGraphFrame, FrameBuild, StandardFrame, UnGraphEntry, }; // Sparse forward engine (`Sem = ForwardEval`) + the abstract standard frames. pub use engines::sparse_forward::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, CallContext, - ContextInsensitive, Owner, SparseForwardInterpreter, SparseForwardTransfer, - StandardAbstractFrame, WideningStrategy, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, CallContext, ContextInsensitive, Owner, SparseForwardInterpreter, + SparseForwardTransfer, StandardAbstractFrame, WideningStrategy, }; // Sparse backward engine (`Sem = StrongDemand`). pub use engines::sparse_backward::{ - BackwardAnalysisState, CFGScope, DemandFrame, DemandInterp, DemandSummary, + BackwardAnalysisState, BodyScope, DemandFrame, DemandInterp, DemandSummary, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; // Dense backward engine (`Sem = ClassicLiveness`) + the dense standard frames. pub use engines::dense_backward::{ - BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, - DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardTransfer, DenseBlockFrame, + BlockLiveness, ClassicLivenessInterp, DenseBackwardCompletion, DenseBackwardDriver, + DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, + DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, DenseBlockFrame, DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, SuccessorEdge, }; -// Lattice anchors (*where* facts attach), scope qualification, the polymorphic -// fact stores, and cfg topology enumeration. Anchor family is a property of -// the solver shape; dispatch meaning lives in `semantics`. +// Lattice anchors (*where* facts attach), scope qualification, and the +// polymorphic fact stores. Anchor family is a property of the solver shape; +// dispatch meaning lives in `semantics`. pub use facts::{ - BlockTopology, CFGTopology, Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, - LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, SparseStore, cfg_topology, + Change, FactStore, LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, SparseStore, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` @@ -135,7 +144,9 @@ pub use fixpoint::{ }; #[cfg(feature = "derive")] -pub use kirin_derive_interpreter::{FunctionEntry, InterpDispatch, Interpretable}; +pub use kirin_derive_interpreter::{ + AbstractFrameBuild, DenseFrameBuild, FrameBuild, FunctionEntry, InterpDispatch, Interpretable, +}; /// Everything a dialect author needs to implement statement semantics — /// forward evaluation (`Interpretable`), backward demand @@ -144,10 +155,10 @@ pub use kirin_derive_interpreter::{FunctionEntry, InterpDispatch, Interpretable} /// (`impl SemanticKey for MyKey { type Shape = ...; }`). pub mod dialect { pub use crate::{ - AnalysisShape, BranchCondition, CallEffect, Callee, ClassicLiveness, ClassicLivenessInterp, - DemandInterp, DenseBackwardEffect, DenseBackwardInterp, DenseBackwardShape, - DenseForwardShape, Edge, ForwardEval, FunctionBody, FunctionEntry, HasProductValue, Interp, - Interpretable, InterpreterError, PointFacts, SemanticKey, SparseBackwardEffect, + AnalysisShape, Body, BranchCondition, CallEffect, CallableBody, Callee, ClassicLiveness, + ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, DenseBackwardInterp, + DenseBackwardShape, DenseForwardShape, Edge, ForwardEval, FunctionEntry, HasProductValue, + Interp, Interpretable, InterpreterError, PointFacts, SemanticKey, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardShape, SparseForwardEffect, SparseForwardInterp, SparseForwardShape, StrongDemand, SuccessorEdge, }; @@ -156,15 +167,17 @@ pub mod dialect { /// Everything a compiler author needs to run engines or customize traversal. pub mod engine { pub use crate::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, AbstractInterpreter, BodyFrame, CallContext, CallFrame, Callee, - Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, - DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBlockFrame, DenseFrameBuild, Env, - ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, FrameDriver, - FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, InterpreterError, Linker, - SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, SparseForwardInterp, - SparseForwardInterpreter, StandardAbstractFrame, StandardDenseBackwardFrame, StandardFrame, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, AbstractInterpreter, BlockFrame, BlockQueries, BodyFrameEntry, + CFGFrame, CFGQueries, CallBodyFramePolicy, CallContext, CallFrame, CallServices, Callee, + Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DefaultBodyFrames, + DenseBackwardCompletion, DenseBackwardFrameEngine, DenseBackwardInterp, + DenseBackwardInterpreter, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, + DiGraphFrame, DiGraphQueries, Env, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, + FrameBuild, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, + InterpreterError, Linker, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, + SparseForwardInterp, SparseForwardInterpreter, StandardAbstractFrame, + StandardDenseBackwardFrame, StandardFrame, StatementDispatch, UnGraphEntry, WideningStrategy, drive_frames, expect_single, }; } diff --git a/crates/kirin-ir/src/builder/block.rs b/crates/kirin-ir/src/builder/block.rs index 84429c3345..6c28adb9e5 100644 --- a/crates/kirin-ir/src/builder/block.rs +++ b/crates/kirin-ir/src/builder/block.rs @@ -1,3 +1,5 @@ +use smallvec::SmallVec; + use crate::node::ssa::{BuilderSSAInfo, BuilderSSAKind, ResolutionInfo, SSAValue}; use crate::node::stmt::StatementParent; use crate::node::*; @@ -182,10 +184,11 @@ impl<'a, L: Dialect> BlockBuilder<'a, L> { } let block = BlockInfo::builder() - .maybe_parent(self.parent) + .maybe_parent(self.parent.map(BlockParent::CFG)) .maybe_name(self.name.map(|n| self.stage.symbols.intern(n))) .node(LinkedListNode::new(id)) .arguments(block_args) + .predecessors(SmallVec::new()) .statements(self.stage.link_statements(&self.statements)) .maybe_terminator(self.terminator) .new(); diff --git a/crates/kirin-ir/src/builder/cfg.rs b/crates/kirin-ir/src/builder/cfg.rs index efecf644a4..80644a9f5f 100644 --- a/crates/kirin-ir/src/builder/cfg.rs +++ b/crates/kirin-ir/src/builder/cfg.rs @@ -1,4 +1,4 @@ -use crate::{Block, BuilderStageInfo, CFG, Dialect, Statement, node::CFGInfo}; +use crate::{Block, BlockParent, BuilderStageInfo, CFG, Dialect, Statement, node::CFGInfo}; pub struct CFGBuilder<'a, L: Dialect> { pub(super) stage: &'a mut BuilderStageInfo, @@ -31,6 +31,16 @@ impl<'a, L: Dialect> CFGBuilder<'a, L> { #[allow(clippy::wrong_self_convention, clippy::new_ret_no_self)] pub fn new(self) -> CFG { let id = self.stage.cfgs.next_id(); + for &block in &self.blocks { + let parent = self.stage.blocks[block].parent; + assert!( + parent.is_none() || parent == Some(BlockParent::CFG(id)), + "Block `{block}` already has a different parent" + ); + } + for &block in &self.blocks { + self.stage.blocks[block].parent = Some(BlockParent::CFG(id)); + } let info = CFGInfo::builder() .id(id) .blocks(self.stage.link_blocks(&self.blocks)) diff --git a/crates/kirin-ir/src/builder/stage_info.rs b/crates/kirin-ir/src/builder/stage_info.rs index d121eb2a4a..5614a1af3d 100644 --- a/crates/kirin-ir/src/builder/stage_info.rs +++ b/crates/kirin-ir/src/builder/stage_info.rs @@ -236,10 +236,13 @@ impl BuilderStageInfo { }, |_info| None, ); - Ok(StageInfo { + let mut stage = StageInfo { nodes: self.nodes, ssas, - }) + }; + stage.rebuild_use_index(); + stage.rebuild_predecessor_index(); + Ok(stage) } /// Convert to [`StageInfo`] without validation. @@ -279,10 +282,13 @@ impl BuilderStageInfo { // Deleted items become None tombstones — safe, no zeroed memory. |_info| None, ); - StageInfo { + let mut stage = StageInfo { nodes: self.nodes, ssas, - } + }; + stage.rebuild_use_index(); + stage.rebuild_predecessor_index(); + stage } } diff --git a/crates/kirin-ir/src/builder/staged.rs b/crates/kirin-ir/src/builder/staged.rs index 88f839c261..775d0b1ae7 100644 --- a/crates/kirin-ir/src/builder/staged.rs +++ b/crates/kirin-ir/src/builder/staged.rs @@ -104,6 +104,40 @@ impl BuilderStageInfo { #[builder(finish_fn = new)] pub fn statement(&mut self, #[builder(into)] definition: L) -> Statement { let id = self.statements.next_id(); + let owned_blocks: Vec = definition.blocks().copied().collect(); + let owned_cfgs: Vec = definition.cfgs().copied().collect(); + let owned_digraphs: Vec = definition.digraphs().copied().collect(); + let owned_ungraphs: Vec = definition.ungraphs().copied().collect(); + + for &block in &owned_blocks { + let parent = self.blocks[block].parent; + assert!( + parent.is_none() || parent == Some(BlockParent::Statement(id)), + "Block `{block}` already has a different parent" + ); + } + for &cfg in &owned_cfgs { + let parent = self.cfgs[cfg].parent; + assert!( + parent.is_none() || parent == Some(id), + "CFG `{cfg:?}` already has a different parent" + ); + } + for &graph in &owned_digraphs { + let parent = self.digraphs[graph].parent; + assert!( + parent.is_none() || parent == Some(id), + "DiGraph `{graph:?}` already has a different parent" + ); + } + for &graph in &owned_ungraphs { + let parent = self.ungraphs[graph].parent; + assert!( + parent.is_none() || parent == Some(id), + "UnGraph `{graph:?}` already has a different parent" + ); + } + let statement = StatementInfo { node: LinkedListNode::new(id), parent: None, @@ -111,6 +145,19 @@ impl BuilderStageInfo { }; let _ = self.statements.alloc(statement); + for block in owned_blocks { + self.blocks[block].parent = Some(BlockParent::Statement(id)); + } + for cfg in owned_cfgs { + self.cfgs[cfg].parent = Some(id); + } + for graph in owned_digraphs { + self.digraphs[graph].parent = Some(id); + } + for graph in owned_ungraphs { + self.ungraphs[graph].parent = Some(id); + } + // Resolve Unresolved(Result(idx)) SSAs now that the statement ID is known let result_ssas: Vec = self.statements[id] .definition diff --git a/crates/kirin-ir/src/detach.rs b/crates/kirin-ir/src/detach.rs index 13ad97a198..0ec2d503a1 100644 --- a/crates/kirin-ir/src/detach.rs +++ b/crates/kirin-ir/src/detach.rs @@ -1,6 +1,6 @@ use crate::arena::GetInfo; use crate::node::stmt::StatementParent; -use crate::node::{Block, Statement}; +use crate::node::{Block, BlockParent, Statement}; use crate::query::{LinkedListElem, LinkedListInfo, ParentInfo}; use crate::{Dialect, StageInfo}; @@ -64,55 +64,51 @@ impl Detach for Statement { } } -macro_rules! impl_detach { - ($ty:ty) => { - impl Detach for $ty { - fn detach(&self, stage: &mut StageInfo) { - let (prev, next, parent) = if let Some(info) = self.get_info_mut(stage) { - let prev = info.get_prev_mut().take(); - let next = info.get_next_mut().take(); - let parent = info.get_parent_mut().take(); - (prev, next, parent) - } else { - (None, None, None) - }; - - if let Some(prev) = prev { - let prev_info = prev.expect_info_mut(stage); - prev_info.node.next = next; - } - if let Some(next) = next { - let next_info = next.expect_info_mut(stage); - *next_info.get_prev_mut() = prev; - } - - if let Some(parent) = parent { - let parent_info = parent.expect_info_mut(stage); - // if prev is None, set head of parent block to next - if prev.is_none() { - debug_assert!( - *parent_info.get_head() == Some(*self), - "Parent block's head does not match the statement being detached" - ); - *parent_info.get_head_mut() = next; - } +impl Detach for Block { + fn detach(&self, stage: &mut StageInfo) { + let (prev, next, parent) = if let Some(info) = self.get_info_mut(stage) { + assert!( + !matches!(info.parent, Some(BlockParent::Statement(_))), + "Cannot detach a block directly owned by a statement" + ); + let prev = info.get_prev_mut().take(); + let next = info.get_next_mut().take(); + let parent = info.get_parent_mut().take(); + (prev, next, parent) + } else { + (None, None, None) + }; - // if next is None, set tail of parent block to prev - if next.is_none() { - debug_assert!( - *parent_info.get_tail() == Some(*self), - "Parent block's tail does not match the statement being detached" - ); - *parent_info.get_tail_mut() = prev; - } + if let Some(prev) = prev { + let prev_info = prev.expect_info_mut(stage); + prev_info.node.next = next; + } + if let Some(next) = next { + let next_info = next.expect_info_mut(stage); + *next_info.get_prev_mut() = prev; + } - *parent_info.get_len_mut() = parent_info.get_len().checked_sub(1).expect( - "linked list length underflow: detaching from a parent with zero length", - ); - } - } + let Some(BlockParent::CFG(parent)) = parent else { + return; + }; + let parent_info = parent.expect_info_mut(stage); + if prev.is_none() { + debug_assert!( + *parent_info.get_head() == Some(*self), + "Parent CFG's head does not match the block being detached" + ); + *parent_info.get_head_mut() = next; } - }; + if next.is_none() { + debug_assert!( + *parent_info.get_tail() == Some(*self), + "Parent CFG's tail does not match the block being detached" + ); + *parent_info.get_tail_mut() = prev; + } + *parent_info.get_len_mut() = parent_info + .get_len() + .checked_sub(1) + .expect("linked list length underflow: detaching from a parent with zero length"); + } } - -impl_detach!(Block); diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index 78659f84c1..7c08936857 100644 --- a/crates/kirin-ir/src/lib.rs +++ b/crates/kirin-ir/src/lib.rs @@ -31,12 +31,13 @@ pub use language::{ }; pub use lattice::{FiniteLattice, HasBottom, HasTop, Lattice, TypeLattice, Widen}; pub use node::{ - Block, BlockArgument, BlockInfo, BuilderKey, BuilderSSAInfo, BuilderSSAKind, CFG, CompileStage, - DeletedSSAValue, DiGraph, DiGraphExtra, DiGraphInfo, Function, FunctionInfo, GlobalSymbol, - GraphInfo, LinkedList, LinkedListNode, Port, PortParent, ResolutionInfo, ResultValue, SSAInfo, - SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, StagedFunction, - StagedFunctionInfo, StagedNamePolicy, Statement, StatementInfo, StatementParent, Successor, - Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, UniqueLiveSpecializationError, + Block, BlockArgument, BlockInfo, BlockParent, BuilderKey, BuilderSSAInfo, BuilderSSAKind, CFG, + CompileStage, DeletedSSAValue, DiGraph, DiGraphExtra, DiGraphInfo, Function, FunctionInfo, + GlobalSymbol, GraphInfo, LinkedList, LinkedListNode, Port, PortParent, ResolutionInfo, + ResultValue, SSAInfo, SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, + StagedFunction, StagedFunctionInfo, StagedNamePolicy, Statement, StatementInfo, + StatementParent, Successor, Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, + UniqueLiveSpecializationError, Use, }; pub use pipeline::Pipeline; pub use product::{HasProduct, Product}; diff --git a/crates/kirin-ir/src/node/block.rs b/crates/kirin-ir/src/node/block.rs index eb90ef25e6..1e348603d2 100644 --- a/crates/kirin-ir/src/node/block.rs +++ b/crates/kirin-ir/src/node/block.rs @@ -1,11 +1,13 @@ +use smallvec::SmallVec; + use crate::{ Dialect, Symbol, arena::{GetInfo, Id, Item}, identifier, - node::cfg::CFG, }; use super::{ + cfg::CFG, linked_list::{LinkedList, LinkedListNode}, ssa::BlockArgument, stmt::Statement, @@ -48,13 +50,31 @@ impl std::fmt::Display for Successor { } } +/// The immediate structural owner of a block. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum BlockParent { + /// The block belongs to a block-list control-flow body. + CFG(CFG), + /// The block is a single-block body owned directly by a statement. + Statement(Statement), +} + #[derive(Clone, Debug, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlockInfo { - pub parent: Option, + pub parent: Option, pub name: Option, pub node: LinkedListNode, pub arguments: Vec, + /// Reverse control-flow index: blocks whose terminators may transfer + /// control to this block. + /// + /// Inline capacity 4: a straight-line block has one predecessor, an + /// if-merge or loop header has two, and a small switch join or a loop + /// with a couple of `break`s stays under four. Wider joins spill to the + /// heap rather than making every block pay for the worst case. + pub predecessors: SmallVec<[Block; 4]>, pub statements: LinkedList, pub terminator: Option, _marker: std::marker::PhantomData, @@ -64,14 +84,16 @@ pub struct BlockInfo { impl BlockInfo { #[builder(finish_fn = new)] pub(crate) fn new( - /// The parent cfg of this block. - parent: Option, + /// The immediate CFG or statement parent of this block. + parent: Option, /// The name of this block. name: Option, /// The linked list node for this block. node: LinkedListNode, /// The arguments of this block. arguments: Vec, + /// The predecessor blocks in the reverse control-flow index. + predecessors: SmallVec<[Block; 4]>, /// The statements contained in this block. statements: Option>, /// The terminator statement of this block, if any. @@ -82,6 +104,7 @@ impl BlockInfo { name, node, arguments, + predecessors, statements: statements.unwrap_or_default(), terminator, _marker: std::marker::PhantomData, diff --git a/crates/kirin-ir/src/node/mod.rs b/crates/kirin-ir/src/node/mod.rs index 2615537850..98ed75c2f9 100644 --- a/crates/kirin-ir/src/node/mod.rs +++ b/crates/kirin-ir/src/node/mod.rs @@ -10,7 +10,7 @@ pub mod stmt; pub mod symbol; pub(crate) mod ungraph; -pub use block::{Block, BlockInfo, Successor}; +pub use block::{Block, BlockInfo, BlockParent, Successor}; pub use cfg::{CFG, CFGInfo}; pub use digraph::{DiGraph, DiGraphInfo}; pub use function::{ @@ -22,7 +22,7 @@ pub use linked_list::{LinkedList, LinkedListNode}; pub use port::{Port, PortParent}; pub use ssa::{ BlockArgument, BuilderKey, BuilderSSAInfo, BuilderSSAKind, DeletedSSAValue, ResolutionInfo, - ResultValue, SSAInfo, SSAKind, SSAValue, TestSSAValue, + ResultValue, SSAInfo, SSAKind, SSAValue, TestSSAValue, Use, }; pub use stmt::{Statement, StatementInfo, StatementParent}; pub use symbol::{GlobalSymbol, Symbol}; diff --git a/crates/kirin-ir/src/node/ssa.rs b/crates/kirin-ir/src/node/ssa.rs index a3e47cc600..4efe340bbb 100644 --- a/crates/kirin-ir/src/node/ssa.rs +++ b/crates/kirin-ir/src/node/ssa.rs @@ -3,6 +3,7 @@ use crate::identifier; use crate::{Dialect, Symbol}; use smallvec::SmallVec; +use super::digraph::DiGraph; use super::port::{Port, PortParent}; use super::{block::Block, stmt::Statement}; @@ -238,10 +239,32 @@ impl From> for BuilderSSAInfo { } } -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Use { - stmt: Statement, - operand_index: usize, +/// One def-use edge: a position in the IR that reads an SSA value. +/// +/// Stored in [`SSAInfo::uses`] as the reverse index of the authoritative +/// storage. Two kinds of position read a value: +/// +/// - a **statement operand** slot (the usual case — `arith.add`'s operands, +/// CFG branch arguments, `scf.yield` values, function returns), and +/// - a **`DiGraph` body yield** — a value the graph exports across its body +/// boundary. A yield has no backing statement (it lives in +/// [`DiGraphInfo::yields`](crate::DiGraphInfo)), so it cannot be named as a +/// statement operand, but it is a genuine use. +/// +/// `UnGraph` has no analogue: its `Extra` is a list of edge *statements*, whose +/// operands are already ordinary statement-operand uses. +/// +/// Populated by +/// [`StageInfo::rebuild_use_index`](crate::StageInfo::rebuild_use_index) at +/// finalization; a mutation layer (the rewriter) must keep it in sync with +/// every operand and yield change. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Use { + /// The `index`-th operand slot of `stmt`, in `HasArguments` order. + StatementOperand { stmt: Statement, index: usize }, + /// The `index`-th yield slot of the directed graph body `graph`. + DiGraphYield { graph: DiGraph, index: usize }, } /// A lookup key for builder placeholders — resolved at build time to the real SSA value. diff --git a/crates/kirin-ir/src/query/info.rs b/crates/kirin-ir/src/query/info.rs index 7a6845c425..c3fc7aab5a 100644 --- a/crates/kirin-ir/src/query/info.rs +++ b/crates/kirin-ir/src/query/info.rs @@ -1,7 +1,7 @@ use crate::{ Dialect, LinkedList, node::{ - Block, BlockInfo, CFG, CFGInfo, LinkedListNode, Statement, StatementInfo, + Block, BlockInfo, BlockParent, CFGInfo, LinkedListNode, Statement, StatementInfo, stmt::StatementParent, }, }; @@ -26,7 +26,7 @@ impl ParentInfo for StatementInfo { } impl ParentInfo for BlockInfo { - type ParentPtr = CFG; + type ParentPtr = BlockParent; fn get_parent(&self) -> &Option { &self.parent } diff --git a/crates/kirin-ir/src/stage/info.rs b/crates/kirin-ir/src/stage/info.rs index aadf8112cd..3158aff588 100644 --- a/crates/kirin-ir/src/stage/info.rs +++ b/crates/kirin-ir/src/stage/info.rs @@ -1,7 +1,10 @@ -use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashSet, + ops::{Deref, DerefMut}, +}; -use crate::arena::Arena; -use crate::node::ssa::SSAInfo; +use crate::arena::{Arena, Id}; +use crate::node::ssa::{SSAInfo, Use}; use crate::{BuilderStageInfo, Dialect, node::*}; use super::arenas::Arenas; @@ -117,6 +120,115 @@ impl StageInfo { &self.ssas } + /// Rebuild the def-use index ([`SSAInfo::uses`](crate::SSAInfo)) from the + /// authoritative storage. + /// + /// Clears every live value's use list, then records one [`Use`](crate::Use) + /// per position that reads a value: + /// + /// - each live statement's operands (in [`HasArguments`](crate::HasArguments) + /// order) → [`Use::StatementOperand`](crate::Use), and + /// - each live `DiGraph` body's yields (in yield order) → + /// [`Use::DiGraphYield`](crate::Use). A yield is a boundary export with no + /// backing statement, so it would otherwise be invisible to the operand + /// scan. + /// + /// The operand and yield slots are the ground truth; this list is a derived + /// reverse index over them. `UnGraph` contributes nothing: its edges are + /// statements whose operands are already covered above; graph ports and + /// block arguments are definitions, not uses. + /// + /// Idempotent — safe to re-run. Called at + /// [`finalize`](crate::BuilderStageInfo::finalize) so finalized IR ships a + /// populated index; a mutation layer must call it (or maintain the index + /// incrementally) after changing operands or yields. + pub fn rebuild_use_index(&mut self) { + let StageInfo { nodes, ssas } = self; + + for item in ssas.iter_mut() { + if let Some(info) = (**item).as_mut() { + info.uses_mut().clear(); + } + } + + for (raw, item) in nodes.statements.items.iter().enumerate() { + if item.deleted() { + continue; + } + let stmt = Statement(Id(raw)); + let operands: Vec = item.data.definition.arguments().copied().collect(); + for (index, operand) in operands.into_iter().enumerate() { + if let Some(slot) = ssas.get_mut(operand) + && let Some(info) = (**slot).as_mut() + { + info.uses_mut().push(Use::StatementOperand { stmt, index }); + } + } + } + + for (raw, item) in nodes.digraphs.items.iter().enumerate() { + if item.deleted() { + continue; + } + let graph = DiGraph::from(Id(raw)); + let yields: Vec = item.data.yields().to_vec(); + for (index, yielded) in yields.into_iter().enumerate() { + if let Some(slot) = ssas.get_mut(yielded) + && let Some(info) = (**slot).as_mut() + { + info.uses_mut().push(Use::DiGraphYield { graph, index }); + } + } + } + } + + /// Rebuild the reverse control-flow index stored in + /// [`BlockInfo::predecessors`](crate::BlockInfo::predecessors). + /// + /// Successor references on statements are the authoritative forward + /// edges. This method clears every live block's cached predecessors, then + /// scans each live statement whose structural parent is a block. For every + /// successor target, the source block is recorded as a predecessor of that + /// target. + /// + /// Multiple successor edges from one source block to the same target still + /// represent one predecessor block, so duplicate `(source, target)` pairs + /// are recorded only once. Blocks directly owned by statements do not need + /// synthetic predecessor entries: backward traversal reaches their owner + /// through [`BlockParent::Statement`](crate::BlockParent::Statement). + /// + /// Idempotent — safe to re-run after successor edges change. Called during + /// finalization so finalized IR ships with a populated reverse index. + pub fn rebuild_predecessor_index(&mut self) { + let StageInfo { nodes, .. } = self; + let (blocks, statements) = (&mut nodes.blocks, &nodes.statements); + + for block in blocks.iter_mut() { + block.predecessors.clear(); + } + + let mut seen = HashSet::new(); + for statement in statements.iter() { + let Some(StatementParent::Block(source)) = statement.parent else { + continue; + }; + + for successor in statement.definition.successors() { + let target = successor.target(); + if !seen.insert((source, target)) { + continue; + } + + let Some(target_info) = blocks.get_mut(target) else { + continue; + }; + if !target_info.deleted() { + target_info.predecessors.push(source); + } + } + } + } + /// Temporarily convert to a [`BuilderStageInfo`] for construction, then /// convert back. /// diff --git a/crates/kirin-ir/tests/builder_block.rs b/crates/kirin-ir/tests/builder_block.rs index 8ae314b163..f1c06bb991 100644 --- a/crates/kirin-ir/tests/builder_block.rs +++ b/crates/kirin-ir/tests/builder_block.rs @@ -88,6 +88,58 @@ fn block_builder_substitutes_builder_block_arguments() { assert!(matches!(ssa1.kind(), SSAKind::BlockArgument(_, 1))); } +#[test] +fn finalize_populates_def_use_index() { + let mut stage = new_stage(); + + let arg0 = stage.block_argument().index(0); + let arg1 = stage.block_argument().index(1); + + // arg0 is read twice (add operand 0, use operand 0); arg1 once (add operand 1). + let add_stmt = stage + .statement() + .definition(BuilderDialect::Add(arg0, arg1)) + .new(); + let use_stmt = stage + .statement() + .definition(BuilderDialect::Use(arg0)) + .new(); + + let block = stage + .block() + .argument(TestType::I32) + .argument(TestType::I64) + .stmt(add_stmt) + .stmt(use_stmt) + .new(); + + let stage = stage.finalize().unwrap(); + let block_info = block.expect_info(&stage); + let real_arg0: SSAValue = block_info.arguments[0].into(); + let real_arg1: SSAValue = block_info.arguments[1].into(); + + let uses0 = real_arg0.get_info(&stage).unwrap().uses(); + assert_eq!(uses0.len(), 2, "arg0 is read by two statements"); + assert!(uses0.contains(&Use::StatementOperand { + stmt: add_stmt, + index: 0 + })); + assert!(uses0.contains(&Use::StatementOperand { + stmt: use_stmt, + index: 0 + })); + + let uses1 = real_arg1.get_info(&stage).unwrap().uses(); + assert_eq!(uses1.len(), 1, "arg1 is read only by the add"); + assert_eq!( + uses1[0], + Use::StatementOperand { + stmt: add_stmt, + index: 1 + } + ); +} + #[test] #[should_panic(expected = "is not a terminator")] fn block_builder_terminator_rejects_non_terminator() { @@ -197,6 +249,7 @@ fn empty_block_iteration() { assert_eq!(block.first_statement(&stage), None); assert_eq!(block.last_statement(&stage), None); assert_eq!(block.terminator(&stage), None); + assert!(block.expect_info(&stage).predecessors.is_empty()); } #[test] @@ -243,15 +296,120 @@ fn cfg_builder_creates_cfg_with_ordered_blocks() { assert_eq!(blocks, vec![b0, b1, b2]); let b0_info = b0.expect_info(&stage); + assert_eq!(b0_info.parent, Some(BlockParent::CFG(cfg))); assert_eq!(b0_info.node.next, Some(b1)); let b1_info = b1.expect_info(&stage); + assert_eq!(b1_info.parent, Some(BlockParent::CFG(cfg))); assert_eq!(b1_info.node.prev, Some(b0)); assert_eq!(b1_info.node.next, Some(b2)); let b2_info = b2.expect_info(&stage); + assert_eq!(b2_info.parent, Some(BlockParent::CFG(cfg))); assert_eq!(b2_info.node.prev, Some(b1)); assert_eq!(b2_info.node.next, None); } +#[test] +fn finalize_populates_block_predecessor_index() { + let mut stage = new_stage(); + let target = stage.block().new(); + + let branch0 = stage + .statement() + .definition(BuilderDialect::Branch(Successor::from_block(target))) + .new(); + let branch1 = stage + .statement() + .definition(BuilderDialect::Branch(Successor::from_block(target))) + .new(); + let source0 = stage.block().terminator(branch0).new(); + let source1 = stage.block().terminator(branch1).new(); + let _cfg = stage + .cfg() + .add_block(source0) + .add_block(source1) + .add_block(target) + .new(); + + let stage = stage.finalize().unwrap(); + assert_eq!( + target.expect_info(&stage).predecessors.as_slice(), + [source0, source1] + ); + assert!(source0.expect_info(&stage).predecessors.is_empty()); + assert!(source1.expect_info(&stage).predecessors.is_empty()); +} + +#[test] +fn predecessor_index_deduplicates_edges_from_the_same_block() { + let mut stage = new_stage(); + let target = stage.block().new(); + let successor = Successor::from_block(target); + let branch = stage + .statement() + .definition(BuilderDialect::CondBranch(successor, successor)) + .new(); + let source = stage.block().terminator(branch).new(); + let _cfg = stage.cfg().add_block(source).add_block(target).new(); + + let stage = stage.finalize().unwrap(); + assert_eq!(target.expect_info(&stage).predecessors.as_slice(), [source]); +} + +#[test] +fn statement_builder_assigns_parent_to_directly_owned_blocks() { + let mut stage = new_stage(); + let then_block = stage.block().new(); + let else_block = stage.block().new(); + + let owner = stage + .statement() + .definition(BuilderDialect::OwnBlocks(then_block, else_block)) + .new(); + + let stage = stage.finalize().unwrap(); + assert_eq!( + then_block.expect_info(&stage).parent, + Some(BlockParent::Statement(owner)) + ); + assert_eq!( + else_block.expect_info(&stage).parent, + Some(BlockParent::Statement(owner)) + ); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_records_cfg_parent() { + let mut stage = new_stage(); + let cfg = stage.cfg().new(); + + let _owner = stage + .statement() + .definition(BuilderDialect::OwnCFG(cfg)) + .new(); + + // A second owner is rejected only if the first statement recorded itself + // in the crate-private `CFGInfo.parent` field. + let _other_owner = stage + .statement() + .definition(BuilderDialect::OwnCFG(cfg)) + .new(); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_rejects_block_owned_by_cfg() { + let mut stage = new_stage(); + let cfg_block = stage.block().new(); + let other_block = stage.block().new(); + let _cfg = stage.cfg().add_block(cfg_block).new(); + + let _owner = stage + .statement() + .definition(BuilderDialect::OwnBlocks(cfg_block, other_block)) + .new(); +} + #[test] fn has_cfg_body_entry_block_returns_first_block() { let mut stage = new_stage(); diff --git a/crates/kirin-ir/tests/builder_graph.rs b/crates/kirin-ir/tests/builder_graph.rs index feba59349b..9b5be82a41 100644 --- a/crates/kirin-ir/tests/builder_graph.rs +++ b/crates/kirin-ir/tests/builder_graph.rs @@ -34,6 +34,39 @@ fn digraph_builder_two_node_dag() { assert_eq!(*s1.parent(&stage), Some(StatementParent::DiGraph(dg))); } +#[test] +fn finalize_indexes_digraph_yields_as_uses() { + let mut stage = new_stage(); + + // s0 produces %r; the graph yields %r. No statement reads %r, so its only + // use is the boundary yield — invisible to an operand-only scan. + let s0 = stage.statement().definition(BuilderDialect::Nop).new(); + let result_ssa = stage + .ssa() + .ty(TestType::I32) + .kind(BuilderSSAKind::Result(s0, 0)) + .new(); + + let dg = stage + .digraph() + .node(s0) + .yield_value(result_ssa) + .name("yielder") + .new(); + + let stage = stage.finalize().unwrap(); + + let uses = result_ssa.get_info(&stage).unwrap().uses(); + assert_eq!(uses.len(), 1, "%r is used only by the graph yield"); + assert_eq!( + uses[0], + Use::DiGraphYield { + graph: dg, + index: 0 + } + ); +} + #[test] fn digraph_builder_port_and_capture_creation() { let mut stage = new_stage(); @@ -72,6 +105,50 @@ fn digraph_builder_port_and_capture_creation() { assert!(ssa_cap.name().is_some()); } +#[test] +fn statement_builder_assigns_parent_to_directly_owned_graphs() { + let mut stage = new_stage(); + let digraph = stage.digraph().new(); + let ungraph = stage.ungraph().new(); + + let owner = stage + .statement() + .definition(BuilderDialect::OwnGraphs(digraph, ungraph)) + .new(); + + let stage = stage.finalize().unwrap(); + assert_eq!(digraph.expect_info(&stage).parent(), Some(owner)); + assert_eq!(ungraph.expect_info(&stage).parent(), Some(owner)); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_rejects_digraph_with_existing_owner() { + let mut stage = new_stage(); + let existing_owner = stage.statement().definition(BuilderDialect::Nop).new(); + let digraph = stage.digraph().parent(existing_owner).new(); + let ungraph = stage.ungraph().new(); + + let _other_owner = stage + .statement() + .definition(BuilderDialect::OwnGraphs(digraph, ungraph)) + .new(); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_rejects_ungraph_with_existing_owner() { + let mut stage = new_stage(); + let existing_owner = stage.statement().definition(BuilderDialect::Nop).new(); + let digraph = stage.digraph().new(); + let ungraph = stage.ungraph().parent(existing_owner).new(); + + let _other_owner = stage + .statement() + .definition(BuilderDialect::OwnGraphs(digraph, ungraph)) + .new(); +} + #[test] fn digraph_builder_resolves_builder_port_placeholders() { let mut stage = new_stage(); diff --git a/crates/kirin-ir/tests/common.rs b/crates/kirin-ir/tests/common.rs index 8e90c759b2..5cecd270b9 100644 --- a/crates/kirin-ir/tests/common.rs +++ b/crates/kirin-ir/tests/common.rs @@ -42,6 +42,11 @@ impl Placeholder for TestType { /// - `Gate(a, b)`: two SSAValue operands (ungraph node) /// - `Wire(r)`: edge that produces a ResultValue (ungraph edge) /// - `Isolated`: no operands, no results (ungraph isolated node) +/// - `Branch(target)`: one-successor control-flow terminator +/// - `CondBranch(a, b)`: two-successor control-flow terminator +/// - `OwnBlocks(a, b)`: structurally owns two blocks +/// - `OwnCFG(cfg)`: structurally owns one CFG +/// - `OwnGraphs(dg, ug)`: structurally owns one directed and one undirected graph #[allow(dead_code)] #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum BuilderDialect { @@ -52,6 +57,11 @@ pub enum BuilderDialect { Gate(SSAValue, SSAValue), Wire(ResultValue), Isolated, + Branch(Successor), + CondBranch(Successor, Successor), + OwnBlocks(Block, Block), + OwnCFG(CFG), + OwnGraphs(DiGraph, UnGraph), } impl<'a> HasArguments<'a> for BuilderDialect { @@ -97,50 +107,73 @@ impl<'a> HasResultsMut<'a> for BuilderDialect { } impl<'a> HasBlocks<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a Block>; + type Iter = std::vec::IntoIter<&'a Block>; fn blocks(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnBlocks(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasBlocksMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut Block>; + type IterMut = std::vec::IntoIter<&'a mut Block>; fn blocks_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnBlocks(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasSuccessors<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a Successor>; + type Iter = std::vec::IntoIter<&'a Successor>; fn successors(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::Branch(target) => vec![target].into_iter(), + BuilderDialect::CondBranch(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasSuccessorsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut Successor>; + type IterMut = std::vec::IntoIter<&'a mut Successor>; fn successors_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::Branch(target) => vec![target].into_iter(), + BuilderDialect::CondBranch(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasCFG<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a CFG>; + type Iter = std::vec::IntoIter<&'a CFG>; fn cfgs(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnCFG(cfg) => vec![cfg].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasCFGMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut CFG>; + type IterMut = std::vec::IntoIter<&'a mut CFG>; fn cfgs_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnCFG(cfg) => vec![cfg].into_iter(), + _ => vec![].into_iter(), + } } } impl IsTerminator for BuilderDialect { fn is_terminator(&self) -> bool { - matches!(self, BuilderDialect::Return) + matches!( + self, + BuilderDialect::Return | BuilderDialect::Branch(_) | BuilderDialect::CondBranch(_, _) + ) } } @@ -163,30 +196,42 @@ impl IsSpeculatable for BuilderDialect { } impl<'a> HasDigraphs<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a DiGraph>; + type Iter = std::vec::IntoIter<&'a DiGraph>; fn digraphs(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(graph, _) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasDigraphsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut DiGraph>; + type IterMut = std::vec::IntoIter<&'a mut DiGraph>; fn digraphs_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(graph, _) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasUngraphs<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a UnGraph>; + type Iter = std::vec::IntoIter<&'a UnGraph>; fn ungraphs(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(_, graph) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasUngraphsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut UnGraph>; + type IterMut = std::vec::IntoIter<&'a mut UnGraph>; fn ungraphs_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(_, graph) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 40fc581875..fd76c16e69 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -26,10 +26,11 @@ pub use live::{Live, LiveSet}; pub use result::{DemandResult, DenseLivenessResult}; use kirin_interpreter::{ - DenseBackwardInterpreter, DenseBackwardTransfer, InterpDispatch, InterpreterError, + Body, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, + DenseBackwardTransfer, DenseFrameBuild, Frame, InterpDispatch, InterpreterError, SparseBackwardDriver, SparseBackwardInterpreter, StageQuery, StandardDenseBackwardFrame, }; -use kirin_ir::{CFG, CompileStage, Pipeline, StageMeta}; +use kirin_ir::{CompileStage, Pipeline, StageMeta}; /// The sparse backward demand engine instantiated at the [`Live`] lattice: /// strong liveness. @@ -41,30 +42,34 @@ pub type Demand<'ir, S, E = InterpreterError> = SparseBackwardInterpreter<'ir, S pub type DenseLiveness<'ir, S, E = InterpreterError, F = StandardDenseBackwardFrame> = DenseBackwardInterpreter<'ir, S, LiveSet, E, F>; -/// Run strong liveness (sparse backward demand) over `cfg` in `stage`. +/// Run strong liveness (sparse backward demand) over `body` in `stage`. TODO: +/// analyze() should accept Callee similar to concrete and constprop's +/// analyze(CompileStage, Callee, args) instead of Body, so that the caller can +/// select a specialization and pass its args. pub fn analyze_demand<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, - cfg: CFG, + body: impl Into, ) -> Result where S: StageMeta + StageQuery + InterpDispatch>, { + let body = body.into(); let mut engine = Demand::::new(pipeline); - engine.analyze(stage, cfg)?; - Ok(DemandResult::from_engine(&engine, stage, cfg)) + engine.analyze(stage, body)?; + Ok(DemandResult::from_engine(&engine, stage, body)) } -/// Run classic per-point liveness (dense backward) over `cfg` in `stage`, -/// with the standard (structured-control-free) frames. Languages with scf -/// compose [`DenseLiveness`] with their own frame type and build the result -/// via [`DenseLivenessResult::from_engine`]. +/// Run classic per-point liveness (dense backward) over `body` in `stage`, +/// with the standard (structured-control-free) frames. Languages with +/// structured dialects select their total frame through +/// [`analyze_dense_with_frame`] instead. pub fn analyze_dense<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, - cfg: CFG, + body: impl Into, ) -> Result where S: StageMeta @@ -79,7 +84,33 @@ where >, >, { - let mut engine = DenseLiveness::::new(pipeline); - engine.analyze(stage, cfg)?; - DenseLivenessResult::from_engine(&mut engine, stage, cfg) + analyze_dense_with_frame::>( + pipeline, stage, body, + ) +} + +/// Run classic per-point liveness (dense backward) over `body` in `stage` +/// with a caller-selected total frame type `F` — the entry point for +/// languages whose structured dialects require a language-specific total +/// frame. The analysis consumes the finalized IR directly; it neither +/// requires nor computes a demand ([`DemandResult`]) pre-pass. +pub fn analyze_dense_with_frame<'ir, S, F>( + pipeline: &'ir Pipeline, + stage: CompileStage, + body: impl Into, +) -> Result +where + S: StageMeta + + StageQuery + + InterpDispatch>, + F: Frame< + DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, + F, + Completion = DenseBackwardCompletion, + > + DenseFrameBuild, +{ + let body = body.into(); + let mut engine = DenseLiveness::::new(pipeline); + engine.analyze(stage, body)?; + Ok(DenseLivenessResult::from_engine(&engine)) } diff --git a/crates/kirin-liveness/src/live.rs b/crates/kirin-liveness/src/live.rs index 2e062427bb..5d29b8a37b 100644 --- a/crates/kirin-liveness/src/live.rs +++ b/crates/kirin-liveness/src/live.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; -use kirin_interpreter::PointFacts; +use kirin_interpreter::{DenseBackwardState, PointFacts}; use kirin_ir::{HasBottom, HasTop, Lattice, SSAValue}; /// The two-point liveness lattice: `Dead` (bottom) ⊑ `Live` (top). @@ -140,7 +140,28 @@ impl HasBottom for LiveSet { } } -/// The dense backward point-state contract: gen/kill mutate the set. +/// How a live set moves between vocabularies. Renaming keeps only the values +/// the rename covers, so a caller that wants pass-through joins +/// [`forget`](DenseBackwardState::forget) itself. +impl DenseBackwardState for LiveSet { + fn rename(&self, params: &[SSAValue], args: &[SSAValue]) -> Self { + let mut out = LiveSet::new(); + for (index, param) in params.iter().enumerate() { + if self.contains(*param) + && let Some(arg) = args.get(index) + { + out.insert(*arg); + } + } + out + } + + fn forget(&self, values: &[SSAValue]) -> Self { + self.iter().filter(|v| !values.contains(v)).collect() + } +} + +/// The classic-liveness point-state contract: gen/kill mutate the set. impl PointFacts for LiveSet { fn insert(&mut self, value: SSAValue) -> bool { LiveSet::insert(self, value) diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs index acc8b59d66..6d7dbc42d8 100644 --- a/crates/kirin-liveness/src/result.rs +++ b/crates/kirin-liveness/src/result.rs @@ -2,11 +2,10 @@ //! per-point sets (classic liveness), plus their composition. use kirin_interpreter::{ - DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, DenseBackwardTransfer, - DenseBlockStore, DenseFrameBuild, DensePointStore, Frame, InterpDispatch, InterpreterError, - ProgramPoint, SparseBackwardInterpreter, StageQuery, + Body, BodyScope, DenseBackwardInterpreter, FactStore, InterpreterError, ProgramPoint, Scoped, + SparseBackwardInterpreter, }; -use kirin_ir::{Block, CFG, CompileStage, Lattice, SSAValue, StageMeta, Statement}; +use kirin_ir::{CompileStage, Lattice, SSAValue, StageMeta}; use crate::live::{Live, LiveSet}; @@ -21,10 +20,10 @@ impl DemandResult { pub(crate) fn from_engine( engine: &SparseBackwardInterpreter<'_, S, Live, InterpreterError>, stage: CompileStage, - cfg: CFG, + body: impl Into, ) -> Self { // The engine's sparse fact view; the demand set is its live support. - let facts = engine.fact_store(stage, cfg); + let facts = engine.fact_store(stage, body); let demanded = facts .iter() .filter(|(_, fact)| fact.is_live()) @@ -44,96 +43,44 @@ impl DemandResult { } } -/// The result of [`analyze_dense`](crate::analyze_dense): classic per-point -/// liveness — block-boundary sets plus reconstructed per-statement sets. +/// The result of [`analyze_dense`](crate::analyze_dense): classic liveness at +/// every block and statement program point. /// /// These sets carry the conventional (regalloc-grade) meaning: every use gens, -/// purity-irrelevant. Strong per-point sets are the composition -/// [`strong_live_before`](Self::strong_live_before) — the classic set +/// purity-irrelevant. Strong per-point sets are +/// [`strong_point_facts`](Self::strong_point_facts): the classic set /// intersected with the demand set. #[derive(Clone, Debug)] pub struct DenseLivenessResult { - blocks: DenseBlockStore, - points: DensePointStore, + facts: FactStore, LiveSet>, } impl DenseLivenessResult { - /// Build the result from a converged dense engine: copy the boundary - /// summaries and reconstruct every per-statement state by replaying each - /// block through the dialect rules. + /// Copy the facts recorded by a converged dense engine. pub fn from_engine<'ir, S, F>( - engine: &mut DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F>, - stage: CompileStage, - cfg: CFG, - ) -> Result + engine: &DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F>, + ) -> Self where - S: StageMeta - + StageQuery - + InterpDispatch>, - F: Frame< - DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, - Completion = DenseBackwardCompletion, - > + DenseFrameBuild, + S: StageMeta, { - let mut blocks = DenseBlockStore::new(); - for block in engine.cfg_blocks() { - if let Some(summary) = engine.block_summary(stage, cfg, block) { - blocks.set_entry(block, summary.live_in.clone()); - blocks.set_exit(block, summary.live_out.clone()); - } + Self { + facts: engine.facts(), } - let points = engine.reconstruct_points(stage, cfg)?; - Ok(Self { blocks, points }) - } - - /// Iterate `(block, live_in, live_out)` triples (order unspecified). - pub fn blocks(&self) -> impl Iterator { - self.blocks.entries().filter_map(|(block, live_in)| { - self.blocks - .exit(block) - .map(|live_out| (block, live_in, live_out)) - }) - } - - /// The set of values live on entry to `block`. - pub fn live_in(&self, block: Block) -> Option<&LiveSet> { - self.blocks.entry(block) - } - - /// The set of values live on exit from `block` (excludes the terminator's - /// own uses, e.g. the branch condition). - pub fn live_out(&self, block: Block) -> Option<&LiveSet> { - self.blocks.exit(block) } - /// The set of values live immediately before `statement`. - pub fn live_before(&self, statement: Statement) -> Option<&LiveSet> { - self.points.get(ProgramPoint::Before(statement)) - } - - /// The set of values live immediately after `statement`. - pub fn live_after(&self, statement: Statement) -> Option<&LiveSet> { - self.points.get(ProgramPoint::After(statement)) - } - - /// Strong per-point set: the classic set intersected with the demand set - /// (values live here *and* transitively needed by a root). - pub fn strong_live_before( - &self, - statement: Statement, - demand: &DemandResult, - ) -> Option { - self.live_before(statement) - .map(|set| set.meet(demand.demanded())) + /// The liveness fact recorded at `point`. + pub fn point_facts(&self, point: Scoped) -> Option<&LiveSet> { + self.facts.get(point) } - /// See [`strong_live_before`](Self::strong_live_before). - pub fn strong_live_after( + /// Strong fact at `point`: the classic set intersected with the demand set + /// (values live there *and* transitively needed by a root). + pub fn strong_point_facts( &self, - statement: Statement, + point: Scoped, demand: &DemandResult, ) -> Option { - self.live_after(statement) + self.point_facts(point) .map(|set| set.meet(demand.demanded())) } } diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 879c5cbeec..50e134f7ae 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -4,7 +4,8 @@ use kirin::prelude::{GetInfo, ParsePipelineText, Pipeline, SSAValue, StageInfo}; use kirin_arith::Arith; -use kirin_liveness::analyze_demand; +use kirin_interpreter::{Body, InterpreterError, ProgramPoint, Scoped}; +use kirin_liveness::{DenseLiveness, analyze_demand}; use kirin_test_languages::ArithFunctionLanguage; const PROGRAM: &str = r#" @@ -48,6 +49,8 @@ fn parse(program: &str) -> Pipeline> { } /// The finalized stage id and the body cfg of `@main`. +// TODO: `analyze` method is wrong, calling demand analysis. +// TODO: `analyze` should use the same entry point. i.e. Callee not CFG/Body. fn main_cfg( pipeline: &Pipeline>, ) -> (kirin::prelude::CompileStage, kirin_ir::CFG) { @@ -312,17 +315,84 @@ fn classic_liveness_boundary_sets() { let entry = nth_block(&pipeline, cfg, 0); let then_block = nth_block(&pipeline, cfg, 1); let else_block = nth_block(&pipeline, cfg, 2); + let scope = (stage, Body::CFG(cfg)); + let point = |item| Scoped::new(scope, item); // live_in(entry): %x (used by add and both edges) and %cond (branch use). - assert_eq!(result.live_in(entry), Some(&live_set(&[x, cond]))); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockEntry(entry))), + Some(&live_set(&[x, cond])) + ); + assert_eq!( + result.point_facts(Scoped::new( + (stage, Body::Block(entry)), + ProgramPoint::BlockEntry(entry), + )), + None, + "the same point under another body scope is a different fact" + ); // live_out(entry): both successors' live-ins mapped across the edges — // {%a} → {%x}, {%b} → {%x}; the branch condition is a terminator *use*, // not part of the boundary set. - assert_eq!(result.live_out(entry), Some(&live_set(&[x]))); - assert_eq!(result.live_in(then_block), Some(&live_set(&[then_param]))); - assert_eq!(result.live_out(then_block), Some(&live_set(&[]))); - assert_eq!(result.live_in(else_block), Some(&live_set(&[else_param]))); - assert_eq!(result.live_out(else_block), Some(&live_set(&[]))); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockExit(entry))), + Some(&live_set(&[x])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockEntry(then_block))), + Some(&live_set(&[then_param])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockExit(then_block))), + Some(&live_set(&[])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockEntry(else_block))), + Some(&live_set(&[else_param])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockExit(else_block))), + Some(&live_set(&[])) + ); +} + +#[test] +fn reusing_dense_engine_replaces_the_previous_scoped_result() { + let pipeline = parse(PROGRAM); + let (stage, cfg) = main_cfg(&pipeline); + let entry = nth_block(&pipeline, cfg, 0); + let then_block = nth_block(&pipeline, cfg, 1); + let mut engine = DenseLiveness::<_, InterpreterError>::new(&pipeline); + + engine.analyze(stage, cfg).expect("CFG analysis succeeds"); + assert!( + engine + .point_facts(Scoped::new( + (stage, Body::CFG(cfg)), + ProgramPoint::BlockEntry(entry), + )) + .is_some() + ); + + engine + .analyze(stage, then_block) + .expect("block analysis succeeds"); + assert_eq!( + engine.point_facts(Scoped::new( + (stage, Body::CFG(cfg)), + ProgramPoint::BlockEntry(entry), + )), + None, + "facts from the previous analysis are not retained" + ); + assert!( + engine + .point_facts(Scoped::new( + (stage, Body::Block(then_block)), + ProgramPoint::BlockEntry(then_block), + )) + .is_some() + ); } #[test] @@ -336,12 +406,19 @@ fn classic_per_point_sets_gen_dead_uses() { let add = find_stmt(&pipeline, cfg, |definition| { matches!(definition, ArithFunctionLanguage::Arith(Arith::Add { .. })) }); + let scope = (stage, Body::CFG(cfg)); // Classic semantics: the dead add still GENS its operands, so %b is live // before it — this is the conventional per-point meaning (the old strong // expectations were demand projections, not dense liveness). - assert_eq!(result.live_before(add), Some(&live_set(&[a, b]))); - assert_eq!(result.live_after(add), Some(&live_set(&[a]))); + assert_eq!( + result.point_facts(Scoped::new(scope, ProgramPoint::Before(add))), + Some(&live_set(&[a, b])) + ); + assert_eq!( + result.point_facts(Scoped::new(scope, ProgramPoint::After(add))), + Some(&live_set(&[a])) + ); } #[test] @@ -360,7 +437,10 @@ fn strong_per_point_sets_are_classic_intersect_demanded() { // The composition recovers the strong (needed) per-point view: %b is // classically live before the dead add but not demanded, so it drops out. let strong = dense - .strong_live_before(add, &demand) + .strong_point_facts( + Scoped::new((stage, Body::CFG(cfg)), ProgramPoint::Before(add)), + &demand, + ) .expect("point reconstructed"); assert_eq!(strong, live_set(&[a])); assert!(!strong.contains(b)); diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index d346a291ca..d245face6a 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -10,14 +10,18 @@ //! both arms and joins their results (abstract). //! - `scf.for` -> [`ScfForFrame`] / [`AbstractScfForFrame`], via [`ScfForDispatch`]. //! -//! Both reuse the framework's generic [`BodyFrame`]/[`AbstractBlockFrame`] to +//! Both reuse the framework's generic [`BlockFrame`]/[`AbstractBlockFrame`] to //! *walk* a chosen body block — those are reusable building blocks, not //! framework-owned structured semantics — but the structured *decision* and -//! result binding are owned by the SCF frame. A language that uses `scf` +//! result binding are owned by the SCF frame: the block walker surfaces a +//! structured `Yield` as [`Completion::Yielded`], which the SCF frame consumes, +//! while a function `Return` ([`Completion::Returned`]) is relayed unchanged so +//! it bubbles to the nearest `CallFrame`. A language that uses `scf` //! composes a total frame type embedding these via [`BuildScfIf`]/[`BuildScfFor`] //! (and the abstract equivalents [`BuildAbstractScfIf`]/[`BuildAbstractScfFor`]). use std::collections::VecDeque; +use std::hash::Hash; use std::marker::PhantomData; use kirin::prelude::Lattice; @@ -28,10 +32,11 @@ use kirin_interpreter::dialect::{ StrongDemand, }; use kirin_interpreter::{ - AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BodyFrame, - CallContext, Completion, ConcreteInterpreter, DenseBackwardCompletion, - DenseBackwardFrameDriver, DenseBlockFrame, DenseFrameBuild, EnvIndex, FrameBuild, FrameDriver, - FrameEffect, PointFacts, SparseForwardTransfer, + AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, CallContext, + Completion, ConcreteInterpreter, DenseBackwardCompletion, DenseBackwardFrameEngine, + DenseBackwardState, DenseBlockFrame, DenseFrameBuild, Env, EnvIndex, + ForwardDataflowFrameEngine, Frame, FrameBuild, FrameEffect, FrameEngine, SparseForwardTransfer, + TerminatorArgs, }; use crate::{For, ForLoopValue, If, Yield}; @@ -107,8 +112,8 @@ where // // Demand converges value-by-value on the sparse backward engine's worklist, // so structured bodies need no walk and loops need no frame fixpoint: this -// rule re-runs whenever a result or a body block parameter it feeds rises -// (the owning statement is the body's *feeder* in the cfg topology). +// rule re-runs whenever a result or a body block parameter it owns rises (the +// owning statement is recorded as the body's structural parent). /// Backward demand for `scf.if`: the condition is an unconditional control /// root (consistent with `cf.cond_br`); a body's yield slot is demanded iff @@ -296,17 +301,21 @@ impl DenseScfIfFrame { _marker: PhantomData, } } +} + +impl Frame for DenseScfIfFrame +where + I: DenseBackwardFrameEngine, + F: DenseFrameBuild + BuildDenseScfIf, + V: Clone + Lattice, + E: From, +{ + type Completion = DenseBackwardCompletion; - pub fn step_into( + fn step_into( mut self, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfIf, - V: Clone + Lattice, - E: From, - { + ) -> Result>, E> { if self.after.is_none() { self.after = Some(interp.state()); } @@ -331,17 +340,11 @@ impl DenseScfIfFrame { } } - pub fn resume_into( + fn resume_into( mut self, completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfIf, - V: Clone + Lattice, - E: From, - { + ) -> Result>, E> { match completion { DenseBackwardCompletion::Structured => { let arm_entry = interp.state(); @@ -357,10 +360,10 @@ impl DenseScfIfFrame { } } - pub fn resume_done_into(self) -> Result>, E> - where - E: From, - { + fn resume_done_into( + self, + _interp: &mut I, + ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.if dense frames resume only with completions", ))) @@ -378,7 +381,7 @@ pub struct DenseScfForFrame { /// Captured on the first step. seed: Option, params: Vec, - yields: Vec, + yields: TerminatorArgs, /// The current body-exit state estimate. entry: Option, _marker: PhantomData E>, @@ -391,37 +394,40 @@ impl DenseScfForFrame { body, seed: None, params: Vec::new(), - yields: Vec::new(), + yields: TerminatorArgs::new(), entry: None, _marker: PhantomData, } } + /// The loop-carried estimate: the state after the loop, plus the body's + /// carried parameters renamed across the back-edge to the slots that yield + /// them. Unlike a CFG edge this does *not* pass anything through — the + /// body's own vocabulary does not escape backwards through the back-edge. + /// + /// `params[0]` is the induction variable, which no yield slot feeds. fn carry(&self, body_entry: &V) -> V where - V: Clone + Lattice + PointFacts, + V: Clone + Lattice + DenseBackwardState, { - let mut next = self.seed.clone().expect("seed captured"); - for (index, param) in self.params.iter().skip(1).enumerate() { - if body_entry.contains(*param) - && let Some(slot) = self.yields.get(index) - { - next.insert(*slot); - } - } - next + let seed = self.seed.clone().expect("seed captured"); + seed.join(&body_entry.rename(&self.params[1..], &self.yields)) } +} + +impl Frame for DenseScfForFrame +where + I: DenseBackwardFrameEngine, + F: DenseFrameBuild + BuildDenseScfFor, + V: Clone + PartialEq + Lattice + DenseBackwardState, + E: From, +{ + type Completion = DenseBackwardCompletion; - pub fn step_into( + fn step_into( mut self, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfFor, - V: Clone + PartialEq + Lattice + PointFacts, - E: From, - { + ) -> Result>, E> { if self.seed.is_none() { self.seed = Some(interp.state()); self.params = interp.block_params(self.stage, self.body)?; @@ -437,17 +443,11 @@ impl DenseScfForFrame { }) } - pub fn resume_into( + fn resume_into( mut self, completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfFor, - V: Clone + PartialEq + Lattice + PointFacts, - E: From, - { + ) -> Result>, E> { match completion { DenseBackwardCompletion::Structured => { let body_entry = interp.state(); @@ -460,10 +460,7 @@ impl DenseScfForFrame { } else { // Stable: the final body entry, minus the body-local // parameters, is the state before the loop. - let mut before = body_entry; - for param in &self.params { - before.remove(*param); - } + let before = body_entry.forget(&self.params); interp.replace_state(before); Ok(FrameEffect::Complete(DenseBackwardCompletion::Structured)) } @@ -474,10 +471,10 @@ impl DenseScfForFrame { } } - pub fn resume_done_into(self) -> Result>, E> - where - E: From, - { + fn resume_done_into( + self, + _interp: &mut I, + ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.for dense frames resume only with completions", ))) @@ -642,10 +639,12 @@ where // Concrete if frame: pick the decided arm, relay its completion. // =========================================================================== -/// Concrete `scf.if` traversal: push the framework [`BodyFrame`] for the decided -/// arm and relay its completion to the pusher. The structured *decision* (which -/// arm) is owned here; an undecided condition is impossible under concrete -/// execution (`IndeterminateBranch`). +/// Concrete `scf.if` traversal: push the framework [`BlockFrame`] for the +/// decided arm, consume the arm's structured `Yield`, and hand the yielded +/// values to the pusher. The structured *decision* (which arm) is owned here; +/// an undecided condition is impossible under concrete execution +/// (`IndeterminateBranch`). A function `Return` inside the arm is relayed +/// unchanged so it bubbles to the nearest `CallFrame`. pub struct ScfIfFrame { stage: CompileStage, env: EnvIndex, @@ -676,36 +675,56 @@ where _marker: PhantomData, } } +} - pub fn step_into(self, _interp: &mut I) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild + BuildScfIf, - { +/// `scf.if` decides its arm *before* the frame is built (the rule reads the +/// condition), so stepping it touches no engine capability at all — only the +/// error type. This is the narrowest bound in the codebase, and it is the point +/// of splitting the driver: a dialect frame that makes a decision and delegates +/// the walking should not have to name an engine that can allocate activations. +impl Frame for ScfIfFrame +where + I: FrameEngine, + F: FrameBuild + BuildScfIf, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into(self, _interp: &mut I) -> Result>, E> { let arm = match self.decided { Some(true) => self.then_body, Some(false) => self.else_body, None => return Err(E::from(InterpreterError::IndeterminateBranch)), }; - let body = BodyFrame::block(self.stage, self.env, arm, Product::new()); + let body = BlockFrame::new(self.stage, self.env, arm, Product::new()); Ok(FrameEffect::Push { parent: F::scf_if(self), - child: F::from_body(body), + child: F::from_block(body), }) } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.if frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( self, completion: Completion, + _interp: &mut I, ) -> Result>, E> { - // Relay the chosen arm's completion (yield-finish or function return). - Ok(FrameEffect::Complete(completion)) + match completion { + // The arm's structured yield: its values are this operation's + // results, delivered to the pusher as a finished sub-computation. + Completion::Yielded(values) => Ok(FrameEffect::Complete(Completion::Finished(values))), + // A `ret` inside the arm: relay it toward the nearest `CallFrame`. + Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + Completion::Finished(_) => Err(E::from(InterpreterError::Custom( + "scf.if arm completed without a structured yield", + ))), + } } } @@ -754,7 +773,7 @@ where fn join_acc(&mut self, interp: &mut I, values: Product) -> Result<(), E> where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, { let merged = match self.acc.take() { None => values, @@ -763,15 +782,19 @@ where self.acc = Some(merged); Ok(()) } +} - pub fn step_into( - mut self, - _interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfIf, - { +impl Frame for AbstractScfIfFrame +where + I: ForwardDataflowFrameEngine, + F: AbstractFrameBuild + BuildAbstractScfIf, + V: Clone + PartialEq + Lattice, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, _interp: &mut I) -> Result>, E> { match self.remaining.pop_front() { None => Ok(FrameEffect::Complete(AbstractCompletion::Finished( self.acc, @@ -786,21 +809,17 @@ where } } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.if frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfIf, - { + ) -> Result>, E> { match completion { AbstractCompletion::Finished(Some(values)) => { self.join_acc(interp, values)?; @@ -863,22 +882,30 @@ where _marker: PhantomData, } } +} - pub fn step_into(self, interp: &mut I) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild + BuildScfFor, - { +/// `scf.for` reads the loop bound/step out of the activation it was given and +/// otherwise only pushes a [`BlockFrame`] — so [`Env`] is its whole requirement. +impl Frame for ScfForFrame +where + I: Env, + F: FrameBuild + BuildScfFor, + V: Clone + ForLoopValue, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { let end = interp.env_read(self.env, self.end)?; match self.induction.loop_condition(&end) { Some(true) => { let args: Product = std::iter::once(self.induction.clone()) .chain(self.carried.iter().cloned()) .collect(); - let body = BodyFrame::block(self.stage, self.env, self.body, args); + let body = BlockFrame::new(self.stage, self.env, self.body, args); Ok(FrameEffect::Push { parent: F::scf_for(self), - child: F::from_body(body), + child: F::from_block(body), }) } Some(false) => Ok(FrameEffect::Complete(Completion::Finished(self.carried))), @@ -886,24 +913,21 @@ where } } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.for frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: Completion, interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild + BuildScfFor, - { + ) -> Result>, E> { match completion { - // The body yielded: advance the induction variable and re-check. - Completion::Finished(yielded) => { + // The body's structured yield: advance the induction variable, + // carry the yielded values forward, and re-check the condition. + Completion::Yielded(yielded) => { let step = interp.env_read(self.env, self.step)?; let next = self .induction @@ -913,8 +937,11 @@ where self.carried = yielded; Ok(FrameEffect::Continue(F::scf_for(self))) } - // A `ret` inside the body returns from the enclosing function. + // A `ret` inside the body: relay it toward the nearest `CallFrame`. Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + Completion::Finished(_) => Err(E::from(InterpreterError::Custom( + "scf.for body completed without a structured yield", + ))), } } } @@ -989,7 +1016,7 @@ where fn join_finish(&mut self, interp: &mut I, values: Product) -> Result<(), E> where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, { let merged = match self.finish.take() { None => values, @@ -998,15 +1025,19 @@ where self.finish = Some(merged); Ok(()) } +} - pub fn step_into( - mut self, - interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfFor, - { +impl Frame for AbstractScfForFrame +where + I: ForwardDataflowFrameEngine, + F: AbstractFrameBuild + BuildAbstractScfFor, + V: Clone + PartialEq + ForLoopValue + Lattice, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, interp: &mut I) -> Result>, E> { if !self.entered { self.entered = true; let end = interp.env_read(self.env, self.end)?; @@ -1030,21 +1061,17 @@ where }) } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.for frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfFor, - { + ) -> Result>, E> { let yielded = match completion { AbstractCompletion::Finished(Some(values)) => values, // The body returned: the loop finishes with what it has joined. diff --git a/crates/kirin-test-languages/Cargo.toml b/crates/kirin-test-languages/Cargo.toml index e9c10660bd..6813b4942d 100644 --- a/crates/kirin-test-languages/Cargo.toml +++ b/crates/kirin-test-languages/Cargo.toml @@ -21,6 +21,7 @@ kirin-derive-chumsky = { workspace = true, optional = true } default = [] simple-language = ["kirin-ir/derive"] arith-function-language = ["kirin-ir/derive", "kirin-arith", "kirin-cf", "kirin-function", "parser", "pretty"] +graph-function-language = ["kirin-ir/derive", "kirin-arith", "kirin-cf", "kirin-constant", "kirin-function", "parser", "pretty"] bitwise-function-language = ["kirin-ir/derive", "kirin-arith", "kirin-bitwise", "kirin-cf", "kirin-function", "parser", "pretty"] callable-language = ["kirin-ir/derive", "kirin-arith", "kirin-function", "parser", "pretty"] namespaced-language = ["kirin-ir/derive", "kirin-arith", "kirin-cf", "kirin-function", "parser", "pretty"] diff --git a/crates/kirin-test-languages/src/arith_function_language.rs b/crates/kirin-test-languages/src/arith_function_language.rs index d7641c898b..92b27d63af 100644 --- a/crates/kirin-test-languages/src/arith_function_language.rs +++ b/crates/kirin-test-languages/src/arith_function_language.rs @@ -34,7 +34,7 @@ pub enum ArithFunctionLanguage { #[cfg(feature = "interpreter")] mod interpreter { use kirin_interpreter::dialect::{ - ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, FunctionBody, + CallableBody, ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, FunctionEntry, Interp, Interpretable, InterpreterError, StrongDemand, }; use kirin_ir::{HasBottom, Product}; @@ -79,10 +79,10 @@ mod interpreter { &self, args: Product, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result, I::Error> { match self { ArithFunctionLanguage::Function { body, .. } => { - Ok(FunctionBody::new(*body).args(args)) + Ok(CallableBody::new(*body).args(args)) } _ => Err(I::Error::from(InterpreterError::NotCallable( interp.statement(), diff --git a/crates/kirin-test-languages/src/graph_function_language.rs b/crates/kirin-test-languages/src/graph_function_language.rs new file mode 100644 index 0000000000..898b0ae7b1 --- /dev/null +++ b/crates/kirin-test-languages/src/graph_function_language.rs @@ -0,0 +1,170 @@ +//! Mixed graph/SSA test language: regular SSA IR (arith + cf + function +//! calls over `CFG` bodies) combined with `DiGraph` computational-graph +//! bodies — the acceptance shape from issue #667. Adds a linear (`Block`- +//! bodied) callable and an inline graph-owning statement so both interpreter +//! entry paths (call and `Push`) are exercised. + +use kirin_arith::{Arith, ArithType, ArithValue}; +use kirin_cf::ControlFlow; +use kirin_constant::Constant; +use kirin_function::{Call, Return}; +use kirin_ir::{ + Block, CFG, DiGraph, Dialect, Placeholder as _, ResultValue, SSAValue, Signature, UnGraph, +}; + +#[derive(Debug, Clone, PartialEq, Dialect)] +#[cfg_attr(feature = "parser", derive(kirin_chumsky::HasParser))] +#[cfg_attr(feature = "pretty", derive(kirin_derive_chumsky::PrettyPrint))] +#[kirin(builders, type = ArithType, crate = kirin_ir)] +#[cfg_attr(feature = "parser", chumsky(crate = kirin_chumsky))] +#[cfg_attr(feature = "pretty", pretty(crate = kirin_prettyless))] +pub enum GraphFunctionLanguage { + /// Standard CFG-bodied function. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + Function { + body: CFG, + sig: Signature, + }, + /// DiGraph-bodied callable (a computational graph as a function body). + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + GraphFunction { + body: DiGraph, + sig: Signature, + }, + /// Linear (single-`Block`) callable: a flat instruction list. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + LinearFunction { + body: Block, + sig: Signature, + }, + /// UnGraph-bodied callable. The framework has no default walker for an + /// undirected graph body: calling one requires the compiler to supply a + /// traversal policy (`FrameBuild::from_ungraph_entry`), otherwise the + /// engine reports `NoDefaultWalker`. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + UnGraphFunction { + body: UnGraph, + sig: Signature, + }, + /// Inline graph evaluation: enters its owned digraph via a pushed frame. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "$graph_eval {lhs}, {rhs} {graph} -> {result:type}") + )] + GraphEval { + lhs: SSAValue, + rhs: SSAValue, + graph: DiGraph, + result: ResultValue, + }, + #[wraps] + Arith(Arith), + #[wraps] + Cf(ControlFlow), + #[wraps] + Constant(Constant), + #[wraps] + Call(Call), + #[wraps] + Return(Return), +} + +// Manual interpreter impls: the inline function/graph variants keep this +// enum off the `#[derive(Interpretable)]` wraps-delegation path. +#[cfg(feature = "interpreter")] +mod interpreter { + use kirin_arith::{ArithValue, CheckedDiv, CheckedRem, interpreter::DivisionByZero}; + use kirin_interpreter::BranchCondition; + use kirin_interpreter::{ + CallableBody, DiGraphFrame, ForwardEval, FrameBuild, FunctionEntry, Interp, Interpretable, + InterpreterError, SparseForwardEffect, SparseForwardInterp, + }; + use kirin_ir::{Product, SSAValue}; + + use super::GraphFunctionLanguage; + + impl Interpretable for GraphFunctionLanguage + where + I: SparseForwardInterp, + I::Frame: FrameBuild, + I::Value: std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Neg + + CheckedDiv + + CheckedRem + + BranchCondition + + TryFrom, + I::Error: From + From<>::Error>, + { + fn interpret(&self, interp: &mut I) -> Result { + match self { + GraphFunctionLanguage::Function { .. } + | GraphFunctionLanguage::GraphFunction { .. } + | GraphFunctionLanguage::LinearFunction { .. } + | GraphFunctionLanguage::UnGraphFunction { .. } => Ok(SparseForwardEffect::Next), + GraphFunctionLanguage::GraphEval { + lhs, + rhs, + graph, + result, + } => { + let args: Product = [interp.read(*lhs)?, interp.read(*rhs)?] + .into_iter() + .collect(); + // A nested (uncallable) graph body: no function activation, + // no callee resolution — the operation pushes the walker + // directly into the current activation. + let frame = DiGraphFrame::new(interp.stage(), interp.index(), *graph, args); + Ok(SparseForwardEffect::Push { + frame: I::Frame::from_digraph(frame), + results: [SSAValue::from(*result)].into_iter().collect(), + }) + } + GraphFunctionLanguage::Arith(op) => op.interpret(interp), + GraphFunctionLanguage::Cf(op) => op.interpret(interp), + GraphFunctionLanguage::Constant(op) => op.interpret(interp), + GraphFunctionLanguage::Call(op) => op.interpret(interp), + GraphFunctionLanguage::Return(op) => op.interpret(interp), + } + } + } + + impl FunctionEntry for GraphFunctionLanguage { + fn function_entry( + &self, + args: Product, + interp: &mut I, + ) -> Result, I::Error> { + match self { + GraphFunctionLanguage::Function { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + GraphFunctionLanguage::GraphFunction { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + GraphFunctionLanguage::LinearFunction { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + GraphFunctionLanguage::UnGraphFunction { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + _ => Err(I::Error::from(InterpreterError::NotCallable( + interp.statement(), + ))), + } + } + } +} diff --git a/crates/kirin-test-languages/src/lib.rs b/crates/kirin-test-languages/src/lib.rs index 64c6851a39..b52ec3cd7e 100644 --- a/crates/kirin-test-languages/src/lib.rs +++ b/crates/kirin-test-languages/src/lib.rs @@ -7,6 +7,8 @@ mod arith_function_language; mod bitwise_function_language; #[cfg(feature = "callable-language")] mod callable_language; +#[cfg(feature = "graph-function-language")] +mod graph_function_language; #[cfg(feature = "namespaced-language")] mod namespaced_language; #[cfg(feature = "simple-language")] @@ -20,6 +22,8 @@ pub use arith_function_language::ArithFunctionLanguage; pub use bitwise_function_language::BitwiseFunctionLanguage; #[cfg(feature = "callable-language")] pub use callable_language::CallableLanguage; +#[cfg(feature = "graph-function-language")] +pub use graph_function_language::GraphFunctionLanguage; #[cfg(feature = "namespaced-language")] pub use namespaced_language::NamespacedLanguage; #[cfg(feature = "simple-language")] diff --git a/docs/design/formalism/operational-semantics.md b/docs/design/formalism/operational-semantics.md index 7fc3ab8335..10fb1b8fe2 100644 --- a/docs/design/formalism/operational-semantics.md +++ b/docs/design/formalism/operational-semantics.md @@ -18,7 +18,7 @@ | Loop transition strategy | `ScopeHook`, `ScopeStep` | [`crates/kirin-interpreter/src/effect.rs`](../../../crates/kirin-interpreter/src/effect.rs) | | Statement dispatch | `Interpretable`, `InterpDispatch` | [`crates/kirin-interpreter/src/dispatch.rs`](../../../crates/kirin-interpreter/src/dispatch.rs) | | Current statement location | `InterpLocation` | [`crates/kirin-interpreter/src/interp.rs`](../../../crates/kirin-interpreter/src/interp.rs) | -| Frame protocol | `Frame`, `FrameDriver` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | +| Frame protocol | `Frame`, `ForwardFrameEngine` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | | Scope continuation frame | `ScopeFrame` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | | Call continuation frame | `CallFrame` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | | Total frame enum | `StandardFrame` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index e8e5cd5a35..e8eee49fd1 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -89,14 +89,22 @@ conflict): the shape-generic `SparseBackwardInterp` (`fact`/`raise_fact`/`effect` plus the block-topology queries) serves any sparse-backward key, and `DemandInterp` (pinned to `StrongDemand` via its supertrait) adds the demand - vocabulary. Rules bind `DemandInterp`: read converged facts (`is_demanded`), + vocabulary. `raise_fact` takes the lattice element to merge, so the shape + moves facts without inspecting them; ⊤ is `demand`'s business, and `HasTop` + rides on `DemandInterp`'s supertrait so rules never spell it. Rules bind `DemandInterp`: read converged facts (`is_demanded`), raise demands (`demand`), and end with `interp.effect()` (= `SparseBackwardEffect`); ordinary dialects are the one-liner `interp.demand_uses_if_observable(self)` (purity-aware neededness via `IsPure`). - `ClassicLiveness` (on `DenseBackwardShape`) — likewise split: the - shape-generic `DenseBackwardInterp` (`insert_fact`/`remove_fact` point-state - mechanics) serves any dense-backward key, and `ClassicLivenessInterp` adds - liveness's spellings. Rules bind `ClassicLivenessInterp`: `gen_live`/ + shape-generic `DenseBackwardInterp` (`point_state`/`point_state_mut`, which + hand the state over opaquely) serves any dense-backward key, and + `ClassicLivenessInterp` adds liveness's spellings — `PointFacts` ("a state is + a set of live values") is that key's contract, required by no engine and no + frame. What the shape *does* need of a state is `Lattice` for merges and + `DenseBackwardState` (`rename`/`forget`) for crossing edges and leaving + scopes; the parameter-to-argument substitution the CFG edge transfer and + `scf.for`'s back-edge both perform lives in those two methods, implemented + for `LiveSet` in `kirin-liveness`. Rules bind `ClassicLivenessInterp`: `gen_live`/ `kill_def`; ordinary dialects (and calls — purity is irrelevant to dense sets) are `interp.gen_uses_kill_defs(self)`; CFG terminators name their edges (`Edges`, in `DenseBackwardEffect`), structured dialects push dense frames @@ -199,7 +207,9 @@ SCF has two such operations: reads the condition value and hands the `Option` decision to the frame; the **frame** picks the arm (concrete; undecided is `IndeterminateBranch`) or explores both arms and **joins** their finish results (abstract). It walks each - arm by pushing the framework `BodyFrame`/`AbstractBlockFrame` building block. + arm by pushing the framework `BlockFrame`/`AbstractBlockFrame` building block, + consumes the arm's `Completion::Yielded` values, and relays a bubbled + `Completion::Returned` unchanged toward the nearest `CallFrame`. - **`scf.for`** → `ScfForFrame` / `AbstractScfForFrame`, built via `ScfForDispatch`. The frame pushes a body frame each iteration, advances the @@ -209,10 +219,11 @@ SCF has two such operations: accumulating finish values across exits — so `scf.for` over a lattice converges, with no framework "scope hook". -The framework `BodyFrame`/`AbstractBlockFrame` (single-block body walkers, -completing on `Yield`) are reusable **building blocks**, not framework-owned -structured semantics: the SCF frames build them to walk a chosen body, but the -structured *decision* and result binding stay in the SCF frame. A language that +The framework `BlockFrame`/`AbstractBlockFrame` (single-block body walkers, +surfacing `Yield` to their parent) are reusable **building blocks**, not +framework-owned structured semantics: the SCF frames build them to walk a +chosen body, but the structured *decision* and result binding stay in the SCF +frame. A language that uses SCF composes a total frame type embedding the standard frames plus `ScfIfFrame`/`ScfForFrame` (via `BuildScfIf`/`BuildScfFor` and the abstract equivalents); see `example/toy-lang`'s `ToyFrame`/`ToyAbstractFrame`. Future @@ -224,7 +235,7 @@ operations are implemented. ```rust pub trait FunctionEntry: Dialect { fn function_entry(&self, args: Product, interp: &mut I) - -> Result, I::Error>; + -> Result, I::Error>; } ``` @@ -232,7 +243,7 @@ Like `Interpretable`, it receives the engine `interp` directly (function entry i forward-only, so there is no `Semantics` parameter). Statements that define function bodies (e.g. `kirin_function::Function`) -return the `FunctionBody { cfg, args }` to enter on invocation (the +return the `CallableBody { body, args }` to enter on invocation (the function-call entry descriptor — not a structured-control abstraction). On language enums it is derived; `#[callable]` marks the variants that forward, all others report `NotCallable`. @@ -294,13 +305,40 @@ belongs to. A generic **frame-stack driver**: it pops the top frame, calls `Frame::step`, and applies the returned `FrameEffect` (`Continue` / `Push` / `Done` / `Complete`) — it owns *no* traversal logic itself. Traversal lives in the -frames. The default total frame type `StandardFrame` wraps the standard -`BodyFrame` (walks a function-body CFG, or a single body block that -completes on `Yield` — `Jump` retargets it, `Return` completes it) and -`CallFrame` (dispatch a callee, await its `Return`). The dialect-produced -`SparseForwardEffect` is consumed by `BodyFrame`, which maps it to a `FrameEffect` -(handling `Push` by pushing the carried frame). `StandardFrame` is -structured-control-free; a custom `F` +frames, organized along two independent axes: + +- **Body representation** (the closed `Body` vocabulary — an intentional IR + design decision): each framework-walkable representation has one + *representation walker* owning traversal mechanics only — `CFGFrame` + (multi-block, follows `Jump`, rejects an undecided `Branch`), `BlockFrame` + (one linear block; `Jump`/`Branch` are errors), and `DiGraphFrame` + (dependency-ordered DAG walk collecting the declared yields). `UnGraph` has + **no default walker**: an undirected graph has no inherent execution order, + so callable-UnGraph traversal is a dialect/compiler-supplied policy + (`FrameBuild::from_ungraph_entry`, defaulting to `NoDefaultWalker`). +- **Entry context**: the same walker serves a *callable* body (entered + through `CallFrame`) and a *nested structured-operation* body (entered + through a dialect frame); analysis owners are the abstract engines' third + context. Walkers never know their role — they surface exits through the + completion protocol (`Completion::Returned` for a function `Return`, + `Completion::Yielded` for a structured `Yield`, `Completion::Finished` for + natural completion such as a digraph's output yields) and the parent frame + decides what each means. + +`CallFrame` is the **call boundary**: it resolves the callee, allocates the +callee activation, selects the entry walker for the closed `Body` variant, +validates the completion kind (`Returned`, or a graph's natural `Finished`; +a structured `Yielded` is an error), frees the callee activation exactly +once, and delivers the values — into the caller's result slots, or as the +run's result for a root call (`ConcreteInterpreter::call` pushes a +`CallFrame::root`, so root and nested calls share one boundary +implementation). Representation walkers never free activations; a `Returned` +bubbles through dialect frames to the nearest `CallFrame`. + +The default total frame type `StandardFrame` bundles the three walkers +plus `CallFrame`. The dialect-produced `SparseForwardEffect` is consumed by +the walkers, which map it to a `FrameEffect` (handling `Push` by pushing the +carried frame). `StandardFrame` is structured-control-free; a custom `F` ([Custom traversal and policies](#custom-traversal-and-policies)) adds dialect frames or replaces traversal without touching the engine. @@ -314,7 +352,7 @@ and one *specialization* of the shared framework in the forward direction (it se lattice-valued abstract engines. `SparseForwardInterpreter` is the forward engine; `SparseBackwardInterpreter` (per-SSA demand / strong liveness) and `DenseBackwardInterpreter` (classic per-point liveness) are the backward -specializations — each with its own fact store, effect, and frame-driver +specializations — each with its own fact store, effect, and engine-capability capability, reusing the same framework (fixpoint driver + `*Transfer` inner `Interp`) and also implementing `AbstractInterpreter`. @@ -378,20 +416,105 @@ type `F` is the engine's generic; it is named in `Interpretable` *only* by a structured dialect building `SparseForwardEffect::Push` (through `SparseForwardInterp::Frame`) — ordinary dialects never mention it. -### Shared protocol vs. forward frame drivers +### Shared protocol vs. forward engine capabilities `Frame`, `FrameEngine`, `FrameEffect`, and `drive_frames` are **shared and direction-neutral** — they say nothing about a value domain or direction, and the backward engines reuse them as-is. On top of that neutral protocol sit the -per-direction frame-driver capability surfaces: `ForwardFrameDriver` / -`ForwardDataflowFrameDriver` for the forward engines (they require `Env`, run -`SparseForwardEffect`, bind block args, write forward results, and summarize -forward calls; `FrameDriver` and `AbstractFrameDriver` are retained as -compatibility aliases), and `DenseBackwardFrameDriver` for the dense backward -engine (statement dispatch, point-state access, edge absorption against the -converged summaries, per-point recording). The sparse backward engine needs no -frame-driver surface at all — its `DemandFrame` dispatches rules directly on -the driver. +per-direction engine-capability surfaces: the forward **component traits** +below, composed by the `ForwardFrameEngine` / `ForwardDataflowFrameEngine` +umbrellas, and `DenseBackwardFrameEngine` for the dense backward engine +(statement dispatch, point-state access, edge absorption against the converged +summaries, per-point recording). The sparse backward engine needs no capability +surface at all — its `DemandFrame` dispatches rules directly on the transfer. + +**"Engine" means three different things**, so the names are kept distinct: + +| name | what it is | +|---|---| +| `drive_frames` | the **frame-stack driver** — the loop. The only thing called a driver at this layer; `ForwardDriver`/`DenseBackwardDriver` are fixpoint-driver *structs*, not capability traits. | +| `FrameEngine` | the **minimal engine contract** the generic frame stack needs: a total `Error` type, nothing more. | +| `ForwardFrameEngine` | the **full engine capability set** for the standard concrete frame universe. | +| `ForwardDataflowFrameEngine` | the capability set for the standard forward-abstract frame universe. | +| component traits | narrowly scoped **services used by individual frames**. | + +#### The forward capability model + +Forward capabilities are split by **what one frame needs**, not by what one +engine happens to provide. A frame's bound is then a precise statement of which +engine operations it can reach, and an engine that implements only part of the +surface still runs the frames it can support. + +| trait | capability | required by | +|---|---|---| +| `StatementDispatch: Interp` | `run_statement` — dispatch to the dialect rule | every executing frame | +| `BlockQueries: Interp` | `block_params`/`first_statement`/`next_statement` | `BlockCursor`, `BlockFrame`, `AbstractBlockFrame`, dialect block walkers | +| `CFGQueries: BlockQueries` | `cfg_entry` | `CFGFrame` | +| `DiGraphQueries: Interp` | `digraph_walk_plan` (default: `NoDefaultWalker`) | `DiGraphFrame`, `AbstractDiGraphFrame` | +| `CallServices: Env` | `alloc_env`/`free_env`/`resolve_call`/`enter_function` | `CallFrame` | + +**The `*Queries` traits are read-only, and only require `Interp`** — so nothing +on them can touch SSA storage, and their names cannot hide a store mutation. The +one operation that needs both a query and a write, binding a block's parameters +to incoming actuals, lives on the crate-private `BlockBinding` extension +(bounded `Env + BlockQueries`) instead. A frame that binds a block entry +therefore spells that requirement out: `BlockCursor::bind_entry` and +`::enter_block` take `Env + BlockQueries`, while `::advance` takes `BlockQueries` +alone and `::write_child_results` takes `Env` alone. + +`CallServices` names *services*, not a convention: **`CallFrame` still owns the +calling convention** — the operation order, which completions are legal, and +freeing the activation exactly once — and this trait only supplies the +primitives. It is deliberately **not** split further: the standard `CallFrame` +consumes all four together, and their pairing is a safety property (an +`alloc_env` without its `free_env` leaks; a second `free_env` double-frees), so +no engine should be able to offer half a call convention. + +`StatementDispatch` and `InterpDispatch` face opposite directions and are easy +to confuse. `InterpDispatch` is implemented by a **stage/language** to route a +statement to the right dialect rule. `StatementDispatch` is implemented by the +**engine** and is what a *frame* calls: it stashes the current location +(`stage`/`statement`/`index`) so the rule can read it back through `Interp`, then +delegates to `InterpDispatch`. + +Two umbrellas compose them, one per engine family. Use an umbrella at the +*universe* level — a total frame enum's engine must support the union of all its +variants — and the components at the *member* level: + +```rust +// Full concrete surface. Adds no methods; blanket-implemented. +pub trait ForwardFrameEngine: + StatementDispatch + CFGQueries + DiGraphQueries + CallServices {} +impl ForwardFrameEngine for T +where T: StatementDispatch + CFGQueries + DiGraphQueries + CallServices {} + +// Abstract dataflow: the traversal it *shares*, plus merge/summarization. +// Notably NOT CallServices, and NOT CFGQueries. +pub trait ForwardDataflowFrameEngine: + Env + StatementDispatch + BlockQueries + DiGraphQueries +{ + type SummaryKey: Clone + Eq + Hash; + fn analysis_merge(..); fn contribute_return(..); fn current_function_key(..); + fn summarize_call(..); fn max_iterations(..); +} +``` + +An abstract engine therefore **no longer inherits the concrete call lifecycle**. +That follows the semantics: forward abstract interpretation *summarizes* a call +(`summarize_call` → `AbstractCallFrame`) rather than descending into it, and +reaches a callable body's entry block through `Owner` seeding in the fixpoint +driver rather than `cfg_entry`. Requiring it to expose `alloc_env`, `free_env`, +`enter_function`, `resolve_call`, and `cfg_entry` was demanding a call +convention it never performs. `tests/frame_engine_capabilities.rs` pins this +down with deliberately incomplete mock engines whose ability to compile *is* the +regression test. + +Binding values into an **explicitly selected** activation is +`Env::bind_values(index, slots, values)`, not a method on any umbrella, so it is +no longer confusable with `SparseForwardInterp::write_results` (the +dialect-facing helper, which binds into the engine's *current* activation, +`interp.index()`). The two differ by *which activation*, not by what they do — +so neither name mentions the `Product` container it happens to accept. ```rust pub enum FrameEffect { Continue(F), Push { parent: F, child: F }, Done, Complete(C) } @@ -399,62 +522,184 @@ pub enum FrameEffect { Continue(F), Push { parent: F, child: F }, Done, Co pub trait FrameEngine { type Error; } // direction-neutral anchor (no value domain) impl FrameEngine for T { type Error = ::Error; } -pub trait Frame: Sized { // implemented by the *total* frame enum +// ONE interface, implemented by every frame — individual walkers and total enums alike. +// The effects are over `F`, the total frame type composed into, never over `Self`. +pub trait Frame: Sized { type Completion; - fn step(self, &mut I) -> Result, I::Error>; - fn resume_done(self, &mut I) -> Result, I::Error>; - fn resume(self, Self::Completion, &mut I) -> Result, I::Error>; + fn step_into(self, &mut I) -> Result, I::Error>; + fn resume_done_into(self, &mut I) -> Result, I::Error>; + fn resume_into(self, Self::Completion, &mut I) -> Result, I::Error>; } -// The one shared, direction-neutral driver loop, used by every engine: -pub fn drive_frames>(engine: &mut I, frames: &mut Vec) +// The one shared, direction-neutral driver loop, used by every engine. The +// stack's element type must be a *universe* — `F: Frame`. +pub fn drive_frames>(engine: &mut I, frames: &mut Vec) -> Result; -// Forward-specific capability surface (alias: FrameDriver): -pub trait ForwardFrameDriver: Env { /* env alloc/free, IR queries, dispatch, resolution */ } +// Forward-specific capability surface: one component trait per kind of +// traversal, plus two umbrellas — see "The forward capability model" above. +pub trait StatementDispatch: Interp { /* run_statement */ } +pub trait BlockQueries: Interp { /* read-only block queries */ } +pub trait CFGQueries: BlockQueries { /* cfg_entry */ } +pub trait DiGraphQueries: Interp { /* digraph_walk_plan */ } +pub trait CallServices: Env { /* alloc/free env, resolve_call, enter_function */ } +pub(crate) trait BlockBinding: Env + BlockQueries { /* bind_block_args */ } ``` +**Members and universes.** The `F` parameter is what lets one trait serve both +roles a frame stack needs: + +- a **member** — an individual walker (`BlockFrame`, `CallFrame`, a dialect's own + frame). It is one variant of `F` and names its successors in `F`, re-wrapping + itself through the relevant `*FrameBuild` hook. Members are generic over `F`, + so the same walker composes into any language's frame type. +- a **universe** — a total frame enum. It implements `Frame` when it is + the stack's element type, and stays generic over `F` so it can *also* be + embedded in a larger enum without re-enumerating its variants. `TracingFrame` + in `toy-lang`'s tests is exactly this: a newtype wrapping `ToyFrame` whole, + counting steps and delegating. (A leaf universe with no members, like the + sparse backward `DemandFrame`, honestly pins `F = Self`.) + +The stack must be homogeneous in *type* while heterogeneous in *kind*, which is +why `F` is a closed sum type rather than `Box`: a run holds a +`CallFrame`, a `CFGFrame`, a `BlockFrame` and a dialect frame simultaneously. + `Frame` is anchored only on `FrameEngine` (a total `Error`), **not** on the forward value engine `Interp` — so the frame protocol is decoupled from forward value interpretation and reusable by other analyses. Every `Interp` is a `FrameEngine` by blanket impl. The engine owns a `Vec` and calls -`drive_frames`, which pops the top frame, `step`s it, and applies the returned -`FrameEffect`. `ForwardFrameDriver: Env` is the richer **forward** capability -surface the *forward* frames call (it requires `Env` because the default -`bind_block_args`/`write_results` use `env_write`); **both forward engines implement -it**. The concrete and -abstract standard frames are two *implementations* of this one protocol — not -parallel frameworks. - -### Concrete frames — `BodyFrame` / `CallFrame` / `StandardFrame` +`drive_frames`, which pops the top frame, `step_into`s it, and applies the +returned `FrameEffect`. The forward component traits above are the richer +capability surfaces the *forward* frames call; each forward frame bounds only the +components it uses, and the concrete engine implements all of them (so it also +gets `ForwardFrameEngine` by blanket impl). The concrete and abstract standard +frames are two *implementations* of this one protocol — not parallel frameworks. + +Narrowest first, the shipped member frames now require: + +| frame | bound | +|---|---| +| `ScfIfFrame` | `FrameEngine` — decides its arm before being built, so it touches no engine capability at all | +| `ScfForFrame` | `Env` — reads the loop bound/step, pushes a `BlockFrame` | +| `CallFrame` | `CallServices` | +| `BlockCursor` | per operation: `BlockQueries` (query) / `Env + BlockQueries` (bind entry) / `Env` (bind child results) | +| `DiGraphFrame::finish`, `AbstractDiGraphFrame::finish` | `Env` — the schedule is already consumed; only the yields are read | +| `BlockFrame` | `BlockQueries + StatementDispatch + SparseForwardInterp` | +| `CFGFrame` | `CFGQueries + StatementDispatch + SparseForwardInterp` | +| `DiGraphFrame` | `DiGraphQueries + StatementDispatch + SparseForwardInterp` | +| `AbstractBlockFrame`, `AbstractCallFrame`, `AbstractDiGraphFrame` | `ForwardDataflowFrameEngine` (+ `SparseForwardInterp` for the walkers) | +| `StandardFrame`, `ToyFrame`, other total concrete enums | `ForwardFrameEngine + SparseForwardInterp` — correct at the universe level | +| `StandardAbstractFrame`, other total abstract enums | `ForwardDataflowFrameEngine + SparseForwardInterp` | + +### Concrete frames — `BlockFrame` / `CFGFrame` / `DiGraphFrame` / `CallFrame` / `StandardFrame` `ConcreteInterpreter` is generic over the total frame type `F` (default -`StandardFrame`). A custom enum reuses the standard `BodyFrame`/`CallFrame` -single-path traversal through `FrameBuild` (`from_body`/`from_call`) and their -`*_into` delegating methods, adds dialect frames / observation, and instantiates -the engine with that `F`. (Examples: `example/toy-lang`'s `ToyFrame`, which adds +`StandardFrame`). A custom enum reuses the standard single-path traversal — +the representation walkers and the call boundary — through `FrameBuild` +(`from_block`/`from_cfg`/`from_call`/`from_digraph`) and their `*_into` +delegating methods, adds dialect frames / observation, and instantiates the +engine with that `F`. Overriding `FrameBuild::from_ungraph_entry` (default: +`NoDefaultWalker`) supplies a callable-UnGraph traversal policy without +touching the generic logic (see the workspace `tests/body_kinds.rs` policy +test). (Further examples: `example/toy-lang`'s `ToyFrame`, which adds `kirin_scf`'s `ScfIfFrame`/`ScfForFrame` via `BuildScfIf`/`BuildScfFor`; and a `TracingFrame` counting call/body visitation while running the real program — see `example/toy-lang`'s `interpreter::tests::advanced`.) -### Abstract frames — `StandardAbstractFrame` / `AbstractFrameBuild` / `ForwardDataflowFrameDriver` +### Callable-body walkers — `CallBodyFramePolicy` / `DefaultBodyFrames` + +`CallFrame` bundles two separable concerns, and only the second is +configurable: + +| Concern | Where | Configurable? | +|---|---|---| +| **call convention** — resolve the callee, allocate its activation, ask `FunctionEntry` for the body, suspend, validate the completion kind, free the activation *exactly once*, bind results | `CallFrame` itself | **no** — this is where double-frees would live | +| **walker choice** — which frame traverses the callee body | `CallBodyFramePolicy` | **yes** | + +`Body` stays a closed vocabulary, so `CallFrame::step_into` still matches it +exhaustively; only the frame each arm builds is chosen by the policy. The +default reproduces today's behaviour exactly: + +| body | `DefaultBodyFrames` | a custom `MyBodyFrames` might use | +|---|---|---| +| `CFG` | `CFGFrame` | `MyCustomCFGFrame` | +| `Block` | `BlockFrame` | `BlockFrame` (delegate to the default) | +| `DiGraph` | `DiGraphFrame` | `MyScheduledGraphFrame` | +| `UnGraph` | `FrameBuild::from_ungraph_entry` → `NoDefaultWalker` unless overridden | `MyCircuitWalker` | + +The policy is selected by the **compiler/language author** through the concrete +total frame type's `FrameBuild::BodyFrames`; `#[derive(FrameBuild)]` emits +`DefaultBodyFrames` unless given +`#[interpret(body_frames = MyBodyFrames)]`. `CallFrame` continues to mean +`CallFrame`. A dialect crate may *offer* reusable walkers +or policies, but a callable dialect should not permanently fix one traversal for +every engine. + +**Concrete execution only, and deliberately so.** Concrete execution descends +into a callee — `CallFrame` → body walker → completion → `CallFrame`. Forward +abstract interpretation does not: `AbstractCallFrame` *summarizes* the call while +the fixpoint engine separately maps a callable body to an `Owner::Block` or +`Owner::Graph` in `seed_entry_block`. Customizing that would be an abstract +body-entry/owner policy, not this one. The backward engines differ further — +sparse backward uses SSA values as owners and never walks callable bodies through +a call frame; dense backward uses block owners and reverse walks. If those ever +need configurable representation traversal, add engine-family-specific policies; +do not make IR owners supply walkers. + +**Not consulted for nested bodies.** `scf.if`/`scf.for` enter their Blocks +through their own dialect frames (chosen per engine by `ScfIfDispatch` / +`ScfForDispatch`), which then reuse a framework `BlockFrame`. Those are *nested* +bodies — they borrow the caller's activation and exit by `Yield` — so the +callable-body policy plays no part. + +### Abstract frames — `StandardAbstractFrame` / `AbstractFrameBuild` / `ForwardDataflowFrameEngine` `SparseForwardInterpreter` is symmetrically generic over a total abstract frame type `F` (default `StandardAbstractFrame`). The standard abstract frames -(`AbstractFunctionFrame`, `AbstractCFGFrame`, `AbstractBlockFrame`, -`AbstractCallFrame`) implement the *same* +(`AbstractBlockFrame`, `AbstractCallFrame`, `AbstractDiGraphFrame`) implement the +*same* `Frame` protocol, but their traversal is the abstract one: a CFG block worklist that joins/widens at merge points, `Branch` exploration, single-block -body walks that complete on `Yield`, and per-key call summarization. A custom +body walks that complete on `Yield`, dependency-ordered graph passes, and +per-key call summarization. A custom enum reuses them through `AbstractFrameBuild` and the `*_into` methods — exactly mirroring the concrete pattern (see `ToyAbstractFrame`, which adds `AbstractScfIfFrame`/`AbstractScfForFrame`, and `TracingAbstractFrame` in the same test module). -Abstract frames need a few capabilities beyond `ForwardFrameDriver`, on -`ForwardDataflowFrameDriver: ForwardFrameDriver` (alias: `AbstractFrameDriver`) — -`analysis_merge`, `contribute_return`, and -`summarize_call`. The interprocedural protocol stays **atomic in the engine**: +**Executable owners and the body vocabulary.** The forward fixpoint's work items +are `Owner`s, and only *executable* owners run frames: `Owner::Block` (one CFG +block) and `Owner::Graph` (one whole graph body). `Owner::Function` is +storage-only — it accumulates a context's joined entry arguments and joined +return, and is never scheduled. `seed_entry_block` is the single place a callable +body becomes executable work, translating the closed `Body` vocabulary into an +owner: `CFG` → its entry block, `Block` → itself, `DiGraph` → an `Owner::Graph`. +A graph owner is one unit because a single dependency-ordered pass is *exact* for +a DAG — no intra-graph widening is needed, and convergence pressure comes only +from entry widening when a new call site raises the owner's entry product. On +completion a graph owner has no successor edges; its declared yields become the +function's return contribution. `UnGraph` bodies are rejected with +`NoDefaultWalker`: an undirected graph has no derivable traversal order, and +unlike the concrete engine's `FrameBuild::from_ungraph_entry` there is currently +no seam through which a compiler could supply one. Analogously, +`AbstractFrameBuild::from_digraph` defaults to rejecting, so a total abstract +frame enum that carries no `AbstractDiGraphFrame` inherits the refusal rather +than pretending to analyze a graph body. + +`AbstractDiGraphFrame` differs from the concrete `DiGraphFrame` in exactly one +substantive way: a `Call` effect pushes an `AbstractCallFrame`, routing the call +through `summarize_call` instead of descending into the callee. Descending would +neither widen nor terminate on recursion. + +Abstract frames need a few capabilities beyond the traversal they share with +concrete execution, on `ForwardDataflowFrameEngine: Env + StatementDispatch + +BlockQueries + DiGraphQueries` — +`analysis_merge`, `contribute_return`, and `summarize_call`. It does **not** +extend `CallServices`: `AbstractCallFrame`'s single engine requirement is +`summarize_call`, so summarizing a call needs no call convention at all. Nor +`CFGQueries`, since the entry block of a callable body arrives via `Owner` +seeding rather than `cfg_entry`. The interprocedural protocol stays **atomic in +the engine**: `summarize_call` performs resolve → key → join-into-callee-entry → record-caller (*including same-key recursion*) → read-return-summary in one step, so a custom frame chooses *what to traverse* but cannot reorder the summary protocol and @@ -529,7 +774,8 @@ and terminating on unknown inputs (both fold to `Top`). Runnable as kill/gen transfer (`gen_uses_kill_defs`, purity-irrelevant). Block owners converge boundary summaries; `live_before`/`live_after` are reconstructed per point on demand (never persisted by the fixpoint); scf owns dense - frames (arm-join, loop fixpoint). + frames (arm-join, loop fixpoint). Classic liveness consumes finalized IR + directly and does not require a sparse-demand pre-pass. Strong per-point sets are the composition `dense ∩ demanded`, not a third analysis. Because the `Semantics` parameter distinguishes impls, one dialect carries all three rules at once, as every shipped dialect demonstrates. diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 630d5741ef..e7096c039f 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -3,16 +3,18 @@ //! The toy language uses `kirin-scf`, whose `scf.for` pushes a dialect-owned //! loop frame ([`ScfForFrame`]/[`AbstractScfForFrame`]). A language that uses //! such a dialect composes its own total frame enum embedding the standard -//! framework frames (via [`FrameBuild`]/[`AbstractFrameBuild`]) plus the -//! dialect frames (via [`BuildScfFor`]/[`BuildAbstractScfFor`]). The engine is +//! framework frames — the representation walkers +//! ([`BlockFrame`]/[`CFGFrame`]/[`DiGraphFrame`]) and the [`CallFrame`] call +//! boundary, via [`FrameBuild`]/[`AbstractFrameBuild`] — plus the dialect +//! frames (via [`BuildScfFor`]/[`BuildAbstractScfFor`]). The engine is //! not forked — only the engine's `F` type parameter changes. use std::hash::Hash; use kirin_interpreter::engine::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BodyFrame, CallFrame, Completion, Frame, FrameBuild, FrameDriver, - FrameEffect, InterpreterError, SparseForwardInterp, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, + CFGFrame, CallFrame, Completion, DefaultBodyFrames, DiGraphFrame, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameBuild, FrameEffect, InterpreterError, SparseForwardInterp, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, BuildScfFor, @@ -23,23 +25,23 @@ use kirin_scf::{ // Concrete // =========================================================================== -/// Concrete total frame: standard body/call traversal plus the SCF if/for frames. +/// Concrete total frame: the standard representation walkers and call +/// boundary plus the SCF if/for frames. +/// +/// The framework injections are derived — one constructor per walker, matched by +/// field type. The two scf variants are injected through `kirin-scf`'s own +/// `BuildScfIf`/`BuildScfFor`, which the dialect declares and a language +/// implements by hand. +#[derive(FrameBuild)] pub enum ToyFrame { - Body(BodyFrame), + Block(BlockFrame), + CFG(CFGFrame), Call(CallFrame), + DiGraph(DiGraphFrame), ScfIf(ScfIfFrame), ScfFor(ScfForFrame), } -impl FrameBuild for ToyFrame { - fn from_body(frame: BodyFrame) -> Self { - ToyFrame::Body(frame) - } - fn from_call(frame: CallFrame) -> Self { - ToyFrame::Call(frame) - } -} - impl BuildScfIf for ToyFrame { fn scf_if(frame: ScfIfFrame) -> Self { ToyFrame::ScfIf(frame) @@ -52,42 +54,49 @@ impl BuildScfFor for ToyFrame { } } -impl Frame for ToyFrame +impl Frame for ToyFrame where - I: FrameDriver + SparseForwardInterp>, + I: ForwardFrameEngine + SparseForwardInterp, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, { type Completion = Completion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ToyFrame::Body(frame) => frame.step_into::(interp), - ToyFrame::Call(frame) => frame.step_into::(interp), - ToyFrame::ScfIf(frame) => frame.step_into::(interp), - ToyFrame::ScfFor(frame) => frame.step_into::(interp), + ToyFrame::Block(frame) => frame.step_into(interp), + ToyFrame::CFG(frame) => frame.step_into(interp), + ToyFrame::Call(frame) => frame.step_into(interp), + ToyFrame::DiGraph(frame) => frame.step_into(interp), + ToyFrame::ScfIf(frame) => frame.step_into(interp), + ToyFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - ToyFrame::Body(frame) => Ok(frame.resume_done_into::()), - ToyFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), - ToyFrame::ScfIf(frame) => frame.resume_done_into::(), - ToyFrame::ScfFor(frame) => frame.resume_done_into::(), + ToyFrame::Block(frame) => frame.resume_done_into(interp), + ToyFrame::CFG(frame) => frame.resume_done_into(interp), + ToyFrame::Call(frame) => frame.resume_done_into(interp), + ToyFrame::DiGraph(frame) => frame.resume_done_into(interp), + ToyFrame::ScfIf(frame) => frame.resume_done_into(interp), + ToyFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ToyFrame::Body(frame) => frame.resume_into::(completion, interp), - ToyFrame::Call(frame) => frame.resume_into::(completion, interp), - ToyFrame::ScfIf(frame) => frame.resume_into::(completion), - ToyFrame::ScfFor(frame) => frame.resume_into::(completion, interp), + ToyFrame::Block(frame) => frame.resume_into(completion, interp), + ToyFrame::CFG(frame) => frame.resume_into(completion, interp), + ToyFrame::Call(frame) => frame.resume_into(completion, interp), + ToyFrame::DiGraph(frame) => frame.resume_into(completion, interp), + ToyFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ToyFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } @@ -97,6 +106,10 @@ where // =========================================================================== /// Abstract total frame: standard abstract traversal plus the SCF if/for frames. +/// +/// No `AbstractDiGraphFrame` variant, so the derive omits `from_digraph` and the +/// trait's refusing default applies — toy-lang has no graph bodies. +#[derive(AbstractFrameBuild)] pub enum ToyAbstractFrame { Block(AbstractBlockFrame), Call(AbstractCallFrame), @@ -104,15 +117,6 @@ pub enum ToyAbstractFrame { ScfFor(AbstractScfForFrame), } -impl AbstractFrameBuild for ToyAbstractFrame { - fn from_block(frame: AbstractBlockFrame) -> Self { - ToyAbstractFrame::Block(frame) - } - fn from_call(frame: AbstractCallFrame) -> Self { - ToyAbstractFrame::Call(frame) - } -} - impl BuildAbstractScfIf for ToyAbstractFrame { fn scf_if(frame: AbstractScfIfFrame) -> Self { ToyAbstractFrame::ScfIf(frame) @@ -125,44 +129,45 @@ impl BuildAbstractScfFor for ToyAbstractFrame { } } -impl Frame for ToyAbstractFrame +impl Frame for ToyAbstractFrame where - I: AbstractFrameDriver - + SparseForwardInterp>, - V: Clone + PartialEq + ForLoopValue, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, + F: AbstractFrameBuild + BuildAbstractScfIf + BuildAbstractScfFor, + V: Clone + PartialEq + ForLoopValue + Lattice, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ToyAbstractFrame::Block(frame) => frame.step_into::(interp), - ToyAbstractFrame::Call(frame) => frame.step_into::(interp), - ToyAbstractFrame::ScfIf(frame) => frame.step_into::(interp), - ToyAbstractFrame::ScfFor(frame) => frame.step_into::(interp), + ToyAbstractFrame::Block(frame) => frame.step_into(interp), + ToyAbstractFrame::Call(frame) => frame.step_into(interp), + ToyAbstractFrame::ScfIf(frame) => frame.step_into(interp), + ToyAbstractFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - ToyAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - ToyAbstractFrame::Call(frame) => frame.resume_done_into::(), - ToyAbstractFrame::ScfIf(frame) => frame.resume_done_into::(), - ToyAbstractFrame::ScfFor(frame) => frame.resume_done_into::(), + ToyAbstractFrame::Block(frame) => frame.resume_done_into(interp), + ToyAbstractFrame::Call(frame) => frame.resume_done_into(interp), + ToyAbstractFrame::ScfIf(frame) => frame.resume_done_into(interp), + ToyAbstractFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ToyAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), - ToyAbstractFrame::Call(frame) => frame.resume_into::(completion), - ToyAbstractFrame::ScfIf(frame) => frame.resume_into::(completion, interp), - ToyAbstractFrame::ScfFor(frame) => frame.resume_into::(completion, interp), + ToyAbstractFrame::Block(frame) => frame.resume_into(completion, interp), + ToyAbstractFrame::Call(frame) => frame.resume_into(completion, interp), + ToyAbstractFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ToyAbstractFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } @@ -171,9 +176,9 @@ where // Dense backward (classic per-point liveness) // =========================================================================== -use kirin_interpreter::PointFacts; +use kirin_interpreter::DenseBackwardState; use kirin_interpreter::engine::{ - DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBlockFrame, DenseFrameBuild, + DenseBackwardCompletion, DenseBackwardFrameEngine, DenseBlockFrame, DenseFrameBuild, }; use kirin_scf::{BuildDenseScfFor, BuildDenseScfIf, DenseScfForFrame, DenseScfIfFrame}; @@ -181,18 +186,13 @@ use kirin::prelude::Lattice; /// Dense backward total frame: the standard block walk plus the SCF dense /// frames (arm-join for `scf.if`, the loop-carried fixpoint for `scf.for`). +#[derive(DenseFrameBuild)] pub enum ToyDenseBackwardFrame { Block(DenseBlockFrame), ScfIf(DenseScfIfFrame), ScfFor(DenseScfForFrame), } -impl DenseFrameBuild for ToyDenseBackwardFrame { - fn from_block(frame: DenseBlockFrame) -> Self { - ToyDenseBackwardFrame::Block(frame) - } -} - impl BuildDenseScfIf for ToyDenseBackwardFrame { fn scf_if(frame: DenseScfIfFrame) -> Self { ToyDenseBackwardFrame::ScfIf(frame) @@ -205,41 +205,43 @@ impl BuildDenseScfFor for ToyDenseBackwardFrame { } } -impl Frame for ToyDenseBackwardFrame +impl Frame for ToyDenseBackwardFrame where - I: DenseBackwardFrameDriver>, - V: Clone + PartialEq + Lattice + PointFacts, + I: DenseBackwardFrameEngine, + F: DenseFrameBuild + BuildDenseScfIf + BuildDenseScfFor, + V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, { type Completion = DenseBackwardCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ToyDenseBackwardFrame::Block(frame) => frame.step_into::(interp), - ToyDenseBackwardFrame::ScfIf(frame) => frame.step_into::(interp), - ToyDenseBackwardFrame::ScfFor(frame) => frame.step_into::(interp), + ToyDenseBackwardFrame::Block(frame) => frame.step_into(interp), + ToyDenseBackwardFrame::ScfIf(frame) => frame.step_into(interp), + ToyDenseBackwardFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into( + self, + interp: &mut I, + ) -> Result>, E> { match self { - ToyDenseBackwardFrame::Block(frame) => frame.resume_done_into::(), - ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_done_into::(), - ToyDenseBackwardFrame::ScfFor(frame) => frame.resume_done_into::(), + ToyDenseBackwardFrame::Block(frame) => frame.resume_done_into(interp), + ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_done_into(interp), + ToyDenseBackwardFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ToyDenseBackwardFrame::Block(frame) => frame.resume_into::(completion, interp), - ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_into::(completion, interp), - ToyDenseBackwardFrame::ScfFor(frame) => { - frame.resume_into::(completion, interp) - } + ToyDenseBackwardFrame::Block(frame) => frame.resume_into(completion, interp), + ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ToyDenseBackwardFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } diff --git a/example/toy-lang/src/interpreter/mod.rs b/example/toy-lang/src/interpreter/mod.rs index 07b6974516..3071453ef7 100644 --- a/example/toy-lang/src/interpreter/mod.rs +++ b/example/toy-lang/src/interpreter/mod.rs @@ -22,7 +22,7 @@ use kirin_interpreter::engine::{ CallContext, ConcreteInterpreter, CrossStageLinker, SameStageLinker, SparseForwardInterpreter, expect_single, }; -use kirin_liveness::{DemandResult, DenseLivenessResult, LiveSet}; +use kirin_liveness::{DenseLivenessResult, LiveSet}; use crate::language::{HighLevel, LowLevel}; use crate::stage::Stage; @@ -47,18 +47,6 @@ pub type ToyConstProp<'ir, Lk = CrossStageLinker> = SparseForwardInterpreter< ToyAbstractFrame, >; -/// Classic per-point liveness (dense backward) over toy programs, with a -/// frame type embedding the SCF dense frames (arm join + loop fixpoint). -/// Strong liveness needs no composition — the sparse demand engine has no -/// frames (loop-carried demand converges on the value worklist), so -/// [`kirin_liveness::analyze_demand`] applies directly. -pub type ToyDenseLiveness<'ir> = kirin_liveness::DenseLiveness< - 'ir, - Stage, - InterpreterError, - ToyDenseBackwardFrame, ->; - /// Execute `function_name` starting at `stage_name`, following calls across /// language boundaries. pub fn run_i64( @@ -188,18 +176,20 @@ fn function_cfg( Ok((stage_id, cfg)) } -/// Run both liveness analyses over `function_name`'s body at `stage_name`: -/// strong liveness (the sparse demanded set — DCE-grade) and classic per-point -/// liveness (dense block-boundary sets — regalloc-grade). -pub fn analyze_liveness( +/// Run classic per-point liveness (dense backward — regalloc-grade +/// block-boundary and per-statement sets) over `function_name`'s body at +/// `stage_name`. Consumes the finalized IR directly; strong demand +/// ([`kirin_liveness::analyze_demand`]) is an independent analysis and is +/// not involved. +pub fn analyze_classic_liveness( pipeline: &Pipeline, stage_name: &str, function_name: &str, -) -> Result<(DemandResult, DenseLivenessResult), InterpreterError> { +) -> Result<(CompileStage, CFG, DenseLivenessResult), InterpreterError> { let (stage, cfg) = function_cfg(pipeline, stage_name, function_name)?; - let demand = kirin_liveness::analyze_demand(pipeline, stage, cfg)?; - let mut engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - engine.analyze(stage, cfg)?; - let dense = DenseLivenessResult::from_engine(&mut engine, stage, cfg)?; - Ok((demand, dense)) + let result = kirin_liveness::analyze_dense_with_frame::< + _, + ToyDenseBackwardFrame, + >(pipeline, stage, cfg)?; + Ok((stage, cfg, result)) } diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index ad20d04554..313b268dc1 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -564,11 +564,13 @@ mod advanced { use std::hash::Hash; use kirin_constprop::{ConstPropContext, ConstPropValue}; - use kirin_interpreter::engine::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BodyFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, - CrossStageLinker, Frame, FrameBuild, FrameDriver, FrameEffect, InterpreterError, - SparseForwardInterp, SparseForwardInterpreter, expect_single, + use kirin_interpreter::SameStageLinker; +use kirin_interpreter::engine::{ + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, + CFGFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, CrossStageLinker, + DefaultBodyFrames, DiGraphFrame, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, + FrameBuild, FrameEffect, InterpreterError, SparseForwardInterp, SparseForwardInterpreter, + expect_single, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, @@ -576,13 +578,16 @@ mod advanced { }; use super::build_pipeline; - use crate::interpreter::ToyError; + use kirin::prelude::Lattice; + + use crate::interpreter::{ToyAbstractFrame, ToyError, ToyFrame}; use crate::stage::Stage; // --- A custom total frame enum ----------------------------------------- // - // It reuses the standard `BodyFrame`/`CallFrame` traversal (and the SCF loop - // frame) verbatim via `FrameBuild`/`BuildScfFor` + the delegating `*_into` + // It reuses the standard representation walkers (`BlockFrame`/`CFGFrame`/ + // `DiGraphFrame`), the `CallFrame` call boundary, and the SCF frames + // verbatim via `FrameBuild`/`BuildScfFor` + the delegating `*_into` // methods, and adds *observation*: every call and every body step is counted // in a side log. The engine is not forked — only `ConcreteInterpreter`'s `F` // type parameter changes. @@ -597,82 +602,72 @@ mod advanced { body_steps: usize, } - enum TracingFrame { - Body(BodyFrame), - Call(CallFrame), - ScfIf(ScfIfFrame), - ScfFor(ScfForFrame), - } + /// A *wrapper* universe: it does not re-enumerate the language's frames, it + /// embeds `ToyFrame` whole and observes it. Only possible because `Frame`'s + /// effects are over the outer total type `F`, not over `Self` — so + /// `ToyFrame: Frame` holds as soon as `TracingFrame` + /// implements the build traits. + struct TracingFrame(ToyFrame); impl FrameBuild for TracingFrame { - fn from_body(frame: BodyFrame) -> Self { - TracingFrame::Body(frame) + type BodyFrames = DefaultBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + Self(ToyFrame::Block(frame)) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self(ToyFrame::CFG(frame)) } fn from_call(frame: CallFrame) -> Self { - TracingFrame::Call(frame) + Self(ToyFrame::Call(frame)) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self(ToyFrame::DiGraph(frame)) } } impl BuildScfIf for TracingFrame { fn scf_if(frame: ScfIfFrame) -> Self { - TracingFrame::ScfIf(frame) + Self(ToyFrame::ScfIf(frame)) } } impl BuildScfFor for TracingFrame { fn scf_for(frame: ScfForFrame) -> Self { - TracingFrame::ScfFor(frame) + Self(ToyFrame::ScfFor(frame)) } } - impl Frame for TracingFrame + impl Frame for TracingFrame where - I: FrameDriver + SparseForwardInterp>, + I: ForwardFrameEngine + SparseForwardInterp, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, { type Completion = Completion; - fn step(self, interp: &mut I) -> Result, I::Error> { - match self { - TracingFrame::Body(frame) => { - TRACE.with(|t| t.borrow_mut().body_steps += 1); - frame.step_into::(interp) - } - TracingFrame::Call(frame) => { - TRACE.with(|t| t.borrow_mut().calls += 1); - frame.step_into::(interp) + fn step_into(self, interp: &mut I) -> Result>, E> { + match &self.0 { + ToyFrame::Block(_) | ToyFrame::CFG(_) => { + TRACE.with(|t| t.borrow_mut().body_steps += 1) } - TracingFrame::ScfIf(frame) => frame.step_into::(interp), - TracingFrame::ScfFor(frame) => frame.step_into::(interp), + ToyFrame::Call(_) => TRACE.with(|t| t.borrow_mut().calls += 1), + _ => {} } + self.0.step_into(interp) } - fn resume_done( - self, - _interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingFrame::Body(frame) => Ok(frame.resume_done_into::()), - TracingFrame::Call(frame) => { - frame.resume_done_into::().map_err(I::Error::from) - } - TracingFrame::ScfIf(frame) => frame.resume_done_into::(), - TracingFrame::ScfFor(frame) => frame.resume_done_into::(), - } + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + self.0.resume_done_into(interp) } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingFrame::Body(frame) => frame.resume_into::(completion, interp), - TracingFrame::Call(frame) => frame.resume_into::(completion, interp), - TracingFrame::ScfIf(frame) => frame.resume_into::(completion), - TracingFrame::ScfFor(frame) => frame.resume_into::(completion, interp), - } + ) -> Result>, E> { + self.0.resume_into(completion, interp) } } @@ -699,15 +694,16 @@ mod advanced { .unwrap(); // (1)+(2): the custom frame ran the real program correctly by reusing - // the standard BodyFrame/CallFrame traversal (no engine fork). + // the standard walker/CallFrame traversal (no engine fork). assert_eq!(result, 120); // (3): traversal is observable through the custom frame. factorial(5) - // makes 4 recursive calls (5→4→3→2→1; the base case at 1 makes none), - // all routed through the custom Call arm; body statements run through - // its Body arm. + // is 5 activations: the root call plus 4 recursive calls (5→4→3→2→1; + // the base case at 1 makes none) — every call, root included, is one + // `CallFrame` routed through the custom Call arm; body statements run + // through its Block/CFG arms. let trace = TRACE.with(|t| *t.borrow()); - assert_eq!(trace.calls, 4); + assert_eq!(trace.calls, 5); assert!(trace.body_steps > 0); } @@ -722,7 +718,7 @@ mod advanced { let pipeline = build_pipeline(include_str!("../../programs/factorial.kirin")); let mut analysis = crate::interpreter::ToyConstProp::new(&pipeline) .with_policy(ConstPropContext::with_budget(2)) - .with_linker(CrossStageLinker); + .with_linker(SameStageLinker); let result = expect_single::( analysis .analyze_by_name("source", "factorial", [ConstPropValue::Const(5)]) @@ -757,91 +753,65 @@ mod advanced { calls: usize, } - enum TracingAbstractFrame { - Block(AbstractBlockFrame), - Call(AbstractCallFrame), - ScfIf(AbstractScfIfFrame), - ScfFor(AbstractScfForFrame), - } + /// The abstract analogue of [`TracingFrame`]: a wrapper universe embedding + /// `ToyAbstractFrame` whole rather than re-listing its variants. + struct TracingAbstractFrame(ToyAbstractFrame); impl AbstractFrameBuild for TracingAbstractFrame { fn from_block(frame: AbstractBlockFrame) -> Self { - TracingAbstractFrame::Block(frame) + Self(ToyAbstractFrame::Block(frame)) } fn from_call(frame: AbstractCallFrame) -> Self { - TracingAbstractFrame::Call(frame) + Self(ToyAbstractFrame::Call(frame)) } } impl BuildAbstractScfIf for TracingAbstractFrame { fn scf_if(frame: AbstractScfIfFrame) -> Self { - TracingAbstractFrame::ScfIf(frame) + Self(ToyAbstractFrame::ScfIf(frame)) } } impl BuildAbstractScfFor for TracingAbstractFrame { fn scf_for(frame: AbstractScfForFrame) -> Self { - TracingAbstractFrame::ScfFor(frame) + Self(ToyAbstractFrame::ScfFor(frame)) } } - impl Frame for TracingAbstractFrame + impl Frame for TracingAbstractFrame where - I: AbstractFrameDriver - + SparseForwardInterp>, - V: Clone + PartialEq + ForLoopValue, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, + F: AbstractFrameBuild + BuildAbstractScfIf + BuildAbstractScfFor, + V: Clone + PartialEq + ForLoopValue + Lattice, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { - match self { - TracingAbstractFrame::Block(frame) => { - ATRACE.with(|t| t.borrow_mut().block_steps += 1); - frame.step_into::(interp) - } - TracingAbstractFrame::Call(frame) => { - ATRACE.with(|t| t.borrow_mut().calls += 1); - frame.step_into::(interp) - } - TracingAbstractFrame::ScfIf(frame) => { - ATRACE.with(|t| t.borrow_mut().if_steps += 1); - frame.step_into::(interp) - } - TracingAbstractFrame::ScfFor(frame) => frame.step_into::(interp), + fn step_into(self, interp: &mut I) -> Result>, E> { + match &self.0 { + ToyAbstractFrame::Block(_) => ATRACE.with(|t| t.borrow_mut().block_steps += 1), + ToyAbstractFrame::Call(_) => ATRACE.with(|t| t.borrow_mut().calls += 1), + ToyAbstractFrame::ScfIf(_) => ATRACE.with(|t| t.borrow_mut().if_steps += 1), + ToyAbstractFrame::ScfFor(_) => {} } + self.0.step_into(interp) } - fn resume_done( + fn resume_done_into( self, - _interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - TracingAbstractFrame::Call(frame) => frame.resume_done_into::(), - TracingAbstractFrame::ScfIf(frame) => frame.resume_done_into::(), - TracingAbstractFrame::ScfFor(frame) => frame.resume_done_into::(), - } + interp: &mut I, + ) -> Result>, E> { + self.0.resume_done_into(interp) } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingAbstractFrame::Block(frame) => { - frame.resume_into::(completion, interp) - } - TracingAbstractFrame::Call(frame) => frame.resume_into::(completion), - TracingAbstractFrame::ScfIf(frame) => { - frame.resume_into::(completion, interp) - } - TracingAbstractFrame::ScfFor(frame) => { - frame.resume_into::(completion, interp) - } - } + ) -> Result>, E> { + self.0.resume_into(completion, interp) } } @@ -896,11 +866,15 @@ mod advanced { // =========================================================================== mod demand { + use std::collections::HashSet; + use kirin::prelude::{ - CFG, CompileStage, GetInfo, HasCFGBody, HasResults, ParsePipelineText, Pipeline, SSAValue, + CFG, CompileStage, GetInfo, HasBlocks, HasCFG, HasCFGBody, HasDigraphs, HasResults, + HasUngraphs, ParsePipelineText, Pipeline, SSAValue, Statement, }; use kirin_arith::{Arith, ArithValue}; use kirin_function::Lexical; + use kirin_interpreter::Body; use kirin_liveness::analyze_demand; use crate::language::HighLevel; @@ -946,29 +920,85 @@ mod demand { .collect() } - /// Find something by matching statement definitions anywhere in the - /// cfg (including scf bodies, via the topology's nested-block - /// enumeration). - pub(super) fn find_value( + /// Find something by walking statement definitions anywhere in the CFG, + /// including bodies nested under statements. + fn find_in_body( pipeline: &Pipeline, cfg: CFG, - select: impl Fn(&HighLevel) -> Option, + mut select: impl FnMut(Statement, &HighLevel) -> Option, ) -> R { let stage_id = pipeline.stage_by_name("source").expect("source stage"); let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let topology = kirin_interpreter::cfg_topology(info, &cfg); - for block in &topology.blocks { - for &stmt in &block.stmts { - if let Some(value) = select(stmt.definition(info)) { + + let mut bodies = vec![Body::CFG(cfg)]; + let mut visited = HashSet::new(); + while let Some(body) = bodies.pop() { + if !visited.insert(body) { + continue; + } + + let statements: Vec = match body { + Body::CFG(cfg) => { + bodies.extend(cfg.blocks(info).map(Body::Block)); + continue; + } + Body::Block(block) => { + let mut statements: Vec = block.statements(info).collect(); + if let Some(terminator) = block.terminator(info) { + statements.push(terminator); + } + statements + } + Body::DiGraph(graph) => graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + Body::UnGraph(graph) => graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + }; + + for statement in statements { + let definition = statement.definition(info); + if let Some(value) = select(statement, definition) { return value; } + bodies.extend(definition.blocks().copied().map(Body::Block)); + bodies.extend(definition.cfgs().copied().map(Body::CFG)); + bodies.extend(definition.digraphs().copied().map(Body::DiGraph)); + bodies.extend(definition.ungraphs().copied().map(Body::UnGraph)); } } panic!("no matching statement in cfg"); } + pub(super) fn find_value( + pipeline: &Pipeline, + cfg: CFG, + select: impl FnMut(&HighLevel) -> Option, + ) -> R { + let mut select = select; + find_in_body(pipeline, cfg, |_, definition| select(definition)) + } + + pub(super) fn find_statement( + pipeline: &Pipeline, + cfg: CFG, + select: impl FnMut(&HighLevel) -> bool, + ) -> Statement { + let mut select = select; + find_in_body(pipeline, cfg, |statement, definition| { + select(definition).then_some(statement) + }) + } + /// The result of the `constant -> i64` statement. pub(super) fn constant_result(pipeline: &Pipeline, cfg: CFG, value: i64) -> SSAValue { find_value(pipeline, cfg, |definition| match definition { @@ -1235,69 +1265,55 @@ specialize @source fn @main(i64, i64) -> i64 { } // =========================================================================== -// Classic (dense, per-point) liveness through scf's dialect-owned dense -// frames: arm-join for `scf.if`, the loop-carried fixpoint for `scf.for`, and -// per-point reconstruction inside structured bodies. +// Classic (dense, per-point) liveness through the toy language's total dense +// frame: arm-join for `scf.if`, the loop-carried fixpoint for `scf.for`, and +// per-point reconstruction inside structured bodies. Runs on the finalized IR +// alone — no demand pre-pass. // =========================================================================== mod dense { - use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue, Statement}; + use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue}; use kirin_arith::{Arith, ArithValue}; - use kirin_liveness::{DenseLivenessResult, LiveSet, analyze_demand}; + use kirin_interpreter::{Body, InterpreterError, ProgramPoint, Scoped}; + use kirin_liveness::{DenseLivenessResult, LiveSet}; + use kirin_scf::StructuredControlFlow; use super::demand::{FOR_CARRIED_DEMAND, IF_DEAD_RESULT}; - use super::demand::{constant_result, entry_params, find_value, parse, source_cfg}; - use crate::interpreter::ToyDenseLiveness; + use super::demand::{ + constant_result, entry_params, find_statement, find_value, parse, source_cfg, + }; + use crate::interpreter::ToyDenseBackwardFrame; use crate::language::HighLevel; use crate::stage::Stage; - /// Run classic dense liveness with the toy total frame (scf frames - /// embedded). + /// Run classic dense liveness with the toy total frame. fn analyze_dense_toy( pipeline: &Pipeline, stage: CompileStage, cfg: CFG, ) -> DenseLivenessResult { - let mut engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - engine.analyze(stage, cfg).expect("analysis succeeds"); - DenseLivenessResult::from_engine(&mut engine, stage, cfg).expect("reconstruction succeeds") - } - - /// The statement whose definition matches `select` (anywhere in the - /// cfg, including scf bodies). - fn find_statement( - pipeline: &Pipeline, - cfg: CFG, - select: impl Fn(&HighLevel) -> bool, - ) -> Statement { - let stage_id = pipeline.stage_by_name("source").expect("source stage"); - let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { - panic!("source stage holds HighLevel"); - }; - let topology = kirin_interpreter::cfg_topology(info, &cfg); - for block in &topology.blocks { - for &stmt in &block.stmts { - if select(stmt.definition(info)) { - return stmt; - } - } - } - panic!("no matching statement in cfg"); + kirin_liveness::analyze_dense_with_frame::< + _, + ToyDenseBackwardFrame, + >(pipeline, stage, cfg) + .expect("analysis succeeds") } fn live_set(values: &[SSAValue]) -> LiveSet { values.iter().copied().collect() } - /// Per-point sets inside an `scf.if` arm follow classic semantics (the - /// yield's operand is live after its def even though the result is dead), - /// and intersecting with the demand set recovers the strong view. + /// Per-point sets inside an `scf.if` arm follow classic semantics: the + /// yield's operand is live after its def even though the result is dead. + /// (The strong view is the `dense ∩ demanded` composition, covered in + /// kirin-liveness — no demand pass runs here.) #[test] fn dense_per_point_inside_scf_if_arm() { let pipeline = parse(IF_DEAD_RESULT); let (stage, cfg) = source_cfg(&pipeline, "if_dead"); let dense = analyze_dense_toy(&pipeline, stage, cfg); - let demand = analyze_demand(&pipeline, stage, cfg).expect("demand succeeds"); + let scope = (stage, Body::CFG(cfg)); + let point = |item| Scoped::new(scope, item); let cond = entry_params(&pipeline, cfg)[0]; let a = constant_result(&pipeline, cfg, 1); @@ -1308,23 +1324,87 @@ mod dense { // Classic: after `%a = constant 1`, %a is live (the yield uses it) // and %cond flows through the arm; before it, %a is killed. - assert_eq!(dense.live_after(a_const), Some(&live_set(&[cond, a]))); - assert_eq!(dense.live_before(a_const), Some(&live_set(&[cond]))); + assert_eq!( + dense.point_facts(point(ProgramPoint::After(a_const))), + Some(&live_set(&[cond, a])) + ); + assert_eq!( + dense.point_facts(point(ProgramPoint::Before(a_const))), + Some(&live_set(&[cond])) + ); + + // The if's dead result is not live after it because nothing uses it; + // before it, only the condition survives the arm join. + let if_stmt = find_statement(&pipeline, cfg, |definition| { + matches!(definition, HighLevel::Structured(_)) + }); + assert_eq!( + dense.point_facts(point(ProgramPoint::Before(if_stmt))), + Some(&live_set(&[cond])) + ); + + let then_block = find_value(&pipeline, cfg, |definition| match definition { + HighLevel::Structured(StructuredControlFlow::If(if_op)) => Some(if_op.then_block()), + _ => None, + }); + assert_eq!( + dense.point_facts(point(ProgramPoint::BlockEntry(then_block))), + Some(&live_set(&[cond])) + ); + assert_eq!( + dense.point_facts(point(ProgramPoint::BlockExit(then_block))), + Some(&live_set(&[cond])) + ); + } - // The if's own points: its dead result is live after it (classic - // records what the walk saw: nothing uses it, so it is NOT live), and - // before it only the condition survives the arm join. + const IF_ARMS_DIFFERENT_USES: &str = r#" +stage @source fn @if_arms(i64, i64, i64) -> i64; + +specialize @source fn @if_arms(i64, i64, i64) -> i64 { + ^entry(%cond: i64, %x: i64, %y: i64) { + %r = if %cond then ^then() { + yield %x; + } else ^else() { + yield %y; + } -> i64; + ret %r; + } +} +"#; + + /// The scf.if liveness frame walks BOTH arms backward and joins their + /// live-entry states: the arms use different SSA values (%x vs %y), so + /// the state before the `if` must contain the condition and both. + #[test] + fn dense_scf_if_joins_both_arm_entries() { + let pipeline = parse(IF_ARMS_DIFFERENT_USES); + let (stage, cfg) = source_cfg(&pipeline, "if_arms"); + let dense = analyze_dense_toy(&pipeline, stage, cfg); + let scope = (stage, Body::CFG(cfg)); + + let params = entry_params(&pipeline, cfg); + let (cond, x, y) = (params[0], params[1], params[2]); let if_stmt = find_statement(&pipeline, cfg, |definition| { matches!(definition, HighLevel::Structured(_)) }); - assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond]))); - - // Strong per-point view: %a is classically live after its def but not - // demanded (the if result is dead), so the composition drops it. - let strong = dense - .strong_live_after(a_const, &demand) - .expect("point reconstructed"); - assert_eq!(strong, live_set(&[cond])); + let r = find_value(&pipeline, cfg, |definition| match definition { + HighLevel::Structured(_) => { + use kirin::prelude::HasResults; + definition.results().next().map(|v| SSAValue::from(*v)) + } + _ => None, + }); + + // After the if only its result matters; before it, the then-arm + // contributed %x, the else-arm %y, and the rule genned %cond. + assert_eq!( + dense.point_facts(Scoped::new(scope, ProgramPoint::After(if_stmt))), + Some(&live_set(&[r])) + ); + assert_eq!( + dense.point_facts(Scoped::new(scope, ProgramPoint::Before(if_stmt))), + Some(&live_set(&[cond, x, y])) + ); } /// The scf.for dense frame iterates the body walk to the loop-carried @@ -1336,6 +1416,7 @@ mod dense { let pipeline = parse(FOR_CARRIED_DEMAND); let (stage, cfg) = source_cfg(&pipeline, "loop_sum"); let dense = analyze_dense_toy(&pipeline, stage, cfg); + let scope = (stage, Body::CFG(cfg)); let params = entry_params(&pipeline, cfg); let (lo, hi, step) = (params[0], params[1], params[2]); @@ -1362,9 +1443,12 @@ mod dense { }); // Around the loop. - assert_eq!(dense.live_after(for_stmt), Some(&live_set(&[sum]))); assert_eq!( - dense.live_before(for_stmt), + dense.point_facts(Scoped::new(scope, ProgramPoint::After(for_stmt))), + Some(&live_set(&[sum])) + ); + assert_eq!( + dense.point_facts(Scoped::new(scope, ProgramPoint::Before(for_stmt))), Some(&live_set(&[lo, hi, step, init])) ); @@ -1372,11 +1456,11 @@ mod dense { // constant are live before the add; the yield slot is live after it // (it feeds the next iteration through the carry). assert_eq!( - dense.live_before(add_stmt), + dense.point_facts(Scoped::new(scope, ProgramPoint::Before(add_stmt))), Some(&live_set(&[lo, hi, step, init, acc, one])) ); assert_eq!( - dense.live_after(add_stmt), + dense.point_facts(Scoped::new(scope, ProgramPoint::After(add_stmt))), Some(&live_set(&[lo, hi, step, init, next])) ); } diff --git a/example/toy-lang/src/main.rs b/example/toy-lang/src/main.rs index 069a4631de..7c29f843d6 100644 --- a/example/toy-lang/src/main.rs +++ b/example/toy-lang/src/main.rs @@ -5,6 +5,7 @@ mod stage; use clap::{Parser, Subcommand}; use kirin::prelude::*; use kirin::pretty::PipelinePrintExt; +use kirin_interpreter::{Body, ProgramPoint, Scoped}; use stage::Stage; @@ -41,8 +42,8 @@ enum Command { /// Run constant propagation instead of concrete execution. #[arg(long)] constprop: bool, - /// Run liveness analysis (strong demand + classic per-point) instead - /// of concrete execution. + /// Run classic per-program-point liveness analysis (dense backward) + /// instead of concrete execution. #[arg(long)] liveness: bool, /// Restrict execution to the entry stage's language; reject calls @@ -108,11 +109,25 @@ fn run_program( } if liveness { - let (demand, dense) = interpreter::analyze_liveness(&pipeline, stage_name, func_name)?; - println!("demanded: {:?}", demand.demanded()); - let mut boundaries: Vec<_> = dense - .blocks() - .map(|(block, live_in, live_out)| format!("{block:?}: in={live_in:?} out={live_out:?}")) + let (stage, cfg, dense) = + interpreter::analyze_classic_liveness(&pipeline, stage_name, func_name)?; + let blocks: Vec<_> = match pipeline + .stage(stage) + .ok_or_else(|| anyhow::anyhow!("resolved stage is missing"))? + { + Stage::Source(info) => cfg.blocks(info).collect(), + Stage::Lowered(info) => cfg.blocks(info).collect(), + }; + let scope = (stage, Body::CFG(cfg)); + let mut boundaries: Vec<_> = blocks + .into_iter() + .filter_map(|block| { + let live_in = + dense.point_facts(Scoped::new(scope, ProgramPoint::BlockEntry(block)))?; + let live_out = + dense.point_facts(Scoped::new(scope, ProgramPoint::BlockExit(block)))?; + Some(format!("{block:?}: in={live_in:?} out={live_out:?}")) + }) .collect(); boundaries.sort(); for line in boundaries { diff --git a/example/toy-lang/tests/e2e.rs b/example/toy-lang/tests/e2e.rs index 4fc3f9373f..2156364027 100644 --- a/example/toy-lang/tests/e2e.rs +++ b/example/toy-lang/tests/e2e.rs @@ -177,3 +177,28 @@ fn test_run_missing_stage() { .assert() .failure(); } + +#[test] +fn test_liveness_prints_dense_sets_without_demand() { + // `--liveness` runs exactly one analysis: classic dense-backward + // per-point liveness. It prints block boundary sets and must NOT run or + // print the independent strong-demand analysis. + toy_lang() + .args([ + "run", + "programs/branching.kirin", + "--stage", + "source", + "--function", + "abs", + "--liveness", + ]) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .assert() + .success() + .stdout( + predicate::str::contains("in=") + .and(predicate::str::contains("out=")) + .and(predicate::str::contains("demanded:").not()), + ); +} diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs new file mode 100644 index 0000000000..2d99523ec1 --- /dev/null +++ b/tests/body_kinds.rs @@ -0,0 +1,1870 @@ +//! Acceptance tests for generic interpreter bodies (issue #667). +//! +//! Two independent axes organize concrete traversal: +//! +//! - **Body representation** — the closed `Body` vocabulary: `CFG`, `Block`, +//! `DiGraph`, `UnGraph`. Each framework-walkable representation has one +//! walker (`CFGFrame`/`BlockFrame`/`DiGraphFrame`); `UnGraph` traversal is +//! a compiler-supplied policy (`FrameBuild::from_ungraph_entry`). +//! - **Entry context** — callable (entered through `CallFrame`, which owns +//! the callee activation) vs. nested (entered through a dialect frame +//! pushed with `SparseForwardEffect::Push`, borrowing the current +//! activation). +//! +//! The tests cover the composition matrix: callable CFG/Block/DiGraph bodies +//! (`CallFrame` → walker), nested DiGraph and scf Blocks (dialect frame → +//! walker), returns bubbling through dialect frames to the nearest +//! `CallFrame`, and the callable-UnGraph policy hook (with and without a +//! policy). +//! +//! The later sections run the *forward dataflow* engine +//! (`SparseForwardInterpreter`, instantiated at the constant-propagation +//! lattice) over the same body vocabulary: `CFG`, `Block` and `DiGraph` +//! callable bodies analyze — the last as an `Owner::Graph` walked by +//! `AbstractDiGraphFrame` — while `UnGraph` bodies and *nested* graph bodies +//! are asserted to be refused. Those sections also pin the graph owner's +//! interprocedural behaviour: calls *inside* a graph body are summarized rather +//! than descended into (so self-recursion converges), entry arguments join and +//! re-run the owner when several call sites share one key, and a directed cycle +//! is rejected identically by both engines. + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::hash::Hash; + +use kirin::prelude::*; +use kirin_arith::{ + Arith, ArithConversionError, ArithType, ArithValue, interpreter::DivisionByZero, +}; +use kirin_cmp::Cmp; +use kirin_constant::Constant; +use kirin_constprop::{ConstPropContext, ConstPropValue}; +use kirin_function::Lexical; +use kirin_interpreter::{ + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, BlockFrame, Body, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, + CallContext, CallFrame, Completion, ConcreteInterpreter, ContextInsensitive, DefaultBodyFrames, + DiGraphFrame, Env, EnvIndex, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, FrameBuild, + FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, + SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, StandardFrame, + StatementDispatch, UnGraphEntry, expect_single, +}; +use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; +use kirin_test_languages::GraphFunctionLanguage; + +/// Total error for the test engines: the framework error plus the value +/// conversion/trap errors the languages' rules can raise. +#[derive(Debug)] +enum TestError { + Core(InterpreterError), + ArithConversion(ArithConversionError), + DivisionByZero, +} + +impl From for TestError { + fn from(error: InterpreterError) -> Self { + Self::Core(error) + } +} +impl From for TestError { + fn from(error: ArithConversionError) -> Self { + Self::ArithConversion(error) + } +} +impl From for TestError { + fn from(_: DivisionByZero) -> Self { + Self::DivisionByZero + } +} +// `ConstPropValue: From` is infallible, so the abstract engine's +// `TryFrom` conversion error is uninhabited. +impl From for TestError { + fn from(never: std::convert::Infallible) -> Self { + match never {} + } +} + +type L = StageInfo; +type Engine<'ir> = + ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, StandardFrame>; + +fn parse(program: &str) -> Pipeline { + let mut pipeline: Pipeline = Pipeline::new(); + ParsePipelineText::parse(&mut pipeline, program).expect("program parses"); + pipeline +} + +fn run(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + expect_single(run_product(pipeline, function, args)?) +} + +/// `run`, keeping the whole returned product — for callables that return more +/// than one value. +fn run_product( + pipeline: &Pipeline, + function: &str, + args: &[i64], +) -> Result, TestError> { + let mut interp: Engine<'_> = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + interp.call_by_name("test", function, args.iter().copied()) +} + +// =========================================================================== +// 1. Callable DiGraph: caller → CallFrame → DiGraphFrame. +// =========================================================================== + +/// A CFG-bodied `main` calls a DiGraph-bodied callable. The `call.named` +/// statement produces a `Call` effect; the resulting `CallFrame` resolves +/// the callee, allocates its activation, and — because the callable body is +/// `Body::DiGraph` — enters a `DiGraphFrame`. The graph walks its arith +/// nodes in dependency order and completes `Finished` with its declared +/// yields, which the `CallFrame` accepts as the call's returned values and +/// writes into `main`'s result slots. +const DIGRAPH_CALLABLE_PROGRAM: &str = r#" +stage @test fn @gadd(i64, i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @gadd(i64, i64) -> i64 digraph ^g0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + yield %s; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 2 -> i64; + %b = constant 3 -> i64; + %r = call.named @gadd(%a, %b) -> i64; + ret %r; + } +} +"#; + +#[test] +fn cfg_main_calls_digraph_function() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 5); +} + +// =========================================================================== +// 2. Nested/pushed DiGraph: dialect operation → DiGraphFrame. +// =========================================================================== + +/// A statement inside a CFG block owns a DiGraph body and enters it with +/// `SparseForwardEffect::Push` — the same way `scf.if` enters its Block +/// arms. Unlike test 1 there is **no** `CallFrame` in the chain: no callee +/// resolution happens, no function activation is allocated or freed — the +/// pushed `DiGraphFrame` runs in the *pusher's* activation, and its +/// `Finished` yields land in the pushing statement's `Push` result slots +/// rather than in call-return slots. +const NESTED_DIGRAPH_PROGRAM: &str = r#" +stage @test fn @main() -> i64; + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 20 -> i64; + %b = constant 22 -> i64; + %r = graph_eval %a, %b digraph ^g0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + yield %s; + } -> i64; + ret %r; + } +} +"#; + +#[test] +fn cfg_statement_pushes_digraph_body() { + let pipeline = parse(NESTED_DIGRAPH_PROGRAM); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); +} + +// =========================================================================== +// 3. Callable Block: caller → CallFrame → BlockFrame. +// =========================================================================== + +/// A linear (single-Block) callable: the flat-instruction-list function +/// shape. `Body::Block` maps to the same +/// `BlockFrame` that walks nested structured blocks — there is no separate +/// "linear function frame"; the `CallFrame` parent is what makes this walk a +/// function body. The block exits with `Return`, which the `CallFrame` +/// validates and consumes. +const BLOCK_CALLABLE_PROGRAM: &str = r#" +stage @test fn @ladd(i64, i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @ladd(i64, i64) -> i64 ^body(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + ret %s; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 40 -> i64; + %b = constant 2 -> i64; + %r = call.named @ladd(%a, %b) -> i64; + ret %r; + } +} +"#; + +#[test] +fn linear_block_callable() { + let pipeline = parse(BLOCK_CALLABLE_PROGRAM); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); +} + +// =========================================================================== +// 4. DiGraph dependency order. +// =========================================================================== + +/// Graph nodes run in dependency order, not declaration order. The graph +/// branches visibly: one input port feeds two independent producers declared +/// *after* their consumer, whose results merge into the yielded node — so a +/// textual/linear walk would read unbound operands. +/// +/// ```text +/// %x ──┬─▶ %c = add %x, %x ──┐ +/// │ ├─▶ %d = mul %c, %e ──▶ yield +/// └─▶ %e = add %x, %one ┘ +/// ``` +const DIGRAPH_TOPO_PROGRAM: &str = r#" +stage @test fn @g(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @g(i64) -> i64 digraph ^g0(%x: i64) { + %d = mul %c, %e -> i64; + %c = add %x, %x -> i64; + %e = add %x, %one -> i64; + %one = constant 1 -> i64; + yield %d; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 3 -> i64; + %r = call.named @g(%a) -> i64; + ret %r; + } +} +"#; + +#[test] +fn digraph_runs_in_topological_order() { + let pipeline = parse(DIGRAPH_TOPO_PROGRAM); + // (3 + 3) * (3 + 1) = 24 — requires running both `add`s (and the + // constant) before the `mul` despite the textual order. + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 24); +} + +// =========================================================================== +// An scf-composed language for tests 5 and 6: structured operations enter +// nested Blocks through dialect frames (ScfIfFrame/ScfForFrame → BlockFrame). +// =========================================================================== + +/// Inline language wrapping functions (CFG bodies), scf, and arithmetic. +/// Specific to this integration suite; shared test dialects live in +/// `kirin-test-languages`. +#[derive( + Debug, Clone, PartialEq, Eq, Hash, Dialect, FunctionEntry, HasParser, PrettyPrint, Interpretable, +)] +#[kirin(builders, type = ArithType)] +enum ScfLanguage { + #[wraps] + #[callable] + Lexical(Lexical), + #[wraps] + Structured(StructuredControlFlow), + #[wraps] + Constant(Constant), + #[wraps] + Arith(Arith), + #[wraps] + Cmp(Cmp), +} + +/// Total frame enum for the scf tests: the standard representation walkers +/// and call boundary plus the dialect-owned SCF frames (composition, not an +/// engine fork). +#[derive(FrameBuild)] +enum ScfTestFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + ScfIf(ScfIfFrame), + ScfFor(ScfForFrame), +} + +impl BuildScfIf for ScfTestFrame { + fn scf_if(frame: ScfIfFrame) -> Self { + ScfTestFrame::ScfIf(frame) + } +} + +impl BuildScfFor for ScfTestFrame { + fn scf_for(frame: ScfForFrame) -> Self { + ScfTestFrame::ScfFor(frame) + } +} + +impl Frame for ScfTestFrame +where + I: ForwardFrameEngine + SparseForwardInterp, + F: FrameBuild + BuildScfIf + BuildScfFor, + V: Clone + kirin_scf::ForLoopValue, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { + match self { + ScfTestFrame::Block(frame) => frame.step_into(interp), + ScfTestFrame::CFG(frame) => frame.step_into(interp), + ScfTestFrame::Call(frame) => frame.step_into(interp), + ScfTestFrame::DiGraph(frame) => frame.step_into(interp), + ScfTestFrame::ScfIf(frame) => frame.step_into(interp), + ScfTestFrame::ScfFor(frame) => frame.step_into(interp), + } + } + + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + match self { + ScfTestFrame::Block(frame) => frame.resume_done_into(interp), + ScfTestFrame::CFG(frame) => frame.resume_done_into(interp), + ScfTestFrame::Call(frame) => frame.resume_done_into(interp), + ScfTestFrame::DiGraph(frame) => frame.resume_done_into(interp), + ScfTestFrame::ScfIf(frame) => frame.resume_done_into(interp), + ScfTestFrame::ScfFor(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match self { + ScfTestFrame::Block(frame) => frame.resume_into(completion, interp), + ScfTestFrame::CFG(frame) => frame.resume_into(completion, interp), + ScfTestFrame::Call(frame) => frame.resume_into(completion, interp), + ScfTestFrame::DiGraph(frame) => frame.resume_into(completion, interp), + ScfTestFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ScfTestFrame::ScfFor(frame) => frame.resume_into(completion, interp), + } + } +} + +type ScfL = StageInfo; +type ScfEngine<'ir> = + ConcreteInterpreter<'ir, ScfL, i64, TestError, SameStageLinker, ScfTestFrame>; + +fn parse_scf(program: &str) -> Pipeline { + let mut pipeline: Pipeline = Pipeline::new(); + ParsePipelineText::parse(&mut pipeline, program).expect("program parses"); + pipeline +} + +fn run_scf(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + let mut interp: ScfEngine<'_> = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +// =========================================================================== +// 5. Nested structured Block: ScfIfFrame/ScfForFrame → BlockFrame. +// =========================================================================== + +/// `scf.if` picks the decided arm and pushes the framework `BlockFrame` for +/// it; the arm's `yield` surfaces as `Completion::Yielded`, which the +/// `ScfIfFrame` consumes and hands to the pusher as the operation's results. +const SCF_ABS_PROGRAM: &str = r#" +stage @test fn @abs(i64) -> i64; + +specialize @test fn @abs(i64) -> i64 { + ^entry(%x: i64) { + %zero = constant 0 -> i64; + %is_neg = lt %x, %zero -> i64; + %result = if %is_neg then ^then() { + %negated = neg %x -> i64; + yield %negated; + } else ^else() { + yield %x; + } -> i64; + ret %result; + } +} +"#; + +#[test] +fn scf_if_arm_yields_to_dialect_frame() { + let pipeline = parse_scf(SCF_ABS_PROGRAM); + assert_eq!(run_scf(&pipeline, "abs", &[-7]).unwrap(), 7); + assert_eq!(run_scf(&pipeline, "abs", &[4]).unwrap(), 4); +} + +/// `scf.for` re-pushes the framework `BlockFrame` per iteration; each +/// `Completion::Yielded` carries the loop-carried values into the next turn, +/// and loop exit completes `Finished` to the pusher. +#[test] +fn scf_for_loop_carries_yielded_values() { + let pipeline = parse_scf( + r#" +stage @test fn @sum_below(i64) -> i64; + +specialize @test fn @sum_below(i64) -> i64 { + ^entry(%n: i64) { + %zero = constant 0 -> i64; + %one = constant 1 -> i64; + %sum = for %zero in %zero..%n step %one iter_args(%zero) do ^body(%i: i64, %acc: i64) { + %next = add %acc, %i -> i64; + yield %next; + } -> i64; + ret %sum; + } +} +"#, + ); + // 0 + 1 + 2 + 3 + 4 = 10. + assert_eq!(run_scf(&pipeline, "sum_below", &[5]).unwrap(), 10); + // Zero iterations: the initial carried value flows through. + assert_eq!(run_scf(&pipeline, "sum_below", &[0]).unwrap(), 0); +} + +// =========================================================================== +// 6. Return through nested structured control. +// =========================================================================== + +/// A function `Return` inside an `scf.if` arm: the arm's `BlockFrame` +/// completes `Returned`, the `ScfIfFrame` relays it (it is not a call +/// boundary), the function's `CFGFrame` relays it too, and the nearest +/// `CallFrame` consumes it — freeing the callee activation exactly once and +/// writing the caller's result slots. Execution must not continue after the +/// return: the statements below the `if` never run on the early-return path. +#[test] +fn return_bubbles_through_scf_frames_to_call_frame() { + let pipeline = parse_scf( + r#" +stage @test fn @clamp0(i64) -> i64; +stage @test fn @twice(i64) -> i64; + +specialize @test fn @clamp0(i64) -> i64 { + ^entry(%x: i64) { + %zero = constant 0 -> i64; + %is_neg = lt %x, %zero -> i64; + %kept = if %is_neg then ^then() { + ret %zero; + } else ^else() { + yield %x; + } -> i64; + %one = constant 1 -> i64; + %r = add %kept, %one -> i64; + ret %r; + } +} + +specialize @test fn @twice(i64) -> i64 { + ^entry(%x: i64) { + %a = call.named @clamp0(%x) -> i64; + %b = call.named @clamp0(%x) -> i64; + %s = add %a, %b -> i64; + ret %s; + } +} +"#, + ); + // Early return: 0, not 0 + 1 — the add after the `if` did not run. + assert_eq!(run_scf(&pipeline, "clamp0", &[-5]).unwrap(), 0); + // Normal path: the arm yields, execution continues after the `if`. + assert_eq!(run_scf(&pipeline, "clamp0", &[5]).unwrap(), 6); + // Two nested calls taking the early-return path in one run: each callee + // activation is freed exactly once and the caller's activation survives, + // otherwise the second call (or the final add) would read freed state. + assert_eq!(run_scf(&pipeline, "twice", &[-5]).unwrap(), 0); + assert_eq!(run_scf(&pipeline, "twice", &[5]).unwrap(), 12); +} + +// =========================================================================== +// 7. Custom callable-UnGraph policy: caller → CallFrame → policy frame. +// =========================================================================== + +// The framework refuses to invent an execution order for an undirected +// graph, so the *compiler* supplies one by overriding +// `FrameBuild::from_ungraph_entry` on its total frame type. This policy +// interprets an ungraph as a sequential dataflow chain: +// +// - **scheduling**: node statements run in the graph's canonical node +// enumeration order (an explicit policy choice — the generic `CallFrame` +// never orders anything); +// - **outputs**: the call returns the result values of the *last* scheduled +// node (an ungraph has no framework output convention such as a digraph's +// declared yields, so the policy defines one). + +/// Total frame enum for the UnGraph-policy engine: the standard frames plus +/// the policy's own walker. Only `from_ungraph_entry` differs from the +/// default composition — the generic traversal logic is untouched. +enum UnPolicyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + Chain(UnGraphChainFrame), +} + +impl FrameBuild for UnPolicyFrame { + type BodyFrames = DefaultBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + UnPolicyFrame::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + UnPolicyFrame::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + UnPolicyFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + UnPolicyFrame::DiGraph(frame) + } + fn from_ungraph_entry(entry: UnGraphEntry) -> Result { + Ok(UnPolicyFrame::Chain(UnGraphChainFrame::new(entry))) + } +} + +/// The compiler-owned callable-UnGraph walker (the policy itself). +struct UnGraphChainFrame { + stage: CompileStage, + /// The callee activation — owned and freed by the awaiting `CallFrame`, + /// merely used here. + index: EnvIndex, + graph: UnGraph, + /// Entry arguments awaiting the boundary-port binding on the first step. + pending: Option>, + /// The policy's schedule (canonical node enumeration order). + schedule: VecDeque, + /// The policy's output convention: the last scheduled node's results. + outputs: Vec, +} + +impl UnGraphChainFrame { + fn new(entry: UnGraphEntry) -> Self { + Self { + stage: entry.stage, + index: entry.index, + graph: entry.graph, + pending: Some(entry.args), + schedule: VecDeque::new(), + outputs: Vec::new(), + } + } +} + +impl<'ir> Frame, UnPolicyFrame> for UnGraphChainFrame { + type Completion = Completion; + + fn step_into( + mut self, + interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + // First step: bind the boundary ports and fix the policy's schedule + // and output convention from the graph's structure. + if let Some(args) = self.pending.take() { + let info = interp + .pipeline() + .stage(self.stage) + .ok_or(InterpreterError::MissingStage(self.stage))?; + let graph_info = self + .graph + .get_info(info) + .ok_or(InterpreterError::Custom("ungraph has no info"))?; + if graph_info.ports().len() != args.len() { + return Err(TestError::Core(InterpreterError::ProductArityMismatch { + expected: graph_info.ports().len(), + actual: args.len(), + })); + } + let ports: Vec<_> = graph_info.ports().to_vec(); + let nodes: Vec = graph_info.graph().node_weights().copied().collect(); + let last = *nodes + .last() + .ok_or(InterpreterError::Custom("empty ungraph body"))?; + self.outputs = last + .definition(info) + .results() + .map(|result| SSAValue::from(*result)) + .collect(); + self.schedule = nodes.into(); + for (port, value) in ports.into_iter().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + return Ok(FrameEffect::Continue(UnPolicyFrame::Chain(self))); + } + + match self.schedule.pop_front() { + Some(statement) => match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(UnPolicyFrame::Chain(self))), + _ => Err(TestError::Core(InterpreterError::Custom( + "the chain policy supports only ordinary dataflow nodes", + ))), + }, + // Natural completion: the policy's outputs become the call's + // returned values (the awaiting CallFrame accepts `Finished`). + None => { + let values: Product = self + .outputs + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(Completion::Finished(values))) + } + } + } + + fn resume_done_into( + self, + _interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + Err(TestError::Core(InterpreterError::Custom( + "the chain policy pushes no children", + ))) + } + + fn resume_into( + self, + _completion: Completion, + _interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + Err(TestError::Core(InterpreterError::Custom( + "the chain policy pushes no children", + ))) + } +} + +type UnEngine<'ir> = ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, UnPolicyFrame>; + +// Pinned to `F = Self` rather than generic: the `Chain` variant holds the +// compiler-supplied `UnGraphChainFrame`, which has no `*FrameBuild` hook (it is +// constructed through `FrameBuild::from_ungraph_entry`), so there is no way to +// re-wrap it into an arbitrary outer `F`. +impl<'ir> Frame, Self> for UnPolicyFrame { + type Completion = Completion; + + fn step_into( + self, + interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + match self { + UnPolicyFrame::Block(frame) => frame.step_into(interp), + UnPolicyFrame::CFG(frame) => frame.step_into(interp), + UnPolicyFrame::Call(frame) => frame.step_into(interp), + UnPolicyFrame::DiGraph(frame) => frame.step_into(interp), + UnPolicyFrame::Chain(frame) => frame.step_into(interp), + } + } + + fn resume_done_into( + self, + interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + match self { + UnPolicyFrame::Block(frame) => frame.resume_done_into(interp), + UnPolicyFrame::CFG(frame) => frame.resume_done_into(interp), + UnPolicyFrame::Call(frame) => frame.resume_done_into(interp), + UnPolicyFrame::DiGraph(frame) => frame.resume_done_into(interp), + UnPolicyFrame::Chain(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + match self { + UnPolicyFrame::Block(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::CFG(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::Call(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::DiGraph(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::Chain(frame) => frame.resume_into(completion, interp), + } + } +} + +const UNGRAPH_PROGRAM: &str = r#" +stage @test fn @usq(i64, i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @usq(i64, i64) -> i64 ungraph ^u0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + %t = mul %s, %s -> i64; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 2 -> i64; + %b = constant 3 -> i64; + %r = call.named @usq(%a, %b) -> i64; + ret %r; + } +} +"#; + +/// A language *with* an UnGraph policy: the call goes caller → `CallFrame` → +/// the compiler's `UnGraphChainFrame`. The `CallFrame` still owns the callee +/// activation and return bookkeeping; only the walker construction was +/// delegated. +#[test] +fn custom_ungraph_policy_is_callable() { + let pipeline = parse(UNGRAPH_PROGRAM); + let mut interp: UnEngine<'_> = ConcreteInterpreter::new(&pipeline); + let result: i64 = + expect_single::(interp.call_by_name("test", "main", []).unwrap()).unwrap(); + // (2 + 3)^2 = 25, via the policy's chain schedule and last-node outputs. + assert_eq!(result, 25); +} + +// =========================================================================== +// 8. UnGraph without a policy: a clear no-default-walker error. +// =========================================================================== + +/// The standard frames supply no UnGraph traversal, so calling an +/// UnGraph-bodied function reports `NoDefaultWalker` instead of inventing a +/// node order. +#[test] +fn ungraph_without_policy_reports_no_default_walker() { + let pipeline = parse(UNGRAPH_PROGRAM); + let error = run(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + error, + TestError::Core(InterpreterError::NoDefaultWalker(Body::UnGraph(_))) + ), + "expected NoDefaultWalker(UnGraph), got {error:?}" + ); +} + +// =========================================================================== +// 9. The same bodies under forward dataflow (abstract interpretation). +// =========================================================================== + +// The tests above pin *concrete* traversal. These pin what the forward +// dataflow engine does with the same closed `Body` vocabulary, at the +// constant-propagation lattice: +// +// - `CFG`, `Block` and `DiGraph` bodies analyze. The engine translates the +// callable body into the executable owner the worklist holds — `Body::CFG` → +// its entry block, `Body::Block` → itself, `Body::DiGraph` → an +// `Owner::Graph` walked by `AbstractDiGraphFrame` as one dependency-ordered +// pass (exact for a DAG, so no intra-graph widening). +// - `UnGraph` bodies are **refused**: an undirected graph has no derivable +// traversal order, and unlike the concrete engine there is no seam through +// which a compiler could supply one. +// - A *nested* graph body (`graph_eval`) is also still refused, for an +// unrelated reason: that dialect rule builds the **concrete** `DiGraphFrame` +// directly instead of selecting one per engine the way scf does, so no +// abstract walker can be substituted. Both refusals are asserted rather than +// left implicit. + +/// Summary key of the constant-propagation context policy. +type CpKey = >::Key; + +/// Total abstract frame for the graph language: the standard abstract +/// traversal (blocks, calls, graph bodies), plus a variant recording the +/// absence of a walker. +/// +/// The language's `Interpretable` rule bounds `I::Frame: FrameBuild<..>` +/// because its `graph_eval` variant pushes a **concrete** `DiGraphFrame`, so an +/// abstract frame type for this language must satisfy that bound even though +/// the abstract engine itself only ever calls `AbstractFrameBuild`. The +/// concrete walkers cannot be embedded here — their completion type is +/// `Completion`, not `AbstractCompletion` — so the `FrameBuild` hooks +/// build `NoWalker`, which reports the gap if it is ever stepped instead of +/// silently running concrete traversal over lattice values. Giving `graph_eval` +/// a per-engine dispatch trait (as `kirin-scf` does for `scf.if`/`scf.for`) +/// would remove the need for both the bound and this variant. +#[derive(AbstractFrameBuild)] +enum GraphAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), + /// No abstract walker exists for this body kind; carries the reason. + NoWalker(&'static str), +} + +impl FrameBuild for GraphAbstractFrame { + type BodyFrames = DefaultBodyFrames; + + fn from_block(_: BlockFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract walker for a concrete Block frame") + } + fn from_cfg(_: CFGFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract walker for a concrete CFG frame") + } + fn from_call(_: CallFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract walker for a concrete call boundary") + } + fn from_digraph(_: DiGraphFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract digraph walker") + } +} + +impl Frame for GraphAbstractFrame +where + I: ForwardDataflowFrameEngine + + SparseForwardInterp, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(self, interp: &mut I) -> Result>, E> { + match self { + GraphAbstractFrame::Block(frame) => frame.step_into(interp), + GraphAbstractFrame::Call(frame) => frame.step_into(interp), + GraphAbstractFrame::DiGraph(frame) => frame.step_into(interp), + GraphAbstractFrame::NoWalker(reason) => Err(E::from(InterpreterError::Custom(reason))), + } + } + + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + match self { + GraphAbstractFrame::Block(frame) => frame.resume_done_into(interp), + GraphAbstractFrame::Call(frame) => frame.resume_done_into(interp), + GraphAbstractFrame::DiGraph(frame) => frame.resume_done_into(interp), + GraphAbstractFrame::NoWalker(reason) => Err(E::from(InterpreterError::Custom(reason))), + } + } + + fn resume_into( + self, + completion: AbstractCompletion, + interp: &mut I, + ) -> Result>, E> { + match self { + GraphAbstractFrame::Block(frame) => frame.resume_into(completion, interp), + GraphAbstractFrame::Call(frame) => frame.resume_into(completion, interp), + GraphAbstractFrame::DiGraph(frame) => frame.resume_into(completion, interp), + GraphAbstractFrame::NoWalker(reason) => Err(E::from(InterpreterError::Custom(reason))), + } + } +} + +type AbstractEngine<'ir> = SparseForwardInterpreter< + 'ir, + L, + ConstPropValue, + TestError, + SameStageLinker, + ConstPropContext, + GraphAbstractFrame, +>; + +/// Run constant propagation from `function` and return its inferred return +/// value at the fixpoint. +fn analyze( + pipeline: &Pipeline, + function: &str, + args: &[ConstPropValue], +) -> Result { + let mut analysis: AbstractEngine<'_> = + SparseForwardInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(analysis.analyze_by_name("test", function, args.iter().cloned())?) +} + +/// Summary key of the *context-insensitive* policy: one key per function, so +/// every call site shares one owner and their arguments join. +type CiKey = >::Key; + +type InsensitiveEngine<'ir> = SparseForwardInterpreter< + 'ir, + L, + ConstPropValue, + TestError, + SameStageLinker, + ContextInsensitive, + GraphAbstractFrame, +>; + +/// The same analysis under [`ContextInsensitive`] keying: distinct call sites +/// collapse onto one owner, so entry arguments must join and the owner must be +/// re-analyzed when they rise. +fn analyze_insensitive( + pipeline: &Pipeline, + function: &str, + args: &[ConstPropValue], +) -> Result { + let mut analysis: InsensitiveEngine<'_> = + SparseForwardInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(analysis.analyze_by_name("test", function, args.iter().cloned())?) +} + +/// A CFG body whose branch condition is an *unknown* argument, so neither +/// successor can be decided: the abstract block frame explores both and joins +/// their returns. Identical arms fold to a constant; differing arms join to +/// `Top`. +const CFG_BRANCH_PROGRAM: &str = r#" +stage @test fn @same(i64) -> i64; +stage @test fn @diff(i64) -> i64; +stage @test fn @caller(i64) -> i64; + +specialize @test fn @same(i64) -> i64 { + ^entry(%c: i64) { + cond_br %c then=^t() else=^f(); + } + ^t() { + %a = constant 7 -> i64; + ret %a; + } + ^f() { + %b = constant 7 -> i64; + ret %b; + } +} + +specialize @test fn @diff(i64) -> i64 { + ^entry(%c: i64) { + cond_br %c then=^t() else=^f(); + } + ^t() { + %a = constant 7 -> i64; + ret %a; + } + ^f() { + %b = constant 9 -> i64; + ret %b; + } +} + +specialize @test fn @caller(i64) -> i64 { + ^entry(%c: i64) { + %r = call.named @same(%c) -> i64; + %one = constant 1 -> i64; + %s = add %r, %one -> i64; + ret %s; + } +} +"#; + +/// `Body::CFG` under forward dataflow: the entry block is seeded, the +/// undecided `cond_br` explores both successors, and the returns are joined. +#[test] +fn abstract_cfg_body_joins_branch_arms() { + let pipeline = parse(CFG_BRANCH_PROGRAM); + // Both arms return the same constant, so the join stays precise even + // though the condition is unknown. + assert_eq!( + analyze(&pipeline, "same", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Const(7) + ); + // Differing arms join to Top — evidence both were actually explored + // rather than one being picked. + assert_eq!( + analyze(&pipeline, "diff", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Top + ); +} + +/// The interprocedural path: a CFG-bodied caller summarizes a CFG-bodied +/// callee and folds the returned summary into its own arithmetic. +#[test] +fn abstract_cfg_body_summarizes_call() { + let pipeline = parse(CFG_BRANCH_PROGRAM); + assert_eq!( + analyze(&pipeline, "caller", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Const(8) + ); +} + +/// `Body::Block` under forward dataflow: a single-block callable is seeded +/// directly as its own entry block (no CFG entry lookup), and is reached both +/// as an analysis root and as a summarized callee. +#[test] +fn abstract_block_body_callable() { + let pipeline = parse(BLOCK_CALLABLE_PROGRAM); + assert_eq!( + analyze( + &pipeline, + "ladd", + &[ConstPropValue::Const(40), ConstPropValue::Const(2)] + ) + .unwrap(), + ConstPropValue::Const(42) + ); + // Same body, reached through a call from a CFG-bodied caller. + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(42) + ); + // An unknown operand propagates through the block body to Top. + assert_eq!( + analyze( + &pipeline, + "ladd", + &[ConstPropValue::Top, ConstPropValue::Const(2)] + ) + .unwrap(), + ConstPropValue::Top + ); +} + +/// `Body::DiGraph` under forward dataflow: the callable graph body becomes an +/// `Owner::Graph` walked by `AbstractDiGraphFrame` — one dependency-ordered +/// pass binding the boundary ports, with the graph's declared yields becoming +/// the function's return summary. Reached both as an analysis root and as a +/// summarized callee. +#[test] +fn abstract_digraph_callable_analyzes() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + assert_eq!( + analyze( + &pipeline, + "gadd", + &[ConstPropValue::Const(2), ConstPropValue::Const(3)] + ) + .unwrap(), + ConstPropValue::Const(5) + ); + // Same body, reached through a call from a CFG-bodied caller: the graph + // owner's yields flow back as the callee's return summary. + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(5) + ); + // An unknown port value propagates through the graph to Top. + assert_eq!( + analyze( + &pipeline, + "gadd", + &[ConstPropValue::Top, ConstPropValue::Const(3)] + ) + .unwrap(), + ConstPropValue::Top + ); +} + +/// The abstract walker uses the same dependency schedule as the concrete one: +/// this graph's nodes are declared consumer-before-producer, so a textual walk +/// would read unbound operands. +#[test] +fn abstract_digraph_follows_dependency_order() { + let pipeline = parse(DIGRAPH_TOPO_PROGRAM); + // (3 + 3) * (3 + 1) = 24, matching `digraph_runs_in_topological_order`. + assert_eq!( + analyze(&pipeline, "g", &[ConstPropValue::Const(3)]).unwrap(), + ConstPropValue::Const(24) + ); + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(24) + ); +} + +/// A callable `UnGraph` body is refused on the same path. Note the concrete +/// escape hatch does *not* apply here: `FrameBuild::from_ungraph_entry` is a +/// concrete-frame hook, so an UnGraph policy supplied for execution buys +/// nothing under abstract interpretation. +#[test] +fn abstract_ungraph_callable_reports_no_default_walker() { + let pipeline = parse(UNGRAPH_PROGRAM); + let error = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + error, + TestError::Core(InterpreterError::NoDefaultWalker(Body::UnGraph(_))) + ), + "expected NoDefaultWalker(UnGraph), got {error:?}" + ); +} + +/// A *nested* graph body is refused too, by a different route: the statement's +/// rule pushes a walker, and the abstract frame type has none to give. The +/// callable cases above never reach a frame at all, so this is the only test +/// covering the pushed path. +#[test] +fn abstract_nested_digraph_reports_no_walker() { + let pipeline = parse(NESTED_DIGRAPH_PROGRAM); + let error = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + error, + TestError::Core(InterpreterError::Custom("no abstract digraph walker")) + ), + "expected the pushed-frame walker gap, got {error:?}" + ); +} + +// =========================================================================== +// 10. Calls *inside* a graph body. +// =========================================================================== + +/// A digraph node that is itself a call. The dependency edge `%a → %b` runs +/// through two call results, so the graph walker must sequence the calls, and +/// each call must be routed through the engine's call protocol rather than +/// evaluated inline. +const DIGRAPH_CALLS_PROGRAM: &str = r#" +stage @test fn @inc(i64) -> i64; +stage @test fn @gcall(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @inc(i64) -> i64 { + ^entry(%v: i64) { + %one = constant 1 -> i64; + %s = add %v, %one -> i64; + ret %s; + } +} + +specialize @test fn @gcall(i64) -> i64 digraph ^g0(%x: i64) { + %b = call.named @inc(%a) -> i64; + %a = call.named @inc(%x) -> i64; + yield %b; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %c = constant 5 -> i64; + %r = call.named @gcall(%c) -> i64; + ret %r; + } +} +"#; + +/// Concretely: the `DiGraphFrame` pushes a `CallFrame` per call node, in +/// dependency order (`%a` before `%b` despite the textual order). +#[test] +fn digraph_node_calls_run_in_dependency_order() { + let pipeline = parse(DIGRAPH_CALLS_PROGRAM); + assert_eq!(run(&pipeline, "gcall", &[5]).unwrap(), 7); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 7); +} + +/// Abstractly the same graph must route each call through +/// `summarize_call` — an `AbstractCallFrame`, *not* the concrete `CallFrame`, +/// which would descend into the callee and bypass the interprocedural +/// protocol. This is the one arm where `AbstractDiGraphFrame` differs +/// substantively from the concrete walker. +#[test] +fn abstract_digraph_node_calls_are_summarized() { + let pipeline = parse(DIGRAPH_CALLS_PROGRAM); + assert_eq!( + analyze(&pipeline, "gcall", &[ConstPropValue::Const(5)]).unwrap(), + ConstPropValue::Const(7) + ); + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(7) + ); + // An unknown port flows through both summarized calls to Top. + assert_eq!( + analyze(&pipeline, "gcall", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Top + ); +} + +/// A graph body that calls *itself*. A digraph cannot branch, so this +/// recursion has no base case and does not terminate concretely — which is +/// exactly why it is analysis-only. It terminates here because the call is +/// summarized: the self-key's return summary starts at `bottom` and the owner +/// re-runs only while it rises. Descending into the callee instead would +/// recurse forever. +const DIGRAPH_RECURSION_PROGRAM: &str = r#" +stage @test fn @grec(i64) -> i64; + +specialize @test fn @grec(i64) -> i64 digraph ^g0(%x: i64) { + %r = call.named @grec(%x) -> i64; + yield %r; +} +"#; + +#[test] +fn abstract_digraph_self_recursion_converges() { + let pipeline = parse(DIGRAPH_RECURSION_PROGRAM); + // Never returns, so the sound fixpoint is `bottom` — and, crucially, the + // analysis reaches it instead of diverging. + assert_eq!( + analyze(&pipeline, "grec", &[ConstPropValue::Const(1)]).unwrap(), + ConstPropValue::Bottom + ); +} + +// =========================================================================== +// 11. Graph-owner re-analysis: two call sites, one owner. +// =========================================================================== + +/// One graph-bodied callee reached from two call sites with different +/// constants. +const DIGRAPH_TWO_CALLERS_PROGRAM: &str = r#" +stage @test fn @gdouble(i64) -> i64; +stage @test fn @twocalls() -> i64; + +specialize @test fn @gdouble(i64) -> i64 digraph ^g0(%x: i64) { + %s = add %x, %x -> i64; + yield %s; +} + +specialize @test fn @twocalls() -> i64 { + ^entry() { + %a = constant 1 -> i64; + %b = constant 2 -> i64; + %p = call.named @gdouble(%a) -> i64; + %q = call.named @gdouble(%b) -> i64; + %s = add %p, %q -> i64; + ret %s; + } +} +"#; + +/// The convergence behaviour of a graph owner, pinned from both sides by +/// running the same program under both keying policies. +/// +/// Under [`ContextInsensitive`] the two call sites share one owner, so its +/// entry product must **join** (`Const(1) ⊔ Const(2)` = `Top`) and the graph +/// must be **re-analyzed** with the wider entry — an owner seeded once and +/// never re-run would leave the second call site reading a stale `Const(4)`. +/// Under `ConstPropContext` the sites key separately and each stays exact. +/// Together these show a graph owner participates in entry widening exactly +/// like a block owner, which is where all of its convergence pressure comes +/// from (one dependency-ordered pass is exact, so nothing widens *inside* the +/// graph). +#[test] +fn abstract_digraph_owner_joins_two_call_sites() { + let pipeline = parse(DIGRAPH_TWO_CALLERS_PROGRAM); + // Shared owner: entry joins to Top, the graph re-runs, both results are Top. + assert_eq!( + analyze_insensitive(&pipeline, "twocalls", &[]).unwrap(), + ConstPropValue::Top + ); + // Distinct keys: 1+1 = 2 and 2+2 = 4, so 2 + 4 = 6. + assert_eq!( + analyze(&pipeline, "twocalls", &[]).unwrap(), + ConstPropValue::Const(6) + ); +} + +// =========================================================================== +// 12. Cyclic DiGraph: rejected when the walk plan is built. +// =========================================================================== + +/// A digraph whose nodes depend on each other. The IR represents this happily — +/// it parses — because a `DiGraph` is not required to be acyclic. +const DIGRAPH_CYCLE_PROGRAM: &str = r#" +stage @test fn @gcycle(i64) -> i64; + +specialize @test fn @gcycle(i64) -> i64 digraph ^g0(%x: i64) { + %a = add %b, %x -> i64; + %b = add %a, %x -> i64; + yield %a; +} +"#; + +/// Both engines reject a directed cycle, with the *same* error and from the +/// *same* place: `digraph_walk_plan` topologically sorts the nodes, and a +/// cyclic graph has no topological order. So the rejection is a property of the +/// walk plan (shared by the concrete and abstract walkers), not of the IR and +/// not of either engine. +/// +/// Supporting cyclic graph bodies is therefore not an extension of the current +/// walkers: it needs a schedule that is not a toposort plus a fixpoint *inside* +/// the graph, which in turn needs a finer unit of re-analysis than +/// `Owner::Graph`'s single exact pass. +#[test] +fn cyclic_digraph_is_rejected_by_both_engines() { + let pipeline = parse(DIGRAPH_CYCLE_PROGRAM); + + let concrete = run(&pipeline, "gcycle", &[1]).unwrap_err(); + assert!( + matches!( + concrete, + TestError::Core(InterpreterError::GraphHasCycle(_)) + ), + "expected GraphHasCycle concretely, got {concrete:?}" + ); + + let abstract_ = analyze(&pipeline, "gcycle", &[ConstPropValue::Const(1)]).unwrap_err(); + assert!( + matches!( + abstract_, + TestError::Core(InterpreterError::GraphHasCycle(_)) + ), + "expected GraphHasCycle abstractly, got {abstract_:?}" + ); +} + +// =========================================================================== +// 13. Multi-yield graphs and boundary arity. +// =========================================================================== + +/// A graph yielding **two** values into a two-result call site. +/// +/// Note the declared signature is `-> i64`, one type, while the function +/// actually returns two values: `Signature` carries a single `ret` type, so it +/// does not constrain return *arity*. The product arity that matters at runtime +/// is the graph's `yield` list versus the call statement's result slots. (This +/// is the same shape as `example/toy-qc/programs/ghz.kirin`, which declares +/// `-> Qubit` and yields three.) +const DIGRAPH_MULTIYIELD_PROGRAM: &str = r#" +stage @test fn @gpair(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @gpair(i64) -> i64 digraph ^g0(%x: i64) { + %a = add %x, %x -> i64; + %b = mul %x, %x -> i64; + yield %a, %b; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %c = constant 3 -> i64; + %p, %q = call.named @gpair(%c) -> i64, i64; + %s = add %p, %q -> i64; + ret %s; + } +} +"#; + +/// Yield order is result-slot order: `%p` gets the first yield, `%q` the +/// second. Asserting the product directly (rather than only their sum) pins +/// that mapping. +#[test] +fn digraph_yields_multiple_values() { + let pipeline = parse(DIGRAPH_MULTIYIELD_PROGRAM); + let values: Vec = run_product(&pipeline, "gpair", &[3]) + .unwrap() + .iter() + .copied() + .collect(); + // 3 + 3 = 6 and 3 * 3 = 9, in yield order. + assert_eq!(values, vec![6, 9]); + // Both land in the caller's two result slots: 6 + 9. + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 15); +} + +/// The abstract walker collects the same product, so a multi-result callee's +/// return summary carries both slots. +#[test] +fn abstract_digraph_yields_multiple_values() { + let pipeline = parse(DIGRAPH_MULTIYIELD_PROGRAM); + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(15) + ); +} + +/// A graph whose boundary ports outnumber the call's arguments. Nothing earlier +/// in the pipeline cross-checks the declared signature against the port list, so +/// the graph walkers arity-check when binding the ports — the same check, and +/// the same error, in both engines. +const DIGRAPH_PORT_ARITY_PROGRAM: &str = r#" +stage @test fn @g2(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @g2(i64) -> i64 digraph ^g0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + yield %s; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %c = constant 3 -> i64; + %r = call.named @g2(%c) -> i64; + ret %r; + } +} +"#; + +#[test] +fn digraph_port_arity_mismatch_is_reported() { + let pipeline = parse(DIGRAPH_PORT_ARITY_PROGRAM); + + let concrete = run(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + concrete, + TestError::Core(InterpreterError::ProductArityMismatch { + expected: 2, + actual: 1 + }) + ), + "expected a port arity mismatch concretely, got {concrete:?}" + ); + + let abstract_ = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + abstract_, + TestError::Core(InterpreterError::ProductArityMismatch { + expected: 2, + actual: 1 + }) + ), + "expected a port arity mismatch abstractly, got {abstract_:?}" + ); +} + +// =========================================================================== +// 14. A custom callable-body walker policy. +// =========================================================================== + +// `CallFrame` owns the call convention — resolve, allocate, enter, suspend, +// validate the completion, free the activation exactly once, bind results — and +// delegates only *which walker enters the callee body* to a +// `CallBodyFramePolicy`. These tests show a language replacing that choice for +// two body kinds without reimplementing any of the lifecycle, and confirm the +// choice does not leak into `scf.if`, which picks its own dialect frame. + +thread_local! { + /// Which body kinds the custom policy was asked for, in order. + static POLICY_LOG: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// A custom policy: instrument `CFG` and `DiGraph` entry, delegate `Block` and +/// `UnGraph` to the framework default. Each arm still builds the *standard* +/// walker — the point is that the language chose it, not that it walks +/// differently. +struct LoggingBodyFrames; + +impl CallBodyFramePolicy for LoggingBodyFrames { + fn from_cfg(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("cfg")); + Ok(PolicyFrame::CFG(CFGFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_block(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("block")); + >::from_block(entry) + } + + fn from_digraph(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("digraph")); + Ok(PolicyFrame::DiGraph(DiGraphFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_ungraph(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("ungraph")); + >::from_ungraph(entry) + } +} + +/// A total frame type that selects `LoggingBodyFrames`. Note the `Call` variant +/// carries the policy, and `FrameBuild::BodyFrames` names it — the derive would +/// emit exactly this via `#[interpret(body_frames = LoggingBodyFrames)]`. +enum PolicyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + ScfIf(ScfIfFrame), + ScfFor(ScfForFrame), +} + +impl FrameBuild for PolicyFrame { + type BodyFrames = LoggingBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + PolicyFrame::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + PolicyFrame::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + PolicyFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + PolicyFrame::DiGraph(frame) + } +} + +impl BuildScfIf for PolicyFrame { + fn scf_if(frame: ScfIfFrame) -> Self { + PolicyFrame::ScfIf(frame) + } +} + +impl BuildScfFor for PolicyFrame { + fn scf_for(frame: ScfForFrame) -> Self { + PolicyFrame::ScfFor(frame) + } +} + +impl Frame for PolicyFrame +where + I: ForwardFrameEngine + + SparseForwardInterp, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, TestError> { + match self { + PolicyFrame::Block(frame) => frame.step_into(interp), + PolicyFrame::CFG(frame) => frame.step_into(interp), + PolicyFrame::Call(frame) => frame.step_into(interp), + PolicyFrame::DiGraph(frame) => frame.step_into(interp), + PolicyFrame::ScfIf(frame) => frame.step_into(interp), + PolicyFrame::ScfFor(frame) => frame.step_into(interp), + } + } + + fn resume_done_into( + self, + interp: &mut I, + ) -> Result>, TestError> { + match self { + PolicyFrame::Block(frame) => frame.resume_done_into(interp), + PolicyFrame::CFG(frame) => frame.resume_done_into(interp), + PolicyFrame::Call(frame) => frame.resume_done_into(interp), + PolicyFrame::DiGraph(frame) => frame.resume_done_into(interp), + PolicyFrame::ScfIf(frame) => frame.resume_done_into(interp), + PolicyFrame::ScfFor(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, TestError> { + match self { + PolicyFrame::Block(frame) => frame.resume_into(completion, interp), + PolicyFrame::CFG(frame) => frame.resume_into(completion, interp), + PolicyFrame::Call(frame) => frame.resume_into(completion, interp), + PolicyFrame::DiGraph(frame) => frame.resume_into(completion, interp), + PolicyFrame::ScfIf(frame) => frame.resume_into(completion, interp), + PolicyFrame::ScfFor(frame) => frame.resume_into(completion, interp), + } + } +} + +type PolicyEngine<'ir> = ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, PolicyFrame>; + +fn run_with_policy(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().clear()); + let mut interp: PolicyEngine<'_> = + ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +fn policy_log() -> Vec<&'static str> { + POLICY_LOG.with(|log| log.borrow().clone()) +} + +/// A root call and a nested call, both routed through the custom policy. The +/// returned values are unchanged — only the *selection* of the walker moved. +#[test] +fn custom_body_policy_enters_callable_bodies() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + + // Root call into a CFG body, which then calls a DiGraph body. + assert_eq!(run_with_policy(&pipeline, "main", &[]).unwrap(), 5); + assert_eq!(policy_log(), vec!["cfg", "digraph"]); + + // Root call straight into the DiGraph body. + assert_eq!(run_with_policy(&pipeline, "gadd", &[2, 3]).unwrap(), 5); + assert_eq!(policy_log(), vec!["digraph"]); +} + +/// The `Block` arm delegates to `DefaultBodyFrames`, so a policy can override +/// only the body kinds it cares about. +#[test] +fn custom_body_policy_can_delegate_to_the_default() { + let pipeline = parse(BLOCK_CALLABLE_PROGRAM); + assert_eq!(run_with_policy(&pipeline, "main", &[]).unwrap(), 42); + assert_eq!(policy_log(), vec!["cfg", "block"]); +} + +/// Isolation: `scf.if` builds its *own* dialect frame via `ScfIfDispatch` and +/// walks the chosen arm with a framework `BlockFrame`. It is a nested body, not +/// a callable one, so the call-body policy must never be consulted for it. +#[test] +fn scf_if_does_not_use_the_call_body_policy() { + let pipeline = parse_scf(SCF_ABS_PROGRAM); + let mut interp: ConcreteInterpreter< + '_, + ScfL, + i64, + TestError, + SameStageLinker, + ScfTestFrame, + > = ConcreteInterpreter::new(&pipeline).with_linker(SameStageLinker); + POLICY_LOG.with(|log| log.borrow_mut().clear()); + let result = + expect_single::(interp.call_by_name("test", "abs", [-7]).unwrap()).unwrap(); + assert_eq!(result, 7); + // `ScfTestFrame` uses the default policy, and in any case the scf arm never + // reaches a call boundary — the log stays empty. + assert!(policy_log().is_empty(), "got {:?}", policy_log()); +} + +// =========================================================================== +// 15. A *genuine* replacement walker, selected through the derive. +// =========================================================================== + +// Section 14 proves the policy is consulted, but each arm still built a +// standard walker and the total frame type implemented `FrameBuild` by hand. This +// section closes both gaps: +// +// - `MyCfgWalker` is a **distinct frame type** with its own `Frame` impl and its +// own enum variant. A callable `CFG` body enters through it, never through +// `DerivedFrame::CFG`. +// - the policy is selected by `#[interpret(body_frames = MyBodyFrames)]`, so the +// derive is **compiled and executed** here rather than only snapshotted. +// +// What this does *not* claim: `MyCfgWalker` delegates the actual block-to-block +// traversal to a `CFGFrame` it owns, rather than reimplementing CFG walking. It +// hands off after the entry step, which is why the assertions count *entries*. +// The substitutable thing Roger asked for is the frame type the language chooses +// at the callable-body boundary, and that is what is replaced. + +#[derive(Clone, Copy, Default, Debug, PartialEq)] +struct CustomTrace { + /// `MyBodyFrames::from_cfg` calls — one per callable CFG activation. + policy_selections: usize, + /// Steps taken *by* `MyCfgWalker`. + my_steps: usize, + /// Steps taken by the standard `DerivedFrame::CFG` variant. Must stay `0`: + /// if the custom walker ever handed the walk back to the framework variant, + /// this counts it. + standard_cfg_steps: usize, +} + +thread_local! { + static CUSTOM: RefCell = const { RefCell::new(CustomTrace { + policy_selections: 0, + my_steps: 0, + standard_cfg_steps: 0, + }) }; +} + +/// A language's own callable-CFG walker: distinct type, distinct variant. +struct MyCfgWalker { + inner: CFGFrame, +} + +impl MyCfgWalker { + /// The inner `CFGFrame` re-wraps *itself* through `FrameBuild::from_cfg`, + /// which lands in `DerivedFrame::CFG`. Lift those back so this walker stays + /// resident for the whole traversal rather than only its entry step. + /// + /// Only the frame that represents *self* is lifted: `Push.child` is a + /// different frame (a call boundary or a pushed dialect frame) and must be + /// left alone. + fn stay_resident( + effect: FrameEffect, Completion>, + ) -> FrameEffect, Completion> { + fn lift(frame: DerivedFrame) -> DerivedFrame { + match frame { + DerivedFrame::CFG(inner) => DerivedFrame::MyCfg(MyCfgWalker { inner }), + other => other, + } + } + match effect { + FrameEffect::Continue(frame) => FrameEffect::Continue(lift(frame)), + FrameEffect::Push { parent, child } => FrameEffect::Push { + parent: lift(parent), + child, + }, + // `Done`/`Complete` carry no frame. + other => other, + } + } +} + +impl Frame> for MyCfgWalker +where + I: ForwardFrameEngine + SparseForwardInterp>, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into( + self, + interp: &mut I, + ) -> Result, Completion>, E> { + CUSTOM.with(|t| t.borrow_mut().my_steps += 1); + self.inner.step_into(interp).map(Self::stay_resident) + } + + fn resume_done_into( + self, + interp: &mut I, + ) -> Result, Completion>, E> { + CUSTOM.with(|t| t.borrow_mut().my_steps += 1); + self.inner.resume_done_into(interp).map(Self::stay_resident) + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result, Completion>, E> { + CUSTOM.with(|t| t.borrow_mut().my_steps += 1); + self.inner + .resume_into(completion, interp) + .map(Self::stay_resident) + } +} + +/// Replaces the `CFG` walker outright; delegates the other three body kinds to +/// the framework default. +struct MyBodyFrames; + +impl CallBodyFramePolicy> for MyBodyFrames +where + V: Clone, + E: From, +{ + fn from_cfg(entry: BodyFrameEntry) -> Result, E> { + // Not `F::from_cfg` — the language's own walker, in its own variant. + CUSTOM.with(|t| t.borrow_mut().policy_selections += 1); + Ok(DerivedFrame::MyCfg(MyCfgWalker { + inner: CFGFrame::new(entry.stage, entry.index, entry.body, entry.args), + })) + } + + fn from_block(entry: BodyFrameEntry) -> Result, E> { + >>::from_block(entry) + } + + fn from_digraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_digraph(entry) + } + + fn from_ungraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_ungraph(entry) + } +} + +/// The policy is chosen by the attribute; the derive emits +/// `type BodyFrames = MyBodyFrames` and the four injection constructors. +#[derive(FrameBuild)] +#[interpret(body_frames = MyBodyFrames)] +enum DerivedFrame { + Block(BlockFrame), + /// Required by `FrameBuild`, and reached only when `MyCfgWalker` hands off + /// mid-walk — never as the entry frame for a callable body. + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + MyCfg(MyCfgWalker), +} + +impl Frame for DerivedFrame +where + I: ForwardFrameEngine + SparseForwardInterp>, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { + match self { + DerivedFrame::Block(frame) => frame.step_into(interp), + DerivedFrame::CFG(frame) => { + CUSTOM.with(|t| t.borrow_mut().standard_cfg_steps += 1); + frame.step_into(interp) + } + DerivedFrame::Call(frame) => frame.step_into(interp), + DerivedFrame::DiGraph(frame) => frame.step_into(interp), + DerivedFrame::MyCfg(frame) => frame.step_into(interp), + } + } + + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + match self { + DerivedFrame::Block(frame) => frame.resume_done_into(interp), + DerivedFrame::CFG(frame) => frame.resume_done_into(interp), + DerivedFrame::Call(frame) => frame.resume_done_into(interp), + DerivedFrame::DiGraph(frame) => frame.resume_done_into(interp), + DerivedFrame::MyCfg(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match self { + DerivedFrame::Block(frame) => frame.resume_into(completion, interp), + DerivedFrame::CFG(frame) => frame.resume_into(completion, interp), + DerivedFrame::Call(frame) => frame.resume_into(completion, interp), + DerivedFrame::DiGraph(frame) => frame.resume_into(completion, interp), + DerivedFrame::MyCfg(frame) => frame.resume_into(completion, interp), + } + } +} + +fn run_derived(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + CUSTOM.with(|t| *t.borrow_mut() = CustomTrace::default()); + let mut interp: ConcreteInterpreter< + '_, + L, + i64, + TestError, + SameStageLinker, + DerivedFrame, + > = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +/// The custom walker is selected by `#[interpret(body_frames = ..)]` and stays +/// resident for the **whole** CFG traversal, because it re-wraps every frame the +/// inner walker hands back (see [`MyCfgWalker::stay_resident`]). +/// +/// Two assertions carry the claim: `standard_cfg_steps == 0` proves the +/// framework's `CFGFrame` variant is never stepped, and `my_steps` well above +/// `policy_selections` proves the walker kept going rather than handing off after +/// entry. Deleting the re-wrap flips this to +/// `my_steps: 1, standard_cfg_steps: 4` — i.e. entry-only — so the assertions +/// have teeth. (Observed here: `my_steps: 6, standard_cfg_steps: 0`.) +#[test] +fn derived_policy_substitutes_a_custom_cfg_walker() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + + // `main` is CFG-bodied → the custom walker. It calls `gadd`, a DiGraph body, + // which the policy delegates to the framework's `DiGraphFrame`. + assert_eq!(run_derived(&pipeline, "main", &[]).unwrap(), 5); + let trace = CUSTOM.with(|t| *t.borrow()); + assert_eq!(trace.policy_selections, 1, "{trace:?}"); + assert_eq!( + trace.standard_cfg_steps, 0, + "custom walker leaked: {trace:?}" + ); + assert!( + trace.my_steps > trace.policy_selections, + "walker handed off after entry: {trace:?}" + ); + + // A root call straight into the DiGraph body: delegated, so no custom CFG + // walker is ever built. + assert_eq!(run_derived(&pipeline, "gadd", &[2, 3]).unwrap(), 5); + assert_eq!(CUSTOM.with(|t| *t.borrow()), CustomTrace::default()); +} + +/// Two nested CFG-bodied callables: the policy is consulted once per activation, +/// and neither activation ever falls back to the standard variant. +#[test] +fn derived_policy_walker_stays_resident_across_activations() { + let pipeline = parse(CFG_BRANCH_PROGRAM); + // `caller` (CFG) calls `same` (CFG) → two callable CFG activations. + assert_eq!(run_derived(&pipeline, "caller", &[0]).unwrap(), 8); + let trace = CUSTOM.with(|t| *t.borrow()); + assert_eq!(trace.policy_selections, 2, "{trace:?}"); + assert_eq!( + trace.standard_cfg_steps, 0, + "custom walker leaked: {trace:?}" + ); + // Both bodies are multi-statement, so residency means many more steps than + // the two entries. + assert!(trace.my_steps > 4, "{trace:?}"); +} diff --git a/tests/compile-fail/call_frame_policy_mismatch.rs b/tests/compile-fail/call_frame_policy_mismatch.rs new file mode 100644 index 0000000000..a56da56e78 --- /dev/null +++ b/tests/compile-fail/call_frame_policy_mismatch.rs @@ -0,0 +1,46 @@ +//! A total frame type whose `Call` variant carries one callable-body policy +//! while `#[derive(FrameBuild)]` is configured with another (here: the default, +//! because no `#[interpret(body_frames = ..)]` was given). +//! +//! The derive deliberately does not try to reconcile the two — it emits +//! `type BodyFrames = DefaultBodyFrames` and lets the generated impl produce a +//! type error naming both policies, which is more informative than anything the +//! macro could say about a path it cannot resolve. + +use kirin_interpreter::{ + BlockFrame, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, CallFrame, DefaultBodyFrames, + DiGraphFrame, FrameBuild, InterpreterError, +}; +use kirin_ir::{Block, CFG, DiGraph, UnGraph}; + +struct MyBodyFrames; + +impl CallBodyFramePolicy> for MyBodyFrames +where + V: Clone, + E: From, +{ + fn from_cfg(entry: BodyFrameEntry) -> Result, E> { + >>::from_cfg(entry) + } + fn from_block(entry: BodyFrameEntry) -> Result, E> { + >>::from_block(entry) + } + fn from_digraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_digraph(entry) + } + fn from_ungraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_ungraph(entry) + } +} + +// Missing: #[interpret(body_frames = MyBodyFrames)] +#[derive(FrameBuild)] +enum MismatchedFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), +} + +fn main() {} diff --git a/tests/compile-fail/call_frame_policy_mismatch.stderr b/tests/compile-fail/call_frame_policy_mismatch.stderr new file mode 100644 index 0000000000..135dd41dc9 --- /dev/null +++ b/tests/compile-fail/call_frame_policy_mismatch.stderr @@ -0,0 +1,13 @@ +error[E0053]: method `from_call` has an incompatible type for trait + --> tests/compile-fail/call_frame_policy_mismatch.rs:42:10 + | +42 | Call(CallFrame), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `DefaultBodyFrames`, found `MyBodyFrames` + | + = note: expected signature `fn(CallFrame) -> MismatchedFrame` + found signature `fn(CallFrame) -> MismatchedFrame` +help: change the parameter type to match the trait + | +42 - Call(CallFrame), +42 + Call(CallFrame), + | diff --git a/tests/frame_engine_capabilities.rs b/tests/frame_engine_capabilities.rs new file mode 100644 index 0000000000..73e9eb29bf --- /dev/null +++ b/tests/frame_engine_capabilities.rs @@ -0,0 +1,575 @@ +//! Compile-time regression tests for the **engine-capability split**. +//! +//! Each engine here is *deliberately incomplete*: it implements only the +//! capability traits one kind of frame consumes, and omits the rest. The value +//! of this file is that **it compiles** — every `assert_frame` / +//! `assert_dataflow_engine` call below is a static proof that the named frame +//! does not secretly require a capability the engine never provides. +//! +//! Before the split there was one monolithic capability trait carrying every +//! operation, so *none* of these four engines could exist: running a block +//! walker meant also supplying `alloc_env`/`free_env`/`resolve_call`/ +//! `enter_function`/`cfg_entry`/`digraph_walk_plan`, and an abstract dataflow +//! engine had to expose a concrete call convention it never performs. +//! +//! | mock engine | pins | +//! |---|---| +//! | `BlockOnlyEngine` | `BlockFrame` needs only `Env + StatementDispatch + BlockQueries` | +//! | `CallOnlyEngine` | `CallFrame` needs only `CallServices` — not even a statement-effect algebra | +//! | `AbstractOnlyEngine` | `ForwardDataflowFrameEngine` requires neither `CallServices` nor `CFGQueries` | +//! | `QueriesOnlyEngine` | the `*Queries` traits are honestly read-only: satisfiable with no `Env` at all | +//! +//! Each is load-bearing. Widening a member frame's bound (adding `CallServices` +//! to `BlockFrame`'s `Frame` impl), re-attaching the call lifecycle to the +//! abstract umbrella, or re-adding `Env` as a `*Queries` supertrait each stops +//! this file compiling and names what regressed. +//! +//! The mock engines panic if actually *run*: nothing here executes IR. That is +//! the point — these are type-level assertions, and the behavioral coverage +//! lives in `tests/body_kinds.rs` and the engine crates. + +// Everything here exists to be *type-checked*, not read: the frame variants +// prove the universes are constructible and the storage exists only to satisfy +// `Env`, so "never read" is the expected state of this file. +#![allow(dead_code)] + +use std::collections::HashMap; + +use kirin_interpreter::{ + AbstractBlockFrame, AbstractCallFrame, AbstractDiGraphFrame, AbstractFrameBuild, BlockFrame, + BlockQueries, CFGFrame, CFGQueries, CallEffect, CallFrame, CallServices, CallableBody, Callee, + DefaultBodyFrames, DiGraphFrame, DiGraphQueries, Env, EnvIndex, ForwardDataflowFrameEngine, + ForwardEval, ForwardFrameEngine, Frame, FrameBuild, FunctionTarget, Interp, InterpreterError, + SparseForwardEffect, StatementDispatch, +}; +use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; + +/// The compile-time assertions this file is made of. +/// +/// None is ever called; instantiating them is what type-checks the bounds. +fn assert_frame() +where + I: kirin_interpreter::FrameEngine, + T: Frame, +{ +} + +fn assert_dataflow_engine() {} + +/// The `*Queries` traits must be satisfiable **without** [`Env`] — that is what +/// makes their names truthful. +fn assert_read_only_queries() {} + +// =========================================================================== +// Shared mock storage +// =========================================================================== + +/// Minimal SSA storage so the mocks can satisfy [`Env`] without pulling in the +/// real engines. +#[derive(Default)] +struct MockStore(HashMap<(usize, SSAValue), i64>); + +// =========================================================================== +// 1. BlockOnlyEngine — walks blocks, and nothing else +// =========================================================================== + +/// Implements: [`Interp`], [`Env`], [`StatementDispatch`], [`BlockQueries`]. +/// +/// **Deliberately omits**: [`CallServices`] (no `alloc_env`/`free_env`/ +/// `resolve_call`/`enter_function`), [`CFGQueries`] (no +/// `cfg_entry`), and [`DiGraphQueries`] (no `digraph_walk_plan`). +/// +/// So this engine cannot enter a function, cannot find a CFG's entry block, and +/// cannot schedule a graph — yet it can still run the block walker. +#[derive(Default)] +struct BlockOnlyEngine { + store: MockStore, +} + +impl Interp for BlockOnlyEngine { + type Value = i64; + type Error = InterpreterError; + type Effect = SparseForwardEffect; + type Semantics = ForwardEval; + + fn stage(&self) -> CompileStage { + unimplemented!("type-level mock") + } + fn statement(&self) -> Statement { + unimplemented!("type-level mock") + } + fn index(&self) -> EnvIndex { + unimplemented!("type-level mock") + } +} + +impl Env for BlockOnlyEngine { + fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { + self.store + .0 + .get(&(index.raw(), value)) + .copied() + .ok_or(InterpreterError::UnboundValue { index, value }) + } + + fn env_write( + &mut self, + index: EnvIndex, + value: SSAValue, + data: i64, + ) -> Result<(), InterpreterError> { + self.store.0.insert((index.raw(), value), data); + Ok(()) + } +} + +impl StatementDispatch for BlockOnlyEngine { + fn run_statement( + &mut self, + _stage: CompileStage, + _statement: Statement, + _index: EnvIndex, + ) -> Result { + unimplemented!("type-level mock") + } +} + +impl BlockQueries for BlockOnlyEngine { + fn block_params( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn first_statement( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn next_statement( + &self, + _stage: CompileStage, + _block: Block, + _after: Statement, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +/// A total concrete frame type for the mocks. +/// +/// Note it *carries* a [`CallFrame`] variant and implements +/// [`FrameBuild::from_call`]: **building** the universe is independent of whether +/// a given engine can **step** every variant. `BlockOnlyEngine` can step the +/// block walker but could never step this `Call` variant — and that is exactly +/// the separation the capability split expresses, so no `Frame` impl is +/// asserted for `MockFrame` itself. +enum MockFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), +} + +impl FrameBuild for MockFrame { + type BodyFrames = DefaultBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + MockFrame::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + MockFrame::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + MockFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + MockFrame::DiGraph(frame) + } +} + +#[test] +fn block_frame_runs_on_an_engine_with_only_block_queries_and_dispatch() { + assert_frame::>(); +} + +// =========================================================================== +// 2. CallOnlyEngine — performs the call lifecycle, and nothing else +// =========================================================================== + +/// Implements: [`Interp`], [`Env`], [`CallServices`]. +/// +/// **Deliberately omits**: [`StatementDispatch`] (cannot dispatch a +/// statement), [`BlockQueries`], [`CFGQueries`], and +/// [`DiGraphQueries`] (cannot query any body shape). +/// +/// Its `Effect` is `()`, not a [`SparseForwardEffect`] — proof that +/// [`CallFrame`] needs neither a statement-effect algebra nor +/// [`SparseForwardInterp`](kirin_interpreter::SparseForwardInterp). The call +/// boundary only allocates, resolves, enters, suspends, frees, and binds +/// results. +#[derive(Default)] +struct CallOnlyEngine { + store: MockStore, +} + +impl Interp for CallOnlyEngine { + type Value = i64; + type Error = InterpreterError; + type Effect = (); + type Semantics = ForwardEval; + + fn stage(&self) -> CompileStage { + unimplemented!("type-level mock") + } + fn statement(&self) -> Statement { + unimplemented!("type-level mock") + } + fn index(&self) -> EnvIndex { + unimplemented!("type-level mock") + } +} + +impl Env for CallOnlyEngine { + fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { + self.store + .0 + .get(&(index.raw(), value)) + .copied() + .ok_or(InterpreterError::UnboundValue { index, value }) + } + + fn env_write( + &mut self, + index: EnvIndex, + value: SSAValue, + data: i64, + ) -> Result<(), InterpreterError> { + self.store.0.insert((index.raw(), value), data); + Ok(()) + } +} + +impl CallServices for CallOnlyEngine { + fn alloc_env(&mut self) -> EnvIndex { + unimplemented!("type-level mock") + } + fn free_env(&mut self, _index: EnvIndex) -> Result<(), InterpreterError> { + unimplemented!("type-level mock") + } + fn resolve_call( + &self, + _stage: CompileStage, + _callee: &Callee, + ) -> Result { + unimplemented!("type-level mock") + } + fn enter_function( + &mut self, + _stage: CompileStage, + _body: Statement, + _args: Product, + _index: EnvIndex, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +#[test] +fn call_frame_runs_on_an_engine_with_only_call_services() { + assert_frame::>(); +} + +// =========================================================================== +// 3. AbstractOnlyEngine — abstract dataflow with no concrete call lifecycle +// =========================================================================== + +/// Implements: [`Interp`], [`Env`], [`StatementDispatch`], +/// [`BlockQueries`], [`DiGraphQueries`], and +/// [`ForwardDataflowFrameEngine`]. +/// +/// **Deliberately omits**: [`CallServices`] and +/// [`CFGQueries`]. +/// +/// This is the assertion that carries item #3's main claim. An abstract engine +/// *summarizes* a call ([`ForwardDataflowFrameEngine::summarize_call`]) instead +/// of descending into it, and reaches a callable body's entry block through +/// owner seeding rather than `cfg_entry` — so it should not have to expose +/// activation allocation, activation cleanup, `enter_function`, `resolve_call`, +/// or `cfg_entry` merely to be an abstract dataflow engine. Before the split it +/// did. +#[derive(Default)] +struct AbstractOnlyEngine { + store: MockStore, +} + +impl Interp for AbstractOnlyEngine { + type Value = i64; + type Error = InterpreterError; + type Effect = SparseForwardEffect; + type Semantics = ForwardEval; + + fn stage(&self) -> CompileStage { + unimplemented!("type-level mock") + } + fn statement(&self) -> Statement { + unimplemented!("type-level mock") + } + fn index(&self) -> EnvIndex { + unimplemented!("type-level mock") + } +} + +impl Env for AbstractOnlyEngine { + fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { + self.store + .0 + .get(&(index.raw(), value)) + .copied() + .ok_or(InterpreterError::UnboundValue { index, value }) + } + + fn env_write( + &mut self, + index: EnvIndex, + value: SSAValue, + data: i64, + ) -> Result<(), InterpreterError> { + self.store.0.insert((index.raw(), value), data); + Ok(()) + } +} + +impl StatementDispatch for AbstractOnlyEngine { + fn run_statement( + &mut self, + _stage: CompileStage, + _statement: Statement, + _index: EnvIndex, + ) -> Result { + unimplemented!("type-level mock") + } +} + +impl BlockQueries for AbstractOnlyEngine { + fn block_params( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn first_statement( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn next_statement( + &self, + _stage: CompileStage, + _block: Block, + _after: Statement, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +/// Taken as-is: the `NoDefaultWalker` default is the whole point of +/// [`DiGraphQueries`] being a separate capability an engine opts into. +impl DiGraphQueries for AbstractOnlyEngine {} + +impl ForwardDataflowFrameEngine for AbstractOnlyEngine { + type SummaryKey = (); + + fn analysis_merge( + &self, + _current: &Product, + _incoming: &Product, + _visits: usize, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + + fn contribute_return(&mut self, _values: Product) -> Result<(), InterpreterError> { + unimplemented!("type-level mock") + } + + fn current_function_key(&self) -> Option<()> { + unimplemented!("type-level mock") + } + + fn summarize_call( + &mut self, + _stage: CompileStage, + _call: CallEffect, + _index: EnvIndex, + ) -> Result<(), InterpreterError> { + unimplemented!("type-level mock") + } + + fn max_iterations(&self) -> usize { + unimplemented!("type-level mock") + } +} + +/// The abstract counterpart of [`MockFrame`], again carrying the call variant it +/// can build but this engine could never step. +enum MockAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), +} + +impl AbstractFrameBuild for MockAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + MockAbstractFrame::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + MockAbstractFrame::Call(frame) + } + fn from_digraph( + frame: AbstractDiGraphFrame, + ) -> Result { + Ok(MockAbstractFrame::DiGraph(frame)) + } +} + +#[test] +fn abstract_engine_needs_no_concrete_call_lifecycle() { + assert_dataflow_engine::(); + + // And the abstract frames it drives really do run on it — including + // `AbstractCallFrame`, whose only engine requirement is `summarize_call`. + // That is the split's payoff: summarizing a call needs no call convention. + assert_frame::< + AbstractOnlyEngine, + MockAbstractFrame, + AbstractBlockFrame, + >(); + assert_frame::< + AbstractOnlyEngine, + MockAbstractFrame, + AbstractCallFrame, + >(); + assert_frame::< + AbstractOnlyEngine, + MockAbstractFrame, + AbstractDiGraphFrame, + >(); +} + +// =========================================================================== +// 4. The umbrellas still work where a universe needs them +// =========================================================================== + +/// The narrowing must not cost the umbrella: a total frame enum's engine has to +/// support the union of all its variants, so [`ForwardFrameEngine`] remains the +/// right bound there. +/// +/// This needs no instantiation — a generic function body is type-checked at +/// *definition* time, so `needs_all::()` fails to compile the moment +/// `ForwardFrameEngine` stops implying all four components (e.g. if the blanket +/// impl were dropped, or a fifth component added to the umbrella without an +/// impl). +#[allow(dead_code)] +fn umbrella_still_covers_every_component() { + fn needs_all() + where + J: StatementDispatch + BlockQueries + DiGraphQueries + CallServices, + { + } + needs_all::(); +} + +/// Conversely: [`ForwardDataflowFrameEngine`] must keep implying the three +/// traversal components it does extend, so abstract frames can rely on them. +#[allow(dead_code)] +fn dataflow_umbrella_covers_its_three_components() { + fn needs_traversal() + where + J: StatementDispatch + BlockQueries + DiGraphQueries, + { + } + needs_traversal::(); +} + +// =========================================================================== +// 5. The `*Queries` traits are honestly read-only +// =========================================================================== + +/// Implements: [`Interp`], [`BlockQueries`], [`CFGQueries`], [`DiGraphQueries`]. +/// +/// **Deliberately omits [`Env`]** — it has no SSA storage at all, not even a +/// field for it. +/// +/// This is the assertion that keeps the *names* truthful. Each `*Queries` trait +/// requires only `Interp`, so none of their methods can touch the store; the +/// one operation that needs both a query and a write (binding a block's +/// parameters) lives on the crate-private `BlockBinding: Env + BlockQueries` +/// instead. Re-adding `Env` as a `*Queries` supertrait — the obvious way to +/// smuggle a mutating default method back in — stops this engine from compiling. +struct QueriesOnlyEngine; + +impl Interp for QueriesOnlyEngine { + type Value = i64; + type Error = InterpreterError; + type Effect = (); + type Semantics = ForwardEval; + + fn stage(&self) -> CompileStage { + unimplemented!("type-level mock") + } + fn statement(&self) -> Statement { + unimplemented!("type-level mock") + } + fn index(&self) -> EnvIndex { + unimplemented!("type-level mock") + } +} + +impl BlockQueries for QueriesOnlyEngine { + fn block_params( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn first_statement( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn next_statement( + &self, + _stage: CompileStage, + _block: Block, + _after: Statement, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +impl CFGQueries for QueriesOnlyEngine { + fn cfg_entry( + &self, + _stage: CompileStage, + _cfg: kirin_ir::CFG, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +impl DiGraphQueries for QueriesOnlyEngine {} + +#[test] +fn query_traits_are_satisfiable_without_env() { + assert_read_only_queries::(); +}