Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f800609
refactor: rename regions to cfgs and update related tests
zhenrongliew Jul 9, 2026
aed4c23
Refactor terminology from Region to Cfg across the codebase
zhenrongliew Jul 9, 2026
1268950
Add acceptance tests for mixed language interpreter bodies
zhenrongliew Jul 13, 2026
0a27ea6
Refactor interpreter frame structure and update toy language integration
zhenrongliew Jul 15, 2026
162f06c
Merge branch 'rust' into dl/generalize-body
Roger-luo Jul 17, 2026
969914e
refactor: simplify liveness analysis by removing demand pre-pass
zhenrongliew Jul 16, 2026
31bd511
Fixed SSAInfo.uses def-use population at finalize. (#687)
zhenrongliew Jul 22, 2026
e0b99cc
refactor: generalize body handling and update topology queries
zhenrongliew Jul 30, 2026
8bc66df
Refactored the DenseBackwardInterp trait to use point_state and point…
zhenrongliew Jul 30, 2026
e6410e4
refactor: introduce AbstractDiGraphFrame for dependency-ordered graph…
zhenrongliew Jul 30, 2026
2a9e7d6
refactor: enhance body handling and interprocedural analysis in tests
zhenrongliew Jul 30, 2026
e4d50b2
refactor: update frame handling methods for clarity and consistency
zhenrongliew Jul 31, 2026
2b12a2b
refactored topology queries already answered in IR
zhenrongliew Jul 31, 2026
d1801fa
Add call body traversal policy and tests
zhenrongliew Aug 3, 2026
6ec0a31
Decoupled the monolithic trait `ForwardFrameDriver` into smaller trai…
zhenrongliew Aug 3, 2026
a61fb14
Refactor block and graph ownership semantics in IR
zhenrongliew Aug 10, 2026
3979057
Refactor liveness to store both block and statement liveness facts.
zhenrongliew Aug 11, 2026
d423569
Refactor dense backward engine to use scoped fact stores and improve …
zhenrongliew Aug 11, 2026
5d01225
Refactor block predecessor handling to use SmallVec for improved memo…
zhenrongliew Aug 12, 2026
27e901f
Changed to use SmallVec. Liveness Analysis now uses block cursor: Las…
zhenrongliew Aug 12, 2026
f6439fc
Refactor CallContext trait to accept FunctionTarget instead of indivi…
zhenrongliew Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,15 @@ For user-defined dialects not in this table, ask the user for domain context dur

- **Dialects are engine-blind**: one `Interpretable` impl serves concrete execution and abstract interpretation; the value domain decides. Undecided conditions (`BranchCondition::is_truthy` / `ForLoopValue::loop_condition` returning `None`) are read in the rule and handed to the dialect's own frame, which rejects them under concrete execution and explores+joins under abstract. (`Branch` is the cf CFG analogue, driven by the engine's CFG frame.) Never write per-engine dialect impls — but a control dialect's *frame* may have distinct concrete/abstract forms, built per-engine through a dialect dispatch trait.

- **Ordinary vs control dialects (frame ownership)**: Ordinary dialects (arith, cmp, constant, bitwise, tuple, ordinary cf branch ops) implement statement-local semantics with the `SparseForwardInterp` helpers and **never see frames**. A dialect whose operations own *structured traversal* defines **dialect-owned frames** and pushes them with `SparseForwardEffect::Push`. The framework's `BodyFrame` / `AbstractBlockFrame` (single-block body walkers) are reusable **building blocks**, not framework-owned structured semantics — a dialect frame may build one to walk a chosen body, but the structured *decision* and result binding stay in the dialect frame.
- **Ordinary vs control dialects (frame ownership)**: Ordinary dialects (arith, cmp, constant, bitwise, tuple, ordinary cf branch ops) implement statement-local semantics with the `SparseForwardInterp` helpers and **never see frames**. A dialect whose operations own *structured traversal* defines **dialect-owned frames** and pushes them with `SparseForwardEffect::Push`. The framework's `BlockFrame` / `AbstractBlockFrame` (single-block body walkers) are reusable **building blocks**, not framework-owned structured semantics — a dialect frame may build one to walk a chosen body, but the structured *decision* and result binding stay in the dialect frame.

