Skip to content

refactor(interpreter)!: rebuild framework around effect algebra and linker components#671

Open
Roger-luo wants to merge 6 commits into
rustfrom
claude/adoring-torvalds-9a2faf
Open

refactor(interpreter)!: rebuild framework around effect algebra and linker components#671
Roger-luo wants to merge 6 commits into
rustfrom
claude/adoring-torvalds-9a2faf

Conversation

@Roger-luo

Copy link
Copy Markdown
Collaborator

Note

Based on rust (the Rust workspace lineage). Includes one prior local commit (feat(toy-lang): add cross-language concrete interpreter) that was not yet pushed; the redesign commit supersedes most of it.

What

Replaces the frame-fusion interpreter framework with a redesign built around a two-persona contract, deleting ~12.9k lines while adding ~1.2k (net −11.7k). All 1184 workspace tests pass; toy-lang's CLI (run, run --constprop) works unchanged.

Dialect-author surface (kirin_interpreter::dialect)

  • Interpretable<I: Interp> has one generic parameter (was six: Interpretable<L, I, F, C, E, T>); I::Value/I::Error are associated types.
  • Ctx hides EnvIndex/Location plumbing: ctx.read(ssa) / ctx.write(result, value).
  • Statements return a closed Effect algebra: Next / Jump / Branch / Call / Yield / Return / Enter / EnterAny. Undecided control flow is expressed by the value domain (is_truthy()/loop_condition() returning None), so one impl serves concrete execution and abstract interpretation — kirin-cf drops from two impls to one; the dual ConcreteBlockTransfer/AbstractBlockTransfer types are gone.
  • Structured control flow uses Scope/ScopeHook: scf.if is two Scope::block(..).bind(..) lines, scf.for is a ~40-line hook. The 830-line ScfFrame state machine and all Scf*Dispatch traits are deleted (scf: 830 → 129 lines).

Compiler-author surface (kirin_interpreter::engine)

  • Engines: ConcreteInterpreter<'ir, S, V, E, Lk> (explicit scope stack) and AbstractInterpreter<'ir, S, V, E, Lk> (fixpoint analyzer).
  • Calling conventions are Linker components passed by value (SameStageLinker default, CrossStageLinker for multi-language pipelines) — never trait impls on engine types, so no more newtype-cloning drivers. toy-lang's 476-line ToyConcreteInterpreter clone is deleted; cross-language execution and cross-language constprop are each one .with_linker(CrossStageLinker) call.
  • toy-lang's interpreter module: 831 → 135 production lines (language enums + a value type + an error enum + entry points). No frame enums, completion enums, profiles, or dispatch impls.

Engine-owned fixpoints

AbstractInterpreter owns all three convergence loops: CFG block worklists with widening (cf back-edges), hook-driven scope re-entry with joined entries (scf loops), and interprocedural entry/return Product<V> function summaries with caller re-enqueueing. This deletes constprop's AdvanceableLocationSummary/ScfForConstPropSummary special-casing; kirin-constprop is now the lattice plus a type alias (1281 → 629 lines, almost all value-domain code).

Derives

Reduced to trait-named macros only: Interpretable, FunctionEntry, InterpDispatch (mirrors ParseDispatch on stage enums; single-language pipelines get a blanket impl). Frame, StageFrame, Completion, LiftError, HasLocation, and forward_through! are removed. Engine-internal IR queries reuse kirin-ir's existing StageAction dispatch via the auto-satisfied StageQuery bundle — no new dispatch machinery beyond InterpDispatch.

Why

The previous framework was over-complicated relative to its goals: six-parameter traits, 13 ad-hoc capability/dispatch traits, a viral TryFrom-lift algebra, per-engine dialect impls, and coherence traps that forced downstream driver clones. This redesign minimizes concepts per persona: a dialect author learns Ctx + Effect + value-domain bounds (plus ScopeHook for structured dialects); a compiler author learns a derive list, a linker choice, and an engine entry point.

