Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a default impl should be an unreachable error

&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