- **SCF is the example**: `scf.if` → `kirin_scf::ScfIfFrame` (concrete) / `AbstractScfIfFrame` (abstract); `scf.for` → `ScfForFrame` / `AbstractScfForFrame`. Each is built per-engine through a dialect dispatch trait (`ScfIfDispatch`/`ScfForDispatch`) and returned as `SparseForwardEffect::Push`. The if frame owns picking the arm (concrete) or exploring both arms + joining (abstract); the for frame owns the loop-carried join/widen fixpoint. A language that uses SCF composes a total frame type embedding the standard frames plus `ScfIfFrame`/`ScfForFrame` (via `BuildScfIf`/`BuildScfFor` and the abstract equivalents); see `example/toy-lang`'s `ToyFrame`/`ToyAbstractFrame`. (Future structured dialects would follow the same pattern; only the existing SCF ops are implemented.)

- **Calling conventions are linkers**: `Linker<S>` resolves `Callee` to a `(stage, specialization, body)` target and is passed to engines by value (`.with_linker(..)`). `SameStageLinker` is the default; `CrossStageLinker` routes calls to whichever stage has a live specialization, which is all that cross-language execution *and* cross-language analysis require. Policy must be a component (field), never a trait impl on an engine type.

- **Engines run frames; traversal lives in frames**: both engines share one driver loop, `drive_frames` (`core/frame.rs`), over the direction-neutral `Frame<I: FrameEngine>` protocol — pop the top frame, `step`, apply the returned `FrameEffect`, owning no traversal logic. `FrameEngine` is the minimal anchor (just a total `Error`); every `Interp` is a `FrameEngine` by blanket impl, so frames are decoupled from the forward eval engine and stay reusable. `ConcreteInterpreter<'ir, S, V, E, Lk, F = StandardFrame<V, E>>` uses the concrete standard frames (`engines/concrete/frames.rs`: `BodyFrame`/`CallFrame`, single-path). `SparseForwardInterpreter<'ir, S, V, E, Lk, P = ContextInsensitive, F = StandardAbstractFrame<V, E, P::Key>>` (the forward abstract engine, implementing the `AbstractInterpreter` trait) uses the forward abstract standard frames (`engines/sparse_forward/frames.rs`: `AbstractBlockFrame`/`AbstractCallFrame`) over the shared owner-summary fixpoint driver — block owners with widening, `Branch` exploration, and per-key interprocedural summaries (caller re-enqueueing incl. same-key self-recursion). The backward engines (`SparseBackwardInterpreter`, `DenseBackwardInterpreter`) implement the same `AbstractInterpreter` trait with their own fact stores/effects/frames, wrapping the same fixpoint driver around their own summary-free `*Transfer` inner `Interp`s. The default `StandardFrame`/`StandardAbstractFrame` are structured-control-free; a language with a structured dialect supplies a custom `F` embedding the standard frames (via `FrameBuild`/`AbstractFrameBuild`) plus that dialect's frames. Analysis crates are a lattice + a policy/frame choice + an engine type alias (see `kirin-constprop` for the forward shape and `kirin-liveness` for the two backward shapes).
- **Engines run frames; traversal lives in frames**: both engines share one driver loop, `drive_frames` (`core/frame.rs`), over the direction-neutral `Frame<I: FrameEngine>` protocol — pop the top frame, `step`, apply the returned `FrameEffect`, owning no traversal logic. `FrameEngine` is the minimal anchor (just a total `Error`); every `Interp` is a `FrameEngine` by blanket impl, so frames are decoupled from the forward eval engine and stay reusable. `ConcreteInterpreter<'ir, S, V, E, Lk, F = StandardFrame<V, E>>` uses the concrete standard frames (`engines/concrete/frames/`: the representation walkers `BlockFrame`/`CFGFrame`/`DiGraphFrame` plus the `CallFrame` call boundary, single-path; `UnGraph` traversal has no framework default — a compiler supplies it via `FrameBuild::from_ungraph_entry`). `SparseForwardInterpreter<'ir, S, V, E, Lk, P = ContextInsensitive, F = StandardAbstractFrame<V, E, P::Key>>` (the forward abstract engine, implementing the `AbstractInterpreter` trait) uses the forward abstract standard frames (`engines/sparse_forward/frames.rs`: `AbstractBlockFrame`/`AbstractCallFrame`) over the shared owner-summary fixpoint driver — block owners with widening, `Branch` exploration, and per-key interprocedural summaries (caller re-enqueueing incl. same-key self-recursion). The backward engines (`SparseBackwardInterpreter`, `DenseBackwardInterpreter`) implement the same `AbstractInterpreter` trait with their own fact stores/effects/frames, wrapping the same fixpoint driver around their own summary-free `*Transfer` inner `Interp`s. The default `StandardFrame`/`StandardAbstractFrame` are structured-control-free; a language with a structured dialect supplies a custom `F` embedding the standard frames (via `FrameBuild`/`AbstractFrameBuild`) plus that dialect's frames. Analysis crates are a lattice + a policy/frame choice + an engine type alias (see `kirin-constprop` for the forward shape and `kirin-liveness` for the two backward shapes).