Notes for reviewers

  • Design doc: docs/design/interpreter/index.md (replaces docs/design/new-interpreter/); AGENTS.md interpreter conventions updated to match.
  • Test coverage moved outward: the framework crate has fewer inline unit tests; correctness is carried by toy-lang's 25 integration tests (concrete + constprop + cross-stage + scf loop fixpoint + cf joins + recursion) and derive snapshots. Engine unit tests against kirin-test-languages are a good follow-up.
  • Deferred (recorded in the design doc): backward dataflow solver for liveness (separate kirin-dataflow engine, deliberately not forced through Effect), context-sensitive call summaries, first-class lambda values, inline Scope::region bodies in the abstract engine.
  • The remaining all-targets clippy lints are pre-existing in untouched crates (kirin-prettyless tests, kirin-derive-toolkit, kirin-test-languages); the repo's pre-commit clippy gate passes.

🤖 Generated with Claude Code

Roger-luo and others added 2 commits June 11, 2026 17:29
Add ToyConcreteInterpreter that can dispatch calls across language
stages, so a source-stage function calling a function bodied only at
the lowered stage (and vice versa) executes concretely. Expose via
run_i64; the CLI Run command now defaults to cross-language, with
--per-language opting back into the single-stage run_source_i64 /
run_lowered_i64 helpers.

The cross-stage call resolution shared with the const-prop fixpoint
moves into a new cross_stage module. As a separate cleanup, the
test-only profiles, single-run abstract helpers, and directional
fixpoint helpers move out of profile.rs / run.rs / fixpoint.rs into
src/interpreter/tests/, so the production files contain no
#[cfg(test)] items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inker components

Replace the frame-fusion interpreter with a two-persona design:

- Dialect authors implement Interpretable<I> (one generic parameter; Ctx
  hides env indices/locations) and return a closed Effect algebra
  (Next/Jump/Branch/Call/Yield/Return/Enter/EnterAny). One impl serves
  concrete and abstract interpretation; undecidedness lives in the value
  domain, not the engine.
- scf is expressed via Scope/ScopeHook; loop fixpoints are engine-owned,
  deleting the ScfFrame state machine, Scf*Dispatch traits, and the
  constprop scf-for location-summary machinery.
- Calling conventions are Linker components (SameStageLinker,
  CrossStageLinker) passed by value; deletes toy-lang's driver clone and
  makes cross-language execution and analysis a one-line choice.
- AbstractInterpreter owns all fixpoints: CFG block worklists with
  widening, scope re-entry until stable, interprocedural entry/return
  function summaries. kirin-constprop reduces to the lattice + an alias.
- Derives reduce to trait-named macros only: Interpretable, FunctionEntry,
  InterpDispatch (mirrors ParseDispatch). Frame/StageFrame/Completion/
  LiftError/HasLocation derives and forward_through! are removed.
- Engine-internal IR queries reuse kirin-ir StageAction dispatch
  (StageQuery); design doc moved to docs/design/interpreter/.

Net -11.7k lines; all 1184 workspace tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Roger-luo Roger-luo requested a review from zhenrongliew June 13, 2026 00:07
let key = (target.stage, target.function);
self.join_entry(key, target.body, call.args)?;
if let (Some(info), Some(caller)) = (self.summaries.get_mut(&key), self.current)
&& caller != key

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.

Constant propogation is broken on Factorial.
e.g. cargo run -p toy-lang -- run example/toy-lang/programs/factorial.kirin --stage source --function factorial --constprop 5 prints Const(1). On base rust branch, prints Const(120).

Recursive self-calls are excluded from caller dependency tracking. By removing  && caller != key, const-prop for factorial 5 now results in Top. A sound result, but less precise than Const(120).

@zhenrongliew

Copy link
Copy Markdown
Collaborator

@Roger-luo mentioned that the changes in this PR

had removed the frame generalization, which removes the capability of customizing traversing order by defining custom frame types.

We still need to rework this more.

@Roger-luo

Copy link
Copy Markdown
Collaborator Author

I think the conclusion is fable 5 is likely highly hyped and is worse than 5.5 which helped me iterate a few previous set of abstractions... so no free lunch, we gotta simplify the abstraction here ourselves first

…rame types

