diff --git a/AGENTS.md b/AGENTS.md index e87979af5..2029c6384 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - `kirin-derive-chumsky` — `#[derive(HasParser, PrettyPrint)]` (proc-macro + code generation) **Interpreter:** -- `kirin-interpreter` — interpreter framework. Shared pieces: `Interp`, `Interpretable`, `Frame`/`drive_frames`, and the owner-summary fixpoint driver (`StandardFixpointInterpreter`). **Semantics vs shape**: dialect rules dispatch on a `SemanticKey` (`ForwardEval`, `StrongDemand`, `ClassicLiveness`, or a downstream key — the Rust analogue of Kirin 1.0's string keys like `"main"`/`"typeinfer"`/`"constprop"`/`"qubit.address"`); each key declares the `AnalysisShape` its solver runs on (`SparseForwardShape`/`SparseBackwardShape`/`DenseForwardShape`/`DenseBackwardShape` — mechanics only, never dispatch tags). Two keys may share one shape. Each key joins its shape's *family* (`SparseForwardSemantic`/`SparseBackwardSemantic`/`DenseForwardSemantic`/`DenseBackwardSemantic`), and the engines/transfers are generic over the key with the canonical default (`SparseForwardTransfer<..., Sem = ForwardEval>`, `SparseBackwardInterpreter<..., Sem = StrongDemand>`, `DenseBackwardInterpreter<..., Sem = ClassicLiveness>`), so a downstream key reuses an engine by instantiating `Sem`. Engine traits are shape-generic mechanics (`SparseForwardInterp` read/write; `SparseBackwardInterp` fact/`raise_fact`/effect/topology; `DenseBackwardInterp` opaque `point_state`/`point_state_mut`); semantics-specific helper vocabulary lives in key-pinned helper traits (`DemandInterp`: `demand`/`is_demanded`/`demand_uses_if_observable`; `ClassicLivenessInterp`: `gen_live`/`kill_def`/`gen_uses_kill_defs`) — demand rules bind `DemandInterp`, classic-liveness rules bind `ClassicLivenessInterp`. **The shape layer never says what a fact is**: `raise_fact` takes the lattice element to merge, the dense point state is opaque, and the only state contracts the engines and dialect frames name are `Lattice` (merges) plus `DenseBackwardState` (`rename`/`forget`, for CFG edges and `scf.for`'s back-edge). Fact-shaped contracts are key-pinned instead — `HasTop` on `DemandInterp`, `PointFacts` on `ClassicLivenessInterp` — carried as associated-type bounds in the supertrait so elaboration keeps dialect rules from spelling them. Effects per shape: `SparseForwardEffect`, `SparseBackwardEffect`, `DenseBackwardEffect`; engines: `ConcreteInterpreter`, `SparseForwardInterpreter`, `SparseBackwardInterpreter`, `DenseBackwardInterpreter`. `InterpDispatch` is keyed on the engine alone — the dispatched key is always `I::Semantics`, so a stage can never be paired with a foreign key's rules. `DenseForwardShape` (typestate) has no key yet. `AbstractInterpreter` is the marker trait for lattice-valued engines. **Source layout** (public API is unchanged — everything re-exports through `lib.rs` plus the `dialect`/`engine` preludes): `core/` (chassis: `Interp`/dispatch/effects/frame protocol/env/error/linker/queries), `semantics/` (`keys.rs` + `shape.rs`), `facts/` (`anchor.rs`/`store.rs`/`topology.rs`), `fixpoint/` (convergence driver), `engines/` (`concrete/`, `sparse_forward/`, `sparse_backward/`, `dense_backward/`, each `interp.rs` + optional `frames.rs`). +- `kirin-interpreter` — interpreter framework. Shared pieces: `Interp`, `Interpretable`, `Frame`/`drive_frames`, and the owner-summary fixpoint driver (`StandardFixpointInterpreter`). **Semantics vs shape**: dialect rules dispatch on a `SemanticKey` (`ForwardEval`, `StrongDemand`, `ClassicLiveness`, or a downstream key — the Rust analogue of Kirin 1.0's string keys like `"main"`/`"typeinfer"`/`"constprop"`/`"qubit.address"`); each key declares the `AnalysisShape` its solver runs on (`SparseForwardShape`/`SparseBackwardShape`/`DenseForwardShape`/`DenseBackwardShape` — mechanics only, never dispatch tags). Two keys may share one shape. Each key joins its shape's *family* (`SparseForwardSemantic`/`SparseBackwardSemantic`/`DenseForwardSemantic`/`DenseBackwardSemantic`), and the engines/transfers are generic over the key with the canonical default (`SparseForwardTransfer<..., Sem = ForwardEval>`, `SparseBackwardInterpreter<..., Sem = StrongDemand>`, `DenseBackwardInterpreter<..., Sem = ClassicLiveness>`), so a downstream key reuses an engine by instantiating `Sem`. Engine traits are shape-generic mechanics (`SparseForwardInterp` read/write; `SparseBackwardInterp` fact/`raise_fact`/effect/topology; `DenseBackwardInterp` opaque `point_state`/`point_state_mut`); semantics-specific helper vocabulary lives in key-pinned helper traits (`DemandInterp`: `demand`/`is_demanded`/`demand_uses_if_observable`; `ClassicLivenessInterp`: `gen_live`/`kill_def`/`gen_uses_kill_defs`) — demand rules bind `DemandInterp`, classic-liveness rules bind `ClassicLivenessInterp`. **The shape layer never says what a fact is**: `raise_fact` takes the lattice element to merge, the dense point state is opaque, and the only state contracts the engines and dialect frames name are `Lattice` (merges) plus `DenseBackwardState` (`rename`/`forget`, for CFG edges and `scf.for`'s back-edge). Fact-shaped contracts are key-pinned instead — `HasTop` on `DemandInterp`, `PointFacts` on `ClassicLivenessInterp` — carried as associated-type bounds in the supertrait so elaboration keeps dialect rules from spelling them. Effects per shape: `SparseForwardEffect`, `SparseBackwardEffect`, `DenseBackwardEffect`; engines: `ConcreteInterpreter`, `SparseForwardInterpreter`, `SparseBackwardInterpreter`, `DenseBackwardInterpreter`. `InterpDispatch` is keyed on the engine alone — the dispatched key is always `I::Semantics`, so a stage can never be paired with a foreign key's rules. `DenseForwardShape` (typestate) has no key yet. `AbstractInterpreter` is the marker trait for lattice-valued engines. **Source layout** (public API is unchanged — everything re-exports through `lib.rs` plus the `dialect`/`engine` preludes): `core/` (chassis: `Interp`/dispatch/effects/frame protocol/`env` — the `EnvStore` container plus `Env`/`SSABinding`/error/linker/queries), `semantics/` (`keys.rs` + `shape.rs`), `facts/` (`anchor.rs`/`store.rs`/`topology.rs`), `fixpoint/` (convergence driver), `engines/` (`concrete/`, `sparse_forward/`, `sparse_backward/`, `dense_backward/`, each `interp.rs` + optional `frames.rs`). **Dialects:** - `kirin-cf`, `kirin-scf`, `kirin-constant`, `kirin-arith`, `kirin-bitwise`, `kirin-cmp`, `kirin-function` @@ -145,7 +145,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Two-persona contract**: Dialect authors implement `Interpretable` and designate a callable definition's body with `#[kirin(callable_body)]`. A rule receives the engine `interp: &mut I` directly and uses the `interp.read`/`interp.write` helpers, which are **default methods on `SparseForwardInterp`** operating on the engine's current activation (`interp.index()`); forward rules bound `I: SparseForwardInterp` so they can return `SparseForwardEffect` as `I::Effect`, plus value-domain bounds on `I::Value`. Compiler authors compose language enums with derives, pick a value type, error type, engine, and linker; when needed, they can also opt into custom frame types or custom abstract policies. Imports come from `kirin_interpreter::dialect` and `kirin_interpreter::engine` respectively. Customizing traversal is part of the compiler-author surface, not a separate persona. -- **Statement dispatch**: Dialect statements implement `Interpretable` — specialized on the engine `I` and the `ForwardEval` semantic key. A forward rule (`I: SparseForwardInterp`) reads/writes SSA state through the `SparseForwardInterp` **default-method** helpers (`interp.read`, `interp.write`, `interp.read_many`, `interp.write_results` — which delegate to the engine's `Env` storage access at `interp.index()`; there is no `ForwardCtx`/`ValueContext` type) and returns `Result`, building `SparseForwardEffect`: atomic ops return `SparseForwardEffect::Next`; control ops return `Jump`/`Branch` (CFG edges), `Call`, `Yield`/`Return` (completions), or `Push` (run a sub-computation by pushing a dialect-owned frame, then bind its results). There is **no** framework "scope" type and no framework "explore alternatives" effect. +- **Statement dispatch**: Dialect statements implement `Interpretable` — specialized on the engine `I` and the `ForwardEval` semantic key. A forward rule (`I: SparseForwardInterp`) reads/writes SSA state through the `SparseForwardInterp` **default-method** helpers (`interp.read`, `interp.write`, `interp.read_many`, `interp.write_results` — which delegate to the engine's `Env` access at `interp.index()`; there is no `ForwardCtx`/`ValueContext` type) and returns `Result`, building `SparseForwardEffect`: atomic ops return `SparseForwardEffect::Next`; control ops return `Jump`/`Branch` (CFG edges), `Call`, `Yield`/`Return` (completions), or `Push` (run a sub-computation by pushing a dialect-owned frame, then bind its results). There is **no** framework "scope" type and no framework "explore alternatives" effect. - **Dialects are engine-blind**: one `Interpretable` impl serves concrete execution and abstract interpretation; the value domain decides. Undecided conditions (`BranchCondition::is_truthy` / `ForLoopValue::loop_condition` returning `None`) are read in the rule and handed to the dialect's own frame, which rejects them under concrete execution and explores+joins under abstract. (`Branch` is the cf CFG analogue, driven by the engine's CFG frame.) Never write per-engine dialect impls — but a control dialect's *frame* may have distinct concrete/abstract forms, built per-engine through a dialect dispatch trait. @@ -155,13 +155,15 @@ 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 then reads the specialization's authoritative definition at `target.stage` through IR's `HasCallableBody`, yielding `ResolvedCallable { target, body }`. 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`/`read`/`write`/`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 bottom, widening, dependencies, or scheduling. `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: log the read, absent is bottom). 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 summaries, dependencies, and the worklist. Centralized environment *storage*, deliberately without centralized call control. Forward dependency bookkeeping (callee-summary → caller owners, plus context-qualified SSA fact → reader owners) lives in `ForwardDeps` (`engines/sparse_forward/deps.rs`), never in a type presented as the value store. + - **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` and `kirin-liveness`). - **Call-body traversal is concrete configuration**: `CallFrame` owns the call convention (resolve, allocate the activation, enter, suspend, validate the completion, free exactly once, bind results) and delegates *only* which child continuation enters the callee body to `CallBodyTraversal`. The private composition root selects the traversal as `T` in its `CallFrame` stack-item variant; `CallFrame` means `CallFrame`. `CallRequest` carries no `T`; converting it into the configured stack-item attaches that compile-time choice. **Concrete execution only** — forward abstract interpretation summarizes calls (`AbstractCallFrame`) and maps a callable body to an `Owner` instead of descending, and the backward engines never walk callable bodies through a call frame. This traversal is not consulted for nested bodies: `scf.if`/`scf.for` and other structured operations keep choosing their own dialect continuations. -- **Engine capabilities are per-frame, not per-engine**: `core/frame.rs` splits the forward engine surface into component traits named after the *capability* they supply — `StatementDispatch: Interp` (dispatch a statement), `BlockQueries: Interp` (read-only block queries), `CFGQueries: BlockQueries` (`cfg_entry`), `DiGraphQueries: Interp` (`digraph_walk_plan`), `CallServices: Env` (activation allocation/freeing and resolved-callable discovery; kept whole because `CallFrame` consumes their pairing as a safety property — `CallFrame` still owns the *convention*). **A member frame bounds only what it consumes** (`ScfIfFrame` needs just `FrameEngine`; `ScfForFrame` just `Env`). A stack-item composition's `Frame` implementation requires the union of its variants' member bounds, because one engine must be able to run every continuation admitted to that stack. `ForwardDataflowFrameEngine` extends only `Env + StatementDispatch + BlockQueries + DiGraphQueries` — an abstract engine summarizes calls and seeds owners, so it must **not** be made to inherit `CallServices` or `CFGQueries`. `tests/frame_engine_capabilities.rs` holds mock engines whose ability to compile is the regression test; widening a member frame's bound breaks it. +- **Engine capabilities are per-frame, not per-engine**: `core/frame.rs` splits the forward engine surface into component traits named after the *capability* they supply — `StatementDispatch: Interp` (dispatch a statement), `BlockQueries: Interp` (read-only block queries), `CFGQueries: BlockQueries` (`cfg_entry`), `DiGraphQueries: Interp` (`digraph_walk_plan`), `CallServices: Interp` (activation lifetime plus resolved-callable discovery; the `alloc_env`/`free_env` pair is kept whole because `CallFrame` consumes their pairing as a safety property — `CallFrame` still owns the *convention*). `Env: Interp` is the one capability defined elsewhere — `core/env/services.rs`, next to the `EnvStore` container it operates on. **`Env` and `CallServices` are independent siblings on `Interp`, neither a supertrait of the other** — "what does an access mean?" and "where do activations come from?" are different questions — so a frame needing both spells both: `CallFrame` is `I: CallServices + Env`. Do not re-couple them; `ForwardFrameEngine` therefore lists `Env` explicitly. **A member frame bounds only what it consumes** (`ScfIfFrame` needs just `FrameEngine`; `ScfForFrame` just `Env`) — which is why `alloc_env`/`free_env` must not migrate onto `Env`: that would make every read/write-only frame claim a lifecycle it never exercises. A stack-item composition's `Frame` implementation requires the union of its variants' member bounds, because one engine must be able to run every continuation admitted to that stack. `ForwardDataflowFrameEngine` extends only `Env + StatementDispatch + BlockQueries + DiGraphQueries` — an abstract engine summarizes calls and seeds owners, so it must **not** be made to inherit `CallServices` or `CFGQueries`. `tests/frame_engine_capabilities.rs` holds mock engines whose ability to compile is the regression test; widening a member frame's bound breaks it. -- **Naming rule for these traits**: `drive_frames` is the only *driver* at this layer (the frame-stack loop); `ForwardDriver`/`DenseBackwardDriver` are fixpoint-driver structs. Capability traits are named for what they supply, never `*FrameDriver` — do not reintroduce that suffix, and do not add compatibility aliases for it. `FrameEngine` = minimal contract for the generic frame stack; `ForwardFrameEngine`/`ForwardDataflowFrameEngine`/`DenseBackwardFrameEngine` = capability sets required by their corresponding families of computations. The `*Queries` traits must stay **read-only** and require only `Interp`: block-entry binding lives on the crate-private `BlockBinding: Env + BlockQueries` so no query trait's name hides a store mutation. Binding into an explicitly named activation is `Env::bind_values(index, slots, values)`; `SparseForwardInterp::write_results` is the dialect-facing current-activation helper. Names describe the operation, not the `Product` container. +- **Naming rule for these traits**: `drive_frames` is the only *driver* at this layer (the frame-stack loop); `ForwardDriver`/`DenseBackwardDriver` are fixpoint-driver structs. Capability traits are named for what they supply, never `*FrameDriver` — do not reintroduce that suffix, and do not add compatibility aliases for it. `FrameEngine` = minimal contract for the generic frame stack; `ForwardFrameEngine`/`ForwardDataflowFrameEngine`/`DenseBackwardFrameEngine` = capability sets required by their corresponding families of computations. The `*Queries` traits must stay **read-only** and require only `Interp`: block-entry binding lives on the crate-private `BlockBinding: SSABinding + BlockQueries` so no query trait's name hides a store mutation. Binding into an explicitly named activation is `SSABinding::bind_values(index, slots, values)`; `SparseForwardInterp::write_results` is the dialect-facing current-activation helper. Names describe the operation, not the `Product` container. - **`StatementDispatch` vs `InterpDispatch`**: opposite directions. `InterpDispatch` is implemented by a *stage/language* to route a statement to its dialect rule. `StatementDispatch` is implemented by the *engine* and is what a frame calls: it stashes the current location (`stage`/`statement`/`index`) for the rule to read back through `Interp`, then delegates to `InterpDispatch`. diff --git a/crates/kirin-constprop/src/context.rs b/crates/kirin-constprop/src/context.rs index aa94209b7..9d868ded5 100644 --- a/crates/kirin-constprop/src/context.rs +++ b/crates/kirin-constprop/src/context.rs @@ -24,7 +24,7 @@ use std::collections::{HashMap, HashSet}; use kirin_interpreter::{ CallContext, ContextInsensitive, InterpreterError, LinkTarget, WideningStrategy, }; -use kirin_ir::{CompileStage, Product, SpecializedFunction}; +use kirin_ir::Product; use crate::ConstPropValue; @@ -43,7 +43,11 @@ pub enum CallCtx { pub struct ConstPropContext { control: ContextInsensitive, max_contexts: usize, - admitted: HashMap<(CompileStage, SpecializedFunction), HashSet>>, + /// Per-target admitted constant tuples. Keyed by the resolved + /// [`LinkTarget`] — the same identity [`ContextInsensitive`] uses as its + /// whole key, so the budget is spent per function-in-a-stage exactly as + /// before. + admitted: HashMap>>, } impl ConstPropContext { @@ -66,15 +70,16 @@ impl Default for ConstPropContext { } } +/// The context-insensitive key ([`LinkTarget`]) plus the call context that +/// refines it. Context sensitivity *adds* to the resolved target's identity +/// rather than re-spelling it. impl CallContext for ConstPropContext { - type Key = (CompileStage, SpecializedFunction, CallCtx); + type Key = (LinkTarget, CallCtx); fn key(&mut self, target: &LinkTarget, args: &Product) -> Self::Key { - let stage = target.stage; - let function = target.specialization; let ctx = match all_const(args) { Some(consts) => { - let admitted = self.admitted.entry((stage, function)).or_default(); + let admitted = self.admitted.entry(*target).or_default(); if admitted.contains(&consts) { CallCtx::Args(consts) } else if admitted.len() < self.max_contexts { @@ -87,7 +92,7 @@ impl CallContext for ConstPropContext { } None => CallCtx::Unknown, }; - (stage, function, ctx) + (*target, ctx) } } diff --git a/crates/kirin-interpreter/src/core/env.rs b/crates/kirin-interpreter/src/core/env.rs deleted file mode 100644 index b3ea12f1b..000000000 --- a/crates/kirin-interpreter/src/core/env.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::collections::HashMap; - -use kirin_ir::{Product, SSAValue}; - -use crate::InterpreterError; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct EnvIndex(usize); - -impl EnvIndex { - pub(crate) fn new(index: usize) -> Self { - Self(index) - } - - pub fn raw(self) -> usize { - self.0 - } -} - -pub trait Store { - type Error; - - fn alloc(&mut self) -> EnvIndex; - fn free(&mut self, index: EnvIndex) -> Result<(), Self::Error>; - fn read(&self, index: EnvIndex, value: SSAValue) -> Result; - fn write(&mut self, index: EnvIndex, value: SSAValue, data: V) -> Result<(), Self::Error>; - - fn read_many(&self, index: EnvIndex, values: &[SSAValue]) -> Result, Self::Error> { - values - .iter() - .map(|value| self.read(index, *value)) - .collect() - } - - fn write_product( - &mut self, - index: EnvIndex, - values: &[SSAValue], - data: Product, - ) -> Result<(), Self::Error> - where - Self::Error: From, - { - if data.len() != values.len() { - return Err(Self::Error::from(InterpreterError::ProductArityMismatch { - expected: values.len(), - actual: data.len(), - })); - } - - for (value, data) in values.iter().copied().zip(data) { - self.write(index, value, data)?; - } - - Ok(()) - } -} - -#[derive(Clone, Debug)] -pub struct EnvStackStore { - stores: Vec>>, - stack: Vec, -} - -impl Default for EnvStackStore { - fn default() -> Self { - Self::new() - } -} - -impl EnvStackStore { - pub fn new() -> Self { - Self { - stores: Vec::new(), - stack: Vec::new(), - } - } - - pub fn push(&mut self) -> EnvIndex { - let index = self.alloc_store(); - self.stack.push(index); - index - } - - pub fn pop(&mut self) -> Result { - let index = self.stack.pop().ok_or(InterpreterError::EmptyEnvStack)?; - self.free_store(index)?; - Ok(index) - } - - pub fn current(&self) -> Result { - self.stack - .last() - .copied() - .ok_or(InterpreterError::EmptyEnvStack) - } - - fn store(&self, index: EnvIndex) -> Result<&HashMap, InterpreterError> { - self.stores - .get(index.raw()) - .and_then(Option::as_ref) - .ok_or(InterpreterError::InvalidEnvIndex(index)) - } - - fn store_mut( - &mut self, - index: EnvIndex, - ) -> Result<&mut HashMap, InterpreterError> { - self.stores - .get_mut(index.raw()) - .and_then(Option::as_mut) - .ok_or(InterpreterError::InvalidEnvIndex(index)) - } - - fn alloc_store(&mut self) -> EnvIndex { - let index = EnvIndex::new(self.stores.len()); - self.stores.push(Some(HashMap::new())); - index - } - - fn free_store(&mut self, index: EnvIndex) -> Result<(), InterpreterError> { - let store = self - .stores - .get_mut(index.raw()) - .ok_or(InterpreterError::InvalidEnvIndex(index))?; - if store.take().is_some() { - Ok(()) - } else { - Err(InterpreterError::InvalidEnvIndex(index)) - } - } -} - -impl Store for EnvStackStore { - type Error = InterpreterError; - - fn alloc(&mut self) -> EnvIndex { - self.alloc_store() - } - - fn free(&mut self, index: EnvIndex) -> Result<(), Self::Error> { - self.free_store(index) - } - - fn read(&self, index: EnvIndex, value: SSAValue) -> Result { - self.store(index)? - .get(&value) - .cloned() - .ok_or(InterpreterError::UnboundValue { index, value }) - } - - fn write(&mut self, index: EnvIndex, value: SSAValue, data: V) -> Result<(), Self::Error> { - self.store_mut(index)?.insert(value, data); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use kirin_ir::TestSSAValue; - - use super::*; - - #[test] - fn stack_store_reads_and_writes_live_envs() { - let mut env = EnvStackStore::new(); - let index = env.push(); - let value = SSAValue::from(TestSSAValue(0)); - - env.write(index, value, 42).unwrap(); - - assert_eq!(env.read(index, value).unwrap(), 42); - assert_eq!(env.current().unwrap(), index); - } - - #[test] - fn popped_env_is_no_longer_live() { - let mut env = EnvStackStore::::new(); - let index = env.push(); - - assert_eq!(env.pop().unwrap(), index); - assert_eq!( - env.read(index, SSAValue::from(TestSSAValue(0))), - Err(InterpreterError::InvalidEnvIndex(index)) - ); - } -} diff --git a/crates/kirin-interpreter/src/core/env/mod.rs b/crates/kirin-interpreter/src/core/env/mod.rs new file mode 100644 index 000000000..3f9cb1e9e --- /dev/null +++ b/crates/kirin-interpreter/src/core/env/mod.rs @@ -0,0 +1,11 @@ +//! Environments: the storage container ([`EnvStore`]) and the engine capability that +//! operates on it ([`Env`]). + +mod services; +mod store; + +#[cfg(test)] +mod tests; + +pub use services::{Env, SSABinding}; +pub use store::{EnvIndex, EnvStore}; diff --git a/crates/kirin-interpreter/src/core/env/services.rs b/crates/kirin-interpreter/src/core/env/services.rs new file mode 100644 index 000000000..5ca937324 --- /dev/null +++ b/crates/kirin-interpreter/src/core/env/services.rs @@ -0,0 +1,101 @@ +use kirin_ir::{Product, SSAValue}; + +use crate::{EnvIndex, Interp, InterpreterError, LatticeAnchor}; + +/// The engine capability for *using* an environment: reading a fact out of one +/// and writing a fact into one, at whichever [`Anchor`](Env::Anchor) family the +/// engine attaches facts to. +/// +/// This is the layer where mechanism becomes policy. [`EnvStore`](crate::EnvStore) is +/// storage — it maps a context key to an environment and holds facts. This +/// trait is what an engine exposes on top of that storage, and each engine +/// decides what its own accesses *mean*: concrete execution reports an unbound +/// SSA read as an error, while a sparse-forward analysis logs the read and +/// treats an absent binding as bottom. +/// +/// **The access interface is anchor-generic.** A sparse engine anchors facts to +/// [`SSAValue`]s; a dense engine anchors them to +/// [`ProgramPoint`](crate::ProgramPoint)s. Both *use* an environment the same +/// way — read a fact, write a fact — so that shared vocabulary must not name one +/// anchor family. Operations that are genuinely SSA-shaped live on +/// [`SSABinding`] instead, which pins `Anchor = SSAValue` and is +/// blanket-implemented, so an SSA-anchored engine gets them for free and a +/// point-anchored engine is never asked for them. +/// +/// **Environment *lifetime* is deliberately not here.** Allocating and freeing +/// an activation belongs to the call boundary, so `alloc_env`/`free_env` live on +/// [`CallServices`](crate::CallServices) with `resolve_callable`, and a +/// [`CallFrame`](crate::CallFrame) owns pairing them correctly. Keeping them off +/// this trait is what lets a frame that only reads and writes — `ScfForFrame`, +/// `BlockCursor::write_child_results` — bound exactly what it consumes, and +/// what lets an abstract dataflow engine expose storage access without a call +/// convention it never performs. +/// +/// Selecting a *context key* is not here either: that is an analysis policy +/// decision, so keyed allocation +/// ([`EnvStore::get_or_allocate`](crate::EnvStore::get_or_allocate)) stays internal to the +/// engine that has a policy, and never appears on this shared surface. +pub trait Env: Interp { + /// Where this engine's environments attach facts: [`SSAValue`] for the + /// sparse shapes, [`ProgramPoint`](crate::ProgramPoint) for the dense ones. + /// + /// It is the same anchor the engine's + /// [`EnvStore<_, Anchor, _>`](crate::EnvStore) is parameterized by, which is + /// why it carries only [`LatticeAnchor`]'s `Clone + Eq + Hash`. + type Anchor: LatticeAnchor; + + /// Read the fact anchored at `anchor` in an activation. + fn env_read(&self, index: EnvIndex, anchor: Self::Anchor) -> Result; + /// Write the fact anchored at `anchor` in an activation. + fn env_write( + &mut self, + index: EnvIndex, + anchor: Self::Anchor, + data: Self::Value, + ) -> Result<(), Self::Error>; +} + +/// Positional SSA binding, for engines whose environments are anchored on +/// [`SSAValue`]. +/// +/// Split out of [`Env`] rather than defaulted on it: binding a *list* of values +/// to a *list* of slots is meaningful only where the anchor is an SSA value, so +/// it is bounded `Env` and blanket-implemented. That keeps +/// [`Env`]'s own vocabulary free of one anchor family while every SSA-anchored +/// engine still gets this for free — no engine implements it, and no dense +/// engine is asked to. +pub trait SSABinding: Env { + /// Positionally bind runtime values to SSA slots in an **explicitly + /// selected** activation, checking arity. + /// + /// The explicitly-addressed counterpart of + /// [`SparseForwardInterp::write_results`](crate::SparseForwardInterp::write_results), + /// which always binds into the engine's *current* activation + /// ([`Interp::index`]). Frames need this one: a frame binds results into the + /// activation it owns, which is not necessarily the one a dialect rule is + /// executing in. The two differ by *which activation*, not by what they do — + /// hence neither name mentions the [`Product`] container. + /// + /// It writes through [`Env::env_write`] rather than reaching into storage, + /// so an engine's logging and absence policy apply to bound values exactly + /// as they do to a dialect rule's writes. + fn bind_values( + &mut self, + index: EnvIndex, + slots: &[SSAValue], + values: Product, + ) -> Result<(), Self::Error> { + if slots.len() != values.len() { + return Err(Self::Error::from(InterpreterError::ProductArityMismatch { + expected: slots.len(), + actual: values.len(), + })); + } + for (slot, value) in slots.iter().copied().zip(values) { + self.env_write(index, slot, value)?; + } + Ok(()) + } +} + +impl> SSABinding for T {} diff --git a/crates/kirin-interpreter/src/core/env/store.rs b/crates/kirin-interpreter/src/core/env/store.rs new file mode 100644 index 000000000..b0ba7ffee --- /dev/null +++ b/crates/kirin-interpreter/src/core/env/store.rs @@ -0,0 +1,180 @@ +use std::collections::HashMap; +use std::hash::Hash; + +use crate::{FactStore, InterpreterError}; + +/// A handle to one allocated environment. The owning engine controls its +/// lifetime; the handle stays invalid once freed, because indices are never +/// reused. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct EnvIndex(usize); + +impl EnvIndex { + pub(crate) fn new(index: usize) -> Self { + Self(index) + } + + pub fn raw(self) -> usize { + self.0 + } +} + +/// One allocated environment: its facts, plus the context key it is registered +/// under, if any. Keeping the key next to the facts is what lets +/// [`EnvStore::free`] drop a context association in constant time without +/// scanning either the directory or the facts. +#[derive(Clone, Debug)] +struct Environment +where + A: Eq + Hash, +{ + key: Option, + facts: FactStore, +} + +/// Environments plus the directory that addresses them by context key. +/// +/// One container owns both halves of environment identity: `K -> EnvIndex` +/// (which analysis context an environment belongs to) and +/// `EnvIndex -> FactStore` (the facts it holds). Fact maps stay separate +/// per environment, so equal anchors under different contexts never collide. +/// +/// The container is deliberately ignorant of what it stores. It knows nothing +/// about bottom values, widening, dependencies, or scheduling, and an absent +/// anchor is reported as absent rather than interpreted: what absence *means* +/// is the engine's decision, made in [`Env`](crate::Env). +/// [`write`](Self::write) assigns and never joins. +/// +/// The *analysis* chooses context identity — a key `K` derived from a resolved +/// call target and its abstract arguments — and this container only maps that +/// identity to a live environment. Concrete execution has no context identity +/// at all: it allocates with [`alloc`](Self::alloc) and uses an uninhabited key +/// type, so two calls can never accidentally share an environment. +#[derive(Clone, Debug)] +pub struct EnvStore +where + A: Eq + Hash, +{ + context_indices: HashMap, + environments: Vec>>, +} + +impl Default for EnvStore +where + A: Eq + Hash, +{ + fn default() -> Self { + Self::new() + } +} + +impl EnvStore +where + A: Eq + Hash, +{ + pub fn new() -> Self { + Self { + context_indices: HashMap::new(), + environments: Vec::new(), + } + } + + /// Allocate a fresh environment with no context association. + /// + /// Every call returns a distinct handle to a distinct, empty environment. + pub fn alloc(&mut self) -> EnvIndex { + self.alloc_with(None) + } + + /// Inspect the facts of a live environment. + pub fn environment(&self, index: EnvIndex) -> Result<&FactStore, InterpreterError> { + self.environments + .get(index.raw()) + .and_then(Option::as_ref) + .map(|environment| &environment.facts) + .ok_or(InterpreterError::InvalidEnvIndex(index)) + } + + /// Read the value stored at `anchor`, or `None` when the anchor holds + /// nothing. + /// + /// The only error is an invalid `index`, which is what keeps a dead + /// environment distinguishable from an absent anchor. + pub fn read(&self, index: EnvIndex, anchor: A) -> Result, InterpreterError> + where + V: Clone, + { + Ok(self.environment(index)?.get(anchor).cloned()) + } + + /// Assign `value` at `anchor`, replacing whatever was stored there. + pub fn write(&mut self, index: EnvIndex, anchor: A, value: V) -> Result<(), InterpreterError> { + self.environment_mut(index)?.set(anchor, value); + Ok(()) + } + + fn alloc_with(&mut self, key: Option) -> EnvIndex { + let index = EnvIndex::new(self.environments.len()); + self.environments.push(Some(Environment { + key, + facts: FactStore::new(), + })); + index + } + + fn environment_mut( + &mut self, + index: EnvIndex, + ) -> Result<&mut FactStore, InterpreterError> { + self.environments + .get_mut(index.raw()) + .and_then(Option::as_mut) + .map(|environment| &mut environment.facts) + .ok_or(InterpreterError::InvalidEnvIndex(index)) + } +} + +impl EnvStore +where + K: Clone + Eq + Hash, + A: Eq + Hash, +{ + /// The environment registered under `key`, without allocating one. + /// + /// Every directory entry addresses a live environment: [`free`](Self::free) + /// drops the association along with the environment. + pub fn context_env(&self, key: &K) -> Option { + self.context_indices.get(key).copied() + } + + /// The environment registered under `key`, allocating and registering one on + /// first use. + /// + /// Equal keys share one environment for as long as it stays live; distinct + /// keys are isolated. + pub fn get_or_allocate(&mut self, key: K) -> EnvIndex { + if let Some(index) = self.context_env(&key) { + return index; + } + let index = self.alloc_with(Some(key.clone())); + self.context_indices.insert(key, index); + index + } + + /// Retire an environment and drop its context association, if it had one. + /// + /// The handle stays invalid afterwards, and a later + /// [`get_or_allocate`](Self::get_or_allocate) of the same key allocates a + /// fresh environment rather than resurrecting this one. + pub fn free(&mut self, index: EnvIndex) -> Result<(), InterpreterError> { + let environment = self + .environments + .get_mut(index.raw()) + .and_then(Option::take) + .ok_or(InterpreterError::InvalidEnvIndex(index))?; + if let Some(key) = environment.key { + self.context_indices.remove(&key); + } + Ok(()) + } +} diff --git a/crates/kirin-interpreter/src/core/env/tests.rs b/crates/kirin-interpreter/src/core/env/tests.rs new file mode 100644 index 000000000..28c799db5 --- /dev/null +++ b/crates/kirin-interpreter/src/core/env/tests.rs @@ -0,0 +1,117 @@ +use std::convert::Infallible; + +use kirin_ir::{SSAValue, TestSSAValue}; + +use super::EnvStore; +use crate::InterpreterError; + +/// The concrete engine's container shape: no context identity at all. +type Anonymous = EnvStore; + +/// A keyed container standing in for an analysis that keys environments by +/// context (a `(stage, function)` pair here, spelled as a plain integer). +type Keyed = EnvStore; + +fn ssa(index: usize) -> SSAValue { + SSAValue::from(TestSSAValue(index)) +} + +#[test] +fn alloc_is_always_fresh_but_equal_keys_reuse_one_environment() { + let mut env = Anonymous::new(); + assert_ne!(env.alloc(), env.alloc()); + + let mut env = Keyed::new(); + let first = env.get_or_allocate(1); + assert_eq!(env.get_or_allocate(1), first); + assert_eq!(env.context_env(&1), Some(first)); + // An unkeyed allocation is never handed out for a key, and vice versa. + assert_ne!(env.alloc(), first); + assert_ne!(env.get_or_allocate(2), first); +} + +#[test] +fn distinct_contexts_isolate_the_same_anchor() { + let mut env = Keyed::new(); + let first = env.get_or_allocate(1); + let second = env.get_or_allocate(2); + let value = ssa(0); + + env.write(first, value, 2).unwrap(); + env.write(second, value, 9).unwrap(); + + assert_eq!(env.read(first, value), Ok(Some(2))); + assert_eq!(env.read(second, value), Ok(Some(9))); +} + +#[test] +fn assignment_replaces_the_stored_value() { + let mut env = Keyed::new(); + let index = env.get_or_allocate(1); + let value = ssa(0); + + env.write(index, value, 2).unwrap(); + env.write(index, value, 5).unwrap(); + + // Storage assigns; merging a new value into an old one is an analysis + // decision made above this layer. + assert_eq!(env.read(index, value), Ok(Some(5))); +} + +#[test] +fn freeing_a_key_releases_it_for_a_fresh_environment() { + let mut env = Keyed::new(); + let first = env.get_or_allocate(1); + let value = ssa(0); + env.write(first, value, 7).unwrap(); + + env.free(first).unwrap(); + + assert_eq!(env.context_env(&1), None); + let second = env.get_or_allocate(1); + assert_ne!(second, first); + // Freed indices are not reused, so the old facts cannot leak into the new + // environment. + assert_eq!(env.read(second, value), Ok(None)); +} + +#[test] +fn freed_environments_stay_invalid() { + let mut env = Keyed::new(); + let index = env.get_or_allocate(1); + + env.free(index).unwrap(); + + assert_eq!( + env.free(index), + Err(InterpreterError::InvalidEnvIndex(index)) + ); + assert_eq!( + env.read(index, ssa(0)), + Err(InterpreterError::InvalidEnvIndex(index)) + ); + assert_eq!( + env.write(index, ssa(0), 1), + Err(InterpreterError::InvalidEnvIndex(index)) + ); +} + +#[test] +fn an_invalid_handle_is_distinguishable_from_an_absent_anchor() { + let mut env = Keyed::new(); + let index = env.get_or_allocate(1); + let missing = ssa(0); + + // Live environment, nothing stored: absent, not an error. The engine — not + // storage — decides whether that is an unbound-value error or bottom. + assert_eq!(env.read(index, missing), Ok(None)); + assert!(!env.environment(index).unwrap().contains(missing)); + + env.free(index).unwrap(); + + assert_eq!( + env.read(index, missing), + Err(InterpreterError::InvalidEnvIndex(index)) + ); + assert!(env.environment(index).is_err()); +} diff --git a/crates/kirin-interpreter/src/core/error.rs b/crates/kirin-interpreter/src/core/error.rs index 833f9e81e..410e62c8b 100644 --- a/crates/kirin-interpreter/src/core/error.rs +++ b/crates/kirin-interpreter/src/core/error.rs @@ -13,8 +13,6 @@ pub enum InterpreterError { InvalidEnvIndex(EnvIndex), #[error("unbound SSA value {value} in environment {index:?}")] UnboundValue { index: EnvIndex, value: SSAValue }, - #[error("environment stack is empty")] - EmptyEnvStack, #[error("frame stack is empty")] EmptyFrameStack, #[error("missing stage {0:?}")] diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 3f82a38b4..ac3cc0131 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -16,7 +16,9 @@ //! - the **component traits** below are narrowly scoped services an interpreter //! engine supplies *to individual frames*, and the two **umbrellas** //! ([`ForwardFrameEngine`], [`ForwardDataflowFrameEngine`]) name the full -//! capability set for a whole standard frame universe. +//! capability set for a whole standard frame universe. [`Env`] is the +//! exception that lives elsewhere ([`env`](super::env)), next to the [`EnvStore`] +//! container it operates on. //! //! # The capability model //! @@ -29,10 +31,24 @@ //! | trait | capability | consumed by | //! |---|---|---| //! | [`StatementDispatch`] | dispatch a statement to its dialect rule | every executing frame | +//! | [`Env`] | read/write one fact at an [`Anchor`](Env::Anchor) in an activation | every frame that touches storage | +//! | [`SSABinding`] | positional binding into SSA slots (`: Env`) | [`CallFrame`](crate::CallFrame), block/graph walkers | //! | [`BlockQueries`] | read-only structural queries for walking one block | [`BlockFrame`](crate::BlockFrame), [`AbstractBlockFrame`](crate::AbstractBlockFrame), dialect block walkers | //! | [`CFGQueries`] | find a CFG's entry block (`: BlockQueries`) | [`CFGFrame`](crate::CFGFrame) | //! | [`DiGraphQueries`] | schedule a digraph body | [`DiGraphFrame`](crate::DiGraphFrame) | -//! | [`CallServices`] | activation storage, linking, callable-entry dispatch | [`CallFrame`](crate::CallFrame) | +//! | [`CallServices`] | activation lifetime + callable discovery | [`CallFrame`](crate::CallFrame), *with* [`SSABinding`] | +//! +//! [`Env`] is *using* an activation — one read, one write, at whichever anchor +//! family the engine attaches facts to. Binding a *list* of values to SSA slots +//! is only meaningful for an SSA-anchored engine, so it lives on the +//! blanket-implemented [`SSABinding`] instead of narrowing [`Env`] itself. +//! Creating and retiring an activation is the call boundary's business, so +//! `alloc_env`/`free_env` sit on [`CallServices`] next to `resolve_callable`. +//! The two are **siblings on [`Interp`]**, neither a supertrait of the other, so +//! a frame that only reads and writes never claims a lifecycle it does not +//! exercise, and the abstract dataflow engine stays free of a call convention it +//! never performs. A frame needing both spells both: `CallFrame` is +//! `I: CallServices + Env`. //! //! The `*Queries` traits are exactly that: **read-only**. The one operation that //! needs both a query and a write — binding a block's parameters to incoming @@ -52,7 +68,7 @@ //! frame enum belongs on an umbrella — a universe's engine must support the //! union of all its variants — while a member frame names only its components: //! -//! - [`ForwardFrameEngine`] — the full concrete surface: all four components, +//! - [`ForwardFrameEngine`] — the full concrete surface: all five components, //! blanket-implemented. //! - [`ForwardDataflowFrameEngine`] — abstract dataflow: the traversal //! components abstract execution *shares*, plus merge/summarization. It does @@ -63,7 +79,9 @@ use std::hash::Hash; use kirin_ir::{Block, CFG, CompileStage, Product, SSAValue, Statement}; -use crate::{Body, CallEffect, Callee, Env, EnvIndex, Interp, InterpreterError, ResolvedCallable}; +use crate::{ + Body, CallEffect, Callee, Env, EnvIndex, Interp, InterpreterError, ResolvedCallable, SSABinding, +}; /// Structural effect a [`Frame`] returns to the engine driver loop. /// @@ -254,8 +272,10 @@ pub trait CFGQueries: BlockQueries { /// Deliberately not on [`BlockQueries`] (whose name promises read-only) and /// deliberately not public: it is frame-internal mechanics, blanket-implemented /// for every engine with both capabilities, so a frame that binds a block entry -/// spells its requirement honestly as `Env + BlockQueries`. -pub(crate) trait BlockBinding: Env + BlockQueries { +/// spells its requirement honestly as `Env + BlockQueries`. +/// It builds on [`SSABinding`] rather than [`Env`] directly, because a block's +/// parameters are SSA slots. +pub(crate) trait BlockBinding: SSABinding + BlockQueries { /// Positionally bind a block's parameters to incoming actuals in `index`, /// checking arity. fn bind_block_args( @@ -280,7 +300,7 @@ pub(crate) trait BlockBinding: Env + BlockQueries { } } -impl BlockBinding for T {} +impl BlockBinding for T {} /// Structural/scheduling queries needed to traverse a /// [`DiGraph`](kirin_ir::DiGraph) body. @@ -308,22 +328,35 @@ pub trait DiGraphQueries: Interp { } } -/// Engine services used by [`CallFrame`](crate::CallFrame): activation storage, -/// linking, and structural callable-body discovery. +/// Engine services a call boundary needs beyond *using* an activation: +/// creating and retiring one, and discovering the callee's body. +/// +/// A **sibling** of [`Env`], not a subtrait of it. The two answer +/// different questions — "what does an access to this activation mean?" versus +/// "where do activations come from, and whose body am I entering?" — so neither +/// silently drags the other in, and each frame states exactly which it +/// consumes. [`CallFrame`](crate::CallFrame) consumes both, and says so: +/// `I: CallServices + Env`. A frame that only reads and writes +/// (`ScfForFrame`, `BlockCursor::write_child_results`) names [`Env`] +/// alone and claims no lifecycle it never exercises. +/// +/// `alloc_env` and `free_env` do stay together: the standard `CallFrame` +/// consumes them as a pair, and their pairing is a safety property — an +/// `alloc_env` without its matching `free_env` is a leak, a second `free_env` a +/// double free. Splitting *those* would let an engine offer half a lifecycle. +/// [`resolve_callable`](Self::resolve_callable) is the linker-plus-target-stage +/// body discovery protocol; it carries no value product, and its mechanism lives +/// in [`linker`](super::linker) — selecting identity is the +/// [`Linker`](crate::Linker)'s job, reading structure is IR's. /// /// **[`CallFrame`](crate::CallFrame) still owns the calling convention** — the /// order of operations, which completions are legal, and freeing the activation /// exactly once. This trait only supplies the primitives it calls. /// -/// Kept whole on purpose: the standard `CallFrame` consumes these services together, -/// and their pairing is a safety property — an `alloc_env` without its matching -/// `free_env` is a leak, a second `free_env` a double free. Splitting them into -/// separate capabilities would let an engine offer half a call convention. -/// /// Notably *not* required by abstract dataflow: forward abstract interpretation /// summarizes a call instead of descending into it, so -/// [`ForwardDataflowFrameEngine`] does not extend this trait. -pub trait CallServices: Env { +/// [`ForwardDataflowFrameEngine`] requires [`Env`] and not this trait. +pub trait CallServices: Interp { /// Allocate a fresh SSA activation record. fn alloc_env(&mut self) -> EnvIndex; /// Free an activation record. @@ -342,18 +375,25 @@ pub trait CallServices: Env { /// /// This is an umbrella, not a definition — it adds no methods and is /// [blanket-implemented](#impl-ForwardFrameEngine-for-T) for any engine -/// providing the four components. Use it at the *composition* level, where the +/// providing the five components. Use it at the *composition* level, where the /// engine driving a configured `FrameStackItem` enum must support the union of /// all its variants. /// Individual member frames should bound only the components they use, so a /// partial engine can still run them. +/// +/// [`Env`] and [`CallServices`] are both listed because they are +/// independent siblings: the standard concrete universe both *uses* activations +/// (every walker) and *creates* them (`CallFrame`), and neither trait implies +/// the other. The env is pinned to `Anchor = SSAValue`: every walker in this +/// universe binds block parameters and result slots, so the umbrella would not +/// actually cover its variants with a free anchor. pub trait ForwardFrameEngine: - StatementDispatch + CFGQueries + DiGraphQueries + CallServices + StatementDispatch + Env + CFGQueries + DiGraphQueries + CallServices { } impl ForwardFrameEngine for T where - T: StatementDispatch + CFGQueries + DiGraphQueries + CallServices + T: StatementDispatch + Env + CFGQueries + DiGraphQueries + CallServices { } @@ -365,7 +405,7 @@ impl ForwardFrameEngine for T where /// [`DiGraphQueries`] — the traversal it genuinely shares — and **deliberately /// not** [`CallServices`] or [`CFGQueries`]. An abstract engine does not descend /// into a callee (it [summarizes](Self::summarize_call) the call), so requiring -/// it to expose concrete activation allocation, activation cleanup, +/// it to expose activation allocation, activation cleanup, and /// `resolve_callable` would be demanding a call convention it /// never performs. `cfg_entry` is likewise absent: the forward abstract engine /// reaches a callable body's entry block through [`Owner`](crate::Owner) seeding @@ -385,7 +425,7 @@ impl ForwardFrameEngine for T where /// custom frame cannot reorder it and break soundness. Frames only decide /// *traversal*: which frame to step next. pub trait ForwardDataflowFrameEngine: - Env + StatementDispatch + BlockQueries + DiGraphQueries + Env + StatementDispatch + BlockQueries + DiGraphQueries { /// The key under which function entry/return summaries are tracked /// (the analysis [`CallContext::Key`](crate::CallContext::Key)). diff --git a/crates/kirin-interpreter/src/core/interp.rs b/crates/kirin-interpreter/src/core/interp.rs index 8ad12102e..70a89a1e2 100644 --- a/crates/kirin-interpreter/src/core/interp.rs +++ b/crates/kirin-interpreter/src/core/interp.rs @@ -1,6 +1,8 @@ use kirin_ir::{CompileStage, Product, SSAValue, Statement}; -use crate::{EnvIndex, InterpreterError, SemanticKey, SparseForwardEffect, SparseForwardSemantic}; +use crate::{ + Env, EnvIndex, InterpreterError, SemanticKey, SparseForwardEffect, SparseForwardSemantic, +}; // An engine names its semantics through [`Interp::Semantics`], a // [`SemanticKey`] from [`semantics`](crate::semantics): [`ForwardEval`](crate::ForwardEval) is @@ -58,49 +60,11 @@ pub trait Interp: Sized { /// by concrete engine specializations, not by this marker. pub trait AbstractInterpreter: Interp {} -/// SSA storage access used by forward engines. -pub trait Env: Interp { - /// Read an SSA value from an activation. - fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result; - /// Write an SSA value into an activation. - fn env_write( - &mut self, - index: EnvIndex, - value: SSAValue, - data: Self::Value, - ) -> Result<(), Self::Error>; - - /// Positionally bind runtime values to SSA slots in an **explicitly - /// selected** activation, checking arity. - /// - /// The explicitly-addressed counterpart of - /// [`SparseForwardInterp::write_results`], which always binds into the - /// engine's *current* activation ([`Interp::index`]). Frames need this one: - /// a frame binds results into the activation it owns, which is not - /// necessarily the one a dialect rule is executing in. The two differ by - /// *which activation*, not by what they do — hence neither name mentions the - /// [`Product`] container. - fn bind_values( - &mut self, - index: EnvIndex, - slots: &[SSAValue], - values: Product, - ) -> Result<(), Self::Error> { - if slots.len() != values.len() { - return Err(Self::Error::from(InterpreterError::ProductArityMismatch { - expected: slots.len(), - actual: values.len(), - })); - } - for (slot, value) in slots.iter().copied().zip(values) { - self.env_write(index, slot, value)?; - } - Ok(()) - } -} - -/// [`SparseForwardShape`](crate::SparseForwardShape)-engine flavor: env -/// access plus [`SparseForwardEffect`]. This is the *shape-generic* engine +/// [`SparseForwardShape`](crate::SparseForwardShape)-engine flavor: +/// SSA-anchored env access plus [`SparseForwardEffect`]. The anchor is pinned to +/// [`SSAValue`] because that is what the shape *is* — a sparse-forward analysis +/// attaches its facts to SSA values — and the helpers below take SSA values +/// directly. This is the *shape-generic* engine /// surface: env/read/write are how any sparse-forward semantics executes /// statements, so the blanket impl below covers every engine whose /// [`Semantics`](Interp::Semantics) is a [`SparseForwardSemantic`] — @@ -116,7 +80,7 @@ pub trait Env: Interp { /// [`SparseForwardEffect::Push`] carries a child; ordinary dialects do not name /// it. pub trait SparseForwardInterp: - Env + Interp::Value, Self::Frame>> + Env + Interp::Value, Self::Frame>> { /// The engine's child-continuation representation carried by /// [`SparseForwardEffect::Push`]. @@ -159,7 +123,7 @@ pub trait SparseForwardInterp: impl SparseForwardInterp for I where - I: Env + Interp>, + I: Env + Interp>, I::Semantics: SparseForwardSemantic, { type Frame = F; diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index ee89185ec..b4310f108 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -1,7 +1,7 @@ //! The shared interpreter chassis: the engine trait ([`Interp`]) and dialect //! dispatch ([`Interpretable`]), effect types, the direction-neutral frame -//! protocol, activation storage, calling conventions, errors, and the IR -//! queries ([`query`]) engines run against a stage. +//! protocol, environments ([`env`]), calling conventions ([`linker`]), errors, +//! and the IR queries ([`query`]) engines run against a stage. //! Everything here is engine-agnostic; the engines compose these pieces. pub(crate) mod dispatch; @@ -16,13 +16,13 @@ pub(crate) mod value; pub use dispatch::{InterpDispatch, Interpretable}; pub use effect::{CallEffect, Callee, Edge, SparseForwardEffect}; -pub use env::{EnvIndex, EnvStackStore, Store}; +pub use env::{Env, EnvIndex, EnvStore, SSABinding}; pub use error::InterpreterError; pub use frame::{ BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, }; -pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; +pub use interp::{AbstractInterpreter, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, LinkTarget, Linker, ResolvedCallable, SameStageLinker}; pub use query::{GraphWalkPlan, StageQuery, TerminatorArgs}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs index d220ff327..4862aeb06 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs @@ -1,7 +1,7 @@ use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; use crate::core::frame::BlockBinding; -use crate::{BlockQueries, Env, EnvIndex, InterpreterError}; +use crate::{BlockQueries, Env, EnvIndex, InterpreterError, SSABinding}; /// Block-cursor mechanics shared by the block-shaped walkers /// ([`BlockFrame`](super::BlockFrame) and [`CFGFrame`](super::CFGFrame)): @@ -44,7 +44,7 @@ impl BlockCursor { /// on this call (the frame should `Continue` and step again). pub(super) fn bind_entry(&mut self, interp: &mut I) -> Result where - I: Env + BlockQueries, + I: Env + BlockQueries, { match self.pending.take() { Some(args) => { @@ -77,7 +77,7 @@ impl BlockCursor { args: &Product, ) -> Result<(), I::Error> where - I: Env + BlockQueries, + I: Env + BlockQueries, { interp.bind_block_args(self.stage, self.index, target, args)?; self.cursor = interp.first_statement(self.stage, target)?; @@ -97,7 +97,7 @@ impl BlockCursor { values: Product, ) -> Result<(), I::Error> where - I: Env, + I: Env, I::Error: From, { let slots = self.resume_slots.take().ok_or_else(|| { diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs index cd1af225b..abf9445a9 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -1,8 +1,8 @@ use kirin_ir::{CompileStage, Product, SSAValue}; use crate::{ - Body, CallEffect, CallServices, Callee, EnvIndex, Frame, FrameEffect, InterpreterError, - ResolvedCallable, + Body, CallEffect, CallServices, Callee, Env, EnvIndex, Frame, FrameEffect, InterpreterError, + ResolvedCallable, SSABinding, }; use super::{BodyFrameEntry, CallBodyTraversal, Completion, DefaultCallBodyTraversal}; @@ -119,9 +119,20 @@ impl From> for CallFrame { } } +/// The call boundary consumes two independent capabilities and names both: +/// [`CallServices`] to create the callee activation, resolve its body, and free +/// it again, and an SSA-anchored [`Env`] to bind the completion's results back +/// into the *caller's* activation ([`SSABinding::bind_values`], which is +/// blanket-implemented, so `Env` is the whole storage +/// requirement). +/// +/// Notably absent: [`StatementDispatch`](crate::StatementDispatch) and any +/// statement-effect algebra. A call boundary resolves, allocates, enters, +/// suspends, frees, and binds — it never interprets a statement, so it must not +/// be made to require the forward engine surface. impl Frame for CallFrame where - I: CallServices, + I: CallServices + Env, T: CallBodyTraversal, V: Clone, E: From, diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs index bc2741908..cd35e5fcb 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs @@ -1,8 +1,8 @@ use kirin_ir::{CompileStage, Product, SSAValue, Statement}; use crate::{ - DiGraphQueries, Env, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, - SparseForwardInterp, StatementDispatch, + DiGraphQueries, Env, EnvIndex, Frame, FrameEffect, InterpreterError, SSABinding, + SparseForwardEffect, SparseForwardInterp, StatementDispatch, }; use super::{CallRequest, Completion}; @@ -75,7 +75,7 @@ where /// not [`DiGraphQueries`], whose schedule was already consumed. fn finish(self, interp: &mut I) -> Result, F>, E> where - I: Env, + I: Env, { let values: Product = self .yields diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index 93bb7196c..b022516c9 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -1,3 +1,4 @@ +use std::convert::Infallible; use std::marker::PhantomData; use kirin_ir::{ @@ -7,13 +8,21 @@ use kirin_ir::{ use crate::core::{linker::link_and_discover_callable, query}; use crate::{ BlockQueries, CFGQueries, CallServices, Callee, Completion, DiGraphQueries, Env, EnvIndex, - EnvStackStore, ForwardEval, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, - Linker, ResolvedCallable, SameStageLinker, SparseForwardEffect, StageQuery, StatementDispatch, - Store, drive_frames, + EnvStore, ForwardEval, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, + ResolvedCallable, SameStageLinker, SparseForwardEffect, StageQuery, StatementDispatch, + drive_frames, }; use super::frames::{CallRequest, FrameStackItem}; +/// The concrete engine's environments. +/// +/// Concrete execution has no analysis contexts: every call allocates its own +/// activation, so the context key is [`Infallible`] — uninhabited, which makes +/// it *impossible* for two calls to share an environment through a common key. +/// Only [`EnvStore::alloc`] is reachable. +type ConcreteEnv = EnvStore; + /// Concrete interpreter mechanism parameterized by one private frame-stack-item type. /// /// Language crates keep `F` private and expose a domain-specific wrapper, as @@ -22,7 +31,7 @@ use super::frames::{CallRequest, FrameStackItem}; pub struct ConcreteInterpreterCore<'ir, S: StageMeta, V, E, Lk, F> { pipeline: &'ir Pipeline, linker: Lk, - store: EnvStackStore, + env: ConcreteEnv, frames: Vec, /// The statement location currently being dispatched, exposed to dialect /// rules through [`Interp::stage`]/[`Interp::statement`]/[`Interp::index`]. @@ -69,7 +78,7 @@ impl<'ir, S: StageMeta, V, E, F> ConcreteInterpreterCore<'ir, S, V, E, SameStage Self { pipeline, linker: SameStageLinker, - store: EnvStackStore::new(), + env: EnvStore::new(), frames: Vec::new(), location: None, _marker: PhantomData, @@ -83,7 +92,7 @@ impl<'ir, S: StageMeta, V, E, Lk, F> ConcreteInterpreterCore<'ir, S, V, E, Lk, F ConcreteInterpreterCore { pipeline: self.pipeline, linker, - store: self.store, + env: self.env, frames: self.frames, location: self.location, _marker: PhantomData, @@ -125,12 +134,21 @@ where V: Clone, E: From, { + /// Concrete execution is SSA-anchored: a runtime value lives at the SSA + /// value that defines it. + type Anchor = SSAValue; + + /// Concrete execution has no bottom to fall back on: reading a slot nothing + /// has written yet is a program error, not a fact about the value. fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { - self.store.read(index, value).map_err(E::from) + self.env + .read(index, value) + .map_err(E::from)? + .ok_or_else(|| E::from(InterpreterError::UnboundValue { index, value })) } fn env_write(&mut self, index: EnvIndex, value: SSAValue, data: V) -> Result<(), E> { - self.store.write(index, value, data).map_err(E::from) + self.env.write(index, value, data).map_err(E::from) } } @@ -146,11 +164,11 @@ where Lk: Linker, { fn alloc_env(&mut self) -> EnvIndex { - self.store.alloc() + self.env.alloc() } fn free_env(&mut self, index: EnvIndex) -> Result<(), E> { - self.store.free(index).map_err(E::from) + self.env.free(index).map_err(E::from) } fn resolve_callable( diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/deps.rs b/crates/kirin-interpreter/src/engines/sparse_forward/deps.rs new file mode 100644 index 000000000..68304a799 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/sparse_forward/deps.rs @@ -0,0 +1,95 @@ +//! Forward dependency bookkeeping: who reruns when something rises. +//! +//! The forward fixpoint has two kinds of dependency, and both are scheduling +//! bookkeeping rather than storage: +//! +//! - **callee summary → caller owners**, the generic +//! [`SummaryDependencyIndex`] edge the driver consults after a summary +//! changes (`ForwardSummaryDeps`, registered on a call, including same-key +//! self-recursion); +//! - **context-qualified SSA fact → reader owners**, for the direct dominated +//! cross-block uses that never travel along a block edge: a block that read a +//! value it did not define reruns when that value rises. +//! +//! [`ForwardDeps`] holds both. Only the first is a driver-visible dependency +//! index; the second is consulted by the forward engine's own `apply_update`. + +use std::collections::{HashMap, HashSet}; +use std::convert::Infallible; +use std::hash::Hash; + +use kirin_ir::SSAValue; + +use crate::{ForwardSummaryDeps, SummaryDependencies, SummaryDependency, SummaryDependencyIndex}; + +use super::interp::Owner; + +/// Context-qualified key for value-reader dependencies: the same [`SSAValue`] +/// under two different function contexts is two distinct facts, so readers never +/// cross-contaminate. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ValueFactKey { + pub function: K, + pub value: SSAValue, +} + +/// Both forward dependency kinds for one analysis. +pub struct ForwardDeps { + summaries: ForwardSummaryDeps>, + value_readers: HashMap, HashSet>>, +} + +impl Default for ForwardDeps { + fn default() -> Self { + Self { + summaries: ForwardSummaryDeps::default(), + value_readers: HashMap::new(), + } + } +} + +impl ForwardDeps { + pub fn new() -> Self { + Self::default() + } +} + +impl ForwardDeps { + /// Record that `reader` read a value it does not define, so it must rerun + /// when that value's fact rises. + pub fn register_reader(&mut self, key: ValueFactKey, reader: Owner) { + self.value_readers.entry(key).or_default().insert(reader); + } + + /// The owners to reschedule now that a context-qualified value has risen. + pub fn readers_of(&self, key: &ValueFactKey) -> Vec> { + self.value_readers + .get(key) + .map(|readers| readers.iter().cloned().collect()) + .unwrap_or_default() + } +} + +impl SummaryDependencyIndex> for ForwardDeps { + type Error = Infallible; + + fn ensure_owner(&mut self, owner: &Owner) -> Result<(), Self::Error> { + self.summaries.ensure_owner(owner) + } + + fn register( + &mut self, + trigger_owner: &Owner, + dependency: SummaryDependency>, + ) -> Result<(), Self::Error> { + self.summaries.register(trigger_owner, dependency) + } + + fn on_summary_changed( + &mut self, + owner: &Owner, + change: Change, + ) -> Result>, Self::Error> { + self.summaries.on_summary_changed(owner, change) + } +} diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs index a68032dde..9b817300c 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs @@ -26,7 +26,7 @@ use kirin_ir::{Block, CompileStage, DiGraph, Product, SSAValue, Statement}; use crate::core::frame::BlockBinding; use crate::{ CallEffect, Edge, Env, EnvIndex, ForwardDataflowFrameEngine, Frame, FrameEffect, - InterpreterError, SparseForwardEffect, SparseForwardInterp, + InterpreterError, SSABinding, SparseForwardEffect, SparseForwardInterp, }; /// Completion payloads produced by the standard abstract frames. @@ -310,7 +310,7 @@ where /// Reading the yields needs [`Env`] alone, not the whole dataflow surface. fn finish(self, interp: &mut I) -> Result, F>, E> where - I: Env, + I: Env, { let values: Product = self .yields diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index aec5c22ac..12925547e 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -6,15 +6,15 @@ //! wrapper over a [`StandardFixpointInterpreter`] driving a summary-free //! [`SparseForwardTransfer`]: //! -//! - **[`SparseForwardTransfer`]** is the [`Interp`] delegate: pipeline, linker, SSA -//! env, analysis policy, per-function return accumulator, and read/write logging; -//! it provides the dialect-dispatch / IR-query surface ([`StatementDispatch`], -//! [`BlockQueries`], [`CFGQueries`], [`DiGraphQueries`], and — for -//! concrete-shaped callers — [`CallServices`]). +//! - **[`SparseForwardTransfer`]** is the [`Interp`] delegate: pipeline, linker, +//! the [`EnvStore`] container (one environment per analysis context, addressed by +//! the policy's context key), analysis policy, per-function return +//! accumulator, and read/write logging; it provides the dialect-dispatch / +//! IR-query surface ([`StatementDispatch`], [`BlockQueries`], [`CFGQueries`], +//! [`DiGraphQueries`], and — for concrete-shaped callers — [`CallServices`]). //! - the **[`StandardFixpointInterpreter`]** driver owns the summaries, the -//! dependency graph ([`ForwardSummaryDeps`]), the owner worklist, and the -//! owner-local [`ForwardStore`] (shared envs + context-qualified value-reader -//! deps). +//! forward dependency bookkeeping ([`ForwardDeps`] — callee-summary edges plus +//! context-qualified value-reader edges), and the owner worklist. //! //! # Owner kinds //! @@ -44,13 +44,15 @@ use crate::core::{linker::link_and_discover_callable, query}; use crate::{ AbstractBlockFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractInterpreter, BlockQueries, Body, CFGQueries, CallEffect, CallServices, Callee, DiGraphQueries, Env, - EnvIndex, EnvStackStore, FixpointProfile, ForwardDataflowFrameEngine, ForwardEval, - ForwardSummaryDeps, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, - LinkTarget, Linker, OwnerSemantics, ResolvedCallable, SameStageLinker, SparseForwardEffect, - SparseForwardSemantic, StageQuery, StandardAbstractFrame, StandardFixpointInterpreter, - StatementDispatch, Store, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, + EnvIndex, EnvStore, FixpointProfile, ForwardDataflowFrameEngine, ForwardEval, Frame, Interp, + InterpDispatch, InterpLocation, InterpreterError, LinkTarget, Linker, OwnerSemantics, + ResolvedCallable, SSABinding, SameStageLinker, SparseForwardEffect, SparseForwardSemantic, + StageQuery, StandardAbstractFrame, StandardFixpointInterpreter, StatementDispatch, Summary, + SummaryDependency, SummaryDependencyIndex, SummaryEffect, }; +use super::deps::{ForwardDeps, ValueFactKey}; + // =========================================================================== // Pluggable analysis seams (policy `P`) // =========================================================================== @@ -87,11 +89,18 @@ impl Default for ContextInsensitive { } } +/// One summary per resolved [`LinkTarget`], shared by every call site of that +/// target. +/// +/// The target *is* the context-insensitive key: it already identifies the stage +/// and specialization a call resolved to, so re-deriving a tuple from it would +/// only be a second spelling of the same identity. Context sensitivity is what +/// *adds* to this key (see `ConstPropContext`), never what re-spells it. impl CallContext for ContextInsensitive { - type Key = (CompileStage, SpecializedFunction); + type Key = LinkTarget; fn key(&mut self, target: &LinkTarget, _args: &Product) -> Self::Key { - (target.stage, target.specialization) + *target } } @@ -247,56 +256,6 @@ impl Summary for ForwardSummary { } } -/// Context-qualified key for value-reader dependencies: the same [`SSAValue`] under -/// two different function contexts is two distinct facts, so readers never -/// cross-contaminate. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ValueFactKey { - pub function: K, - pub value: SSAValue, -} - -/// Owner-local analysis state carried in the driver's `store`: one shared env per -/// function context (so direct dominated cross-block uses resolve), plus the -/// context-qualified value-reader dependency index. **Not** part of the public -/// function-summary surface. -pub struct ForwardStore { - envs: HashMap, - value_readers: HashMap, HashSet>>, - _marker: PhantomData V>, -} - -impl ForwardStore { - fn new() -> Self { - Self { - envs: HashMap::new(), - value_readers: HashMap::new(), - _marker: PhantomData, - } - } -} - -impl ForwardStore { - fn env(&self, function: &K) -> Option { - self.envs.get(function).copied() - } - - fn set_env(&mut self, function: K, index: EnvIndex) { - self.envs.insert(function, index); - } - - fn register_reader(&mut self, key: ValueFactKey, reader: Owner) { - self.value_readers.entry(key).or_default().insert(reader); - } - - fn readers_of(&self, key: &ValueFactKey) -> Vec> { - self.value_readers - .get(key) - .map(|readers| readers.iter().cloned().collect()) - .unwrap_or_default() - } -} - /// A single mutation the driver applies through [`apply_update`](ForwardDriver::apply_update). enum ForwardUpdate { /// Merge call args into a function context's entry (widen by visits); on rise, @@ -345,14 +304,16 @@ where type Completion = AbstractCompletion; } -/// The forward driver: a [`StandardFixpointInterpreter`] over [`SparseForwardTransfer`] -/// with owner summaries, forward dependencies, and the owner-local -/// [`ForwardStore`]. +/// The forward driver: a [`StandardFixpointInterpreter`] over +/// [`SparseForwardTransfer`] with owner summaries and [`ForwardDeps`]. +/// +/// It needs no side store: environments live in the transfer's [`EnvStore`] +/// container, and both dependency kinds live in the dependency index. type ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> = StandardFixpointInterpreter< SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem>, SparseForwardProfile>::Key, F>, - ForwardStore<

>::Key, V>, - ForwardSummaryDeps>::Key>>, + (), + ForwardDeps<

>::Key>, >; // =========================================================================== @@ -375,7 +336,10 @@ pub struct SparseForwardTransfer< { pipeline: &'ir Pipeline, linker: Lk, - store: EnvStackStore, + /// One environment per analysis context, addressed by the policy's context + /// key. Blocks of the same context share it (so direct dominated cross-block + /// uses resolve); distinct contexts are isolated. + env: EnvStore<

>::Key, SSAValue, V>, analysis: P, max_iterations: usize, location: Option, @@ -401,7 +365,7 @@ where Self { pipeline, linker: SameStageLinker, - store: EnvStackStore::new(), + env: EnvStore::new(), analysis: P::default(), max_iterations: 1000, location: None, @@ -423,7 +387,7 @@ where SparseForwardTransfer { pipeline: self.pipeline, linker, - store: self.store, + env: self.env, analysis: self.analysis, max_iterations: self.max_iterations, location: self.location, @@ -455,7 +419,7 @@ where SparseForwardTransfer { pipeline: self.pipeline, linker: self.linker, - store: EnvStackStore::new(), + env: EnvStore::new(), analysis, max_iterations: self.max_iterations, location: None, @@ -475,6 +439,22 @@ where self.pipeline } + /// The environment shared by every owner of analysis context `key`, + /// allocating and registering it on first use. + /// + /// Keyed allocation is *this engine's* business: the analysis policy chooses + /// the context key, so no shared engine surface exposes this method. + /// [`CallServices`] offers only the unkeyed `alloc_env`/`free_env`, and + /// [`Env`] does not deal in activation lifetime at all. + fn context_env(&mut self, key:

>::Key) -> EnvIndex { + self.env.get_or_allocate(key) + } + + /// The environment of an already-seeded context, if it has one. + fn lookup_context_env(&self, key: &

>::Key) -> Option { + self.env.context_env(key) + } + /// Begin logging reads/writes for a block-owner walk. fn begin_block_log(&mut self) { self.logging = true; @@ -570,6 +550,9 @@ where P: CallContext, Sem: SparseForwardSemantic, { + /// The sparse-forward shape anchors its facts on SSA values. + type Anchor = SSAValue; + fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { // Log the read regardless of whether it resolves to a bound value or // bottom — an unbound read of a value defined elsewhere is exactly the @@ -577,18 +560,21 @@ where if self.logging { self.read_log.borrow_mut().push(value); } - match self.store.read(index, value) { - Ok(value) => Ok(value), - Err(InterpreterError::UnboundValue { .. }) => Ok(V::bottom()), - Err(error) => Err(E::from(error)), - } + // An absent binding is bottom, not an error: this analysis may reach a + // use before the definition's owner has run. An invalid environment + // handle is still an error. + Ok(self + .env + .read(index, value) + .map_err(E::from)? + .unwrap_or_else(V::bottom)) } fn env_write(&mut self, index: EnvIndex, value: SSAValue, data: V) -> Result<(), E> { if self.logging { self.write_log.push(value); } - self.store.write(index, value, data).map_err(E::from) + self.env.write(index, value, data).map_err(E::from) } } @@ -619,11 +605,11 @@ where Sem: SparseForwardSemantic, { fn alloc_env(&mut self) -> EnvIndex { - self.store.alloc() + self.env.alloc() } fn free_env(&mut self, index: EnvIndex) -> Result<(), E> { - self.store.free(index).map_err(E::from) + self.env.free(index).map_err(E::from) } fn resolve_callable( @@ -1084,7 +1070,7 @@ where } } for value in risen { - let readers = self.store().readers_of(&ValueFactKey { + let readers = self.dependency_index().readers_of(&ValueFactKey { function: function.clone(), value, }); @@ -1097,8 +1083,9 @@ where } } - /// Resolve the executable entry owner of `key`'s function (allocating its - /// shared env on first use) and seed it with the entry arguments. + /// Resolve the executable entry owner of `key`'s function (allocating the + /// context's shared environment on first use) and seed it with the entry + /// arguments. /// /// This is the one place a *function* becomes runnable *work*: it translates /// the callable [`Body`] into the executable [`Owner`] the worklist can @@ -1111,10 +1098,7 @@ where stage: CompileStage, body: Body, ) -> Result<(), E> { - if self.store().env(key).is_none() { - let env = self.alloc_env(); - self.store_mut().set_env(key.clone(), env); - } + self.inner_mut().context_env(key.clone()); let entry_args = self .summary(&Owner::Function(key.clone())) .and_then(|info| info.as_function()) @@ -1248,11 +1232,14 @@ where "block owner's function is unseeded", )) })?; - let env = interp.store().env(&function).ok_or_else(|| { - E::from(InterpreterError::Custom( - "block owner's function has no shared env", - )) - })?; + let env = interp + .inner() + .lookup_context_env(&function) + .ok_or_else(|| { + E::from(InterpreterError::Custom( + "block owner's function has no shared env", + )) + })?; interp.inner_mut().begin_block_log(); match owner { Owner::Block { block, .. } => { @@ -1300,18 +1287,21 @@ where } let (reads, writes) = interp.inner_mut().take_logs(); - let env = interp.store().env(&function).ok_or_else(|| { - E::from(InterpreterError::Custom( - "block owner's function has no shared env", - )) - })?; + let env = interp + .inner() + .lookup_context_env(&function) + .ok_or_else(|| { + E::from(InterpreterError::Custom( + "block owner's function has no shared env", + )) + })?; // Register external direct reads (values read but not written locally) as // context-qualified value-reader deps on this block owner. let written: HashSet = writes.iter().copied().collect(); for value in reads { if !written.contains(&value) { - interp.store_mut().register_reader( + interp.dependency_index_mut().register_reader( ValueFactKey { function: function.clone(), value, @@ -1403,9 +1393,9 @@ where Self { driver: StandardFixpointInterpreter::with_dependency_index( SparseForwardTransfer::new(pipeline), - ForwardStore::new(), (), - ForwardSummaryDeps::new(), + (), + ForwardDeps::new(), ), } } @@ -1428,9 +1418,9 @@ where SparseForwardInterpreter { driver: StandardFixpointInterpreter::with_dependency_index( transfer, - ForwardStore::new(), (), - ForwardSummaryDeps::new(), + (), + ForwardDeps::new(), ), } } @@ -1458,9 +1448,9 @@ where SparseForwardInterpreter { driver: StandardFixpointInterpreter::with_dependency_index( transfer, - ForwardStore::new(), (), - ForwardSummaryDeps::new(), + (), + ForwardDeps::new(), ), } } @@ -1473,9 +1463,9 @@ where Self { driver: StandardFixpointInterpreter::with_dependency_index( transfer, - ForwardStore::new(), (), - ForwardSummaryDeps::new(), + (), + ForwardDeps::new(), ), } } @@ -1506,8 +1496,12 @@ where stage: CompileStage, function: SpecializedFunction, ) -> Option<&Product> { + let target = LinkTarget { + stage, + specialization: function, + }; self.driver - .summary(&Owner::Function((stage, function))) + .summary(&Owner::Function(target)) .and_then(|info| info.as_function()) .and_then(|function| function.ret.as_ref()) } diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs b/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs index 4320833d5..cf3225212 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs @@ -2,6 +2,7 @@ //! constant propagation, interval analysis — the value domain, not the key, //! distinguishes them. +pub(crate) mod deps; pub(crate) mod frames; pub(crate) mod interp; diff --git a/crates/kirin-interpreter/src/facts/store.rs b/crates/kirin-interpreter/src/facts/store.rs index e7699735d..eef9e3a30 100644 --- a/crates/kirin-interpreter/src/facts/store.rs +++ b/crates/kirin-interpreter/src/facts/store.rs @@ -1,47 +1,46 @@ -//! Anchor-keyed dataflow fact stores. +//! Shared anchor-keyed storage for interpreter values and analysis facts. //! -//! [`FactStore`] is the reusable fact container: one fact `F` per -//! [`LatticeAnchor`] `A`, with absent anchors carrying the analysis's bottom -//! fact. It is deliberately separate from the operational activation store -//! ([`EnvIndex`](crate::EnvIndex) / [`EnvStackStore`](crate::EnvStackStore)): -//! that is the CESK-style runtime/abstract-execution environment handle, while -//! this is where analyses keep dataflow facts. The familiar stores are -//! instantiations picked by the analysis's anchor: sparse analyses anchor -//! facts to SSA values ([`SparseStore`], scope-qualified as -//! [`ScopedSparseStore`]), while dense analyses use -//! `FactStore, F>` directly. +//! [`FactStore`] holds one payload `F` per hashable anchor `A`. +//! [`EnvStore`](crate::EnvStore) allocates one of these per environment, addressed by +//! analysis context, for concrete and forward abstract interpretation. Backward +//! analyses use the map directly with SSA or program-point anchors, qualified by +//! scope where needed. Engines decide what absence means and when facts must +//! join. use std::collections::HashMap; +use std::hash::Hash; use kirin_ir::SSAValue; -use super::anchor::{Change, LatticeAnchor, Scoped}; +use super::anchor::{Change, Scoped}; -/// One dataflow fact per lattice anchor. +/// One interpreter value or analysis fact per anchor. /// -/// Anchors absent from the store carry the analysis's bottom fact. +/// Missing anchors return `None`. Concrete engines report unbound values; +/// abstract engines can interpret absence as bottom. Assignment never joins +/// implicitly; [`join_with`](Self::join_with) takes an explicit bottom and merge. #[derive(Clone, Debug)] pub struct FactStore where - A: LatticeAnchor, + A: Eq + Hash, { facts: HashMap, } -impl Default for FactStore { +impl Default for FactStore { fn default() -> Self { Self::new() } } -impl FactStore { +impl FactStore { pub fn new() -> Self { Self { facts: HashMap::new(), } } - /// Read the fact stored at `anchor`, or `None` if it carries bottom. + /// Read the fact stored at `anchor`, or `None` if absent. pub fn get(&self, anchor: A) -> Option<&F> { self.facts.get(&anchor) } @@ -51,12 +50,12 @@ impl FactStore { self.facts.insert(anchor, fact); } - /// `true` if a (non-bottom) fact is stored at `anchor`. + /// `true` if a fact is explicitly stored at `anchor`, including bottom. pub fn contains(&self, anchor: A) -> bool { self.facts.contains_key(&anchor) } - /// Number of anchors carrying a non-bottom fact. + /// Number of explicitly stored facts. pub fn len(&self) -> usize { self.facts.len() } @@ -65,16 +64,18 @@ impl FactStore { self.facts.is_empty() } - /// Iterate `(anchor, fact)` pairs for anchors carrying a non-bottom fact - /// (anchors cloned; order unspecified). - pub fn iter(&self) -> impl Iterator { + /// Iterate stored `(anchor, fact)` pairs (anchors cloned; order unspecified). + pub fn iter(&self) -> impl Iterator + where + A: Clone, + { self.facts .iter() .map(|(anchor, fact)| (anchor.clone(), fact)) } } -impl FactStore { +impl FactStore { /// Join `incoming` into the fact at `anchor` using `merge`, reporting /// whether the stored fact changed. `bottom` supplies the implicit fact /// for an absent anchor. diff --git a/crates/kirin-interpreter/src/fixpoint/delegates.rs b/crates/kirin-interpreter/src/fixpoint/delegates.rs index c9de9677e..84a33cd41 100644 --- a/crates/kirin-interpreter/src/fixpoint/delegates.rs +++ b/crates/kirin-interpreter/src/fixpoint/delegates.rs @@ -9,7 +9,7 @@ //! blanket [`SparseForwardInterp`](crate::SparseForwardInterp) impl and its //! `read`/`write` helpers. -use kirin_ir::{CompileStage, SSAValue, Statement}; +use kirin_ir::{CompileStage, Statement}; use crate::{Env, EnvIndex, Interp}; @@ -43,16 +43,18 @@ where I: Env, P: FixpointProfile, { - fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { - self.inner.env_read(index, value) + type Anchor = I::Anchor; + + fn env_read(&self, index: EnvIndex, anchor: I::Anchor) -> Result { + self.inner.env_read(index, anchor) } fn env_write( &mut self, index: EnvIndex, - value: SSAValue, + anchor: I::Anchor, data: Self::Value, ) -> Result<(), Self::Error> { - self.inner.env_write(index, value, data) + self.inner.env_write(index, anchor, data) } } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 1430cca83..861bde263 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -64,39 +64,24 @@ mod facts; mod fixpoint; mod semantics; -// The shared chassis: engine trait + dialect dispatch, effect types, -// activation storage, calling conventions, errors, and IR queries. pub use self::core::{ - AbstractInterpreter, Env, GraphWalkPlan, Interp, InterpLocation, SparseForwardInterp, + AbstractInterpreter, GraphWalkPlan, Interp, InterpLocation, SparseForwardInterp, +}; +pub use self::core::{ + BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, }; pub use self::core::{BranchCondition, HasProductValue, expect_single}; pub use self::core::{CallEffect, Callee, Edge, SparseForwardEffect}; pub use self::core::{CrossStageLinker, LinkTarget, Linker, ResolvedCallable, SameStageLinker}; -pub use self::core::{EnvIndex, EnvStackStore, Store}; +pub use self::core::{Env, EnvIndex, EnvStore, SSABinding}; pub use self::core::{InterpDispatch, Interpretable}; pub use self::core::{InterpreterError, StageQuery, TerminatorArgs}; -pub use kirin_ir::Body; -// The shared, direction-neutral frame protocol: `Frame`/`FrameEffect`/ -// `drive_frames` (the frame-stack driver loop) anchored on `FrameEngine`, the -// minimal engine contract. On top of it, the forward engine capabilities a frame -// can require: one narrowly scoped component trait per kind of traversal -// (`StatementDispatch`, `BlockQueries`, `CFGQueries`, `DiGraphQueries`, -// `CallServices`), so a member frame bounds only what it consumes, plus two -// whole-universe umbrellas — `ForwardFrameEngine` (full standard concrete -// surface) and `ForwardDataflowFrameEngine` (standard forward-abstract surface). -pub use self::core::{ - BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, - ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, -}; - -// Concrete execution engine + the concrete standard frames: the -// representation walkers (`BlockFrame`/`CFGFrame`/`DiGraphFrame` — `UnGraph` -// traversal is a dialect/compiler call-body traversal) and the `CallFrame` -// call boundary. pub use engines::concrete::{ BlockFrame, BodyFrameEntry, CFGFrame, CallBodyTraversal, CallFrame, CallRequest, Completion, ConcreteInterpreter, ConcreteInterpreterCore, DefaultCallBodyTraversal, DiGraphFrame, }; +pub use kirin_ir::Body; // Sparse forward engine (`Sem = ForwardEval`) + the abstract standard frames. pub use engines::sparse_forward::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, CallContext, @@ -174,8 +159,8 @@ pub mod engine { DenseBackwardInterp, DenseBackwardInterpreter, DenseBackwardState, DenseBlockFrame, DiGraphFrame, DiGraphQueries, Env, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, FrameEffect, FrameEngine, Interp, InterpDispatch, InterpreterError, LinkTarget, Linker, - ResolvedCallable, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, - SparseForwardInterp, SparseForwardInterpreter, StandardAbstractFrame, StatementDispatch, - WideningStrategy, drive_frames, expect_single, + ResolvedCallable, SSABinding, SameStageLinker, SparseBackwardInterp, + SparseBackwardInterpreter, SparseForwardInterp, SparseForwardInterpreter, + StandardAbstractFrame, StatementDispatch, WideningStrategy, drive_frames, expect_single, }; } diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index 47b3784f7..be7d19c9c 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -844,10 +844,11 @@ where } /// `scf.for` reads the loop bound/step out of the activation it was given and -/// otherwise only pushes a [`BlockFrame`] — so [`Env`] is its whole requirement. +/// otherwise only pushes a [`BlockFrame`] — so an SSA-anchored [`Env`] is its +/// whole requirement. impl Frame for ScfForFrame where - I: Env, + I: Env, F: From>, V: Clone + ForLoopValue, E: From, diff --git a/docs/design/formalism/index.md b/docs/design/formalism/index.md index 9f62962ec..5c67424d8 100644 --- a/docs/design/formalism/index.md +++ b/docs/design/formalism/index.md @@ -19,7 +19,7 @@ generic over `I: Interp`; the concrete/abstract distinction is carried by We use: - `P`: immutable pipeline IR (`Pipeline`) -- `σ`: dynamic SSA store/environment state (`EnvStackStore`) +- `σ`: dynamic SSA store/environment state (`EnvStore`) - `κ`: explicit continuation as frame stack (scope + call frames) - `ρ`: active environment capability (`EnvIndex`) - `ι`: instantiated interpreter/engine value (`I: Interp`) diff --git a/docs/design/formalism/state-environment-model.md b/docs/design/formalism/state-environment-model.md index 68b79eccb..fed1a884b 100644 --- a/docs/design/formalism/state-environment-model.md +++ b/docs/design/formalism/state-environment-model.md @@ -3,25 +3,28 @@ > Part of the [Rust Interpreter Formalism](index.md). This part uses both shorthand (`σ`, `ρ`) and direct API names (`EnvIndex`, -`EnvStackStore`, `Interp`) to keep proofs and implementation traces aligned. +`EnvStore`, `Env`, `Interp`) to keep proofs and implementation traces +aligned. ## Reading Recipe - **Formal read:** Read this as the state transformer substrate for `⟨s, ρ, σ⟩ ⇓_ι ...`, with `σ` and `ρ` defining where values live and how they evolve. -- **API read:** Inspect `crates/kirin-interpreter/src/{interp.rs,env.rs}` first, then concrete/forward-abstract `Interp` impls in `crates/kirin-interpreter/src/{concrete_interp.rs,forward_abstract_interp.rs}` for `env_read/env_write` behavior. +- **API read:** Inspect `crates/kirin-interpreter/src/core/{interp.rs,env/}` first, then the concrete/forward-abstract `Env` impls in `crates/kirin-interpreter/src/engines/{concrete,sparse_forward}/interp.rs` for `env_read/env_write` behavior. ## II.0 Symbol-to-code mapping | Formal symbol / concept | Rust type / function | Code | | --- | --- | --- | -| Interpreter interface | `Interp` | [`crates/kirin-interpreter/src/interp.rs`](../../../crates/kirin-interpreter/src/interp.rs) | -| Statement location | `InterpLocation` | [`crates/kirin-interpreter/src/interp.rs`](../../../crates/kirin-interpreter/src/interp.rs) | -| Forward eval helpers | `SparseForwardInterp` | [`crates/kirin-interpreter/src/interp.rs`](../../../crates/kirin-interpreter/src/interp.rs) | -| Environment capability | `EnvIndex` | [`crates/kirin-interpreter/src/env.rs`](../../../crates/kirin-interpreter/src/env.rs) | -| Environment trait | `Env` | [`crates/kirin-interpreter/src/env.rs`](../../../crates/kirin-interpreter/src/env.rs) | -| Concrete store | `EnvStackStore` | [`crates/kirin-interpreter/src/env.rs`](../../../crates/kirin-interpreter/src/env.rs) | -| Concrete `env_read` semantics | `ConcreteInterpreter` impl of `Env` | [`crates/kirin-interpreter/src/concrete_interp.rs`](../../../crates/kirin-interpreter/src/concrete_interp.rs) | -| Forward abstract `env_read` semantics | `SparseForwardInterpreter` impl of `Env` | [`crates/kirin-interpreter/src/forward_abstract_interp.rs`](../../../crates/kirin-interpreter/src/forward_abstract_interp.rs) | +| Interpreter interface | `Interp` | [`core/interp.rs`](../../../crates/kirin-interpreter/src/core/interp.rs) | +| Statement location | `InterpLocation` | [`core/interp.rs`](../../../crates/kirin-interpreter/src/core/interp.rs) | +| Forward eval helpers | `SparseForwardInterp` | [`core/interp.rs`](../../../crates/kirin-interpreter/src/core/interp.rs) | +| Environment capability | `EnvIndex` | [`core/env/store.rs`](../../../crates/kirin-interpreter/src/core/env/store.rs) | +| Environment access trait | `Env` | [`core/env/services.rs`](../../../crates/kirin-interpreter/src/core/env/services.rs) | +| Activation lifetime | `CallServices` (`alloc_env`/`free_env`; a sibling of `Env`) | [`core/frame.rs`](../../../crates/kirin-interpreter/src/core/frame.rs) | +| Environment container | `EnvStore` | [`core/env/store.rs`](../../../crates/kirin-interpreter/src/core/env/store.rs) | +| Shared fact map | `FactStore` | [`facts/store.rs`](../../../crates/kirin-interpreter/src/facts/store.rs) | +| Concrete `env_read` semantics | `ConcreteInterpreterCore` impl of `Env` | [`engines/concrete/interp.rs`](../../../crates/kirin-interpreter/src/engines/concrete/interp.rs) | +| Forward abstract `env_read` semantics | `SparseForwardTransfer` impl of `Env` | [`engines/sparse_forward/interp.rs`](../../../crates/kirin-interpreter/src/engines/sparse_forward/interp.rs) | | Structured scope carrier | `Scope`, `ScopeBody`, `ScopeHook`, `ScopeStep` | [`crates/kirin-interpreter/src/effect.rs`](../../../crates/kirin-interpreter/src/effect.rs) | | Value tuple packet | `Product` | [`crates/kirin-ir/src/product.rs`](../../../crates/kirin-ir/src/product.rs) | @@ -52,32 +55,43 @@ the current `(stage, statement, env)` as an `InterpLocation` and exposes: - `interp.write_results(results, product)` So the formal transition `σ -> σ'` for a statement is realized operationally by -mutations performed through `SparseForwardInterp` helpers over `Env::env_write`; -it is not a separate explicit return value from `interpret`. +mutations performed through `SparseForwardInterp` helpers over +`Env::env_write`; it is not a separate explicit return value from +`interpret`. API-level correspondence: - `ρ` corresponds to `interp.index()` / `EnvIndex` -- `σ` corresponds to engine-owned `EnvStackStore` +- `σ` corresponds to the engine-owned `EnvStore` container - `σ[ρ, x] = v` corresponds to `interp.write(x, v)` or `env_write(ρ, x, v)` -## II.2 Environment Store +## II.2 Environment Container -Concrete storage uses `EnvStackStore`: +Concrete and forward abstract storage use `EnvStore`: -- `stores: Vec>>` -- `EnvIndex` is a capability (index into `stores`) -- `alloc` adds a new live record -- `free` retires a record (`None`) -- `read`/`write` are per-record SSA accesses +- `context_indices: HashMap` — which analysis context an environment belongs to +- `environments: Vec>>` — each holding one `FactStore` +- `EnvIndex` is a capability (index into `environments`), never reused after `free` +- `alloc` adds a new live record with no context association +- `get_or_allocate(k)` returns `k`'s live record, adding one on first use +- `free` retires a record and drops its context association +- `read`/`write` are per-record anchor accesses; `environment` inspects a record's fact map + +The container maps context identity to storage and nothing else: the *analysis* +chooses `K` (see `CallContext`), `write` assigns rather than joins, and `read` +reports an absent anchor as absent, leaving absence semantics, context selection, +and convergence to the engine above it. Concrete execution instantiates +`K = Infallible`, so it can only `alloc`. Backward analyses use `FactStore` +directly with their existing scoped anchors. Formal view: -- `σ : EnvIndex -> (SSAValue -> V)` over live indices +- `σ : EnvIndex -> (A -> V)` over live indices, plus `δ : K -> EnvIndex` over live contexts - `alloc(σ) = (ρ, σ[ρ <- empty])` -- `free(σ, ρ)` removes liveness for `ρ` +- `getOrAlloc(σ, δ, k) = (δ(k), σ, δ)` if `k ∈ dom δ`, else `(ρ, σ[ρ <- empty], δ[k <- ρ])` +- `free(σ, δ, ρ)` removes liveness for `ρ` and drops `k` with `δ(k) = ρ` - `write(σ, ρ, x, v)` updates `x` in record `ρ` -- `read(σ, ρ, x)` fetches bound value (or error if unbound/invalid in concrete) +- `read(σ, ρ, x)` fetches the stored value, or reports absence (the engine decides: error in concrete, `⊥` in abstract) ## II.3 Concrete vs Abstract Read Semantics diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 3161237fa..7d4c46c5f 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -52,11 +52,22 @@ pub trait Interp: Sized { // the engine-side driver — ANALYSIS-A fn index(&self) -> EnvIndex; // (the SSA activation) } -// SSA environment access used by forward engines. +// The engine's capability for *using* an environment. `EnvStore` is the +// storage container underneath; activation lifetime is on `CallServices`. +// Anchor-generic: `SSAValue` for the sparse shapes, `ProgramPoint` for the dense +// ones. The access interface names no anchor family. pub trait Env: Interp { - fn env_read(..) -> Result; - fn env_write(..) -> Result<(), Self::Error>; + type Anchor: LatticeAnchor; + fn env_read(&self, EnvIndex, Self::Anchor) -> Result; + fn env_write(&mut self, EnvIndex, Self::Anchor, Self::Value) -> Result<(), Self::Error>; } + +// SSA-shaped positional binding, split out and blanket-implemented so `Env` +// itself stays anchor-generic and no dense engine is asked for it. +pub trait SSABinding: Env { + fn bind_values(..) -> Result<(), Self::Error>; // default, writes via env_write +} +impl> SSABinding for T {} ``` ```rust @@ -157,8 +168,9 @@ both execution and analysis**: `kirin-arith`'s `Add` rule computes `3 + 5` under `ConcreteInterpreter<.., i64, ..>` and folds `Const(3) + Const(5)` under constant propagation, with no analysis-specific code in the dialect. -`SparseForwardInterp` is the **forward engine** trait: it requires `Env` and -`Semantics = ForwardEval`, and exposes the SSA read/write helpers as **default +`SparseForwardInterp` is the **forward engine** trait: it requires +`Env` (the sparse-forward shape *is* SSA-anchored) and a +`SparseForwardSemantic` key, and exposes the SSA read/write helpers as **default methods**, hiding environment indices and locations: `interp.read(ssa)`, `interp.write(result, value)`, `interp.read_many(&values)`, `interp.write_results(&results, product)`. They delegate to the engine's [`Env`] @@ -438,8 +450,8 @@ composed them. See [Custom traversal and policies](#custom-traversal-and-policie The **forward dataflow** engine — a lattice-based forward abstract interpreter, and one *specialization* of the shared framework in the forward direction (it sets -`Effect = SparseForwardEffect` and `Semantics = ForwardEval`, stores SSA activations via -`Env`, and drives forward frames). The name +`Effect = SparseForwardEffect` and `Semantics = ForwardEval`, stores SSA activations in +an `EnvStore` container, and drives forward frames). The name `AbstractInterpreter` is reserved for the shared trait implemented by lattice-valued abstract engines. `SparseForwardInterpreter` is the forward engine; `SparseBackwardInterpreter` (per-SSA demand / strong liveness) and @@ -470,7 +482,7 @@ frames: until stable — `scf.for` loops converge. The fixpoint is the dialect frame's, using the engine's `analysis_merge`. - **Functions**: each resolved call target is summarized under a key chosen by - the `CallContext` strategy (`ContextInsensitive` → `(stage, specialization)`), with an + the `CallContext` strategy (`ContextInsensitive` → the resolved `LinkTarget`), with an entry/return `Product` summary. Calls join arguments into the callee's entry (enqueueing it on change) and read its current return summary (`bottom` until it converges); return-summary changes re-enqueue recorded @@ -549,33 +561,65 @@ surface still runs the frames it can support. | trait | capability | required by | |---|---|---| | `StatementDispatch: Interp` | `run_statement` — dispatch to the dialect rule | every executing frame | +| `Env: Interp` | `env_read`/`env_write` at `Env::Anchor` (`core/env/services.rs`) | every frame that touches storage | +| `SSABinding: Env` | `bind_values` (blanket-implemented, `core/env/services.rs`) | `CallFrame`, the block/graph walkers | | `BlockQueries: Interp` | `block_params`/`first_statement`/`next_statement` | `BlockCursor`, `BlockFrame`, `AbstractBlockFrame`, dialect block walkers | | `CFGQueries: BlockQueries` | `cfg_entry` | `CFGFrame` | | `DiGraphQueries: Interp` | `digraph_walk_plan` (default: `NoDefaultWalker`) | `DiGraphFrame`, `AbstractDiGraphFrame` | -| `CallServices: Env` | `alloc_env`/`free_env`/`resolve_callable` | `CallFrame` | +| `CallServices: Interp` | `alloc_env`/`free_env`/`resolve_callable` | `CallFrame`, *together with* `Env` | **The `*Queries` traits are read-only, and only require `Interp`** — so nothing on them can touch SSA storage, and their names cannot hide a store mutation. The one operation that needs both a query and a write, binding a block's parameters to incoming actuals, lives on the crate-private `BlockBinding` extension -(bounded `Env + BlockQueries`) instead. A frame that binds a block entry +(bounded `SSABinding + BlockQueries`) instead. A frame that binds a block entry therefore spells that requirement out: `BlockCursor::bind_entry` and -`::enter_block` take `Env + BlockQueries`, while `::advance` takes `BlockQueries` -alone and `::write_child_results` takes `Env` alone. +`::enter_block` take `Env + BlockQueries`, while `::advance` +takes `BlockQueries` alone and `::write_child_results` takes +`Env` alone. + +`Env` is *using* an environment: one read and one write, at whichever +`Env::Anchor` family the engine attaches facts to. Positional binding into SSA +slots is only meaningful for an SSA-anchored engine, so it lives on the +blanket-implemented `SSABinding` rather than narrowing `Env` for everyone — the +same read/write vocabulary then serves a dense, `ProgramPoint`-anchored engine +unchanged (`PointAnchoredEngine` in `tests/frame_engine_capabilities.rs` is that +assertion). Binding still goes through `env_write`, so an engine's logging and +absence policy apply to bound values too. `Env` deliberately stops there. Choosing +a *context key* is analysis policy, so `EnvStore::get_or_allocate` stays internal to +the engine that has a policy; and *creating or retiring* an activation is the +call boundary's business, so `alloc_env`/`free_env` are on `CallServices`. + +**The two are independent siblings on `Interp`** — neither is a supertrait of +the other — because they answer different questions: "what does an access to +this activation mean?" versus "where do activations come from, and whose body am +I entering?". A frame consuming both names both: -`CallServices` names *services*, not a convention: **`CallFrame` still owns the -calling convention** — the operation order, which completions are legal, and -freeing the activation exactly once — and this trait only supplies the -primitives. The public `CallServices::resolve_callable` method exposes +```rust +impl Frame for CallFrame +where I: CallServices + Env, .. +``` + +That keeps the bounds honest in both directions. A frame that only reads and +writes an activation — `ScfForFrame`, `BlockCursor::write_child_results` — names +`Env` and claims no lifecycle it never exercises. An engine that can +create and retire activations owes no value-access policy for doing so. And the +forward abstract engine gets storage access without a call convention it never +performs — `AbstractOnlyEngine` in `tests/frame_engine_capabilities.rs` +implements `Env` with no `CallServices` at all. + +Within `CallServices` the lifetime pair is deliberately **not** split further: +the standard `CallFrame` consumes `alloc_env`/`free_env` together, and their +pairing is a safety property (an `alloc_env` without its `free_env` leaks; a +second `free_env` double-frees), so no engine should be able to offer half a +lifecycle. The public `CallServices::resolve_callable` method exposes linker-plus-target-stage body discovery using the engine's configured pipeline and linker; it carries no value product. The built-in engines share the crate-private `link_and_discover_callable` helper for root entry and nested -calls. Compiler authors configure resolution policy through `.with_linker(...)`. -The trait is deliberately -**not** split further: the standard `CallFrame` consumes all three services -together, and their pairing is a safety property (an -`alloc_env` without its `free_env` leaks; a second `free_env` double-frees), so -no engine should be able to offer half a call convention. +calls, and compiler authors configure resolution policy through +`.with_linker(...)`. None of this makes the trait a convention: **`CallFrame` +still owns the calling convention** — the operation order, which completions are +legal, and freeing the activation exactly once. `StatementDispatch` and `InterpDispatch` face opposite directions and are easy to confuse. `InterpDispatch` is implemented by a **stage/language** to route a @@ -589,16 +633,17 @@ engine must support the union of all admitted member continuations, while each member itself should name only the component capabilities it consumes: ```rust -// Full concrete surface. Adds no methods; blanket-implemented. +// Full concrete surface. Adds no methods; blanket-implemented. `Env` +// is listed explicitly: `CallServices` does not imply it. pub trait ForwardFrameEngine: - StatementDispatch + CFGQueries + DiGraphQueries + CallServices {} + StatementDispatch + Env + CFGQueries + DiGraphQueries + CallServices {} impl ForwardFrameEngine for T -where T: StatementDispatch + CFGQueries + DiGraphQueries + CallServices {} +where T: StatementDispatch + Env + CFGQueries + DiGraphQueries + CallServices {} // Abstract dataflow: the traversal it *shares*, plus merge/summarization. // Notably NOT CallServices, and NOT CFGQueries. pub trait ForwardDataflowFrameEngine: - Env + StatementDispatch + BlockQueries + DiGraphQueries + Env + StatementDispatch + BlockQueries + DiGraphQueries { type SummaryKey: Clone + Eq + Hash; fn analysis_merge(..); fn contribute_return(..); fn current_function_key(..); @@ -612,12 +657,11 @@ That follows the semantics: forward abstract interpretation *summarizes* a call reaches a callable body's entry block through `Owner` seeding in the fixpoint driver rather than `cfg_entry`. Requiring its frame universe to expose `alloc_env`, `free_env`, `resolve_callable`, and `cfg_entry` was demanding a call -convention it never performs. `tests/frame_engine_capabilities.rs` pins this -down with deliberately incomplete mock engines whose ability to compile *is* the -regression test. +convention it never performs. `tests/frame_engine_capabilities.rs` pins this down with deliberately +incomplete mock engines whose ability to compile *is* the regression test. Binding values into an **explicitly selected** activation is -`Env::bind_values(index, slots, values)`, not a method on any umbrella, so it is +`SSABinding::bind_values(index, slots, values)`, not a method on any umbrella, so it is no longer confusable with `SparseForwardInterp::write_results` (the dialect-facing helper, which binds into the engine's *current* activation, `interp.index()`). The two differ by *which activation*, not by what they do — @@ -653,8 +697,10 @@ pub trait StatementDispatch: Interp { /* run_statement */ } pub trait BlockQueries: Interp { /* read-only block queries */ } pub trait CFGQueries: BlockQueries { /* cfg_entry */ } pub trait DiGraphQueries: Interp { /* digraph_walk_plan */ } -pub trait CallServices: Env { /* alloc/free env, resolve_callable */ } -pub(crate) trait BlockBinding: Env + BlockQueries { /* bind_block_args */ } +pub trait Env: Interp { /* type Anchor; env read/write */ } +pub trait SSABinding: Env { /* bind_values (blanket) */ } +pub trait CallServices: Interp { /* alloc/free env, resolve_callable */ } +pub(crate) trait BlockBinding: SSABinding + BlockQueries { /* bind_block_args */ } ``` **Members and stack items.** Concrete composition separates two roles that the old @@ -697,10 +743,10 @@ Narrowest first, the shipped member frames now require: | frame | bound | |---|---| | `ScfIfFrame` | `FrameEngine` — decides its arm before being built, so it touches no engine capability at all | -| `ScfForFrame` | `Env` — reads the loop bound/step, pushes a `BlockFrame` | -| `CallFrame` | `CallServices` | -| `BlockCursor` | per operation: `BlockQueries` (query) / `Env + BlockQueries` (bind entry) / `Env` (bind child results) | -| `DiGraphFrame::finish`, `AbstractDiGraphFrame::finish` | `Env` — the schedule is already consumed; only the yields are read | +| `ScfForFrame` | `Env` — reads the loop bound/step, pushes a `BlockFrame` | +| `CallFrame` | `CallServices + Env` — creates/frees the callee activation and binds results into the caller's; no dispatch, no statement-effect algebra | +| `BlockCursor` | per operation: `BlockQueries` (query) / `Env + BlockQueries` (bind entry) / `Env` (bind child results) | +| `DiGraphFrame::finish`, `AbstractDiGraphFrame::finish` | `Env` — the schedule is already consumed; only the yields are read | | `BlockFrame` | `BlockQueries + StatementDispatch + SparseForwardInterp` | | `CFGFrame` | `CFGQueries + StatementDispatch + SparseForwardInterp` | | `DiGraphFrame` | `DiGraphQueries + StatementDispatch + SparseForwardInterp` | @@ -815,8 +861,8 @@ through `summarize_call` instead of descending into the callee. Descending would neither widen nor terminate on recursion. Abstract frames need a few capabilities beyond the traversal they share with -concrete execution, on `ForwardDataflowFrameEngine: Env + StatementDispatch + -BlockQueries + DiGraphQueries` — +concrete execution, on `ForwardDataflowFrameEngine: Env + +StatementDispatch + BlockQueries + DiGraphQueries` — `analysis_merge`, `contribute_return`, and `summarize_call`. It does **not** extend `CallServices`: `AbstractCallFrame`'s single engine requirement is `summarize_call`, so summarizing a call needs no call convention at all. Nor @@ -828,6 +874,57 @@ the engine**: frame chooses *what to traverse* but cannot reorder the summary protocol and break soundness. +### Shared fact storage and environments + +`FactStore` is the common anchor-to-payload map for interpreter values and +analysis facts. Anchors need only `Eq + Hash`; payloads need no lattice contract +for ordinary lookup and assignment. `set` assigns, `get` returns `None` for an +absent anchor, and `join_with` explicitly receives the analysis's bottom and merge +operation and reports whether the stored fact changed. + +`EnvStore` is the environment **storage container**, and it owns both halves +of environment identity: + +```rust +context_indices: HashMap, // which context an environment belongs to +environments: Vec>>, // its facts (one FactStore each) +``` + +Fact maps stay separate per environment, so equal anchors under different +contexts never collide. `alloc()` returns a fresh unkeyed environment; +`get_or_allocate(K)` returns the live environment for a context or allocates and +registers one; `read`/`write`/`free`/`environment` are the remaining operations. +Each environment remembers the key it was registered under, so `free` drops the +context association in constant time — a later `get_or_allocate` of the same key +therefore allocates a fresh environment rather than resurrecting a dead one. +Freed indices are never reused. + +The container is deliberately ignorant of what it stores. It knows nothing about +constprop, bottom values, widening, dependencies, or scheduling; `write` assigns +and never joins; and `read` reports an absent anchor as `Ok(None)` rather than +interpreting it, so an invalid `EnvIndex` (an error) stays distinguishable from +an anchor that holds nothing. Backward analyses still use `FactStore` directly +with their existing scoped anchors instead of allocating environments; migrating +them onto `EnvStore` is the remaining work. + +**Who decides what.** The layering is: + +| layer | decides | +|---|---| +| analysis policy (`CallContext`) | context identity — the key `K` from a resolved target plus abstract arguments. Context-*insensitive* means `K = LinkTarget`: one environment and one summary per resolved target, shared across its call sites | +| `EnvStore` | mapping that identity to a live environment, and holding its facts | +| `Env` | what an access *means* for this engine (unbound-is-an-error vs. bottom, read/write logging), at its `Env::Anchor` family | +| `CallServices` | activation lifetime (`alloc_env`/`free_env`) plus callable resolution (`resolve_callable`) — a sibling of `Env`, not a subtrait | +| `CallFrame` | *when* those lifetime operations run, and pairing them exactly once | +| fixpoint driver | summaries, dependencies, worklist | + +Keyed allocation never reaches a shared engine surface: `CallServices` exposes +only the unkeyed `alloc_env`/`free_env`, and the sparse-forward engine calls +`get_or_allocate` internally, because only it has a context policy. +Concrete execution has no context identity at all — its container is +`EnvStore`, whose uninhabited key type makes it *impossible* +for two calls to share an environment through a common key. + ### Abstract policies — `CallContext` and `WideningStrategy` `SparseForwardInterpreter` is generic over an analysis parameter `P` providing two decisions: @@ -838,11 +935,13 @@ pub trait CallContext { type Key: Eq + Hash + Clone; pub trait WideningStrategy { fn merge(&self, current, incoming, visits) -> Result, _>; } ``` -`ContextInsensitive` keys by `(stage, specialization)` — every call site of a -function shares one summary — and joins-then-widens after `widen_after` visits. +`ContextInsensitive` keys by the resolved `LinkTarget` — every call site of a +target shares one summary — and joins-then-widens after `widen_after` visits. +The target already *is* that identity, so the key does not re-spell it as a +tuple, and a context-sensitive policy *adds* to it rather than replacing it: `kirin-constprop`'s -`ConstPropContext` keys distinct fully-constant argument tuples to distinct -summaries — bounded by a per-function budget, with overflow and non-constant +`ConstPropContext` keys `(LinkTarget, CallCtx)`, mapping distinct fully-constant +argument tuples to distinct summaries — bounded by a per-target budget, with overflow and non-constant arguments collapsing to one shared `Unknown` context (joined → sound `Top`). That is what makes recursive constant propagation precise on both linear recursion (`factorial(Const(5)) → Const(120)`) and overlapping-subproblem diff --git a/tests/frame_engine_capabilities.rs b/tests/frame_engine_capabilities.rs index e79ec594e..9d3232f1e 100644 --- a/tests/frame_engine_capabilities.rs +++ b/tests/frame_engine_capabilities.rs @@ -1,52 +1,26 @@ -//! Compile-time regression tests for the **engine-capability split**. +//! Compile-time capability checks. No mock executes IR or stores values. //! -//! Each engine here is *deliberately incomplete*: it implements only the -//! capability traits one kind of frame consumes, and omits the rest. The value -//! of this file is that **it compiles** — every `assert_frame` / -//! `assert_dataflow_engine` call below is a static proof that the named frame -//! does not secretly require a capability the engine never provides. -//! -//! Before the split there was one monolithic capability trait carrying every -//! operation, so *none* of these four engines could exist: running a block -//! walker meant also supplying `alloc_env`/`free_env`/`resolve_callable`/ -//! `cfg_entry`/`digraph_walk_plan`, and an abstract dataflow -//! engine had to expose a concrete call convention it never performs. -//! -//! | mock engine | pins | +//! | Engine | Boundary protected | //! |---|---| -//! | `BlockOnlyEngine` | `BlockFrame` needs only `Env + StatementDispatch + BlockQueries` | -//! | `CallOnlyEngine` | `CallFrame` needs only `CallServices` — not even a statement-effect algebra | -//! | `AbstractOnlyEngine` | `ForwardDataflowFrameEngine` requires neither `CallServices` nor `CFGQueries` | -//! | `QueriesOnlyEngine` | the `*Queries` traits are honestly read-only: satisfiable with no `Env` at all | +//! | BlockOnlyEngine | Block walking needs no call or graph services | +//! | CallOnlyEngine | Calls need no statement dispatch or forward effects | +//! | AbstractOnlyEngine | Abstract frames need no concrete call lifecycle or CFG queries | +//! | QueriesOnlyEngine | Structural queries need no environment access | +//! | PointAnchoredEngine | Environment access supports program-point anchors | //! -//! Each is load-bearing. Widening a member frame's bound (adding `CallServices` -//! to `BlockFrame`'s `Frame` impl), re-attaching the call lifecycle to the -//! abstract umbrella, or re-adding `Env` as a `*Queries` supertrait each stops -//! this file compiling and names what regressed. -//! -//! The mock engines panic if actually *run*: nothing here executes IR. That is -//! the point — these are type-level assertions, and the behavioral coverage -//! lives in `tests/body_kinds.rs` and the engine crates. +//! Behavior is covered by `body_kinds` and the engine tests. -// Everything here exists to be *type-checked*, not read: the child variants -// prove the narrow conversion bounds are satisfiable and the storage exists -// only to satisfy `Env`, so "never read" is the expected state of this file. #![allow(dead_code)] -use std::collections::HashMap; - use kirin_interpreter::{ AbstractBlockFrame, AbstractCallFrame, AbstractDiGraphFrame, BlockFrame, BlockQueries, CFGFrame, CFGQueries, CallEffect, CallFrame, CallRequest, CallServices, Callee, DefaultCallBodyTraversal, DiGraphFrame, DiGraphQueries, Env, EnvIndex, - ForwardDataflowFrameEngine, ForwardEval, ForwardFrameEngine, Frame, Interp, InterpreterError, - ResolvedCallable, SparseForwardEffect, StatementDispatch, + ForwardDataflowFrameEngine, ForwardEval, Frame, Interp, InterpreterError, ProgramPoint, + ResolvedCallable, SSABinding, SparseForwardEffect, StatementDispatch, }; use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; -/// The compile-time assertions this file is made of. -/// -/// None is ever called; instantiating them is what type-checks the bounds. fn assert_frame() where I: kirin_interpreter::FrameEngine, @@ -56,102 +30,77 @@ where fn assert_dataflow_engine() {} -/// The `*Queries` traits must be satisfiable **without** [`Env`] — that is what -/// makes their names truthful. fn assert_read_only_queries() {} -/// Minimal child representation for the concrete member proofs. -enum CapabilityChild { - Block(BlockFrame), - CFG(CFGFrame), - Call(CallRequest), - DiGraph(DiGraphFrame), +fn assert_point_env>() {} + +// Type-checked for every SSA environment, without forward-effect bounds. +fn ssa_env_implies_binding>() { + fn requires_binding() {} + requires_binding::(); } +struct CapabilityChild; + impl From> for CapabilityChild { - fn from(frame: BlockFrame) -> Self { - Self::Block(frame) + fn from(_: BlockFrame) -> Self { + unimplemented!("compile-time capability test") } } impl From> for CapabilityChild { - fn from(frame: CFGFrame) -> Self { - Self::CFG(frame) + fn from(_: CFGFrame) -> Self { + unimplemented!("compile-time capability test") } } impl From> for CapabilityChild { - fn from(request: CallRequest) -> Self { - Self::Call(request) + fn from(_: CallRequest) -> Self { + unimplemented!("compile-time capability test") } } impl From> for CapabilityChild { - fn from(frame: DiGraphFrame) -> Self { - Self::DiGraph(frame) + fn from(_: DiGraphFrame) -> Self { + unimplemented!("compile-time capability test") } } -// =========================================================================== -// Shared mock storage -// =========================================================================== - -/// Minimal SSA storage so the mocks can satisfy [`Env`] without pulling in the -/// real engines. -#[derive(Default)] -struct MockStore(HashMap<(usize, SSAValue), i64>); - -// =========================================================================== -// 1. BlockOnlyEngine — walks blocks, and nothing else -// =========================================================================== - -/// Implements: [`Interp`], [`Env`], [`StatementDispatch`], [`BlockQueries`]. -/// -/// **Deliberately omits**: [`CallServices`] (no `alloc_env`/`free_env`/ -/// `resolve_callable`), [`CFGQueries`] (no -/// `cfg_entry`), and [`DiGraphQueries`] (no `digraph_walk_plan`). -/// -/// So this engine cannot enter a function, cannot find a CFG's entry block, and -/// cannot schedule a graph — yet it can still run the block walker. -#[derive(Default)] -struct BlockOnlyEngine { - store: MockStore, +// Common location stubs; each engine's capabilities remain explicit below. +macro_rules! interp_stub { + ($engine:ty, $effect:ty) => { + impl Interp for $engine { + type Value = i64; + type Error = InterpreterError; + type Effect = $effect; + type Semantics = ForwardEval; + + fn stage(&self) -> CompileStage { + unimplemented!("compile-time capability test") + } + fn statement(&self) -> Statement { + unimplemented!("compile-time capability test") + } + fn index(&self) -> EnvIndex { + unimplemented!("compile-time capability test") + } + } + }; } -impl Interp for BlockOnlyEngine { - type Value = i64; - type Error = InterpreterError; - type Effect = SparseForwardEffect; - type Semantics = ForwardEval; +struct BlockOnlyEngine; - fn stage(&self) -> CompileStage { - unimplemented!("type-level mock") - } - fn statement(&self) -> Statement { - unimplemented!("type-level mock") - } - fn index(&self) -> EnvIndex { - unimplemented!("type-level mock") - } -} +interp_stub!(BlockOnlyEngine, SparseForwardEffect); impl Env for BlockOnlyEngine { - fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { - self.store - .0 - .get(&(index.raw(), value)) - .copied() - .ok_or(InterpreterError::UnboundValue { index, value }) + type Anchor = SSAValue; + + fn env_read(&self, _: EnvIndex, _: SSAValue) -> Result { + unimplemented!("compile-time capability test") } - fn env_write( - &mut self, - index: EnvIndex, - value: SSAValue, - data: i64, - ) -> Result<(), InterpreterError> { - self.store.0.insert((index.raw(), value), data); - Ok(()) + fn env_write(&mut self, _: EnvIndex, _: SSAValue, _: i64) -> Result<(), InterpreterError> { + unimplemented!("compile-time capability test") } } @@ -162,7 +111,7 @@ impl StatementDispatch for BlockOnlyEngine { _statement: Statement, _index: EnvIndex, ) -> Result { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } @@ -172,14 +121,14 @@ impl BlockQueries for BlockOnlyEngine { _stage: CompileStage, _block: Block, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn first_statement( &self, _stage: CompileStage, _block: Block, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn next_statement( &self, @@ -187,7 +136,7 @@ impl BlockQueries for BlockOnlyEngine { _block: Block, _after: Statement, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } @@ -196,141 +145,56 @@ fn block_frame_runs_on_an_engine_with_only_block_queries_and_dispatch() { assert_frame::>(); } -// =========================================================================== -// 2. CallOnlyEngine — performs the call lifecycle, and nothing else -// =========================================================================== - -/// Implements: [`Interp`], [`Env`], [`CallServices`]. -/// -/// **Deliberately omits**: [`StatementDispatch`] (cannot dispatch a -/// statement), [`BlockQueries`], [`CFGQueries`], and -/// [`DiGraphQueries`] (cannot query any body shape). -/// -/// Its `Effect` is `()`, not a [`SparseForwardEffect`] — proof that -/// [`CallFrame`] needs neither a statement-effect algebra nor -/// [`SparseForwardInterp`](kirin_interpreter::SparseForwardInterp). The call -/// boundary only allocates, resolves, enters, suspends, frees, and binds -/// results. -#[derive(Default)] -struct CallOnlyEngine { - store: MockStore, -} +struct CallOnlyEngine; -impl Interp for CallOnlyEngine { - type Value = i64; - type Error = InterpreterError; - type Effect = (); - type Semantics = ForwardEval; - - fn stage(&self) -> CompileStage { - unimplemented!("type-level mock") - } - fn statement(&self) -> Statement { - unimplemented!("type-level mock") - } - fn index(&self) -> EnvIndex { - unimplemented!("type-level mock") - } -} +interp_stub!(CallOnlyEngine, ()); impl Env for CallOnlyEngine { - fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { - self.store - .0 - .get(&(index.raw(), value)) - .copied() - .ok_or(InterpreterError::UnboundValue { index, value }) + type Anchor = SSAValue; + + fn env_read(&self, _: EnvIndex, _: SSAValue) -> Result { + unimplemented!("compile-time capability test") } - fn env_write( - &mut self, - index: EnvIndex, - value: SSAValue, - data: i64, - ) -> Result<(), InterpreterError> { - self.store.0.insert((index.raw(), value), data); - Ok(()) + fn env_write(&mut self, _: EnvIndex, _: SSAValue, _: i64) -> Result<(), InterpreterError> { + unimplemented!("compile-time capability test") } } impl CallServices for CallOnlyEngine { fn alloc_env(&mut self) -> EnvIndex { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn free_env(&mut self, _index: EnvIndex) -> Result<(), InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn resolve_callable( &self, _stage: CompileStage, _callee: &Callee, ) -> Result { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } #[test] -fn call_frame_runs_on_an_engine_with_only_call_services() { +fn call_frame_needs_only_call_services_and_ssa_env() { assert_frame::>(); } -// =========================================================================== -// 3. AbstractOnlyEngine — abstract dataflow with no concrete call lifecycle -// =========================================================================== - -/// Implements: [`Interp`], [`Env`], [`StatementDispatch`], -/// [`BlockQueries`], [`DiGraphQueries`], and -/// [`ForwardDataflowFrameEngine`]. -/// -/// **Deliberately omits**: [`CallServices`] and -/// [`CFGQueries`]. -/// -/// This is the assertion that carries item #3's main claim. An abstract engine -/// *summarizes* a call ([`ForwardDataflowFrameEngine::summarize_call`]) instead -/// of descending into it, and reaches a callable body's entry block through -/// owner seeding rather than `cfg_entry` — so it should not have to expose -/// activation allocation, activation cleanup, `resolve_callable`, -/// or `cfg_entry` merely to be an abstract dataflow engine. Before the split it -/// did. -#[derive(Default)] -struct AbstractOnlyEngine { - store: MockStore, -} - -impl Interp for AbstractOnlyEngine { - type Value = i64; - type Error = InterpreterError; - type Effect = SparseForwardEffect; - type Semantics = ForwardEval; +struct AbstractOnlyEngine; - fn stage(&self) -> CompileStage { - unimplemented!("type-level mock") - } - fn statement(&self) -> Statement { - unimplemented!("type-level mock") - } - fn index(&self) -> EnvIndex { - unimplemented!("type-level mock") - } -} +interp_stub!(AbstractOnlyEngine, SparseForwardEffect); impl Env for AbstractOnlyEngine { - fn env_read(&self, index: EnvIndex, value: SSAValue) -> Result { - self.store - .0 - .get(&(index.raw(), value)) - .copied() - .ok_or(InterpreterError::UnboundValue { index, value }) + type Anchor = SSAValue; + + fn env_read(&self, _: EnvIndex, _: SSAValue) -> Result { + unimplemented!("compile-time capability test") } - fn env_write( - &mut self, - index: EnvIndex, - value: SSAValue, - data: i64, - ) -> Result<(), InterpreterError> { - self.store.0.insert((index.raw(), value), data); - Ok(()) + fn env_write(&mut self, _: EnvIndex, _: SSAValue, _: i64) -> Result<(), InterpreterError> { + unimplemented!("compile-time capability test") } } @@ -341,7 +205,7 @@ impl StatementDispatch for AbstractOnlyEngine { _statement: Statement, _index: EnvIndex, ) -> Result { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } @@ -351,14 +215,14 @@ impl BlockQueries for AbstractOnlyEngine { _stage: CompileStage, _block: Block, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn first_statement( &self, _stage: CompileStage, _block: Block, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn next_statement( &self, @@ -366,12 +230,10 @@ impl BlockQueries for AbstractOnlyEngine { _block: Block, _after: Statement, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } -/// Taken as-is: the `NoDefaultWalker` default is the whole point of -/// [`DiGraphQueries`] being a separate capability an engine opts into. impl DiGraphQueries for AbstractOnlyEngine {} impl ForwardDataflowFrameEngine for AbstractOnlyEngine { @@ -383,15 +245,15 @@ impl ForwardDataflowFrameEngine for AbstractOnlyEngine { _incoming: &Product, _visits: usize, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn contribute_return(&mut self, _values: Product) -> Result<(), InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn current_function_key(&self) -> Option<()> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn summarize_call( @@ -400,36 +262,31 @@ impl ForwardDataflowFrameEngine for AbstractOnlyEngine { _call: CallEffect, _index: EnvIndex, ) -> Result<(), InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn max_iterations(&self) -> usize { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } -/// Minimal abstract stack-item composition used to prove the member bounds. -enum MockAbstractFrame { - Block(AbstractBlockFrame), - Call(AbstractCallFrame), - DiGraph(AbstractDiGraphFrame), -} +struct MockAbstractFrame; impl From> for MockAbstractFrame { - fn from(frame: AbstractBlockFrame) -> Self { - Self::Block(frame) + fn from(_: AbstractBlockFrame) -> Self { + unimplemented!("compile-time capability test") } } impl From> for MockAbstractFrame { - fn from(frame: AbstractCallFrame) -> Self { - Self::Call(frame) + fn from(_: AbstractCallFrame) -> Self { + unimplemented!("compile-time capability test") } } impl From> for MockAbstractFrame { - fn from(frame: AbstractDiGraphFrame) -> Self { - Self::DiGraph(frame) + fn from(_: AbstractDiGraphFrame) -> Self { + unimplemented!("compile-time capability test") } } @@ -437,9 +294,6 @@ impl From> for MockAbstractFrame fn abstract_engine_needs_no_concrete_call_lifecycle() { assert_dataflow_engine::(); - // And the abstract frames it drives really do run on it — including - // `AbstractCallFrame`, whose only engine requirement is `summarize_call`. - // That is the split's payoff: summarizing a call needs no call convention. assert_frame::< AbstractOnlyEngine, MockAbstractFrame, @@ -457,74 +311,9 @@ fn abstract_engine_needs_no_concrete_call_lifecycle() { >(); } -// =========================================================================== -// 4. The umbrellas still work where a universe needs them -// =========================================================================== - -/// The narrowing must not cost the umbrella: a total frame enum's engine has to -/// support the union of all its variants, so [`ForwardFrameEngine`] remains the -/// right bound there. -/// -/// This needs no instantiation — a generic function body is type-checked at -/// *definition* time, so `needs_all::()` fails to compile the moment -/// `ForwardFrameEngine` stops implying all four components (e.g. if the blanket -/// impl were dropped, or a fifth component added to the umbrella without an -/// impl). -#[allow(dead_code)] -fn umbrella_still_covers_every_component() { - fn needs_all() - where - J: StatementDispatch + BlockQueries + DiGraphQueries + CallServices, - { - } - needs_all::(); -} - -/// Conversely: [`ForwardDataflowFrameEngine`] must keep implying the three -/// traversal components it does extend, so abstract frames can rely on them. -#[allow(dead_code)] -fn dataflow_umbrella_covers_its_three_components() { - fn needs_traversal() - where - J: StatementDispatch + BlockQueries + DiGraphQueries, - { - } - needs_traversal::(); -} - -// =========================================================================== -// 5. The `*Queries` traits are honestly read-only -// =========================================================================== - -/// Implements: [`Interp`], [`BlockQueries`], [`CFGQueries`], [`DiGraphQueries`]. -/// -/// **Deliberately omits [`Env`]** — it has no SSA storage at all, not even a -/// field for it. -/// -/// This is the assertion that keeps the *names* truthful. Each `*Queries` trait -/// requires only `Interp`, so none of their methods can touch the store; the -/// one operation that needs both a query and a write (binding a block's -/// parameters) lives on the crate-private `BlockBinding: Env + BlockQueries` -/// instead. Re-adding `Env` as a `*Queries` supertrait — the obvious way to -/// smuggle a mutating default method back in — stops this engine from compiling. struct QueriesOnlyEngine; -impl Interp for QueriesOnlyEngine { - type Value = i64; - type Error = InterpreterError; - type Effect = (); - type Semantics = ForwardEval; - - fn stage(&self) -> CompileStage { - unimplemented!("type-level mock") - } - fn statement(&self) -> Statement { - unimplemented!("type-level mock") - } - fn index(&self) -> EnvIndex { - unimplemented!("type-level mock") - } -} +interp_stub!(QueriesOnlyEngine, ()); impl BlockQueries for QueriesOnlyEngine { fn block_params( @@ -532,14 +321,14 @@ impl BlockQueries for QueriesOnlyEngine { _stage: CompileStage, _block: Block, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn first_statement( &self, _stage: CompileStage, _block: Block, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } fn next_statement( &self, @@ -547,7 +336,7 @@ impl BlockQueries for QueriesOnlyEngine { _block: Block, _after: Statement, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } @@ -557,7 +346,7 @@ impl CFGQueries for QueriesOnlyEngine { _stage: CompileStage, _cfg: kirin_ir::CFG, ) -> Result, InterpreterError> { - unimplemented!("type-level mock") + unimplemented!("compile-time capability test") } } @@ -567,3 +356,24 @@ impl DiGraphQueries for QueriesOnlyEngine {} fn query_traits_are_satisfiable_without_env() { assert_read_only_queries::(); } + +struct PointAnchoredEngine; + +interp_stub!(PointAnchoredEngine, ()); + +impl Env for PointAnchoredEngine { + type Anchor = ProgramPoint; + + fn env_read(&self, _: EnvIndex, _: ProgramPoint) -> Result { + unimplemented!("compile-time capability test") + } + + fn env_write(&mut self, _: EnvIndex, _: ProgramPoint, _: i64) -> Result<(), InterpreterError> { + unimplemented!("compile-time capability test") + } +} + +#[test] +fn env_access_is_anchor_generic() { + assert_point_env::(); +}