- **Customizing traversal**: `core/frame.rs` defines the shared `Frame`/`FrameEffect`/`drive_frames` protocol plus forward driver traits. Concrete custom frames embed `BodyFrame`/`CallFrame` via `FrameBuild`; forward abstract custom frames embed `Abstract*Frame`s via `AbstractFrameBuild`. Structured dialects may push dialect-owned frames with `SparseForwardEffect::Push`; ordinary dialects never name frame types. Abstract summary keying and join/widen policy stay in `CallContext`/`WideningStrategy`.
- **Customizing traversal**: `core/frame.rs` defines the shared `Frame`/`FrameEffect`/`drive_frames` protocol plus forward driver traits. Concrete custom frames embed `BlockFrame`/`CFGFrame`/`DiGraphFrame`/`CallFrame` via `FrameBuild` (whose `from_ungraph_entry` hook supplies a callable-UnGraph policy); forward abstract custom frames embed `Abstract*Frame`s via `AbstractFrameBuild`. Structured dialects may push dialect-owned frames with `SparseForwardEffect::Push`; ordinary dialects never name frame types. Abstract summary keying and join/widen policy stay in `CallContext`/`WideningStrategy`.

- **Stage dispatch**: stage enums add `#[derive(InterpDispatch)]` next to `StageMeta`/`ParseDispatch`; single-language pipelines get a blanket impl. `InterpDispatch<I>` is keyed on the engine alone; the dispatched key is always `I::Semantics`. The engine sets its current location then passes itself to dispatch, which forwards to the matching `Interpretable`/`FunctionEntry` rule. Engine-internal IR queries go through `StageQuery`.

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ kirin-cmp = { workspace = true }
kirin-constant = { workspace = true }
kirin-function = { workspace = true }
kirin-scf = { workspace = true }
kirin-interpreter = { workspace = true, features = ["derive"] }
kirin-test-languages = { workspace = true, features = [
"arith-function-language",
"graph-function-language",
"interpreter",
"bitwise-function-language",
"callable-language",
"namespaced-language",
Expand Down
2 changes: 1 addition & 1 deletion crates/kirin-derive-interpreter/src/function_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fn emit_function_entry(
args: #ir_crate::Product<<__EntryI as #interp_crate::Interp>::Value>,
interp: &mut __EntryI,
) -> Result<
#interp_crate::FunctionBody<<__EntryI as #interp_crate::Interp>::Value>,
#interp_crate::CallableBody<<__EntryI as #interp_crate::Interp>::Value>,
<__EntryI as #interp_crate::Interp>::Error,
> {
#body
Expand Down
2 changes: 1 addition & 1 deletion crates/kirin-derive-interpreter/src/interp_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ pub fn generate(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
args: #ir_crate::Product<<__InterpI as #interp_crate::Interp>::Value>,
interp: &mut __InterpI,
) -> Result<
#interp_crate::FunctionBody<<__InterpI as #interp_crate::Interp>::Value>,
#interp_crate::CallableBody<<__InterpI as #interp_crate::Interp>::Value>,
<__InterpI as #interp_crate::Interp>::Error,
> {
match self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ where
args: ::kirin::ir::Product<<__EntryI as ::kirin_interpreter::Interp>::Value>,
interp: &mut __EntryI,
) -> Result<
::kirin_interpreter::FunctionBody<<__EntryI as ::kirin_interpreter::Interp>::Value>,
::kirin_interpreter::CallableBody<<__EntryI as ::kirin_interpreter::Interp>::Value>,
<__EntryI as ::kirin_interpreter::Interp>::Error,
> {
match self {
Expand Down
12 changes: 6 additions & 6 deletions crates/kirin-function/src/interpreter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use kirin::prelude::{CompileTimeValue, HasBottom, HasCFGBody, Product, SSAValue};
use kirin_interpreter::dialect::{
CallEffect, Callee, ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect,
ForwardEval, FunctionBody, FunctionEntry, Interp, Interpretable, InterpreterError,
CallEffect, CallableBody, Callee, ClassicLiveness, ClassicLivenessInterp, DemandInterp,
DenseBackwardEffect, ForwardEval, FunctionEntry, Interp, Interpretable, InterpreterError,
SparseForwardEffect, SparseForwardInterp, StrongDemand,
};

Expand Down Expand Up @@ -98,8 +98,8 @@ where
&self,
args: Product<I::Value>,
_interp: &mut I,
) -> Result<FunctionBody<I::Value>, I::Error> {
Ok(FunctionBody::new(*self.cfg()).args(args))
) -> Result<CallableBody<I::Value>, I::Error> {
Ok(CallableBody::new(*self.cfg()).args(args))
}
}

Expand All @@ -112,8 +112,8 @@ where
&self,
args: Product<I::Value>,
_interp: &mut I,
) -> Result<FunctionBody<I::Value>, I::Error> {
Ok(FunctionBody::new(*self.cfg()).args(args))
) -> Result<CallableBody<I::Value>, I::Error> {
Ok(CallableBody::new(*self.cfg()).args(args))
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/kirin-interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2024"

[dependencies]
kirin-ir = { workspace = true }
petgraph = { workspace = true }
kirin-derive-interpreter = { workspace = true, optional = true }
smallvec = { workspace = true }
thiserror = { workspace = true }
Expand Down
10 changes: 5 additions & 5 deletions crates/kirin-interpreter/src/core/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use kirin_ir::{Dialect, Product, StageInfo, StageMeta, Statement};

use crate::{FunctionBody, Interp};
use crate::{CallableBody, Interp};

/// Statement semantics. The single trait dialect authors implement.
///
Expand All @@ -18,7 +18,7 @@ pub trait Interpretable<I: Interp, Semantics>: Dialect {
/// Function-entry semantics for callable statements.
///
/// Implemented by statements that define function bodies (e.g.
/// `kirin_function::Function`); describes the [`FunctionBody`] an engine enters
/// `kirin_function::Function`); describes the [`CallableBody`] an engine enters
/// when the function is invoked. Derived on language enums with
/// `#[derive(FunctionEntry)]` where `#[callable]` marks the variants that wrap
/// callable statements.
Expand All @@ -27,7 +27,7 @@ pub trait FunctionEntry<I: Interp>: Dialect {
&self,
args: Product<I::Value>,
interp: &mut I,
) -> Result<FunctionBody<I::Value>, I::Error>;
) -> Result<CallableBody<I::Value>, I::Error>;
}

/// Monomorphic statement dispatch over a stage enum.
Expand All @@ -54,7 +54,7 @@ pub trait InterpDispatch<I: Interp>: StageMeta {
body: Statement,
args: Product<I::Value>,
interp: &mut I,
) -> Result<FunctionBody<I::Value>, I::Error>;
) -> Result<CallableBody<I::Value>, I::Error>;
}

impl<I, L> InterpDispatch<I> for StageInfo<L>
Expand All @@ -76,7 +76,7 @@ where
body: Statement,
args: Product<I::Value>,
interp: &mut I,
) -> Result<FunctionBody<I::Value>, I::Error> {
) -> Result<CallableBody<I::Value>, I::Error> {
let definition = body.definition(self).clone();
definition.function_entry(args, interp)
}
Expand Down
70 changes: 58 additions & 12 deletions crates/kirin-interpreter/src/core/effect.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
use kirin_ir::{
Block, CFG, CompileStage, Function, Product, SSAValue, SpecializedFunction, StagedFunction,
Symbol,
Block, CFG, CompileStage, DiGraph, Function, Product, SSAValue, SpecializedFunction,
StagedFunction, Symbol, UnGraph,
};

/// A traversal descriptor: which body was the engine handed?
///
/// Interpreter vocabulary, not an IR concept — dialect ops keep their precise
/// field types (`Block`, `CFG`, `DiGraph`, `UnGraph`); a `Body` appears only at
/// the moment a body is handed to the interpreter (callable entry, topology
/// queries, analysis scopes). Bodies carry no semantics of their own: the
/// statement that owns a body defines what entering and exiting it means.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Body {
Block(Block),
CFG(CFG),
DiGraph(DiGraph),
UnGraph(UnGraph),
}

impl From<Block> for Body {
fn from(block: Block) -> Self {
Self::Block(block)
}
}

impl From<CFG> for Body {
fn from(cfg: CFG) -> Self {
Self::CFG(cfg)
}
}

impl From<DiGraph> for Body {
fn from(graph: DiGraph) -> Self {
Self::DiGraph(graph)
}
}

impl From<UnGraph> for Body {
fn from(graph: UnGraph) -> Self {
Self::UnGraph(graph)
}
}

/// The closed forward control algebra a statement produces.
///
/// Atomic statements read operands, write results, and return [`SparseForwardEffect::Next`].
Expand Down Expand Up @@ -86,27 +125,34 @@ pub enum Callee {
Specialized(SpecializedFunction),
}

