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
10 changes: 6 additions & 4 deletions AGENTS.md

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions crates/kirin-constprop/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,7 +43,11 @@ pub enum CallCtx {
pub struct ConstPropContext {
control: ContextInsensitive,
max_contexts: usize,
admitted: HashMap<(CompileStage, SpecializedFunction), HashSet<Vec<i64>>>,
/// 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<LinkTarget, HashSet<Vec<i64>>>,
}

impl ConstPropContext {
Expand All @@ -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<ConstPropValue> for ConstPropContext {
type Key = (CompileStage, SpecializedFunction, CallCtx);
type Key = (LinkTarget, CallCtx);

fn key(&mut self, target: &LinkTarget, args: &Product<ConstPropValue>) -> 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 {
Expand All @@ -87,7 +92,7 @@ impl CallContext<ConstPropValue> for ConstPropContext {
}
None => CallCtx::Unknown,
};
(stage, function, ctx)
(*target, ctx)
}
}

Expand Down
187 changes: 0 additions & 187 deletions crates/kirin-interpreter/src/core/env.rs

This file was deleted.

11 changes: 11 additions & 0 deletions crates/kirin-interpreter/src/core/env/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
101 changes: 101 additions & 0 deletions crates/kirin-interpreter/src/core/env/services.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Value, Self::Error>;
/// 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<Anchor = SSAValue>` 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<Anchor = SSAValue> {
/// 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<Self::Value>,
) -> 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<T: Env<Anchor = SSAValue>> SSABinding for T {}
Loading