diff --git a/AGENTS.md b/AGENTS.md index 6001475c8..b1f3b985e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,8 +106,8 @@ For user-defined dialects not in this table, ask the user for domain context dur **Analysis:** - `kirin-interval` — Interval domain for abstract interpretation -- `kirin-demand` — strong liveness: interprocedural per-SSA-value demand (`analyze_demand`, sparse backward; `DemandResult` per reached callable context) -- `kirin-liveness` — classic per-point liveness (`analyze_dense`, dense backward, intraprocedural) +- `kirin-demand` — strong liveness: interprocedural per-SSA-value demand (`DemandAnalysis`, sparse backward; facts per reached callable context) +- `kirin-liveness` — classic per-point liveness (`DenseLiveness`, dense backward, intraprocedural) **Testing:** - `kirin-test-types` — Pure test type definitions (`UnitType`, `SimpleType`, `Value`) @@ -156,7 +156,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Calling conventions are linkers**: `Linker` resolves `Callee` relative to `lookup_stage` into `LinkTarget { stage, specialization }` 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. Body discovery is the getter `LinkTarget::body(pipeline)`, which reads the specialization's authoritative definition at `target.stage` through IR's `HasCallableBody`. The specialization record remains authoritative; the getter avoids storing a redundant target/body pair. Built-in `CallServices::discover_body` implementations delegate to the getter, and custom implementations must preserve its lookup and error behavior; the trait does not enforce this relationship. Engines holding a pipeline call the getter; `CallFrame` cannot reach one, so `CallServices` splits `resolve_callee` (identity) from `discover_body` (discovery). Linking selects identity; discovery reads structure; engines initialize their own boundary inputs. Policy must be a component (field), never a trait impl on an engine type. -- **Environments: one container, one capability**: `EnvStore` (`core/env/store.rs`) is the storage container and owns *both* halves of environment identity — `context_indices: HashMap` and per-environment `FactStore` fact maps (never flattened into one scope-qualified map). Its API is `alloc`/`get_or_allocate(K)`/`context_env`/`contexts`/`read`/`write`/`join`/`free`/`environment`. Invariants: `alloc` is always fresh and unkeyed; equal keys reuse one live environment; distinct keys isolate identical anchors; freed indices are never reused and `free` drops the context association (each environment remembers its own key, so this is O(1) — never scan); `write` assigns and never joins; `read` returns `Ok(None)` for an absent anchor and errors only on a dead `EnvIndex`, so **absence is distinguishable from invalidity**. Storage knows nothing about widening, dependencies, or scheduling. Analysis updates use `join` (absent is bottom; joining bottom stores nothing), which merges via `Lattice::join_assign` and returns whether the fact rose; the engine schedules dependents. All three abstract engines merge through `join`, except that constprop's block and function entries merge through its `WideningStrategy` (join, then widen by visits) and are then written. Constprop's store anchor is `SSAValue`; its return is context metadata, `ForwardContext { ret: Option>, callers }` (`None` = never returned, distinct from returning nothing): a reached `ret` joins into it slot by slot and, if it rose, reruns the callers. `Env: Interp` (`core/env/services.rs`) is the anchor-generic engine capability for *using* an environment: `type Anchor: LatticeAnchor`, with `env_read`/`env_write` accepting `Self::Anchor`. Keep the `LatticeAnchor` requirement. Each engine decides what an access *means* (concrete: unbound read is an error; sparse forward: absent is bottom, and a write joins). Forward engines pin `Anchor = SSAValue`; the access interface also supports `ProgramPoint` anchors. SSA positional binding lives on `SSABinding: Env`, blanket-implemented for every SSA-anchored environment. Its `bind_values` checks arity and writes through `env_write`; block-entry binding builds on it, so engine policy applies to bound values too. Binding does not require `SparseForwardInterp` or a forward statement-effect algebra. Two things stay off it: activation *lifetime* (`alloc_env`/`free_env` are on the sibling `CallServices`, because creating and retiring an activation is the call boundary's business) and context-key *selection* (choosing `K` is analysis policy via `CallContext`, so `get_or_allocate` stays internal to the engine that has a policy — sparse forward's `context_env`/`lookup_context_env`). Concrete execution uses `EnvStore` — an uninhabited key makes accidental sharing impossible. The layering is: analysis chooses context identity → `EnvStore` maps it to storage → `Env` gives an access meaning → the sibling `CallServices` supplies the lifetime primitives → `CallFrame` decides when they run → the fixpoint driver owns dependencies and the worklist. Centralized environment *storage*, deliberately without centralized call control. Each abstract engine's transfer records the facts that rose in a `changed` log, which the driver drains after each work item to schedule dependents; forward finds an SSA fact's dependents from the IR use list (`query::value_units`: the innermost reached unit around each use, skipping the defining unit), so it keeps no reader map; work items are `(EnvIndex, Body)` (`Body::Block` for a CFG block, `Body::DiGraph` for a graph). +- **Environments: one container, one capability**: `EnvStore` (`core/env/store.rs`) is the storage container and owns *both* halves of environment identity — `context_indices: HashMap` and per-environment `FactStore` fact maps (never flattened into one scope-qualified map). Its API is `alloc`/`get_or_allocate(K)`/`context_env`/`key`/`contexts`/`read`/`write`/`join`/`free`/`environment`. Invariants: `alloc` is always fresh and unkeyed; equal keys reuse one live environment; distinct keys isolate identical anchors; freed indices are never reused and `free` drops the context association (each environment remembers its own key, so this is O(1) — never scan); `write` assigns and never joins; `read` returns `Ok(None)` for an absent anchor and errors only on a dead `EnvIndex`, so **absence is distinguishable from invalidity**. Storage knows nothing about widening, dependencies, or scheduling. Analysis updates use `join` (absent is bottom; joining bottom stores nothing), which merges via `Lattice::join_assign` and returns whether the fact rose; the engine schedules dependents. All three abstract engines merge through `join`, except that constprop's block and function entries merge through its `WideningStrategy` (join, then widen by visits) and are then written. Constprop's store anchor is `SSAValue`; its return is context metadata, `ForwardContext { ret: Option>, callers }` (`None` = never returned, distinct from returning nothing): a reached `ret` joins into it slot by slot and, if it rose, reruns the callers. `Env: Interp` (`core/env/services.rs`) is the anchor-generic engine capability for *using* an environment: `type Anchor: LatticeAnchor`, with `env_read`/`env_write` accepting `Self::Anchor`. Keep the `LatticeAnchor` requirement. Each engine decides what an access *means* (concrete: unbound read is an error; sparse forward: absent is bottom, and a write joins). Forward engines pin `Anchor = SSAValue`; the access interface also supports `ProgramPoint` anchors. SSA positional binding lives on `SSABinding: Env`, blanket-implemented for every SSA-anchored environment. Its `bind_values` checks arity and writes through `env_write`; block-entry binding builds on it, so engine policy applies to bound values too. Binding does not require `SparseForwardInterp` or a forward statement-effect algebra. Two things stay off it: activation *lifetime* (`alloc_env`/`free_env` are on the sibling `CallServices`, because creating and retiring an activation is the call boundary's business) and context-key *selection* (choosing `K` is analysis policy via `CallContext`, so `get_or_allocate` stays internal to the engine that has a policy — sparse forward's `context_env`/`lookup_context_env`). Concrete execution uses `EnvStore` — an uninhabited key makes accidental sharing impossible. The layering is: analysis chooses context identity → `EnvStore` maps it to storage → `Env` gives an access meaning → the sibling `CallServices` supplies the lifetime primitives → `CallFrame` decides when they run → the fixpoint driver owns dependencies and the worklist. Centralized environment *storage*, deliberately without centralized call control. Each abstract engine's transfer records the facts that rose in a `changed` log, which the driver drains after each work item to schedule dependents; forward finds an SSA fact's dependents from the IR use list (`query::value_units`: the innermost reached unit around each use, skipping the defining unit), so it keeps no reader map; work items are `(EnvIndex, Body)` (`Body::Block` for a CFG block, `Body::DiGraph` for a graph). - **Engines run frames; traversal lives in frames**: all frame-using engines share one driver loop, `drive_frames` (`core/frame.rs`), over the direction-neutral `Frame` protocol — pop the top stack item, call `step_into`, and apply its `FrameEffect`, while owning no representation traversal itself. A reusable *member continuation* returns `Self` for `Continue` and `Push.parent`; the generic currently named `F` is only the configured representation of a differently typed pushed child. A private closed *frame stack item* enum is the composition root: it owns `From` conversions, exhaustively dispatches to members, and uses `FrameEffect::map_next` to wrap returned member state. `drive_frames` sees only the homogeneous stack-item type and requires it to use itself as its child representation. `FrameEngine` remains the minimal anchor (just a total `Error`), so this protocol is independent of forward evaluation. `ConcreteInterpreter` hides the framework-default concrete stack item; a language with additional continuations wraps `ConcreteInterpreterCore<.., FrameStackItem>` behind its own API. Sparse-forward currently exposes `StandardAbstractFrame` as its default composition pending the separate facade/core review. Dense backward uses `DenseBlockFrame` directly by default and a language-private stack-item enum when structured continuations coexist. Sparse backward's `DemandFrame` is already its complete homogeneous composition because it never pushes a differently typed child. Analysis crates remain a lattice plus an engine/policy/composition choice (see `kirin-constprop`, `kirin-demand`, and `kirin-liveness`). @@ -178,7 +178,9 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Function dialect naming**: `kirin_function::Function` is the standard function statement. New code should use `Function` with its derived `HasCallableBody` and `SparseForwardEffect::Call`/`SparseForwardEffect::Return`. -- **Backward analyses (implemented)**: liveness ships as **two** analyses, both real framework clients: strong liveness in `kirin-demand` and classic per-point liveness in `kirin-liveness`; neither crate depends on the other. *Strong liveness* (`analyze_demand`, `StrongDemand`) is interprocedural per-SSA-value demand: facts are keyed `(EnvIndex, SSAValue)` in the transfer's `EnvStore`, one environment per callable context `K` selected by a `DemandContext` policy (default `ContextInsensitive` → `LinkTarget`; never a global body scope — the work carries its context), and work items are `(EnvIndex, Statement)`. Rules dispatch on `SparseBackwardTransfer`, as forward and dense rules dispatch on their transfers. When a fact rises (`EnvStore::join` returns `true`), the driver reruns the statements that translate it (defining statement, predecessors' terminators, or graph-port owner); the one registered dependency is callee parameter fact → the caller's call statement (`DemandCallers`, the driver's store slot), registered when a call rule returns `SparseBackwardEffect::Call`, even for bottom reads. Ordinary dialects are a one-liner (`interp.demand_uses_if_observable(self)` on `DemandInterp` — purity-aware via `IsPure`). Roots are requested outputs, control roots (`cf.cond_br`/`scf.if` conditions, `scf.for` bounds), and impure ordinary statements — **not** returns or calls: `ret` declares operand `i` as output `i` during discovery (`declare_return`) and a requested output merges directly into those operands, and calls activate only through demanded results (the `Call` effect), so ignored/zero-result calls demand nothing (demandedness only, no effect preservation). Selecting a context allocates its environment at once, so callers can register on its parameters; the context is discovered once, on activation (recorded before its scan, which skips nested callable definitions; its metadata exists only once discovered, and entry parameters are read from the IR); discovery is applied after the activating work completes, so rule dispatch never re-enters. SCF needs no heterogeneous continuation stack (loop-carried demand converges on the value worklist; the SCF rules use `block_params`/`terminator_args` queries). *Classic per-point liveness* (`analyze_dense`, `ClassicLiveness`) is the textbook kill-defs/gen-all-uses transfer over block walks (`DenseBlockFrame`, `absorb_edges` maps successor live-ins across edges with pass-through for dominated direct cross-block uses). Each `analyze` solves one callable in one `EnvStore` environment on the transfer; work items are `(EnvIndex, Body::Block(block))`; every recorded point joins into the environment (a recorded bottom point stays present), and a rise of `BlockEntry(b)` reruns the predecessors registered when their walk read it (`DenseReaders`, the driver's store slot). Queries take a bare `ProgramPoint` (`point_facts`, `facts`); SCF owns dense member continuations (`DenseScfIfFrame` arm join, `DenseScfForFrame` loop fixpoint), and a language-private stack-item enum composes them with `DenseBlockFrame` through narrow `From` conversions (see toy-lang's `ToyDenseBackwardFrame`). Filtering a dense set by demand drops values no root needs, but only over-approximates per-point strong liveness: a value can be live at a point through a faint use while its demand comes from another use. CFG topology (blocks incl. nested bodies, feeders) comes from `StageQuery` actions — enumeration only; use/def/edge-arg *semantics* stay in dialect rules. One dialect carries one rule per semantic key without coherence conflicts, and two keys can share one shape — the shipped dialects (each carrying `ForwardEval` + `StrongDemand` + `ClassicLiveness` rules) are the living evidence. Dense point-observation ownership and the adequacy of block owners/body coverage are explicitly deferred architecture questions, not claims established by the current composition. +- **Reading results: `Analyzed` + `AnalysisResults`**: every abstract engine's `analyze` (and `analyze_by_name`/`analyze_by_symbol`) consumes the engine and returns `Analyzed` (`engines/results.rs`), the converged state of exactly one analysis. `Analyzed` has no `analyze`, so an analysis is always fresh and has one root; analyzing another root takes another engine. All three engines implement the one read-only trait `AnalysisResults` on it. A *context* is one analyzed copy of a callable under specific boundary facts (`Context` is the policy key for the sparse engines and the `LinkTarget` for liveness). Required, per context: `root`, `contexts`, `callable_of(ctx)` (identity: which `LinkTarget` the context is a copy of), `fact(ctx, anchor)`/`facts(ctx)` (data; absent is bottom), and `response(ctx)` — what the context answers its caller: constprop's return product (`None` = no return reached, distinct from an empty product), demand's entry-parameter facts, and `None` for liveness, which follows no calls. Provided once for all engines, per callable: `callables`, `contexts_of`, `joined_fact`, `joined_facts`, `joined_response` — joins over the contexts **reached from the root**, computed on request and never stored; they claim nothing about callers the analysis did not visit. `Analyzed` stores only the callable → environments index (`by_callable`, built once by `analyze`), which the sparse engines' `contexts_of` reads through `EnvStore::key`, so a per-callable query never scans the other callables' contexts. The root is a handle to one context, not a third level of facts. `analyze` returns no boundary product: read `response(root)`. Do not add per-engine result structs, result-copying snapshots, or free `analyze_*` functions; the analysis crates export only a lattice, a policy, and an engine alias. Keep every per-context query keyed by context (a context is a query key and `response`/`facts` its value), so a later query-based engine changes the implementation, not this API. + +- **Backward analyses (implemented)**: liveness ships as **two** analyses, both real framework clients: strong liveness in `kirin-demand` and classic per-point liveness in `kirin-liveness`; neither crate depends on the other. *Strong liveness* (`DemandAnalysis`, `StrongDemand`) is interprocedural per-SSA-value demand: facts are keyed `(EnvIndex, SSAValue)` in the transfer's `EnvStore`, one environment per callable context `K` selected by a `DemandContext` policy (default `ContextInsensitive` → `LinkTarget`; never a global body scope — the work carries its context), and work items are `(EnvIndex, Statement)`. Rules dispatch on `SparseBackwardTransfer`, as forward and dense rules dispatch on their transfers. When a fact rises (`EnvStore::join` returns `true`), the driver reruns the statements that translate it (defining statement, predecessors' terminators, or graph-port owner); the one registered dependency is callee parameter fact → the caller's call statement (`DemandCallers`, the driver's store slot), registered when a call rule returns `SparseBackwardEffect::Call`, even for bottom reads. Ordinary dialects are a one-liner (`interp.demand_uses_if_observable(self)` on `DemandInterp` — purity-aware via `IsPure`). Roots are requested outputs, control roots (`cf.cond_br`/`scf.if` conditions, `scf.for` bounds), and impure ordinary statements — **not** returns or calls: `ret` declares operand `i` as output `i` during discovery (`declare_return`) and a requested output merges directly into those operands, and calls activate only through demanded results (the `Call` effect), so ignored/zero-result calls demand nothing (demandedness only, no effect preservation). Selecting a context allocates its environment at once, so callers can register on its parameters; the context is discovered once, on activation (recorded before its scan, which skips nested callable definitions; its metadata exists only once discovered, and entry parameters are read from the IR); discovery is applied after the activating work completes, so rule dispatch never re-enters. SCF needs no heterogeneous continuation stack (loop-carried demand converges on the value worklist; the SCF rules use `block_params`/`terminator_args` queries). *Classic per-point liveness* (`DenseLiveness`, `ClassicLiveness`) is the textbook kill-defs/gen-all-uses transfer over block walks (`DenseBlockFrame`, `absorb_edges` maps successor live-ins across edges with pass-through for dominated direct cross-block uses). Each `analyze` solves one callable in one `EnvStore` environment on the transfer; work items are `(EnvIndex, Body::Block(block))`; every recorded point joins into the environment (a recorded bottom point stays present), and a rise of `BlockEntry(b)` reruns the predecessors registered when their walk read it (`DenseReaders`, the driver's store slot). Results are read through `AnalysisResults` with the analyzed `LinkTarget` as the only context (`fact(root, point)`); SCF owns dense member continuations (`DenseScfIfFrame` arm join, `DenseScfForFrame` loop fixpoint), and a language-private stack-item enum composes them with `DenseBlockFrame` through narrow `From` conversions (see toy-lang's `ToyDenseBackwardFrame`). Filtering a dense set by demand drops values no root needs, but only over-approximates per-point strong liveness: a value can be live at a point through a faint use while its demand comes from another use. CFG topology (blocks incl. nested bodies, feeders) comes from `StageQuery` actions — enumeration only; use/def/edge-arg *semantics* stay in dialect rules. One dialect carries one rule per semantic key without coherence conflicts, and two keys can share one shape — the shipped dialects (each carrying `ForwardEval` + `StrongDemand` + `ClassicLiveness` rules) are the living evidence. Dense point-observation ownership and the adequacy of block owners/body coverage are explicitly deferred architecture questions, not claims established by the current composition. ## Chumsky Parser Conventions diff --git a/crates/kirin-constprop/src/lib.rs b/crates/kirin-constprop/src/lib.rs index 88580a239..511a7cf46 100644 --- a/crates/kirin-constprop/src/lib.rs +++ b/crates/kirin-constprop/src/lib.rs @@ -15,9 +15,12 @@ //! use kirin_constprop::{ConstProp, ConstPropValue}; //! use kirin_interpreter::engine::CrossStageLinker; //! -//! let analysis = ConstProp::::new(&pipeline) -//! .with_linker(CrossStageLinker); -//! let (result, analysis) = analysis.analyze_by_name("source", "abs", [ConstPropValue::Const(value)])?; +//! use kirin_interpreter::engine::AnalysisResults; +//! +//! let analyzed = ConstProp::::new(&pipeline) +//! .with_linker(CrossStageLinker) +//! .analyze_by_name("source", "abs", [ConstPropValue::Const(value)])?; +//! let result = analyzed.response(analyzed.root()); //! ``` mod context; diff --git a/crates/kirin-demand/src/lib.rs b/crates/kirin-demand/src/lib.rs index a1af33f21..f1f7b657b 100644 --- a/crates/kirin-demand/src/lib.rs +++ b/crates/kirin-demand/src/lib.rs @@ -2,9 +2,10 @@ //! needed by a root?* //! //! This crate is the sparse backward demand analysis: one [`Demand`] bit per -//! SSA value in each reached callable context, computed by [`analyze_demand`] -//! on the shared sparse backward engine -//! ([`kirin_interpreter::SparseBackwardInterpreter`]). +//! SSA value in each reached callable context, computed by [`DemandAnalysis`], +//! the shared sparse backward engine +//! ([`kirin_interpreter::SparseBackwardInterpreter`]) at the [`Demand`] +//! lattice. //! Roots are the requested outputs of the analyzed callable, control roots //! (branch conditions, loop bounds), and impure ordinary statements. Demand //! crosses supported calls: a demanded call result requests the matching @@ -16,23 +17,19 @@ //! there is no structural fallback. //! //! ```ignore -//! let result = kirin_demand::analyze_demand(&pipeline, stage, callee, [Demand::Live])?; -//! assert!(result.is_demanded(some_value)); +//! use kirin_interpreter::AnalysisResults; +//! +//! let analyzed = DemandAnalysis::::new(&pipeline).analyze(stage, callee, [Demand::Live])?; +//! assert!(analyzed.is_demanded(analyzed.root(), some_value)); //! ``` mod context; -mod result; mod value; pub use context::{DemandCallCtx, OutputDemandContext}; -pub use result::DemandResult; pub use value::Demand; -use kirin_interpreter::{ - Callee, InterpDispatch, InterpreterError, SameStageLinker, SparseBackwardInterpreter, - SparseBackwardTransfer, StageQuery, -}; -use kirin_ir::{CompileStage, Pipeline, StageMeta}; +use kirin_interpreter::{InterpreterError, SameStageLinker, SparseBackwardInterpreter}; /// The sparse backward demand engine instantiated at the [`Demand`] lattice: /// strong liveness. @@ -43,29 +40,3 @@ pub type DemandAnalysis< Lk = SameStageLinker, P = OutputDemandContext, > = SparseBackwardInterpreter<'ir, S, Demand, E, Lk, P>; - -/// Resolve `callee` and run strong liveness (sparse backward demand) from -/// its callable body. Supply one lattice value per result, in result order. -pub fn analyze_demand<'ir, S>( - pipeline: &'ir Pipeline, - stage: CompileStage, - callee: Callee, - result_values: impl IntoIterator, -) -> Result -where - S: StageMeta - + StageQuery - + InterpDispatch< - SparseBackwardTransfer< - 'ir, - S, - Demand, - InterpreterError, - SameStageLinker, - OutputDemandContext, - >, - >, -{ - let (_, engine) = DemandAnalysis::::new(pipeline).analyze(stage, callee, result_values)?; - DemandResult::from_engine(&engine) -} diff --git a/crates/kirin-demand/src/result.rs b/crates/kirin-demand/src/result.rs deleted file mode 100644 index e39d02e71..000000000 --- a/crates/kirin-demand/src/result.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! The demand result: the demanded SSA values of every reached context. - -use std::collections::{HashMap, HashSet}; -use std::hash::Hash; - -use kirin_interpreter::{ - BodyScope, DemandContext, InterpreterError, LinkTarget, SparseBackwardInterpreter, -}; -use kirin_ir::{SSAValue, StageMeta}; - -use crate::{Demand, DemandCallCtx}; - -/// The result of [`analyze_demand`](crate::analyze_demand): the demanded SSA -/// values of every callable context the analysis reached, keyed by the -/// engine's context key `K` (default: [`LinkTarget`] plus [`DemandCallCtx`]). -/// -/// The root-scoped accessors ([`demanded`](Self::demanded), -/// [`is_demanded`](Self::is_demanded)) read the analyzed root's context; -/// [`contexts`](Self::contexts) lists every reached context. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DemandResult -where - K: Eq + Hash, -{ - root: K, - contexts: HashMap)>, -} - -impl DemandResult { - /// Copy the demanded sets of every context a converged engine discovered. - pub fn from_engine( - engine: &SparseBackwardInterpreter<'_, S, Demand, E, Lk, P>, - ) -> Result - where - S: StageMeta, - E: From, - P: DemandContext, - { - let root = engine.root().cloned().ok_or(InterpreterError::Custom( - "demand analysis has no successful root", - ))?; - let mut contexts = HashMap::new(); - for context in engine.contexts() { - let scope = engine.scope(context).ok_or(InterpreterError::Custom( - "discovered demand context has no scope", - ))?; - let demanded = engine - .facts(context) - .filter(|(_, fact)| fact.is_live()) - .map(|(value, _)| value) - .collect(); - contexts.insert(context.clone(), (scope, demanded)); - } - if !contexts.contains_key(&root) { - return Err(InterpreterError::Custom( - "demand root context was never discovered", - )); - } - Ok(Self { root, contexts }) - } - - /// The analyzed root's context key. - pub fn root(&self) -> &K { - &self.root - } - - /// The resolved `(target stage, callable body)` analyzed by the root. - pub fn root_scope(&self) -> BodyScope { - self.contexts[&self.root].0 - } - - /// The root's demanded SSA values (the strong-liveness fact). - pub fn demanded(&self) -> &HashSet { - &self.contexts[&self.root].1 - } - - /// `true` iff `value` is transitively needed by a root in the root's - /// context. - pub fn is_demanded(&self, value: impl Into) -> bool { - self.demanded().contains(&value.into()) - } - - /// Every context the analysis reached, in no particular order. - pub fn contexts(&self) -> impl Iterator { - self.contexts.keys() - } -} diff --git a/crates/kirin-demand/tests/cfg.rs b/crates/kirin-demand/tests/cfg.rs index a9dddaef8..59830eb9b 100644 --- a/crates/kirin-demand/tests/cfg.rs +++ b/crates/kirin-demand/tests/cfg.rs @@ -4,8 +4,8 @@ use kirin::prelude::{GetInfo, ParsePipelineText, Pipeline, SSAValue, StageInfo}; use kirin_arith::Arith; -use kirin_demand::{Demand, analyze_demand}; -use kirin_interpreter::{Body, Callee}; +use kirin_demand::{Demand, DemandAnalysis}; +use kirin_interpreter::{AnalysisResults, Body, Callee}; use kirin_test_languages::ArithFunctionLanguage; const PROGRAM: &str = r#" @@ -103,9 +103,11 @@ fn find_arith( fn strong_liveness_over_branching_function() { let pipeline = parse(PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = - analyze_demand(&pipeline, caller_stage, callee, [Demand::Live]).expect("analysis succeeds"); - let (_, Body::CFG(cfg)) = result.root_scope() else { + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let Ok(Body::CFG(cfg)) = root.body(&pipeline) else { panic!("@main resolves to a CFG body") }; @@ -120,19 +122,25 @@ fn strong_liveness_over_branching_function() { Arith::Add { lhs, result, .. } => Some((SSAValue::from(*result), *lhs)), _ => None, }); - assert!(!result.is_demanded(dead)); + assert!(!result.is_demanded(result.root(), dead)); assert_eq!(add_lhs, x, "the add reads the entry parameter"); // Roots and their transitive demands: return operands, the branch // condition, and the edge arguments feeding demanded successor params. - assert!(result.is_demanded(cond), "branch condition is a root"); assert!( - result.is_demanded(then_param), + result.is_demanded(result.root(), cond), + "branch condition is a root" + ); + assert!( + result.is_demanded(result.root(), then_param), "ret %a demands ^then's param" ); - assert!(result.is_demanded(else_param), "neg feeds ret in ^else"); assert!( - result.is_demanded(x), + result.is_demanded(result.root(), else_param), + "neg feeds ret in ^else" + ); + assert!( + result.is_demanded(result.root(), x), "both edges pass %x into demanded params" ); @@ -140,16 +148,21 @@ fn strong_liveness_over_branching_function() { Arith::Neg { result, .. } => Some(SSAValue::from(*result)), _ => None, }); - assert!(result.is_demanded(neg), "ret %neg demands the neg result"); + assert!( + result.is_demanded(result.root(), neg), + "ret %neg demands the neg result" + ); } #[test] fn unused_successor_block_argument_does_not_keep_edge_arg_live() { let pipeline = parse(DEAD_EDGE_ARG_PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = - analyze_demand(&pipeline, caller_stage, callee, [Demand::Live]).expect("analysis succeeds"); - let (_, Body::CFG(cfg)) = result.root_scope() else { + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let Ok(Body::CFG(cfg)) = root.body(&pipeline) else { panic!("@main resolves to a CFG body") }; @@ -160,12 +173,15 @@ fn unused_successor_block_argument_does_not_keep_edge_arg_live() { // `^else` returns `%live` directly (a dominated cross-block use), never // touching its own parameter — so the `%dead` edge argument stays dead. - assert!(result.is_demanded(live)); - assert!(result.is_demanded(cond)); - assert!(result.is_demanded(then_param)); - assert!(!result.is_demanded(else_param), "^else's param is unused"); + assert!(result.is_demanded(result.root(), live)); + assert!(result.is_demanded(result.root(), cond)); + assert!(result.is_demanded(result.root(), then_param)); assert!( - !result.is_demanded(dead), + !result.is_demanded(result.root(), else_param), + "^else's param is unused" + ); + assert!( + !result.is_demanded(result.root(), dead), "an edge arg feeding an undemanded param stays dead" ); } @@ -211,23 +227,27 @@ specialize @test fn @main(i64, i64) -> i64 cfg { fn terminator_operands_become_demanded() { let pipeline = parse(RET_PARAM_PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = - analyze_demand(&pipeline, caller_stage, callee, [Demand::Live]).expect("analysis succeeds"); - let (_, Body::CFG(cfg)) = result.root_scope() else { + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let Ok(Body::CFG(cfg)) = root.body(&pipeline) else { panic!("@main resolves to a CFG body") }; let x = block_params(&pipeline, cfg, 0)[0]; - assert!(result.is_demanded(x)); + assert!(result.is_demanded(result.root(), x)); } #[test] fn demanded_result_marks_operands_demanded() { let pipeline = parse(DEMANDED_RESULT_PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = - analyze_demand(&pipeline, caller_stage, callee, [Demand::Live]).expect("analysis succeeds"); - let (_, Body::CFG(cfg)) = result.root_scope() else { + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let Ok(Body::CFG(cfg)) = root.body(&pipeline) else { panic!("@main resolves to a CFG body") }; @@ -236,18 +256,20 @@ fn demanded_result_marks_operands_demanded() { Arith::Add { result, .. } => Some(SSAValue::from(*result)), _ => None, }); - assert!(result.is_demanded(sum)); - assert!(result.is_demanded(params[0])); - assert!(result.is_demanded(params[1])); + assert!(result.is_demanded(result.root(), sum)); + assert!(result.is_demanded(result.root(), params[0])); + assert!(result.is_demanded(result.root(), params[1])); } #[test] fn dead_result_leaves_operands_dead() { let pipeline = parse(DEAD_RESULT_PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = - analyze_demand(&pipeline, caller_stage, callee, [Demand::Live]).expect("analysis succeeds"); - let (_, Body::CFG(cfg)) = result.root_scope() else { + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let Ok(Body::CFG(cfg)) = root.body(&pipeline) else { panic!("@main resolves to a CFG body") }; @@ -256,10 +278,16 @@ fn dead_result_leaves_operands_dead() { Arith::Add { result, .. } => Some(SSAValue::from(*result)), _ => None, }); - assert!(!result.is_demanded(sum), "the add result is never used"); - assert!(result.is_demanded(params[0]), "ret %a demands the param"); assert!( - !result.is_demanded(params[1]), + !result.is_demanded(result.root(), sum), + "the add result is never used" + ); + assert!( + result.is_demanded(result.root(), params[0]), + "ret %a demands the param" + ); + assert!( + !result.is_demanded(result.root(), params[1]), "a pure statement with a dead result demands nothing" ); } diff --git a/crates/kirin-demand/tests/demand_boundary.rs b/crates/kirin-demand/tests/demand_boundary.rs index a43949a2d..d5861bbac 100644 --- a/crates/kirin-demand/tests/demand_boundary.rs +++ b/crates/kirin-demand/tests/demand_boundary.rs @@ -1,8 +1,8 @@ //! Backward boundary packets and context-specific inspection. use kirin::prelude::*; use kirin_demand::OutputDemandContext; -use kirin_demand::{Demand, DemandAnalysis, DemandResult}; -use kirin_interpreter::InterpreterError; +use kirin_demand::{Demand, DemandAnalysis}; +use kirin_interpreter::{AnalysisResults, InterpreterError}; use kirin_test_languages::GraphFunctionLanguage; type TestStage = StageInfo; @@ -57,55 +57,55 @@ fn pipeline() -> Pipeline { #[test] fn output_product_returns_parameter_facts_and_preserves_context_queries() { let pipeline = pipeline(); - let analysis = DemandAnalysis::::new(&pipeline); let outputs: Product<_> = [Demand::Live, Demand::Dead, Demand::Live] .into_iter() .collect(); - let (inputs, analysis) = analysis.analyze_by_name("test", "main", outputs).unwrap(); + let first = DemandAnalysis::::new(&pipeline) + .analyze_by_name("test", "main", outputs) + .unwrap(); assert_eq!( - inputs.as_slice(), + first.response(first.root()).unwrap().as_slice(), &[Demand::Dead, Demand::Live, Demand::Live] ); - let first = analysis.root().unwrap().clone(); - let details = DemandResult::from_engine(&analysis).unwrap(); - assert_eq!(details.root(), &first); assert!( - details.contexts().count() >= 2, + first.contexts().count() >= 2, "callee facts remain queryable" ); let outputs: Product<_> = [Demand::Dead, Demand::Live, Demand::Dead] .into_iter() .collect(); - let (inputs, analysis) = analysis.analyze_by_name("test", "main", outputs).unwrap(); + let second = DemandAnalysis::::new(&pipeline) + .analyze_by_name("test", "main", outputs) + .unwrap(); assert_eq!( - inputs.as_slice(), + second.response(second.root()).unwrap().as_slice(), &[Demand::Live, Demand::Dead, Demand::Dead] ); - assert_ne!(analysis.root(), Some(&first)); - assert_eq!(analysis.result_value(&first, 0), Some(Demand::Live)); + assert_ne!(second.root(), first.root()); } #[test] fn result_values_preserve_control_roots() { let pipeline = pipeline(); - let (inputs, _) = DemandAnalysis::::new(&pipeline) + let analyzed = DemandAnalysis::::new(&pipeline) .analyze_by_name("test", "branch", [Demand::Dead]) .unwrap(); - assert_eq!(inputs.as_slice(), &[Demand::Dead, Demand::Live]); + assert_eq!( + analyzed.response(analyzed.root()).unwrap().as_slice(), + &[Demand::Dead, Demand::Live] + ); } #[test] fn root_arity_errors_are_reported() { let pipeline = pipeline(); - let (_, analysis) = DemandAnalysis::::new(&pipeline) - .with_policy(OutputDemandContext::with_budget(0)) - .analyze_by_name("test", "main", [Demand::Live, Demand::Live, Demand::Live]) - .unwrap(); - assert!(analysis.root().is_some()); let wrong: Product = Product::new(); assert_eq!( - analysis.analyze_by_name("test", "branch", wrong).err(), + DemandAnalysis::::new(&pipeline) + .with_policy(OutputDemandContext::with_budget(0)) + .analyze_by_name("test", "branch", wrong) + .err(), Some(InterpreterError::ProductArityMismatch { expected: 1, actual: 0 @@ -116,14 +116,15 @@ fn root_arity_errors_are_reported() { #[test] fn call_sites_map_one_callee_parameter_to_different_arguments() { let pipeline = pipeline(); - let mut analysis = DemandAnalysis::::new(&pipeline); for (outputs, inputs) in [ ([Demand::Live, Demand::Live], [Demand::Live, Demand::Live]), ([Demand::Live, Demand::Dead], [Demand::Dead, Demand::Live]), ([Demand::Dead, Demand::Live], [Demand::Live, Demand::Dead]), ] { - let (actual, next) = analysis.analyze_by_name("test", "swap", outputs).unwrap(); - analysis = next; + let analyzed = DemandAnalysis::::new(&pipeline) + .analyze_by_name("test", "swap", outputs) + .unwrap(); + let actual = analyzed.response(analyzed.root()).unwrap(); assert_eq!(actual.as_slice(), &inputs, "outputs {outputs:?}"); } } diff --git a/crates/kirin-demand/tests/discovery.rs b/crates/kirin-demand/tests/discovery.rs index 52a061516..290c487c2 100644 --- a/crates/kirin-demand/tests/discovery.rs +++ b/crates/kirin-demand/tests/discovery.rs @@ -1,6 +1,6 @@ use kirin::prelude::*; use kirin_demand::{Demand, DemandAnalysis}; -use kirin_interpreter::{Body, Callee, Linker, SameStageLinker}; +use kirin_interpreter::{AnalysisResults, Body, Callee, Linker, SameStageLinker}; use kirin_test_languages::GraphFunctionLanguage; #[test] @@ -48,13 +48,14 @@ specialize @test fn @identity(i64) -> i64 cfg {{ .replace_operand(branch, 0, result) .unwrap(); - let (inputs, analysis) = DemandAnalysis::>::new(&pipeline) + let analysis = DemandAnalysis::>::new(&pipeline) .analyze(stage, callee, [Demand::Dead]) .unwrap(); + let inputs = analysis.response(analysis.root()).unwrap(); assert_eq!(inputs.as_slice(), &[Demand::Live, Demand::Dead]); assert_eq!(analysis.contexts().count(), 2); assert_eq!( - analysis.fact(analysis.root().unwrap(), result), + analysis.fact(analysis.root(), result.into()), Some(&Demand::Live) ); } diff --git a/crates/kirin-demand/tests/recursive_demand.rs b/crates/kirin-demand/tests/recursive_demand.rs index 56ba4111b..87230cae4 100644 --- a/crates/kirin-demand/tests/recursive_demand.rs +++ b/crates/kirin-demand/tests/recursive_demand.rs @@ -1,5 +1,6 @@ use kirin::prelude::*; use kirin_demand::{Demand, DemandAnalysis}; +use kirin_interpreter::AnalysisResults; use kirin_test_languages::GraphFunctionLanguage; #[test] @@ -31,9 +32,10 @@ specialize @test fn @second(i64, i64, i64) -> i64 cfg {{ ); let mut pipeline: Pipeline> = Pipeline::new(); ParsePipelineText::parse(&mut pipeline, &source).unwrap(); - let (inputs, analysis) = DemandAnalysis::>::new(&pipeline) + let analysis = DemandAnalysis::>::new(&pipeline) .analyze_by_name("test", "second", [Demand::Live]) .unwrap(); + let inputs = analysis.response(analysis.root()).unwrap(); assert_eq!( inputs.as_slice(), &[Demand::Live, Demand::Dead, Demand::Live] diff --git a/crates/kirin-interpreter/src/core/env/store.rs b/crates/kirin-interpreter/src/core/env/store.rs index e42b0561d..3deb91142 100644 --- a/crates/kirin-interpreter/src/core/env/store.rs +++ b/crates/kirin-interpreter/src/core/env/store.rs @@ -197,6 +197,16 @@ where self.context_indices.get(key).copied() } + /// The context key `index` is registered under: the reverse of + /// [`context_env`](Self::context_env). `None` for an unkeyed or dead + /// environment. + pub fn key(&self, index: EnvIndex) -> Option<&K> { + self.environments + .get(index) + .and_then(|slot| (**slot).as_ref()) + .and_then(|environment| environment.key.as_ref()) + } + /// The environment registered under `key`, allocating and registering one on /// first use. /// diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 53fce2cef..1cb94e414 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -50,7 +50,7 @@ use std::collections::HashMap; use std::marker::PhantomData; use kirin_ir::{ - Block, CompileStage, HasArguments, HasBottom, HasResults, Lattice, Pipeline, SSAValue, + Block, CompileStage, HasArguments, HasBottom, HasResults, Lattice, Pipeline, Product, SSAValue, StageMeta, Statement, Symbol, }; @@ -58,10 +58,10 @@ use super::frames::DenseBlockFrame; use crate::Body; use crate::core::query; use crate::{ - AbstractInterpreter, BlockQueries, Callee, ClassicLiveness, DenseBackwardSemantic, EnvIndex, - EnvStore, FactStore, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, - InterpreterError, LinkTarget, Linker, ProgramPoint, SameStageLinker, StageQuery, - StandardFixpointInterpreter, StatementDispatch, TerminatorArgs, WorkAnalysis, + AbstractInterpreter, AnalysisResults, Analyzed, BlockQueries, Callee, ClassicLiveness, + DenseBackwardSemantic, EnvIndex, EnvStore, FixpointProfile, Frame, Interp, InterpDispatch, + InterpLocation, InterpreterError, LinkTarget, Linker, ProgramPoint, SameStageLinker, + StageQuery, StandardFixpointInterpreter, StatementDispatch, TerminatorArgs, WorkAnalysis, }; // =========================================================================== @@ -647,9 +647,9 @@ where /// Dense backward liveness engine (classic per-program-point liveness). /// /// ```ignore -/// let analysis = DenseBackwardInterpreter::::new(&pipeline); -/// let (_target, analysis) = analysis.analyze(stage, callee)?; -/// let live_in = analysis.point_facts(ProgramPoint::BlockEntry(block)); +/// let analyzed = DenseBackwardInterpreter::::new(&pipeline) +/// .analyze(stage, callee)?; +/// let live_in = analyzed.fact(analyzed.root(), ProgramPoint::BlockEntry(block)); /// ``` pub struct DenseBackwardInterpreter< 'ir, @@ -708,24 +708,6 @@ where pub fn pipeline(&self) -> &'ir Pipeline { self.driver.inner().pipeline() } - - /// The converged fact at a program point of the analyzed callable. - pub fn point_facts(&self, point: ProgramPoint) -> Option<&V> { - let transfer = self.driver.inner(); - let (_, env) = transfer.root().ok()?; - transfer.env.environment(env).ok()?.get(point) - } - - /// Snapshot every program-point fact of the analyzed callable. - pub fn facts(&self) -> FactStore { - let transfer = self.driver.inner(); - transfer - .root() - .ok() - .and_then(|(_, env)| transfer.env.environment(env).ok()) - .cloned() - .unwrap_or_default() - } } impl<'ir, S, V, E, F, Lk, Sem> DenseBackwardInterpreter<'ir, S, V, E, F, Lk, Sem> @@ -755,7 +737,7 @@ where self, stage_name: &str, function_name: &str, - ) -> Result<(LinkTarget, Self), E> { + ) -> Result, E> { let stage = self .driver .inner() @@ -776,7 +758,7 @@ where self, stage: CompileStage, symbol: Symbol, - ) -> Result<(LinkTarget, Self), E> { + ) -> Result, E> { self.analyze(stage, symbol.into()) } @@ -784,9 +766,9 @@ where /// and drain the block worklist. Dependencies are discovered from /// terminator edges; unsupported graph roots fail before solving. /// - /// Each call solves from fresh state. It consumes the engine and returns - /// it on success, like the other analysis engines; an error drops it. - pub fn analyze(mut self, stage: CompileStage, callee: Callee) -> Result<(LinkTarget, Self), E> { + /// It consumes the engine and returns the converged [`Analyzed`] state, + /// like the other analysis engines; an error drops the engine. + pub fn analyze(mut self, stage: CompileStage, callee: Callee) -> Result, E> { let pipeline = self.driver.inner().pipeline(); let target = self.linker.resolve(pipeline, stage, &callee)?; let body = target.body(pipeline)?; @@ -800,6 +782,60 @@ where let work = blocks.into_iter().map(|block| (env, Body::Block(block))); self.driver .solve_many(&mut DenseBackwardWorkAnalysis, work)?; - Ok((target, self)) + Ok(Analyzed::new(self, [(target, env)])) + } +} + +/// One callable, one context: the context is the analyzed [`LinkTarget`], +/// and there is no response because calls are not followed. +impl<'ir, S, V, E, F, Lk, Sem> AnalysisResults + for Analyzed> +where + S: StageMeta, + V: Clone + PartialEq + Lattice, + E: From, + Sem: DenseBackwardSemantic, +{ + type Context = LinkTarget; + type Anchor = ProgramPoint; + type Value = V; + + fn root(&self) -> &LinkTarget { + let (target, _) = self + .engine + .driver + .inner() + .root + .as_ref() + .expect("`analyze` sets the root before building `Analyzed`"); + target + } + + fn contexts(&self) -> impl Iterator { + std::iter::once(self.root()) + } + + fn callable_of(&self, context: &LinkTarget) -> Option { + (context == self.root()).then_some(*context) + } + + fn fact(&self, context: &LinkTarget, point: ProgramPoint) -> Option<&V> { + let transfer = self.engine.driver.inner(); + let env = transfer.env.context_env(context)?; + transfer.env.environment(env).ok()?.get(point) + } + + fn facts(&self, context: &LinkTarget) -> impl Iterator { + let transfer = self.engine.driver.inner(); + transfer + .env + .context_env(context) + .and_then(|env| transfer.env.environment(env).ok()) + .into_iter() + .flat_map(|facts| facts.iter()) + } + + fn response(&self, _context: &LinkTarget) -> Option> { + None } } diff --git a/crates/kirin-interpreter/src/engines/mod.rs b/crates/kirin-interpreter/src/engines/mod.rs index c35de8dbd..4e65ca836 100644 --- a/crates/kirin-interpreter/src/engines/mod.rs +++ b/crates/kirin-interpreter/src/engines/mod.rs @@ -7,3 +7,7 @@ pub(crate) mod concrete; pub(crate) mod dense_backward; pub(crate) mod sparse_backward; pub(crate) mod sparse_forward; + +mod results; + +pub use results::{AnalysisResults, Analyzed}; diff --git a/crates/kirin-interpreter/src/engines/results.rs b/crates/kirin-interpreter/src/engines/results.rs new file mode 100644 index 000000000..2e59c3130 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/results.rs @@ -0,0 +1,145 @@ +//! Reading converged analysis facts: [`Analyzed`] is what an abstract +//! engine's `analyze` returns, and [`AnalysisResults`] is the one read-only +//! view all three engines implement on it. + +use std::collections::{HashMap, HashSet}; + +use kirin_ir::{Lattice, Product}; + +use crate::{EnvIndex, FactStore, LatticeAnchor, LinkTarget}; + +/// A converged analysis: the state of engine `I` after one `analyze` call. +/// +/// Only `analyze` builds one, so it always has a root, and it cannot run +/// again: analyzing another root takes another engine. Read it through +/// [`AnalysisResults`]. +pub struct Analyzed { + pub(crate) engine: I, + /// The environments of each reached callable's contexts. Built once: + /// the analysis cannot run again, so it never goes stale. + pub(crate) by_callable: HashMap>, +} + +impl Analyzed { + /// Wrap a converged `engine`, indexing its `contexts` (each reached + /// context's callable and environment) by callable. + pub(crate) fn new( + engine: I, + contexts: impl IntoIterator, + ) -> Self { + let mut by_callable: HashMap> = HashMap::new(); + for (callable, env) in contexts { + by_callable.entry(callable).or_default().push(env); + } + Self { + engine, + by_callable, + } + } + + /// The environments of `callable`'s contexts; empty if it was not reached. + pub(crate) fn envs_of(&self, callable: LinkTarget) -> impl Iterator { + self.by_callable + .get(&callable) + .into_iter() + .flatten() + .copied() + } +} + +/// The read-only view of a converged analysis. +/// +/// A *context* is one analyzed copy of a callable under specific boundary +/// facts. Facts are read at two levels: per context, and per callable (joined +/// over the callable's contexts). The *root* is the context `analyze` was +/// asked for; it is read like any other context. +/// +/// Per-callable answers join the contexts **reached from the root**. They say +/// nothing about callers the analysis never visited. +pub trait AnalysisResults { + /// The identity of one analyzed copy of a callable. + type Context; + /// Where a fact attaches: an SSA value or a program point. + type Anchor: LatticeAnchor; + type Value: Lattice + Clone; + + /// The context `analyze` was asked for. + fn root(&self) -> &Self::Context; + + /// Every context reached from the root, the root included, in no + /// particular order. + fn contexts(&self) -> impl Iterator; + + /// Which callable `context` is a copy of; `None` if the analysis did not + /// reach `context`. + fn callable_of(&self, context: &Self::Context) -> Option; + + /// The fact at `anchor` in `context`. `None` means absent, which is + /// bottom. + fn fact(&self, context: &Self::Context, anchor: Self::Anchor) -> Option<&Self::Value>; + + /// Every stored fact of `context`. + fn facts(&self, context: &Self::Context) -> impl Iterator; + + /// What `context` answers its caller, one fact per slot. `None` means it + /// has no answer, which differs from an empty one. + fn response(&self, context: &Self::Context) -> Option>; + + /// Every callable reached, each once, in no particular order. + fn callables(&self) -> impl Iterator { + let mut seen = HashSet::new(); + self.contexts() + .filter_map(|context| self.callable_of(context)) + .filter(move |callable| seen.insert(*callable)) + } + + /// The contexts that analyze `callable`. + fn contexts_of(&self, callable: LinkTarget) -> impl Iterator { + self.contexts() + .filter(move |context| self.callable_of(context) == Some(callable)) + } + + /// The join of the fact at `anchor` over `callable`'s contexts. `None` + /// means absent in all of them. + fn joined_fact(&self, callable: LinkTarget, anchor: Self::Anchor) -> Option { + self.contexts_of(callable) + .filter_map(|context| self.fact(context, anchor.clone())) + .cloned() + .reduce(|joined, fact| joined.join(&fact)) + } + + /// The join of every stored fact over `callable`'s contexts. + fn joined_facts(&self, callable: LinkTarget) -> FactStore { + let mut joined: FactStore = FactStore::new(); + for context in self.contexts_of(callable) { + for (anchor, fact) in self.facts(context) { + match joined.get_mut(anchor.clone()) { + Some(current) => { + current.join_assign(fact); + } + None => joined.set(anchor, fact.clone()), + } + } + } + joined + } + + /// The slot-wise join of [`response`](Self::response) over `callable`'s + /// contexts that answered. `None` if none answered, or if two answers + /// disagree on arity. + fn joined_response(&self, callable: LinkTarget) -> Option> { + let mut responses = self + .contexts_of(callable) + .filter_map(|context| self.response(context)); + let mut joined = responses.next()?; + for response in responses { + if response.len() != joined.len() { + return None; + } + for (slot, fact) in joined.iter_mut().zip(response.iter()) { + slot.join_assign(fact); + } + } + Some(joined) + } +} diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index f98e2d0a3..d897e5cda 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -63,18 +63,12 @@ use kirin_ir::{ use super::DemandFrame; use crate::core::query; use crate::{ - AbstractInterpreter, Body, CallEffect, Callee, ContextInsensitive, EnvIndex, EnvStore, - FixpointProfile, Interp, InterpDispatch, InterpLocation, InterpreterError, LinkTarget, Linker, - SameStageLinker, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, - StrongDemand, TerminatorArgs, WorkAnalysis, + AbstractInterpreter, AnalysisResults, Analyzed, Body, CallEffect, Callee, ContextInsensitive, + EnvIndex, EnvStore, FixpointProfile, Interp, InterpDispatch, InterpLocation, InterpreterError, + LinkTarget, Linker, SameStageLinker, SparseBackwardSemantic, StageQuery, + StandardFixpointInterpreter, StrongDemand, TerminatorArgs, WorkAnalysis, }; -/// A resolved `(stage, body)` pair: the structure one callable context -/// analyzes. -/// -/// Arena ids are per-stage, so the stage is part of the scope. -pub type BodyScope = (CompileStage, Body); - // =========================================================================== // Pluggable analysis seams (policy `P`) // =========================================================================== @@ -227,11 +221,10 @@ where // Callable contexts, work items, and the driver // =========================================================================== -/// One callable context: its body, declared return operands, and requested -/// output facts. SSA facts live in its environment; parameters come from the IR. +/// One callable context: its callable, declared return operands, and +/// requested output facts. SSA facts live in its environment; parameters come from the IR. pub struct CallableContext { pub(super) target: LinkTarget, - pub(super) body: Body, /// `outputs[i]` holds operand `i` of each declared return site; `None` /// until a return rule declares one. pub(super) outputs: Option>>, @@ -239,10 +232,9 @@ pub struct CallableContext { } impl CallableContext { - pub(super) fn new(target: LinkTarget, body: Body) -> Self { + pub(super) fn new(target: LinkTarget) -> Self { Self { target, - body, outputs: None, requested: None, } @@ -679,7 +671,7 @@ where let (body, _) = self.inner().callable_entry(target)?; self.inner_mut() .contexts - .insert(env, CallableContext::new(target, body)); + .insert(env, CallableContext::new(target)); let stage = target.stage; let mut bodies = VecDeque::from([body]); @@ -829,20 +821,15 @@ where /// context the [`DemandContext`] policy `P` selects (default: one context per /// resolved [`LinkTarget`](crate::LinkTarget)). /// -/// `analyze` consumes the engine and hands it back only on success, so a -/// failed analysis can never be resumed: build a new engine instead. -/// Successful analyses accumulate: discovered contexts and their facts are -/// reused, so shared callees are analyzed once. Facts only rise, so a request -/// whose policy key matches an earlier context returns the join with the -/// earlier requests — never less demanded than a fresh engine, possibly more, -/// and dependent on query order. That happens for every repeated callee under -/// [`ContextInsensitive`], and under a bounded policy once its fallback -/// context is shared. Use a fresh engine for an isolated answer. +/// `analyze` consumes the engine and returns the converged [`Analyzed`] +/// state, read through [`AnalysisResults`]. Each analysis is fresh: analyzing +/// another root takes another engine. /// /// ```ignore -/// let analysis = SparseBackwardInterpreter::::new(&pipeline); -/// let (params, analysis) = analysis.analyze(stage, callee, [Demand::Live])?; -/// let demanded = analysis.is_demanded(analysis.root().unwrap(), value); +/// let analyzed = SparseBackwardInterpreter::::new(&pipeline) +/// .analyze(stage, callee, [Demand::Live])?; +/// let params = analyzed.response(analyzed.root()); +/// let demanded = analyzed.is_demanded(analyzed.root(), value); /// ``` pub struct SparseBackwardInterpreter< 'ir, @@ -917,78 +904,6 @@ where pub fn pipeline(&self) -> &'ir Pipeline { self.driver.inner().pipeline() } - - /// The root of the most recent analysis; detailed facts remain available - /// through context queries. - pub fn root(&self) -> Option<&P::Key> { - self.root.as_ref() - } - - /// The discovered callable contexts, in no particular order. - pub fn contexts(&self) -> impl Iterator { - let transfer = self.driver.inner(); - transfer - .env - .contexts() - .filter(|(_, env)| transfer.contexts.contains_key(env)) - .map(|(key, _)| key) - } - - /// The `(target stage, callable body)` a discovered context analyzes. - pub fn scope(&self, context: &P::Key) -> Option { - let transfer = self.driver.inner(); - let context = transfer.contexts.get(&transfer.discovered(context)?)?; - Some((context.target.stage, context.body)) - } - - /// The converged demand fact for `value` in `context`. - pub fn fact(&self, context: &P::Key, value: impl Into) -> Option<&V> { - let transfer = self.driver.inner(); - let env = transfer.discovered(context)?; - transfer.env.environment(env).ok()?.get(value.into()) - } - - /// The joined demand on operand `index` of every return site of `context`. - pub fn result_value(&self, context: &P::Key, index: usize) -> Option { - let transfer = self.driver.inner(); - let env = transfer.discovered(context)?; - let operands = transfer.contexts.get(&env)?.outputs.as_ref()?.get(index)?; - operands - .iter() - .filter_map(|operand| self.fact(context, *operand)) - .cloned() - .reduce(|joined, fact| joined.join(&fact)) - } - - /// All converged `(value, fact)` pairs in `context`, reading only that - /// context's environment. - pub fn facts<'a>(&'a self, context: &'a P::Key) -> impl Iterator { - let transfer = self.driver.inner(); - transfer - .discovered(context) - .and_then(|env| transfer.env.environment(env).ok()) - .into_iter() - .flat_map(|facts| facts.iter()) - } - - /// The converged value facts in `context` as a [`SparseStore`] (the - /// sparse per-SSA-value fact view; absent = bottom). - pub fn fact_store(&self, context: &P::Key) -> SparseStore { - let mut store = SparseStore::new(); - for (value, fact) in self.facts(context) { - store.set(value, fact.clone()); - } - store - } - - /// `true` iff `value` carries a non-bottom demand fact in `context`. - pub fn is_demanded(&self, context: &P::Key, value: impl Into) -> bool - where - V: HasBottom, - { - self.fact(context, value) - .is_some_and(|fact| *fact != V::bottom()) - } } impl<'ir, S, V, E, Lk, P, Sem> SparseBackwardInterpreter<'ir, S, V, E, Lk, P, Sem> @@ -1007,7 +922,7 @@ where stage_name: &str, function_name: &str, result_values: impl IntoIterator, - ) -> Result<(Product, Self), E> { + ) -> Result, E> { let stage = self .pipeline() .stage_by_name(stage_name) @@ -1026,28 +941,26 @@ where stage: CompileStage, symbol: Symbol, result_values: impl IntoIterator, - ) -> Result<(Product, Self), E> { + ) -> Result, E> { self.analyze(stage, symbol.into(), result_values) } - /// Infer one fact per entry parameter, in parameter order, from output - /// lattice values. Supply exactly one lattice value per result. + /// Infer one fact per entry parameter from output lattice values. Supply + /// exactly one lattice value per result. /// /// Enqueue each new context's statements, retain the supplied output facts /// for its return rules, then drain the shared fixpoint worklist. Bottom /// output facts still allow intrinsic roots to contribute parameter facts. - /// Facts join with prior requests if the context policy reuses a key. - /// Returns the engine on success, so detailed context facts remain - /// accessible through [`Self::root`], [`Self::contexts`], and - /// [`Self::fact`]; an error drops it. + /// The parameter facts are the root's + /// [`response`](AnalysisResults::response); an error drops the engine. pub fn analyze( mut self, stage: CompileStage, callee: Callee, result_values: impl IntoIterator, - ) -> Result<(Product, Self), E> { + ) -> Result, E> { let outputs: Product = result_values.into_iter().collect(); - let (target, root, env) = self + let (_, root, _) = self .driver .inner_mut() .submit_call(stage, &callee, outputs)?; @@ -1055,9 +968,101 @@ where self.driver.wake_changed()?; self.driver .drain_worklist(&mut SparseBackwardWorkAnalysis)?; - let params = self.driver.inner().entry_params(target)?; - let facts = self.driver.inner().param_facts(env, ¶ms)?; + let contexts: Vec<_> = self + .driver + .inner() + .contexts + .iter() + .map(|(env, context)| (context.target, *env)) + .collect(); self.root = Some(root); - Ok((facts, self)) + Ok(Analyzed::new(self, contexts)) + } +} + +impl<'ir, S, V, E, Lk, P, Sem> AnalysisResults + for Analyzed> +where + S: StageMeta + StageQuery + InterpDispatch>, + V: Clone + PartialEq + Lattice + HasBottom, + E: From, + Lk: Linker, + P: DemandContext, + Sem: SparseBackwardSemantic, +{ + type Context = P::Key; + type Anchor = SSAValue; + type Value = V; + + fn root(&self) -> &P::Key { + self.engine + .root + .as_ref() + .expect("`analyze` sets the root before building `Analyzed`") + } + + /// The discovered callable contexts. + fn contexts(&self) -> impl Iterator { + let transfer = self.engine.driver.inner(); + transfer + .env + .contexts() + .filter(|(_, env)| transfer.contexts.contains_key(env)) + .map(|(key, _)| key) + } + + fn callable_of(&self, context: &P::Key) -> Option { + let transfer = self.engine.driver.inner(); + let env = transfer.discovered(context)?; + Some(transfer.contexts.get(&env)?.target) + } + + fn contexts_of(&self, callable: LinkTarget) -> impl Iterator { + let transfer = self.engine.driver.inner(); + self.envs_of(callable) + .filter_map(|env| transfer.env.key(env)) + } + + fn fact(&self, context: &P::Key, value: SSAValue) -> Option<&V> { + let transfer = self.engine.driver.inner(); + let env = transfer.discovered(context)?; + transfer.env.environment(env).ok()?.get(value) + } + + fn facts(&self, context: &P::Key) -> impl Iterator { + let transfer = self.engine.driver.inner(); + transfer + .discovered(context) + .and_then(|env| transfer.env.environment(env).ok()) + .into_iter() + .flat_map(|facts| facts.iter()) + } + + /// The demand on each entry parameter, in parameter order. + fn response(&self, context: &P::Key) -> Option> { + let transfer = self.engine.driver.inner(); + let env = transfer.discovered(context)?; + let target = transfer.contexts.get(&env)?.target; + let params = transfer.entry_params(target).ok()?; + transfer.param_facts(env, ¶ms).ok() + } +} + +impl<'ir, S, V, E, Lk, P, Sem> Analyzed> +where + S: StageMeta, + V: Clone + PartialEq + Lattice + HasBottom, + E: From, + P: DemandContext, + Sem: SparseBackwardSemantic, +{ + /// `true` iff `value` carries a non-bottom demand fact in `context`. + pub fn is_demanded(&self, context: &P::Key, value: impl Into) -> bool { + let transfer = self.engine.driver.inner(); + transfer + .discovered(context) + .and_then(|env| transfer.env.environment(env).ok()) + .and_then(|facts| facts.get(value.into())) + .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 4801b830c..c9e784569 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs @@ -7,6 +7,6 @@ pub(crate) mod interp; pub use frames::DemandFrame; pub use interp::{ - BodyScope, DemandContext, DemandInterp, SparseBackwardDriver, SparseBackwardEffect, - SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, + DemandContext, DemandInterp, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, + SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 9fcea3492..53c3aade1 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -36,18 +36,18 @@ use std::hash::Hash; use std::marker::PhantomData; use kirin_ir::{ - Block, CFG, CompileStage, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, - StageMeta, Statement, Symbol, Widen, + Block, CFG, CompileStage, HasBottom, Pipeline, Product, SSAValue, StageMeta, Statement, Symbol, + Widen, }; use crate::core::query; use crate::{ AbstractBlockFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractInterpreter, - BlockQueries, Body, CFGQueries, CallEffect, CallServices, Callee, DiGraphQueries, Env, - EnvIndex, EnvStore, FixpointProfile, ForwardDataflowFrameEngine, ForwardEval, Frame, Interp, - InterpDispatch, InterpLocation, InterpreterError, LinkTarget, Linker, SSABinding, - SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, StandardAbstractFrame, - StandardFixpointInterpreter, StatementDispatch, WorkAnalysis, + AnalysisResults, Analyzed, BlockQueries, Body, CFGQueries, CallEffect, CallServices, Callee, + DiGraphQueries, Env, EnvIndex, EnvStore, FixpointProfile, ForwardDataflowFrameEngine, + ForwardEval, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, LinkTarget, + Linker, SSABinding, SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, + StandardAbstractFrame, StandardFixpointInterpreter, StatementDispatch, WorkAnalysis, }; // =========================================================================== @@ -122,7 +122,8 @@ where /// One selected function context: what it analyzes, what it returns, and who /// reads the return. struct ForwardContext { - stage: CompileStage, + /// The callable this context analyzes. + target: LinkTarget, /// The unit a call enters: the CFG's entry block, a block body, or a graph. entry: Body, /// The join of every reached return; `None` means no return reached yet, @@ -759,7 +760,7 @@ where self.inner_mut().contexts.insert( env, ForwardContext { - stage: target.stage, + target, entry, ret: None, callers: Vec::new(), @@ -777,7 +778,7 @@ where /// changed. fn merge_entry(&mut self, work: ForwardWork, args: Product) -> Result<(), E> { let (env, body) = work; - let stage = self.inner().context(env)?.stage; + let stage = self.inner().context(env)?.target.stage; let params = self.inner().entry_params(stage, body)?; if params.len() != args.len() { return Err(E::from(match body { @@ -858,7 +859,7 @@ where fn wake_changed(&mut self) -> Result<(), E> { let mut woken = HashSet::new(); for (env, value) in std::mem::take(&mut self.inner_mut().changed) { - let stage = self.inner().context(env)?.stage; + let stage = self.inner().context(env)?.target.stage; let (def, uses) = query::value_units(self.inner().pipeline(), stage, value)?; let def = self.inner().reached(env, &def); for units in uses { @@ -922,7 +923,7 @@ where interp: &mut ForwardDriver<'ir, S, V, E, Lk, P, F, Sem>, &(env, body): &ForwardWork, ) -> Result { - let stage = interp.inner().context(env)?.stage; + let stage = interp.inner().context(env)?.target.stage; let args: Product = interp .inner() .entry_params(stage, body)? @@ -985,17 +986,15 @@ where /// Traversal is owned by the total frame type `F`; context keying and merge/widen /// behavior are owned by the policy `P`. /// -/// `analyze` consumes the engine and hands it back only on success, so a -/// failed analysis can never be resumed: build a new engine instead. -/// Successful analyses accumulate facts, so a request whose policy key -/// matches an earlier context returns the join with the earlier requests -/// (for every repeated callee under [`ContextInsensitive`]) and can depend on -/// query order. Use a fresh engine for an isolated answer. +/// `analyze` consumes the engine and returns the converged [`Analyzed`] +/// state, read through [`AnalysisResults`]. Each analysis is fresh: analyzing +/// another root takes another engine. /// /// ```ignore -/// let analysis = SparseForwardInterpreter::, MyError>::new(&pipeline) -/// .with_linker(CrossStageLinker); -/// let (result, analysis) = analysis.analyze_by_name("source", "abs", [ConstPropValue::Const(value)])?; +/// let analyzed = SparseForwardInterpreter::, MyError>::new(&pipeline) +/// .with_linker(CrossStageLinker) +/// .analyze_by_name("source", "abs", [ConstPropValue::Const(value)])?; +/// let result = analyzed.response(analyzed.root()); /// ``` pub struct SparseForwardInterpreter< 'ir, @@ -1013,6 +1012,8 @@ pub struct SparseForwardInterpreter< Sem: SparseForwardSemantic, { driver: ForwardDriver<'ir, S, V, E, Lk, P, F, Sem>, + /// The analyzed root's context key, set by `analyze`. + root: Option, } impl<'ir, S: StageMeta, V, E, P, F, Sem> @@ -1026,6 +1027,7 @@ where pub fn new(pipeline: &'ir Pipeline) -> Self { Self { driver: StandardFixpointInterpreter::new(SparseForwardTransfer::new(pipeline), ()), + root: None, } } } @@ -1046,6 +1048,7 @@ where let transfer = self.driver.into_inner().with_linker(linker); SparseForwardInterpreter { driver: StandardFixpointInterpreter::new(transfer, ()), + root: None, } } @@ -1063,6 +1066,7 @@ where let transfer = self.driver.into_inner().with_analysis(analysis); SparseForwardInterpreter { driver: StandardFixpointInterpreter::new(transfer, ()), + root: None, } } @@ -1072,12 +1076,20 @@ where let transfer = self.driver.into_inner().with_policy(analysis); Self { driver: StandardFixpointInterpreter::new(transfer, ()), + root: None, } } pub fn pipeline(&self) -> &'ir Pipeline { self.driver.inner().pipeline() } + + /// A selected context's environment and metadata. + fn selected(&self, key: &P::Key) -> Option<(EnvIndex, &ForwardContext)> { + let transfer = self.driver.inner(); + let env = transfer.env.context_env(key)?; + Some((env, transfer.contexts.get(&env)?)) + } } impl<'ir, S: StageMeta, V, E, Lk, F, Sem> @@ -1093,22 +1105,6 @@ where self.driver.inner_mut().analysis.widen_after = joins; self } - - /// The joined return of an analyzed function (context-insensitive keying - /// only); `None` if it was never analyzed or never returned. - pub fn return_summary( - &self, - stage: CompileStage, - function: SpecializedFunction, - ) -> Option> { - let target = LinkTarget { - stage, - specialization: function, - }; - let transfer = self.driver.inner(); - let env = transfer.env.context_env(&target)?; - transfer.context(env).ok()?.ret.clone() - } } impl<'ir, S, V, E, Lk, P, F, Sem> SparseForwardInterpreter<'ir, S, V, E, Lk, P, F, Sem> @@ -1123,14 +1119,14 @@ where + From> + From>, { - /// Resolve `stage`/`function` by name and analyze. Returns the function's - /// inferred return product at the fixpoint (empty if it never returns). + /// Resolve `stage`/`function` by name and analyze. See + /// [`analyze`](Self::analyze). pub fn analyze_by_name( self, stage_name: &str, function_name: &str, args: impl IntoIterator, - ) -> Result<(Product, Self), E> { + ) -> Result, E> { let stage = self .driver .inner() @@ -1148,40 +1144,106 @@ where /// Resolve a stage-local `symbol` through the linker and analyze the /// selected callable. This is the convenience form of - /// [`analyze`](Self::analyze) with [`Callee::Named`], and returns its - /// inferred return product at the fixpoint. + /// [`analyze`](Self::analyze) with [`Callee::Named`]. pub fn analyze_by_symbol( self, stage: CompileStage, symbol: Symbol, args: impl IntoIterator, - ) -> Result<(Product, Self), E> { + ) -> Result, E> { self.analyze(stage, symbol.into(), args) } /// Run the fixpoint from a single entry. Seeds the entry function's entry unit - /// and drains the worklist. Returns the engine on success; an error drops - /// it. + /// and drains the worklist. The inferred return product is the root's + /// [`response`](AnalysisResults::response) (`None` if it never returns); + /// an error drops the engine. pub fn analyze( mut self, stage: CompileStage, callee: Callee, args: impl IntoIterator, - ) -> Result<(Product, Self), E> { + ) -> Result, E> { let args: Product = args.into_iter().collect(); let env = self.driver.contribute_call_entry(stage, &callee, args)?; self.driver.wake_changed()?; let mut analysis = SparseForwardWorkAnalysis::new(); self.driver.drain_worklist(&mut analysis)?; - let ret = self - .driver - .inner() - .context(env)? - .ret - .clone() - .unwrap_or_default(); - Ok((ret, self)) + let transfer = self.driver.inner(); + let root = transfer + .env + .key(env) + .cloned() + .ok_or_else(|| E::from(UNSELECTED))?; + let contexts: Vec<_> = transfer + .contexts + .iter() + .map(|(env, context)| (context.target, *env)) + .collect(); + self.root = Some(root); + Ok(Analyzed::new(self, contexts)) + } +} + +impl<'ir, S, V, E, Lk, P, F, Sem> AnalysisResults + for Analyzed> +where + S: StageMeta, + V: Clone + Widen, + E: From, + P: CallContext, + Sem: SparseForwardSemantic, +{ + type Context = P::Key; + type Anchor = SSAValue; + type Value = V; + + fn root(&self) -> &P::Key { + self.engine + .root + .as_ref() + .expect("`analyze` sets the root before building `Analyzed`") + } + + /// The selected function contexts. + fn contexts(&self) -> impl Iterator { + let transfer = self.engine.driver.inner(); + transfer + .env + .contexts() + .filter(|(_, env)| transfer.contexts.contains_key(env)) + .map(|(key, _)| key) + } + + fn callable_of(&self, context: &P::Key) -> Option { + Some(self.engine.selected(context)?.1.target) + } + + fn contexts_of(&self, callable: LinkTarget) -> impl Iterator { + let transfer = self.engine.driver.inner(); + self.envs_of(callable) + .filter_map(|env| transfer.env.key(env)) + } + + fn fact(&self, context: &P::Key, value: SSAValue) -> Option<&V> { + let (env, _) = self.engine.selected(context)?; + let transfer = self.engine.driver.inner(); + transfer.env.environment(env).ok()?.get(value) + } + + fn facts(&self, context: &P::Key) -> impl Iterator { + let transfer = self.engine.driver.inner(); + self.engine + .selected(context) + .and_then(|(env, _)| transfer.env.environment(env).ok()) + .into_iter() + .flat_map(|facts| facts.iter()) + } + + /// The join of every reached return; `None` if no return was reached. + fn response(&self, context: &P::Key) -> Option> { + self.engine.selected(context)?.1.ret.clone() } } diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index f29e2d486..64f2a5c0d 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -6,4 +6,4 @@ pub(crate) mod anchor; pub(crate) mod store; pub use anchor::{LatticeAnchor, ProgramPoint}; -pub use store::{FactStore, SparseStore}; +pub use store::FactStore; diff --git a/crates/kirin-interpreter/src/facts/store.rs b/crates/kirin-interpreter/src/facts/store.rs index 398f6d00a..12f3682f0 100644 --- a/crates/kirin-interpreter/src/facts/store.rs +++ b/crates/kirin-interpreter/src/facts/store.rs @@ -7,7 +7,6 @@ //! scope where needed. Engines decide what absence means and when facts must //! join. -use kirin_ir::SSAValue; use rustc_hash::FxHashMap; use super::anchor::LatticeAnchor; @@ -81,15 +80,10 @@ impl FactStore { } } -/// A sparse store keyed by SSA values, for sparse-shaped analyses -/// ([`SparseForwardShape`](crate::SparseForwardShape) / -/// [`SparseBackwardShape`](crate::SparseBackwardShape)). -pub type SparseStore = FactStore; - #[cfg(test)] mod tests { use crate::ProgramPoint; - use kirin_ir::{Block, Id, Statement, TestSSAValue}; + use kirin_ir::{Block, Id, SSAValue, Statement, TestSSAValue}; use super::*; @@ -99,7 +93,7 @@ mod tests { #[test] fn fact_store_get_set() { - let mut store: SparseStore = FactStore::new(); + let mut store: FactStore = FactStore::new(); assert!(store.is_empty()); assert_eq!(store.get(ssa(1)), None); diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index fb216b8d4..db8d76d7a 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -90,10 +90,11 @@ pub use engines::sparse_forward::{ }; // Sparse backward engine (`Sem = StrongDemand`). pub use engines::sparse_backward::{ - BodyScope, DemandContext, DemandFrame, DemandInterp, SparseBackwardDriver, - SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, - SparseBackwardTransfer, + DemandContext, DemandFrame, DemandInterp, SparseBackwardDriver, SparseBackwardEffect, + SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; +// The read-only view of a converged analysis, shared by the abstract engines. +pub use engines::{AnalysisResults, Analyzed}; // Dense backward engine (`Sem = ClassicLiveness`) + the dense standard frames. pub use engines::dense_backward::{ ClassicLivenessInterp, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardEffect, @@ -104,7 +105,7 @@ pub use engines::dense_backward::{ // 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::{FactStore, LatticeAnchor, ProgramPoint, SparseStore}; +pub use facts::{FactStore, LatticeAnchor, ProgramPoint}; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` // dispatch tags) and the solver shapes each key runs on. @@ -144,15 +145,15 @@ pub mod dialect { pub mod engine { pub use crate::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, - AbstractInterpreter, BlockFrame, BlockQueries, BodyFrameEntry, CFGFrame, CFGQueries, - CallBodyTraversal, CallContext, CallFrame, CallRequest, CallServices, Callee, Completion, - ConcreteInterpreter, ConcreteInterpreterCore, ContextInsensitive, CrossStageLinker, - DefaultCallBodyTraversal, DemandContext, DenseBackwardCompletion, DenseBackwardFrameEngine, - DenseBackwardInterp, DenseBackwardInterpreter, DenseBlockFrame, DiGraphFrame, - DiGraphQueries, Env, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, FrameEffect, - FrameEngine, Interp, InterpDispatch, InterpreterError, LinkTarget, Linker, SSABinding, - SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, SparseForwardInterp, - SparseForwardInterpreter, StandardAbstractFrame, StatementDispatch, WideningStrategy, - drive_frames, expect_single, + AbstractInterpreter, AnalysisResults, Analyzed, BlockFrame, BlockQueries, BodyFrameEntry, + CFGFrame, CFGQueries, CallBodyTraversal, CallContext, CallFrame, CallRequest, CallServices, + Callee, Completion, ConcreteInterpreter, ConcreteInterpreterCore, ContextInsensitive, + CrossStageLinker, DefaultCallBodyTraversal, DemandContext, DenseBackwardCompletion, + DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, DenseBlockFrame, + DiGraphFrame, DiGraphQueries, Env, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, + FrameEffect, FrameEngine, Interp, InterpDispatch, InterpreterError, LinkTarget, Linker, + SSABinding, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, + SparseForwardInterp, SparseForwardInterpreter, StandardAbstractFrame, StatementDispatch, + WideningStrategy, drive_frames, expect_single, }; } diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 93d2d2bac..cf1941122 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -1,7 +1,7 @@ //! Classic SSA liveness for Kirin: a [`LiveSet`] at every program point (the -//! regalloc-grade fact), computed by the dense backward engine -//! ([`kirin_interpreter::DenseBackwardInterpreter`]) through -//! [`analyze_dense`]. Every use gens and every definition kills; purity is +//! regalloc-grade fact), computed by [`DenseLiveness`], the dense backward +//! engine ([`kirin_interpreter::DenseBackwardInterpreter`]) at [`LiveSet`] +//! point states. Every use gens and every definition kills; purity is //! irrelevant, so dead code's operands appear in these sets. Transfer comes //! from each dialect's `Interpretable` rule. //! @@ -9,22 +9,19 @@ //! separate sparse backward analysis in `kirin-demand`. //! //! ```ignore -//! let result = kirin_liveness::analyze_dense(&pipeline, stage, callee)?; -//! let live = result.point_facts(point); +//! use kirin_interpreter::AnalysisResults; +//! +//! let analyzed = DenseLiveness::::new(&pipeline).analyze(stage, callee)?; +//! let live = analyzed.fact(analyzed.root(), point); //! ``` mod live; -mod result; pub use live::LiveSet; -pub use result::DenseLivenessResult; use kirin_interpreter::{ - Callee, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, - DenseBackwardTransfer, DenseBlockFrame, Frame, InterpDispatch, InterpreterError, - SameStageLinker, StageQuery, + DenseBackwardInterpreter, DenseBlockFrame, InterpreterError, SameStageLinker, }; -use kirin_ir::{CompileStage, Pipeline, StageMeta}; /// The dense backward engine instantiated at [`LiveSet`] point states: /// classic per-program-point liveness. A language with structured dialects @@ -36,55 +33,3 @@ pub type DenseLiveness< F = DenseBlockFrame, Lk = SameStageLinker, > = DenseBackwardInterpreter<'ir, S, LiveSet, E, F, Lk>; - -/// Resolve `callee` and run classic per-point liveness over its body, -/// with the standard reverse block walker. Languages with structured dialects -/// select their stack-item type through -/// [`analyze_dense_with_frame`] instead. -pub fn analyze_dense<'ir, S>( - pipeline: &'ir Pipeline, - stage: CompileStage, - callee: Callee, -) -> Result -where - S: StageMeta - + StageQuery - + InterpDispatch< - DenseBackwardTransfer< - 'ir, - S, - LiveSet, - InterpreterError, - DenseBlockFrame, - >, - >, -{ - analyze_dense_with_frame::>( - pipeline, stage, callee, - ) -} - -/// Resolve `callee` and run classic per-point liveness over its body with a -/// caller-selected stack-item type `F` — the entry point for -/// languages whose structured dialects require a language-specific private -/// composition. The analysis consumes the finalized IR directly; it neither -/// requires nor computes a strong-liveness pre-pass. -pub fn analyze_dense_with_frame<'ir, S, F>( - pipeline: &'ir Pipeline, - stage: CompileStage, - callee: Callee, -) -> Result -where - S: StageMeta - + StageQuery - + InterpDispatch>, - F: Frame< - DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, - F, - Completion = DenseBackwardCompletion, - > + From>, -{ - let (root, engine) = - DenseLiveness::::new(pipeline).analyze(stage, callee)?; - Ok(DenseLivenessResult::from_engine(&engine, root)) -} diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs deleted file mode 100644 index 1c66e752a..000000000 --- a/crates/kirin-liveness/src/result.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! The classic liveness result: dense per-point live sets. - -use kirin_interpreter::{ - DenseBackwardInterpreter, FactStore, InterpreterError, LinkTarget, ProgramPoint, -}; -use kirin_ir::StageMeta; - -use crate::live::LiveSet; - -/// 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. -#[derive(Clone, Debug)] -pub struct DenseLivenessResult { - root: LinkTarget, - facts: FactStore, -} - -impl DenseLivenessResult { - /// Copy the facts recorded by a converged dense engine. - pub fn from_engine<'ir, S, F, Lk>( - engine: &DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F, Lk>, - root: LinkTarget, - ) -> Self - where - S: StageMeta, - { - Self { - root, - facts: engine.facts(), - } - } - - /// The resolved callable analyzed by this result. - pub fn root(&self) -> LinkTarget { - self.root - } - - /// The liveness fact recorded at `point` of the analyzed callable. - pub fn point_facts(&self, point: ProgramPoint) -> Option<&LiveSet> { - self.facts.get(point) - } -} diff --git a/crates/kirin-liveness/tests/callable_root.rs b/crates/kirin-liveness/tests/callable_root.rs index 2e02caabe..1c74598fd 100644 --- a/crates/kirin-liveness/tests/callable_root.rs +++ b/crates/kirin-liveness/tests/callable_root.rs @@ -4,7 +4,8 @@ use kirin::prelude::*; use kirin_demand::{Demand, DemandAnalysis}; use kirin_interpreter::{ - Body, Callee, CrossStageLinker, InterpreterError, LinkTarget, Linker, SameStageLinker, + AnalysisResults, Body, Callee, CrossStageLinker, InterpreterError, LinkTarget, Linker, + SameStageLinker, }; use kirin_liveness::DenseLiveness; use kirin_test_languages::GraphFunctionLanguage; @@ -131,17 +132,17 @@ fn every_callee_variant_uses_the_same_backward_root_protocol() { let (stage, callees) = all_callee_variants(&pipeline); for callee in callees { - let (_, demand) = DemandAnalysis::::new(&pipeline) + let demand = DemandAnalysis::::new(&pipeline) .analyze(stage, callee, [Demand::Live]) .expect("demand succeeds"); - let root = demand.root().unwrap(); - let demand_scope = demand.scope(root).expect("root context is selected"); - assert!(matches!(demand_scope, (_, Body::Block(_)))); + let root = demand.root(); + let demand_target = demand.callable_of(root).expect("root context is selected"); + assert!(matches!(demand_target.body(&pipeline), Ok(Body::Block(_)))); - let (dense_root, _) = DenseLiveness::::new(&pipeline) + let dense = DenseLiveness::::new(&pipeline) .analyze(stage, callee) .expect("dense succeeds"); - assert_eq!(dense_root, root.0); + assert_eq!(*dense.root(), root.0); } } @@ -206,30 +207,23 @@ fn cross_stage_linking_discovers_the_body_at_the_target_stage() { Err(InterpreterError::MissingSpecialization(_)) )); - let (_, demand) = DemandAnalysis::::new(&pipeline) + let demand = DemandAnalysis::::new(&pipeline) .with_linker(CrossStageLinker) .analyze(source, callee, [Demand::Live]) .expect("cross-stage demand succeeds"); - let root = demand.root().unwrap(); + let root = demand.root(); assert_eq!( root.0.stage, lowered, "the root context is the resolved target" ); - let demand_scope = demand.scope(root).expect("root context is selected"); - let (dense_root, dense) = DenseLiveness::::new(&pipeline) + let demand_target = demand.callable_of(root).expect("root context is selected"); + let dense = DenseLiveness::::new(&pipeline) .with_linker(CrossStageLinker) .analyze(source, callee) .expect("cross-stage dense succeeds"); - assert_eq!(demand_scope.0, lowered); - assert!(matches!(demand_scope.1, Body::Block(_))); - assert_eq!(dense_root, root.0); - - // Dense analysis rebuilds solver state between roots; configuration state - // must survive that reset. - let (again, _) = dense - .analyze(source, callee) - .expect("custom linker survives dense reset"); - assert_eq!(again, dense_root); + assert_eq!(demand_target.stage, lowered); + assert!(matches!(demand_target.body(&pipeline), Ok(Body::Block(_)))); + assert_eq!(*dense.root(), root.0); } #[derive(Clone, Copy)] @@ -297,17 +291,17 @@ fn specialized_handles_are_validated_before_accepting_a_candidate_stage() { .unwrap(), target ); - let (_, demand) = DemandAnalysis::::new(&pipeline) + let demand = DemandAnalysis::::new(&pipeline) .with_linker(CrossStageLinker) .analyze(source, callee, [Demand::Live]) .unwrap(); - let (dense, _) = DenseLiveness::::new(&pipeline) + let dense = DenseLiveness::::new(&pipeline) .with_linker(CrossStageLinker) .analyze(source, callee) .unwrap(); - let root = demand.root().unwrap(); + let root = demand.root(); assert_eq!(root.0, target); - assert_eq!(dense, root.0); + assert_eq!(*dense.root(), root.0); } #[test] diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 0be8bb515..cd97d770d 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -8,9 +8,9 @@ use kirin::prelude::{GetInfo, ParsePipelineText, Pipeline, SSAValue, StageInfo}; use kirin_arith::Arith; -use kirin_demand::{Demand, analyze_demand}; -use kirin_interpreter::{Body, Callee, InterpreterError, ProgramPoint}; -use kirin_liveness::{DenseLiveness, LiveSet, analyze_dense}; +use kirin_demand::{Demand, DemandAnalysis}; +use kirin_interpreter::{AnalysisResults, Body, Callee, ProgramPoint}; +use kirin_liveness::{DenseLiveness, LiveSet}; use kirin_test_languages::ArithFunctionLanguage; const PROGRAM: &str = r#" @@ -31,24 +31,6 @@ specialize @test fn @main(i64, i64) -> i64 cfg { } "#; -const ENGINE_REUSE_PROGRAM: &str = r#" -stage @test fn @first(i64) -> i64; -stage @test fn @second(i64) -> i64; - -specialize @test fn @first(i64) -> i64 cfg { - ^entry(%x: i64) { - ret %x; - } -} - -specialize @test fn @second(i64) -> i64 cfg { - ^entry(%y: i64) { - %neg = neg %y -> i64; - ret %neg; - } -} -"#; - const DEAD_RESULT_PROGRAM: &str = r#" stage @test fn @main(i64, i64) -> i64; @@ -138,7 +120,9 @@ fn live_set(values: &[SSAValue]) -> LiveSet { fn classic_liveness_boundary_sets() { let pipeline = parse(PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = analyze_dense(&pipeline, caller_stage, callee).expect("analysis succeeds"); + let result = DenseLiveness::<_>::new(&pipeline) + .analyze(caller_stage, callee) + .expect("analysis succeeds"); let root = result.root(); let Body::CFG(cfg) = root.body(&pipeline).expect("@main has a body") else { panic!("@main resolves to a CFG body") @@ -154,78 +138,41 @@ fn classic_liveness_boundary_sets() { // live_in(entry): %x (used by add and both edges) and %cond (branch use). assert_eq!( - result.point_facts(ProgramPoint::BlockEntry(entry)), + result.fact(root, ProgramPoint::BlockEntry(entry)), Some(&live_set(&[x, cond])) ); // 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.point_facts(ProgramPoint::BlockExit(entry)), + result.fact(root, ProgramPoint::BlockExit(entry)), Some(&live_set(&[x])) ); assert_eq!( - result.point_facts(ProgramPoint::BlockEntry(then_block)), + result.fact(root, ProgramPoint::BlockEntry(then_block)), Some(&live_set(&[then_param])) ); assert_eq!( - result.point_facts(ProgramPoint::BlockExit(then_block)), + result.fact(root, ProgramPoint::BlockExit(then_block)), Some(&live_set(&[])) ); assert_eq!( - result.point_facts(ProgramPoint::BlockEntry(else_block)), + result.fact(root, ProgramPoint::BlockEntry(else_block)), Some(&live_set(&[else_param])) ); assert_eq!( - result.point_facts(ProgramPoint::BlockExit(else_block)), + result.fact(root, ProgramPoint::BlockExit(else_block)), Some(&live_set(&[])) ); } -#[test] -fn reusing_dense_engine_replaces_the_previous_scoped_result() { - let pipeline = parse(ENGINE_REUSE_PROGRAM); - let (stage, first) = function_root(&pipeline, "first"); - let (_, second) = function_root(&pipeline, "second"); - let engine = DenseLiveness::<_, InterpreterError>::new(&pipeline); - - let (first_root, engine) = engine - .analyze(stage, first) - .expect("first callable analysis succeeds"); - let Body::CFG(first_cfg) = first_root.body(&pipeline).expect("@first has a body") else { - panic!("@first resolves to a CFG body") - }; - let first_entry = nth_block(&pipeline, first_cfg, 0); - assert!( - engine - .point_facts(ProgramPoint::BlockEntry(first_entry)) - .is_some() - ); - - let (second_root, engine) = engine - .analyze(stage, second) - .expect("second callable analysis succeeds"); - let Body::CFG(second_cfg) = second_root.body(&pipeline).expect("@second has a body") else { - panic!("@second resolves to a CFG body") - }; - let second_entry = nth_block(&pipeline, second_cfg, 0); - assert_eq!( - engine.point_facts(ProgramPoint::BlockEntry(first_entry)), - None, - "facts from the previous analysis are not retained" - ); - assert!( - engine - .point_facts(ProgramPoint::BlockEntry(second_entry)) - .is_some() - ); -} - #[test] fn classic_per_point_sets_gen_dead_uses() { let pipeline = parse(DEAD_RESULT_PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let result = analyze_dense(&pipeline, caller_stage, callee).expect("analysis succeeds"); + let result = DenseLiveness::<_>::new(&pipeline) + .analyze(caller_stage, callee) + .expect("analysis succeeds"); let root = result.root(); let Body::CFG(cfg) = root.body(&pipeline).expect("@main has a body") else { panic!("@main resolves to a CFG body") @@ -240,11 +187,11 @@ fn classic_per_point_sets_gen_dead_uses() { // before it — this is the conventional per-point meaning (the old strong // expectations were demand projections, not dense liveness). assert_eq!( - result.point_facts(ProgramPoint::Before(add)), + result.fact(root, ProgramPoint::Before(add)), Some(&live_set(&[a, b])) ); assert_eq!( - result.point_facts(ProgramPoint::After(add)), + result.fact(root, ProgramPoint::After(add)), Some(&live_set(&[a])) ); } @@ -253,11 +200,14 @@ fn classic_per_point_sets_gen_dead_uses() { fn classic_sets_filtered_by_demand_drop_unneeded_values() { let pipeline = parse(DEAD_RESULT_PROGRAM); let (caller_stage, callee) = function_root(&pipeline, "main"); - let dense = analyze_dense(&pipeline, caller_stage, callee).expect("dense analysis succeeds"); - let demand = analyze_demand(&pipeline, caller_stage, callee, [Demand::Live]) + let dense = DenseLiveness::<_>::new(&pipeline) + .analyze(caller_stage, callee) + .expect("dense analysis succeeds"); + let demand = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [Demand::Live]) .expect("demand analysis succeeds"); let root = dense.root(); - assert_eq!(root, demand.root().0); + assert_eq!(*root, demand.root().0); let Body::CFG(cfg) = root.body(&pipeline).expect("@main has a body") else { panic!("@main resolves to a CFG body") }; @@ -271,11 +221,11 @@ fn classic_sets_filtered_by_demand_drop_unneeded_values() { // Filtering the classic set by demand drops %b: it is classically live // before the dead add but not demanded. let classic = dense - .point_facts(ProgramPoint::Before(add)) + .fact(root, ProgramPoint::Before(add)) .expect("point reconstructed"); let strong: LiveSet = classic .iter() - .filter(|value| demand.is_demanded(*value)) + .filter(|value| demand.is_demanded(demand.root(), *value)) .collect(); assert_eq!(strong, live_set(&[a])); assert!(!strong.contains(b)); diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index eb2d402ab..db618220d 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -564,7 +564,7 @@ space; a key naming two targets is an error. Facts live in the transfer's `EnvStore::join`; the driver holds only the worklist and the call-boundary callers. Selecting a context allocates its environment at once, so a caller can register on its parameters before the -deferred discovery scan runs. The per-context metadata (target, body, and the +deferred discovery scan runs. The per-context metadata (target and the return operands per output) lives on the transfer, keyed by `EnvIndex`. Each context also retains joined output requests so return rules visited later receive them. SSA facts remain in `EnvStore`; entry parameters come from the IR. @@ -615,14 +615,12 @@ these initial visits, including return declarations and calls. Fact raises join immediately; later changes schedule dependents through the table above. Thus a call may observe demand from an earlier initial visit without depending on block storage order. -The public backward `analyze` methods return `Product` parameter facts in -entry-parameter order, dual to forward analysis's returned output facts. `root()` -exposes the most recent successful root context separately; `contexts()` and -`fact()` retain detailed per-context inspection. `kirin_demand::analyze_demand` -collects these details into `DemandResult`, while `DemandAnalysis::analyze` -returns boundary facts directly. The two-point fact type is `Demand::{Dead, Live}`. -Exact products are checked against discovered return arity; all-bottom products -still seed intrinsic roots. Reused context keys accumulate facts monotonically. +The public backward `analyze` methods return the converged `Analyzed` state +(see [Reading results](#reading-results--analyzed-and-analysisresults)). The +root's `response` is the `Product` of parameter facts in entry-parameter +order, dual to forward analysis's return product. The two-point fact type is +`Demand::{Dead, Live}`. Exact products are checked against discovered return +arity; all-bottom products still seed intrinsic roots. Roots of an analysis are the supplied result lattice values, selected positionally, plus the control roots. @@ -635,14 +633,86 @@ Arity errors, resolution errors, and unsupported (graph) callee bodies are errors, never opaque fallbacks. There is no opaque-call policy. Indirect calls and closures are unsupported: like their forward rules, the `Lambda`/`Bind` demand rules return errors. -Facts accumulate across successful `analyze` calls on one engine, so shared -callees are discovered and solved once; `with_linker` and `with_policy` reset -the analysis state. Because facts only rise, a request whose policy key -matches an earlier context returns the join with the earlier requests: never -less demanded than a fresh engine, possibly more, and dependent on query -order. That happens for every repeated callee under `ContextInsensitive`, and -under `OutputDemandContext` once a callable's budget falls back to `Shared`. -An error consumes the engine, so a failed discovery is never reused. +Each `analyze` is one fresh analysis: it consumes the engine, and the +`Analyzed` state it returns cannot run again. Within that analysis shared +callees are discovered and solved once, and a request whose policy key matches +an earlier context of the same analysis joins into it. An error drops the +engine, so a failed discovery is never reused. + +### Reading results — `Analyzed` and `AnalysisResults` + +Every abstract engine's `analyze` consumes the engine and returns +`Analyzed`: the converged state of exactly one analysis. It has no +`analyze` of its own, so it always has one root and never mixes two runs. +All three engines implement one read-only trait on it: + +```rust +pub trait AnalysisResults { + type Context; // sparse engines: the policy key; liveness: LinkTarget + type Anchor: LatticeAnchor; // SSAValue or ProgramPoint + type Value: Lattice + Clone; + + // per context (required) + fn root(&self) -> &Self::Context; + fn contexts(&self) -> impl Iterator; + fn callable_of(&self, &Self::Context) -> Option; + fn fact(&self, &Self::Context, Self::Anchor) -> Option<&Self::Value>; + fn facts(&self, &Self::Context) -> impl Iterator; + fn response(&self, &Self::Context) -> Option>; + + // per callable (provided) + fn callables(&self) -> impl Iterator; + fn contexts_of(&self, LinkTarget) -> impl Iterator; + fn joined_fact(&self, LinkTarget, Self::Anchor) -> Option; + fn joined_facts(&self, LinkTarget) -> FactStore; + fn joined_response(&self, LinkTarget) -> Option>; +} +``` + +A **context** is one analyzed copy of a callable under specific boundary +facts. Facts are read at two levels: per context, and per callable (the join +over the callable's contexts). The **root** is the context `analyze` was asked +for. It is a handle, read like any other context; it exists because the +root's callable can have other contexts (recursion). + +| | Constprop | Demand | Liveness | +|---|---|---|---| +| `Context` | `CallContext::Key` | `DemandContext::Key` | `LinkTarget` | +| `contexts()` | selected contexts | discovered contexts | the root | +| `callable_of` | recorded target | recorded target | the context itself | +| `fact` / `facts` | the context's environment | the context's environment | the root's environment | +| `response` | joined return; `None` if no `ret` was reached | entry-parameter facts | `None` (calls are not followed) | + +`callable_of` is identity (which callable); `facts` is data (what was +concluded). With `LinkTarget::body`, `callable_of` leads from a context to its +IR. + +The **response** is what a context answers its caller. A callable's inputs +each have one SSA value, its parameter; its outputs do not, because output `i` +is fed by operand `i` of every return site. So constprop's return is context +metadata, not an anchored fact, and it cannot be derived from operand facts: +it joins only the *reached* return sites, and it distinguishes "never +returned" (`None`) from "returned nothing" (an empty product). Demand is the +mirror image: its response sits on the parameters, which are SSA values. + +Per-callable answers are computed on request from the per-context +environments and are never stored. They join the contexts **reached from the +root**: they claim nothing about callers the analysis did not visit, and +whether that is safe for a rewrite is the client's decision. + +`Analyzed` stores one thing beside the engine: an index from each reached +callable to its contexts' environments, built once when `analyze` finishes. +The sparse engines' `contexts_of` reads it and maps each environment back to +its context through `EnvStore::key`, so a per-callable query visits only that +callable's contexts. Liveness has one context and keeps the provided scan. + +Every per-context query is keyed by context, so a context is a query key and +`response`/`facts` its value. That keeps the API compatible with a later +query-based (memoized, incremental) engine, which this is not. + +The analysis crates add no result types: `kirin-constprop`, `kirin-demand` +and `kirin-liveness` export a lattice, a policy, and an engine alias +(`ConstProp`, `DemandAnalysis`, `DenseLiveness`). ### Engine internals: stage dispatch and IR queries @@ -1166,8 +1236,8 @@ and terminating on unknown inputs (both fold to `Top`). Runnable as points, nested structured-block boundaries) joins into that environment, so a block's `BlockEntry` always equals its first statement's `Before`. A rise of a block's `BlockEntry` reruns the predecessors that read it. - Queries take a bare `ProgramPoint` (the callable is the one `analyze` - returned); scf owns dense frames (arm-join, loop fixpoint). Classic liveness consumes finalized IR + Results are read through `AnalysisResults` with the analyzed + `LinkTarget` as the only context; scf owns dense frames (arm-join, loop fixpoint). Classic liveness consumes finalized IR directly and does not require a sparse-demand pre-pass. The two analyses live in separate crates (`kirin-demand`, `kirin-liveness`) that do not depend on each other. Filtering a dense set by demand drops diff --git a/example/toy-lang/src/interpreter/mod.rs b/example/toy-lang/src/interpreter/mod.rs index 6852b68f0..21bff3f3f 100644 --- a/example/toy-lang/src/interpreter/mod.rs +++ b/example/toy-lang/src/interpreter/mod.rs @@ -20,13 +20,13 @@ use std::hash::Hash; use kirin::prelude::{CFG, CompileStage, Pipeline}; use kirin_constprop::{ConstPropContext, ConstPropValue}; -use kirin_demand::DemandResult; +use kirin_demand::DemandAnalysis; use kirin_interpreter::engine::{ - ConcreteInterpreterCore, CrossStageLinker, Linker, SameStageLinker, SparseForwardInterpreter, - expect_single, + AnalysisResults, Analyzed, ConcreteInterpreterCore, CrossStageLinker, Linker, SameStageLinker, + SparseForwardInterpreter, expect_single, }; use kirin_interpreter::{Body, Callee, InterpDispatch, InterpreterError, SparseForwardTransfer}; -use kirin_liveness::{DenseLiveness, DenseLivenessResult, LiveSet}; +use kirin_liveness::{DenseLiveness, LiveSet}; use kirin_scf::ForLoopValue; use crate::stage::Stage; @@ -159,50 +159,49 @@ where >, { let analysis: ToyConstProp<'_, C> = ToyConstProp::new(pipeline).with_linker(CrossStageLinker); - let (ret, _) = analysis.analyze_by_name(stage_name, function_name, args.iter().cloned())?; - expect_single(ret) + let analyzed = analysis.analyze_by_name(stage_name, function_name, args.iter().cloned())?; + // A function that never returns has no response: no value to report. + expect_single(analyzed.response(analyzed.root()).unwrap_or_default()) } /// 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_demand::analyze_demand`]) is an independent analysis and is -/// not involved. +/// ([`analyze_demand`]) is an independent analysis and is not involved. /// /// Stage-pinned: a `function_name` with no live specialization at `stage_name` /// is an error, not a cue to analyze some other stage's body. -pub fn analyze_classic_liveness( - pipeline: &Pipeline, +pub fn analyze_classic_liveness<'ir>( + pipeline: &'ir Pipeline, stage_name: &str, function_name: &str, -) -> Result<(CompileStage, CFG, DenseLivenessResult), InterpreterError> { - let analysis: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - let (root, analysis) = analysis.analyze_by_name(stage_name, function_name)?; - let result = DenseLivenessResult::from_engine(&analysis, root); +) -> Result<(CompileStage, CFG, Analyzed>), InterpreterError> { + let analysis: ToyDenseLiveness<'_> = ToyDenseLiveness::<_>::new(pipeline); + let analyzed = analysis.analyze_by_name(stage_name, function_name)?; + let root = *analyzed.root(); let Body::CFG(cfg) = root.body(pipeline)? else { return Err(InterpreterError::Custom( "classic liveness target is not a CFG function", )); }; - Ok((root.stage, cfg, result)) + Ok((root.stage, cfg, analyzed)) } /// Run strong liveness (sparse backward demand) over `function_name`'s body /// at `stage_name`. Independent of [`analyze_classic_liveness`], and /// stage-pinned in the same way. -pub fn analyze_demand( - pipeline: &Pipeline, +pub fn analyze_demand<'ir>( + pipeline: &'ir Pipeline, stage_name: &str, function_name: &str, -) -> Result { +) -> Result>, InterpreterError> { let stage = pipeline .stage_by_name(stage_name) .ok_or_else(|| InterpreterError::MissingStageName(stage_name.into()))?; let function = pipeline .lookup_function_by_name(function_name) .ok_or_else(|| InterpreterError::MissingFunctionName(function_name.into()))?; - kirin_demand::analyze_demand( - pipeline, + DemandAnalysis::<_>::new(pipeline).analyze( stage, Callee::Function(function), [kirin_demand::Demand::Live], diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 49a88e5d1..ded73cb9b 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -580,7 +580,7 @@ fn constprop_lowered_unknown_cf_branch_joins_matching_returns() { /// Analysis-author surface for a custom abstract policy budget. mod advanced { use kirin_constprop::ConstPropContext; - use kirin_interpreter::{SameStageLinker, engine::expect_single}; + use kirin_interpreter::{AnalysisResults, SameStageLinker, engine::expect_single}; use super::{ConstProp, build_pipeline}; @@ -598,9 +598,10 @@ mod advanced { let analysis = crate::interpreter::ToyConstProp::new(&pipeline) .with_policy(ConstPropContext::with_budget(2)) .with_linker(SameStageLinker); - let (ret, _) = analysis + let analyzed = analysis .analyze_by_name("source", "factorial", [ConstProp::Const(5)]) .unwrap(); + let ret = analyzed.response(analyzed.root()).unwrap(); let result = expect_single::(ret).unwrap(); assert_eq!(result, ConstProp::Top); } @@ -621,9 +622,9 @@ mod demand { ParsePipelineText, Pipeline, SSAValue, Statement, }; use kirin_arith::{Arith, ArithValue}; - use kirin_demand::analyze_demand; + use kirin_demand::DemandAnalysis; use kirin_function::Lexical; - use kirin_interpreter::{Body, BodyScope, Callee}; + use kirin_interpreter::{AnalysisResults, Body, Callee, LinkTarget}; use crate::language::HighLevel; use crate::stage::Stage; @@ -645,12 +646,12 @@ mod demand { } /// Extract a CFG only after the interpreter has resolved the callable and - /// reported the target scope it actually analyzed. - pub(super) fn cfg_scope(scope: BodyScope) -> (CompileStage, CFG) { - let (stage, Body::CFG(cfg)) = scope else { - panic!("expected analysis root to resolve to a CFG, got {scope:?}"); + /// reported the target it actually analyzed. + pub(super) fn cfg_scope(pipeline: &Pipeline, target: LinkTarget) -> (CompileStage, CFG) { + let Ok(Body::CFG(cfg)) = target.body(pipeline) else { + panic!("expected analysis root to resolve to a CFG, got {target:?}"); }; - (stage, cfg) + (target.stage, cfg) } /// The parameters of the CFG's entry block. @@ -783,14 +784,11 @@ specialize @source fn @if_body(i64) -> i64 cfg { fn scf_if_body_demand_follows_result_demand() { let pipeline = parse(IF_BODY_DEMAND); let (caller_stage, callee) = source_root(&pipeline, "if_body"); - let result = analyze_demand( - &pipeline, - caller_stage, - callee, - [kirin_demand::Demand::Live], - ) - .expect("analysis succeeds"); - let (_, cfg) = cfg_scope(result.root_scope()); + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [kirin_demand::Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let (_, cfg) = cfg_scope(&pipeline, root); let cond = entry_params(&pipeline, cfg)[0]; let if_result = find_value(&pipeline, cfg, |definition| match definition { @@ -798,18 +796,24 @@ specialize @source fn @if_body(i64) -> i64 cfg { _ => None, }); - assert!(result.is_demanded(if_result), "ret demands the if result"); - assert!(result.is_demanded(cond), "condition is a control root"); assert!( - result.is_demanded(constant_result(&pipeline, cfg, 1)), + result.is_demanded(result.root(), if_result), + "ret demands the if result" + ); + assert!( + result.is_demanded(result.root(), cond), + "condition is a control root" + ); + assert!( + result.is_demanded(result.root(), constant_result(&pipeline, cfg, 1)), "then-arm yield operand feeds the demanded result" ); assert!( - result.is_demanded(constant_result(&pipeline, cfg, 2)), + result.is_demanded(result.root(), constant_result(&pipeline, cfg, 2)), "else-arm yield operand feeds the demanded result" ); assert!( - !result.is_demanded(constant_result(&pipeline, cfg, 9)), + !result.is_demanded(result.root(), constant_result(&pipeline, cfg, 9)), "a dead constant inside a body stays dead" ); } @@ -839,14 +843,11 @@ specialize @source fn @if_dead(i64) -> i64 cfg { fn scf_if_dead_result_keeps_only_condition() { let pipeline = parse(IF_DEAD_RESULT); let (caller_stage, callee) = source_root(&pipeline, "if_dead"); - let result = analyze_demand( - &pipeline, - caller_stage, - callee, - [kirin_demand::Demand::Live], - ) - .expect("analysis succeeds"); - let (_, cfg) = cfg_scope(result.root_scope()); + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [kirin_demand::Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let (_, cfg) = cfg_scope(&pipeline, root); let cond = entry_params(&pipeline, cfg)[0]; let if_result = find_value(&pipeline, cfg, |definition| match definition { @@ -854,11 +855,14 @@ specialize @source fn @if_dead(i64) -> i64 cfg { _ => None, }); - assert!(result.is_demanded(cond), "condition is a control root"); - assert!(result.is_demanded(constant_result(&pipeline, cfg, 0))); - assert!(!result.is_demanded(if_result)); - assert!(!result.is_demanded(constant_result(&pipeline, cfg, 1))); - assert!(!result.is_demanded(constant_result(&pipeline, cfg, 2))); + assert!( + result.is_demanded(result.root(), cond), + "condition is a control root" + ); + assert!(result.is_demanded(result.root(), constant_result(&pipeline, cfg, 0))); + assert!(!result.is_demanded(result.root(), if_result)); + assert!(!result.is_demanded(result.root(), constant_result(&pipeline, cfg, 1))); + assert!(!result.is_demanded(result.root(), constant_result(&pipeline, cfg, 2))); } pub(super) const FOR_CARRIED_DEMAND: &str = r#" @@ -886,14 +890,11 @@ specialize @source fn @loop_sum(i64, i64, i64) -> i64 cfg { fn scf_for_loop_carried_demand_converges() { let pipeline = parse(FOR_CARRIED_DEMAND); let (caller_stage, callee) = source_root(&pipeline, "loop_sum"); - let result = analyze_demand( - &pipeline, - caller_stage, - callee, - [kirin_demand::Demand::Live], - ) - .expect("analysis succeeds"); - let (_, cfg) = cfg_scope(result.root_scope()); + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [kirin_demand::Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let (_, cfg) = cfg_scope(&pipeline, root); let params = entry_params(&pipeline, cfg); let (lo, hi, step) = (params[0], params[1], params[2]); @@ -922,20 +923,27 @@ specialize @source fn @loop_sum(i64, i64, i64) -> i64 cfg { }; assert!( - result.is_demanded(constant_result(&pipeline, cfg, 0)), + result.is_demanded(result.root(), constant_result(&pipeline, cfg, 0)), "init" ); assert!( - result.is_demanded(constant_result(&pipeline, cfg, 1)), + result.is_demanded(result.root(), constant_result(&pipeline, cfg, 1)), "one" ); - assert!(result.is_demanded(next), "yield slot"); - assert!(result.is_demanded(carried), "carried param feeds the add"); + assert!(result.is_demanded(result.root(), next), "yield slot"); assert!( - !result.is_demanded(induction), + result.is_demanded(result.root(), carried), + "carried param feeds the add" + ); + assert!( + !result.is_demanded(result.root(), induction), "the unused induction variable stays dead" ); - assert!(result.is_demanded(lo) && result.is_demanded(hi) && result.is_demanded(step)); + assert!( + result.is_demanded(result.root(), lo) + && result.is_demanded(result.root(), hi) + && result.is_demanded(result.root(), step) + ); } const FOR_DEAD_RESULT: &str = r#" @@ -961,14 +969,11 @@ specialize @source fn @loop_dead(i64, i64, i64) -> i64 cfg { fn scf_for_dead_result_keeps_only_bounds() { let pipeline = parse(FOR_DEAD_RESULT); let (caller_stage, callee) = source_root(&pipeline, "loop_dead"); - let result = analyze_demand( - &pipeline, - caller_stage, - callee, - [kirin_demand::Demand::Live], - ) - .expect("analysis succeeds"); - let (_, cfg) = cfg_scope(result.root_scope()); + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [kirin_demand::Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let (_, cfg) = cfg_scope(&pipeline, root); let params = entry_params(&pipeline, cfg); let next = find_value(&pipeline, cfg, |definition| match definition { @@ -976,19 +981,22 @@ specialize @source fn @loop_dead(i64, i64, i64) -> i64 cfg { _ => None, }); - assert!(result.is_demanded(params[0]), "bounds are control roots"); - assert!(result.is_demanded(params[1])); - assert!(result.is_demanded(params[2])); - assert!(result.is_demanded(constant_result(&pipeline, cfg, 7))); assert!( - !result.is_demanded(constant_result(&pipeline, cfg, 0)), + result.is_demanded(result.root(), params[0]), + "bounds are control roots" + ); + assert!(result.is_demanded(result.root(), params[1])); + assert!(result.is_demanded(result.root(), params[2])); + assert!(result.is_demanded(result.root(), constant_result(&pipeline, cfg, 7))); + assert!( + !result.is_demanded(result.root(), constant_result(&pipeline, cfg, 0)), "init dead" ); assert!( - !result.is_demanded(constant_result(&pipeline, cfg, 1)), + !result.is_demanded(result.root(), constant_result(&pipeline, cfg, 1)), "body interior dead" ); - assert!(!result.is_demanded(next), "yield slot dead"); + assert!(!result.is_demanded(result.root(), next), "yield slot dead"); } const CALL_PURITY: &str = r#" @@ -1019,14 +1027,11 @@ specialize @source fn @main(i64, i64) -> i64 cfg { fn ignored_call_does_not_demand_arguments() { let pipeline = parse(CALL_PURITY); let (caller_stage, callee) = source_root(&pipeline, "main"); - let result = analyze_demand( - &pipeline, - caller_stage, - callee, - [kirin_demand::Demand::Live], - ) - .expect("analysis succeeds"); - let (_, cfg) = cfg_scope(result.root_scope()); + let result = DemandAnalysis::<_>::new(&pipeline) + .analyze(caller_stage, callee, [kirin_demand::Demand::Live]) + .expect("analysis succeeds"); + let root = result.callable_of(result.root()).expect("root is reached"); + let (_, cfg) = cfg_scope(&pipeline, root); let params = entry_params(&pipeline, cfg); let (x, y) = (params[0], params[1]); @@ -1041,11 +1046,20 @@ specialize @source fn @main(i64, i64) -> i64 cfg { _ => None, }); - assert!(!result.is_demanded(x), "an ignored call demands nothing"); - assert!(result.is_demanded(constant_result(&pipeline, cfg, 0))); - assert!(!result.is_demanded(unused), "the call result is unused"); - assert!(!result.is_demanded(y), "pure add with dead result"); - assert!(!result.is_demanded(deadsum)); + assert!( + !result.is_demanded(result.root(), x), + "an ignored call demands nothing" + ); + assert!(result.is_demanded(result.root(), constant_result(&pipeline, cfg, 0))); + assert!( + !result.is_demanded(result.root(), unused), + "the call result is unused" + ); + assert!( + !result.is_demanded(result.root(), y), + "pure add with dead result" + ); + assert!(!result.is_demanded(result.root(), deadsum)); } } @@ -1059,8 +1073,8 @@ specialize @source fn @main(i64, i64) -> i64 cfg { mod dense { use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue}; use kirin_arith::{Arith, ArithValue}; - use kirin_interpreter::{Body, Callee, ProgramPoint}; - use kirin_liveness::{DenseLivenessResult, LiveSet}; + use kirin_interpreter::{AnalysisResults, Analyzed, Body, Callee, ProgramPoint}; + use kirin_liveness::LiveSet; use kirin_scf::StructuredControlFlow; use super::demand::{FOR_CARRIED_DEMAND, IF_DEAD_RESULT}; @@ -1073,19 +1087,20 @@ mod dense { /// Run classic dense liveness with the toy total frame, returning the /// analyzed root's CFG alongside the result. - fn analyze_dense_toy( - pipeline: &Pipeline, + fn analyze_dense_toy<'ir>( + pipeline: &'ir Pipeline, caller_stage: CompileStage, callee: Callee, - ) -> (DenseLivenessResult, CFG) { - let engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - let (root, engine) = engine + ) -> (Analyzed>, CFG) { + let engine: ToyDenseLiveness<'_> = ToyDenseLiveness::<_>::new(pipeline); + let analyzed = engine .analyze(caller_stage, callee) .expect("analysis succeeds"); + let root = *analyzed.root(); let Body::CFG(cfg) = root.body(pipeline).expect("analyzed root has a body") else { panic!("expected analysis root to resolve to a CFG, got {root:?}"); }; - (DenseLivenessResult::from_engine(&engine, root), cfg) + (analyzed, cfg) } fn live_set(values: &[SSAValue]) -> LiveSet { @@ -1112,11 +1127,11 @@ 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.point_facts(ProgramPoint::After(a_const)), + dense.fact(dense.root(), ProgramPoint::After(a_const)), Some(&live_set(&[cond, a])) ); assert_eq!( - dense.point_facts(ProgramPoint::Before(a_const)), + dense.fact(dense.root(), ProgramPoint::Before(a_const)), Some(&live_set(&[cond])) ); @@ -1126,7 +1141,7 @@ mod dense { matches!(definition, HighLevel::Structured(_)) }); assert_eq!( - dense.point_facts(ProgramPoint::Before(if_stmt)), + dense.fact(dense.root(), ProgramPoint::Before(if_stmt)), Some(&live_set(&[cond])) ); @@ -1135,11 +1150,11 @@ mod dense { _ => None, }); assert_eq!( - dense.point_facts(ProgramPoint::BlockEntry(then_block)), + dense.fact(dense.root(), ProgramPoint::BlockEntry(then_block)), Some(&live_set(&[cond])) ); assert_eq!( - dense.point_facts(ProgramPoint::BlockExit(then_block)), + dense.fact(dense.root(), ProgramPoint::BlockExit(then_block)), Some(&live_set(&[cond])) ); } @@ -1184,11 +1199,11 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 cfg { // 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(ProgramPoint::After(if_stmt)), + dense.fact(dense.root(), ProgramPoint::After(if_stmt)), Some(&live_set(&[r])) ); assert_eq!( - dense.point_facts(ProgramPoint::Before(if_stmt)), + dense.fact(dense.root(), ProgramPoint::Before(if_stmt)), Some(&live_set(&[cond, x, y])) ); } @@ -1229,11 +1244,11 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 cfg { // Around the loop. assert_eq!( - dense.point_facts(ProgramPoint::After(for_stmt)), + dense.fact(dense.root(), ProgramPoint::After(for_stmt)), Some(&live_set(&[sum])) ); assert_eq!( - dense.point_facts(ProgramPoint::Before(for_stmt)), + dense.fact(dense.root(), ProgramPoint::Before(for_stmt)), Some(&live_set(&[lo, hi, step, init])) ); @@ -1241,11 +1256,11 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 cfg { // constant are live before the add; the yield slot is live after it // (it feeds the next iteration through the carry). assert_eq!( - dense.point_facts(ProgramPoint::Before(add_stmt)), + dense.fact(dense.root(), ProgramPoint::Before(add_stmt)), Some(&live_set(&[lo, hi, step, init, acc, one])) ); assert_eq!( - dense.point_facts(ProgramPoint::After(add_stmt)), + dense.fact(dense.root(), 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 3a7329893..36061b4d1 100644 --- a/example/toy-lang/src/main.rs +++ b/example/toy-lang/src/main.rs @@ -5,7 +5,7 @@ mod stage; use clap::{Parser, Subcommand}; use kirin::prelude::*; use kirin::pretty::PipelinePrintExt; -use kirin_interpreter::ProgramPoint; +use kirin_interpreter::{AnalysisResults, ProgramPoint}; use stage::Stage; @@ -122,9 +122,17 @@ fn run_program( if demand { let result = interpreter::analyze_demand(&pipeline, stage_name, func_name)?; - let demanded = result.demanded().iter().copied(); + let root = result.root(); + let stage = result + .callable_of(root) + .ok_or_else(|| anyhow::anyhow!("demand root was not reached"))? + .stage; + let demanded = result + .facts(root) + .filter(|(_, fact)| fact.is_live()) + .map(|(value, _)| value); let demanded = match pipeline - .stage(result.root_scope().0) + .stage(stage) .ok_or_else(|| anyhow::anyhow!("resolved stage is missing"))? { Stage::Source(info) => value_set(info, demanded), @@ -177,10 +185,10 @@ fn run_program( fn print_block_liveness( info: &StageInfo, cfg: CFG, - dense: &kirin_liveness::DenseLivenessResult, + dense: &impl AnalysisResults, ) { for block in cfg.blocks(info) { - let point = |item| dense.point_facts(item); + let point = |item| dense.fact(dense.root(), item); let (Some(live_in), Some(live_out)) = ( point(ProgramPoint::BlockEntry(block)), point(ProgramPoint::BlockExit(block)), diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 6754799cf..215da3fb1 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -36,11 +36,11 @@ use kirin_constant::Constant; use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_function::Lexical; use kirin_interpreter::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, BlockFrame, - Body, CFGFrame, CallFrame, CallRequest, Callee, Completion, ConcreteInterpreter, - ConcreteInterpreterCore, ContextInsensitive, DefaultCallBodyTraversal, DiGraphFrame, Frame, - FrameEffect, FrameEngine, Interpretable, InterpreterError, SameStageLinker, - SparseForwardInterpreter, expect_single, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AnalysisResults, BlockFrame, Body, CFGFrame, CallFrame, CallRequest, Callee, Completion, + ConcreteInterpreter, ConcreteInterpreterCore, ContextInsensitive, DefaultCallBodyTraversal, + DiGraphFrame, Frame, FrameEffect, FrameEngine, Interpretable, InterpreterError, + SameStageLinker, SparseForwardInterpreter, expect_single, }; use kirin_scf::{ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -700,8 +700,8 @@ fn analyze( ) -> Result { let analysis: AbstractEngine<'_> = SparseForwardInterpreter::new(pipeline).with_linker(SameStageLinker); - let (ret, _) = analysis.analyze_by_name("test", function, args.iter().cloned())?; - expect_single(ret) + let analyzed = analysis.analyze_by_name("test", function, args.iter().cloned())?; + expect_single(analyzed.response(analyzed.root()).unwrap_or_default()) } type InsensitiveEngine<'ir> = SparseForwardInterpreter< @@ -724,8 +724,8 @@ fn analyze_insensitive( ) -> Result { let analysis: InsensitiveEngine<'_> = SparseForwardInterpreter::new(pipeline).with_linker(SameStageLinker); - let (ret, _) = analysis.analyze_by_name("test", function, args.iter().cloned())?; - expect_single(ret) + let analyzed = analysis.analyze_by_name("test", function, args.iter().cloned())?; + expect_single(analyzed.response(analyzed.root()).unwrap_or_default()) } #[test] @@ -762,28 +762,23 @@ fn sparse_forward_symbol_entry_matches_name_and_direct_callee() { let args = || [ConstPropValue::Const(2), ConstPropValue::Const(3)]; let by_name: AbstractEngine<'_> = SparseForwardInterpreter::new(&pipeline); - let by_name = expect_single::( - by_name.analyze_by_name("test", "gadd", args()).unwrap().0, - ) - .unwrap(); + let by_name = by_name.analyze_by_name("test", "gadd", args()).unwrap(); + let by_name = + expect_single::(by_name.response(by_name.root()).unwrap()).unwrap(); let by_symbol: AbstractEngine<'_> = SparseForwardInterpreter::new(&pipeline); - let by_symbol = expect_single::( - by_symbol - .analyze_by_symbol(stage, symbol, args()) - .unwrap() - .0, - ) - .unwrap(); + let by_symbol = by_symbol.analyze_by_symbol(stage, symbol, args()).unwrap(); + let by_symbol = + expect_single::(by_symbol.response(by_symbol.root()).unwrap()) + .unwrap(); let by_callee: AbstractEngine<'_> = SparseForwardInterpreter::new(&pipeline); - let by_callee = expect_single::( - by_callee - .analyze(stage, Callee::Named(symbol), args()) - .unwrap() - .0, - ) - .unwrap(); + let by_callee = by_callee + .analyze(stage, Callee::Named(symbol), args()) + .unwrap(); + let by_callee = + expect_single::(by_callee.response(by_callee.root()).unwrap()) + .unwrap(); assert_eq!(by_name, ConstPropValue::Const(5)); assert_eq!(by_symbol, by_name); diff --git a/tests/constprop_generic.rs b/tests/constprop_generic.rs index 83f2d2a6a..b78afba70 100644 --- a/tests/constprop_generic.rs +++ b/tests/constprop_generic.rs @@ -1,6 +1,6 @@ use kirin::prelude::*; use kirin_constprop::{ConstProp, ConstPropValue}; -use kirin_interpreter::{InterpreterError, expect_single}; +use kirin_interpreter::{AnalysisResults, InterpreterError, expect_single}; use kirin_test_languages::ScalarFunctionLanguage; #[test] @@ -21,13 +21,14 @@ specialize @test fn @add(i32, i32) -> i32 cfg { .unwrap(); let analysis = ConstProp::<_, i32, InterpreterError>::new(&pipeline); - let (result, _) = analysis + let analyzed = analysis .analyze_by_name( "test", "add", [ConstPropValue::Const(2), ConstPropValue::Const(3)], ) .unwrap(); + let result = analyzed.response(analyzed.root()).unwrap(); assert_eq!( expect_single::<_, InterpreterError>(result).unwrap(), ConstPropValue::Const(5)