/// The body a callable statement enters when invoked: a CFG plus the
/// entry arguments bound to its entry block.
/// The body a callable statement enters when invoked, plus the entry
/// arguments bound to its boundary (block parameters / graph ports).
///
/// This is the function-call entry descriptor — the call mechanism, not a
/// structured-control abstraction. A [`FunctionEntry`](crate::FunctionEntry)
/// rule returns one; the engine builds the body frame that walks the CFG.
pub struct FunctionBody<V> {
pub cfg: CFG,
/// rule returns one; the call boundary picks the walker that matches the
/// body kind. Any body kind may be callable — the statement declaring itself
/// callable defines the semantics; the framework supplies default walkers
/// for `CFG`, `Block`, and `DiGraph`, while `UnGraph` traversal is a
/// dialect/compiler-supplied policy (the concrete engine's
/// `FrameBuild::from_ungraph_entry` hook), rejected with
/// [`InterpreterError::NoDefaultWalker`](crate::InterpreterError) when no
/// policy is provided.
pub struct CallableBody<V> {
pub body: Body,
pub args: Product<V>,
}

impl<V> FunctionBody<V> {
/// A function body over `cfg`, with no entry arguments yet.
pub fn new(cfg: CFG) -> Self {
impl<V> CallableBody<V> {
/// A callable body, with no entry arguments yet.
pub fn new(body: impl Into<Body>) -> Self {
Self {
cfg,
body: body.into(),
args: Product::new(),
}
}

/// Entry arguments bound to the CFG entry block's parameters.
/// Entry arguments bound to the body's boundary parameters.
pub fn args(mut self, args: impl IntoIterator<Item = V>) -> Self {
self.args = args.into_iter().collect();
self
Expand Down
8 changes: 6 additions & 2 deletions crates/kirin-interpreter/src/core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ pub enum InterpreterError {
MissingCallTarget(Symbol),
#[error("cfg has no entry block")]
EmptyCFG,
#[error("body {0:?} has no default walker in this engine")]
NoDefaultWalker(crate::Body),
#[error("digraph {0:?} has a cycle; the default walker only runs DAGs")]
GraphHasCycle(kirin_ir::DiGraph),
#[error("CFG control flow (jump/branch) inside a single-block or graph body")]
CFGControlFlowInStructuredBody,
#[error("block {0:?} fell through without a terminator effect")]
BlockFellThrough(Block),
#[error("function body fell through without returning")]
FunctionBodyFellThrough,
#[error("yield outside of an enclosing scope at {0:?}")]
UnexpectedYield(Statement),
#[error("statement {0:?} is not callable")]
Expand Down
14 changes: 11 additions & 3 deletions crates/kirin-interpreter/src/core/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::hash::Hash;
use kirin_ir::{Block, CFG, CompileStage, Product, SSAValue, Statement};

use crate::{
CallEffect, Callee, Env, EnvIndex, FunctionBody, FunctionTarget, Interp, InterpreterError,
CallEffect, CallableBody, Callee, Env, EnvIndex, FunctionTarget, Interp, InterpreterError,
};

/// Structural effect a [`Frame`] returns to the engine driver loop.
Expand Down Expand Up @@ -117,14 +117,14 @@ pub trait ForwardFrameDriver: Env {
statement: Statement,
index: EnvIndex,
) -> Result<Self::Effect, Self::Error>;
/// Build the [`FunctionBody`] a callable statement enters on invocation.
/// Build the [`CallableBody`] a callable statement enters on invocation.
fn enter_function(
&mut self,
stage: CompileStage,
body: Statement,
args: Product<Self::Value>,
index: EnvIndex,
) -> Result<FunctionBody<Self::Value>, Self::Error>;
) -> Result<CallableBody<Self::Value>, Self::Error>;

fn block_params(&self, stage: CompileStage, block: Block)
-> Result<Vec<SSAValue>, Self::Error>;
Expand All @@ -141,6 +141,14 @@ pub trait ForwardFrameDriver: Env {
) -> Result<Option<Statement>, Self::Error>;
fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result<Option<Block>, Self::Error>;

/// The default walk plan of a digraph body (ports, toposorted nodes,
/// yields). Errors on cyclic digraphs.
fn digraph_walk_plan(
Comment thread
zhenrongliew marked this conversation as resolved.
Outdated
&self,
stage: CompileStage,
graph: kirin_ir::DiGraph,
) -> Result<crate::GraphWalkPlan, Self::Error>;

/// Bind a block's parameters to incoming actuals in `env` (arity-checked).
fn bind_block_args(
&mut self,
Expand Down
Loading