- Added a new `frame` module to support generalized frame-based traversal in the interpreter.
- Introduced `Frame`, `FrameEffect`, and `FrameDriver` traits to facilitate customizable traversal logic.
- Implemented `StandardFrame` as the default total frame enum, encapsulating `ScopeFrame` and `CallFrame`.
- Enhanced `ConcreteInterpreter` to utilize the new frame system, allowing for more flexible execution.
- Updated `AbstractInterpreter` to support customizable policies for summary keying, enabling context-sensitive analyses.
- Added `ConstPropContext` for bounded argument tuple context sensitivity in constant propagation analysis.
@zhenrongliew

Copy link
Copy Markdown
Collaborator

Pushed follow-up commits addressing the frame-generalization and recursive constprop concerns.

Summary:

  • Kept the simplified dialect-author API (Interpretable<I>, Ctx, Effect) so dialect impls remain engine/frame-blind.
  • Restored customizable frame-based traversal on the concrete engine. ConcreteInterpreter is generic over a total frame type, with StandardFrame as the default and a test proving custom frames can observe traversal without forking the engine.
  • Made abstract interpretation policy-driven via CallContext and AbstractControl, separating summary keying from join/widen behavior.
  • Fixed recursive constprop with bounded arg-tuple context sensitivity plus same-key self-dependency tracking. Known recursion now folds precisely (factorial(5) -> Const(120), Fibonacci covered too); unknown/over-budget contexts converge soundly to Top.

… formalism, covering syntax, state model, operational semantics, and type system
@zhenrongliew zhenrongliew self-assigned this Jun 17, 2026
@zhenrongliew zhenrongliew marked this pull request as draft June 17, 2026 16:02
- Split frame types into abstract and concrete implementations in the interpreter.
- Enhance `If` and `For` structures in the SCF module with additional methods for better access to their components.
- Add a new `DataflowLanguage` for testing backward dataflow analyses, integrating various dialects including constants, arithmetic, and control flow.
- Update Cargo.toml to include new dependencies and features for the dataflow language.
- Implement a custom abstract frame in the toy language tests to observe traversal during analysis.
@zhenrongliew

Copy link
Copy Markdown
Collaborator

Update after rework:

This rework keeps the simplified dialect API, but restore frame traversal customization for compiler authors.

Main architectural shape now:

  • Dialect authors still use the simple Interpretable<I> / Ctx / Effect API. Frame types do not leak into dialect implementations.
  • Concrete and abstract interpreters both support custom frame types again.
  • Abstract interpretation now separates traversal frames from analysis policy:
    • frames decide traversal,
    • CallContext decides summary keying,
    • WideningStrategy decides join/widen behavior.
  • Recursive constprop is fixed with bounded context-sensitive summaries (ConstPropContext) plus same-key recursion dependency tracking. Known recursive factorial/fibonacci now produce constants; unknown/over-budget cases conservatively become Top.
  • Liveness was implemented separately as backward dataflow in kirin-dataflow, not through Interpretable/Effect, preserving the two-persona model.

Validation:

  • cargo test --workspace passes.
  • Focused kirin-dataflow suite passes: 16 tests covering straight-line liveness, dead pure ops, branches, block-argument edge transfer, joins, CFG loops, calls, multi-results, unreachable blocks, scf.if, scf.for, and malformed edge errors.
  • Toy-lang interpreter/constprop tests pass, including recursive factorial/fibonacci, cross-stage behavior, and custom concrete/abstract frame traversal tests.

@zhenrongliew zhenrongliew marked this pull request as ready for review June 17, 2026 20:20
@@ -0,0 +1,488 @@
//! Backward liveness solver: generic use/def transfer, a CFG worklist with

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this is using any of our abstract interpretation framework tho? @zhenrongliew

Comment on lines +22 to +25
use kirin_cf::ControlFlow;
use kirin_function::Return;
use kirin_ir::{Block, CompileTimeValue, SSAValue};
use kirin_scf::{For, If, StructuredControlFlow, Yield};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this is not dialect local

@zhenrongliew

Copy link
Copy Markdown
Collaborator

I’ve opened a smaller follow-up PR here: #677.

That PR supersedes the interpreter-framework refactor in this one since the direction this was taking is not what we want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants