From f80060963d98efceee6a52524bbeba88939b3840 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 9 Jul 2026 17:45:48 -0400 Subject: [PATCH 01/21] refactor: rename regions to cfgs and update related tests --- ...dialect_derive_struct_with_cfg_block.snap} | 32 +++++++++---------- ...ests__standalone__standalone_has_cfgs.snap | 26 +++++++++++++++ ...s__standalone__standalone_has_regions.snap | 26 --------------- .../src/builder/{region.rs => cfg.rs} | 0 .../kirin-ir/src/node/{region.rs => cfg.rs} | 0 ...n_prettyless__tests__print_cfg_empty.snap} | 0 ...ss__tests__print_cfg_multiple_blocks.snap} | 0 7 files changed, 42 insertions(+), 42 deletions(-) rename crates/kirin-derive-ir/src/tests/snapshots/{kirin_derive_ir__tests__dialect__dialect_derive_struct_with_region_block.snap => kirin_derive_ir__tests__dialect__dialect_derive_struct_with_cfg_block.snap} (94%) create mode 100644 crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_cfgs.snap delete mode 100644 crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_regions.snap rename crates/kirin-ir/src/builder/{region.rs => cfg.rs} (100%) rename crates/kirin-ir/src/node/{region.rs => cfg.rs} (100%) rename crates/kirin-prettyless/src/tests/snapshots/{kirin_prettyless__tests__print_region_empty.snap => kirin_prettyless__tests__print_cfg_empty.snap} (100%) rename crates/kirin-prettyless/src/tests/snapshots/{kirin_prettyless__tests__print_region_multiple_blocks.snap => kirin_prettyless__tests__print_cfg_multiple_blocks.snap} (100%) diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_region_block.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_cfg_block.snap similarity index 94% rename from crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_region_block.snap rename to crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_cfg_block.snap index 14e3b1f013..0d5f2cb573 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_region_block.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_cfg_block.snap @@ -225,55 +225,55 @@ impl<'a> Iterator for IfOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for IfOp { - type Iter = IfOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for IfOp { + type Iter = IfOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { condition, then_block, else_block, body, } = self; - IfOpRegionsIter { + IfOpCfgsIter { inner: std::iter::once(body), } } } #[automatically_derived] #[doc(hidden)] -pub struct IfOpRegionsIter<'a> { - inner: std::iter::Once<&'a ::kirin::ir::Region>, +pub struct IfOpCfgsIter<'a> { + inner: std::iter::Once<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for IfOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for IfOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for IfOp { - type IterMut = IfOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for IfOp { + type IterMut = IfOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { condition, then_block, else_block, body, } = self; - IfOpRegionsMutIter { + IfOpCfgsMutIter { inner: std::iter::once(body), } } } #[automatically_derived] #[doc(hidden)] -pub struct IfOpRegionsMutIter<'a> { - inner: std::iter::Once<&'a mut ::kirin::ir::Region>, +pub struct IfOpCfgsMutIter<'a> { + inner: std::iter::Once<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for IfOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for IfOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_cfgs.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_cfgs.snap new file mode 100644 index 0000000000..2997364253 --- /dev/null +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_cfgs.snap @@ -0,0 +1,26 @@ +--- +source: crates/kirin-derive-ir/src/tests/standalone.rs +expression: rustfmt(tokens.to_string()) +--- +#[automatically_derived] +impl<'a> ::kirin::ir::HasCfgs<'a> for Lambda { + type Iter = LambdaCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { + let Self { body } = self; + LambdaCfgsIter { + inner: std::iter::once(body), + } + } +} +#[automatically_derived] +#[doc(hidden)] +pub struct LambdaCfgsIter<'a> { + inner: std::iter::Once<&'a ::kirin::ir::Cfg>, +} +#[automatically_derived] +impl<'a> Iterator for LambdaCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; + fn next(&mut self) -> Option { + self.inner.next() + } +} diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_regions.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_regions.snap deleted file mode 100644 index abbb052240..0000000000 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__standalone__standalone_has_regions.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/kirin-derive-ir/src/generate.rs -expression: rustfmt(tokens.to_string()) ---- -#[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for Lambda { - type Iter = LambdaRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { - let Self { body } = self; - LambdaRegionsIter { - inner: std::iter::once(body), - } - } -} -#[automatically_derived] -#[doc(hidden)] -pub struct LambdaRegionsIter<'a> { - inner: std::iter::Once<&'a ::kirin::ir::Region>, -} -#[automatically_derived] -impl<'a> Iterator for LambdaRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; - fn next(&mut self) -> Option { - self.inner.next() - } -} diff --git a/crates/kirin-ir/src/builder/region.rs b/crates/kirin-ir/src/builder/cfg.rs similarity index 100% rename from crates/kirin-ir/src/builder/region.rs rename to crates/kirin-ir/src/builder/cfg.rs diff --git a/crates/kirin-ir/src/node/region.rs b/crates/kirin-ir/src/node/cfg.rs similarity index 100% rename from crates/kirin-ir/src/node/region.rs rename to crates/kirin-ir/src/node/cfg.rs diff --git a/crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_region_empty.snap b/crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_cfg_empty.snap similarity index 100% rename from crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_region_empty.snap rename to crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_cfg_empty.snap diff --git a/crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_region_multiple_blocks.snap b/crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_cfg_multiple_blocks.snap similarity index 100% rename from crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_region_multiple_blocks.snap rename to crates/kirin-prettyless/src/tests/snapshots/kirin_prettyless__tests__print_cfg_multiple_blocks.snap From aed4c2397c39ae484d1cabf22b6e7d9073f0741c Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 9 Jul 2026 17:46:08 -0400 Subject: [PATCH 02/21] Refactor terminology from Region to Cfg across the codebase --- AGENTS.md | 8 +- crates/kirin-bitwise/src/tests.rs | 8 +- crates/kirin-cf/src/tests.rs | 10 +- crates/kirin-chumsky/src/ast/blocks.rs | 28 ++-- .../src/function_text/dispatch.rs | 2 +- .../kirin-chumsky/src/function_text/syntax.rs | 2 +- .../kirin-chumsky/src/function_text/tests.rs | 12 +- crates/kirin-chumsky/src/parsers/blocks.rs | 22 +-- crates/kirin-chumsky/src/traits/emit_ir.rs | 2 +- crates/kirin-chumsky/src/traits/has_parser.rs | 4 +- crates/kirin-chumsky/src/traits/parse_emit.rs | 6 +- crates/kirin-cmp/src/tests.rs | 10 +- crates/kirin-constant/src/tests.rs | 10 +- .../src/codegen/emit_ir/field_emit.rs | 2 +- .../src/codegen/emit_ir/generate.rs | 4 +- .../src/codegen/parser/chain.rs | 6 +- crates/kirin-derive-chumsky/src/field_kind.rs | 26 ++- crates/kirin-derive-chumsky/src/format.rs | 2 +- crates/kirin-derive-chumsky/src/input.rs | 8 +- crates/kirin-derive-chumsky/src/validation.rs | 22 +-- crates/kirin-derive-ir/src/generate.rs | 24 +-- crates/kirin-derive-ir/src/lib.rs | 4 +- crates/kirin-derive-ir/src/tests/dialect.rs | 4 +- ...ect__dialect_derive_custom_crate_path.snap | 36 ++-- ..._mixed_does_not_generate_lift_project.snap | 44 ++--- ...ct_derive_enum_mixed_wraps_and_fields.snap | 44 ++--- ...m_pure_wrapper_generates_lift_project.snap | 62 +++---- ...alect__dialect_derive_enum_with_wraps.snap | 48 +++--- ...pper_with_side_fields_no_lift_project.snap | 48 +++--- ...num_wraps_with_extra_fields_from_impl.snap | 49 +++--- ...ect_derive_enum_wraps_with_terminator.snap | 48 +++--- ..._dialect_derive_struct_all_properties.snap | 36 ++-- ...__dialect__dialect_derive_struct_edge.snap | 36 ++-- ...lect__dialect_derive_struct_no_fields.snap | 36 ++-- ...t__dialect_derive_struct_option_block.snap | 36 ++-- ...dialect__dialect_derive_struct_symbol.snap | 36 ++-- ...ect__dialect_derive_struct_terminator.snap | 36 ++-- ...__dialect_derive_struct_vec_ssa_value.snap | 36 ++-- ...t__dialect_derive_struct_with_digraph.snap | 36 ++-- ...dialect_derive_struct_with_ssa_fields.snap | 36 ++-- ...dialect_derive_struct_with_successors.snap | 36 ++-- ...t__dialect_derive_struct_with_ungraph.snap | 36 ++-- ...wrapper_struct_generates_lift_project.snap | 36 ++-- ..._struct_generates_lift_project_bridge.snap | 36 ++-- ...t_derive_wrapper_struct_has_signature.snap | 36 ++-- .../kirin-derive-ir/src/tests/standalone.rs | 8 +- .../src/ir/fields/data.rs | 12 +- .../src/ir/fields/info.rs | 6 +- .../kirin-derive-toolkit/src/ir/fields/mod.rs | 2 +- .../src/ir/statement/accessors.rs | 6 +- .../src/ir/statement/definition.rs | 4 +- .../src/parse_dispatch.rs | 2 +- .../src/template/builder_template/helpers.rs | 6 +- .../method_pattern/field_collection.rs | 9 +- .../src/template/trait_impl.rs | 8 +- crates/kirin-function/src/body.rs | 6 +- crates/kirin-function/src/call/tests.rs | 6 +- crates/kirin-function/src/interpreter.rs | 6 +- crates/kirin-function/src/lambda.rs | 10 +- crates/kirin-function/src/ret.rs | 6 +- crates/kirin-interpreter/src/core/effect.rs | 18 +- crates/kirin-interpreter/src/core/error.rs | 4 +- crates/kirin-interpreter/src/core/frame.rs | 8 +- crates/kirin-interpreter/src/core/query.rs | 50 +++--- .../src/engines/concrete/frames.rs | 17 +- .../src/engines/concrete/interp.rs | 8 +- .../src/engines/dense_backward/interp.rs | 58 +++---- .../src/engines/sparse_backward/interp.rs | 92 +++++----- .../src/engines/sparse_backward/mod.rs | 2 +- .../src/engines/sparse_forward/interp.rs | 14 +- crates/kirin-interpreter/src/facts/anchor.rs | 6 +- crates/kirin-interpreter/src/facts/mod.rs | 4 +- crates/kirin-interpreter/src/facts/store.rs | 8 +- .../kirin-interpreter/src/facts/topology.rs | 40 +++-- crates/kirin-interpreter/src/lib.rs | 8 +- crates/kirin-ir/src/builder/block.rs | 6 +- crates/kirin-ir/src/builder/cfg.rs | 16 +- crates/kirin-ir/src/builder/context.rs | 6 +- crates/kirin-ir/src/builder/mod.rs | 4 +- crates/kirin-ir/src/builder/stage_info.rs | 6 +- crates/kirin-ir/src/language.rs | 28 ++-- crates/kirin-ir/src/lib.rs | 18 +- crates/kirin-ir/src/node/block.rs | 8 +- crates/kirin-ir/src/node/cfg.rs | 28 ++-- crates/kirin-ir/src/node/mod.rs | 4 +- crates/kirin-ir/src/node/stmt.rs | 6 +- crates/kirin-ir/src/query/info.rs | 6 +- crates/kirin-ir/src/stage/arenas.rs | 14 +- crates/kirin-ir/src/stage/info.rs | 6 +- crates/kirin-ir/src/stage/tests.rs | 20 +-- crates/kirin-ir/tests/builder_block.rs | 72 ++++---- crates/kirin-ir/tests/common.rs | 12 +- crates/kirin-liveness/src/lib.rs | 20 +-- crates/kirin-liveness/src/result.rs | 12 +- crates/kirin-liveness/tests/cfg.rs | 122 +++++++------- .../kirin-prettyless/src/document/builder.rs | 2 +- .../src/document/ir_render.rs | 14 +- .../kirin-prettyless/src/tests/edge_cases.rs | 20 +-- crates/kirin-prettyless/src/tests/impls.rs | 10 +- crates/kirin-prettyless/src/tests/mod.rs | 4 +- crates/kirin-prettyless/src/tests/pipeline.rs | 6 +- .../src/tests/sprint_with_globals.rs | 2 +- crates/kirin-prettyless/src/traits.rs | 4 +- crates/kirin-scf/src/interpreter.rs | 2 +- crates/kirin-scf/src/lib.rs | 6 +- crates/kirin-scf/src/tests.rs | 6 +- .../src/arith_function_language.rs | 4 +- .../src/bitwise_function_language.rs | 4 +- .../src/callable_language.rs | 4 +- .../src/namespaced_language.rs | 4 +- .../src/simple_language.rs | 4 +- crates/kirin-tuple/src/tests.rs | 10 +- .../formalism/state-environment-model.md | 2 +- docs/design/formalism/syntax.md | 12 +- docs/design/graph-body-rust-interface.md | 4 +- docs/design/graph-ir-node.md | 2 +- docs/design/hybrid_ir_visualization.dot | 4 +- docs/design/hybrid_ir_visualization.svg | 4 +- docs/design/interpreter/index.md | 8 +- example/simple.rs | 8 +- example/toy-lang/src/interpreter/mod.rs | 28 ++-- example/toy-lang/src/interpreter/tests.rs | 158 +++++++++--------- src/dialects/scf.rs | 2 +- tests/roundtrip/composite.rs | 4 +- tests/roundtrip/constant.rs | 2 +- tests/roundtrip/digraph.rs | 20 +-- tests/roundtrip/function.rs | 4 +- tests/roundtrip/scf.rs | 2 +- tests/roundtrip/tuple.rs | 2 +- tests/simple.rs | 2 +- 130 files changed, 1141 insertions(+), 1190 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a7c5ebc42d..e0523c3a4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ Rust edition 2024. No `rust-toolchain.toml`; uses the default toolchain. Use [Conventional Commits](https://www.conventionalcommits.org/): `(): ` -Examples: `feat(chumsky): add region parser`, `fix(derive): handle empty enum variants` +Examples: `feat(chumsky): add cfg parser`, `fix(derive): handle empty enum variants` Avoid large paragraphs in commit messages, keep them concise and focused on the changes made. @@ -133,7 +133,7 @@ For user-defined dialects not in this table, ask the user for domain context dur ## IR Design Conventions -- **Block vs Region**: A `Block` is a single linear sequence of statements with an optional terminator. A `Region` is a container for multiple blocks (`LinkedList`). When modeling MLIR-style operations, check whether the MLIR op uses `SingleBlock` regions — if so, use `Block` in Kirin, not `Region`. For example, MLIR's `scf.if` and `scf.for` have `SingleBlock` + `SingleBlockImplicitTerminator` traits, so `kirin-scf` correctly uses `Block` fields for their bodies. +- **Block vs CFG**: A `Block` is a single linear sequence of statements with an optional terminator. A `Cfg` is a control-flow-graph body: a container for multiple blocks (`LinkedList`). (The `Cfg` type was originally named `Region` after MLIR, but in Kirin it is specifically the block-list CFG body — single-block bodies use `Block`, and graph-like bodies use `DiGraph`/`UnGraph`.) When modeling MLIR-style operations, check whether the MLIR op uses `SingleBlock` regions — if so, use `Block` in Kirin, not `Cfg`. For example, MLIR's `scf.if` and `scf.for` have `SingleBlock` + `SingleBlockImplicitTerminator` traits, so `kirin-scf` correctly uses `Block` fields for their bodies. - **`BlockInfo::terminator` is a cached pointer**: The `terminator` field in `BlockInfo` is a cached pointer to the last statement in the block — it is NOT a separate statement. `StatementIter` only iterates the linked list of non-terminator statements. When querying the last statement, use `Block::last_statement(stage)` which returns `terminator.or_else(|| statements.tail())`. Do not assume the terminator is distinct from the statements list. @@ -167,7 +167,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Function dialect naming**: `kirin_function::Function` is the standard function statement. New code should use `Function` with `FunctionEntry` and `SparseForwardEffect::Call`/`SparseForwardEffect::Return`. -- **Backward analyses (implemented)**: liveness ships as **two** analyses in `kirin-liveness`, both real framework clients. *Strong liveness* (`analyze_demand`, `StrongDemand`) is per-SSA-value demand: summary owners ARE scope-qualified SSA values (`Scoped<(CompileStage, Region), SSAValue>`), the driver's default self-dependent index is the demand worklist, ordinary dialects are a one-liner (`interp.demand_uses_if_observable(self)` on `DemandInterp` — purity-aware via `IsPure`: impure statements and terminator/return operands are roots), and scf needs **no frames** (loop-carried demand converges on the value worklist; the scf rules use `block_params`/`terminator_args` queries). *Classic per-point liveness* (`analyze_dense`, `ClassicLiveness`) is the textbook kill-defs/gen-all-uses transfer over block owners with backward block walks (`DenseBlockFrame`, `absorb_edges` maps successor live-ins across edges with pass-through for dominated direct cross-block uses); scf owns dense frames (`DenseScfIfFrame` arm-join, `DenseScfForFrame` loop fixpoint) composed via `DenseFrameBuild`/`BuildDenseScf*` (see toy-lang's `ToyDenseBackwardFrame`). Strong per-point sets are the composition `dense ∩ demanded`, not a third analysis. Region topology (blocks incl. nested bodies, feeders) comes from `StageQuery` actions — enumeration only; use/def/edge-arg *semantics* stay in dialect rules. One dialect carries one rule per semantic key without coherence conflicts, and two keys can share one shape — the mock compile-time proofs were removed; the shipped dialects (each carrying `ForwardEval` + `StrongDemand` + `ClassicLiveness` rules) are the living evidence. +- **Backward analyses (implemented)**: liveness ships as **two** analyses in `kirin-liveness`, both real framework clients. *Strong liveness* (`analyze_demand`, `StrongDemand`) is per-SSA-value demand: summary owners ARE scope-qualified SSA values (`Scoped<(CompileStage, Cfg), SSAValue>`), the driver's default self-dependent index is the demand worklist, ordinary dialects are a one-liner (`interp.demand_uses_if_observable(self)` on `DemandInterp` — purity-aware via `IsPure`: impure statements and terminator/return operands are roots), and scf needs **no frames** (loop-carried demand converges on the value worklist; the scf rules use `block_params`/`terminator_args` queries). *Classic per-point liveness* (`analyze_dense`, `ClassicLiveness`) is the textbook kill-defs/gen-all-uses transfer over block owners with backward block walks (`DenseBlockFrame`, `absorb_edges` maps successor live-ins across edges with pass-through for dominated direct cross-block uses); scf owns dense frames (`DenseScfIfFrame` arm-join, `DenseScfForFrame` loop fixpoint) composed via `DenseFrameBuild`/`BuildDenseScf*` (see toy-lang's `ToyDenseBackwardFrame`). Strong per-point sets are the composition `dense ∩ demanded`, not a third analysis. CFG topology (blocks incl. nested bodies, feeders) comes from `StageQuery` actions — enumeration only; use/def/edge-arg *semantics* stay in dialect rules. One dialect carries one rule per semantic key without coherence conflicts, and two keys can share one shape — the mock compile-time proofs were removed; the shipped dialects (each carrying `ForwardEval` + `StrongDemand` + `ClassicLiveness` rules) are the living evidence. ## Chumsky Parser Conventions @@ -177,7 +177,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - **`ParseDispatch` for pipeline parsing**: Multi-dialect pipeline parsing uses `ParseDispatch` (a monomorphic dispatch trait) instead of HRTB-based `SupportsStageDispatchMut`. Add `#[derive(ParseDispatch)]` alongside `#[derive(StageMeta)]` on stage enums. Single-dialect pipelines (`Pipeline>`) get a blanket `ParseDispatch` impl. Zero HRTB in the dispatch chain. -- **`#[wraps]` works with Region/Block-containing types**: Dialect types that contain `Region` or `Block` fields (e.g., `Lambda`, `Function`, SCF operations) can be composed via `#[wraps]` + `HasParser`. See `example/toy-lang/src/language.rs` where `Lexical` (contains `Function` with Region and `Lambda` with Region) and `StructuredControlFlow` (contains `If`/`For` with Block fields) are both used with `#[wraps]`. +- **`#[wraps]` works with Cfg/Block-containing types**: Dialect types that contain `Cfg` or `Block` fields (e.g., `Lambda`, `Function`, SCF operations) can be composed via `#[wraps]` + `HasParser`. See `example/toy-lang/src/language.rs` where `Lexical` (contains `Function` with a Cfg body and `Lambda` with a Cfg body) and `StructuredControlFlow` (contains `If`/`For` with Block fields) are both used with `#[wraps]`. - **`Ctx` default parameter for unified traits**: When the same trait method needs extra context for some implementors (e.g., `CompileStage` for `Pipeline`) but not others (e.g., `StageInfo`), use a default type parameter `Ctx = ()` on the trait. Pair with a blanket `Ext` trait that erases the `()` arg for ergonomic call sites. See `ParseStatementText` / `ParseStatementTextExt`. diff --git a/crates/kirin-bitwise/src/tests.rs b/crates/kirin-bitwise/src/tests.rs index 65356c1a54..7a43ba2db5 100644 --- a/crates/kirin-bitwise/src/tests.rs +++ b/crates/kirin-bitwise/src/tests.rs @@ -1,5 +1,5 @@ use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, TestSSAValue, }; use kirin_test_types::UnitType; @@ -132,7 +132,7 @@ fn all_have_one_result() { } } -// --- HasSuccessors / HasBlocks / HasRegions: all empty --- +// --- HasSuccessors / HasBlocks / HasCfgs: all empty --- #[test] fn no_successors() { @@ -149,9 +149,9 @@ fn no_blocks() { } #[test] -fn no_regions() { +fn no_cfgs() { for op in all_variants() { - assert_eq!(op.regions().count(), 0); + assert_eq!(op.cfgs().count(), 0); } } diff --git a/crates/kirin-cf/src/tests.rs b/crates/kirin-cf/src/tests.rs index 419ea5e36b..9fe6dc3bf8 100644 --- a/crates/kirin-cf/src/tests.rs +++ b/crates/kirin-cf/src/tests.rs @@ -1,5 +1,5 @@ use kirin::ir::{ - Block, HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + Block, HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, Successor, TestSSAValue, }; use kirin_test_types::UnitType; @@ -133,7 +133,7 @@ fn cond_branch_has_two_successors() { assert_eq!(succs.len(), 2); } -// --- HasBlocks / HasRegions: empty --- +// --- HasBlocks / HasCfgs: empty --- #[test] fn no_blocks() { @@ -142,9 +142,9 @@ fn no_blocks() { } #[test] -fn no_regions() { - assert_eq!(make_branch().regions().count(), 0); - assert_eq!(make_cond_branch().regions().count(), 0); +fn no_cfgs() { + assert_eq!(make_branch().cfgs().count(), 0); + assert_eq!(make_cond_branch().cfgs().count(), 0); } // --- Clone + PartialEq --- diff --git a/crates/kirin-chumsky/src/ast/blocks.rs b/crates/kirin-chumsky/src/ast/blocks.rs index aad1d88af5..dca789a2bb 100644 --- a/crates/kirin-chumsky/src/ast/blocks.rs +++ b/crates/kirin-chumsky/src/ast/blocks.rs @@ -65,7 +65,7 @@ pub struct Block<'src, TypeOutput, StmtOutput> { pub statements: Vec>, } -/// A region containing multiple blocks. +/// A CFG containing multiple blocks. /// /// Represents syntax like: /// ```ignore @@ -78,8 +78,8 @@ pub struct Block<'src, TypeOutput, StmtOutput> { /// The `TypeOutput` parameter is the parsed type representation. /// The `StmtOutput` parameter is the parsed statement representation. #[derive(Debug, Clone, PartialEq)] -pub struct Region<'src, TypeOutput, StmtOutput> { - /// The blocks in the region. +pub struct Cfg<'src, TypeOutput, StmtOutput> { + /// The blocks in the CFG. pub blocks: Vec>>, } @@ -102,7 +102,7 @@ where } /// Emit a single block AST node into the IR, reusing an existing block ID if -/// the name was already registered (e.g. by a two-pass Region emit). +/// the name was already registered (e.g. by a two-pass Cfg emit). /// /// Uses a two-phase approach: first creates the block with its arguments (to /// get real `BlockArgument` SSAs), then emits statements and attaches them. @@ -182,7 +182,7 @@ where ctx.stage .attach_statements_to_block(block, &stmts, terminator); - // Register the block only if not already registered (two-pass Region + // Register the block only if not already registered (two-pass Cfg // creates stubs first, so the name may already be present). if let Some(label) = block_ast.label && ctx.lookup_block(label.value).is_none() @@ -230,7 +230,7 @@ where } } -impl<'src, TypeOutput, StmtOutput> Region<'src, TypeOutput, StmtOutput> { +impl<'src, TypeOutput, StmtOutput> Cfg<'src, TypeOutput, StmtOutput> { pub fn emit_with( &self, ctx: &mut EmitContext<'_, IR>, @@ -238,7 +238,7 @@ impl<'src, TypeOutput, StmtOutput> Region<'src, TypeOutput, StmtOutput> { &StmtOutput, &mut EmitContext<'ctx, IR>, ) -> Result, - ) -> Result + ) -> Result where IR: Dialect, TypeOutput: EmitIR, @@ -280,8 +280,8 @@ impl<'src, TypeOutput, StmtOutput> Region<'src, TypeOutput, StmtOutput> { // Drop scope guard — inner names are discarded. drop(guard); - // Build the region using the stub IDs (now containing real data). - let mut builder = ctx.stage.region(); + // Build the cfg using the stub IDs (now containing real data). + let mut builder = ctx.stage.cfg(); for block in stub_blocks { builder = builder.add_block(block); } @@ -289,21 +289,21 @@ impl<'src, TypeOutput, StmtOutput> Region<'src, TypeOutput, StmtOutput> { } } -/// Implementation of EmitIR for Region AST nodes. +/// Implementation of EmitIR for Cfg AST nodes. /// -/// This builds an IR region containing all the parsed blocks. +/// This builds an IR CFG containing all the parsed blocks. /// Uses two-pass emit to support forward block references (e.g. `br ^exit` /// before `^exit` is defined). /// /// The `TypeOutput: EmitIR` bound allows proper type -/// conversion for block arguments within the region via the EmitIR trait. -impl<'src, TypeOutput, StmtOutput, IR> EmitIR for Region<'src, TypeOutput, StmtOutput> +/// conversion for block arguments within the CFG via the EmitIR trait. +impl<'src, TypeOutput, StmtOutput, IR> EmitIR for Cfg<'src, TypeOutput, StmtOutput> where IR: Dialect, TypeOutput: EmitIR, StmtOutput: EmitIR, { - type Output = kirin_ir::Region; + type Output = kirin_ir::Cfg; fn emit(&self, ctx: &mut EmitContext<'_, IR>) -> Result { self.emit_with(ctx, &|stmt, ctx| stmt.emit(ctx)) diff --git a/crates/kirin-chumsky/src/function_text/dispatch.rs b/crates/kirin-chumsky/src/function_text/dispatch.rs index 15426dd41c..34dd094e1a 100644 --- a/crates/kirin-chumsky/src/function_text/dispatch.rs +++ b/crates/kirin-chumsky/src/function_text/dispatch.rs @@ -3,7 +3,7 @@ //! [`ParseDispatch`] replaces the HRTB-based `SupportsStageDispatchMut` path //! for pipeline text parsing. Each stage enum variant dispatches to a concrete //! dialect parser with concrete lifetimes, which avoids the E0275 trait-solver -//! overflow that occurs when `Block`/`Region`-containing types use `#[wraps]`. +//! overflow that occurs when `Block`/`Cfg`-containing types use `#[wraps]`. //! //! For single-dialect pipelines (`Pipeline>`), a blanket impl is //! provided so no derive macro is needed. diff --git a/crates/kirin-chumsky/src/function_text/syntax.rs b/crates/kirin-chumsky/src/function_text/syntax.rs index d9e5a872b2..94110b3bfe 100644 --- a/crates/kirin-chumsky/src/function_text/syntax.rs +++ b/crates/kirin-chumsky/src/function_text/syntax.rs @@ -70,7 +70,7 @@ where } /// Body span scanner. Matches an optional keyword prefix (e.g. `digraph`, -/// `ungraph`) followed by a brace-balanced `{ ... }` region. Returns the +/// `ungraph`) followed by a brace-balanced `{ ... }` CFG. Returns the /// span covering everything from the first non-brace token (or the opening /// brace) through the matching closing brace. Does not parse body contents. fn body_span<'src, I>() -> impl Parser<'src, I, SimpleSpan, ParserError<'src>> diff --git a/crates/kirin-chumsky/src/function_text/tests.rs b/crates/kirin-chumsky/src/function_text/tests.rs index 49e86a9ce3..de72f1bada 100644 --- a/crates/kirin-chumsky/src/function_text/tests.rs +++ b/crates/kirin-chumsky/src/function_text/tests.rs @@ -2,8 +2,8 @@ use std::collections::BTreeSet; use chumsky::prelude::*; use kirin_ir::{ - Function, FunctionInfo, GlobalSymbol, HasBottom, HasTop, InternTable, Lattice, Pipeline, - Placeholder, Region, Signature, StageInfo, TypeLattice, + Cfg, Function, FunctionInfo, GlobalSymbol, HasBottom, HasTop, InternTable, Lattice, Pipeline, + Placeholder, Signature, StageInfo, TypeLattice, }; use kirin_prettyless::PrintExt; @@ -89,7 +89,7 @@ trivial_type_lattice!(I32Type, "i32", just(Token::Identifier("i32"))); #[kirin(builders, type = UnitType, crate = kirin_ir)] #[chumsky(crate = crate, format = "fn {:name}{sig} {body}")] struct FunctionBody { - body: Region, + body: Cfg, sig: Signature, } @@ -97,7 +97,7 @@ struct FunctionBody { #[kirin(builders, type = I32Type, crate = kirin_ir)] #[chumsky(crate = crate, format = "fn {:name}{sig} {body}")] struct LowerBody { - body: Region, + body: Cfg, sig: Signature, } @@ -286,8 +286,8 @@ fn test_pipeline_roundtrip_print_parse_print() { pipeline.stage_mut(stage_a).unwrap().with_builder(|b| { let block = b.block().new(); - let region = b.region().add_block(block).new(); - let body = FunctionBody::new(b, region, Signature::new(vec![], UnitType, ())); + let cfg = b.cfg().add_block(block).new(); + let body = FunctionBody::new(b, cfg, Signature::new(vec![], UnitType, ())); b.specialize() .staged_func(staged_function) .signature(unit_sig()) diff --git a/crates/kirin-chumsky/src/parsers/blocks.rs b/crates/kirin-chumsky/src/parsers/blocks.rs index 60e42c138e..6025f10763 100644 --- a/crates/kirin-chumsky/src/parsers/blocks.rs +++ b/crates/kirin-chumsky/src/parsers/blocks.rs @@ -162,7 +162,7 @@ where }) } -/// Parses a region containing multiple blocks. +/// Parses a CFG containing multiple blocks. /// /// Matches: /// ```text @@ -176,10 +176,10 @@ where /// /// The type parameter `T` specifies the type annotation type (typically the TypeLattice). /// The type parameter `S` is the statement AST type produced by the language parser. -/// The parser produces `Region<'t, ::Output, S>`. -pub fn region<'t, I, T, S>( +/// The parser produces `Cfg<'t, ::Output, S>`. +pub fn cfg<'t, I, T, S>( language: RecursiveParser<'t, I, S>, -) -> impl Parser<'t, I, Region<'t, >::Output, S>, ParserError<'t>> +) -> impl Parser<'t, I, Cfg<'t, >::Output, S>, ParserError<'t>> where I: TokenInput<'t>, T: HasParser<'t>, @@ -190,8 +190,8 @@ where .repeated() .collect::>() .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .map(|blocks| Region { blocks }) - .labelled("region") + .map(|blocks| Cfg { blocks }) + .labelled("cfg") } /// Parses block body statements (without header, without braces). @@ -217,12 +217,12 @@ where .labelled("block body statements") } -/// Parses region body (blocks without outer braces). +/// Parses CFG body (blocks without outer braces). /// /// Matches a sequence of blocks, each optionally terminated by a semicolon. -/// This is the inner content of a region, used for `:body` projections on -/// Region fields where the caller provides surrounding syntax via the format string. -pub fn region_body<'t, I, T, S>( +/// This is the inner content of a CFG, used for `:body` projections on +/// Cfg fields where the caller provides surrounding syntax via the format string. +pub fn cfg_body<'t, I, T, S>( language: RecursiveParser<'t, I, S>, ) -> impl Parser<'t, I, Vec>::Output, S>>>, ParserError<'t>> where @@ -234,5 +234,5 @@ where .then_ignore(just(Token::Semicolon).or_not()) .repeated() .collect::>() - .labelled("region body") + .labelled("cfg body") } diff --git a/crates/kirin-chumsky/src/traits/emit_ir.rs b/crates/kirin-chumsky/src/traits/emit_ir.rs index f73f6f40f3..d40898fffb 100644 --- a/crates/kirin-chumsky/src/traits/emit_ir.rs +++ b/crates/kirin-chumsky/src/traits/emit_ir.rs @@ -42,7 +42,7 @@ type ForwardRefCreator = fn(&mut BuilderStageInfo, &str) -> kirin_ir::SSAV /// Context for emitting IR from parsed AST, tracking name mappings. /// /// The `stage` field is a `&mut BuilderStageInfo` since emit is a build-time -/// operation that needs access to builder methods (block, region, ssa, etc.). +/// operation that needs access to builder methods (block, cfg, ssa, etc.). pub struct EmitContext<'a, L: Dialect> { pub stage: &'a mut BuilderStageInfo, /// Scope stack for SSA name bindings. Inner scopes shadow outer ones. diff --git a/crates/kirin-chumsky/src/traits/has_parser.rs b/crates/kirin-chumsky/src/traits/has_parser.rs index 3760c2c3be..bb7687eecd 100644 --- a/crates/kirin-chumsky/src/traits/has_parser.rs +++ b/crates/kirin-chumsky/src/traits/has_parser.rs @@ -33,7 +33,7 @@ pub trait HasParser<'t> { /// /// This trait provides recursive parsing capabilities for dialects. /// The AST type is parameterized by `TypeOutput` (for type annotations) and -/// `LanguageOutput` (for nested statements in blocks/regions). +/// `LanguageOutput` (for nested statements in blocks/cfgs). /// /// Using explicit type parameters instead of GAT projections avoids infinite /// compilation times when the Language type is self-referential. @@ -43,7 +43,7 @@ pub trait HasDialectParser<'t>: Sized { /// The AST type produced by parsing this dialect. /// /// - `TypeOutput`: The parsed representation of type annotations - /// - `LanguageOutput`: The AST type for statements in blocks/regions + /// - `LanguageOutput`: The AST type for statements in blocks/cfgs type Output: Clone + PartialEq where TypeOutput: Clone + PartialEq + 't, diff --git a/crates/kirin-chumsky/src/traits/parse_emit.rs b/crates/kirin-chumsky/src/traits/parse_emit.rs index d9107a0bf1..fb0596280b 100644 --- a/crates/kirin-chumsky/src/traits/parse_emit.rs +++ b/crates/kirin-chumsky/src/traits/parse_emit.rs @@ -64,15 +64,15 @@ impl From for ChumskyError { /// /// 1. **Derive**: `#[derive(HasParser)]` generates this automatically. /// 2. **Marker**: Implement `SimpleParseEmit` for non-recursive dialects -/// (no `Block`/`Region` fields) to get a blanket impl for free. +/// (no `Block`/`Cfg` fields) to get a blanket impl for free. /// 3. **Manual**: Implement directly for full control over parse+emit. /// /// # Decision table /// /// | Dialect characteristics | Recommended path | /// |------------------------------------------|--------------------------| -/// | Has `Block`, `Region`, or `DiGraph` fields | `#[derive(HasParser)]` | -/// | No `Block`/`Region`/`DiGraph`, no recursion | `impl SimpleParseEmit` | +/// | Has `Block`, `Cfg`, or `DiGraph` fields | `#[derive(HasParser)]` | +/// | No `Block`/`Cfg`/`DiGraph`, no recursion | `impl SimpleParseEmit` | /// | Custom parse logic or non-standard emit | `impl ParseEmit` manually | pub trait ParseEmit: Dialect { /// Parse input text and emit a single IR statement. diff --git a/crates/kirin-cmp/src/tests.rs b/crates/kirin-cmp/src/tests.rs index 925520c80b..0c0288210e 100644 --- a/crates/kirin-cmp/src/tests.rs +++ b/crates/kirin-cmp/src/tests.rs @@ -1,5 +1,5 @@ use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, TestSSAValue, }; use kirin_test_types::UnitType; @@ -141,13 +141,13 @@ fn no_blocks() { } } -// --- HasRegions: no regions --- +// --- HasCfgs: no cfgs --- #[test] -fn no_regions() { +fn no_cfgs() { for op in all_variants() { - let regions: Vec<_> = op.regions().collect(); - assert_eq!(regions.len(), 0, "expected 0 regions for {op:?}"); + let cfgs: Vec<_> = op.cfgs().collect(); + assert_eq!(cfgs.len(), 0, "expected 0 cfgs for {op:?}"); } } diff --git a/crates/kirin-constant/src/tests.rs b/crates/kirin-constant/src/tests.rs index 3257d8bd7a..cc983b142b 100644 --- a/crates/kirin-constant/src/tests.rs +++ b/crates/kirin-constant/src/tests.rs @@ -1,6 +1,6 @@ use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, - IsTerminator, TestSSAValue, Typeof, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsTerminator, + TestSSAValue, Typeof, }; use kirin::pretty::{ArenaDoc, DocAllocator, Document, PrettyPrint}; @@ -92,7 +92,7 @@ fn one_result() { assert_eq!(c.results().count(), 1); } -// --- HasSuccessors / HasBlocks / HasRegions: all empty --- +// --- HasSuccessors / HasBlocks / HasCfgs: all empty --- #[test] fn no_successors() { @@ -105,8 +105,8 @@ fn no_blocks() { } #[test] -fn no_regions() { - assert_eq!(make_constant(0).regions().count(), 0); +fn no_cfgs() { + assert_eq!(make_constant(0).cfgs().count(), 0); } // --- Clone + PartialEq --- diff --git a/crates/kirin-derive-chumsky/src/codegen/emit_ir/field_emit.rs b/crates/kirin-derive-chumsky/src/codegen/emit_ir/field_emit.rs index ce55096152..d8f6bfa011 100644 --- a/crates/kirin-derive-chumsky/src/codegen/emit_ir/field_emit.rs +++ b/crates/kirin-derive-chumsky/src/codegen/emit_ir/field_emit.rs @@ -55,7 +55,7 @@ impl GenerateEmitIR { FieldCategory::Block => quote! { let #emitted_var = #var.value.emit_with(ctx, emit_language_output)?; }, - FieldCategory::Region => quote! { + FieldCategory::Cfg => quote! { let #emitted_var = #var.emit_with(ctx, emit_language_output)?; }, FieldCategory::DiGraph => quote! { diff --git a/crates/kirin-derive-chumsky/src/codegen/emit_ir/generate.rs b/crates/kirin-derive-chumsky/src/codegen/emit_ir/generate.rs index a15e9142de..54b41133d5 100644 --- a/crates/kirin-derive-chumsky/src/codegen/emit_ir/generate.rs +++ b/crates/kirin-derive-chumsky/src/codegen/emit_ir/generate.rs @@ -117,7 +117,7 @@ impl GenerateEmitIR { matches!( field.category(), FieldCategory::Block - | FieldCategory::Region + | FieldCategory::Cfg | FieldCategory::DiGraph | FieldCategory::UnGraph ) @@ -132,7 +132,7 @@ impl GenerateEmitIR { matches!( field.category(), FieldCategory::Block - | FieldCategory::Region + | FieldCategory::Cfg | FieldCategory::DiGraph | FieldCategory::UnGraph ) diff --git a/crates/kirin-derive-chumsky/src/codegen/parser/chain.rs b/crates/kirin-derive-chumsky/src/codegen/parser/chain.rs index fae28f10f8..a580779f56 100644 --- a/crates/kirin-derive-chumsky/src/codegen/parser/chain.rs +++ b/crates/kirin-derive-chumsky/src/codegen/parser/chain.rs @@ -662,14 +662,14 @@ impl GenerateHasDialectParser { } } } - FieldCategory::Region => { - // region_body returns Vec> + FieldCategory::Cfg => { + // cfg_body returns Vec> let blocks_expr = find_var(BodyProjection::Body) .map(|v| quote! { #v }) .unwrap_or_else(|| quote! { ::std::vec::Vec::new() }); quote! { - #crate_path::Region { + #crate_path::Cfg { blocks: #blocks_expr, } } diff --git a/crates/kirin-derive-chumsky/src/field_kind.rs b/crates/kirin-derive-chumsky/src/field_kind.rs index ee988103b2..767406976c 100644 --- a/crates/kirin-derive-chumsky/src/field_kind.rs +++ b/crates/kirin-derive-chumsky/src/field_kind.rs @@ -26,7 +26,7 @@ impl FieldCategoryExt for FieldCategory { FieldCategory::Result => "result_value", FieldCategory::Block => "block", FieldCategory::Successor => "successor", - FieldCategory::Region => "region", + FieldCategory::Cfg => "cfg", FieldCategory::Symbol => "symbol", FieldCategory::Value => "value", FieldCategory::DiGraph => "digraph", @@ -66,8 +66,8 @@ pub fn ast_type( FieldCategory::Successor => { quote! { #crate_path::BlockLabel<'t> } } - FieldCategory::Region => { - quote! { #crate_path::Region<'t, #type_output, LanguageOutput> } + FieldCategory::Cfg => { + quote! { #crate_path::Cfg<'t, #type_output, LanguageOutput> } } FieldCategory::Symbol => { quote! { #crate_path::SymbolName<'t> } @@ -141,14 +141,14 @@ pub fn parser_expr( FieldCategory::Successor => { quote! { #crate_path::block_label() } } - FieldCategory::Region => match opt { + FieldCategory::Cfg => match opt { FormatOption::Default => { - quote! { #crate_path::region::<_, #ir_type, _>(language.clone()) } + quote! { #crate_path::cfg::<_, #ir_type, _>(language.clone()) } } FormatOption::Body(BodyProjection::Body) => { - quote! { #crate_path::region_body::<_, #ir_type, _>(language.clone()) } + quote! { #crate_path::cfg_body::<_, #ir_type, _>(language.clone()) } } - _ => unreachable!("validation prevents other projections on Region fields"), + _ => unreachable!("validation prevents other projections on Cfg fields"), }, FieldCategory::Symbol => { quote! { #crate_path::symbol() } @@ -269,18 +269,16 @@ pub fn print_expr( FieldCategory::Successor => quote! { #prettyless_path::PrettyPrint::pretty_print(#field_ref, doc) }, - FieldCategory::Region => match opt { - FormatOption::Default => quote! { doc.print_region(#field_ref) }, + FieldCategory::Cfg => match opt { + FormatOption::Default => quote! { doc.print_cfg(#field_ref) }, FormatOption::Body(BodyProjection::Body) => quote! { - doc.print_region_body_only(#field_ref) + doc.print_cfg_body_only(#field_ref) }, FormatOption::Body(_) => { - unreachable!( - "Ports/Captures/Yields/Args projections are not valid on Region fields" - ) + unreachable!("Ports/Captures/Yields/Args projections are not valid on Cfg fields") } FormatOption::Name | FormatOption::Type | FormatOption::Signature(_) => { - unreachable!("Name/Type/Signature projections are not valid on Region fields") + unreachable!("Name/Type/Signature projections are not valid on Cfg fields") } }, FieldCategory::Symbol => quote! { diff --git a/crates/kirin-derive-chumsky/src/format.rs b/crates/kirin-derive-chumsky/src/format.rs index 19024e90bc..d9e5cc3685 100644 --- a/crates/kirin-derive-chumsky/src/format.rs +++ b/crates/kirin-derive-chumsky/src/format.rs @@ -40,7 +40,7 @@ //! | Result | -- | -- | yes | | | | | | | //! | Block | yes | | | | | yes | yes | | | //! | Successor | yes | | | | | | | | | -//! | Region | yes | | | | | | yes | | | +//! | Cfg | yes | | | | | | yes | | | //! | Symbol | yes | | | | | | | | | //! | Value | yes | | | | | | | | | //! | DiGraph | yes | | | yes | yes | | yes | | | diff --git a/crates/kirin-derive-chumsky/src/input.rs b/crates/kirin-derive-chumsky/src/input.rs index 6625f8919c..ffc3a6441c 100644 --- a/crates/kirin-derive-chumsky/src/input.rs +++ b/crates/kirin-derive-chumsky/src/input.rs @@ -7,7 +7,7 @@ use crate::{ChumskyLayout, PrettyPrintLayout}; /// Parses derive input for chumsky macros. /// /// For value-only definitions, `#[kirin(type = ...)]` is optional. -/// For dialect-like definitions using SSA/Result/Block/Region fields, +/// For dialect-like definitions using SSA/Result/Block/Cfg fields, /// `#[kirin(type = ...)]` remains required. pub fn parse_derive_input( ast: &syn::DeriveInput, @@ -26,7 +26,7 @@ pub fn parse_derive_input( if input_requires_ir_type(&input) { return Err(darling::Error::custom( - "`#[kirin(type = ...)]` is required when using SSAValue, ResultValue, Block, or Region fields", + "`#[kirin(type = ...)]` is required when using SSAValue, ResultValue, Block, or Cfg fields", ) .with_span(&ast.ident)); } @@ -59,7 +59,7 @@ fn statement_requires_ir_type( FieldCategory::Argument | FieldCategory::Result | FieldCategory::Block - | FieldCategory::Region + | FieldCategory::Cfg ) }) } @@ -83,7 +83,7 @@ pub fn parse_pretty_derive_input( if input_requires_ir_type(&input) { return Err(darling::Error::custom( - "`#[kirin(type = ...)]` is required when using SSAValue, ResultValue, Block, or Region fields", + "`#[kirin(type = ...)]` is required when using SSAValue, ResultValue, Block, or Cfg fields", ) .with_span(&ast.ident)); } diff --git a/crates/kirin-derive-chumsky/src/validation.rs b/crates/kirin-derive-chumsky/src/validation.rs index 3ecd863c0d..28601c1127 100644 --- a/crates/kirin-derive-chumsky/src/validation.rs +++ b/crates/kirin-derive-chumsky/src/validation.rs @@ -350,7 +350,7 @@ impl<'ir> ValidationVisitor<'ir> { FieldCategory::Block => { (&[BodyProjection::Args, BodyProjection::Body], "Block") } - FieldCategory::Region => (&[BodyProjection::Body], "Region"), + FieldCategory::Cfg => (&[BodyProjection::Body], "Cfg"), _ => continue, }; let missing: Vec<&str> = required @@ -492,7 +492,7 @@ impl<'ir> FormatVisitor<'ir> for ValidationVisitor<'ir> { category, FieldCategory::DiGraph | FieldCategory::UnGraph - | FieldCategory::Region + | FieldCategory::Cfg | FieldCategory::Block ) } @@ -508,7 +508,7 @@ impl<'ir> FormatVisitor<'ir> for ValidationVisitor<'ir> { let valid_on = match proj { BodyProjection::Ports | BodyProjection::Captures => "DiGraph or UnGraph", BodyProjection::Args => "Block", - BodyProjection::Body => "DiGraph, UnGraph, Region, or Block", + BodyProjection::Body => "DiGraph, UnGraph, Cfg, or Block", }; self.add_error(format!( "'{}' projection is only valid on {} fields, but '{}' is a {} field", @@ -763,12 +763,12 @@ mod tests { } } - fn make_region(index: usize, name: &str) -> FieldInfo { + fn make_cfg(index: usize, name: &str) -> FieldInfo { FieldInfo { index, ident: Some(syn::Ident::new(name, proc_macro2::Span::call_site())), collection: Collection::Single, - data: FieldData::Region, + data: FieldData::Cfg, } } @@ -808,8 +808,8 @@ mod tests { } #[test] - fn body_projection_on_region_is_valid() { - let fields = vec![make_region(0, "body")]; + fn body_projection_on_cfg_is_valid() { + let fields = vec![make_cfg(0, "body")]; let stmt = make_stmt(fields.clone()); let format = Format::parse("{body:body}", None).unwrap(); assert!(validate_format(&stmt, &format, &fields).is_ok()); @@ -824,8 +824,8 @@ mod tests { } #[test] - fn ports_projection_on_region_is_invalid() { - let fields = vec![make_region(0, "body")]; + fn ports_projection_on_cfg_is_invalid() { + let fields = vec![make_cfg(0, "body")]; let stmt = make_stmt(fields.clone()); let format = Format::parse("{body:ports}", None).unwrap(); let err = validate_format(&stmt, &format, &fields).unwrap_err(); @@ -837,8 +837,8 @@ mod tests { } #[test] - fn args_projection_on_region_is_invalid() { - let fields = vec![make_region(0, "body")]; + fn args_projection_on_cfg_is_invalid() { + let fields = vec![make_cfg(0, "body")]; let stmt = make_stmt(fields.clone()); let format = Format::parse("{body:args}", None).unwrap(); let err = validate_format(&stmt, &format, &fields).unwrap_err(); diff --git a/crates/kirin-derive-ir/src/generate.rs b/crates/kirin-derive-ir/src/generate.rs index 8485cd740d..f1ae211c28 100644 --- a/crates/kirin-derive-ir/src/generate.rs +++ b/crates/kirin-derive-ir/src/generate.rs @@ -83,20 +83,20 @@ pub(crate) const HAS_SUCCESSORS_MUT: FieldIterConfig = FieldIterConfig { trait_method: "successors_mut", trait_type_iter: "IterMut", }; -pub(crate) const HAS_REGIONS: FieldIterConfig = FieldIterConfig { - kind: FieldIterKind::Regions, +pub(crate) const HAS_CFGS: FieldIterConfig = FieldIterConfig { + kind: FieldIterKind::Cfgs, mutable: false, - trait_name: "HasRegions", - matching_type: "Region", - trait_method: "regions", + trait_name: "HasCfgs", + matching_type: "Cfg", + trait_method: "cfgs", trait_type_iter: "Iter", }; -pub(crate) const HAS_REGIONS_MUT: FieldIterConfig = FieldIterConfig { - kind: FieldIterKind::Regions, +pub(crate) const HAS_CFGS_MUT: FieldIterConfig = FieldIterConfig { + kind: FieldIterKind::Cfgs, mutable: true, - trait_name: "HasRegionsMut", - matching_type: "Region", - trait_method: "regions_mut", + trait_name: "HasCfgsMut", + matching_type: "Cfg", + trait_method: "cfgs_mut", trait_type_iter: "IterMut", }; pub(crate) const HAS_DIGRAPHS: FieldIterConfig = FieldIterConfig { @@ -141,8 +141,8 @@ pub(crate) const FIELD_ITER_CONFIGS: [FieldIterConfig; 14] = [ HAS_BLOCKS_MUT, HAS_SUCCESSORS, HAS_SUCCESSORS_MUT, - HAS_REGIONS, - HAS_REGIONS_MUT, + HAS_CFGS, + HAS_CFGS_MUT, HAS_DIGRAPHS, HAS_DIGRAPHS_MUT, HAS_UNGRAPHS, diff --git a/crates/kirin-derive-ir/src/lib.rs b/crates/kirin-derive-ir/src/lib.rs index aed2b5ce32..40765ce67d 100644 --- a/crates/kirin-derive-ir/src/lib.rs +++ b/crates/kirin-derive-ir/src/lib.rs @@ -76,8 +76,8 @@ derive_field_iter_macro!( HasSuccessorsMut, HAS_SUCCESSORS_MUT ); -derive_field_iter_macro!(derive_has_regions, HasRegions, HAS_REGIONS); -derive_field_iter_macro!(derive_has_regions_mut, HasRegionsMut, HAS_REGIONS_MUT); +derive_field_iter_macro!(derive_has_cfgs, HasCfgs, HAS_CFGS); +derive_field_iter_macro!(derive_has_cfgs_mut, HasCfgsMut, HAS_CFGS_MUT); derive_field_iter_macro!(derive_has_digraphs, HasDigraphs, HAS_DIGRAPHS); derive_field_iter_macro!(derive_has_digraphs_mut, HasDigraphsMut, HAS_DIGRAPHS_MUT); derive_field_iter_macro!(derive_has_ungraphs, HasUngraphs, HAS_UNGRAPHS); diff --git a/crates/kirin-derive-ir/src/tests/dialect.rs b/crates/kirin-derive-ir/src/tests/dialect.rs index aa03ceb808..b11b7d7a9e 100644 --- a/crates/kirin-derive-ir/src/tests/dialect.rs +++ b/crates/kirin-derive-ir/src/tests/dialect.rs @@ -27,14 +27,14 @@ fn test_dialect_derive_struct_with_ssa_fields() { } #[test] -fn test_dialect_derive_struct_with_region_block() { +fn test_dialect_derive_struct_with_cfg_block() { let input: syn::DeriveInput = syn::parse_quote! { #[kirin(type = SimpleType)] struct IfOp { condition: Value, then_block: Block, else_block: Block, - body: Region, + body: Cfg, } }; insta::assert_snapshot!(generate_dialect_code(input)); diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_custom_crate_path.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_custom_crate_path.snap index 161ac18cdc..27cc8e1bdc 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_custom_crate_path.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_custom_crate_path.snap @@ -171,43 +171,43 @@ impl<'a> Iterator for NopSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> kirin_ir::HasRegions<'a> for Nop { - type Iter = NopRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { - NopRegionsIter { - inner: std::iter::empty::<&'a kirin_ir::Region>(), +impl<'a> kirin_ir::HasCfgs<'a> for Nop { + type Iter = NopCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { + NopCfgsIter { + inner: std::iter::empty::<&'a kirin_ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct NopRegionsIter<'a> { - inner: std::iter::Empty<&'a kirin_ir::Region>, +pub struct NopCfgsIter<'a> { + inner: std::iter::Empty<&'a kirin_ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for NopRegionsIter<'a> { - type Item = &'a kirin_ir::Region; +impl<'a> Iterator for NopCfgsIter<'a> { + type Item = &'a kirin_ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> kirin_ir::HasRegionsMut<'a> for Nop { - type IterMut = NopRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { - NopRegionsMutIter { - inner: std::iter::empty::<&'a mut kirin_ir::Region>(), +impl<'a> kirin_ir::HasCfgsMut<'a> for Nop { + type IterMut = NopCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { + NopCfgsMutIter { + inner: std::iter::empty::<&'a mut kirin_ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct NopRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut kirin_ir::Region>, +pub struct NopCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut kirin_ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for NopRegionsMutIter<'a> { - type Item = &'a mut kirin_ir::Region; +impl<'a> Iterator for NopCfgsMutIter<'a> { + type Item = &'a mut kirin_ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_does_not_generate_lift_project.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_does_not_generate_lift_project.snap index 237570a66f..56e07774fa 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_does_not_generate_lift_project.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_does_not_generate_lift_project.snap @@ -243,28 +243,28 @@ impl<'a> Iterator for MixedOpsSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for MixedOps { - type Iter = MixedOpsRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for MixedOps { + type Iter = MixedOpsCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { Self::Add(field_0) => { - MixedOpsRegionsIter::Add(::regions(field_0)) + MixedOpsCfgsIter::Add(::cfgs(field_0)) } Self::Literal { value } => { - MixedOpsRegionsIter::Literal(std::iter::empty::<&'a ::kirin::ir::Region>()) + MixedOpsCfgsIter::Literal(std::iter::empty::<&'a ::kirin::ir::Cfg>()) } } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedOpsRegionsIter<'a> { - Add(>::Iter), - Literal(std::iter::Empty<&'a ::kirin::ir::Region>), +pub enum MixedOpsCfgsIter<'a> { + Add(>::Iter), + Literal(std::iter::Empty<&'a ::kirin::ir::Cfg>), } #[automatically_derived] -impl<'a> Iterator for MixedOpsRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for MixedOpsCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Add(inner) => inner.next(), @@ -273,28 +273,28 @@ impl<'a> Iterator for MixedOpsRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for MixedOps { - type IterMut = MixedOpsRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for MixedOps { + type IterMut = MixedOpsCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Add(field_0) => MixedOpsRegionsMutIter::Add( - ::regions_mut(field_0), - ), + Self::Add(field_0) => { + MixedOpsCfgsMutIter::Add(::cfgs_mut(field_0)) + } Self::Literal { value } => { - MixedOpsRegionsMutIter::Literal(std::iter::empty::<&'a mut ::kirin::ir::Region>()) + MixedOpsCfgsMutIter::Literal(std::iter::empty::<&'a mut ::kirin::ir::Cfg>()) } } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedOpsRegionsMutIter<'a> { - Add(>::IterMut), - Literal(std::iter::Empty<&'a mut ::kirin::ir::Region>), +pub enum MixedOpsCfgsMutIter<'a> { + Add(>::IterMut), + Literal(std::iter::Empty<&'a mut ::kirin::ir::Cfg>), } #[automatically_derived] -impl<'a> Iterator for MixedOpsRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for MixedOpsCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Add(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_wraps_and_fields.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_wraps_and_fields.snap index 398e15a1a3..90fee95a67 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_wraps_and_fields.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_mixed_wraps_and_fields.snap @@ -243,28 +243,28 @@ impl<'a> Iterator for MixedOpsSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for MixedOps { - type Iter = MixedOpsRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for MixedOps { + type Iter = MixedOpsCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { Self::Add(field_0) => { - MixedOpsRegionsIter::Add(::regions(field_0)) + MixedOpsCfgsIter::Add(::cfgs(field_0)) } Self::Literal { value } => { - MixedOpsRegionsIter::Literal(std::iter::empty::<&'a ::kirin::ir::Region>()) + MixedOpsCfgsIter::Literal(std::iter::empty::<&'a ::kirin::ir::Cfg>()) } } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedOpsRegionsIter<'a> { - Add(>::Iter), - Literal(std::iter::Empty<&'a ::kirin::ir::Region>), +pub enum MixedOpsCfgsIter<'a> { + Add(>::Iter), + Literal(std::iter::Empty<&'a ::kirin::ir::Cfg>), } #[automatically_derived] -impl<'a> Iterator for MixedOpsRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for MixedOpsCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Add(inner) => inner.next(), @@ -273,28 +273,28 @@ impl<'a> Iterator for MixedOpsRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for MixedOps { - type IterMut = MixedOpsRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for MixedOps { + type IterMut = MixedOpsCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Add(field_0) => MixedOpsRegionsMutIter::Add( - ::regions_mut(field_0), - ), + Self::Add(field_0) => { + MixedOpsCfgsMutIter::Add(::cfgs_mut(field_0)) + } Self::Literal { value } => { - MixedOpsRegionsMutIter::Literal(std::iter::empty::<&'a mut ::kirin::ir::Region>()) + MixedOpsCfgsMutIter::Literal(std::iter::empty::<&'a mut ::kirin::ir::Cfg>()) } } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedOpsRegionsMutIter<'a> { - Add(>::IterMut), - Literal(std::iter::Empty<&'a mut ::kirin::ir::Region>), +pub enum MixedOpsCfgsMutIter<'a> { + Add(>::IterMut), + Literal(std::iter::Empty<&'a mut ::kirin::ir::Cfg>), } #[automatically_derived] -impl<'a> Iterator for MixedOpsRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for MixedOpsCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Add(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_pure_wrapper_generates_lift_project.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_pure_wrapper_generates_lift_project.snap index ababe00eba..39fc25296d 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_pure_wrapper_generates_lift_project.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_pure_wrapper_generates_lift_project.snap @@ -283,32 +283,32 @@ impl<'a> Iterator for CompositeOpsSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for CompositeOps { - type Iter = CompositeOpsRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for CompositeOps { + type Iter = CompositeOpsCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { - Self::Alpha(field_0) => CompositeOpsRegionsIter::Alpha( - ::regions(field_0), - ), + Self::Alpha(field_0) => { + CompositeOpsCfgsIter::Alpha(::cfgs(field_0)) + } Self::Beta(field_0) => { - CompositeOpsRegionsIter::Beta(::regions(field_0)) + CompositeOpsCfgsIter::Beta(::cfgs(field_0)) + } + Self::Gamma(field_0) => { + CompositeOpsCfgsIter::Gamma(::cfgs(field_0)) } - Self::Gamma(field_0) => CompositeOpsRegionsIter::Gamma( - ::regions(field_0), - ), } } } #[automatically_derived] #[doc(hidden)] -pub enum CompositeOpsRegionsIter<'a> { - Alpha(>::Iter), - Beta(>::Iter), - Gamma(>::Iter), +pub enum CompositeOpsCfgsIter<'a> { + Alpha(>::Iter), + Beta(>::Iter), + Gamma(>::Iter), } #[automatically_derived] -impl<'a> Iterator for CompositeOpsRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for CompositeOpsCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Alpha(inner) => inner.next(), @@ -318,32 +318,32 @@ impl<'a> Iterator for CompositeOpsRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for CompositeOps { - type IterMut = CompositeOpsRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for CompositeOps { + type IterMut = CompositeOpsCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Alpha(field_0) => CompositeOpsRegionsMutIter::Alpha( - ::regions_mut(field_0), + Self::Alpha(field_0) => CompositeOpsCfgsMutIter::Alpha( + ::cfgs_mut(field_0), ), - Self::Beta(field_0) => CompositeOpsRegionsMutIter::Beta( - ::regions_mut(field_0), + Self::Beta(field_0) => CompositeOpsCfgsMutIter::Beta( + ::cfgs_mut(field_0), ), - Self::Gamma(field_0) => CompositeOpsRegionsMutIter::Gamma( - ::regions_mut(field_0), + Self::Gamma(field_0) => CompositeOpsCfgsMutIter::Gamma( + ::cfgs_mut(field_0), ), } } } #[automatically_derived] #[doc(hidden)] -pub enum CompositeOpsRegionsMutIter<'a> { - Alpha(>::IterMut), - Beta(>::IterMut), - Gamma(>::IterMut), +pub enum CompositeOpsCfgsMutIter<'a> { + Alpha(>::IterMut), + Beta(>::IterMut), + Gamma(>::IterMut), } #[automatically_derived] -impl<'a> Iterator for CompositeOpsRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for CompositeOpsCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Alpha(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_with_wraps.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_with_wraps.snap index bb9a5ab655..3464c95666 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_with_wraps.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_with_wraps.snap @@ -243,28 +243,28 @@ impl<'a> Iterator for ArithLanguageSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for ArithLanguage { - type Iter = ArithLanguageRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for ArithLanguage { + type Iter = ArithLanguageCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { Self::Add(field_0) => { - ArithLanguageRegionsIter::Add(::regions(field_0)) + ArithLanguageCfgsIter::Add(::cfgs(field_0)) } Self::Sub(field_0) => { - ArithLanguageRegionsIter::Sub(::regions(field_0)) + ArithLanguageCfgsIter::Sub(::cfgs(field_0)) } } } } #[automatically_derived] #[doc(hidden)] -pub enum ArithLanguageRegionsIter<'a> { - Add(>::Iter), - Sub(>::Iter), +pub enum ArithLanguageCfgsIter<'a> { + Add(>::Iter), + Sub(>::Iter), } #[automatically_derived] -impl<'a> Iterator for ArithLanguageRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for ArithLanguageCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Add(inner) => inner.next(), @@ -273,28 +273,28 @@ impl<'a> Iterator for ArithLanguageRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for ArithLanguage { - type IterMut = ArithLanguageRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for ArithLanguage { + type IterMut = ArithLanguageCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Add(field_0) => ArithLanguageRegionsMutIter::Add( - ::regions_mut(field_0), - ), - Self::Sub(field_0) => ArithLanguageRegionsMutIter::Sub( - ::regions_mut(field_0), - ), + Self::Add(field_0) => { + ArithLanguageCfgsMutIter::Add(::cfgs_mut(field_0)) + } + Self::Sub(field_0) => { + ArithLanguageCfgsMutIter::Sub(::cfgs_mut(field_0)) + } } } } #[automatically_derived] #[doc(hidden)] -pub enum ArithLanguageRegionsMutIter<'a> { - Add(>::IterMut), - Sub(>::IterMut), +pub enum ArithLanguageCfgsMutIter<'a> { + Add(>::IterMut), + Sub(>::IterMut), } #[automatically_derived] -impl<'a> Iterator for ArithLanguageRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for ArithLanguageCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Add(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wrapper_with_side_fields_no_lift_project.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wrapper_with_side_fields_no_lift_project.snap index 9b400179a2..b7d99bd7e6 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wrapper_with_side_fields_no_lift_project.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wrapper_with_side_fields_no_lift_project.snap @@ -243,28 +243,28 @@ impl<'a> Iterator for MixedWrapsSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for MixedWraps { - type Iter = MixedWrapsRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for MixedWraps { + type Iter = MixedWrapsCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { - Self::Simple(field_0) => MixedWrapsRegionsIter::Simple( - ::regions(field_0), - ), + Self::Simple(field_0) => { + MixedWrapsCfgsIter::Simple(::cfgs(field_0)) + } Self::Wrapped { inner, tag } => { - MixedWrapsRegionsIter::Wrapped(::regions(inner)) + MixedWrapsCfgsIter::Wrapped(::cfgs(inner)) } } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedWrapsRegionsIter<'a> { - Simple(>::Iter), - Wrapped(>::Iter), +pub enum MixedWrapsCfgsIter<'a> { + Simple(>::Iter), + Wrapped(>::Iter), } #[automatically_derived] -impl<'a> Iterator for MixedWrapsRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for MixedWrapsCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Simple(inner) => inner.next(), @@ -273,28 +273,28 @@ impl<'a> Iterator for MixedWrapsRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for MixedWraps { - type IterMut = MixedWrapsRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for MixedWraps { + type IterMut = MixedWrapsCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Simple(field_0) => MixedWrapsRegionsMutIter::Simple( - ::regions_mut(field_0), + Self::Simple(field_0) => MixedWrapsCfgsMutIter::Simple( + ::cfgs_mut(field_0), ), - Self::Wrapped { inner, tag } => MixedWrapsRegionsMutIter::Wrapped( - ::regions_mut(inner), + Self::Wrapped { inner, tag } => MixedWrapsCfgsMutIter::Wrapped( + ::cfgs_mut(inner), ), } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedWrapsRegionsMutIter<'a> { - Simple(>::IterMut), - Wrapped(>::IterMut), +pub enum MixedWrapsCfgsMutIter<'a> { + Simple(>::IterMut), + Wrapped(>::IterMut), } #[automatically_derived] -impl<'a> Iterator for MixedWrapsRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for MixedWrapsCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Simple(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_extra_fields_from_impl.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_extra_fields_from_impl.snap index bcf53762e5..b7d99bd7e6 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_extra_fields_from_impl.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_extra_fields_from_impl.snap @@ -1,6 +1,5 @@ --- source: crates/kirin-derive-ir/src/tests/dialect.rs -assertion_line: 268 expression: code --- #[automatically_derived] @@ -244,28 +243,28 @@ impl<'a> Iterator for MixedWrapsSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for MixedWraps { - type Iter = MixedWrapsRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for MixedWraps { + type Iter = MixedWrapsCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { - Self::Simple(field_0) => MixedWrapsRegionsIter::Simple( - ::regions(field_0), - ), + Self::Simple(field_0) => { + MixedWrapsCfgsIter::Simple(::cfgs(field_0)) + } Self::Wrapped { inner, tag } => { - MixedWrapsRegionsIter::Wrapped(::regions(inner)) + MixedWrapsCfgsIter::Wrapped(::cfgs(inner)) } } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedWrapsRegionsIter<'a> { - Simple(>::Iter), - Wrapped(>::Iter), +pub enum MixedWrapsCfgsIter<'a> { + Simple(>::Iter), + Wrapped(>::Iter), } #[automatically_derived] -impl<'a> Iterator for MixedWrapsRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for MixedWrapsCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Simple(inner) => inner.next(), @@ -274,28 +273,28 @@ impl<'a> Iterator for MixedWrapsRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for MixedWraps { - type IterMut = MixedWrapsRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for MixedWraps { + type IterMut = MixedWrapsCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Simple(field_0) => MixedWrapsRegionsMutIter::Simple( - ::regions_mut(field_0), + Self::Simple(field_0) => MixedWrapsCfgsMutIter::Simple( + ::cfgs_mut(field_0), ), - Self::Wrapped { inner, tag } => MixedWrapsRegionsMutIter::Wrapped( - ::regions_mut(inner), + Self::Wrapped { inner, tag } => MixedWrapsCfgsMutIter::Wrapped( + ::cfgs_mut(inner), ), } } } #[automatically_derived] #[doc(hidden)] -pub enum MixedWrapsRegionsMutIter<'a> { - Simple(>::IterMut), - Wrapped(>::IterMut), +pub enum MixedWrapsCfgsMutIter<'a> { + Simple(>::IterMut), + Wrapped(>::IterMut), } #[automatically_derived] -impl<'a> Iterator for MixedWrapsRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for MixedWrapsCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Simple(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_terminator.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_terminator.snap index 7525b13a69..0a6ae7203d 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_terminator.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_enum_wraps_with_terminator.snap @@ -243,28 +243,28 @@ impl<'a> Iterator for CfOpsSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for CfOps { - type Iter = CfOpsRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for CfOps { + type Iter = CfOpsCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { match self { Self::Branch(field_0) => { - CfOpsRegionsIter::Branch(::regions(field_0)) + CfOpsCfgsIter::Branch(::cfgs(field_0)) } Self::Return(field_0) => { - CfOpsRegionsIter::Return(::regions(field_0)) + CfOpsCfgsIter::Return(::cfgs(field_0)) } } } } #[automatically_derived] #[doc(hidden)] -pub enum CfOpsRegionsIter<'a> { - Branch(>::Iter), - Return(>::Iter), +pub enum CfOpsCfgsIter<'a> { + Branch(>::Iter), + Return(>::Iter), } #[automatically_derived] -impl<'a> Iterator for CfOpsRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for CfOpsCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Branch(inner) => inner.next(), @@ -273,28 +273,28 @@ impl<'a> Iterator for CfOpsRegionsIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for CfOps { - type IterMut = CfOpsRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for CfOps { + type IterMut = CfOpsCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { match self { - Self::Branch(field_0) => CfOpsRegionsMutIter::Branch( - ::regions_mut(field_0), - ), - Self::Return(field_0) => CfOpsRegionsMutIter::Return( - ::regions_mut(field_0), - ), + Self::Branch(field_0) => { + CfOpsCfgsMutIter::Branch(::cfgs_mut(field_0)) + } + Self::Return(field_0) => { + CfOpsCfgsMutIter::Return(::cfgs_mut(field_0)) + } } } } #[automatically_derived] #[doc(hidden)] -pub enum CfOpsRegionsMutIter<'a> { - Branch(>::IterMut), - Return(>::IterMut), +pub enum CfOpsCfgsMutIter<'a> { + Branch(>::IterMut), + Return(>::IterMut), } #[automatically_derived] -impl<'a> Iterator for CfOpsRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for CfOpsCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { match self { Self::Branch(inner) => inner.next(), diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_all_properties.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_all_properties.snap index e2621cbdbe..47ec80a8ea 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_all_properties.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_all_properties.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for ConstantSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for Constant { - type Iter = ConstantRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for Constant { + type Iter = ConstantCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { result } = self; - ConstantRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + ConstantCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ConstantRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct ConstantCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ConstantRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for ConstantCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for Constant { - type IterMut = ConstantRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for Constant { + type IterMut = ConstantCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { result } = self; - ConstantRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + ConstantCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ConstantRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct ConstantCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ConstantRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for ConstantCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_edge.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_edge.snap index 60d1acbf00..476ff6e3b4 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_edge.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_edge.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for ZxWireSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for ZxWire { - type Iter = ZxWireRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for ZxWire { + type Iter = ZxWireCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { res } = self; - ZxWireRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + ZxWireCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ZxWireRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct ZxWireCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ZxWireRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for ZxWireCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for ZxWire { - type IterMut = ZxWireRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for ZxWire { + type IterMut = ZxWireCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { res } = self; - ZxWireRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + ZxWireCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ZxWireRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct ZxWireCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ZxWireRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for ZxWireCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_no_fields.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_no_fields.snap index 483626c23b..fdbea115b1 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_no_fields.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_no_fields.snap @@ -171,43 +171,43 @@ impl<'a> Iterator for NopSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for Nop { - type Iter = NopRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { - NopRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), +impl<'a> ::kirin::ir::HasCfgs<'a> for Nop { + type Iter = NopCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { + NopCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct NopRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct NopCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for NopRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for NopCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for Nop { - type IterMut = NopRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { - NopRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), +impl<'a> ::kirin::ir::HasCfgsMut<'a> for Nop { + type IterMut = NopCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { + NopCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct NopRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct NopCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for NopRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for NopCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_option_block.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_option_block.snap index 603d50e378..61f11f0eb5 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_option_block.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_option_block.snap @@ -217,53 +217,53 @@ impl<'a> Iterator for ConditionalOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for ConditionalOp { - type Iter = ConditionalOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for ConditionalOp { + type Iter = ConditionalOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { cond, then_block, else_block, } = self; - ConditionalOpRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + ConditionalOpCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ConditionalOpRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct ConditionalOpCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ConditionalOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for ConditionalOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for ConditionalOp { - type IterMut = ConditionalOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for ConditionalOp { + type IterMut = ConditionalOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { cond, then_block, else_block, } = self; - ConditionalOpRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + ConditionalOpCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ConditionalOpRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct ConditionalOpCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ConditionalOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for ConditionalOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_symbol.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_symbol.snap index 6403144f9c..071e9d813f 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_symbol.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_symbol.snap @@ -177,45 +177,45 @@ impl<'a> Iterator for CallExternSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for CallExtern { - type Iter = CallExternRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for CallExtern { + type Iter = CallExternCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { target, args } = self; - CallExternRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + CallExternCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct CallExternRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct CallExternCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for CallExternRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for CallExternCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for CallExtern { - type IterMut = CallExternRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for CallExtern { + type IterMut = CallExternCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { target, args } = self; - CallExternRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + CallExternCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct CallExternRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct CallExternCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for CallExternRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for CallExternCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_terminator.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_terminator.snap index c2605ad4ec..e250632cab 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_terminator.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_terminator.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for ReturnSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for Return { - type Iter = ReturnRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for Return { + type Iter = ReturnCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { value } = self; - ReturnRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + ReturnCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ReturnRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct ReturnCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ReturnRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for ReturnCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for Return { - type IterMut = ReturnRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for Return { + type IterMut = ReturnCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { value } = self; - ReturnRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + ReturnCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ReturnRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct ReturnCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ReturnRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for ReturnCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_vec_ssa_value.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_vec_ssa_value.snap index 81f12313cc..37cea93750 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_vec_ssa_value.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_vec_ssa_value.snap @@ -177,45 +177,45 @@ impl<'a> Iterator for CallOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for CallOp { - type Iter = CallOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for CallOp { + type Iter = CallOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { args, result } = self; - CallOpRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + CallOpCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct CallOpRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct CallOpCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for CallOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for CallOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for CallOp { - type IterMut = CallOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for CallOp { + type IterMut = CallOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { args, result } = self; - CallOpRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + CallOpCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct CallOpRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct CallOpCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for CallOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for CallOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_digraph.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_digraph.snap index 6d9c12ebc1..9df3eadebc 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_digraph.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_digraph.snap @@ -225,55 +225,55 @@ impl<'a> Iterator for QuantumEvalSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for QuantumEval { - type Iter = QuantumEvalRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for QuantumEval { + type Iter = QuantumEvalCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { qubit, angle, body, res, } = self; - QuantumEvalRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + QuantumEvalCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct QuantumEvalRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct QuantumEvalCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for QuantumEvalRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for QuantumEvalCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for QuantumEval { - type IterMut = QuantumEvalRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for QuantumEval { + type IterMut = QuantumEvalCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { qubit, angle, body, res, } = self; - QuantumEvalRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + QuantumEvalCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct QuantumEvalRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct QuantumEvalCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for QuantumEvalRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for QuantumEvalCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ssa_fields.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ssa_fields.snap index 8503ec711b..6f10414957 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ssa_fields.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ssa_fields.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for BinaryOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for BinaryOp { - type Iter = BinaryOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for BinaryOp { + type Iter = BinaryOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { result, lhs, rhs } = self; - BinaryOpRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + BinaryOpCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct BinaryOpRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct BinaryOpCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for BinaryOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for BinaryOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for BinaryOp { - type IterMut = BinaryOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for BinaryOp { + type IterMut = BinaryOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { result, lhs, rhs } = self; - BinaryOpRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + BinaryOpCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct BinaryOpRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct BinaryOpCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for BinaryOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for BinaryOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_successors.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_successors.snap index e614b48cfc..78fbcaf0e8 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_successors.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_successors.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for BranchSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for Branch { - type Iter = BranchRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for Branch { + type Iter = BranchCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { target, args } = self; - BranchRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + BranchCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct BranchRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct BranchCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for BranchRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for BranchCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for Branch { - type IterMut = BranchRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for Branch { + type IterMut = BranchCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { target, args } = self; - BranchRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + BranchCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct BranchRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct BranchCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for BranchRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for BranchCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ungraph.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ungraph.snap index 6b337973be..2e8d2e4bf3 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ungraph.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_struct_with_ungraph.snap @@ -217,53 +217,53 @@ impl<'a> Iterator for ZxEvalSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for ZxEval { - type Iter = ZxEvalRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for ZxEval { + type Iter = ZxEvalCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self { boundary, captures, body, } = self; - ZxEvalRegionsIter { - inner: std::iter::empty::<&'a ::kirin::ir::Region>(), + ZxEvalCfgsIter { + inner: std::iter::empty::<&'a ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ZxEvalRegionsIter<'a> { - inner: std::iter::Empty<&'a ::kirin::ir::Region>, +pub struct ZxEvalCfgsIter<'a> { + inner: std::iter::Empty<&'a ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ZxEvalRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for ZxEvalCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for ZxEval { - type IterMut = ZxEvalRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for ZxEval { + type IterMut = ZxEvalCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self { boundary, captures, body, } = self; - ZxEvalRegionsMutIter { - inner: std::iter::empty::<&'a mut ::kirin::ir::Region>(), + ZxEvalCfgsMutIter { + inner: std::iter::empty::<&'a mut ::kirin::ir::Cfg>(), } } } #[automatically_derived] #[doc(hidden)] -pub struct ZxEvalRegionsMutIter<'a> { - inner: std::iter::Empty<&'a mut ::kirin::ir::Region>, +pub struct ZxEvalCfgsMutIter<'a> { + inner: std::iter::Empty<&'a mut ::kirin::ir::Cfg>, } #[automatically_derived] -impl<'a> Iterator for ZxEvalRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for ZxEvalCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project.snap index d36f1bbea9..52d8f7de10 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for WrapperOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for WrapperOp { - type Iter = WrapperOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for WrapperOp { + type Iter = WrapperOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self(field_0) = self; - WrapperOpRegionsIter { - inner: ::regions(field_0), + WrapperOpCfgsIter { + inner: ::cfgs(field_0), } } } #[automatically_derived] #[doc(hidden)] -pub struct WrapperOpRegionsIter<'a> { - inner: >::Iter, +pub struct WrapperOpCfgsIter<'a> { + inner: >::Iter, } #[automatically_derived] -impl<'a> Iterator for WrapperOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for WrapperOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for WrapperOp { - type IterMut = WrapperOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for WrapperOp { + type IterMut = WrapperOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self(field_0) = self; - WrapperOpRegionsMutIter { - inner: ::regions_mut(field_0), + WrapperOpCfgsMutIter { + inner: ::cfgs_mut(field_0), } } } #[automatically_derived] #[doc(hidden)] -pub struct WrapperOpRegionsMutIter<'a> { - inner: >::IterMut, +pub struct WrapperOpCfgsMutIter<'a> { + inner: >::IterMut, } #[automatically_derived] -impl<'a> Iterator for WrapperOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for WrapperOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project_bridge.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project_bridge.snap index 998d5d3f78..8e36c4f73b 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project_bridge.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_generates_lift_project_bridge.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for WrapperOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for WrapperOp { - type Iter = WrapperOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for WrapperOp { + type Iter = WrapperOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self(field_0) = self; - WrapperOpRegionsIter { - inner: ::regions(field_0), + WrapperOpCfgsIter { + inner: ::cfgs(field_0), } } } #[automatically_derived] #[doc(hidden)] -pub struct WrapperOpRegionsIter<'a> { - inner: >::Iter, +pub struct WrapperOpCfgsIter<'a> { + inner: >::Iter, } #[automatically_derived] -impl<'a> Iterator for WrapperOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for WrapperOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for WrapperOp { - type IterMut = WrapperOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for WrapperOp { + type IterMut = WrapperOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self(field_0) = self; - WrapperOpRegionsMutIter { - inner: ::regions_mut(field_0), + WrapperOpCfgsMutIter { + inner: ::cfgs_mut(field_0), } } } #[automatically_derived] #[doc(hidden)] -pub struct WrapperOpRegionsMutIter<'a> { - inner: >::IterMut, +pub struct WrapperOpCfgsMutIter<'a> { + inner: >::IterMut, } #[automatically_derived] -impl<'a> Iterator for WrapperOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for WrapperOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_has_signature.snap b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_has_signature.snap index d36f1bbea9..52d8f7de10 100644 --- a/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_has_signature.snap +++ b/crates/kirin-derive-ir/src/tests/snapshots/kirin_derive_ir__tests__dialect__dialect_derive_wrapper_struct_has_signature.snap @@ -179,45 +179,45 @@ impl<'a> Iterator for WrapperOpSuccessorsMutIter<'a> { } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegions<'a> for WrapperOp { - type Iter = WrapperOpRegionsIter<'a>; - fn regions(&'a self) -> Self::Iter { +impl<'a> ::kirin::ir::HasCfgs<'a> for WrapperOp { + type Iter = WrapperOpCfgsIter<'a>; + fn cfgs(&'a self) -> Self::Iter { let Self(field_0) = self; - WrapperOpRegionsIter { - inner: ::regions(field_0), + WrapperOpCfgsIter { + inner: ::cfgs(field_0), } } } #[automatically_derived] #[doc(hidden)] -pub struct WrapperOpRegionsIter<'a> { - inner: >::Iter, +pub struct WrapperOpCfgsIter<'a> { + inner: >::Iter, } #[automatically_derived] -impl<'a> Iterator for WrapperOpRegionsIter<'a> { - type Item = &'a ::kirin::ir::Region; +impl<'a> Iterator for WrapperOpCfgsIter<'a> { + type Item = &'a ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } } #[automatically_derived] -impl<'a> ::kirin::ir::HasRegionsMut<'a> for WrapperOp { - type IterMut = WrapperOpRegionsMutIter<'a>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> ::kirin::ir::HasCfgsMut<'a> for WrapperOp { + type IterMut = WrapperOpCfgsMutIter<'a>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { let Self(field_0) = self; - WrapperOpRegionsMutIter { - inner: ::regions_mut(field_0), + WrapperOpCfgsMutIter { + inner: ::cfgs_mut(field_0), } } } #[automatically_derived] #[doc(hidden)] -pub struct WrapperOpRegionsMutIter<'a> { - inner: >::IterMut, +pub struct WrapperOpCfgsMutIter<'a> { + inner: >::IterMut, } #[automatically_derived] -impl<'a> Iterator for WrapperOpRegionsMutIter<'a> { - type Item = &'a mut ::kirin::ir::Region; +impl<'a> Iterator for WrapperOpCfgsMutIter<'a> { + type Item = &'a mut ::kirin::ir::Cfg; fn next(&mut self) -> Option { self.inner.next() } diff --git a/crates/kirin-derive-ir/src/tests/standalone.rs b/crates/kirin-derive-ir/src/tests/standalone.rs index 91ef216c72..a94bf95e57 100644 --- a/crates/kirin-derive-ir/src/tests/standalone.rs +++ b/crates/kirin-derive-ir/src/tests/standalone.rs @@ -1,5 +1,5 @@ //! Snapshot tests for standalone single-trait derive macros -//! (HasArguments, HasResults, HasRegions, HasDigraphs, HasUngraphs, IsTerminator, IsEdge). +//! (HasArguments, HasResults, HasCfgs, HasDigraphs, HasUngraphs, IsTerminator, IsEdge). use crate::generate::*; use kirin_test_utils::rustfmt; @@ -46,14 +46,14 @@ fn test_standalone_has_results() { } #[test] -fn test_standalone_has_regions() { +fn test_standalone_has_cfgs() { let input: syn::DeriveInput = syn::parse_quote! { #[kirin(type = SimpleType)] struct Lambda { - body: Region, + body: Cfg, } }; - let tokens = generate_field_iter(&input, HAS_REGIONS).expect("Failed to generate HasRegions"); + let tokens = generate_field_iter(&input, HAS_CFGS).expect("Failed to generate HasCfgs"); insta::assert_snapshot!(rustfmt(tokens.to_string())); } diff --git a/crates/kirin-derive-toolkit/src/ir/fields/data.rs b/crates/kirin-derive-toolkit/src/ir/fields/data.rs index f917973034..2615a3f456 100644 --- a/crates/kirin-derive-toolkit/src/ir/fields/data.rs +++ b/crates/kirin-derive-toolkit/src/ir/fields/data.rs @@ -13,8 +13,8 @@ pub enum FieldCategory { Block, /// Control-flow successor (`Successor`). Successor, - /// Nested region (`Region` / `Region`). - Region, + /// Nested CFG (`Cfg` / `Cfg`). + Cfg, /// Directed graph body (`DiGraph`). DiGraph, /// Undirected graph body (`UnGraph`). @@ -54,7 +54,7 @@ pub enum FieldData { }, Block, Successor, - Region, + Cfg, DiGraph, UnGraph, Signature, @@ -82,7 +82,7 @@ impl Clone for FieldData { }, FieldData::Block => FieldData::Block, FieldData::Successor => FieldData::Successor, - FieldData::Region => FieldData::Region, + FieldData::Cfg => FieldData::Cfg, FieldData::DiGraph => FieldData::DiGraph, FieldData::UnGraph => FieldData::UnGraph, FieldData::Signature => FieldData::Signature, @@ -127,8 +127,8 @@ mod tests { } #[test] - fn field_category_is_ssa_like_region() { - assert!(!FieldCategory::Region.is_ssa_like()); + fn field_category_is_ssa_like_cfg() { + assert!(!FieldCategory::Cfg.is_ssa_like()); } #[test] diff --git a/crates/kirin-derive-toolkit/src/ir/fields/info.rs b/crates/kirin-derive-toolkit/src/ir/fields/info.rs index 801fa8081b..8fb50f4cda 100644 --- a/crates/kirin-derive-toolkit/src/ir/fields/info.rs +++ b/crates/kirin-derive-toolkit/src/ir/fields/info.rs @@ -47,7 +47,7 @@ impl FieldInfo { FieldData::Result { .. } => FieldCategory::Result, FieldData::Block => FieldCategory::Block, FieldData::Successor => FieldCategory::Successor, - FieldData::Region => FieldCategory::Region, + FieldData::Cfg => FieldCategory::Cfg, FieldData::DiGraph => FieldCategory::DiGraph, FieldData::UnGraph => FieldCategory::UnGraph, FieldData::Signature => FieldCategory::Signature, @@ -63,7 +63,7 @@ impl FieldInfo { FieldCategory::Result => "result", FieldCategory::Block => "block", FieldCategory::Successor => "successor", - FieldCategory::Region => "region", + FieldCategory::Cfg => "cfg", FieldCategory::DiGraph => "digraph", FieldCategory::UnGraph => "ungraph", FieldCategory::Signature => "signature", @@ -237,7 +237,7 @@ mod tests { ), (FieldData::Block, "block"), (FieldData::Successor, "successor"), - (FieldData::Region, "region"), + (FieldData::Cfg, "cfg"), (FieldData::Signature, "signature"), (FieldData::Symbol, "symbol"), ( diff --git a/crates/kirin-derive-toolkit/src/ir/fields/mod.rs b/crates/kirin-derive-toolkit/src/ir/fields/mod.rs index 3e7a6326d2..e06f3d1c38 100644 --- a/crates/kirin-derive-toolkit/src/ir/fields/mod.rs +++ b/crates/kirin-derive-toolkit/src/ir/fields/mod.rs @@ -8,7 +8,7 @@ //! | `ResultValue` / `ResultValue` | [`Result`](FieldCategory::Result) | SSA output value | //! | `Block` | [`Block`](FieldCategory::Block) | Basic block reference | //! | `Successor` | [`Successor`](FieldCategory::Successor) | Control-flow successor | -//! | `Region` / `Region` | [`Region`](FieldCategory::Region) | Nested region | +//! | `Cfg` / `Cfg` | [`Cfg`](FieldCategory::Cfg) | Nested CFG | //! | `Symbol` | [`Symbol`](FieldCategory::Symbol) | Symbol reference | //! | anything else | [`Value`](FieldCategory::Value) | Plain Rust value | //! diff --git a/crates/kirin-derive-toolkit/src/ir/statement/accessors.rs b/crates/kirin-derive-toolkit/src/ir/statement/accessors.rs index 55674d418d..1a75d3c197 100644 --- a/crates/kirin-derive-toolkit/src/ir/statement/accessors.rs +++ b/crates/kirin-derive-toolkit/src/ir/statement/accessors.rs @@ -37,11 +37,11 @@ impl Statement { .filter(|f| f.category() == FieldCategory::Successor) } - /// Iterates fields classified as [`FieldCategory::Region`]. - pub fn regions(&self) -> impl Iterator> { + /// Iterates fields classified as [`FieldCategory::Cfg`]. + pub fn cfgs(&self) -> impl Iterator> { self.fields .iter() - .filter(|f| f.category() == FieldCategory::Region) + .filter(|f| f.category() == FieldCategory::Cfg) } /// Iterates fields classified as [`FieldCategory::DiGraph`]. diff --git a/crates/kirin-derive-toolkit/src/ir/statement/definition.rs b/crates/kirin-derive-toolkit/src/ir/statement/definition.rs index dcbebd059d..5c61eeb11c 100644 --- a/crates/kirin-derive-toolkit/src/ir/statement/definition.rs +++ b/crates/kirin-derive-toolkit/src/ir/statement/definition.rs @@ -226,12 +226,12 @@ impl Statement { }); } - if let Some(collection) = Collection::from_type(ty, "Region") { + if let Some(collection) = Collection::from_type(ty, "Cfg") { return Ok(FieldInfo { index, ident, collection, - data: FieldData::Region, + data: FieldData::Cfg, }); } diff --git a/crates/kirin-derive-toolkit/src/parse_dispatch.rs b/crates/kirin-derive-toolkit/src/parse_dispatch.rs index 5f777ec2d4..4d829e6246 100644 --- a/crates/kirin-derive-toolkit/src/parse_dispatch.rs +++ b/crates/kirin-derive-toolkit/src/parse_dispatch.rs @@ -2,7 +2,7 @@ //! //! Generates a monomorphic [`ParseDispatch`] implementation that dispatches to //! concrete dialect parsers with concrete lifetimes, avoiding the HRTB bounds -//! that cause E0275 with `Block`/`Region`-containing types. +//! that cause E0275 with `Block`/`Cfg`-containing types. //! //! Reuses the same `#[stage(...)]` attribute parsing as `StageMeta`. diff --git a/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs b/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs index 92d4031c43..1431604dea 100644 --- a/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs +++ b/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs @@ -101,7 +101,7 @@ fn build_fn_inputs(info: &StatementInfo, ir_type: &syn::Path) -> Vec { @@ -147,7 +147,7 @@ fn build_fn_let_inputs(info: &StatementInfo, ir_type: &syn::Path) -> Vec { @@ -166,7 +166,7 @@ fn field_type_for_category(collection: &Collection, category: FieldCategory) -> FieldCategory::Result => "ResultValue", FieldCategory::Block => "Block", FieldCategory::Successor => "Successor", - FieldCategory::Region => "Region", + FieldCategory::Cfg => "Cfg", FieldCategory::DiGraph => "DiGraph", FieldCategory::UnGraph => "UnGraph", FieldCategory::Symbol => "Symbol", diff --git a/crates/kirin-derive-toolkit/src/template/method_pattern/field_collection.rs b/crates/kirin-derive-toolkit/src/template/method_pattern/field_collection.rs index abd6e74790..d0117bc8bc 100644 --- a/crates/kirin-derive-toolkit/src/template/method_pattern/field_collection.rs +++ b/crates/kirin-derive-toolkit/src/template/method_pattern/field_collection.rs @@ -20,8 +20,8 @@ pub enum FieldIterKind { Blocks, /// Successor block references. Successors, - /// Nested region fields. - Regions, + /// Nested CFG fields. + Cfgs, /// Directed graph body fields. Digraphs, /// Undirected graph body fields. @@ -99,10 +99,7 @@ impl FieldCollection { .successors() .map(FieldAccess::from_field_info) .collect(), - FieldIterKind::Regions => statement - .regions() - .map(FieldAccess::from_field_info) - .collect(), + FieldIterKind::Cfgs => statement.cfgs().map(FieldAccess::from_field_info).collect(), FieldIterKind::Digraphs => statement .digraphs() .map(FieldAccess::from_field_info) diff --git a/crates/kirin-derive-toolkit/src/template/trait_impl.rs b/crates/kirin-derive-toolkit/src/template/trait_impl.rs index faea401484..b5770babb5 100644 --- a/crates/kirin-derive-toolkit/src/template/trait_impl.rs +++ b/crates/kirin-derive-toolkit/src/template/trait_impl.rs @@ -303,15 +303,15 @@ impl Template for MarkerTemplate { /// Configuration for a field iterator trait. pub struct FieldIterConfig { - /// Which field category to iterate over (e.g., regions, blocks, successors). + /// Which field category to iterate over (e.g., cfgs, blocks, successors). pub kind: FieldIterKind, /// Whether the iterator yields mutable references. pub mutable: bool, - /// Fully qualified trait name (e.g., `"HasRegions"`). + /// Fully qualified trait name (e.g., `"HasCfgs"`). pub trait_name: &'static str, - /// The IR type that fields must match (e.g., `"Region"`). + /// The IR type that fields must match (e.g., `"Cfg"`). pub matching_type: &'static str, - /// Method name on the trait (e.g., `"regions"`). + /// Method name on the trait (e.g., `"cfgs"`). pub trait_method: &'static str, /// Associated type name for the iterator (e.g., `"Iter"`). pub trait_type_iter: &'static str, diff --git a/crates/kirin-function/src/body.rs b/crates/kirin-function/src/body.rs index cc7d1104b6..f1ffd776fe 100644 --- a/crates/kirin-function/src/body.rs +++ b/crates/kirin-function/src/body.rs @@ -9,14 +9,14 @@ use kirin::prelude::*; #[kirin(builders, type = T)] #[chumsky(format = "fn {:name}{sig} {body}")] pub struct Function { - pub(crate) body: Region, + pub(crate) body: Cfg, pub(crate) sig: Signature, #[kirin(default)] marker: std::marker::PhantomData, } -impl HasRegionBody for Function { - fn region(&self) -> &Region { +impl HasCfgBody for Function { + fn cfg(&self) -> &Cfg { &self.body } } diff --git a/crates/kirin-function/src/call/tests.rs b/crates/kirin-function/src/call/tests.rs index f561208a50..c95c2ae22a 100644 --- a/crates/kirin-function/src/call/tests.rs +++ b/crates/kirin-function/src/call/tests.rs @@ -1,6 +1,6 @@ use super::*; use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, TestSSAValue, }; use kirin_test_types::UnitType; @@ -51,8 +51,8 @@ fn no_blocks() { } #[test] -fn no_regions() { - assert_eq!(make_call(0).regions().count(), 0); +fn no_cfgs() { + assert_eq!(make_call(0).cfgs().count(), 0); } #[test] diff --git a/crates/kirin-function/src/interpreter.rs b/crates/kirin-function/src/interpreter.rs index 3e948a4533..05029a515f 100644 --- a/crates/kirin-function/src/interpreter.rs +++ b/crates/kirin-function/src/interpreter.rs @@ -1,4 +1,4 @@ -use kirin::prelude::{CompileTimeValue, HasBottom, HasRegionBody, Product, SSAValue}; +use kirin::prelude::{CompileTimeValue, HasBottom, HasCfgBody, Product, SSAValue}; use kirin_interpreter::dialect::{ CallEffect, Callee, ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, ForwardEval, FunctionBody, FunctionEntry, Interp, Interpretable, InterpreterError, @@ -99,7 +99,7 @@ where args: Product, _interp: &mut I, ) -> Result, I::Error> { - Ok(FunctionBody::new(*self.region()).args(args)) + Ok(FunctionBody::new(*self.cfg()).args(args)) } } @@ -113,7 +113,7 @@ where args: Product, _interp: &mut I, ) -> Result, I::Error> { - Ok(FunctionBody::new(*self.region()).args(args)) + Ok(FunctionBody::new(*self.cfg()).args(args)) } } diff --git a/crates/kirin-function/src/lambda.rs b/crates/kirin-function/src/lambda.rs index 44c918de96..6bc865c02d 100644 --- a/crates/kirin-function/src/lambda.rs +++ b/crates/kirin-function/src/lambda.rs @@ -11,12 +11,12 @@ use kirin::prelude::*; /// a `sig: Signature` field. The reasons are: /// /// - **Parameters are implicit in block arguments.** A lambda's parameter types -/// are defined by the block arguments of its `body` region's entry block. +/// are defined by the block arguments of its `body` CFG's entry block. /// Duplicating them in a `Signature` would create a consistency hazard. /// /// - **Return type is already present.** The `res: ResultValue` field carries /// the lambda's return type, which is the only part of the signature that -/// cannot be recovered from the body region alone. +/// cannot be recovered from the body cfg alone. /// /// - **Captures are not part of the function type.** In PL theory, a closure's /// *function type* describes its parameter and return types, not its captured @@ -36,14 +36,14 @@ use kirin::prelude::*; pub struct Lambda { name: Symbol, captures: Vec, - pub(crate) body: Region, + pub(crate) body: Cfg, res: ResultValue, #[kirin(default)] marker: std::marker::PhantomData, } -impl HasRegionBody for Lambda { - fn region(&self) -> &Region { +impl HasCfgBody for Lambda { + fn cfg(&self) -> &Cfg { &self.body } } diff --git a/crates/kirin-function/src/ret.rs b/crates/kirin-function/src/ret.rs index f15eb0541c..11f5680a35 100644 --- a/crates/kirin-function/src/ret.rs +++ b/crates/kirin-function/src/ret.rs @@ -13,7 +13,7 @@ pub struct Return { mod tests { use super::*; use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, TestSSAValue, }; use kirin_test_types::UnitType; @@ -78,8 +78,8 @@ mod tests { } #[test] - fn no_regions() { - assert_eq!(make_return().regions().count(), 0); + fn no_cfgs() { + assert_eq!(make_return().cfgs().count(), 0); } #[test] diff --git a/crates/kirin-interpreter/src/core/effect.rs b/crates/kirin-interpreter/src/core/effect.rs index 8a0f1a8114..fa33570af4 100644 --- a/crates/kirin-interpreter/src/core/effect.rs +++ b/crates/kirin-interpreter/src/core/effect.rs @@ -1,5 +1,5 @@ use kirin_ir::{ - Block, CompileStage, Function, Product, Region, SSAValue, SpecializedFunction, StagedFunction, + Block, Cfg, CompileStage, Function, Product, SSAValue, SpecializedFunction, StagedFunction, Symbol, }; @@ -24,7 +24,7 @@ use kirin_ir::{ pub enum SparseForwardEffect { /// Statement done; continue with the next statement. Next, - /// Unconditional transfer to a block in the current region. + /// Unconditional transfer to a block in the current CFG. Jump(Edge), /// Conditional transfer whose condition is undecided in the value domain. Branch(Vec>), @@ -86,27 +86,27 @@ pub enum Callee { Specialized(SpecializedFunction), } -/// The body a callable statement enters when invoked: a CFG region plus the +/// The body a callable statement enters when invoked: a CFG plus the /// entry arguments bound to its entry block. /// /// 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 region. +/// rule returns one; the engine builds the body frame that walks the CFG. pub struct FunctionBody { - pub region: Region, + pub cfg: Cfg, pub args: Product, } impl FunctionBody { - /// A function body over `region`, with no entry arguments yet. - pub fn new(region: Region) -> Self { + /// A function body over `cfg`, with no entry arguments yet. + pub fn new(cfg: Cfg) -> Self { Self { - region, + cfg, args: Product::new(), } } - /// Entry arguments bound to the region entry block's parameters. + /// Entry arguments bound to the CFG entry block's parameters. pub fn args(mut self, args: impl IntoIterator) -> Self { self.args = args.into_iter().collect(); self diff --git a/crates/kirin-interpreter/src/core/error.rs b/crates/kirin-interpreter/src/core/error.rs index 24d2d26597..5211e87d26 100644 --- a/crates/kirin-interpreter/src/core/error.rs +++ b/crates/kirin-interpreter/src/core/error.rs @@ -39,8 +39,8 @@ pub enum InterpreterError { }, #[error("missing call target {0:?}")] MissingCallTarget(Symbol), - #[error("region has no entry block")] - EmptyRegion, + #[error("cfg has no entry block")] + EmptyCfg, #[error("block {0:?} fell through without a terminator effect")] BlockFellThrough(Block), #[error("function body fell through without returning")] diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 2f6de5ed8e..688eefe8fb 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -7,7 +7,7 @@ use std::hash::Hash; -use kirin_ir::{Block, CompileStage, Product, Region, SSAValue, Statement}; +use kirin_ir::{Block, Cfg, CompileStage, Product, SSAValue, Statement}; use crate::{ CallEffect, Callee, Env, EnvIndex, FunctionBody, FunctionTarget, Interp, InterpreterError, @@ -139,11 +139,7 @@ pub trait ForwardFrameDriver: Env { block: Block, after: Statement, ) -> Result, Self::Error>; - fn region_entry( - &self, - stage: CompileStage, - region: Region, - ) -> Result, Self::Error>; + fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, Self::Error>; /// Bind a block's parameters to incoming actuals in `env` (arity-checked). fn bind_block_args( diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index e54ee3e14d..d871a2c0ca 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -1,20 +1,20 @@ //! Engine-internal IR queries over stage enums. //! //! Engines need a handful of language-independent facts (block parameters, -//! statement order, region entry, function specialization) from typed +//! statement order, CFG entry, function specialization) from typed //! `StageInfo` values. Each query is a [`StageAction`] dispatched through //! kirin-ir's `StageDispatch` machinery; [`StageQuery`] bundles them into one //! bound that any well-formed stage enum satisfies automatically. use kirin_ir::{ - Block, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasRegions, HasStageInfo, - HasSuccessors, Pipeline, Region, SSAKind, SSAValue, SpecializedFunction, StageAction, - StageInfo, StageMeta, StagedFunction, Statement, SupportsStageDispatch, Symbol, + Block, Cfg, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasCfgs, HasStageInfo, + HasSuccessors, Pipeline, SSAKind, SSAValue, SpecializedFunction, StageAction, StageInfo, + StageMeta, StagedFunction, Statement, SupportsStageDispatch, Symbol, UniqueLiveSpecializationError, }; use crate::InterpreterError; -use crate::facts::topology::{self, RegionTopology}; +use crate::facts::topology::{self, CfgTopology}; /// Block parameters as SSA values. pub struct BlockParams(pub Block); @@ -95,10 +95,10 @@ where } } -/// Entry block of a region. -pub struct RegionEntry(pub Region); +/// Entry block of a CFG. +pub struct CfgEntry(pub Cfg); -impl StageAction for RegionEntry +impl StageAction for CfgEntry where S: StageMeta + HasStageInfo, L: Dialect, @@ -230,17 +230,17 @@ where } } -/// The topology of a region: blocks (including nested structured bodies), +/// The topology of a CFG: blocks (including nested structured bodies), /// statements per block, CFG successors, and block feeders. -pub struct RegionTopologyQuery(pub Region); +pub struct CfgTopologyQuery(pub Cfg); -impl StageAction for RegionTopologyQuery +impl StageAction for CfgTopologyQuery where S: StageMeta + HasStageInfo, L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasRegions<'a>, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a>, { - type Output = RegionTopology; + type Output = CfgTopology; type Error = InterpreterError; fn run( @@ -248,7 +248,7 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - Ok(topology::region_topology(info, &self.0)) + Ok(topology::cfg_topology(info, &self.0)) } } @@ -282,7 +282,7 @@ pub trait StageQuery: + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch< UniqueSpecialization, Result, @@ -291,7 +291,7 @@ pub trait StageQuery: + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch { } @@ -300,7 +300,7 @@ impl StageQuery for S where + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch< UniqueSpecialization, Result, @@ -309,7 +309,7 @@ impl StageQuery for S where + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch { } @@ -354,12 +354,12 @@ pub(crate) fn next_statement( dispatch(pipeline, stage, NextStatement { block, after }) } -pub(crate) fn region_entry( +pub(crate) fn cfg_entry( pipeline: &Pipeline, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> Result, InterpreterError> { - dispatch(pipeline, stage, RegionEntry(region)) + dispatch(pipeline, stage, CfgEntry(cfg)) } pub(crate) fn unique_specialization( @@ -402,10 +402,10 @@ pub(crate) fn terminator_arguments( dispatch(pipeline, stage, TerminatorArguments(block)) } -pub(crate) fn region_topology( +pub(crate) fn cfg_topology( pipeline: &Pipeline, stage: CompileStage, - region: Region, -) -> Result { - dispatch(pipeline, stage, RegionTopologyQuery(region)) + cfg: Cfg, +) -> Result { + dispatch(pipeline, stage, CfgTopologyQuery(cfg)) } diff --git a/crates/kirin-interpreter/src/engines/concrete/frames.rs b/crates/kirin-interpreter/src/engines/concrete/frames.rs index ec6b87220a..1972bd4b46 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames.rs @@ -2,7 +2,7 @@ //! protocol. //! //! These are the default total frames for [`ConcreteInterpreter`](crate::ConcreteInterpreter): -//! [`BodyFrame`] (walks a function-body CFG region or a single body block) and +//! [`BodyFrame`] (walks a function-body CFG or a single body block) and //! [`CallFrame`] (call/return). They implement the shared [`Frame`] trait by //! consuming the dialect [`SparseForwardEffect`] and driving a single deterministic //! path. Structured-control dialects do not get a framework "scope": they push @@ -13,7 +13,7 @@ //! via [`FrameBuild`] plus its dialect frames. The forward abstract analogue //! lives in [`sparse_forward::frames`](crate::engines::sparse_forward::frames). -use kirin_ir::{Block, CompileStage, Product, Region, SSAValue, Statement}; +use kirin_ir::{Block, Cfg, CompileStage, Product, SSAValue, Statement}; use crate::{ CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, @@ -44,7 +44,7 @@ pub trait FrameBuild: Sized { fn from_call(frame: CallFrame) -> Self; } -/// Traversal of one body: a function-body CFG region (multi-block, with jumps) +/// Traversal of one body: a function-body CFG (multi-block, with jumps) /// or a single body block (scf-style, terminated by a yield). pub struct BodyFrame { stage: CompileStage, @@ -68,21 +68,21 @@ where V: Clone, E: From, { - /// Walk a function body: start at the entry block of `region`, binding + /// Walk a function body: start at the entry block of `cfg`, binding /// `args` to its parameters. Owns the activation and is the return boundary. pub fn function( interp: &mut I, stage: CompileStage, index: EnvIndex, - region: Region, + cfg: Cfg, args: Product, ) -> Result where I: FrameDriver, { let entry = interp - .region_entry(stage, region)? - .ok_or_else(|| E::from(InterpreterError::EmptyRegion))?; + .cfg_entry(stage, cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?; Self::start(interp, stage, index, entry, args, true, true) } @@ -289,8 +289,7 @@ where let target = interp.resolve_call(resolve_stage, &callee)?; let index = interp.alloc_env(); let body = interp.enter_function(target.stage, target.body, args, index)?; - let frame = - BodyFrame::function(interp, target.stage, index, body.region, body.args)?; + let frame = BodyFrame::function(interp, target.stage, index, body.cfg, body.args)?; Ok(FrameEffect::Push { parent: F::from_call(CallFrame::Awaiting { caller_env, diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index 1c36f5cfa9..6c7665ac06 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use kirin_ir::{Block, CompileStage, Pipeline, Product, Region, SSAValue, StageMeta, Statement}; +use kirin_ir::{Block, Cfg, CompileStage, Pipeline, Product, SSAValue, StageMeta, Statement}; use crate::core::query; use crate::{ @@ -191,8 +191,8 @@ where query::next_statement(self.pipeline, stage, block, after).map_err(E::from) } - fn region_entry(&self, stage: CompileStage, region: Region) -> Result, E> { - query::region_entry(self.pipeline, stage, region).map_err(E::from) + fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, E> { + query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } } @@ -233,7 +233,7 @@ where let index = self.alloc_env(); let args: Product = args.into_iter().collect(); let body = self.enter_function(target.stage, target.body, args, index)?; - let frame = BodyFrame::function(self, target.stage, index, body.region, body.args)?; + let frame = BodyFrame::function(self, target.stage, index, body.cfg, body.args)?; self.frames.push(F::from_body(frame)); self.run() } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index e908b73680..823ff776bd 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -46,17 +46,17 @@ use std::marker::PhantomData; use kirin_ir::{ - Block, CompileStage, HasArguments, HasBottom, HasResults, Lattice, Pipeline, Region, SSAValue, + Block, Cfg, CompileStage, HasArguments, HasBottom, HasResults, Lattice, Pipeline, SSAValue, StageMeta, Statement, }; use super::frames::{DenseBlockFrame, DenseFrameBuild}; use crate::core::query; -use crate::engines::sparse_backward::RegionScope; +use crate::engines::sparse_backward::CfgScope; use crate::{ - AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, + AbstractInterpreter, BackwardSummaryDeps, CfgTopology, ClassicLiveness, DenseBackwardSemantic, DensePointStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, - InterpreterError, OwnerSemantics, ProgramPoint, RegionTopology, Scoped, StageQuery, + InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, }; @@ -321,7 +321,7 @@ where E: From, Sem: DenseBackwardSemantic, { - type SummaryKey = Scoped; + type SummaryKey = Scoped; type Summary = BlockLiveness; type Frame = F; type Completion = DenseBackwardCompletion; @@ -337,11 +337,11 @@ pub enum DenseBackwardCompletion { } /// Analysis-local state carried in the driver's `store` slot: the scope, -/// the region topology, and an optional per-point recorder filled by the +/// the CFG topology, and an optional per-point recorder filled by the /// block frames during [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). pub struct DenseAnalysisState { - scope: Option, - topology: RegionTopology, + scope: Option, + topology: CfgTopology, recorder: Option>, } @@ -349,7 +349,7 @@ impl Default for DenseAnalysisState { fn default() -> Self { Self { scope: None, - topology: RegionTopology::default(), + topology: CfgTopology::default(), recorder: None, } } @@ -362,7 +362,7 @@ pub type DenseBackwardDriver<'ir, S, V, E, F, Sem = ClassicLiveness> = StandardF DenseBackwardTransfer<'ir, S, V, E, F, Sem>, DenseBackwardProfile, DenseAnalysisState, - BackwardSummaryDeps>, + BackwardSummaryDeps>, >; // =========================================================================== @@ -548,7 +548,7 @@ struct DenseBackwardSemantics; impl<'ir, S, V, E, F, Sem> OwnerSemantics< DenseBackwardDriver<'ir, S, V, E, F, Sem>, - Scoped, + Scoped, BlockLiveness, F, DenseBackwardCompletion, @@ -564,7 +564,7 @@ where fn bottom_summary( &mut self, _interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - _owner: &Scoped, + _owner: &Scoped, ) -> Result, E> { Ok(BlockLiveness::bottom()) } @@ -572,10 +572,10 @@ where fn entry_frame( &mut self, interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - owner: &Scoped, + owner: &Scoped, _summary: &BlockLiveness, ) -> Result { - let (stage, _region) = owner.scope; + let (stage, _cfg) = owner.scope; // Each owner walk starts from an empty exit state; the terminator's // absorbed edges seed the real live-out. interp.replace_state(V::bottom()); @@ -585,9 +585,9 @@ where fn complete_owner( &mut self, _interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - owner: Scoped, + owner: Scoped, completion: DenseBackwardCompletion, - ) -> Result, BlockLiveness>, E> { + ) -> Result, BlockLiveness>, E> { match completion { DenseBackwardCompletion::Block { live_in, live_out } => Ok(SummaryEffect::Update { owner, @@ -608,8 +608,8 @@ where /// /// ```ignore /// let mut analysis = DenseBackwardInterpreter::::new(&pipeline); -/// analysis.analyze(stage, region)?; -/// let boundary = analysis.block_summary(stage, region, block); +/// analysis.analyze(stage, cfg)?; +/// let boundary = analysis.block_summary(stage, cfg, block); /// ``` pub struct DenseBackwardInterpreter< 'ir, @@ -649,18 +649,18 @@ where self.driver.inner().pipeline() } - /// The converged boundary states of `block` under the `(stage, region)` + /// The converged boundary states of `block` under the `(stage, cfg)` /// scope. pub fn block_summary( &self, stage: CompileStage, - region: Region, + cfg: Cfg, block: Block, ) -> Option<&BlockLiveness> { - self.driver.summary(&Scoped::new((stage, region), block)) + self.driver.summary(&Scoped::new((stage, cfg), block)) } - /// The analyzed region's CFG blocks (post-`analyze`). + /// The analyzed CFG's own top-level blocks (post-`analyze`). pub fn cfg_blocks(&self) -> Vec { self.driver .store() @@ -680,13 +680,13 @@ where F: Frame, Completion = DenseBackwardCompletion> + DenseFrameBuild, { - /// Run the block-boundary fixpoint over `region` in `stage`: seed every + /// Run the block-boundary fixpoint over `cfg` in `stage`: seed every /// CFG block (a backward analysis must visit them all) and drain the /// worklist; dependencies are discovered from the terminators' edges. - pub fn analyze(&mut self, stage: CompileStage, region: Region) -> Result<(), E> { - let scope = (stage, region); - let topology = query::region_topology(self.driver.inner().pipeline(), stage, region)?; - let owners: Vec> = topology + pub fn analyze(&mut self, stage: CompileStage, cfg: Cfg) -> Result<(), E> { + let scope = (stage, cfg); + let topology = query::cfg_topology(self.driver.inner().pipeline(), stage, cfg)?; + let owners: Vec> = topology .cfg_blocks() .map(|block| Scoped::new(scope, block.block)) .collect(); @@ -708,9 +708,9 @@ where pub fn reconstruct_points( &mut self, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> Result, E> { - let scope = (stage, region); + let scope = (stage, cfg); self.driver.store_mut().recorder = Some(DensePointStore::new()); for block in self.cfg_blocks() { // The CfgOwner walk re-absorbs the converged successor summaries, diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index 63b13fff5a..e5851b7b97 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -20,11 +20,11 @@ //! the real dispatch location, and the per-rule demand buffer. //! - the **[`StandardFixpointInterpreter`]** driver owns the demand facts //! (summaries keyed by [`Scoped`] SSA values — never bare values), the value -//! worklist, and the analysis state (scope + region topology). +//! worklist, and the analysis state (scope + CFG topology). //! //! # Owners are values; scheduling is demand propagation //! -//! `SummaryKey = Scoped<(CompileStage, Region), SSAValue>`: the fact anchor is +//! `SummaryKey = Scoped<(CompileStage, Cfg), SSAValue>`: the fact anchor is //! the owner. The driver's *default* self-dependent index //! ([`OwnerSummaryDeps`]) is exactly demand propagation — a value whose fact //! rises is rescheduled, and analyzing a value means dispatching the rules @@ -33,7 +33,7 @@ //! - a statement **result** → the defining statement's backward rule; //! - a **block argument** → each of the block's *feeders* (terminators //! targeting the block, statements owning it as a structured body) from the -//! [`RegionTopology`]; +//! [`CfgTopology`]; //! - a graph **port** → unsupported (loud error). //! //! Rules read converged facts ([`DemandInterp::is_demanded`]) and raise new @@ -49,23 +49,23 @@ use std::marker::PhantomData; use std::mem; use kirin_ir::{ - Block, CompileStage, HasArguments, HasBottom, HasResults, HasTop, IsPure, Lattice, Pipeline, - Region, SSAKind, SSAValue, StageMeta, Statement, + Block, Cfg, CompileStage, HasArguments, HasBottom, HasResults, HasTop, IsPure, Lattice, + Pipeline, SSAKind, SSAValue, StageMeta, Statement, }; use crate::core::query; use crate::{ - AbstractInterpreter, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, InterpDispatch, - InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, RegionTopology, Scoped, + AbstractInterpreter, CfgTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, + InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, Scoped, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, StrongDemand, Summary, SummaryEffect, }; -/// The scope a region-level backward analysis qualifies its facts with. +/// The scope a CFG-level backward analysis qualifies its facts with. /// /// Arena ids are per-stage, so the stage is part of the scope; analyzing two -/// regions in one engine keeps their facts distinct. -pub type RegionScope = (CompileStage, Region); +/// cfgs in one engine keeps their facts distinct. +pub type CfgScope = (CompileStage, Cfg); // =========================================================================== // Effect + dialect-facing trait @@ -275,18 +275,18 @@ where E: From, Sem: SparseBackwardSemantic, { - type SummaryKey = Scoped; + type SummaryKey = Scoped; type Summary = DemandSummary; type Frame = DemandFrame; type Completion = Vec<(SSAValue, V)>; } /// Analysis-local state carried in the driver's `store` slot: the scope facts -/// are qualified with, and the region topology (feeders for block arguments). +/// are qualified with, and the CFG topology (feeders for block arguments). #[derive(Default)] pub struct BackwardAnalysisState { - scope: Option, - topology: RegionTopology, + scope: Option, + topology: CfgTopology, } /// The sparse backward driver: a [`StandardFixpointInterpreter`] over @@ -296,7 +296,7 @@ pub type SparseBackwardDriver<'ir, S, V, E, Sem = StrongDemand> = StandardFixpoi SparseBackwardTransfer<'ir, S, V, E, Sem>, SparseBackwardProfile, BackwardAnalysisState, - OwnerSummaryDeps>, + OwnerSummaryDeps>, >; // =========================================================================== @@ -444,7 +444,7 @@ struct SparseBackwardSemantics; impl<'ir, S, V, E, Sem> OwnerSemantics< SparseBackwardDriver<'ir, S, V, E, Sem>, - Scoped, + Scoped, DemandSummary, DemandFrame, Vec<(SSAValue, V)>, @@ -459,7 +459,7 @@ where fn bottom_summary( &mut self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - _owner: &Scoped, + _owner: &Scoped, ) -> Result, E> { Ok(DemandSummary(V::bottom())) } @@ -467,10 +467,10 @@ where fn entry_frame( &mut self, interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - owner: &Scoped, + owner: &Scoped, _summary: &DemandSummary, ) -> Result, E> { - let (stage, _region) = owner.scope; + let (stage, _cfg) = owner.scope; let kind = query::value_kind(interp.inner().pipeline(), stage, owner.item)?; let work = match kind { SSAKind::Result(statement, _) => vec![statement], @@ -487,9 +487,9 @@ where fn complete_owner( &mut self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - owner: Scoped, + owner: Scoped, completion: Vec<(SSAValue, V)>, - ) -> Result, DemandSummary>, E> { + ) -> Result, DemandSummary>, E> { // Scope-qualify the bare values the rules demanded. Ok(SummaryEffect::Many( completion @@ -508,8 +508,8 @@ where /// /// ```ignore /// let mut analysis = SparseBackwardInterpreter::::new(&pipeline); -/// analysis.analyze(stage, region)?; -/// let demanded = analysis.is_demanded(stage, region, value); +/// analysis.analyze(stage, cfg)?; +/// let demanded = analysis.is_demanded(stage, cfg, value); /// ``` pub struct SparseBackwardInterpreter<'ir, S: StageMeta, V, E = InterpreterError, Sem = StrongDemand> where @@ -542,25 +542,16 @@ where self.driver.inner().pipeline() } - /// The converged demand fact for `value` under the `(stage, region)` scope. - pub fn fact( - &self, - stage: CompileStage, - region: Region, - value: impl Into, - ) -> Option<&V> { + /// The converged demand fact for `value` under the `(stage, cfg)` scope. + pub fn fact(&self, stage: CompileStage, cfg: Cfg, value: impl Into) -> Option<&V> { self.driver - .summary(&Scoped::new((stage, region), value.into())) + .summary(&Scoped::new((stage, cfg), value.into())) .map(|summary| &summary.0) } - /// All converged `(value, fact)` pairs under the `(stage, region)` scope. - pub fn facts( - &self, - stage: CompileStage, - region: Region, - ) -> impl Iterator { - let scope = (stage, region); + /// All converged `(value, fact)` pairs under the `(stage, cfg)` scope. + pub fn facts(&self, stage: CompileStage, cfg: Cfg) -> impl Iterator { + let scope = (stage, cfg); self.driver .summaries() .iter() @@ -568,11 +559,11 @@ where .map(|(owner, summary)| (owner.item, &summary.0)) } - /// The converged facts under the `(stage, region)` scope as a + /// The converged facts under the `(stage, cfg)` scope as a /// [`SparseStore`] (the sparse per-SSA-value fact view; absent = bottom). - pub fn fact_store(&self, stage: CompileStage, region: Region) -> SparseStore { + pub fn fact_store(&self, stage: CompileStage, cfg: Cfg) -> SparseStore { let mut store = SparseStore::new(); - for (value, fact) in self.facts(stage, region) { + for (value, fact) in self.facts(stage, cfg) { store.set(value, fact.clone()); } store @@ -586,16 +577,16 @@ where E: From, Sem: SparseBackwardSemantic, { - /// Run the demand fixpoint over `region` in `stage`. + /// Run the demand fixpoint over `cfg` in `stage`. /// - /// **Prepass**: enumerate the region topology (blocks including structured + /// **Prepass**: enumerate the CFG topology (blocks including structured /// bodies, statements, feeders), then run every statement's rule once with /// nothing demanded — impure statements and terminators contribute the /// demand roots. **Propagation**: drain the value worklist; each risen /// value dispatches the rules that translate its demand. - pub fn analyze(&mut self, stage: CompileStage, region: Region) -> Result<(), E> { - let scope = (stage, region); - let topology = query::region_topology(self.driver.inner().pipeline(), stage, region)?; + pub fn analyze(&mut self, stage: CompileStage, cfg: Cfg) -> Result<(), E> { + let scope = (stage, cfg); + let topology = query::cfg_topology(self.driver.inner().pipeline(), stage, cfg)?; let statements: Vec = topology .blocks .iter() @@ -628,16 +619,11 @@ where } /// `true` iff `value` carries a non-bottom demand fact under the scope. - pub fn is_demanded( - &self, - stage: CompileStage, - region: Region, - value: impl Into, - ) -> bool + pub fn is_demanded(&self, stage: CompileStage, cfg: Cfg, value: impl Into) -> bool where V: HasBottom, { - self.fact(stage, region, value) + self.fact(stage, cfg, value) .is_some_and(|fact| *fact != V::bottom()) } } diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs index b358fb66d1..eb2afc6176 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod interp; pub use interp::{ - BackwardAnalysisState, DemandFrame, DemandInterp, DemandSummary, RegionScope, + BackwardAnalysisState, CfgScope, DemandFrame, DemandInterp, DemandSummary, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 785ec4b010..f97af1e723 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -33,7 +33,7 @@ use std::hash::Hash; use std::marker::PhantomData; use kirin_ir::{ - Block, CompileStage, HasBottom, Pipeline, Product, Region, SSAValue, SpecializedFunction, + Block, Cfg, CompileStage, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, StageMeta, Statement, Widen, }; @@ -684,8 +684,8 @@ where query::next_statement(self.pipeline, stage, block, after).map_err(E::from) } - fn region_entry(&self, stage: CompileStage, region: Region) -> Result, E> { - query::region_entry(self.pipeline, stage, region).map_err(E::from) + fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, E> { + query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } } @@ -750,8 +750,8 @@ where self.inner().next_statement(stage, block, after) } - fn region_entry(&self, stage: CompileStage, region: Region) -> Result, E> { - self.inner().region_entry(stage, region) + fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, E> { + self.inner().cfg_entry(stage, cfg) } } @@ -1052,8 +1052,8 @@ where .expect("function summary present"); let body_info = self.enter_function(stage, body, entry_args, env)?; let entry_block = self - .region_entry(stage, body_info.region)? - .ok_or_else(|| E::from(InterpreterError::EmptyRegion))?; + .cfg_entry(stage, body_info.cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?; if let Some(function) = self .summary_mut(&Owner::Function(key.clone())) .and_then(|info| info.as_function_mut()) diff --git a/crates/kirin-interpreter/src/facts/anchor.rs b/crates/kirin-interpreter/src/facts/anchor.rs index 7fe407220e..6d40d05472 100644 --- a/crates/kirin-interpreter/src/facts/anchor.rs +++ b/crates/kirin-interpreter/src/facts/anchor.rs @@ -71,9 +71,9 @@ impl LatticeAnchor for DenseAnchor {} /// An anchor or owner qualified by the scope/context it belongs to. /// /// Framework-level summary keys are never bare anchors: the same [`SSAValue`] -/// or [`Block`] under two scopes (two stages, two analyzed regions, two call -/// contexts) is two distinct facts, so keys carry their scope. Region-level -/// analyses use `(CompileStage, Region)` as the scope; interprocedural +/// or [`Block`] under two scopes (two stages, two analyzed cfgs, two call +/// contexts) is two distinct facts, so keys carry their scope. Cfg-level +/// analyses use `(CompileStage, Cfg)` as the scope; interprocedural /// analyses generalize `K` to a call-context key (the backward analogue of the /// forward engine's context-qualified value keys). #[derive(Clone, Debug, PartialEq, Eq, Hash)] diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index 706ed4d8a5..abb02d8cca 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -1,5 +1,5 @@ //! Dataflow fact vocabulary: anchors (*where* facts attach), the polymorphic -//! fact stores, and region topology enumeration. Fixpoint clients use these, +//! fact stores, and CFG topology enumeration. Fixpoint clients use these, //! but they are dataflow vocabulary, not the convergence driver itself. pub(crate) mod anchor; @@ -8,4 +8,4 @@ pub(crate) mod topology; pub use anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; pub use store::{DenseBlockStore, DensePointStore, FactStore, ScopedSparseStore, SparseStore}; -pub use topology::{BlockTopology, RegionTopology, region_topology}; +pub use topology::{BlockTopology, CfgTopology, cfg_topology}; diff --git a/crates/kirin-interpreter/src/facts/store.rs b/crates/kirin-interpreter/src/facts/store.rs index 7f3d530850..f2e688aa8f 100644 --- a/crates/kirin-interpreter/src/facts/store.rs +++ b/crates/kirin-interpreter/src/facts/store.rs @@ -170,7 +170,7 @@ impl DenseBlockStore { #[cfg(test)] mod tests { - use kirin_ir::{Block, CompileStage, Id, Region, Statement, TestSSAValue}; + use kirin_ir::{Block, Cfg, CompileStage, Id, Statement, TestSSAValue}; use super::*; @@ -205,14 +205,14 @@ mod tests { #[test] fn scoped_anchors_keep_scopes_distinct() { - type Scope = (CompileStage, Region); + type Scope = (CompileStage, Cfg); let scope_a: Scope = ( CompileStage::from(Id::from(ssa(100))), - Region::from(Id::from(ssa(200))), + Cfg::from(Id::from(ssa(200))), ); let scope_b: Scope = ( CompileStage::from(Id::from(ssa(101))), - Region::from(Id::from(ssa(200))), + Cfg::from(Id::from(ssa(200))), ); let mut store: ScopedSparseStore = FactStore::new(); diff --git a/crates/kirin-interpreter/src/facts/topology.rs b/crates/kirin-interpreter/src/facts/topology.rs index 4abab5e5e8..d56675da6f 100644 --- a/crates/kirin-interpreter/src/facts/topology.rs +++ b/crates/kirin-interpreter/src/facts/topology.rs @@ -1,20 +1,18 @@ -//! Dialect-neutral region topology enumeration. +//! Dialect-neutral CFG topology enumeration. //! -//! Backward analyses need the *shape* of a region: which blocks exist +//! Backward analyses need the *shape* of a CFG: which blocks exist //! (including blocks nested inside structured statements), each block's //! statements, the CFG successor relation, and each block's *feeders* — the //! statements whose rules can translate demand on that block's parameters //! (terminators targeting it, statements owning it). This is topology only — //! uses/defs/edge-argument *semantics* stay in dialect //! [`Interpretable`](crate::Interpretable) rules; the enumeration consumes the -//! generic [`HasSuccessors`]/[`HasBlocks`]/[`HasRegions`] contract every +//! generic [`HasSuccessors`]/[`HasBlocks`]/[`HasCfgs`] contract every //! dialect derives. use std::collections::{HashMap, HashSet}; -use kirin_ir::{ - Block, Dialect, HasBlocks, HasRegions, HasSuccessors, Region, StageInfo, Statement, -}; +use kirin_ir::{Block, Cfg, Dialect, HasBlocks, HasCfgs, HasSuccessors, StageInfo, Statement}; /// The shape of one block: its statements and CFG successors. #[derive(Clone, Debug)] @@ -25,19 +23,19 @@ pub struct BlockTopology { /// CFG successor blocks (targets of the block's terminator). pub successors: Vec, /// `true` for blocks nested inside a statement (structured bodies), - /// `false` for the analyzed region's own CFG blocks. + /// `false` for the analyzed CFG's own top-level blocks. pub nested: bool, } -/// The shape of a region: all blocks (region CFG blocks first-level and +/// The shape of a CFG: all blocks (the CFG's own top-level blocks and /// structured bodies, recursively) plus the block-feeder index. #[derive(Clone, Debug, Default)] -pub struct RegionTopology { +pub struct CfgTopology { pub blocks: Vec, feeders: HashMap>, } -impl RegionTopology { +impl CfgTopology { /// The statements whose rules can translate demand on `block`'s parameters: /// terminators with an edge into `block`, plus statements owning `block` /// as a structured body. @@ -45,21 +43,21 @@ impl RegionTopology { self.feeders.get(&block).map(Vec::as_slice).unwrap_or(&[]) } - /// The analyzed region's own CFG blocks (excluding nested bodies). + /// The analyzed CFG's own top-level blocks (excluding nested bodies). pub fn cfg_blocks(&self) -> impl Iterator { self.blocks.iter().filter(|block| !block.nested) } } -/// Enumerate the topology of `region` in the finalized `stage`. -pub fn region_topology(stage: &StageInfo, region: &Region) -> RegionTopology +/// Enumerate the topology of `cfg` in the finalized `stage`. +pub fn cfg_topology(stage: &StageInfo, cfg: &Cfg) -> CfgTopology where L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasRegions<'a>, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a>, { - let mut topology = RegionTopology::default(); + let mut topology = CfgTopology::default(); let mut visited = HashSet::new(); - for block in region.blocks(stage) { + for block in cfg.blocks(stage) { collect_block(stage, block, false, &mut topology, &mut visited); } topology @@ -69,11 +67,11 @@ fn collect_block( stage: &StageInfo, block: Block, nested: bool, - topology: &mut RegionTopology, + topology: &mut CfgTopology, visited: &mut HashSet, ) where L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasRegions<'a>, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a>, { if !visited.insert(block) { return; @@ -105,13 +103,13 @@ fn collect_block( for &stmt in &stmts { let definition = stmt.definition(stage); let owned_blocks: Vec = definition.blocks().copied().collect(); - let owned_regions: Vec = definition.regions().copied().collect(); + let owned_cfgs: Vec = definition.cfgs().copied().collect(); for owned in owned_blocks { topology.feeders.entry(owned).or_default().push(stmt); collect_block(stage, owned, true, topology, visited); } - for owned_region in owned_regions { - for owned in owned_region.blocks(stage) { + for owned_cfg in owned_cfgs { + for owned in owned_cfg.blocks(stage) { topology.feeders.entry(owned).or_default().push(stmt); collect_block(stage, owned, true, topology, visited); } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 9537d42d35..4f8055eea3 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -94,7 +94,7 @@ pub use engines::sparse_forward::{ }; // Sparse backward engine (`Sem = StrongDemand`). pub use engines::sparse_backward::{ - BackwardAnalysisState, DemandFrame, DemandInterp, DemandSummary, RegionScope, + BackwardAnalysisState, CfgScope, DemandFrame, DemandInterp, DemandSummary, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; @@ -107,11 +107,11 @@ pub use engines::dense_backward::{ }; // Lattice anchors (*where* facts attach), scope qualification, the polymorphic -// fact stores, and region topology enumeration. Anchor family is a property of +// fact stores, and cfg topology enumeration. Anchor family is a property of // the solver shape; dispatch meaning lives in `semantics`. pub use facts::{ - BlockTopology, Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, LatticeAnchor, - ProgramPoint, RegionTopology, Scoped, ScopedSparseStore, SparseStore, region_topology, + BlockTopology, CfgTopology, Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, + LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, SparseStore, cfg_topology, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` diff --git a/crates/kirin-ir/src/builder/block.rs b/crates/kirin-ir/src/builder/block.rs index 3149a4bb88..8508457b03 100644 --- a/crates/kirin-ir/src/builder/block.rs +++ b/crates/kirin-ir/src/builder/block.rs @@ -5,7 +5,7 @@ use crate::{BuilderStageInfo, Dialect}; pub struct BlockBuilder<'a, L: Dialect> { stage: &'a mut BuilderStageInfo, - parent: Option, + parent: Option, name: Option, arguments: Vec<(L::Type, Option)>, statements: Vec, @@ -24,8 +24,8 @@ impl<'a, L: Dialect> BlockBuilder<'a, L> { } } - /// Attach the block to a parent region without pushing it to the region's block list. - pub fn parent(mut self, parent: Region) -> Self { + /// Attach the block to a parent cfg without pushing it to the CFG's block list. + pub fn parent(mut self, parent: Cfg) -> Self { self.parent = Some(parent); self } diff --git a/crates/kirin-ir/src/builder/cfg.rs b/crates/kirin-ir/src/builder/cfg.rs index 0300f552a1..f28aa77c75 100644 --- a/crates/kirin-ir/src/builder/cfg.rs +++ b/crates/kirin-ir/src/builder/cfg.rs @@ -1,12 +1,12 @@ -use crate::{Block, BuilderStageInfo, Dialect, Region, Statement, node::RegionInfo}; +use crate::{Block, BuilderStageInfo, Cfg, Dialect, Statement, node::CfgInfo}; -pub struct RegionBuilder<'a, L: Dialect> { +pub struct CfgBuilder<'a, L: Dialect> { pub(super) stage: &'a mut BuilderStageInfo, pub(super) parent: Option, pub(super) blocks: Vec, } -impl<'a, L: Dialect> RegionBuilder<'a, L> { +impl<'a, L: Dialect> CfgBuilder<'a, L> { pub fn from_stage(stage: &'a mut BuilderStageInfo) -> Self { Self { stage, @@ -22,21 +22,21 @@ impl<'a, L: Dialect> RegionBuilder<'a, L> { pub fn add_block(mut self, block: Block) -> Self { if self.blocks.contains(&block) { - panic!("Block `{}` is already added to the region", block); + panic!("Block `{}` is already added to the cfg", block); } self.blocks.push(block); self } #[allow(clippy::wrong_self_convention, clippy::new_ret_no_self)] - pub fn new(self) -> Region { - let id = self.stage.regions.next_id(); - let info = RegionInfo::builder() + pub fn new(self) -> Cfg { + let id = self.stage.cfgs.next_id(); + let info = CfgInfo::builder() .id(id) .blocks(self.stage.link_blocks(&self.blocks)) .maybe_parent(self.parent) .new(); - let _ = self.stage.regions.alloc(info); + let _ = self.stage.cfgs.alloc(info); id } } diff --git a/crates/kirin-ir/src/builder/context.rs b/crates/kirin-ir/src/builder/context.rs index 262bd707dc..12a64bbaec 100644 --- a/crates/kirin-ir/src/builder/context.rs +++ b/crates/kirin-ir/src/builder/context.rs @@ -1,6 +1,6 @@ use super::block::BlockBuilder; +use super::cfg::CfgBuilder; use super::digraph::DiGraphBuilder; -use super::region::RegionBuilder; use super::ungraph::UnGraphBuilder; use crate::{BuilderStageInfo, Dialect}; @@ -10,8 +10,8 @@ impl BuilderStageInfo { BlockBuilder::from_stage(self) } - pub fn region(&mut self) -> RegionBuilder<'_, L> { - RegionBuilder::from_stage(self) + pub fn cfg(&mut self) -> CfgBuilder<'_, L> { + CfgBuilder::from_stage(self) } pub fn digraph(&mut self) -> DiGraphBuilder<'_, L> { diff --git a/crates/kirin-ir/src/builder/mod.rs b/crates/kirin-ir/src/builder/mod.rs index c9c9b30f7b..570fb98287 100644 --- a/crates/kirin-ir/src/builder/mod.rs +++ b/crates/kirin-ir/src/builder/mod.rs @@ -12,7 +12,7 @@ //! → stage.statement() create statements //! → stage.block_argument() create placeholder SSAs //! → stage.block() build blocks (resolves placeholders) -//! → stage.region() group blocks into regions +//! → stage.cfg() group blocks into cfgs //! → stage.staged_function() register callable functions //! → stage.specialize() add specializations //! → stage.finalize() validate → StageInfo (clean SSAInfo) @@ -21,12 +21,12 @@ //! See [`BuilderStageInfo`] for detailed usage examples. mod block; +mod cfg; mod context; pub mod digraph; pub mod error; mod graph_common; mod redefine; -mod region; mod stage_info; mod staged; pub mod ungraph; diff --git a/crates/kirin-ir/src/builder/stage_info.rs b/crates/kirin-ir/src/builder/stage_info.rs index 77b3220b66..5147833c81 100644 --- a/crates/kirin-ir/src/builder/stage_info.rs +++ b/crates/kirin-ir/src/builder/stage_info.rs @@ -64,7 +64,7 @@ impl std::error::Error for FinalizeError {} /// Builder for constructing IR within a single compilation stage. /// /// `BuilderStageInfo` holds the same node arenas as [`StageInfo`] (blocks, -/// statements, regions, graphs, etc.) but uses [`BuilderSSAInfo`] for the SSA +/// statements, cfgs, graphs, etc.) but uses [`BuilderSSAInfo`] for the SSA /// arena — allowing `Option` and [`BuilderSSAKind`] placeholders during /// construction. /// @@ -105,11 +105,11 @@ impl std::error::Error for FinalizeError {} /// .new(); /// ``` /// -/// Regions (containers of blocks): +/// Cfgs (containers of blocks): /// ```ignore /// let entry = stage.block().new(); /// let exit = stage.block().new(); -/// let region = stage.region().add_block(entry).add_block(exit).new(); +/// let cfg = stage.cfg().add_block(entry).add_block(exit).new(); /// ``` /// /// # Finalization diff --git a/crates/kirin-ir/src/language.rs b/crates/kirin-ir/src/language.rs index 1eab8ba445..6b735e0c59 100644 --- a/crates/kirin-ir/src/language.rs +++ b/crates/kirin-ir/src/language.rs @@ -42,14 +42,14 @@ pub trait HasSuccessorsMut<'a> { fn successors_mut(&'a mut self) -> Self::IterMut; } -pub trait HasRegions<'a> { - type Iter: Iterator; - fn regions(&'a self) -> Self::Iter; +pub trait HasCfgs<'a> { + type Iter: Iterator; + fn cfgs(&'a self) -> Self::Iter; } -pub trait HasRegionsMut<'a> { - type IterMut: Iterator; - fn regions_mut(&'a mut self) -> Self::IterMut; +pub trait HasCfgsMut<'a> { + type IterMut: Iterator; + fn cfgs_mut(&'a mut self) -> Self::IterMut; } pub trait HasDigraphs<'a> { @@ -72,17 +72,17 @@ pub trait HasUngraphsMut<'a> { fn ungraphs_mut(&'a mut self) -> Self::IterMut; } -/// Structural trait for dialect operations that have a single region body. +/// Structural trait for dialect operations that have a single CFG body. /// /// This trait is intentionally not a supertrait of `Dialect` — it applies to /// individual operations (e.g., `FunctionBody`, `Lambda`) that contain a single -/// `Region`, not to the dialect enum itself. It enables shared helper functions -/// for interpreter and analysis code that operate on region-bearing operations. -pub trait HasRegionBody { - fn region(&self) -> &crate::Region; +/// `Cfg`, not to the dialect enum itself. It enables shared helper functions +/// for interpreter and analysis code that operate on CFG-bearing operations. +pub trait HasCfgBody { + fn cfg(&self) -> &crate::Cfg; fn entry_block(&self, stage: &crate::StageInfo) -> Option { - self.region().blocks(stage).next() + self.cfg().blocks(stage).next() } } @@ -127,8 +127,8 @@ pub trait Dialect: + for<'a> HasBlocksMut<'a> + for<'a> HasSuccessors<'a> + for<'a> HasSuccessorsMut<'a> - + for<'a> HasRegions<'a> - + for<'a> HasRegionsMut<'a> + + for<'a> HasCfgs<'a> + + for<'a> HasCfgsMut<'a> + for<'a> HasDigraphs<'a> + for<'a> HasDigraphsMut<'a> + for<'a> HasUngraphs<'a> diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index a2b23f527b..4c972f4a45 100644 --- a/crates/kirin-ir/src/lib.rs +++ b/crates/kirin-ir/src/lib.rs @@ -25,17 +25,17 @@ pub use comptime::{CompileTimeValue, Placeholder, Typeof}; pub use detach::Detach; pub use intern::InternTable; pub use language::{ - Dialect, HasArguments, HasArgumentsMut, HasBlocks, HasBlocksMut, HasDigraphs, HasDigraphsMut, - HasRegionBody, HasRegions, HasRegionsMut, HasResults, HasResultsMut, HasSuccessors, + Dialect, HasArguments, HasArgumentsMut, HasBlocks, HasBlocksMut, HasCfgBody, HasCfgs, + HasCfgsMut, HasDigraphs, HasDigraphsMut, HasResults, HasResultsMut, HasSuccessors, HasSuccessorsMut, HasUngraphs, HasUngraphsMut, IsConstant, IsEdge, IsPure, IsSpeculatable, IsTerminator, }; pub use lattice::{FiniteLattice, HasBottom, HasTop, Lattice, TypeLattice, Widen}; pub use node::{ - Block, BlockArgument, BlockInfo, BuilderKey, BuilderSSAInfo, BuilderSSAKind, CompileStage, + Block, BlockArgument, BlockInfo, BuilderKey, BuilderSSAInfo, BuilderSSAKind, Cfg, CompileStage, DeletedSSAValue, DiGraph, DiGraphExtra, DiGraphInfo, Function, FunctionInfo, GlobalSymbol, - GraphInfo, LinkedList, LinkedListNode, Port, PortParent, Region, ResolutionInfo, ResultValue, - SSAInfo, SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, StagedFunction, + GraphInfo, LinkedList, LinkedListNode, Port, PortParent, ResolutionInfo, ResultValue, SSAInfo, + SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, StagedFunction, StagedFunctionInfo, StagedNamePolicy, Statement, StatementInfo, StatementParent, Successor, Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, UniqueLiveSpecializationError, }; @@ -54,9 +54,9 @@ pub use stage::{ /// Re-exports of the most commonly used types for dialect authors. pub mod prelude { pub use crate::{ - Block, BuilderStageInfo, CompileStage, Dialect, Function, GetInfo, HasRegionBody, - HasSignature, HasStageInfo, Pipeline, Region, ResultValue, SSAValue, Signature, - SignatureSemantics, StageInfo, StageMeta, Statement, + Block, BuilderStageInfo, Cfg, CompileStage, Dialect, Function, GetInfo, HasCfgBody, + HasSignature, HasStageInfo, Pipeline, ResultValue, SSAValue, Signature, SignatureSemantics, + StageInfo, StageMeta, Statement, }; pub use crate::{ CompileTimeValue, HasProduct, Placeholder, Product, Project, ProjectError, TryProject, @@ -66,7 +66,7 @@ pub mod prelude { #[cfg(feature = "derive")] pub use kirin_derive_ir::{ - Dialect, HasArguments, HasDigraphs, HasRegions, HasResults, HasSuccessors, HasUngraphs, + Dialect, HasArguments, HasCfgs, HasDigraphs, HasResults, HasSuccessors, HasUngraphs, IsConstant, IsEdge, IsPure, IsSpeculatable, IsTerminator, LiftProject, ParseDispatch, StageMeta, }; diff --git a/crates/kirin-ir/src/node/block.rs b/crates/kirin-ir/src/node/block.rs index b20f900f8c..9768678411 100644 --- a/crates/kirin-ir/src/node/block.rs +++ b/crates/kirin-ir/src/node/block.rs @@ -2,7 +2,7 @@ use crate::{ Dialect, Symbol, arena::{GetInfo, Id, Item}, identifier, - node::region::Region, + node::cfg::Cfg, }; use super::{ @@ -51,7 +51,7 @@ impl std::fmt::Display for Successor { #[derive(Clone, Debug, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlockInfo { - pub parent: Option, + pub parent: Option, pub name: Option, pub node: LinkedListNode, pub arguments: Vec, @@ -64,8 +64,8 @@ pub struct BlockInfo { impl BlockInfo { #[builder(finish_fn = new)] pub(crate) fn new( - /// The parent region of this block. - parent: Option, + /// The parent cfg of this block. + parent: Option, /// The name of this block. name: Option, /// The linked list node for this block. diff --git a/crates/kirin-ir/src/node/cfg.rs b/crates/kirin-ir/src/node/cfg.rs index 07d5d24dae..963d9f0e62 100644 --- a/crates/kirin-ir/src/node/cfg.rs +++ b/crates/kirin-ir/src/node/cfg.rs @@ -6,27 +6,27 @@ use super::linked_list::LinkedList; use super::stmt::Statement; identifier! { - /// A unique identifier for a region. - struct Region + /// A unique identifier for a CFG (a block-list control-flow body). + struct Cfg } #[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct RegionInfo { - pub(crate) id: Region, +pub struct CfgInfo { + pub(crate) id: Cfg, pub(crate) parent: Option, pub(crate) blocks: LinkedList, _marker: std::marker::PhantomData, } #[bon::bon] -impl RegionInfo { +impl CfgInfo { #[builder(finish_fn = new)] pub fn new( - /// The unique identifier for this region. - id: Region, - /// The parent statement of this region, if any. + /// The unique identifier for this CFG. + id: Cfg, + /// The parent statement of this CFG, if any. parent: Option, - /// The blocks contained in this region. + /// The blocks contained in this CFG. blocks: LinkedList, ) -> Self { Self { @@ -38,19 +38,19 @@ impl RegionInfo { } } -impl GetInfo for Region { - type Info = Item>; +impl GetInfo for Cfg { + type Info = Item>; fn get_info<'a>(&self, stage: &'a crate::StageInfo) -> Option<&'a Self::Info> { - stage.regions.get(*self) + stage.cfgs.get(*self) } fn get_info_mut<'a>(&self, stage: &'a mut crate::StageInfo) -> Option<&'a mut Self::Info> { - stage.regions.get_mut(*self) + stage.cfgs.get_mut(*self) } } -impl Region { +impl Cfg { pub fn blocks<'a, L: Dialect>(&self, stage: &'a crate::StageInfo) -> BlockIter<'a, L> { let info = self.expect_info(stage); BlockIter { diff --git a/crates/kirin-ir/src/node/mod.rs b/crates/kirin-ir/src/node/mod.rs index 33a54e3469..c3e1a848d0 100644 --- a/crates/kirin-ir/src/node/mod.rs +++ b/crates/kirin-ir/src/node/mod.rs @@ -1,16 +1,17 @@ pub mod block; +pub mod cfg; pub(crate) mod digraph; pub mod function; pub(crate) mod graph; pub mod linked_list; pub(crate) mod port; -pub mod region; pub mod ssa; pub mod stmt; pub mod symbol; pub(crate) mod ungraph; pub use block::{Block, BlockInfo, Successor}; +pub use cfg::{Cfg, CfgInfo}; pub use digraph::{DiGraph, DiGraphInfo}; pub use function::{ CompileStage, Function, FunctionInfo, SpecializedFunction, SpecializedFunctionInfo, @@ -19,7 +20,6 @@ pub use function::{ pub use graph::{DiGraphExtra, GraphInfo, UnGraphExtra}; pub use linked_list::{LinkedList, LinkedListNode}; pub use port::{Port, PortParent}; -pub use region::{Region, RegionInfo}; pub use ssa::{ BlockArgument, BuilderKey, BuilderSSAInfo, BuilderSSAKind, DeletedSSAValue, ResolutionInfo, ResultValue, SSAInfo, SSAKind, SSAValue, TestSSAValue, diff --git a/crates/kirin-ir/src/node/stmt.rs b/crates/kirin-ir/src/node/stmt.rs index a0296e5c4e..0cad0d9ea4 100644 --- a/crates/kirin-ir/src/node/stmt.rs +++ b/crates/kirin-ir/src/node/stmt.rs @@ -60,11 +60,11 @@ impl Statement { self.expect_info(stage).definition.arguments() } - pub fn regions<'a, L: Dialect>( + pub fn cfgs<'a, L: Dialect>( &self, stage: &'a crate::StageInfo, - ) -> >::Iter { - self.expect_info(stage).definition.regions() + ) -> >::Iter { + self.expect_info(stage).definition.cfgs() } pub fn blocks<'a, L: Dialect>( diff --git a/crates/kirin-ir/src/query/info.rs b/crates/kirin-ir/src/query/info.rs index d4307b56ee..6dbfbb21ea 100644 --- a/crates/kirin-ir/src/query/info.rs +++ b/crates/kirin-ir/src/query/info.rs @@ -1,7 +1,7 @@ use crate::{ Dialect, LinkedList, node::{ - Block, BlockInfo, LinkedListNode, Region, RegionInfo, Statement, StatementInfo, + Block, BlockInfo, Cfg, CfgInfo, LinkedListNode, Statement, StatementInfo, stmt::StatementParent, }, }; @@ -26,7 +26,7 @@ impl ParentInfo for StatementInfo { } impl ParentInfo for BlockInfo { - type ParentPtr = Region; + type ParentPtr = Cfg; fn get_parent(&self) -> &Option { &self.parent } @@ -82,7 +82,7 @@ impl LinkedListInfo for BlockInfo { } } -impl LinkedListInfo for RegionInfo { +impl LinkedListInfo for CfgInfo { type Ptr = Block; fn get_linked_list(&self) -> &LinkedList { &self.blocks diff --git a/crates/kirin-ir/src/stage/arenas.rs b/crates/kirin-ir/src/stage/arenas.rs index 8a6b3e0795..8f40c24a0e 100644 --- a/crates/kirin-ir/src/stage/arenas.rs +++ b/crates/kirin-ir/src/stage/arenas.rs @@ -1,7 +1,7 @@ use crate::arena::Arena; +use crate::node::cfg::CfgInfo; use crate::node::digraph::{DiGraph, DiGraphInfo}; use crate::node::function::CompileStage; -use crate::node::region::RegionInfo; use crate::node::ungraph::{UnGraph, UnGraphInfo}; use crate::{Dialect, InternTable, node::*}; @@ -22,7 +22,7 @@ pub struct Arenas { pub(crate) stage_id: Option, pub(crate) staged_functions: Arena>, pub(crate) staged_name_policy: StagedNamePolicy, - pub(crate) regions: Arena>, + pub(crate) cfgs: Arena>, pub(crate) blocks: Arena>, pub(crate) statements: Arena>, pub(crate) digraphs: Arena>, @@ -40,7 +40,7 @@ where stage_id: None, staged_functions: Arena::default(), staged_name_policy: StagedNamePolicy::default(), - regions: Arena::default(), + cfgs: Arena::default(), blocks: Arena::default(), statements: Arena::default(), digraphs: Arena::default(), @@ -61,7 +61,7 @@ where stage_id: self.stage_id, staged_functions: self.staged_functions.clone(), staged_name_policy: self.staged_name_policy, - regions: self.regions.clone(), + cfgs: self.cfgs.clone(), blocks: self.blocks.clone(), statements: self.statements.clone(), digraphs: self.digraphs.clone(), @@ -127,9 +127,9 @@ impl Arenas { self.staged_name_policy = policy; } - /// Get a reference to the regions arena. - pub fn region_arena(&self) -> &Arena> { - &self.regions + /// Get a reference to the cfgs arena. + pub fn cfg_arena(&self) -> &Arena> { + &self.cfgs } /// Get a reference to the blocks arena. diff --git a/crates/kirin-ir/src/stage/info.rs b/crates/kirin-ir/src/stage/info.rs index d38c156f90..aadf8112cd 100644 --- a/crates/kirin-ir/src/stage/info.rs +++ b/crates/kirin-ir/src/stage/info.rs @@ -8,7 +8,7 @@ use super::arenas::Arenas; /// Finalized IR for a single compilation stage. /// -/// `StageInfo` holds the node arenas (blocks, statements, regions, graphs, +/// `StageInfo` holds the node arenas (blocks, statements, cfgs, graphs, /// functions) and a clean SSA arena where every value has a resolved type and /// kind. It is the read-only output of [`BuilderStageInfo::finalize`]. /// @@ -55,8 +55,8 @@ use super::arenas::Arenas; /// let arg = b.block_argument().index(0); /// let ret = b.statement().definition(MyDialect::Return(arg)).new(); /// let block = b.block().argument(MyType::I64).terminator(ret).new(); -/// let region = b.region().add_block(block).new(); -/// let body = b.statement().definition(MyDialect::FuncBody(region)).new(); +/// let cfg = b.cfg().add_block(block).new(); +/// let body = b.statement().definition(MyDialect::FuncBody(cfg)).new(); /// /// b.specialize().staged_func(sf).body(body).new().unwrap(); /// }); diff --git a/crates/kirin-ir/src/stage/tests.rs b/crates/kirin-ir/src/stage/tests.rs index 59495b651f..4cfc10bd91 100644 --- a/crates/kirin-ir/src/stage/tests.rs +++ b/crates/kirin-ir/src/stage/tests.rs @@ -2,11 +2,11 @@ use std::convert::Infallible; use super::*; use crate::{ - Block, CompileStage, DiGraph, Dialect, GlobalSymbol, HasArguments, HasArgumentsMut, HasBlocks, - HasBlocksMut, HasDigraphs, HasDigraphsMut, HasRegions, HasRegionsMut, HasResults, + Block, Cfg, CompileStage, DiGraph, Dialect, GlobalSymbol, HasArguments, HasArgumentsMut, + HasBlocks, HasBlocksMut, HasCfgs, HasCfgsMut, HasDigraphs, HasDigraphsMut, HasResults, HasResultsMut, HasStageInfo, HasSuccessors, HasSuccessorsMut, HasUngraphs, HasUngraphsMut, Id, - IsConstant, IsEdge, IsPure, IsSpeculatable, IsTerminator, Pipeline, Region, ResultValue, - SSAValue, StageInfo, StageMeta, StagedNamePolicy, Successor, UnGraph, + IsConstant, IsEdge, IsPure, IsSpeculatable, IsTerminator, Pipeline, ResultValue, SSAValue, + StageInfo, StageMeta, StagedNamePolicy, Successor, UnGraph, }; #[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] @@ -89,18 +89,18 @@ macro_rules! impl_empty_dialect_traits { } } - impl<'a> HasRegions<'a> for $dialect { - type Iter = std::iter::Empty<&'a Region>; + impl<'a> HasCfgs<'a> for $dialect { + type Iter = std::iter::Empty<&'a Cfg>; - fn regions(&'a self) -> Self::Iter { + fn cfgs(&'a self) -> Self::Iter { std::iter::empty() } } - impl<'a> HasRegionsMut<'a> for $dialect { - type IterMut = std::iter::Empty<&'a mut Region>; + impl<'a> HasCfgsMut<'a> for $dialect { + type IterMut = std::iter::Empty<&'a mut Cfg>; - fn regions_mut(&'a mut self) -> Self::IterMut { + fn cfgs_mut(&'a mut self) -> Self::IterMut { std::iter::empty() } } diff --git a/crates/kirin-ir/tests/builder_block.rs b/crates/kirin-ir/tests/builder_block.rs index 860d4d45ed..6473ad6e78 100644 --- a/crates/kirin-ir/tests/builder_block.rs +++ b/crates/kirin-ir/tests/builder_block.rs @@ -1,4 +1,4 @@ -//! Integration tests for block builder, region builder, statement iteration, +//! Integration tests for block builder, cfg builder, statement iteration, //! detach, SSA creation, and linked list helpers. mod common; @@ -6,13 +6,13 @@ mod common; use common::{BuilderDialect, TestType, new_stage}; use kirin_ir::*; -struct RegionBodyOp { - region: Region, +struct CfgBodyOp { + cfg: Cfg, } -impl HasRegionBody for RegionBodyOp { - fn region(&self) -> &Region { - &self.region +impl HasCfgBody for CfgBodyOp { + fn cfg(&self) -> &Cfg { + &self.cfg } } @@ -226,25 +226,20 @@ fn block_argument_placeholder_substitution_with_zero_args() { assert!(info.arguments.is_empty()); } -// --- RegionBuilder tests --- +// --- CfgBuilder tests --- #[test] -fn region_builder_creates_region_with_ordered_blocks() { +fn cfg_builder_creates_cfg_with_ordered_blocks() { let mut stage = new_stage(); let b0 = stage.block().new(); let b1 = stage.block().new(); let b2 = stage.block().new(); - let region = stage - .region() - .add_block(b0) - .add_block(b1) - .add_block(b2) - .new(); + let cfg = stage.cfg().add_block(b0).add_block(b1).add_block(b2).new(); let stage = stage.finalize().unwrap(); - assert_eq!(region.blocks(&stage).len(), 3); - let blocks: Vec<_> = region.blocks(&stage).collect(); + assert_eq!(cfg.blocks(&stage).len(), 3); + let blocks: Vec<_> = cfg.blocks(&stage).collect(); assert_eq!(blocks, vec![b0, b1, b2]); let b0_info = b0.expect_info(&stage); @@ -258,52 +253,47 @@ fn region_builder_creates_region_with_ordered_blocks() { } #[test] -fn has_region_body_entry_block_returns_first_block() { +fn has_cfg_body_entry_block_returns_first_block() { let mut stage = new_stage(); let b0 = stage.block().new(); let b1 = stage.block().new(); - let region = stage.region().add_block(b0).add_block(b1).new(); - let op = RegionBodyOp { region }; + let cfg = stage.cfg().add_block(b0).add_block(b1).new(); + let op = CfgBodyOp { cfg }; let stage = stage.finalize().unwrap(); assert_eq!(op.entry_block(&stage), Some(b0)); } #[test] -#[should_panic(expected = "already added to the region")] -fn region_builder_panics_on_duplicate_block() { +#[should_panic(expected = "already added to the cfg")] +fn cfg_builder_panics_on_duplicate_block() { let mut stage = new_stage(); let b0 = stage.block().new(); - let _ = stage.region().add_block(b0).add_block(b0).new(); + let _ = stage.cfg().add_block(b0).add_block(b0).new(); } #[test] -fn region_block_iter_single_block() { +fn cfg_block_iter_single_block() { let mut stage = new_stage(); let b0 = stage.block().new(); - let region = stage.region().add_block(b0).new(); + let cfg = stage.cfg().add_block(b0).new(); let stage = stage.finalize().unwrap(); - let blocks: Vec<_> = region.blocks(&stage).collect(); + let blocks: Vec<_> = cfg.blocks(&stage).collect(); assert_eq!(blocks, vec![b0]); - assert_eq!(region.blocks(&stage).len(), 1); + assert_eq!(cfg.blocks(&stage).len(), 1); } #[test] -fn region_block_iter_double_ended() { +fn cfg_block_iter_double_ended() { let mut stage = new_stage(); let b0 = stage.block().new(); let b1 = stage.block().new(); let b2 = stage.block().new(); - let region = stage - .region() - .add_block(b0) - .add_block(b1) - .add_block(b2) - .new(); + let cfg = stage.cfg().add_block(b0).add_block(b1).add_block(b2).new(); let stage = stage.finalize().unwrap(); - let mut iter = region.blocks(&stage); + let mut iter = cfg.blocks(&stage); assert_eq!(iter.next_back(), Some(b2)); assert_eq!(iter.next(), Some(b0)); assert_eq!(iter.next_back(), Some(b1)); @@ -312,14 +302,14 @@ fn region_block_iter_double_ended() { } #[test] -fn region_block_iter_exact_size() { +fn cfg_block_iter_exact_size() { let mut stage = new_stage(); let b0 = stage.block().new(); let b1 = stage.block().new(); - let region = stage.region().add_block(b0).add_block(b1).new(); + let cfg = stage.cfg().add_block(b0).add_block(b1).new(); let stage = stage.finalize().unwrap(); - let mut iter = region.blocks(&stage); + let mut iter = cfg.blocks(&stage); assert_eq!(iter.len(), 2); iter.next(); assert_eq!(iter.len(), 1); @@ -328,14 +318,14 @@ fn region_block_iter_exact_size() { } #[test] -fn empty_region() { +fn empty_cfg() { let mut stage = new_stage(); - let region = stage.region().new(); + let cfg = stage.cfg().new(); let stage = stage.finalize().unwrap(); - let blocks: Vec<_> = region.blocks(&stage).collect(); + let blocks: Vec<_> = cfg.blocks(&stage).collect(); assert!(blocks.is_empty()); - assert_eq!(region.blocks(&stage).len(), 0); + assert_eq!(cfg.blocks(&stage).len(), 0); } // --- Detach tests --- diff --git a/crates/kirin-ir/tests/common.rs b/crates/kirin-ir/tests/common.rs index 52c3603c07..e32a46e762 100644 --- a/crates/kirin-ir/tests/common.rs +++ b/crates/kirin-ir/tests/common.rs @@ -124,16 +124,16 @@ impl<'a> HasSuccessorsMut<'a> for BuilderDialect { } } -impl<'a> HasRegions<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a Region>; - fn regions(&'a self) -> Self::Iter { +impl<'a> HasCfgs<'a> for BuilderDialect { + type Iter = std::iter::Empty<&'a Cfg>; + fn cfgs(&'a self) -> Self::Iter { std::iter::empty() } } -impl<'a> HasRegionsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut Region>; - fn regions_mut(&'a mut self) -> Self::IterMut { +impl<'a> HasCfgsMut<'a> for BuilderDialect { + type IterMut = std::iter::Empty<&'a mut Cfg>; + fn cfgs_mut(&'a mut self) -> Self::IterMut { std::iter::empty() } } diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 74b78534a2..56f90893a2 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -15,7 +15,7 @@ //! sets are the intersection of the dense sets with this demand set. //! //! ```ignore -//! let result = kirin_liveness::analyze_demand(&pipeline, stage, region)?; +//! let result = kirin_liveness::analyze_demand(&pipeline, stage, cfg)?; //! assert!(result.is_demanded(some_value)); //! ``` @@ -29,7 +29,7 @@ use kirin_interpreter::{ DenseBackwardInterpreter, DenseBackwardTransfer, InterpDispatch, InterpreterError, SparseBackwardDriver, SparseBackwardInterpreter, StageQuery, StandardDenseBackwardFrame, }; -use kirin_ir::{CompileStage, Pipeline, Region, StageMeta}; +use kirin_ir::{Cfg, CompileStage, Pipeline, StageMeta}; /// The sparse backward demand engine instantiated at the [`Live`] lattice: /// strong liveness. @@ -41,11 +41,11 @@ pub type Demand<'ir, S, E = InterpreterError> = SparseBackwardInterpreter<'ir, S pub type DenseLiveness<'ir, S, E = InterpreterError, F = StandardDenseBackwardFrame> = DenseBackwardInterpreter<'ir, S, LiveSet, E, F>; -/// Run strong liveness (sparse backward demand) over `region` in `stage`. +/// Run strong liveness (sparse backward demand) over `cfg` in `stage`. pub fn analyze_demand<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> Result where S: StageMeta @@ -53,18 +53,18 @@ where + InterpDispatch>, { let mut engine = Demand::::new(pipeline); - engine.analyze(stage, region)?; - Ok(DemandResult::from_engine(&engine, stage, region)) + engine.analyze(stage, cfg)?; + Ok(DemandResult::from_engine(&engine, stage, cfg)) } -/// Run classic per-point liveness (dense backward) over `region` in `stage`, +/// Run classic per-point liveness (dense backward) over `cfg` in `stage`, /// with the standard (structured-control-free) frames. Languages with scf /// compose [`DenseLiveness`] with their own frame type and build the result /// via [`DenseLivenessResult::from_engine`]. pub fn analyze_dense<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> Result where S: StageMeta @@ -80,6 +80,6 @@ where >, { let mut engine = DenseLiveness::::new(pipeline); - engine.analyze(stage, region)?; - DenseLivenessResult::from_engine(&mut engine, stage, region) + engine.analyze(stage, cfg)?; + DenseLivenessResult::from_engine(&mut engine, stage, cfg) } diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs index 348a05c5ee..b7e337608f 100644 --- a/crates/kirin-liveness/src/result.rs +++ b/crates/kirin-liveness/src/result.rs @@ -6,7 +6,7 @@ use kirin_interpreter::{ DenseBlockStore, DenseFrameBuild, DensePointStore, Frame, InterpDispatch, InterpreterError, ProgramPoint, SparseBackwardInterpreter, StageQuery, }; -use kirin_ir::{Block, CompileStage, Lattice, Region, SSAValue, StageMeta, Statement}; +use kirin_ir::{Block, Cfg, CompileStage, Lattice, SSAValue, StageMeta, Statement}; use crate::live::{Live, LiveSet}; @@ -21,10 +21,10 @@ impl DemandResult { pub(crate) fn from_engine( engine: &SparseBackwardInterpreter<'_, S, Live, InterpreterError>, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> Self { // The engine's sparse fact view; the demand set is its live support. - let facts = engine.fact_store(stage, region); + let facts = engine.fact_store(stage, cfg); let demanded = facts .iter() .filter(|(_, fact)| fact.is_live()) @@ -64,7 +64,7 @@ impl DenseLivenessResult { pub fn from_engine<'ir, S, F>( engine: &mut DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F>, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> Result where S: StageMeta @@ -77,12 +77,12 @@ impl DenseLivenessResult { { let mut blocks = DenseBlockStore::new(); for block in engine.cfg_blocks() { - if let Some(summary) = engine.block_summary(stage, region, block) { + if let Some(summary) = engine.block_summary(stage, cfg, block) { blocks.set_entry(block, summary.live_in.clone()); blocks.set_exit(block, summary.live_out.clone()); } } - let points = engine.reconstruct_points(stage, region)?; + let points = engine.reconstruct_points(stage, cfg)?; Ok(Self { blocks, points }) } diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 855f4de1c0..a58f82eed0 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -47,10 +47,10 @@ fn parse(program: &str) -> Pipeline> { pipeline } -/// The finalized stage id and the body region of `@main`. -fn main_region( +/// The finalized stage id and the body cfg of `@main`. +fn main_cfg( pipeline: &Pipeline>, -) -> (kirin::prelude::CompileStage, kirin_ir::Region) { +) -> (kirin::prelude::CompileStage, kirin_ir::Cfg) { let stage_id = pipeline.stage_by_name("test").expect("stage @test exists"); let stage = pipeline.stage(stage_id).expect("stage info"); @@ -61,25 +61,22 @@ fn main_region( let spec = &sf_info.specializations()[0]; let body = *spec.body(); - let region = match body.definition(stage) { + let cfg = match body.definition(stage) { ArithFunctionLanguage::Function { body, .. } => *body, other => panic!("expected a function body, got {other:?}"), }; - (stage_id, region) + (stage_id, cfg) } -/// The parameters of the `index`-th block of `region`, as SSA values. +/// The parameters of the `index`-th block of `cfg`, as SSA values. fn block_params( pipeline: &Pipeline>, - region: kirin_ir::Region, + cfg: kirin_ir::Cfg, index: usize, ) -> Vec { let stage_id = pipeline.stage_by_name("test").expect("stage @test exists"); let stage = pipeline.stage(stage_id).expect("stage info"); - let block = region - .blocks(stage) - .nth(index) - .expect("block index in range"); + let block = cfg.blocks(stage).nth(index).expect("block index in range"); block .expect_info(stage) .arguments @@ -89,16 +86,16 @@ fn block_params( .collect() } -/// Find an `Arith` statement in `region` matching `select`, returning what it +/// Find an `Arith` statement in `cfg` matching `select`, returning what it /// selects (e.g. the result and operands of the one `add`). fn find_arith( pipeline: &Pipeline>, - region: kirin_ir::Region, + cfg: kirin_ir::Cfg, select: impl Fn(&Arith) -> Option, ) -> R { let stage_id = pipeline.stage_by_name("test").expect("stage @test exists"); let stage = pipeline.stage(stage_id).expect("stage info"); - for block in region.blocks(stage) { + for block in cfg.blocks(stage) { for stmt in block.statements(stage) { if let ArithFunctionLanguage::Arith(op) = stmt.definition(stage) && let Some(selected) = select(op) @@ -107,23 +104,23 @@ fn find_arith( } } } - panic!("no matching arith statement in region"); + panic!("no matching arith statement in cfg"); } #[test] fn strong_liveness_over_branching_function() { let pipeline = parse(PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let entry_params = block_params(&pipeline, region, 0); + let entry_params = block_params(&pipeline, cfg, 0); let (x, cond) = (entry_params[0], entry_params[1]); - let then_param = block_params(&pipeline, region, 1)[0]; - let else_param = block_params(&pipeline, region, 2)[0]; + let then_param = block_params(&pipeline, cfg, 1)[0]; + let else_param = block_params(&pipeline, cfg, 2)[0]; // The dead `add` result is never demanded — its rule is purity-aware, so // it contributes no operand demand of its own. - let (dead, add_lhs) = find_arith(&pipeline, region, |op| match op { + let (dead, add_lhs) = find_arith(&pipeline, cfg, |op| match op { Arith::Add { lhs, result, .. } => Some((SSAValue::from(*result), *lhs)), _ => None, }); @@ -143,7 +140,7 @@ fn strong_liveness_over_branching_function() { "both edges pass %x into demanded params" ); - let neg = find_arith(&pipeline, region, |op| match op { + let neg = find_arith(&pipeline, cfg, |op| match op { Arith::Neg { result, .. } => Some(SSAValue::from(*result)), _ => None, }); @@ -153,13 +150,13 @@ fn strong_liveness_over_branching_function() { #[test] fn unused_successor_block_argument_does_not_keep_edge_arg_live() { let pipeline = parse(DEAD_EDGE_ARG_PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let entry_params = block_params(&pipeline, region, 0); + let entry_params = block_params(&pipeline, cfg, 0); let (live, dead, cond) = (entry_params[0], entry_params[1], entry_params[2]); - let then_param = block_params(&pipeline, region, 1)[0]; - let else_param = block_params(&pipeline, region, 2)[0]; + let then_param = block_params(&pipeline, cfg, 1)[0]; + let else_param = block_params(&pipeline, cfg, 2)[0]; // `^else` returns `%live` directly (a dominated cross-block use), never // touching its own parameter — so the `%dead` edge argument stays dead. @@ -213,21 +210,21 @@ specialize @test fn @main(i64, i64) -> i64 { #[test] fn terminator_operands_become_demanded() { let pipeline = parse(RET_PARAM_PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let x = block_params(&pipeline, region, 0)[0]; + let x = block_params(&pipeline, cfg, 0)[0]; assert!(result.is_demanded(x)); } #[test] fn demanded_result_marks_operands_demanded() { let pipeline = parse(DEMANDED_RESULT_PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let params = block_params(&pipeline, region, 0); - let sum = find_arith(&pipeline, region, |op| match op { + let params = block_params(&pipeline, cfg, 0); + let sum = find_arith(&pipeline, cfg, |op| match op { Arith::Add { result, .. } => Some(SSAValue::from(*result)), _ => None, }); @@ -239,11 +236,11 @@ fn demanded_result_marks_operands_demanded() { #[test] fn dead_result_leaves_operands_dead() { let pipeline = parse(DEAD_RESULT_PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let params = block_params(&pipeline, region, 0); - let sum = find_arith(&pipeline, region, |op| match op { + let params = block_params(&pipeline, cfg, 0); + let sum = find_arith(&pipeline, cfg, |op| match op { Arith::Add { result, .. } => Some(SSAValue::from(*result)), _ => None, }); @@ -264,29 +261,26 @@ fn dead_result_leaves_operands_dead() { use kirin_liveness::{LiveSet, analyze_dense}; -/// The `index`-th block of `region`. +/// The `index`-th block of `cfg`. fn nth_block( pipeline: &Pipeline>, - region: kirin_ir::Region, + cfg: kirin_ir::Cfg, index: usize, ) -> kirin_ir::Block { let stage_id = pipeline.stage_by_name("test").expect("stage @test exists"); let stage = pipeline.stage(stage_id).expect("stage info"); - region - .blocks(stage) - .nth(index) - .expect("block index in range") + cfg.blocks(stage).nth(index).expect("block index in range") } /// The statement whose definition matches `select`. fn find_stmt( pipeline: &Pipeline>, - region: kirin_ir::Region, + cfg: kirin_ir::Cfg, select: impl Fn(&ArithFunctionLanguage) -> bool, ) -> kirin_ir::Statement { let stage_id = pipeline.stage_by_name("test").expect("stage @test exists"); let stage = pipeline.stage(stage_id).expect("stage info"); - for block in region.blocks(stage) { + for block in cfg.blocks(stage) { for stmt in block.statements(stage) { if select(stmt.definition(stage)) { return stmt; @@ -298,7 +292,7 @@ fn find_stmt( return terminator; } } - panic!("no matching statement in region"); + panic!("no matching statement in cfg"); } fn live_set(values: &[SSAValue]) -> LiveSet { @@ -308,16 +302,16 @@ fn live_set(values: &[SSAValue]) -> LiveSet { #[test] fn classic_liveness_boundary_sets() { let pipeline = parse(PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_dense(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_dense(&pipeline, stage, cfg).expect("analysis succeeds"); - let entry_params = block_params(&pipeline, region, 0); + let entry_params = block_params(&pipeline, cfg, 0); let (x, cond) = (entry_params[0], entry_params[1]); - let then_param = block_params(&pipeline, region, 1)[0]; - let else_param = block_params(&pipeline, region, 2)[0]; - let entry = nth_block(&pipeline, region, 0); - let then_block = nth_block(&pipeline, region, 1); - let else_block = nth_block(&pipeline, region, 2); + let then_param = block_params(&pipeline, cfg, 1)[0]; + let else_param = block_params(&pipeline, cfg, 2)[0]; + let entry = nth_block(&pipeline, cfg, 0); + let then_block = nth_block(&pipeline, cfg, 1); + let else_block = nth_block(&pipeline, cfg, 2); // live_in(entry): %x (used by add and both edges) and %cond (branch use). assert_eq!(result.live_in(entry), Some(&live_set(&[x, cond]))); @@ -334,12 +328,12 @@ fn classic_liveness_boundary_sets() { #[test] fn classic_per_point_sets_gen_dead_uses() { let pipeline = parse(DEAD_RESULT_PROGRAM); - let (stage, region) = main_region(&pipeline); - let result = analyze_dense(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let result = analyze_dense(&pipeline, stage, cfg).expect("analysis succeeds"); - let params = block_params(&pipeline, region, 0); + let params = block_params(&pipeline, cfg, 0); let (a, b) = (params[0], params[1]); - let add = find_stmt(&pipeline, region, |definition| { + let add = find_stmt(&pipeline, cfg, |definition| { matches!(definition, ArithFunctionLanguage::Arith(Arith::Add { .. })) }); @@ -353,13 +347,13 @@ fn classic_per_point_sets_gen_dead_uses() { #[test] fn strong_per_point_sets_are_classic_intersect_demanded() { let pipeline = parse(DEAD_RESULT_PROGRAM); - let (stage, region) = main_region(&pipeline); - let dense = analyze_dense(&pipeline, stage, region).expect("dense analysis succeeds"); - let demand = analyze_demand(&pipeline, stage, region).expect("demand analysis succeeds"); + let (stage, cfg) = main_cfg(&pipeline); + let dense = analyze_dense(&pipeline, stage, cfg).expect("dense analysis succeeds"); + let demand = analyze_demand(&pipeline, stage, cfg).expect("demand analysis succeeds"); - let params = block_params(&pipeline, region, 0); + let params = block_params(&pipeline, cfg, 0); let (a, b) = (params[0], params[1]); - let add = find_stmt(&pipeline, region, |definition| { + let add = find_stmt(&pipeline, cfg, |definition| { matches!(definition, ArithFunctionLanguage::Arith(Arith::Add { .. })) }); diff --git a/crates/kirin-prettyless/src/document/builder.rs b/crates/kirin-prettyless/src/document/builder.rs index fc212fb3c9..c540351278 100644 --- a/crates/kirin-prettyless/src/document/builder.rs +++ b/crates/kirin-prettyless/src/document/builder.rs @@ -14,7 +14,7 @@ use crate::{ArenaDoc, Config, PrettyPrint}; /// - **Arena methods** (via `Deref`): `text()`, `nil()`, /// `line_()`, etc. for building document fragments. /// - **IR printing methods**: `print_statement()`, `print_block()`, -/// `print_region()`, etc. for rendering structured IR nodes. +/// `print_cfg()`, etc. for rendering structured IR nodes. /// /// For most use cases, prefer [`PrettyPrintExt::render`] or /// [`PrettyPrintExt::sprint`] which construct and use a `Document` internally. diff --git a/crates/kirin-prettyless/src/document/ir_render.rs b/crates/kirin-prettyless/src/document/ir_render.rs index 99b76ccc5f..5eb1f85357 100644 --- a/crates/kirin-prettyless/src/document/ir_render.rs +++ b/crates/kirin-prettyless/src/document/ir_render.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use kirin_ir::{ - Block, DiGraph, Dialect, GetInfo, GlobalSymbol, Id, Port, Region, SSAInfo, SSAValue, Signature, + Block, Cfg, DiGraph, Dialect, GetInfo, GlobalSymbol, Id, Port, SSAInfo, SSAValue, Signature, SpecializedFunction, StagedFunction, Statement, Symbol, UnGraph, }; use petgraph::visit::IntoNodeReferences; @@ -153,10 +153,10 @@ where header + self.text(" {") + self.block_indent(inner) + self.line_() + self.text("}") } - /// Pretty print a region with its blocks. - pub fn print_region(&'a self, region: &Region) -> ArenaDoc<'a> { + /// Pretty print a CFG with its blocks. + pub fn print_cfg(&'a self, cfg: &Cfg) -> ArenaDoc<'a> { let mut inner = self.nil(); - for block in region.blocks(self.stage) { + for block in cfg.blocks(self.stage) { inner += self.print_block(&block); inner += self.line_(); } @@ -458,10 +458,10 @@ where inner } - /// Print a Region body only: blocks without outer braces. - pub fn print_region_body_only(&'a self, region: &Region) -> ArenaDoc<'a> { + /// Print a Cfg body only: blocks without outer braces. + pub fn print_cfg_body_only(&'a self, cfg: &Cfg) -> ArenaDoc<'a> { let mut inner = self.nil(); - for block in region.blocks(self.stage) { + for block in cfg.blocks(self.stage) { inner += self.print_block(&block); inner += self.line_(); } diff --git a/crates/kirin-prettyless/src/tests/edge_cases.rs b/crates/kirin-prettyless/src/tests/edge_cases.rs index 8c31c868e1..b9be75fc66 100644 --- a/crates/kirin-prettyless/src/tests/edge_cases.rs +++ b/crates/kirin-prettyless/src/tests/edge_cases.rs @@ -108,10 +108,10 @@ fn test_print_block_multiple_unnamed_args() { insta::assert_snapshot!(buf); } -// --- Region with multiple blocks --- +// --- Cfg with multiple blocks --- #[test] -fn test_print_region_multiple_blocks() { +fn test_print_cfg_multiple_blocks() { let mut stage: BuilderStageInfo = BuilderStageInfo::default(); let a = SimpleLanguage::op_constant(&mut stage, 1i64); @@ -126,8 +126,8 @@ fn test_print_region_multiple_blocks() { let ret3 = SimpleLanguage::op_return(&mut stage, c.result); let block3 = stage.block().stmt(c).terminator(ret3).new(); - let region = stage - .region() + let cfg = stage + .cfg() .add_block(block1) .add_block(block2) .add_block(block3) @@ -135,7 +135,7 @@ fn test_print_region_multiple_blocks() { let stage = stage.finalize().unwrap(); let doc = Document::new(Default::default(), &stage); - let arena_doc = doc.print_region(®ion); + let arena_doc = doc.print_cfg(&cfg); let mut buf = String::new(); arena_doc.render_fmt(120, &mut buf).unwrap(); insta::assert_snapshot!(buf); @@ -309,7 +309,7 @@ fn test_staged_function_unnamed() { let a = SimpleLanguage::op_constant(ctx, 0i64); let ret = SimpleLanguage::op_return(ctx, a.result); let block = ctx.block().stmt(a).terminator(ret).new(); - let body = ctx.region().add_block(block).new(); + let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); }); @@ -336,7 +336,7 @@ fn test_staged_function_no_params() { let a = SimpleLanguage::op_constant(&mut stage, 0i64); let ret = SimpleLanguage::op_return(&mut stage, a.result); let block = stage.block().stmt(a).terminator(ret).new(); - let body = stage.region().add_block(block).new(); + let body = stage.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(&mut stage, body); let _ = stage .specialize() @@ -390,7 +390,7 @@ fn test_pipeline_render_builder_write_to() { let a = SimpleLanguage::op_constant(ctx, 5i64); let ret = SimpleLanguage::op_return(ctx, a.result); let block = ctx.block().stmt(a).terminator(ret).new(); - let body = ctx.region().add_block(block).new(); + let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); }); @@ -426,7 +426,7 @@ fn test_function_render_builder_write_to() { let a = SimpleLanguage::op_constant(ctx, 99i64); let ret = SimpleLanguage::op_return(ctx, a.result); let block = ctx.block().stmt(a).terminator(ret).new(); - let body = ctx.region().add_block(block).new(); + let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); }); @@ -457,7 +457,7 @@ fn test_render_very_narrow_width() { let a = SimpleLanguage::op_constant(&mut stage, 1i64); let ret = SimpleLanguage::op_return(&mut stage, a.result); let block = stage.block().stmt(a).terminator(ret).new(); - let body = stage.region().add_block(block).new(); + let body = stage.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(&mut stage, body); let _ = stage .specialize() diff --git a/crates/kirin-prettyless/src/tests/impls.rs b/crates/kirin-prettyless/src/tests/impls.rs index 6a545ceaa8..dd0550ed4a 100644 --- a/crates/kirin-prettyless/src/tests/impls.rs +++ b/crates/kirin-prettyless/src/tests/impls.rs @@ -371,20 +371,20 @@ fn test_print_block_with_named_args() { } // ============================================================================ -// print_region tests +// print_cfg tests // ============================================================================ #[test] -fn test_print_region_empty() { +fn test_print_cfg_empty() { let mut gs: InternTable = InternTable::default(); let test_sym = gs.intern("test".to_string()); let mut stage: BuilderStageInfo = BuilderStageInfo::default(); let _ = stage.staged_function().name(test_sym).new().unwrap(); - let region = stage.region().new(); + let cfg = stage.cfg().new(); let stage = stage.finalize().unwrap(); let doc = Document::new(Default::default(), &stage); - let arena_doc = doc.print_region(®ion); + let arena_doc = doc.print_cfg(&cfg); let mut buf = String::new(); arena_doc.render_fmt(80, &mut buf).unwrap(); insta::assert_snapshot!(buf); @@ -427,7 +427,7 @@ fn test_render_builder_config() { .stmt(c) .terminator(ret) .new(); - let body = stage.region().add_block(block).new(); + let body = stage.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(&mut stage, body); let f = stage .specialize() diff --git a/crates/kirin-prettyless/src/tests/mod.rs b/crates/kirin-prettyless/src/tests/mod.rs index 692a826115..8a5ccca40b 100644 --- a/crates/kirin-prettyless/src/tests/mod.rs +++ b/crates/kirin-prettyless/src/tests/mod.rs @@ -21,7 +21,7 @@ impl PrettyPrint for SimpleLanguage { } SimpleLanguage::Constant(value, _res) => doc.text(format!("constant {}", value)), SimpleLanguage::Return(retval) => doc.text("return ") + retval.pretty_print(doc), - SimpleLanguage::Function(region, _) => doc.print_region(region), + SimpleLanguage::Function(cfg, _) => doc.print_cfg(cfg), } } } @@ -71,7 +71,7 @@ fn create_test_function() -> ( .terminator(ret) .new(); - let body = stage.region().add_block(block_a).add_block(block_b).new(); + let body = stage.cfg().add_block(block_a).add_block(block_b).new(); let fdef = SimpleLanguage::op_function(&mut stage, body); let f = stage .specialize() diff --git a/crates/kirin-prettyless/src/tests/pipeline.rs b/crates/kirin-prettyless/src/tests/pipeline.rs index 8ba5a27c46..04636aa454 100644 --- a/crates/kirin-prettyless/src/tests/pipeline.rs +++ b/crates/kirin-prettyless/src/tests/pipeline.rs @@ -21,7 +21,7 @@ fn test_pipeline_function_print() { let a = SimpleLanguage::op_constant(ctx0, 42i64); let ret = SimpleLanguage::op_return(ctx0, a.result); let block = ctx0.block().stmt(a).terminator(ret).new(); - let body = ctx0.region().add_block(block).new(); + let body = ctx0.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx0, body); ctx0.specialize().staged_func(sf0).body(fdef).new().unwrap(); }); @@ -46,7 +46,7 @@ fn test_pipeline_function_print() { let c = SimpleLanguage::op_add(ctx1, a.result, b.result); let ret = SimpleLanguage::op_return(ctx1, c.result); let block = ctx1.block().stmt(a).stmt(b).stmt(c).terminator(ret).new(); - let body = ctx1.region().add_block(block).new(); + let body = ctx1.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx1, body); ctx1.specialize().staged_func(sf1).body(fdef).new().unwrap(); }); @@ -78,7 +78,7 @@ fn test_pipeline_unnamed_stage() { let a = SimpleLanguage::op_constant(ctx, 7i64); let ret = SimpleLanguage::op_return(ctx, a.result); let block = ctx.block().stmt(a).terminator(ret).new(); - let body = ctx.region().add_block(block).new(); + let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); }); diff --git a/crates/kirin-prettyless/src/tests/sprint_with_globals.rs b/crates/kirin-prettyless/src/tests/sprint_with_globals.rs index 6bd7edb6bb..800af41212 100644 --- a/crates/kirin-prettyless/src/tests/sprint_with_globals.rs +++ b/crates/kirin-prettyless/src/tests/sprint_with_globals.rs @@ -14,7 +14,7 @@ fn test_sprint_with_globals() { let a = SimpleLanguage::op_constant(&mut stage, 42i64); let ret = SimpleLanguage::op_return(&mut stage, a.result); let block = stage.block().stmt(a).terminator(ret).new(); - let body = stage.region().add_block(block).new(); + let body = stage.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(&mut stage, body); let _ = stage .specialize() diff --git a/crates/kirin-prettyless/src/traits.rs b/crates/kirin-prettyless/src/traits.rs index 528f463c89..432b585a2f 100644 --- a/crates/kirin-prettyless/src/traits.rs +++ b/crates/kirin-prettyless/src/traits.rs @@ -16,10 +16,10 @@ use prettyless::DocAllocator; /// should produce output that roundtrips through the parser. /// /// The bounds on `L` (`PrettyPrint` and `Type: Display`) are required because: -/// - `L: PrettyPrint` is needed to print nested Block/Region structures +/// - `L: PrettyPrint` is needed to print nested Block/Cfg structures /// - `Type: Display` is needed to print type annotations (`:type` format) /// -/// For IR nodes that require context (like `Statement`, `Block`, `Region`), use +/// For IR nodes that require context (like `Statement`, `Block`, `Cfg`), use /// the convenience methods provided on `Document` instead. /// /// # Example diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index 8de0f1acef..e9fc9994bb 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -108,7 +108,7 @@ where // Demand converges value-by-value on the sparse backward engine's worklist, // so structured bodies need no walk and loops need no frame fixpoint: this // rule re-runs whenever a result or a body block parameter it feeds rises -// (the owning statement is the body's *feeder* in the region topology). +// (the owning statement is the body's *feeder* in the cfg topology). /// Backward demand for `scf.if`: the condition is an unconditional control /// root (consistent with `cf.cond_br`); a body's yield slot is demanded iff diff --git a/crates/kirin-scf/src/lib.rs b/crates/kirin-scf/src/lib.rs index 5e8aeea08f..4f1c52c9f5 100644 --- a/crates/kirin-scf/src/lib.rs +++ b/crates/kirin-scf/src/lib.rs @@ -3,7 +3,7 @@ //! This dialect provides high-level control flow operations that model //! structured programming constructs. Unlike `kirin-cf` which uses //! unstructured branches, `kirin-scf` operations have lexically scoped -//! regions with guaranteed single-entry semantics. +//! cfgs with guaranteed single-entry semantics. //! //! # Operations //! @@ -13,9 +13,9 @@ //! | `for %iv in %lo..%hi step %s iter_args(..) do {..} [-> types]` | Counted loop with multi-accumulator support | //! | `yield [%v1, %v2, ..]` | Terminates an SCF body block, yielding 0-to-N values to the parent | //! -//! # Block vs Region +//! # Block vs Cfg //! -//! All body fields use `Block` (not `Region`) because MLIR's `scf.if` and +//! All body fields use `Block` (not `Cfg`) because MLIR's `scf.if` and //! `scf.for` have the `SingleBlock` + `SingleBlockImplicitTerminator` //! traits. A `yield` terminates each body block. //! diff --git a/crates/kirin-scf/src/tests.rs b/crates/kirin-scf/src/tests.rs index b981b2fd47..e87ca9678e 100644 --- a/crates/kirin-scf/src/tests.rs +++ b/crates/kirin-scf/src/tests.rs @@ -1,5 +1,5 @@ use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, TestSSAValue, }; use kirin_test_types::UnitType; @@ -90,8 +90,8 @@ fn yield_no_blocks() { } #[test] -fn yield_no_regions() { - assert_eq!(make_yield().regions().count(), 0); +fn yield_no_cfgs() { + assert_eq!(make_yield().cfgs().count(), 0); } // --- Clone + PartialEq for Yield --- diff --git a/crates/kirin-test-languages/src/arith_function_language.rs b/crates/kirin-test-languages/src/arith_function_language.rs index 487e3aa342..898da14da0 100644 --- a/crates/kirin-test-languages/src/arith_function_language.rs +++ b/crates/kirin-test-languages/src/arith_function_language.rs @@ -1,7 +1,7 @@ use kirin_arith::{Arith, ArithType}; use kirin_cf::ControlFlow; use kirin_function::Return; -use kirin_ir::{Dialect, Region, Signature}; +use kirin_ir::{Cfg, Dialect, Signature}; /// Test language: Function + Arith + ControlFlow + Return. /// Used for arith pipeline roundtrips and as bare (no-namespace) language. @@ -17,7 +17,7 @@ pub enum ArithFunctionLanguage { chumsky(format = "fn {:name}{sig} {body}") )] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] diff --git a/crates/kirin-test-languages/src/bitwise_function_language.rs b/crates/kirin-test-languages/src/bitwise_function_language.rs index 9f82f925c1..affc926eec 100644 --- a/crates/kirin-test-languages/src/bitwise_function_language.rs +++ b/crates/kirin-test-languages/src/bitwise_function_language.rs @@ -2,7 +2,7 @@ use kirin_arith::ArithType; use kirin_bitwise::Bitwise; use kirin_cf::ControlFlow; use kirin_function::Return; -use kirin_ir::{Dialect, Region, Signature}; +use kirin_ir::{Cfg, Dialect, Signature}; /// Test language: Function + Bitwise + ControlFlow + Return. /// Used for bitwise pipeline roundtrip tests. @@ -18,7 +18,7 @@ pub enum BitwiseFunctionLanguage { chumsky(format = "fn {:name}{sig} {body}") )] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] diff --git a/crates/kirin-test-languages/src/callable_language.rs b/crates/kirin-test-languages/src/callable_language.rs index 6196ae001b..12e3021bdc 100644 --- a/crates/kirin-test-languages/src/callable_language.rs +++ b/crates/kirin-test-languages/src/callable_language.rs @@ -1,6 +1,6 @@ use kirin_arith::ArithType; use kirin_function::{Bind, Call, Return}; -use kirin_ir::{Dialect, Region, Signature}; +use kirin_ir::{Cfg, Dialect, Signature}; /// Test language: Function + Bind + Call + Return. /// Used for function call/bind roundtrip tests. @@ -16,7 +16,7 @@ pub enum CallableLanguage { chumsky(format = "fn {:name}{sig} {body}") )] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] diff --git a/crates/kirin-test-languages/src/namespaced_language.rs b/crates/kirin-test-languages/src/namespaced_language.rs index 137ecf8030..a4cdbe1f59 100644 --- a/crates/kirin-test-languages/src/namespaced_language.rs +++ b/crates/kirin-test-languages/src/namespaced_language.rs @@ -1,7 +1,7 @@ use kirin_arith::{Arith, ArithType}; use kirin_cf::ControlFlow; use kirin_function::Return; -use kirin_ir::{Dialect, Region, Signature}; +use kirin_ir::{Cfg, Dialect, Signature}; /// Test language with namespace prefixes on wraps variants. /// Arith ops become `arith.add`, ControlFlow becomes `cf.br`, Return becomes `func.ret`. @@ -17,7 +17,7 @@ pub enum NamespacedLanguage { chumsky(format = "fn {:name}{sig} {body}") )] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] diff --git a/crates/kirin-test-languages/src/simple_language.rs b/crates/kirin-test-languages/src/simple_language.rs index ea1142623c..e84c723fdc 100644 --- a/crates/kirin-test-languages/src/simple_language.rs +++ b/crates/kirin-test-languages/src/simple_language.rs @@ -1,5 +1,5 @@ use crate::{SimpleType, Value}; -use kirin_ir::{Dialect, Region, ResultValue, SSAValue}; +use kirin_ir::{Cfg, Dialect, ResultValue, SSAValue}; #[derive(Clone, Debug, PartialEq, Dialect)] #[cfg_attr(feature = "parser", derive(kirin_chumsky::HasParser))] @@ -35,5 +35,5 @@ pub enum SimpleLanguage { any(feature = "parser", feature = "pretty"), chumsky(format = "$function {0}") )] - Function(Region, #[kirin(type = SimpleType::F64)] ResultValue), + Function(Cfg, #[kirin(type = SimpleType::F64)] ResultValue), } diff --git a/crates/kirin-tuple/src/tests.rs b/crates/kirin-tuple/src/tests.rs index 4eca91299f..e93186300d 100644 --- a/crates/kirin-tuple/src/tests.rs +++ b/crates/kirin-tuple/src/tests.rs @@ -1,5 +1,5 @@ use kirin::ir::{ - HasArguments, HasBlocks, HasRegions, HasResults, HasSuccessors, IsConstant, IsPure, + HasArguments, HasBlocks, HasCfgs, HasResults, HasSuccessors, IsConstant, IsPure, IsSpeculatable, IsTerminator, TestSSAValue, }; use kirin_test_types::UnitType; @@ -73,8 +73,8 @@ fn new_tuple_no_blocks() { } #[test] -fn new_tuple_no_regions() { - assert_eq!(make_new_tuple().regions().count(), 0); +fn new_tuple_no_cfgs() { + assert_eq!(make_new_tuple().cfgs().count(), 0); } // --- Unpack: not a terminator --- @@ -127,8 +127,8 @@ fn unpack_no_blocks() { } #[test] -fn unpack_no_regions() { - assert_eq!(make_unpack().regions().count(), 0); +fn unpack_no_cfgs() { + assert_eq!(make_unpack().cfgs().count(), 0); } // --- Clone + PartialEq --- diff --git a/docs/design/formalism/state-environment-model.md b/docs/design/formalism/state-environment-model.md index 31efc6ee52..02ef0bd976 100644 --- a/docs/design/formalism/state-environment-model.md +++ b/docs/design/formalism/state-environment-model.md @@ -106,7 +106,7 @@ This is performed by engine/frame protocol, not by dialect statements. Structured control (`scf.if`, `scf.for`) is represented by `Scope`: -- `body: ScopeBody` (`Block`, `Region`, or `Immediate`) +- `body: ScopeBody` (`Block`, `Cfg`, or `Immediate`) - `args: Product` (entry arguments) - `results: Product` (landing slots) - `hook: Option>>` diff --git a/docs/design/formalism/syntax.md b/docs/design/formalism/syntax.md index 25501a9342..c6a811bcf7 100644 --- a/docs/design/formalism/syntax.md +++ b/docs/design/formalism/syntax.md @@ -7,8 +7,8 @@ just enough formal shorthand to compose with Parts II-IV. ## Reading Recipe -- **Formal read:** Treat this as the grammar-level domain of `s` (statement), owners (`Region`/`Block`), and SSA carriers consumed by the judgment. -- **API read:** Verify mappings in `crates/kirin-ir/src/{pipeline.rs,stage/info.rs,language.rs,node/{function/*,region.rs,block.rs,stmt.rs,ssa.rs},product.rs}` and stage/language composition in `example/toy-lang/src/{language.rs,stage.rs}`. +- **Formal read:** Treat this as the grammar-level domain of `s` (statement), owners (`Cfg`/`Block`), and SSA carriers consumed by the judgment. +- **API read:** Verify mappings in `crates/kirin-ir/src/{pipeline.rs,stage/info.rs,language.rs,node/{function/*,cfg.rs,block.rs,stmt.rs,ssa.rs},product.rs}` and stage/language composition in `example/toy-lang/src/{language.rs,stage.rs}`. The formal names below map directly to concrete Rust IR/runtime types. @@ -20,7 +20,7 @@ The formal names below map directly to concrete Rust IR/runtime types. | `Function` | `FunctionInfo` / `Function` | [`crates/kirin-ir/src/node/function/generic.rs`](../../../crates/kirin-ir/src/node/function/generic.rs) | | `StagedFunction` | `StagedFunctionInfo` / `StagedFunction` | [`crates/kirin-ir/src/node/function/staged.rs`](../../../crates/kirin-ir/src/node/function/staged.rs) | | `SpecializedFunction` | `SpecializedFunctionInfo` / `SpecializedFunction` | [`crates/kirin-ir/src/node/function/specialized.rs`](../../../crates/kirin-ir/src/node/function/specialized.rs) | -| `Region` | `RegionInfo` / `Region` | [`crates/kirin-ir/src/node/region.rs`](../../../crates/kirin-ir/src/node/region.rs) | +| `Cfg` | `CfgInfo` / `Cfg` | [`crates/kirin-ir/src/node/cfg.rs`](../../../crates/kirin-ir/src/node/cfg.rs) | | `Block` | `BlockInfo` / `Block` / `Successor` | [`crates/kirin-ir/src/node/block.rs`](../../../crates/kirin-ir/src/node/block.rs) | | `Statement` | `StatementInfo` / `Statement` | [`crates/kirin-ir/src/node/stmt.rs`](../../../crates/kirin-ir/src/node/stmt.rs) | | `SSAValue` | `SSAValue`, `ResultValue`, `BlockArgument` | [`crates/kirin-ir/src/node/ssa.rs`](../../../crates/kirin-ir/src/node/ssa.rs) | @@ -34,7 +34,7 @@ Kirin syntax for interpreter purposes is SSA IR over staged pipelines: - `StageInfo` is per-stage storage for one language `L`. - `L: Dialect` is the stage language (often an enum wrapping multiple dialects). - `Function -> StagedFunction -> SpecializedFunction` is the callable hierarchy. -- `Region -> Block -> Statement` is executable structure. +- `Cfg -> Block -> Statement` is executable structure. - `SSAValue` connects operands/results/block arguments across statements. The interpreter executes this graph-like SSA structure, not an expression tree. @@ -50,8 +50,8 @@ Function ::= FunctionInfo + staged variants StagedFunction ::= stage-specific callable variant Specialized ::= concrete specialization with body Statement -Statement ::= dialect definition + operands + results + nested blocks/regions/successors -Region ::= Block* +Statement ::= dialect definition + operands + results + nested blocks/cfgs/successors +Cfg ::= Block* Block ::= BlockArgument* ; Statement* ; optional terminator cache SSAValue ::= ResultValue | BlockArgument | Port ``` diff --git a/docs/design/graph-body-rust-interface.md b/docs/design/graph-body-rust-interface.md index 16ff501db6..a6f4908d65 100644 --- a/docs/design/graph-body-rust-interface.md +++ b/docs/design/graph-body-rust-interface.md @@ -184,7 +184,7 @@ pub enum FieldCategory { Result, Block, Successor, - Region, + Cfg, DiGraph, // new UnGraph, // new Symbol, @@ -204,7 +204,7 @@ pub enum FieldData { Field classification in `parse_field` adds type-name checks for `"DiGraph"` and `"UnGraph"`, supporting `Single`, `Vec`, and `Option` collection wrapping. -`#[derive(Dialect)]` auto-generates `HasDigraphs`/`HasUngraphs` impls for all dialects. Dialects with no `DiGraph`/`UnGraph` fields get empty-iterator impls (same pattern as `HasBlocks`/`HasRegions`). +`#[derive(Dialect)]` auto-generates `HasDigraphs`/`HasUngraphs` impls for all dialects. Dialects with no `DiGraph`/`UnGraph` fields get empty-iterator impls (same pattern as `HasBlocks`/`HasCfgs`). ### #[kirin(edge)] Attribute diff --git a/docs/design/graph-ir-node.md b/docs/design/graph-ir-node.md index 4fa9d45e82..7682b97d97 100644 --- a/docs/design/graph-ir-node.md +++ b/docs/design/graph-ir-node.md @@ -1,6 +1,6 @@ # Native Graph IR Node — Text Format and Semantics Design -This design introduces two new IR body kinds — `digraph` and `ungraph` — alongside Block and Region. A graph body uses standard statement syntax where SSAValues represent edges. The leading keyword (`^`, `digraph`, `ungraph`) selects the backing storage: Block (linked list), petgraph DiGraph, or petgraph UnGraph. +This design introduces two new IR body kinds — `digraph` and `ungraph` — alongside Block and CFG (`Cfg`). A graph body uses standard statement syntax where SSAValues represent edges. The leading keyword (`^`, `digraph`, `ungraph`) selects the backing storage: Block (linked list), petgraph DiGraph, or petgraph UnGraph. - For **directed graphs**, the text format follows MLIR graph region semantics (relaxed dominance, SSA def-use = directed edges). - For **undirected graphs**, `edge`-prefixed statements introduce edge SSAValues, and statements that share edge references are connected. diff --git a/docs/design/hybrid_ir_visualization.dot b/docs/design/hybrid_ir_visualization.dot index 8a70dff221..43700045b0 100644 --- a/docs/design/hybrid_ir_visualization.dot +++ b/docs/design/hybrid_ir_visualization.dot @@ -57,8 +57,8 @@ digraph kirin { color="#1a73e8" fontsize=12 - subgraph cluster_region { - label=<Region> + subgraph cluster_cfg { + label=<CFG> style="dashed,filled" fillcolor="#ecf0f8" color="#7baaf7" diff --git a/docs/design/hybrid_ir_visualization.svg b/docs/design/hybrid_ir_visualization.svg index 8e66dd1f69..5beffb0f2a 100644 --- a/docs/design/hybrid_ir_visualization.svg +++ b/docs/design/hybrid_ir_visualization.svg @@ -46,9 +46,9 @@ (i64) → i64 -cluster_region +cluster_cfg -Region +CFG cluster_entry diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index e3479bf7a6..74d98673a8 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -232,7 +232,7 @@ Like `Interpretable`, it receives the engine `interp` directly (function entry i forward-only, so there is no `Semantics` parameter). Statements that define function bodies (e.g. `kirin_function::Function`) -return the `FunctionBody { region, args }` to enter on invocation (the +return the `FunctionBody { cfg, args }` to enter on invocation (the function-call entry descriptor — not a structured-control abstraction). On language enums it is derived; `#[callable]` marks the variants that forward, all others report `NotCallable`. @@ -295,7 +295,7 @@ A generic **frame-stack driver**: it pops the top frame, calls `Frame::step`, and applies the returned `FrameEffect` (`Continue` / `Push` / `Done` / `Complete`) — it owns *no* traversal logic itself. Traversal lives in the frames. The default total frame type `StandardFrame` wraps the standard -`BodyFrame` (walks a function-body region CFG, or a single body block that +`BodyFrame` (walks a function-body CFG, or a single body block that completes on `Yield` — `Jump` retargets it, `Return` completes it) and `CallFrame` (dispatch a callee, await its `Return`). The dialect-produced `SparseForwardEffect` is consumed by `BodyFrame`, which maps it to a `FrameEffect` @@ -332,7 +332,7 @@ keying and merge; the interprocedural protocol (summary tables, caller recording) stays atomic in the engine. Three nested fixpoints, expressed as frames: -- **CFG**: each function body region is a block worklist; block parameters +- **CFG**: each function-body CFG is a block worklist; block parameters join across incoming edges and widen after `widen_after` visits — `cf` back-edge loops converge. - **Pushed loop frames**: a dialect loop frame (e.g. `scf.for`'s @@ -365,7 +365,7 @@ Two mechanisms keep engines generic over stage enums: matching `Interpretable`/`FunctionEntry` rule. - `StageQuery` — a bound bundle over kirin-ir's `StageDispatch`/`StageAction` machinery for language-independent IR facts (block parameters, statement - order, region entry, specialization lookup, symbol resolution). Satisfied + order, CFG entry, specialization lookup, symbol resolution). Satisfied automatically by any stage enum; used by engines and linkers internally. ## Custom traversal and policies diff --git a/example/simple.rs b/example/simple.rs index abde1e7547..d635f59d2f 100644 --- a/example/simple.rs +++ b/example/simple.rs @@ -10,20 +10,20 @@ use kirin_function::{Bind, Call, Return}; /// Higher-level language: structured control flow (`if`) and lexical /// lambdas that capture variables from the enclosing scope. /// -/// Block/Region-containing dialect types (SCF, Lambda) are inlined here +/// Block/Cfg-containing dialect types (SCF, Lambda) are inlined here /// to demonstrate the format-string parser path for inline variants. /// These types can also be composed via `#[wraps]` — see `toy-lang`. #[derive(Debug, Clone, PartialEq, Eq, Hash, Dialect, HasParser, PrettyPrint)] #[kirin(builders, type = ArithType)] enum HighLevel { #[chumsky(format = "{body}")] - Function { body: Region }, + Function { body: Cfg }, #[chumsky(format = "$lambda {name} captures({captures}) {body} -> {res:type}")] Lambda { name: Symbol, captures: Vec, - body: Region, + body: Cfg, #[kirin(type = ArithType::placeholder())] res: ResultValue, }, @@ -49,7 +49,7 @@ enum HighLevel { #[kirin(builders, type = ArithType)] enum LowLevel { #[chumsky(format = "{body}")] - Function { body: Region }, + Function { body: Cfg }, #[wraps] Arith(Arith), diff --git a/example/toy-lang/src/interpreter/mod.rs b/example/toy-lang/src/interpreter/mod.rs index 6fb55a7fa3..0638f0bf25 100644 --- a/example/toy-lang/src/interpreter/mod.rs +++ b/example/toy-lang/src/interpreter/mod.rs @@ -14,7 +14,7 @@ mod tests; pub use error::ToyError; pub use frame::{ToyAbstractFrame, ToyDenseBackwardFrame, ToyFrame}; -use kirin::prelude::{CompileStage, GetInfo, Pipeline, Region, UniqueLiveSpecializationError}; +use kirin::prelude::{Cfg, CompileStage, GetInfo, Pipeline, UniqueLiveSpecializationError}; use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_function::{Lexical, Lifted}; use kirin_interpreter::InterpreterError; @@ -113,12 +113,12 @@ pub fn analyze_constprop( expect_single(analysis.analyze_by_name(stage_name, function_name, args.iter().cloned())?) } -/// The body region of `function_name`'s specialization at `stage_name`. -fn function_region( +/// The body cfg of `function_name`'s specialization at `stage_name`. +fn function_cfg( pipeline: &Pipeline, stage_name: &str, function_name: &str, -) -> Result<(CompileStage, Region), InterpreterError> { +) -> Result<(CompileStage, Cfg), InterpreterError> { let stage_id = pipeline .stage_by_name(stage_name) .ok_or_else(|| InterpreterError::MissingStageName(stage_name.into()))?; @@ -129,7 +129,7 @@ fn function_region( .stage(stage_id) .ok_or(InterpreterError::MissingStage(stage_id))?; - let region = match stage { + let cfg = match stage { Stage::Source(info) => { let staged_info = staged .get_info(info) @@ -151,8 +151,8 @@ fn function_region( .ok_or(InterpreterError::Custom("specialized function has no body"))?; match spec_info.body().definition(info) { HighLevel::Lexical(Lexical::Function(function)) => { - use kirin::prelude::HasRegionBody; - *function.region() + use kirin::prelude::HasCfgBody; + *function.cfg() } _ => return Err(InterpreterError::Custom("expected a function body")), } @@ -178,14 +178,14 @@ fn function_region( .ok_or(InterpreterError::Custom("specialized function has no body"))?; match spec_info.body().definition(info) { LowLevel::Lifted(Lifted::Function(function)) => { - use kirin::prelude::HasRegionBody; - *function.region() + use kirin::prelude::HasCfgBody; + *function.cfg() } _ => return Err(InterpreterError::Custom("expected a function body")), } } }; - Ok((stage_id, region)) + Ok((stage_id, cfg)) } /// Run both liveness analyses over `function_name`'s body at `stage_name`: @@ -196,10 +196,10 @@ pub fn analyze_liveness( stage_name: &str, function_name: &str, ) -> Result<(DemandResult, DenseLivenessResult), InterpreterError> { - let (stage, region) = function_region(pipeline, stage_name, function_name)?; - let demand = kirin_liveness::analyze_demand(pipeline, stage, region)?; + let (stage, cfg) = function_cfg(pipeline, stage_name, function_name)?; + let demand = kirin_liveness::analyze_demand(pipeline, stage, cfg)?; let mut engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - engine.analyze(stage, region)?; - let dense = DenseLivenessResult::from_engine(&mut engine, stage, region)?; + engine.analyze(stage, cfg)?; + let dense = DenseLivenessResult::from_engine(&mut engine, stage, cfg)?; Ok((demand, dense)) } diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 95f8d846cb..3869fd65d0 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -262,10 +262,10 @@ fn build_cross_stage_specialized_pipeline() -> Pipeline { .stmt(call) .terminator(ret) .new(); - let region = builder.region().add_block(block).new(); + let cfg = builder.cfg().add_block(block).new(); let body = Function::::new( builder, - region, + cfg, Signature::new(vec![ArithType::I64], ArithType::I64, ()), ); builder @@ -897,8 +897,7 @@ mod advanced { mod demand { use kirin::prelude::{ - CompileStage, GetInfo, HasRegionBody, HasResults, ParsePipelineText, Pipeline, Region, - SSAValue, + Cfg, CompileStage, GetInfo, HasCfgBody, HasResults, ParsePipelineText, Pipeline, SSAValue, }; use kirin_arith::{Arith, ArithValue}; use kirin_function::Lexical; @@ -913,8 +912,8 @@ mod demand { pipeline } - /// The source stage id, its info, and the body region of `name`. - pub(super) fn source_region(pipeline: &Pipeline, name: &str) -> (CompileStage, Region) { + /// The source stage id, its info, and the body cfg of `name`. + pub(super) fn source_cfg(pipeline: &Pipeline, name: &str) -> (CompileStage, Cfg) { let stage_id = pipeline.stage_by_name("source").expect("source stage"); let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); @@ -924,20 +923,20 @@ mod demand { .expect("staged function"); let sf_info = sf.get_info(info).expect("staged function info"); let body = *sf_info.specializations()[0].body(); - let region = match body.definition(info) { - HighLevel::Lexical(Lexical::Function(function)) => *function.region(), + let cfg = match body.definition(info) { + HighLevel::Lexical(Lexical::Function(function)) => *function.cfg(), other => panic!("expected a function body, got {other:?}"), }; - (stage_id, region) + (stage_id, cfg) } - /// The parameters of the region's entry block. - pub(super) fn entry_params(pipeline: &Pipeline, region: Region) -> Vec { + /// The parameters of the CFG's entry block. + pub(super) fn entry_params(pipeline: &Pipeline, cfg: Cfg) -> Vec { let stage_id = pipeline.stage_by_name("source").expect("source stage"); let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let block = region.blocks(info).next().expect("entry block"); + let block = cfg.blocks(info).next().expect("entry block"); block .expect_info(info) .arguments @@ -948,18 +947,18 @@ mod demand { } /// Find something by matching statement definitions anywhere in the - /// region (including scf bodies, via the topology's nested-block + /// cfg (including scf bodies, via the topology's nested-block /// enumeration). pub(super) fn find_value( pipeline: &Pipeline, - region: Region, + cfg: Cfg, select: impl Fn(&HighLevel) -> Option, ) -> R { let stage_id = pipeline.stage_by_name("source").expect("source stage"); let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let topology = kirin_interpreter::region_topology(info, ®ion); + let topology = kirin_interpreter::cfg_topology(info, &cfg); for block in &topology.blocks { for &stmt in &block.stmts { if let Some(value) = select(stmt.definition(info)) { @@ -967,16 +966,12 @@ mod demand { } } } - panic!("no matching statement in region"); + panic!("no matching statement in cfg"); } /// The result of the `constant -> i64` statement. - pub(super) fn constant_result( - pipeline: &Pipeline, - region: Region, - value: i64, - ) -> SSAValue { - find_value(pipeline, region, |definition| match definition { + pub(super) fn constant_result(pipeline: &Pipeline, cfg: Cfg, value: i64) -> SSAValue { + find_value(pipeline, cfg, |definition| match definition { HighLevel::Constant(constant) if constant.value == ArithValue::I64(value) => { Some(constant.result.into()) } @@ -1008,11 +1003,11 @@ specialize @source fn @if_body(i64) -> i64 { #[test] fn scf_if_body_demand_follows_result_demand() { let pipeline = parse(IF_BODY_DEMAND); - let (stage, region) = source_region(&pipeline, "if_body"); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = source_cfg(&pipeline, "if_body"); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let cond = entry_params(&pipeline, region)[0]; - let if_result = find_value(&pipeline, region, |definition| match definition { + let cond = entry_params(&pipeline, cfg)[0]; + let if_result = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Structured(_) => definition.results().next().map(|r| SSAValue::from(*r)), _ => None, }); @@ -1020,15 +1015,15 @@ specialize @source fn @if_body(i64) -> i64 { assert!(result.is_demanded(if_result), "ret demands the if result"); assert!(result.is_demanded(cond), "condition is a control root"); assert!( - result.is_demanded(constant_result(&pipeline, region, 1)), + result.is_demanded(constant_result(&pipeline, cfg, 1)), "then-arm yield operand feeds the demanded result" ); assert!( - result.is_demanded(constant_result(&pipeline, region, 2)), + result.is_demanded(constant_result(&pipeline, cfg, 2)), "else-arm yield operand feeds the demanded result" ); assert!( - !result.is_demanded(constant_result(&pipeline, region, 9)), + !result.is_demanded(constant_result(&pipeline, cfg, 9)), "a dead constant inside a body stays dead" ); } @@ -1057,20 +1052,20 @@ specialize @source fn @if_dead(i64) -> i64 { #[test] fn scf_if_dead_result_keeps_only_condition() { let pipeline = parse(IF_DEAD_RESULT); - let (stage, region) = source_region(&pipeline, "if_dead"); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = source_cfg(&pipeline, "if_dead"); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let cond = entry_params(&pipeline, region)[0]; - let if_result = find_value(&pipeline, region, |definition| match definition { + let cond = entry_params(&pipeline, cfg)[0]; + let if_result = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Structured(_) => definition.results().next().map(|r| SSAValue::from(*r)), _ => None, }); assert!(result.is_demanded(cond), "condition is a control root"); - assert!(result.is_demanded(constant_result(&pipeline, region, 0))); + assert!(result.is_demanded(constant_result(&pipeline, cfg, 0))); assert!(!result.is_demanded(if_result)); - assert!(!result.is_demanded(constant_result(&pipeline, region, 1))); - assert!(!result.is_demanded(constant_result(&pipeline, region, 2))); + assert!(!result.is_demanded(constant_result(&pipeline, cfg, 1))); + assert!(!result.is_demanded(constant_result(&pipeline, cfg, 2))); } pub(super) const FOR_CARRIED_DEMAND: &str = r#" @@ -1097,12 +1092,12 @@ specialize @source fn @loop_sum(i64, i64, i64) -> i64 { #[test] fn scf_for_loop_carried_demand_converges() { let pipeline = parse(FOR_CARRIED_DEMAND); - let (stage, region) = source_region(&pipeline, "loop_sum"); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = source_cfg(&pipeline, "loop_sum"); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let params = entry_params(&pipeline, region); + let params = entry_params(&pipeline, cfg); let (lo, hi, step) = (params[0], params[1], params[2]); - let next = find_value(&pipeline, region, |definition| match definition { + let next = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Arith(Arith::Add { result, .. }) => Some(SSAValue::from(*result)), _ => None, }); @@ -1112,7 +1107,7 @@ specialize @source fn @loop_sum(i64, i64, i64) -> i64 { let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let body = find_value(&pipeline, region, |definition| match definition { + let body = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Structured(kirin_scf::StructuredControlFlow::For(op)) => Some(op.body()), _ => None, }); @@ -1127,11 +1122,11 @@ specialize @source fn @loop_sum(i64, i64, i64) -> i64 { }; assert!( - result.is_demanded(constant_result(&pipeline, region, 0)), + result.is_demanded(constant_result(&pipeline, cfg, 0)), "init" ); assert!( - result.is_demanded(constant_result(&pipeline, region, 1)), + result.is_demanded(constant_result(&pipeline, cfg, 1)), "one" ); assert!(result.is_demanded(next), "yield slot"); @@ -1165,11 +1160,11 @@ specialize @source fn @loop_dead(i64, i64, i64) -> i64 { #[test] fn scf_for_dead_result_keeps_only_bounds() { let pipeline = parse(FOR_DEAD_RESULT); - let (stage, region) = source_region(&pipeline, "loop_dead"); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = source_cfg(&pipeline, "loop_dead"); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let params = entry_params(&pipeline, region); - let next = find_value(&pipeline, region, |definition| match definition { + let params = entry_params(&pipeline, cfg); + let next = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Arith(Arith::Add { result, .. }) => Some(SSAValue::from(*result)), _ => None, }); @@ -1177,13 +1172,13 @@ specialize @source fn @loop_dead(i64, i64, i64) -> i64 { assert!(result.is_demanded(params[0]), "bounds are control roots"); assert!(result.is_demanded(params[1])); assert!(result.is_demanded(params[2])); - assert!(result.is_demanded(constant_result(&pipeline, region, 7))); + assert!(result.is_demanded(constant_result(&pipeline, cfg, 7))); assert!( - !result.is_demanded(constant_result(&pipeline, region, 0)), + !result.is_demanded(constant_result(&pipeline, cfg, 0)), "init dead" ); assert!( - !result.is_demanded(constant_result(&pipeline, region, 1)), + !result.is_demanded(constant_result(&pipeline, cfg, 1)), "body interior dead" ); assert!(!result.is_demanded(next), "yield slot dead"); @@ -1215,24 +1210,24 @@ specialize @source fn @main(i64, i64) -> i64 { #[test] fn call_arguments_are_demand_roots() { let pipeline = parse(CALL_PURITY); - let (stage, region) = source_region(&pipeline, "main"); - let result = analyze_demand(&pipeline, stage, region).expect("analysis succeeds"); + let (stage, cfg) = source_cfg(&pipeline, "main"); + let result = analyze_demand(&pipeline, stage, cfg).expect("analysis succeeds"); - let params = entry_params(&pipeline, region); + let params = entry_params(&pipeline, cfg); let (x, y) = (params[0], params[1]); - let unused = find_value(&pipeline, region, |definition| match definition { + let unused = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Lexical(Lexical::Call(_)) => { definition.results().next().map(|r| SSAValue::from(*r)) } _ => None, }); - let deadsum = find_value(&pipeline, region, |definition| match definition { + let deadsum = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Arith(Arith::Add { result, .. }) => Some(SSAValue::from(*result)), _ => None, }); assert!(result.is_demanded(x), "call args are roots (impure)"); - assert!(result.is_demanded(constant_result(&pipeline, region, 0))); + assert!(result.is_demanded(constant_result(&pipeline, cfg, 0))); assert!(!result.is_demanded(unused), "the call result is unused"); assert!(!result.is_demanded(y), "pure add with dead result"); assert!(!result.is_demanded(deadsum)); @@ -1246,12 +1241,12 @@ specialize @source fn @main(i64, i64) -> i64 { // =========================================================================== mod dense { - use kirin::prelude::{CompileStage, Pipeline, Region, SSAValue, Statement}; + use kirin::prelude::{Cfg, CompileStage, Pipeline, SSAValue, Statement}; use kirin_arith::{Arith, ArithValue}; use kirin_liveness::{DenseLivenessResult, LiveSet, analyze_demand}; use super::demand::{FOR_CARRIED_DEMAND, IF_DEAD_RESULT}; - use super::demand::{constant_result, entry_params, find_value, parse, source_region}; + use super::demand::{constant_result, entry_params, find_value, parse, source_cfg}; use crate::interpreter::ToyDenseLiveness; use crate::language::HighLevel; use crate::stage::Stage; @@ -1261,26 +1256,25 @@ mod dense { fn analyze_dense_toy( pipeline: &Pipeline, stage: CompileStage, - region: Region, + cfg: Cfg, ) -> DenseLivenessResult { let mut engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - engine.analyze(stage, region).expect("analysis succeeds"); - DenseLivenessResult::from_engine(&mut engine, stage, region) - .expect("reconstruction succeeds") + engine.analyze(stage, cfg).expect("analysis succeeds"); + DenseLivenessResult::from_engine(&mut engine, stage, cfg).expect("reconstruction succeeds") } /// The statement whose definition matches `select` (anywhere in the - /// region, including scf bodies). + /// cfg, including scf bodies). fn find_statement( pipeline: &Pipeline, - region: Region, + cfg: Cfg, select: impl Fn(&HighLevel) -> bool, ) -> Statement { let stage_id = pipeline.stage_by_name("source").expect("source stage"); let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let topology = kirin_interpreter::region_topology(info, ®ion); + let topology = kirin_interpreter::cfg_topology(info, &cfg); for block in &topology.blocks { for &stmt in &block.stmts { if select(stmt.definition(info)) { @@ -1288,7 +1282,7 @@ mod dense { } } } - panic!("no matching statement in region"); + panic!("no matching statement in cfg"); } fn live_set(values: &[SSAValue]) -> LiveSet { @@ -1301,13 +1295,13 @@ mod dense { #[test] fn dense_per_point_inside_scf_if_arm() { let pipeline = parse(IF_DEAD_RESULT); - let (stage, region) = source_region(&pipeline, "if_dead"); - let dense = analyze_dense_toy(&pipeline, stage, region); - let demand = analyze_demand(&pipeline, stage, region).expect("demand succeeds"); + let (stage, cfg) = source_cfg(&pipeline, "if_dead"); + let dense = analyze_dense_toy(&pipeline, stage, cfg); + let demand = analyze_demand(&pipeline, stage, cfg).expect("demand succeeds"); - let cond = entry_params(&pipeline, region)[0]; - let a = constant_result(&pipeline, region, 1); - let a_const = find_statement(&pipeline, region, |definition| match definition { + let cond = entry_params(&pipeline, cfg)[0]; + let a = constant_result(&pipeline, cfg, 1); + let a_const = find_statement(&pipeline, cfg, |definition| match definition { HighLevel::Constant(constant) => constant.value == ArithValue::I64(1), _ => None::<()>.is_some(), }); @@ -1320,7 +1314,7 @@ mod dense { // The if's own points: its dead result is live after it (classic // records what the walk saw: nothing uses it, so it is NOT live), and // before it only the condition survives the arm join. - let if_stmt = find_statement(&pipeline, region, |definition| { + let if_stmt = find_statement(&pipeline, cfg, |definition| { matches!(definition, HighLevel::Structured(_)) }); assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond]))); @@ -1340,26 +1334,26 @@ mod dense { #[test] fn dense_loop_carried_fixpoint() { let pipeline = parse(FOR_CARRIED_DEMAND); - let (stage, region) = source_region(&pipeline, "loop_sum"); - let dense = analyze_dense_toy(&pipeline, stage, region); + let (stage, cfg) = source_cfg(&pipeline, "loop_sum"); + let dense = analyze_dense_toy(&pipeline, stage, cfg); - let params = entry_params(&pipeline, region); + let params = entry_params(&pipeline, cfg); let (lo, hi, step) = (params[0], params[1], params[2]); - let init = constant_result(&pipeline, region, 0); - let one = constant_result(&pipeline, region, 1); - let (next, acc) = find_value(&pipeline, region, |definition| match definition { + let init = constant_result(&pipeline, cfg, 0); + let one = constant_result(&pipeline, cfg, 1); + let (next, acc) = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Arith(Arith::Add { lhs, result, .. }) => { Some((SSAValue::from(*result), *lhs)) } _ => None, }); - let for_stmt = find_statement(&pipeline, region, |definition| { + let for_stmt = find_statement(&pipeline, cfg, |definition| { matches!(definition, HighLevel::Structured(_)) }); - let add_stmt = find_statement(&pipeline, region, |definition| { + let add_stmt = find_statement(&pipeline, cfg, |definition| { matches!(definition, HighLevel::Arith(Arith::Add { .. })) }); - let sum = find_value(&pipeline, region, |definition| match definition { + let sum = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Structured(_) => { use kirin::prelude::HasResults; definition.results().next().map(|r| SSAValue::from(*r)) diff --git a/src/dialects/scf.rs b/src/dialects/scf.rs index 0bb5091b5e..a96452b879 100644 --- a/src/dialects/scf.rs +++ b/src/dialects/scf.rs @@ -12,7 +12,7 @@ use crate::ir::{Block, HasArguments, ResultValue, SSAValue}; IsPure, IsTerminator, IsConstant, - HasRegions, + HasCfgs, HasSuccessors, )] pub enum SCFInstruction { diff --git a/tests/roundtrip/composite.rs b/tests/roundtrip/composite.rs index 1f2f9c8477..4eb48ad629 100644 --- a/tests/roundtrip/composite.rs +++ b/tests/roundtrip/composite.rs @@ -152,7 +152,7 @@ fn test_roundtrip_return() { assert_eq!(output.trim(), input); } -/// Test roundtrip for a full function with region containing multiple blocks and statements. +/// Test roundtrip for a full function with CFG containing multiple blocks and statements. #[test] fn test_roundtrip_function() { let mut stage: BuilderStageInfo = BuilderStageInfo::default(); @@ -199,7 +199,7 @@ fn test_roundtrip_function() { assert_eq!(output.trim_end(), input); } -/// Test roundtrip for a function with multiple blocks in the region. +/// Test roundtrip for a function with multiple blocks in the CFG. #[test] fn test_roundtrip_function_multiple_blocks() { let mut stage: BuilderStageInfo = BuilderStageInfo::default(); diff --git a/tests/roundtrip/constant.rs b/tests/roundtrip/constant.rs index 6f8897ab7c..348a9a4957 100644 --- a/tests/roundtrip/constant.rs +++ b/tests/roundtrip/constant.rs @@ -11,7 +11,7 @@ use kirin_test_utils::roundtrip; enum ConstantLanguage { #[chumsky(format = "fn {:name}{sig} {body}")] Function { - body: Region, + body: Cfg, sig: Signature, }, #[kirin(constant, pure)] diff --git a/tests/roundtrip/digraph.rs b/tests/roundtrip/digraph.rs index 2f6f95d62b..06080a7013 100644 --- a/tests/roundtrip/digraph.rs +++ b/tests/roundtrip/digraph.rs @@ -228,13 +228,13 @@ specialize @test fn @foo(f64) -> f64 (%x: f64) { %r = add %x, %x; ret %r; } ); } -// --- Use Case 6: Region body-only projection pipeline test --- +// --- Use Case 6: Cfg body-only projection pipeline test --- -/// A dialect using Region body-only projection. +/// A dialect using Cfg body-only projection. #[derive(Debug, Clone, PartialEq, Eq, Hash, Dialect, HasParser, PrettyPrint)] #[kirin(builders, type = SimpleType, crate = kirin::ir)] #[chumsky(crate = kirin::parsers)] -enum RegionProjectedLang { +enum CfgProjectedLang { #[chumsky(format = "$add {0}, {1}")] Add( SSAValue, @@ -247,14 +247,14 @@ enum RegionProjectedLang { /// Function body: `fn {:name}{sig} {{ {body:body} }}` #[chumsky(format = "fn {:name}{sig} {{ {body:body} }}")] FuncBody { - body: Region, + body: Cfg, sig: Signature, }, } #[test] -fn test_region_projected_pipeline_roundtrip() { - let mut pipeline = make_test_pipeline::(); +fn test_cfg_projected_pipeline_roundtrip() { + let mut pipeline = make_test_pipeline::(); let input = r#" stage @test fn @foo(f64) -> f64; @@ -262,19 +262,19 @@ specialize @test fn @foo(f64) -> f64 { ^entry(%x: f64) { %r = add %x, %x; ret %r "#; pipeline .parse(input) - .expect("should parse region projection format"); + .expect("should parse cfg projection format"); let printed = pipeline.sprint(); - let mut pipeline2 = make_test_pipeline::(); + let mut pipeline2 = make_test_pipeline::(); pipeline2 .parse(printed.trim()) - .expect("should reparse region projection format"); + .expect("should reparse cfg projection format"); let printed2 = pipeline2.sprint(); assert_eq!( printed.trim(), printed2.trim(), - "region projection pipeline roundtrip should be stable" + "cfg projection pipeline roundtrip should be stable" ); } diff --git a/tests/roundtrip/function.rs b/tests/roundtrip/function.rs index 3a7f676a5a..6a5aa388a1 100644 --- a/tests/roundtrip/function.rs +++ b/tests/roundtrip/function.rs @@ -14,7 +14,7 @@ use kirin_test_utils::roundtrip; enum SplitSigLanguage { #[chumsky(format = "fn {:name}({sig:inputs}) -> {sig:return} {body}")] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] @@ -124,7 +124,7 @@ specialize @A fn @main(i32) -> i32 { // --- Tests from lambda_print.rs --- -// Lambda (Region-containing) works with #[wraps] delegation. +// Lambda (Cfg-containing) works with #[wraps] delegation. #[derive(Debug, Clone, PartialEq, Eq, Hash, Dialect, HasParser, PrettyPrint)] #[kirin(builders, type = SimpleType, crate = kirin::ir)] #[chumsky(crate = kirin::parsers)] diff --git a/tests/roundtrip/scf.rs b/tests/roundtrip/scf.rs index 36f61a25b7..891e9db077 100644 --- a/tests/roundtrip/scf.rs +++ b/tests/roundtrip/scf.rs @@ -11,7 +11,7 @@ use kirin_test_utils::roundtrip; enum ScfLanguage { #[chumsky(format = "fn {:name}{sig} {body}")] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] diff --git a/tests/roundtrip/tuple.rs b/tests/roundtrip/tuple.rs index a7273f2b42..fd690ef7ad 100644 --- a/tests/roundtrip/tuple.rs +++ b/tests/roundtrip/tuple.rs @@ -11,7 +11,7 @@ use kirin_tuple::Tuple; enum TupleLanguage { #[chumsky(format = "fn {:name}{sig} {body}")] Function { - body: Region, + body: Cfg, sig: Signature, }, #[wraps] diff --git a/tests/simple.rs b/tests/simple.rs index 24dd2474a8..44a9de9917 100644 --- a/tests/simple.rs +++ b/tests/simple.rs @@ -44,7 +44,7 @@ fn test_block() { .terminator(ret) .new(); - let body = stage.region().add_block(block_a).add_block(block_b).new(); + let body = stage.cfg().add_block(block_a).add_block(block_b).new(); let fdef = SimpleLanguage::op_function(&mut stage, body); let f = stage .specialize() From 12689502f69a9e2dae9bca7bc8b355b300e455f9 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Mon, 13 Jul 2026 16:50:54 -0400 Subject: [PATCH 03/21] Add acceptance tests for mixed language interpreter bodies This commit introduces a new test file `body_kinds.rs` that contains acceptance tests for the generic interpreter bodies. The tests cover various scenarios including: - A Cfg-bodied `main` calling a DiGraph-bodied function. - A statement inside a Cfg block that owns a DiGraph body. - A linear callable with a flat instruction list. - Ensuring that graph nodes run in topological order. These tests aim to validate the interaction between Cfg-SSA code and DiGraph computational graphs, ensuring correct execution and error handling. --- Cargo.lock | 2 + Cargo.toml | 3 + .../src/function_entry.rs | 2 +- .../src/interp_dispatch.rs | 2 +- ..._function_entry_for_callable_variants.snap | 2 +- crates/kirin-function/src/interpreter.rs | 12 +- crates/kirin-interpreter/Cargo.toml | 1 + crates/kirin-interpreter/src/core/dispatch.rs | 10 +- crates/kirin-interpreter/src/core/effect.rs | 68 ++++- crates/kirin-interpreter/src/core/error.rs | 6 + crates/kirin-interpreter/src/core/frame.rs | 14 +- crates/kirin-interpreter/src/core/mod.rs | 4 +- crates/kirin-interpreter/src/core/query.rs | 90 +++++- .../src/engines/concrete/frames.rs | 289 +++++++++++++++++- .../src/engines/concrete/interp.rs | 44 ++- .../src/engines/concrete/mod.rs | 4 +- .../src/engines/dense_backward/interp.rs | 17 +- .../src/engines/sparse_backward/interp.rs | 88 ++++-- .../src/engines/sparse_forward/interp.rs | 36 ++- crates/kirin-interpreter/src/facts/mod.rs | 5 +- .../kirin-interpreter/src/facts/topology.rs | 229 +++++++++++--- crates/kirin-interpreter/src/lib.rs | 24 +- crates/kirin-liveness/src/lib.rs | 20 +- crates/kirin-liveness/src/result.rs | 17 +- crates/kirin-test-languages/Cargo.toml | 1 + .../src/arith_function_language.rs | 6 +- .../src/graph_function_language.rs | 149 +++++++++ crates/kirin-test-languages/src/lib.rs | 4 + docs/design/interpreter/index.md | 4 +- example/toy-lang/src/interpreter/frame.rs | 11 +- example/toy-lang/src/interpreter/tests.rs | 11 +- tests/body_kinds.rs | 158 ++++++++++ 32 files changed, 1158 insertions(+), 175 deletions(-) create mode 100644 crates/kirin-test-languages/src/graph_function_language.rs create mode 100644 tests/body_kinds.rs diff --git a/Cargo.lock b/Cargo.lock index e4e149d90c..ede9dbd3e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,6 +711,7 @@ dependencies = [ "kirin-cmp", "kirin-constant", "kirin-function", + "kirin-interpreter", "kirin-ir", "kirin-lexer", "kirin-prettyless", @@ -884,6 +885,7 @@ version = "0.1.0" dependencies = [ "kirin-derive-interpreter", "kirin-ir", + "petgraph", "smallvec", "thiserror 2.0.18", ] diff --git a/Cargo.toml b/Cargo.toml index e17ca838c1..bf7ed375f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,8 +117,11 @@ kirin-cmp = { workspace = true } kirin-constant = { workspace = true } kirin-function = { workspace = true } kirin-scf = { workspace = true } +kirin-interpreter = { workspace = true } kirin-test-languages = { workspace = true, features = [ "arith-function-language", + "graph-function-language", + "interpreter", "bitwise-function-language", "callable-language", "namespaced-language", diff --git a/crates/kirin-derive-interpreter/src/function_entry.rs b/crates/kirin-derive-interpreter/src/function_entry.rs index 94f11abcae..b1ad6e3bea 100644 --- a/crates/kirin-derive-interpreter/src/function_entry.rs +++ b/crates/kirin-derive-interpreter/src/function_entry.rs @@ -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 diff --git a/crates/kirin-derive-interpreter/src/interp_dispatch.rs b/crates/kirin-derive-interpreter/src/interp_dispatch.rs index 8a2149615e..7eb02631da 100644 --- a/crates/kirin-derive-interpreter/src/interp_dispatch.rs +++ b/crates/kirin-derive-interpreter/src/interp_dispatch.rs @@ -116,7 +116,7 @@ pub fn generate(input: &DeriveInput) -> Result { 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 { diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap index 1da0bb6b7e..8524f2e6d5 100644 --- a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__function_entry__tests__function_entry_for_callable_variants.snap @@ -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 { diff --git a/crates/kirin-function/src/interpreter.rs b/crates/kirin-function/src/interpreter.rs index 05029a515f..fbfd715dd9 100644 --- a/crates/kirin-function/src/interpreter.rs +++ b/crates/kirin-function/src/interpreter.rs @@ -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, }; @@ -98,8 +98,8 @@ where &self, args: Product, _interp: &mut I, - ) -> Result, I::Error> { - Ok(FunctionBody::new(*self.cfg()).args(args)) + ) -> Result, I::Error> { + Ok(CallableBody::new(*self.cfg()).args(args)) } } @@ -112,8 +112,8 @@ where &self, args: Product, _interp: &mut I, - ) -> Result, I::Error> { - Ok(FunctionBody::new(*self.cfg()).args(args)) + ) -> Result, I::Error> { + Ok(CallableBody::new(*self.cfg()).args(args)) } } diff --git a/crates/kirin-interpreter/Cargo.toml b/crates/kirin-interpreter/Cargo.toml index 6ad2250608..5fca996fca 100644 --- a/crates/kirin-interpreter/Cargo.toml +++ b/crates/kirin-interpreter/Cargo.toml @@ -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 } diff --git a/crates/kirin-interpreter/src/core/dispatch.rs b/crates/kirin-interpreter/src/core/dispatch.rs index 64cd2e7d5e..19f6428f87 100644 --- a/crates/kirin-interpreter/src/core/dispatch.rs +++ b/crates/kirin-interpreter/src/core/dispatch.rs @@ -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. /// @@ -18,7 +18,7 @@ pub trait Interpretable: 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. @@ -27,7 +27,7 @@ pub trait FunctionEntry: Dialect { &self, args: Product, interp: &mut I, - ) -> Result, I::Error>; + ) -> Result, I::Error>; } /// Monomorphic statement dispatch over a stage enum. @@ -54,7 +54,7 @@ pub trait InterpDispatch: StageMeta { body: Statement, args: Product, interp: &mut I, - ) -> Result, I::Error>; + ) -> Result, I::Error>; } impl InterpDispatch for StageInfo @@ -76,7 +76,7 @@ where body: Statement, args: Product, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result, I::Error> { let definition = body.definition(self).clone(); definition.function_entry(args, interp) } diff --git a/crates/kirin-interpreter/src/core/effect.rs b/crates/kirin-interpreter/src/core/effect.rs index fa33570af4..58be464716 100644 --- a/crates/kirin-interpreter/src/core/effect.rs +++ b/crates/kirin-interpreter/src/core/effect.rs @@ -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 for Body { + fn from(block: Block) -> Self { + Self::Block(block) + } +} + +impl From for Body { + fn from(cfg: Cfg) -> Self { + Self::Cfg(cfg) + } +} + +impl From for Body { + fn from(graph: DiGraph) -> Self { + Self::DiGraph(graph) + } +} + +impl From 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`]. @@ -86,27 +125,32 @@ 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 { - pub cfg: Cfg, +/// rule returns one; the engine 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` (linear functions), and `DiGraph`, and rejects `UnGraph` with +/// [`InterpreterError::NoDefaultWalker`](crate::InterpreterError) unless a +/// dialect supplies its own walk. +pub struct CallableBody { + pub body: Body, pub args: Product, } -impl FunctionBody { - /// A function body over `cfg`, with no entry arguments yet. - pub fn new(cfg: Cfg) -> Self { +impl CallableBody { + /// A callable body, with no entry arguments yet. + pub fn new(body: impl Into) -> 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) -> Self { self.args = args.into_iter().collect(); self diff --git a/crates/kirin-interpreter/src/core/error.rs b/crates/kirin-interpreter/src/core/error.rs index 5211e87d26..f1e7605eef 100644 --- a/crates/kirin-interpreter/src/core/error.rs +++ b/crates/kirin-interpreter/src/core/error.rs @@ -41,6 +41,12 @@ 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 structured or linear body")] + CfgControlFlowInStructuredBody, #[error("block {0:?} fell through without a terminator effect")] BlockFellThrough(Block), #[error("function body fell through without returning")] diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 688eefe8fb..59e0e241f2 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -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. @@ -117,14 +117,14 @@ pub trait ForwardFrameDriver: Env { statement: Statement, index: EnvIndex, ) -> Result; - /// 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, index: EnvIndex, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; fn block_params(&self, stage: CompileStage, block: Block) -> Result, Self::Error>; @@ -141,6 +141,14 @@ pub trait ForwardFrameDriver: Env { ) -> Result, Self::Error>; fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, Self::Error>; + /// The default walk plan of a digraph body (ports, toposorted nodes, + /// yields). Errors on cyclic digraphs. + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result; + /// Bind a block's parameters to incoming actuals in `env` (arity-checked). fn bind_block_args( &mut self, diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index cdcd5f9b0b..1b57be2345 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -14,7 +14,7 @@ pub(crate) mod query; pub(crate) mod value; pub use dispatch::{FunctionEntry, InterpDispatch, Interpretable}; -pub use effect::{CallEffect, Callee, Edge, FunctionBody, SparseForwardEffect}; +pub use effect::{Body, CallEffect, CallableBody, Callee, Edge, SparseForwardEffect}; pub use env::{EnvIndex, EnvStackStore, Store}; pub use error::InterpreterError; pub use frame::{ @@ -22,5 +22,5 @@ pub use frame::{ }; pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; -pub use query::StageQuery; +pub use query::{GraphWalkPlan, StageQuery}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index d871a2c0ca..e95b229475 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -13,8 +13,9 @@ use kirin_ir::{ UniqueLiveSpecializationError, }; +use crate::Body; use crate::InterpreterError; -use crate::facts::topology::{self, CfgTopology}; +use crate::facts::topology::{self, BodyTopology}; /// Block parameters as SSA values. pub struct BlockParams(pub Block); @@ -115,6 +116,52 @@ where } } +/// Everything the default digraph walker needs: the boundary ports, the +/// node statements in topological order, and the graph's yields. +#[derive(Clone, Debug)] +pub struct GraphWalkPlan { + pub ports: Vec, + pub schedule: Vec, + pub yields: Vec, +} + +/// The walk plan of a digraph body (ports, toposorted nodes, yields). +/// +/// Fails with [`InterpreterError::GraphHasCycle`] on cyclic digraphs: they +/// are structurally legal IR but have no single-pass execution order. +pub struct DiGraphWalkQuery(pub kirin_ir::DiGraph); + +impl StageAction for DiGraphWalkQuery +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = GraphWalkPlan; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + let graph_info = self + .0 + .get_info(info) + .ok_or(InterpreterError::GraphHasCycle(self.0))?; + let order = petgraph::algo::toposort(graph_info.graph(), None) + .map_err(|_| InterpreterError::GraphHasCycle(self.0))?; + let schedule = order + .into_iter() + .map(|node| graph_info.graph()[node]) + .collect(); + Ok(GraphWalkPlan { + ports: graph_info.ports().to_vec(), + schedule, + yields: graph_info.yields().to_vec(), + }) + } +} + /// The unique live specialization of a staged function. pub struct UniqueSpecialization(pub StagedFunction); @@ -230,17 +277,22 @@ where } } -/// The topology of a CFG: blocks (including nested structured bodies), -/// statements per block, CFG successors, and block feeders. -pub struct CfgTopologyQuery(pub Cfg); +/// The topology of a body: blocks and graph parts (including nested +/// structured bodies), statements per part, CFG successors, block feeders, +/// and graph-port boundaries. +pub struct BodyTopologyQuery(pub Body); -impl StageAction for CfgTopologyQuery +impl StageAction for BodyTopologyQuery where S: StageMeta + HasStageInfo, L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a>, + for<'a> L: HasSuccessors<'a> + + HasBlocks<'a> + + HasCfgs<'a> + + kirin_ir::HasDigraphs<'a> + + kirin_ir::HasUngraphs<'a>, { - type Output = CfgTopology; + type Output = BodyTopology; type Error = InterpreterError; fn run( @@ -248,7 +300,7 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - Ok(topology::cfg_topology(info, &self.0)) + Ok(topology::body_topology(info, self.0)) } } @@ -291,7 +343,8 @@ pub trait StageQuery: + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch + + SupportsStageDispatch { } @@ -309,7 +362,8 @@ impl StageQuery for S where + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch + + SupportsStageDispatch { } @@ -402,10 +456,18 @@ pub(crate) fn terminator_arguments( dispatch(pipeline, stage, TerminatorArguments(block)) } -pub(crate) fn cfg_topology( +pub(crate) fn digraph_walk_plan( pipeline: &Pipeline, stage: CompileStage, - cfg: Cfg, -) -> Result { - dispatch(pipeline, stage, CfgTopologyQuery(cfg)) + graph: kirin_ir::DiGraph, +) -> Result { + dispatch(pipeline, stage, DiGraphWalkQuery(graph)) +} + +pub(crate) fn body_topology( + pipeline: &Pipeline, + stage: CompileStage, + body: Body, +) -> Result { + dispatch(pipeline, stage, BodyTopologyQuery(body)) } diff --git a/crates/kirin-interpreter/src/engines/concrete/frames.rs b/crates/kirin-interpreter/src/engines/concrete/frames.rs index 1972bd4b46..cdde9416e4 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames.rs @@ -16,7 +16,7 @@ use kirin_ir::{Block, Cfg, CompileStage, Product, SSAValue, Statement}; use crate::{ - CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, + Body, CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, }; @@ -42,15 +42,28 @@ pub enum Completion { pub trait FrameBuild: Sized { fn from_body(frame: BodyFrame) -> Self; fn from_call(frame: CallFrame) -> Self; + fn from_digraph(frame: DiGraphFrame) -> Self; } /// Traversal of one body: a function-body CFG (multi-block, with jumps) /// or a single body block (scf-style, terminated by a yield). +/// How a [`BodyFrame`] treats its current block's control flow. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockMode { + /// A block belonging to a CFG: `Jump`/`Branch` move between blocks. + CfgBlock, + /// A structured (scf-style) or linear-function body: a single block whose + /// exit is `Yield` (structured) or `Return` (linear function); + /// `Jump`/`Branch` are an error. + StructuredBody, +} + pub struct BodyFrame { stage: CompileStage, index: EnvIndex, owns_env: bool, function_boundary: bool, + mode: BlockMode, block: Block, cursor: Option, /// Entry arguments not yet bound. A body frame built by a dialect frame is @@ -83,7 +96,41 @@ where let entry = interp .cfg_entry(stage, cfg)? .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?; - Self::start(interp, stage, index, entry, args, true, true) + Self::start( + interp, + stage, + index, + entry, + args, + true, + true, + BlockMode::CfgBlock, + ) + } + + /// Walk a linear (single-`Block`) function body: bind `args` to the + /// block's parameters. Owns the activation and is the return boundary; + /// `Jump`/`Branch`/`Yield` are errors — the exit convention is `Return`. + pub fn linear_function( + interp: &mut I, + stage: CompileStage, + index: EnvIndex, + block: Block, + args: Product, + ) -> Result + where + I: FrameDriver, + { + Self::start( + interp, + stage, + index, + block, + args, + true, + true, + BlockMode::StructuredBody, + ) } /// A single body block (scf-style), to bind `args` to its parameters on the @@ -95,6 +142,7 @@ where index, owns_env: false, function_boundary: false, + mode: BlockMode::StructuredBody, block, cursor: None, pending: Some(args), @@ -111,6 +159,7 @@ where args: Product, owns_env: bool, function_boundary: bool, + mode: BlockMode, ) -> Result where I: FrameDriver, @@ -122,6 +171,7 @@ where index, owns_env, function_boundary, + mode, block, cursor, pending: None, @@ -156,12 +206,20 @@ where match interp.run_statement(self.stage, statement, self.index)? { SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_body(self))), SparseForwardEffect::Jump(edge) => { + if self.mode == BlockMode::StructuredBody { + return Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)); + } interp.bind_block_args(self.stage, self.index, edge.target, &edge.args)?; self.cursor = interp.first_statement(self.stage, edge.target)?; self.block = edge.target; Ok(FrameEffect::Continue(F::from_body(self))) } - SparseForwardEffect::Branch(_) => Err(E::from(InterpreterError::IndeterminateBranch)), + SparseForwardEffect::Branch(_) => { + if self.mode == BlockMode::StructuredBody { + return Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)); + } + Err(E::from(InterpreterError::IndeterminateBranch)) + } SparseForwardEffect::Push { frame, results } => { self.resume_slots = Some(results); Ok(FrameEffect::Push { @@ -289,13 +347,37 @@ where let target = interp.resolve_call(resolve_stage, &callee)?; let index = interp.alloc_env(); let body = interp.enter_function(target.stage, target.body, args, index)?; - let frame = BodyFrame::function(interp, target.stage, index, body.cfg, body.args)?; + let child = match body.body { + Body::Cfg(cfg) => F::from_body(BodyFrame::function( + interp, + target.stage, + index, + cfg, + body.args, + )?), + Body::Block(block) => F::from_body(BodyFrame::linear_function( + interp, + target.stage, + index, + block, + body.args, + )?), + Body::DiGraph(graph) => F::from_digraph(DiGraphFrame::function( + target.stage, + index, + graph, + body.args, + )), + other @ Body::UnGraph(_) => { + return Err(I::Error::from(InterpreterError::NoDefaultWalker(other))); + } + }; Ok(FrameEffect::Push { parent: F::from_call(CallFrame::Awaiting { caller_env, results, }), - child: F::from_body(frame), + child, }) } CallFrame::Awaiting { .. } => Err(I::Error::from(InterpreterError::Custom( @@ -343,9 +425,200 @@ where /// The default total concrete frame enum: standard concrete traversal (no /// structured-control dialect frames). +/// Traversal of one digraph body: bind entry arguments to the graph's +/// boundary ports, run the node statements in topological order, and +/// complete with the graph's yielded values. +/// +/// Pure construction — the walk plan is fetched and the ports are bound on +/// the first `step`, so a dialect frame can build one without engine access +/// (the same lazy pattern as [`BodyFrame::block`]). CFG control flow +/// (`Jump`/`Branch`) and `Yield`/`Return` are errors inside a graph: a +/// digraph's outputs are its declared yields, not a statement effect. +pub struct DiGraphFrame { + stage: CompileStage, + index: EnvIndex, + owns_env: bool, + function_boundary: bool, + graph: kirin_ir::DiGraph, + /// Entry arguments not yet bound (bound on the first `step`). + pending: Option>, + /// Remaining schedule, in topological order; `None` until the first step. + schedule: Option>, + yields: Vec, + /// Result slots awaiting a pushed child frame's `Finished` completion. + resume_slots: Option>, + _marker: std::marker::PhantomData (V, E)>, +} + +impl DiGraphFrame +where + V: Clone, + E: From, +{ + /// Walk a digraph as a function body: owns the activation and is the + /// call's return boundary (completes `Returned` with the yields). + pub fn function( + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + args: Product, + ) -> Self { + Self::with_boundary(stage, index, graph, args, true, true) + } + + /// Walk a digraph owned by a statement inside another body (pushed by a + /// dialect frame): borrows the caller's activation and completes + /// `Finished` with the yields. + pub fn nested( + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + args: Product, + ) -> Self { + Self::with_boundary(stage, index, graph, args, false, false) + } + + fn with_boundary( + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + args: Product, + owns_env: bool, + function_boundary: bool, + ) -> Self { + Self { + stage, + index, + owns_env, + function_boundary, + graph, + pending: Some(args), + schedule: None, + yields: Vec::new(), + resume_slots: None, + _marker: std::marker::PhantomData, + } + } + + /// Execute the next scheduled node and translate its + /// [`SparseForwardEffect`] into a [`FrameEffect`] over the total frame + /// type `F`. + pub fn step_into(mut self, interp: &mut I) -> Result>, E> + where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + { + // First step: fetch the walk plan and bind the boundary ports. + if let Some(args) = self.pending.take() { + let plan = interp.digraph_walk_plan(self.stage, self.graph)?; + if plan.ports.len() != args.len() { + return Err(E::from(InterpreterError::ProductArityMismatch { + expected: plan.ports.len(), + actual: args.len(), + })); + } + for (port, value) in plan.ports.iter().copied().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + self.schedule = Some(plan.schedule.into()); + self.yields = plan.yields; + return Ok(FrameEffect::Continue(F::from_digraph(self))); + } + + let Some(statement) = self.schedule.as_mut().and_then(|s| s.pop_front()) else { + return self.finish::(interp); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_digraph(self))), + SparseForwardEffect::Push { frame, results } => { + self.resume_slots = Some(results); + Ok(FrameEffect::Push { + parent: F::from_digraph(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.stage, self.index, call); + Ok(FrameEffect::Push { + parent: F::from_digraph(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)) + } + SparseForwardEffect::Yield(_) => Err(E::from(InterpreterError::Custom( + "yield inside a digraph body (a digraph's outputs are its declared yields)", + ))), + SparseForwardEffect::Return(_) => Err(E::from(InterpreterError::Custom( + "return inside a digraph body", + ))), + } + } + + /// Schedule exhausted: read the yields from the environment and complete. + fn finish(self, interp: &mut I) -> Result>, E> + where + I: FrameDriver, + F: FrameBuild, + { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + if self.function_boundary { + if self.owns_env { + interp.free_env(self.index)?; + } + Ok(FrameEffect::Complete(Completion::Returned(values))) + } else { + Ok(FrameEffect::Complete(Completion::Finished(values))) + } + } + + /// A child finished without a payload (e.g. a returned call whose results + /// are already written): resume the schedule. + pub fn resume_done_into(self) -> FrameEffect> + where + F: FrameBuild, + { + FrameEffect::Continue(F::from_digraph(self)) + } + + /// A child bubbled a completion: a pushed frame `Finished` (bind its + /// values into the awaiting result slots) or a callee `Returned`. + pub fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> + where + I: FrameDriver, + F: FrameBuild, + { + match completion { + Completion::Finished(values) => { + let slots = self.resume_slots.take().ok_or_else(|| { + E::from(InterpreterError::Custom( + "digraph resume without result slots", + )) + })?; + interp.write_results(self.index, &slots, values)?; + Ok(FrameEffect::Continue(F::from_digraph(self))) + } + Completion::Returned(_) => Err(E::from(InterpreterError::Custom( + "return bubbled into a digraph body", + ))), + } + } +} + pub enum StandardFrame { Body(BodyFrame), Call(CallFrame), + DiGraph(DiGraphFrame), } impl FrameBuild for StandardFrame { @@ -355,6 +628,9 @@ impl FrameBuild for StandardFrame { fn from_call(frame: CallFrame) -> Self { StandardFrame::Call(frame) } + fn from_digraph(frame: DiGraphFrame) -> Self { + StandardFrame::DiGraph(frame) + } } impl Frame for StandardFrame @@ -369,6 +645,7 @@ where match self { StandardFrame::Body(frame) => frame.step_into::(interp), StandardFrame::Call(frame) => frame.step_into::(interp), + StandardFrame::DiGraph(frame) => frame.step_into::(interp), } } @@ -376,6 +653,7 @@ where match self { StandardFrame::Body(frame) => Ok(frame.resume_done_into::()), StandardFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), + StandardFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), } } @@ -387,6 +665,7 @@ where match self { StandardFrame::Body(frame) => frame.resume_into::(completion, interp), StandardFrame::Call(frame) => frame.resume_into::(completion, interp), + StandardFrame::DiGraph(frame) => frame.resume_into::(completion, interp), } } } diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index 6c7665ac06..42315bbd94 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -4,8 +4,8 @@ use kirin_ir::{Block, Cfg, CompileStage, Pipeline, Product, SSAValue, StageMeta, use crate::core::query; use crate::{ - BodyFrame, Callee, Completion, Env, EnvIndex, EnvStackStore, ForwardEval, Frame, FrameBuild, - FrameDriver, FunctionBody, FunctionTarget, Interp, InterpDispatch, InterpLocation, + Body, BodyFrame, CallableBody, Callee, Completion, Env, EnvIndex, EnvStackStore, ForwardEval, + Frame, FrameBuild, FrameDriver, FunctionTarget, Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, SameStageLinker, SparseForwardEffect, StageQuery, StandardFrame, Store, drive_frames, }; @@ -159,7 +159,7 @@ where body: Statement, args: Product, index: EnvIndex, - ) -> Result, E> { + ) -> Result, E> { let pipeline = self.pipeline; let info = pipeline .stage(stage) @@ -194,6 +194,14 @@ where fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, E> { query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } + + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + query::digraph_walk_plan(self.pipeline, stage, graph).map_err(E::from) + } } impl<'ir, S, V, E, Lk, F> ConcreteInterpreter<'ir, S, V, E, Lk, F> @@ -233,8 +241,34 @@ where let index = self.alloc_env(); let args: Product = args.into_iter().collect(); let body = self.enter_function(target.stage, target.body, args, index)?; - let frame = BodyFrame::function(self, target.stage, index, body.cfg, body.args)?; - self.frames.push(F::from_body(frame)); + let frame = match body.body { + Body::Cfg(cfg) => F::from_body(BodyFrame::function( + self, + target.stage, + index, + cfg, + body.args, + )?), + Body::Block(block) => F::from_body(BodyFrame::linear_function( + self, + target.stage, + index, + block, + body.args, + )?), + Body::DiGraph(graph) => { + F::from_digraph(crate::engines::concrete::DiGraphFrame::function( + target.stage, + index, + graph, + body.args, + )) + } + other @ Body::UnGraph(_) => { + return Err(E::from(InterpreterError::NoDefaultWalker(other))); + } + }; + self.frames.push(frame); self.run() } diff --git a/crates/kirin-interpreter/src/engines/concrete/mod.rs b/crates/kirin-interpreter/src/engines/concrete/mod.rs index 968305c9ff..ea1a276d8c 100644 --- a/crates/kirin-interpreter/src/engines/concrete/mod.rs +++ b/crates/kirin-interpreter/src/engines/concrete/mod.rs @@ -3,5 +3,7 @@ pub(crate) mod frames; pub(crate) mod interp; -pub use frames::{BodyFrame, CallFrame, Completion, FrameBuild, StandardFrame}; +pub use frames::{ + BlockMode, BodyFrame, CallFrame, Completion, DiGraphFrame, FrameBuild, StandardFrame, +}; pub use interp::ConcreteInterpreter; diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 823ff776bd..475a74fbcc 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -51,6 +51,7 @@ use kirin_ir::{ }; use super::frames::{DenseBlockFrame, DenseFrameBuild}; +use crate::Body; use crate::core::query; use crate::engines::sparse_backward::CfgScope; use crate::{ @@ -654,10 +655,11 @@ where pub fn block_summary( &self, stage: CompileStage, - cfg: Cfg, + body: impl Into, block: Block, ) -> Option<&BlockLiveness> { - self.driver.summary(&Scoped::new((stage, cfg), block)) + self.driver + .summary(&Scoped::new((stage, body.into()), block)) } /// The analyzed CFG's own top-level blocks (post-`analyze`). @@ -683,9 +685,10 @@ where /// Run the block-boundary fixpoint over `cfg` in `stage`: seed every /// CFG block (a backward analysis must visit them all) and drain the /// worklist; dependencies are discovered from the terminators' edges. - pub fn analyze(&mut self, stage: CompileStage, cfg: Cfg) -> Result<(), E> { - let scope = (stage, cfg); - let topology = query::cfg_topology(self.driver.inner().pipeline(), stage, cfg)?; + pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { + let body = body.into(); + let scope = (stage, body); + let topology = query::body_topology(self.driver.inner().pipeline(), stage, body)?; let owners: Vec> = topology .cfg_blocks() .map(|block| Scoped::new(scope, block.block)) @@ -708,9 +711,9 @@ where pub fn reconstruct_points( &mut self, stage: CompileStage, - cfg: Cfg, + body: impl Into, ) -> Result, E> { - let scope = (stage, cfg); + let scope = (stage, body.into()); self.driver.store_mut().recorder = Some(DensePointStore::new()); for block in self.cfg_blocks() { // The CfgOwner walk re-absorbs the converged successor summaries, diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index e5851b7b97..8729f599f9 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -55,17 +55,20 @@ use kirin_ir::{ use crate::core::query; use crate::{ - AbstractInterpreter, CfgTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, + AbstractInterpreter, Body, CfgTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, Scoped, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, StrongDemand, Summary, SummaryEffect, }; -/// The scope a CFG-level backward analysis qualifies its facts with. +/// The scope a body-level backward analysis qualifies its facts with. /// /// Arena ids are per-stage, so the stage is part of the scope; analyzing two -/// cfgs in one engine keeps their facts distinct. -pub type CfgScope = (CompileStage, Cfg); +/// bodies in one engine keeps their facts distinct. +pub type BodyScope = (CompileStage, Body); + +/// Deprecated name for [`BodyScope`]; kept for one release. +pub type CfgScope = BodyScope; // =========================================================================== // Effect + dialect-facing trait @@ -476,9 +479,18 @@ where SSAKind::Result(statement, _) => vec![statement], SSAKind::BlockArgument(block, _) => interp.store().topology.feeders(block).to_vec(), SSAKind::Port(..) => { - return Err(E::from(InterpreterError::Custom( - "graph ports are not supported by sparse backward demand", - ))); + // A port is a boundary SSA value: the statement owning the + // graph translates demand across the boundary (its rule maps + // port/index to operands, captures, or results). + let boundary = interp.store().topology.port_boundary(owner.item); + match boundary { + Some(boundary) => vec![boundary.owner], + None => { + return Err(E::from(InterpreterError::Custom( + "graph port outside the analyzed body", + ))); + } + } } }; Ok(DemandFrame::new(stage, work)) @@ -542,16 +554,25 @@ where self.driver.inner().pipeline() } - /// The converged demand fact for `value` under the `(stage, cfg)` scope. - pub fn fact(&self, stage: CompileStage, cfg: Cfg, value: impl Into) -> Option<&V> { + /// The converged demand fact for `value` under the `(stage, body)` scope. + pub fn fact( + &self, + stage: CompileStage, + body: impl Into, + value: impl Into, + ) -> Option<&V> { self.driver - .summary(&Scoped::new((stage, cfg), value.into())) + .summary(&Scoped::new((stage, body.into()), value.into())) .map(|summary| &summary.0) } - /// All converged `(value, fact)` pairs under the `(stage, cfg)` scope. - pub fn facts(&self, stage: CompileStage, cfg: Cfg) -> impl Iterator { - let scope = (stage, cfg); + /// All converged `(value, fact)` pairs under the `(stage, body)` scope. + pub fn facts( + &self, + stage: CompileStage, + body: impl Into, + ) -> impl Iterator { + let scope = (stage, body.into()); self.driver .summaries() .iter() @@ -559,11 +580,11 @@ where .map(|(owner, summary)| (owner.item, &summary.0)) } - /// The converged facts under the `(stage, cfg)` scope as a + /// The converged facts under the `(stage, body)` scope as a /// [`SparseStore`] (the sparse per-SSA-value fact view; absent = bottom). - pub fn fact_store(&self, stage: CompileStage, cfg: Cfg) -> SparseStore { + pub fn fact_store(&self, stage: CompileStage, body: impl Into) -> SparseStore { let mut store = SparseStore::new(); - for (value, fact) in self.facts(stage, cfg) { + for (value, fact) in self.facts(stage, body) { store.set(value, fact.clone()); } store @@ -577,21 +598,19 @@ where E: From, Sem: SparseBackwardSemantic, { - /// Run the demand fixpoint over `cfg` in `stage`. + /// Run the demand fixpoint over `body` in `stage`. /// - /// **Prepass**: enumerate the CFG topology (blocks including structured - /// bodies, statements, feeders), then run every statement's rule once with - /// nothing demanded — impure statements and terminators contribute the - /// demand roots. **Propagation**: drain the value worklist; each risen - /// value dispatches the rules that translate its demand. - pub fn analyze(&mut self, stage: CompileStage, cfg: Cfg) -> Result<(), E> { - let scope = (stage, cfg); - let topology = query::cfg_topology(self.driver.inner().pipeline(), stage, cfg)?; - let statements: Vec = topology - .blocks - .iter() - .flat_map(|block| block.stmts.iter().copied()) - .collect(); + /// **Prepass**: enumerate the body topology (blocks and graph parts, + /// including structured bodies, statements, feeders, port boundaries), + /// then run every statement's rule once with nothing demanded — impure + /// statements and terminators contribute the demand roots. + /// **Propagation**: drain the value worklist; each risen value dispatches + /// the rules that translate its demand. + pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { + let body = body.into(); + let scope = (stage, body); + let topology = query::body_topology(self.driver.inner().pipeline(), stage, body)?; + let statements: Vec = topology.statements().collect(); *self.driver.store_mut() = BackwardAnalysisState { scope: Some(scope), topology, @@ -619,11 +638,16 @@ where } /// `true` iff `value` carries a non-bottom demand fact under the scope. - pub fn is_demanded(&self, stage: CompileStage, cfg: Cfg, value: impl Into) -> bool + pub fn is_demanded( + &self, + stage: CompileStage, + body: impl Into, + value: impl Into, + ) -> bool where V: HasBottom, { - self.fact(stage, cfg, value) + self.fact(stage, body, value) .is_some_and(|fact| *fact != V::bottom()) } } diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index f97af1e723..3266c85bd2 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -40,8 +40,8 @@ use kirin_ir::{ use crate::core::query; use crate::{ AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, - AbstractInterpreter, CallEffect, Callee, Env, EnvIndex, EnvStackStore, FixpointProfile, - ForwardEval, ForwardFrameDriver, ForwardSummaryDeps, Frame, FunctionBody, FunctionTarget, + AbstractInterpreter, Body, CallEffect, CallableBody, Callee, Env, EnvIndex, EnvStackStore, + FixpointProfile, ForwardEval, ForwardFrameDriver, ForwardSummaryDeps, Frame, FunctionTarget, Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, OwnerSemantics, SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, StandardAbstractFrame, StandardFixpointInterpreter, Store, Summary, SummaryDependency, SummaryDependencyIndex, @@ -652,7 +652,7 @@ where body: Statement, args: Product, index: EnvIndex, - ) -> Result, E> { + ) -> Result, E> { let pipeline = self.pipeline; let info = pipeline .stage(stage) @@ -687,6 +687,14 @@ where fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, E> { query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } + + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + query::digraph_walk_plan(self.pipeline, stage, graph).map_err(E::from) + } } // =========================================================================== @@ -729,7 +737,7 @@ where body: Statement, args: Product, index: EnvIndex, - ) -> Result, E> { + ) -> Result, E> { self.inner_mut().enter_function(stage, body, args, index) } @@ -753,6 +761,14 @@ where fn cfg_entry(&self, stage: CompileStage, cfg: Cfg) -> Result, E> { self.inner().cfg_entry(stage, cfg) } + + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + self.inner().digraph_walk_plan(stage, graph) + } } impl<'ir, S, V, E, Lk, P, F, Sem> AbstractFrameDriver for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> @@ -1051,9 +1067,15 @@ where .map(|function| function.entry.clone()) .expect("function summary present"); let body_info = self.enter_function(stage, body, entry_args, env)?; - let entry_block = self - .cfg_entry(stage, body_info.cfg)? - .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?; + let entry_block = match body_info.body { + Body::Cfg(cfg) => self + .cfg_entry(stage, cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?, + Body::Block(block) => block, + other @ (Body::DiGraph(_) | Body::UnGraph(_)) => { + return Err(E::from(InterpreterError::NoDefaultWalker(other))); + } + }; if let Some(function) = self .summary_mut(&Owner::Function(key.clone())) .and_then(|info| info.as_function_mut()) diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index abb02d8cca..b53c157918 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -8,4 +8,7 @@ pub(crate) mod topology; pub use anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; pub use store::{DenseBlockStore, DensePointStore, FactStore, ScopedSparseStore, SparseStore}; -pub use topology::{BlockTopology, CfgTopology, cfg_topology}; +pub use topology::{ + BlockTopology, BodyTopology, CfgTopology, GraphTopology, PortBoundary, body_topology, + cfg_topology, +}; diff --git a/crates/kirin-interpreter/src/facts/topology.rs b/crates/kirin-interpreter/src/facts/topology.rs index d56675da6f..2de8d670b4 100644 --- a/crates/kirin-interpreter/src/facts/topology.rs +++ b/crates/kirin-interpreter/src/facts/topology.rs @@ -1,18 +1,25 @@ -//! Dialect-neutral CFG topology enumeration. +//! Dialect-neutral body topology enumeration. //! -//! Backward analyses need the *shape* of a CFG: which blocks exist -//! (including blocks nested inside structured statements), each block's -//! statements, the CFG successor relation, and each block's *feeders* — the -//! statements whose rules can translate demand on that block's parameters -//! (terminators targeting it, statements owning it). This is topology only — -//! uses/defs/edge-argument *semantics* stay in dialect -//! [`Interpretable`](crate::Interpretable) rules; the enumeration consumes the -//! generic [`HasSuccessors`]/[`HasBlocks`]/[`HasCfgs`] contract every -//! dialect derives. +//! Backward analyses need the *shape* of a body: which blocks and graph +//! nodes exist (including bodies nested inside structured statements), each +//! block's statements, the CFG successor relation, each block's *feeders* — +//! the statements whose rules can translate demand on that block's +//! parameters (terminators targeting it, statements owning it) — and each +//! graph port's *boundary* (the statement owning the graph, and the port's +//! slot index). This is topology only — uses/defs/edge-argument *semantics* +//! stay in dialect [`Interpretable`](crate::Interpretable) rules; the +//! enumeration consumes the generic [`HasSuccessors`]/[`HasBlocks`]/ +//! [`HasCfgs`]/[`HasDigraphs`]/[`HasUngraphs`] contract every dialect +//! derives. use std::collections::{HashMap, HashSet}; -use kirin_ir::{Block, Cfg, Dialect, HasBlocks, HasCfgs, HasSuccessors, StageInfo, Statement}; +use kirin_ir::{ + Block, Cfg, DiGraph, Dialect, GetInfo, HasBlocks, HasCfgs, HasDigraphs, HasSuccessors, + HasUngraphs, Port, SSAValue, StageInfo, Statement, UnGraph, +}; + +use crate::Body; /// The shape of one block: its statements and CFG successors. #[derive(Clone, Debug)] @@ -23,19 +30,52 @@ pub struct BlockTopology { /// CFG successor blocks (targets of the block's terminator). pub successors: Vec, /// `true` for blocks nested inside a statement (structured bodies), - /// `false` for the analyzed CFG's own top-level blocks. + /// `false` for the analyzed body's own top-level blocks. pub nested: bool, } -/// The shape of a CFG: all blocks (the CFG's own top-level blocks and -/// structured bodies, recursively) plus the block-feeder index. +/// The shape of one graph body: its node statements, in declaration order. +/// +/// Order is enumeration order, not a schedule — execution scheduling is the +/// walker's job, and backward prepasses only need *all* statements. +#[derive(Clone, Debug)] +pub struct GraphTopology { + /// `Body::DiGraph(..)` or `Body::UnGraph(..)`. + pub graph: Body, + pub stmts: Vec, + /// `true` for graphs nested inside a statement, `false` for the analyzed + /// body itself. + pub nested: bool, +} + +/// Where a graph port sits on its owning statement's boundary. +/// +/// This is a **location**, not a value mapping: the owning statement's +/// dialect rule translates port/index into its operands, captures, or +/// results — for values (forward) and demand (backward) alike. +#[derive(Clone, Copy, Debug)] +pub struct PortBoundary { + /// The statement that owns the graph. + pub owner: Statement, + /// Which boundary slot this port occupies. + pub index: usize, +} + +/// The shape of a body: all blocks and graph parts (the analyzed body's own +/// plus structured bodies, recursively), the block-feeder index, and the +/// graph-port boundary index. #[derive(Clone, Debug, Default)] -pub struct CfgTopology { +pub struct BodyTopology { pub blocks: Vec, + pub graphs: Vec, feeders: HashMap>, + port_boundary: HashMap, } -impl CfgTopology { +/// Deprecated name for [`BodyTopology`]; kept for one release. +pub type CfgTopology = BodyTopology; + +impl BodyTopology { /// The statements whose rules can translate demand on `block`'s parameters: /// terminators with an edge into `block`, plus statements owning `block` /// as a structured body. @@ -43,35 +83,75 @@ impl CfgTopology { self.feeders.get(&block).map(Vec::as_slice).unwrap_or(&[]) } - /// The analyzed CFG's own top-level blocks (excluding nested bodies). + /// The analyzed body's own top-level blocks (excluding nested bodies). pub fn cfg_blocks(&self) -> impl Iterator { self.blocks.iter().filter(|block| !block.nested) } + + /// Where `port` sits on its owning statement's boundary, if the port + /// belongs to a graph enumerated by this topology. + pub fn port_boundary(&self, port: impl Into) -> Option<&PortBoundary> { + self.port_boundary.get(&port.into()) + } + + /// Every statement enumerated by this topology: block statements first, + /// then graph node statements. + pub fn statements(&self) -> impl Iterator + '_ { + self.blocks + .iter() + .flat_map(|block| block.stmts.iter().copied()) + .chain(self.graphs.iter().flat_map(|g| g.stmts.iter().copied())) + } } -/// Enumerate the topology of `cfg` in the finalized `stage`. -pub fn cfg_topology(stage: &StageInfo, cfg: &Cfg) -> CfgTopology +/// Enumerate the topology of `body` in the finalized `stage`. +pub fn body_topology(stage: &StageInfo, body: Body) -> BodyTopology where L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a>, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a> + HasDigraphs<'a> + HasUngraphs<'a>, { - let mut topology = CfgTopology::default(); + let mut topology = BodyTopology::default(); let mut visited = HashSet::new(); - for block in cfg.blocks(stage) { - collect_block(stage, block, false, &mut topology, &mut visited); + match body { + Body::Cfg(cfg) => { + for block in cfg.blocks(stage) { + collect_block(stage, block, false, &mut topology, &mut visited); + } + } + Body::Block(block) => { + collect_block(stage, block, false, &mut topology, &mut visited); + } + Body::DiGraph(graph) => { + collect_digraph(stage, graph, false, &mut topology, &mut visited); + } + Body::UnGraph(graph) => { + collect_ungraph(stage, graph, false, &mut topology, &mut visited); + } } topology } +/// Enumerate the topology of `cfg` in the finalized `stage`. +/// +/// Deprecated spelling of [`body_topology`] over a `Cfg`; kept for one +/// release. +pub fn cfg_topology(stage: &StageInfo, cfg: &Cfg) -> BodyTopology +where + L: Dialect, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a> + HasDigraphs<'a> + HasUngraphs<'a>, +{ + body_topology(stage, Body::Cfg(*cfg)) +} + fn collect_block( stage: &StageInfo, block: Block, nested: bool, - topology: &mut CfgTopology, + topology: &mut BodyTopology, visited: &mut HashSet, ) where L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a>, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a> + HasDigraphs<'a> + HasUngraphs<'a>, { if !visited.insert(block) { return; @@ -99,20 +179,97 @@ fn collect_block( nested, }); - // Structured bodies: the owning statement feeds each owned block. for &stmt in &stmts { - let definition = stmt.definition(stage); - let owned_blocks: Vec = definition.blocks().copied().collect(); - let owned_cfgs: Vec = definition.cfgs().copied().collect(); - for owned in owned_blocks { + collect_owned_bodies(stage, stmt, topology, visited); + } +} + +fn collect_digraph( + stage: &StageInfo, + graph: DiGraph, + nested: bool, + topology: &mut BodyTopology, + visited: &mut HashSet, +) where + L: Dialect, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a> + HasDigraphs<'a> + HasUngraphs<'a>, +{ + let info = graph.expect_info(stage); + let stmts: Vec = info.graph().node_weights().copied().collect(); + record_ports(info.parent(), info.ports(), topology); + topology.graphs.push(GraphTopology { + graph: Body::DiGraph(graph), + stmts: stmts.clone(), + nested, + }); + for stmt in stmts { + collect_owned_bodies(stage, stmt, topology, visited); + } +} + +fn collect_ungraph( + stage: &StageInfo, + graph: UnGraph, + nested: bool, + topology: &mut BodyTopology, + visited: &mut HashSet, +) where + L: Dialect, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a> + HasDigraphs<'a> + HasUngraphs<'a>, +{ + let info = graph.expect_info(stage); + let stmts: Vec = info.graph().node_weights().copied().collect(); + record_ports(info.parent(), info.ports(), topology); + topology.graphs.push(GraphTopology { + graph: Body::UnGraph(graph), + stmts: stmts.clone(), + nested, + }); + for stmt in stmts { + collect_owned_bodies(stage, stmt, topology, visited); + } +} + +/// Descend into every body owned by `stmt`: structured blocks/cfgs (the +/// owning statement feeds each owned block) and owned graphs (their ports' +/// boundary is recorded). +fn collect_owned_bodies( + stage: &StageInfo, + stmt: Statement, + topology: &mut BodyTopology, + visited: &mut HashSet, +) where + L: Dialect, + for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCfgs<'a> + HasDigraphs<'a> + HasUngraphs<'a>, +{ + let definition = stmt.definition(stage); + let owned_blocks: Vec = definition.blocks().copied().collect(); + let owned_cfgs: Vec = definition.cfgs().copied().collect(); + let owned_digraphs: Vec = definition.digraphs().copied().collect(); + let owned_ungraphs: Vec = definition.ungraphs().copied().collect(); + for owned in owned_blocks { + topology.feeders.entry(owned).or_default().push(stmt); + collect_block(stage, owned, true, topology, visited); + } + for owned_cfg in owned_cfgs { + for owned in owned_cfg.blocks(stage) { topology.feeders.entry(owned).or_default().push(stmt); collect_block(stage, owned, true, topology, visited); } - for owned_cfg in owned_cfgs { - for owned in owned_cfg.blocks(stage) { - topology.feeders.entry(owned).or_default().push(stmt); - collect_block(stage, owned, true, topology, visited); - } - } + } + for owned in owned_digraphs { + collect_digraph(stage, owned, true, topology, visited); + } + for owned in owned_ungraphs { + collect_ungraph(stage, owned, true, topology, visited); + } +} + +fn record_ports(owner: Option, ports: &[Port], topology: &mut BodyTopology) { + let Some(owner) = owner else { return }; + for (index, &port) in ports.iter().enumerate() { + topology + .port_boundary + .insert(SSAValue::from(port), PortBoundary { owner, index }); } } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 4f8055eea3..4b146f030d 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -66,9 +66,11 @@ 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, Interp, InterpLocation, SparseForwardInterp}; +pub use self::core::{ + AbstractInterpreter, Env, GraphWalkPlan, Interp, InterpLocation, SparseForwardInterp, +}; +pub use self::core::{Body, CallEffect, CallableBody, Callee, Edge, SparseForwardEffect}; pub use self::core::{BranchCondition, HasProductValue, expect_single}; -pub use self::core::{CallEffect, Callee, Edge, FunctionBody, SparseForwardEffect}; pub use self::core::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use self::core::{EnvIndex, EnvStackStore, Store}; pub use self::core::{FunctionEntry, InterpDispatch, Interpretable}; @@ -84,7 +86,8 @@ pub use self::core::ForwardFrameDriver as FrameDriver; // Concrete execution engine + the concrete standard frames. pub use engines::concrete::{ - BodyFrame, CallFrame, Completion, ConcreteInterpreter, FrameBuild, StandardFrame, + BlockMode, BodyFrame, CallFrame, Completion, ConcreteInterpreter, DiGraphFrame, FrameBuild, + StandardFrame, }; // Sparse forward engine (`Sem = ForwardEval`) + the abstract standard frames. pub use engines::sparse_forward::{ @@ -110,8 +113,9 @@ pub use engines::dense_backward::{ // fact stores, and cfg topology enumeration. Anchor family is a property of // the solver shape; dispatch meaning lives in `semantics`. pub use facts::{ - BlockTopology, CfgTopology, Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, - LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, SparseStore, cfg_topology, + BlockTopology, BodyTopology, CfgTopology, Change, DenseAnchor, DenseBlockStore, + DensePointStore, FactStore, GraphTopology, LatticeAnchor, PortBoundary, ProgramPoint, Scoped, + ScopedSparseStore, SparseStore, body_topology, cfg_topology, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` @@ -144,10 +148,10 @@ pub use kirin_derive_interpreter::{FunctionEntry, InterpDispatch, Interpretable} /// (`impl SemanticKey for MyKey { type Shape = ...; }`). pub mod dialect { pub use crate::{ - AnalysisShape, BranchCondition, CallEffect, Callee, ClassicLiveness, ClassicLivenessInterp, - DemandInterp, DenseBackwardEffect, DenseBackwardInterp, DenseBackwardShape, - DenseForwardShape, Edge, ForwardEval, FunctionBody, FunctionEntry, HasProductValue, Interp, - Interpretable, InterpreterError, PointFacts, SemanticKey, SparseBackwardEffect, + AnalysisShape, Body, BranchCondition, CallEffect, CallableBody, Callee, ClassicLiveness, + ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, DenseBackwardInterp, + DenseBackwardShape, DenseForwardShape, Edge, ForwardEval, FunctionEntry, HasProductValue, + Interp, Interpretable, InterpreterError, PointFacts, SemanticKey, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardShape, SparseForwardEffect, SparseForwardInterp, SparseForwardShape, StrongDemand, SuccessorEdge, }; @@ -160,7 +164,7 @@ pub mod engine { AbstractFrameDriver, AbstractInterpreter, BodyFrame, CallContext, CallFrame, Callee, Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBlockFrame, DenseFrameBuild, Env, + DenseBackwardInterpreter, DenseBlockFrame, DenseFrameBuild, DiGraphFrame, Env, ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, FrameDriver, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, InterpreterError, Linker, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, SparseForwardInterp, diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 56f90893a2..bb54c9b65b 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -26,7 +26,7 @@ pub use live::{Live, LiveSet}; pub use result::{DemandResult, DenseLivenessResult}; use kirin_interpreter::{ - DenseBackwardInterpreter, DenseBackwardTransfer, InterpDispatch, InterpreterError, + Body, DenseBackwardInterpreter, DenseBackwardTransfer, InterpDispatch, InterpreterError, SparseBackwardDriver, SparseBackwardInterpreter, StageQuery, StandardDenseBackwardFrame, }; use kirin_ir::{Cfg, CompileStage, Pipeline, StageMeta}; @@ -41,30 +41,31 @@ pub type Demand<'ir, S, E = InterpreterError> = SparseBackwardInterpreter<'ir, S pub type DenseLiveness<'ir, S, E = InterpreterError, F = StandardDenseBackwardFrame> = DenseBackwardInterpreter<'ir, S, LiveSet, E, F>; -/// Run strong liveness (sparse backward demand) over `cfg` in `stage`. +/// Run strong liveness (sparse backward demand) over `body` in `stage`. pub fn analyze_demand<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, - cfg: Cfg, + body: impl Into, ) -> Result where S: StageMeta + StageQuery + InterpDispatch>, { + let body = body.into(); let mut engine = Demand::::new(pipeline); - engine.analyze(stage, cfg)?; - Ok(DemandResult::from_engine(&engine, stage, cfg)) + engine.analyze(stage, body)?; + Ok(DemandResult::from_engine(&engine, stage, body)) } -/// Run classic per-point liveness (dense backward) over `cfg` in `stage`, +/// Run classic per-point liveness (dense backward) over `body` in `stage`, /// with the standard (structured-control-free) frames. Languages with scf /// compose [`DenseLiveness`] with their own frame type and build the result /// via [`DenseLivenessResult::from_engine`]. pub fn analyze_dense<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, - cfg: Cfg, + body: impl Into, ) -> Result where S: StageMeta @@ -79,7 +80,8 @@ where >, >, { + let body = body.into(); let mut engine = DenseLiveness::::new(pipeline); - engine.analyze(stage, cfg)?; - DenseLivenessResult::from_engine(&mut engine, stage, cfg) + engine.analyze(stage, body)?; + DenseLivenessResult::from_engine(&mut engine, stage, body) } diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs index b7e337608f..93528e2de5 100644 --- a/crates/kirin-liveness/src/result.rs +++ b/crates/kirin-liveness/src/result.rs @@ -2,9 +2,9 @@ //! per-point sets (classic liveness), plus their composition. use kirin_interpreter::{ - DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, DenseBackwardTransfer, - DenseBlockStore, DenseFrameBuild, DensePointStore, Frame, InterpDispatch, InterpreterError, - ProgramPoint, SparseBackwardInterpreter, StageQuery, + Body, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, + DenseBackwardTransfer, DenseBlockStore, DenseFrameBuild, DensePointStore, Frame, + InterpDispatch, InterpreterError, ProgramPoint, SparseBackwardInterpreter, StageQuery, }; use kirin_ir::{Block, Cfg, CompileStage, Lattice, SSAValue, StageMeta, Statement}; @@ -21,10 +21,10 @@ impl DemandResult { pub(crate) fn from_engine( engine: &SparseBackwardInterpreter<'_, S, Live, InterpreterError>, stage: CompileStage, - cfg: Cfg, + body: impl Into, ) -> Self { // The engine's sparse fact view; the demand set is its live support. - let facts = engine.fact_store(stage, cfg); + let facts = engine.fact_store(stage, body); let demanded = facts .iter() .filter(|(_, fact)| fact.is_live()) @@ -64,7 +64,7 @@ impl DenseLivenessResult { pub fn from_engine<'ir, S, F>( engine: &mut DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F>, stage: CompileStage, - cfg: Cfg, + body: impl Into, ) -> Result where S: StageMeta @@ -75,14 +75,15 @@ impl DenseLivenessResult { Completion = DenseBackwardCompletion, > + DenseFrameBuild, { + let body = body.into(); let mut blocks = DenseBlockStore::new(); for block in engine.cfg_blocks() { - if let Some(summary) = engine.block_summary(stage, cfg, block) { + if let Some(summary) = engine.block_summary(stage, body, block) { blocks.set_entry(block, summary.live_in.clone()); blocks.set_exit(block, summary.live_out.clone()); } } - let points = engine.reconstruct_points(stage, cfg)?; + let points = engine.reconstruct_points(stage, body)?; Ok(Self { blocks, points }) } diff --git a/crates/kirin-test-languages/Cargo.toml b/crates/kirin-test-languages/Cargo.toml index e9c10660bd..6813b4942d 100644 --- a/crates/kirin-test-languages/Cargo.toml +++ b/crates/kirin-test-languages/Cargo.toml @@ -21,6 +21,7 @@ kirin-derive-chumsky = { workspace = true, optional = true } default = [] simple-language = ["kirin-ir/derive"] arith-function-language = ["kirin-ir/derive", "kirin-arith", "kirin-cf", "kirin-function", "parser", "pretty"] +graph-function-language = ["kirin-ir/derive", "kirin-arith", "kirin-cf", "kirin-constant", "kirin-function", "parser", "pretty"] bitwise-function-language = ["kirin-ir/derive", "kirin-arith", "kirin-bitwise", "kirin-cf", "kirin-function", "parser", "pretty"] callable-language = ["kirin-ir/derive", "kirin-arith", "kirin-function", "parser", "pretty"] namespaced-language = ["kirin-ir/derive", "kirin-arith", "kirin-cf", "kirin-function", "parser", "pretty"] diff --git a/crates/kirin-test-languages/src/arith_function_language.rs b/crates/kirin-test-languages/src/arith_function_language.rs index 898da14da0..b39ac27539 100644 --- a/crates/kirin-test-languages/src/arith_function_language.rs +++ b/crates/kirin-test-languages/src/arith_function_language.rs @@ -34,7 +34,7 @@ pub enum ArithFunctionLanguage { #[cfg(feature = "interpreter")] mod interpreter { use kirin_interpreter::dialect::{ - ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, FunctionBody, + CallableBody, ClassicLiveness, ClassicLivenessInterp, DemandInterp, DenseBackwardEffect, FunctionEntry, Interp, Interpretable, InterpreterError, StrongDemand, }; use kirin_ir::{HasBottom, Product}; @@ -79,10 +79,10 @@ mod interpreter { &self, args: Product, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result, I::Error> { match self { ArithFunctionLanguage::Function { body, .. } => { - Ok(FunctionBody::new(*body).args(args)) + Ok(CallableBody::new(*body).args(args)) } _ => Err(I::Error::from(InterpreterError::NotCallable( interp.statement(), diff --git a/crates/kirin-test-languages/src/graph_function_language.rs b/crates/kirin-test-languages/src/graph_function_language.rs new file mode 100644 index 0000000000..ffb8896f02 --- /dev/null +++ b/crates/kirin-test-languages/src/graph_function_language.rs @@ -0,0 +1,149 @@ +//! Mixed graph/SSA test language: regular SSA IR (arith + cf + function +//! calls over `Cfg` bodies) combined with `DiGraph` computational-graph +//! bodies — the acceptance shape from issue #667. Adds a linear (`Block`- +//! bodied) callable and an inline graph-owning statement so both interpreter +//! entry paths (call and `Push`) are exercised. + +use kirin_arith::{Arith, ArithType, ArithValue}; +use kirin_cf::ControlFlow; +use kirin_constant::Constant; +use kirin_function::{Call, Return}; +use kirin_ir::{Block, Cfg, DiGraph, Dialect, Placeholder as _, ResultValue, SSAValue, Signature}; + +#[derive(Debug, Clone, PartialEq, Dialect)] +#[cfg_attr(feature = "parser", derive(kirin_chumsky::HasParser))] +#[cfg_attr(feature = "pretty", derive(kirin_derive_chumsky::PrettyPrint))] +#[kirin(builders, type = ArithType, crate = kirin_ir)] +#[cfg_attr(feature = "parser", chumsky(crate = kirin_chumsky))] +#[cfg_attr(feature = "pretty", pretty(crate = kirin_prettyless))] +pub enum GraphFunctionLanguage { + /// Standard Cfg-bodied function. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + Function { + body: Cfg, + sig: Signature, + }, + /// DiGraph-bodied callable (a computational graph as a function body). + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + GraphFunction { + body: DiGraph, + sig: Signature, + }, + /// Linear (single-`Block`) callable: a flat instruction list. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + LinearFunction { + body: Block, + sig: Signature, + }, + /// Inline graph evaluation: enters its owned digraph via a pushed frame. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "$graph_eval {lhs}, {rhs} {graph} -> {result:type}") + )] + GraphEval { + lhs: SSAValue, + rhs: SSAValue, + graph: DiGraph, + result: ResultValue, + }, + #[wraps] + Arith(Arith), + #[wraps] + Cf(ControlFlow), + #[wraps] + Constant(Constant), + #[wraps] + Call(Call), + #[wraps] + Return(Return), +} + +// Manual interpreter impls: the inline function/graph variants keep this +// enum off the `#[derive(Interpretable)]` wraps-delegation path. +#[cfg(feature = "interpreter")] +mod interpreter { + use kirin_arith::{ArithValue, CheckedDiv, CheckedRem, interpreter::DivisionByZero}; + use kirin_interpreter::BranchCondition; + use kirin_interpreter::{ + CallableBody, DiGraphFrame, ForwardEval, FrameBuild, FunctionEntry, Interp, Interpretable, + InterpreterError, SparseForwardEffect, SparseForwardInterp, + }; + use kirin_ir::{Product, SSAValue}; + + use super::GraphFunctionLanguage; + + impl Interpretable for GraphFunctionLanguage + where + I: SparseForwardInterp, + I::Frame: FrameBuild, + I::Value: std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Neg + + CheckedDiv + + CheckedRem + + BranchCondition + + TryFrom, + I::Error: From + From<>::Error>, + { + fn interpret(&self, interp: &mut I) -> Result { + match self { + GraphFunctionLanguage::Function { .. } + | GraphFunctionLanguage::GraphFunction { .. } + | GraphFunctionLanguage::LinearFunction { .. } => Ok(SparseForwardEffect::Next), + GraphFunctionLanguage::GraphEval { + lhs, + rhs, + graph, + result, + } => { + let args: Product = [interp.read(*lhs)?, interp.read(*rhs)?] + .into_iter() + .collect(); + let frame = DiGraphFrame::nested(interp.stage(), interp.index(), *graph, args); + Ok(SparseForwardEffect::Push { + frame: I::Frame::from_digraph(frame), + results: [SSAValue::from(*result)].into_iter().collect(), + }) + } + GraphFunctionLanguage::Arith(op) => op.interpret(interp), + GraphFunctionLanguage::Cf(op) => op.interpret(interp), + GraphFunctionLanguage::Constant(op) => op.interpret(interp), + GraphFunctionLanguage::Call(op) => op.interpret(interp), + GraphFunctionLanguage::Return(op) => op.interpret(interp), + } + } + } + + impl FunctionEntry for GraphFunctionLanguage { + fn function_entry( + &self, + args: Product, + interp: &mut I, + ) -> Result, I::Error> { + match self { + GraphFunctionLanguage::Function { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + GraphFunctionLanguage::GraphFunction { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + GraphFunctionLanguage::LinearFunction { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } + _ => Err(I::Error::from(InterpreterError::NotCallable( + interp.statement(), + ))), + } + } + } +} diff --git a/crates/kirin-test-languages/src/lib.rs b/crates/kirin-test-languages/src/lib.rs index 64c6851a39..b52ec3cd7e 100644 --- a/crates/kirin-test-languages/src/lib.rs +++ b/crates/kirin-test-languages/src/lib.rs @@ -7,6 +7,8 @@ mod arith_function_language; mod bitwise_function_language; #[cfg(feature = "callable-language")] mod callable_language; +#[cfg(feature = "graph-function-language")] +mod graph_function_language; #[cfg(feature = "namespaced-language")] mod namespaced_language; #[cfg(feature = "simple-language")] @@ -20,6 +22,8 @@ pub use arith_function_language::ArithFunctionLanguage; pub use bitwise_function_language::BitwiseFunctionLanguage; #[cfg(feature = "callable-language")] pub use callable_language::CallableLanguage; +#[cfg(feature = "graph-function-language")] +pub use graph_function_language::GraphFunctionLanguage; #[cfg(feature = "namespaced-language")] pub use namespaced_language::NamespacedLanguage; #[cfg(feature = "simple-language")] diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 74d98673a8..7336cf6809 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -224,7 +224,7 @@ operations are implemented. ```rust pub trait FunctionEntry: Dialect { fn function_entry(&self, args: Product, interp: &mut I) - -> Result, I::Error>; + -> Result, I::Error>; } ``` @@ -232,7 +232,7 @@ Like `Interpretable`, it receives the engine `interp` directly (function entry i forward-only, so there is no `Semantics` parameter). Statements that define function bodies (e.g. `kirin_function::Function`) -return the `FunctionBody { cfg, args }` to enter on invocation (the +return the `CallableBody { body, args }` to enter on invocation (the function-call entry descriptor — not a structured-control abstraction). On language enums it is derived; `#[callable]` marks the variants that forward, all others report `NotCallable`. diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 630d5741ef..276b491e5a 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -11,8 +11,8 @@ use std::hash::Hash; use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BodyFrame, CallFrame, Completion, Frame, FrameBuild, FrameDriver, - FrameEffect, InterpreterError, SparseForwardInterp, + AbstractFrameDriver, BodyFrame, CallFrame, Completion, DiGraphFrame, Frame, FrameBuild, + FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, BuildScfFor, @@ -27,6 +27,7 @@ use kirin_scf::{ pub enum ToyFrame { Body(BodyFrame), Call(CallFrame), + DiGraph(DiGraphFrame), ScfIf(ScfIfFrame), ScfFor(ScfForFrame), } @@ -38,6 +39,9 @@ impl FrameBuild for ToyFrame { fn from_call(frame: CallFrame) -> Self { ToyFrame::Call(frame) } + fn from_digraph(frame: DiGraphFrame) -> Self { + ToyFrame::DiGraph(frame) + } } impl BuildScfIf for ToyFrame { @@ -64,6 +68,7 @@ where match self { ToyFrame::Body(frame) => frame.step_into::(interp), ToyFrame::Call(frame) => frame.step_into::(interp), + ToyFrame::DiGraph(frame) => frame.step_into::(interp), ToyFrame::ScfIf(frame) => frame.step_into::(interp), ToyFrame::ScfFor(frame) => frame.step_into::(interp), } @@ -73,6 +78,7 @@ where match self { ToyFrame::Body(frame) => Ok(frame.resume_done_into::()), ToyFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), + ToyFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), ToyFrame::ScfIf(frame) => frame.resume_done_into::(), ToyFrame::ScfFor(frame) => frame.resume_done_into::(), } @@ -86,6 +92,7 @@ where match self { ToyFrame::Body(frame) => frame.resume_into::(completion, interp), ToyFrame::Call(frame) => frame.resume_into::(completion, interp), + ToyFrame::DiGraph(frame) => frame.resume_into::(completion, interp), ToyFrame::ScfIf(frame) => frame.resume_into::(completion), ToyFrame::ScfFor(frame) => frame.resume_into::(completion, interp), } diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 3869fd65d0..7057924243 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -567,8 +567,8 @@ mod advanced { use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BodyFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, - CrossStageLinker, Frame, FrameBuild, FrameDriver, FrameEffect, InterpreterError, - SparseForwardInterp, SparseForwardInterpreter, expect_single, + CrossStageLinker, DiGraphFrame, Frame, FrameBuild, FrameDriver, FrameEffect, + InterpreterError, SparseForwardInterp, SparseForwardInterpreter, expect_single, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, @@ -600,6 +600,7 @@ mod advanced { enum TracingFrame { Body(BodyFrame), Call(CallFrame), + DiGraph(DiGraphFrame), ScfIf(ScfIfFrame), ScfFor(ScfForFrame), } @@ -611,6 +612,9 @@ mod advanced { fn from_call(frame: CallFrame) -> Self { TracingFrame::Call(frame) } + fn from_digraph(frame: DiGraphFrame) -> Self { + TracingFrame::DiGraph(frame) + } } impl BuildScfIf for TracingFrame { @@ -643,6 +647,7 @@ mod advanced { TRACE.with(|t| t.borrow_mut().calls += 1); frame.step_into::(interp) } + TracingFrame::DiGraph(frame) => frame.step_into::(interp), TracingFrame::ScfIf(frame) => frame.step_into::(interp), TracingFrame::ScfFor(frame) => frame.step_into::(interp), } @@ -657,6 +662,7 @@ mod advanced { TracingFrame::Call(frame) => { frame.resume_done_into::().map_err(I::Error::from) } + TracingFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), TracingFrame::ScfIf(frame) => frame.resume_done_into::(), TracingFrame::ScfFor(frame) => frame.resume_done_into::(), } @@ -670,6 +676,7 @@ mod advanced { match self { TracingFrame::Body(frame) => frame.resume_into::(completion, interp), TracingFrame::Call(frame) => frame.resume_into::(completion, interp), + TracingFrame::DiGraph(frame) => frame.resume_into::(completion, interp), TracingFrame::ScfIf(frame) => frame.resume_into::(completion), TracingFrame::ScfFor(frame) => frame.resume_into::(completion, interp), } diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs new file mode 100644 index 0000000000..0b1e9d332a --- /dev/null +++ b/tests/body_kinds.rs @@ -0,0 +1,158 @@ +//! Acceptance tests for generic interpreter bodies (issue #667): a mixed +//! language where regular Cfg-SSA code and DiGraph computational graphs +//! call into each other, plus linear (Block-bodied) callables. + +use kirin::prelude::*; +use kirin_arith::{ArithConversionError, interpreter::DivisionByZero}; +use kirin_interpreter::{ + ConcreteInterpreter, InterpreterError, SameStageLinker, StandardFrame, expect_single, +}; +use kirin_test_languages::GraphFunctionLanguage; + +/// Total error for the test engine: the framework error plus the value +/// conversion/trap errors the mixed language's rules can raise. +#[derive(Debug)] +enum TestError { + Core(InterpreterError), + ArithConversion(ArithConversionError), + DivisionByZero, +} + +impl From for TestError { + fn from(error: InterpreterError) -> Self { + Self::Core(error) + } +} +impl From for TestError { + fn from(error: ArithConversionError) -> Self { + Self::ArithConversion(error) + } +} +impl From for TestError { + fn from(_: DivisionByZero) -> Self { + Self::DivisionByZero + } +} + +type L = StageInfo; +type Engine<'ir> = + ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, StandardFrame>; + +fn parse(program: &str) -> Pipeline { + let mut pipeline: Pipeline = Pipeline::new(); + ParsePipelineText::parse(&mut pipeline, program).expect("program parses"); + pipeline +} + +fn run(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + let mut interp: Engine<'_> = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +/// Entry path 1 (the call path): a Cfg-bodied `main` calls a DiGraph-bodied +/// callable; the engine builds a `DiGraphFrame`, runs the graph's arith +/// nodes in dependency order, and returns its yields. +#[test] +fn cfg_main_calls_digraph_function() { + let pipeline = parse( + r#" +stage @test fn @gadd(i64, i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @gadd(i64, i64) -> i64 digraph ^g0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + yield %s; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 2 -> i64; + %b = constant 3 -> i64; + %r = call.named @gadd(%a, %b) -> i64; + ret %r; + } +} +"#, + ); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 5); +} + +/// Entry path 2 (the dialect-frame path): a statement inside a Cfg block +/// owns a DiGraph body and enters it with `Push` — the same way `scf.if` +/// enters its Block arms. +#[test] +fn cfg_statement_pushes_digraph_body() { + let pipeline = parse( + r#" +stage @test fn @main() -> i64; + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 20 -> i64; + %b = constant 22 -> i64; + %r = graph_eval %a, %b digraph ^g0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + yield %s; + } -> i64; + ret %r; + } +} +"#, + ); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); +} + +/// A linear (single-Block) callable: the flat-instruction-list function +/// shape (QOS-style compile targets). Exits with `Return`. +#[test] +fn linear_block_callable() { + let pipeline = parse( + r#" +stage @test fn @ladd(i64, i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @ladd(i64, i64) -> i64 ^body(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + ret %s; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 40 -> i64; + %b = constant 2 -> i64; + %r = call.named @ladd(%a, %b) -> i64; + ret %r; + } +} +"#, + ); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); +} + +/// Graph nodes run in dependency order, not declaration order: the yield +/// depends on a node declared after its operand producer. +#[test] +fn digraph_runs_in_topological_order() { + let pipeline = parse( + r#" +stage @test fn @g(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @g(i64) -> i64 digraph ^g0(%x: i64) { + %d = mul %c, %c -> i64; + %c = add %x, %x -> i64; + yield %d; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 3 -> i64; + %r = call.named @g(%a) -> i64; + ret %r; + } +} +"#, + ); + // (3 + 3)^2 = 36 — requires running `add` before `mul` despite text order. + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 36); +} From 0a27ea643516093eabeb3ab97870eefc920b5bf4 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Wed, 15 Jul 2026 15:13:39 -0400 Subject: [PATCH 04/21] Refactor interpreter frame structure and update toy language integration - Renamed `BodyFrame` to `BlockFrame` and adjusted related documentation to reflect the new terminology. - Enhanced the `ToyFrame` enum to include `BlockFrame` and `CfgFrame`, replacing `BodyFrame`. - Updated `FrameBuild` implementations in `ToyFrame` and `TracingFrame` to accommodate the new frame types. - Revised tests in `tests/body_kinds.rs` to ensure compatibility with the updated frame structure. - Added comprehensive tests for structured control flow (SCF) operations, including `scf.if` and `scf.for`, demonstrating their integration with the new frame types. - Implemented a custom callable-UnGraph policy to handle ungraph traversal, ensuring proper execution order and output handling. --- AGENTS.md | 6 +- Cargo.toml | 2 +- crates/kirin-interpreter/src/core/effect.rs | 14 +- crates/kirin-interpreter/src/core/error.rs | 4 +- .../src/engines/concrete/frames.rs | 671 ------------------ .../engines/concrete/frames/block_cursor.rs | 107 +++ .../engines/concrete/frames/block_frame.rs | 117 +++ .../src/engines/concrete/frames/call_frame.rs | 190 +++++ .../src/engines/concrete/frames/cfg_frame.rs | 140 ++++ .../engines/concrete/frames/digraph_frame.rs | 178 +++++ .../src/engines/concrete/frames/mod.rs | 63 ++ .../src/engines/concrete/frames/protocol.rs | 87 +++ .../engines/concrete/frames/standard_frame.rs | 69 ++ .../src/engines/concrete/interp.rs | 48 +- .../src/engines/concrete/mod.rs | 3 +- .../src/engines/sparse_forward/frames.rs | 2 +- crates/kirin-interpreter/src/lib.rs | 15 +- crates/kirin-scf/src/interpreter.rs | 49 +- .../src/graph_function_language.rs | 27 +- docs/design/interpreter/index.md | 68 +- example/toy-lang/src/interpreter/frame.rs | 32 +- example/toy-lang/src/interpreter/tests.rs | 45 +- tests/body_kinds.rs | 585 ++++++++++++++- 23 files changed, 1712 insertions(+), 810 deletions(-) delete mode 100644 crates/kirin-interpreter/src/engines/concrete/frames.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/mod.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs create mode 100644 crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs diff --git a/AGENTS.md b/AGENTS.md index e0523c3a4d..9dc43a1eb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` 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` 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>` uses the concrete standard frames (`engines/concrete/frames.rs`: `BodyFrame`/`CallFrame`, single-path). `SparseForwardInterpreter<'ir, S, V, E, Lk, P = ContextInsensitive, F = StandardAbstractFrame>` (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` 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>` 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>` (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` 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`. diff --git a/Cargo.toml b/Cargo.toml index bf7ed375f5..0788d53acc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ kirin-cmp = { workspace = true } kirin-constant = { workspace = true } kirin-function = { workspace = true } kirin-scf = { workspace = true } -kirin-interpreter = { workspace = true } +kirin-interpreter = { workspace = true, features = ["derive"] } kirin-test-languages = { workspace = true, features = [ "arith-function-language", "graph-function-language", diff --git a/crates/kirin-interpreter/src/core/effect.rs b/crates/kirin-interpreter/src/core/effect.rs index 58be464716..4176a2e1f0 100644 --- a/crates/kirin-interpreter/src/core/effect.rs +++ b/crates/kirin-interpreter/src/core/effect.rs @@ -130,12 +130,14 @@ pub enum Callee { /// /// This is the function-call entry descriptor — the call mechanism, not a /// structured-control abstraction. A [`FunctionEntry`](crate::FunctionEntry) -/// rule returns one; the engine 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` (linear functions), and `DiGraph`, and rejects `UnGraph` with -/// [`InterpreterError::NoDefaultWalker`](crate::InterpreterError) unless a -/// dialect supplies its own walk. +/// 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 { pub body: Body, pub args: Product, diff --git a/crates/kirin-interpreter/src/core/error.rs b/crates/kirin-interpreter/src/core/error.rs index f1e7605eef..96f57dd120 100644 --- a/crates/kirin-interpreter/src/core/error.rs +++ b/crates/kirin-interpreter/src/core/error.rs @@ -45,12 +45,10 @@ pub enum InterpreterError { 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 structured or linear body")] + #[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")] diff --git a/crates/kirin-interpreter/src/engines/concrete/frames.rs b/crates/kirin-interpreter/src/engines/concrete/frames.rs deleted file mode 100644 index cdde9416e4..0000000000 --- a/crates/kirin-interpreter/src/engines/concrete/frames.rs +++ /dev/null @@ -1,671 +0,0 @@ -//! The **concrete** implementation of the shared [`frame`](crate::core::frame) -//! protocol. -//! -//! These are the default total frames for [`ConcreteInterpreter`](crate::ConcreteInterpreter): -//! [`BodyFrame`] (walks a function-body CFG or a single body block) and -//! [`CallFrame`] (call/return). They implement the shared [`Frame`] trait by -//! consuming the dialect [`SparseForwardEffect`] and driving a single deterministic -//! path. Structured-control dialects do not get a framework "scope": they push -//! a frame **they own** through [`SparseForwardEffect::Push`] (that frame may build a -//! [`BodyFrame`] to walk a chosen body — a reusable building block, not -//! framework-owned structured semantics). A language that combines such a -//! dialect defines its own total frame enum embedding [`BodyFrame`]/[`CallFrame`] -//! via [`FrameBuild`] plus its dialect frames. The forward abstract analogue -//! lives in [`sparse_forward::frames`](crate::engines::sparse_forward::frames). - -use kirin_ir::{Block, Cfg, CompileStage, Product, SSAValue, Statement}; - -use crate::{ - Body, CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, - SparseForwardEffect, SparseForwardInterp, -}; - -/// Completion payloads produced by the standard concrete frames. -/// -/// `Returned` bubbles a function return across frames to the enclosing -/// [`CallFrame`]; `Finished` carries the values a pushed body frame yielded back -/// to whoever pushed it (written into that push's result slots). -pub enum Completion { - /// A function returned these values; bubbles to the enclosing - /// [`CallFrame`], or finishes the run at the root. - Returned(Product), - /// A pushed body frame yielded these values to its pusher. - Finished(Product), -} - -/// Construction trait letting any total frame enum embed the standard concrete -/// frames. -/// -/// The default [`StandardFrame`] implements it trivially; a language that adds -/// structured-control dialects implements it on its own enum to reuse -/// [`BodyFrame`]/[`CallFrame`] traversal while adding its own dialect frames. -pub trait FrameBuild: Sized { - fn from_body(frame: BodyFrame) -> Self; - fn from_call(frame: CallFrame) -> Self; - fn from_digraph(frame: DiGraphFrame) -> Self; -} - -/// Traversal of one body: a function-body CFG (multi-block, with jumps) -/// or a single body block (scf-style, terminated by a yield). -/// How a [`BodyFrame`] treats its current block's control flow. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BlockMode { - /// A block belonging to a CFG: `Jump`/`Branch` move between blocks. - CfgBlock, - /// A structured (scf-style) or linear-function body: a single block whose - /// exit is `Yield` (structured) or `Return` (linear function); - /// `Jump`/`Branch` are an error. - StructuredBody, -} - -pub struct BodyFrame { - stage: CompileStage, - index: EnvIndex, - owns_env: bool, - function_boundary: bool, - mode: BlockMode, - block: Block, - cursor: Option, - /// Entry arguments not yet bound. A body frame built by a dialect frame is - /// constructed without engine access — it binds on its first `step`, so - /// building it requires no [`FrameDriver`] (a dialect frame builds these as - /// plain values, no engine capability or trait-resolution cycle). - pending: Option>, - /// Result slots awaiting a pushed body frame's `Finished` completion. - resume_slots: Option>, - _marker: std::marker::PhantomData (V, E)>, -} - -impl BodyFrame -where - V: Clone, - E: From, -{ - /// Walk a function body: start at the entry block of `cfg`, binding - /// `args` to its parameters. Owns the activation and is the return boundary. - pub fn function( - interp: &mut I, - stage: CompileStage, - index: EnvIndex, - cfg: Cfg, - args: Product, - ) -> Result - where - I: FrameDriver, - { - let entry = interp - .cfg_entry(stage, cfg)? - .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?; - Self::start( - interp, - stage, - index, - entry, - args, - true, - true, - BlockMode::CfgBlock, - ) - } - - /// Walk a linear (single-`Block`) function body: bind `args` to the - /// block's parameters. Owns the activation and is the return boundary; - /// `Jump`/`Branch`/`Yield` are errors — the exit convention is `Return`. - pub fn linear_function( - interp: &mut I, - stage: CompileStage, - index: EnvIndex, - block: Block, - args: Product, - ) -> Result - where - I: FrameDriver, - { - Self::start( - interp, - stage, - index, - block, - args, - true, - true, - BlockMode::StructuredBody, - ) - } - - /// A single body block (scf-style), to bind `args` to its parameters on the - /// first step. Borrows the caller's activation and is not a return boundary. - /// Pure construction — needs no engine access. - pub fn block(stage: CompileStage, index: EnvIndex, block: Block, args: Product) -> Self { - Self { - stage, - index, - owns_env: false, - function_boundary: false, - mode: BlockMode::StructuredBody, - block, - cursor: None, - pending: Some(args), - resume_slots: None, - _marker: std::marker::PhantomData, - } - } - - fn start( - interp: &mut I, - stage: CompileStage, - index: EnvIndex, - block: Block, - args: Product, - owns_env: bool, - function_boundary: bool, - mode: BlockMode, - ) -> Result - where - I: FrameDriver, - { - interp.bind_block_args(stage, index, block, &args)?; - let cursor = interp.first_statement(stage, block)?; - Ok(Self { - stage, - index, - owns_env, - function_boundary, - mode, - block, - cursor, - pending: None, - resume_slots: None, - _marker: std::marker::PhantomData, - }) - } - - /// Execute the next statement and translate its [`SparseForwardEffect`] into a - /// [`FrameEffect`] over the total frame type `F`. - pub fn step_into(mut self, interp: &mut I) -> Result>, E> - where - I: FrameDriver + SparseForwardInterp, - F: FrameBuild, - { - // Bind entry arguments lazily on the first step (a dialect-built body - // frame carries them unbound). - if let Some(args) = self.pending.take() { - interp.bind_block_args(self.stage, self.index, self.block, &args)?; - self.cursor = interp.first_statement(self.stage, self.block)?; - return Ok(FrameEffect::Continue(F::from_body(self))); - } - let Some(statement) = self.cursor else { - return Err(E::from(if self.function_boundary { - InterpreterError::FunctionBodyFellThrough - } else { - InterpreterError::BlockFellThrough(self.block) - })); - }; - self.cursor = interp.next_statement(self.stage, self.block, statement)?; - - match interp.run_statement(self.stage, statement, self.index)? { - SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_body(self))), - SparseForwardEffect::Jump(edge) => { - if self.mode == BlockMode::StructuredBody { - return Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)); - } - interp.bind_block_args(self.stage, self.index, edge.target, &edge.args)?; - self.cursor = interp.first_statement(self.stage, edge.target)?; - self.block = edge.target; - Ok(FrameEffect::Continue(F::from_body(self))) - } - SparseForwardEffect::Branch(_) => { - if self.mode == BlockMode::StructuredBody { - return Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)); - } - Err(E::from(InterpreterError::IndeterminateBranch)) - } - SparseForwardEffect::Push { frame, results } => { - self.resume_slots = Some(results); - Ok(FrameEffect::Push { - parent: F::from_body(self), - child: frame, - }) - } - SparseForwardEffect::Call(call) => { - let pending = CallFrame::pending(self.stage, self.index, call); - Ok(FrameEffect::Push { - parent: F::from_body(self), - child: F::from_call(pending), - }) - } - SparseForwardEffect::Yield(values) => { - if self.function_boundary { - return Err(E::from(InterpreterError::Custom( - "yield reached a function boundary", - ))); - } - Ok(FrameEffect::Complete(Completion::Finished(values))) - } - SparseForwardEffect::Return(values) => self.finish_return::(interp, values), - } - } - - /// A child finished without a payload (its results are already in the - /// shared index, e.g. a returned call): resume at the advanced cursor. - pub fn resume_done_into(self) -> FrameEffect> - where - F: FrameBuild, - { - FrameEffect::Continue(F::from_body(self)) - } - - /// A child bubbled a completion: a pushed body frame `Finished` (write its - /// values into the pending slots and continue) or a `Returned` (a return - /// happened in the child — keep bubbling). - pub fn resume_into( - mut self, - completion: Completion, - interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - match completion { - Completion::Finished(values) => { - let slots = self.resume_slots.take().ok_or_else(|| { - E::from(InterpreterError::Custom("body resume without result slots")) - })?; - interp.write_results(self.index, &slots, values)?; - Ok(FrameEffect::Continue(F::from_body(self))) - } - Completion::Returned(values) => self.finish_return::(interp, values), - } - } - - /// Produce a `Returned` completion, freeing the activation record when this - /// frame is the owning function boundary. - fn finish_return( - self, - interp: &mut I, - values: Product, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - if self.function_boundary && self.owns_env { - interp.free_env(self.index)?; - } - Ok(FrameEffect::Complete(Completion::Returned(values))) - } -} - -/// Call/return bookkeeping: dispatch a function invocation, then await its -/// return and land the results in the caller's activation. -pub enum CallFrame { - /// Not yet dispatched: resolve the callee, enter its body. - Pending { - resolve_stage: CompileStage, - callee: Callee, - args: Product, - caller_env: EnvIndex, - results: Product, - }, - /// Dispatched: awaiting the callee's `Returned` completion. - Awaiting { - caller_env: EnvIndex, - results: Product, - }, -} - -impl CallFrame -where - V: Clone, -{ - /// Build a pending call frame from a [`CallEffect`]. - pub fn pending(scope_stage: CompileStage, caller_env: EnvIndex, call: CallEffect) -> Self { - CallFrame::Pending { - resolve_stage: call.stage.unwrap_or(scope_stage), - callee: call.callee, - args: call.args, - caller_env, - results: call.results, - } - } - - pub fn step_into(self, interp: &mut I) -> Result>, I::Error> - where - I: FrameDriver, - I::Error: From, - F: FrameBuild, - { - match self { - CallFrame::Pending { - resolve_stage, - callee, - args, - caller_env, - results, - } => { - let target = interp.resolve_call(resolve_stage, &callee)?; - let index = interp.alloc_env(); - let body = interp.enter_function(target.stage, target.body, args, index)?; - let child = match body.body { - Body::Cfg(cfg) => F::from_body(BodyFrame::function( - interp, - target.stage, - index, - cfg, - body.args, - )?), - Body::Block(block) => F::from_body(BodyFrame::linear_function( - interp, - target.stage, - index, - block, - body.args, - )?), - Body::DiGraph(graph) => F::from_digraph(DiGraphFrame::function( - target.stage, - index, - graph, - body.args, - )), - other @ Body::UnGraph(_) => { - return Err(I::Error::from(InterpreterError::NoDefaultWalker(other))); - } - }; - Ok(FrameEffect::Push { - parent: F::from_call(CallFrame::Awaiting { - caller_env, - results, - }), - child, - }) - } - CallFrame::Awaiting { .. } => Err(I::Error::from(InterpreterError::Custom( - "call frame stepped while awaiting a return", - ))), - } - } - - pub fn resume_done_into(self) -> Result>, InterpreterError> { - Err(InterpreterError::Custom( - "call frame resumed without a return", - )) - } - - pub fn resume_into( - self, - completion: Completion, - interp: &mut I, - ) -> Result>, I::Error> - where - I: FrameDriver, - I::Error: From, - F: FrameBuild, - { - match (self, completion) { - ( - CallFrame::Awaiting { - caller_env, - results, - }, - Completion::Returned(values), - ) => { - interp.write_results(caller_env, &results, values)?; - Ok(FrameEffect::Done) - } - (CallFrame::Awaiting { .. }, Completion::Finished(_)) => Err(I::Error::from( - InterpreterError::Custom("call frame resumed with a body completion"), - )), - (CallFrame::Pending { .. }, _) => Err(I::Error::from(InterpreterError::Custom( - "call frame resumed before dispatch", - ))), - } - } -} - -/// The default total concrete frame enum: standard concrete traversal (no -/// structured-control dialect frames). -/// Traversal of one digraph body: bind entry arguments to the graph's -/// boundary ports, run the node statements in topological order, and -/// complete with the graph's yielded values. -/// -/// Pure construction — the walk plan is fetched and the ports are bound on -/// the first `step`, so a dialect frame can build one without engine access -/// (the same lazy pattern as [`BodyFrame::block`]). CFG control flow -/// (`Jump`/`Branch`) and `Yield`/`Return` are errors inside a graph: a -/// digraph's outputs are its declared yields, not a statement effect. -pub struct DiGraphFrame { - stage: CompileStage, - index: EnvIndex, - owns_env: bool, - function_boundary: bool, - graph: kirin_ir::DiGraph, - /// Entry arguments not yet bound (bound on the first `step`). - pending: Option>, - /// Remaining schedule, in topological order; `None` until the first step. - schedule: Option>, - yields: Vec, - /// Result slots awaiting a pushed child frame's `Finished` completion. - resume_slots: Option>, - _marker: std::marker::PhantomData (V, E)>, -} - -impl DiGraphFrame -where - V: Clone, - E: From, -{ - /// Walk a digraph as a function body: owns the activation and is the - /// call's return boundary (completes `Returned` with the yields). - pub fn function( - stage: CompileStage, - index: EnvIndex, - graph: kirin_ir::DiGraph, - args: Product, - ) -> Self { - Self::with_boundary(stage, index, graph, args, true, true) - } - - /// Walk a digraph owned by a statement inside another body (pushed by a - /// dialect frame): borrows the caller's activation and completes - /// `Finished` with the yields. - pub fn nested( - stage: CompileStage, - index: EnvIndex, - graph: kirin_ir::DiGraph, - args: Product, - ) -> Self { - Self::with_boundary(stage, index, graph, args, false, false) - } - - fn with_boundary( - stage: CompileStage, - index: EnvIndex, - graph: kirin_ir::DiGraph, - args: Product, - owns_env: bool, - function_boundary: bool, - ) -> Self { - Self { - stage, - index, - owns_env, - function_boundary, - graph, - pending: Some(args), - schedule: None, - yields: Vec::new(), - resume_slots: None, - _marker: std::marker::PhantomData, - } - } - - /// Execute the next scheduled node and translate its - /// [`SparseForwardEffect`] into a [`FrameEffect`] over the total frame - /// type `F`. - pub fn step_into(mut self, interp: &mut I) -> Result>, E> - where - I: FrameDriver + SparseForwardInterp, - F: FrameBuild, - { - // First step: fetch the walk plan and bind the boundary ports. - if let Some(args) = self.pending.take() { - let plan = interp.digraph_walk_plan(self.stage, self.graph)?; - if plan.ports.len() != args.len() { - return Err(E::from(InterpreterError::ProductArityMismatch { - expected: plan.ports.len(), - actual: args.len(), - })); - } - for (port, value) in plan.ports.iter().copied().zip(args) { - interp.env_write(self.index, SSAValue::from(port), value)?; - } - self.schedule = Some(plan.schedule.into()); - self.yields = plan.yields; - return Ok(FrameEffect::Continue(F::from_digraph(self))); - } - - let Some(statement) = self.schedule.as_mut().and_then(|s| s.pop_front()) else { - return self.finish::(interp); - }; - - match interp.run_statement(self.stage, statement, self.index)? { - SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_digraph(self))), - SparseForwardEffect::Push { frame, results } => { - self.resume_slots = Some(results); - Ok(FrameEffect::Push { - parent: F::from_digraph(self), - child: frame, - }) - } - SparseForwardEffect::Call(call) => { - let pending = CallFrame::pending(self.stage, self.index, call); - Ok(FrameEffect::Push { - parent: F::from_digraph(self), - child: F::from_call(pending), - }) - } - SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { - Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)) - } - SparseForwardEffect::Yield(_) => Err(E::from(InterpreterError::Custom( - "yield inside a digraph body (a digraph's outputs are its declared yields)", - ))), - SparseForwardEffect::Return(_) => Err(E::from(InterpreterError::Custom( - "return inside a digraph body", - ))), - } - } - - /// Schedule exhausted: read the yields from the environment and complete. - fn finish(self, interp: &mut I) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - let values: Product = self - .yields - .iter() - .map(|&value| interp.env_read(self.index, value)) - .collect::>()?; - if self.function_boundary { - if self.owns_env { - interp.free_env(self.index)?; - } - Ok(FrameEffect::Complete(Completion::Returned(values))) - } else { - Ok(FrameEffect::Complete(Completion::Finished(values))) - } - } - - /// A child finished without a payload (e.g. a returned call whose results - /// are already written): resume the schedule. - pub fn resume_done_into(self) -> FrameEffect> - where - F: FrameBuild, - { - FrameEffect::Continue(F::from_digraph(self)) - } - - /// A child bubbled a completion: a pushed frame `Finished` (bind its - /// values into the awaiting result slots) or a callee `Returned`. - pub fn resume_into( - mut self, - completion: Completion, - interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - match completion { - Completion::Finished(values) => { - let slots = self.resume_slots.take().ok_or_else(|| { - E::from(InterpreterError::Custom( - "digraph resume without result slots", - )) - })?; - interp.write_results(self.index, &slots, values)?; - Ok(FrameEffect::Continue(F::from_digraph(self))) - } - Completion::Returned(_) => Err(E::from(InterpreterError::Custom( - "return bubbled into a digraph body", - ))), - } - } -} - -pub enum StandardFrame { - Body(BodyFrame), - Call(CallFrame), - DiGraph(DiGraphFrame), -} - -impl FrameBuild for StandardFrame { - fn from_body(frame: BodyFrame) -> Self { - StandardFrame::Body(frame) - } - fn from_call(frame: CallFrame) -> Self { - StandardFrame::Call(frame) - } - fn from_digraph(frame: DiGraphFrame) -> Self { - StandardFrame::DiGraph(frame) - } -} - -impl Frame for StandardFrame -where - I: FrameDriver + SparseForwardInterp>, - V: Clone, - E: From, -{ - type Completion = Completion; - - fn step(self, interp: &mut I) -> Result, I::Error> { - match self { - StandardFrame::Body(frame) => frame.step_into::(interp), - StandardFrame::Call(frame) => frame.step_into::(interp), - StandardFrame::DiGraph(frame) => frame.step_into::(interp), - } - } - - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { - match self { - StandardFrame::Body(frame) => Ok(frame.resume_done_into::()), - StandardFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), - StandardFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), - } - } - - fn resume( - self, - completion: Self::Completion, - interp: &mut I, - ) -> Result, I::Error> { - match self { - StandardFrame::Body(frame) => frame.resume_into::(completion, interp), - StandardFrame::Call(frame) => frame.resume_into::(completion, interp), - StandardFrame::DiGraph(frame) => frame.resume_into::(completion, interp), - } - } -} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs new file mode 100644 index 0000000000..b1d96c386a --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs @@ -0,0 +1,107 @@ +use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; + +use crate::{EnvIndex, FrameDriver, InterpreterError}; + +/// Block-cursor mechanics shared by the block-shaped walkers +/// ([`BlockFrame`](super::BlockFrame) and [`CfgFrame`](super::CfgFrame)): +/// the current block, the statement cursor, lazily bound entry arguments, +/// and the result slots awaiting a pushed child's completion values. +/// +/// Traversal state only — no environment ownership, no invocation role. +pub(super) struct BlockCursor { + pub(super) stage: CompileStage, + pub(super) index: EnvIndex, + pub(super) block: Block, + cursor: Option, + /// Entry arguments not yet bound. A frame built by a dialect frame is + /// constructed without engine access — it binds on its first `step`, so + /// construction needs no [`FrameDriver`]. + pending: Option>, + /// Result slots awaiting a pushed child frame's completion values. + resume_slots: Option>, +} + +impl BlockCursor { + pub(super) fn new( + stage: CompileStage, + index: EnvIndex, + block: Block, + args: Product, + ) -> Self { + Self { + stage, + index, + block, + cursor: None, + pending: Some(args), + resume_slots: None, + } + } + + /// Bind pending entry arguments to the block's parameters and position + /// the cursor at its first statement. Returns `true` if binding happened + /// on this call (the frame should `Continue` and step again). + pub(super) fn bind_entry(&mut self, interp: &mut I) -> Result + where + I: FrameDriver, + { + match self.pending.take() { + Some(args) => { + interp.bind_block_args(self.stage, self.index, self.block, &args)?; + self.cursor = interp.first_statement(self.stage, self.block)?; + Ok(true) + } + None => Ok(false), + } + } + + /// Take the current statement, advancing the cursor past it. + pub(super) fn advance(&mut self, interp: &I) -> Result, I::Error> + where + I: FrameDriver, + { + let Some(statement) = self.cursor else { + return Ok(None); + }; + self.cursor = interp.next_statement(self.stage, self.block, statement)?; + Ok(Some(statement)) + } + + /// Move to `target` (a CFG jump): bind its parameters and reset the + /// cursor to its first statement. + pub(super) fn enter_block( + &mut self, + interp: &mut I, + target: Block, + args: &Product, + ) -> Result<(), I::Error> + where + I: FrameDriver, + { + interp.bind_block_args(self.stage, self.index, target, args)?; + self.cursor = interp.first_statement(self.stage, target)?; + self.block = target; + Ok(()) + } + + /// Stash the result slots of a `Push` until the child completes. + pub(super) fn expect_results(&mut self, results: Product) { + self.resume_slots = Some(results); + } + + /// Write a completed child's values into the stashed result slots. + pub(super) fn write_child_results( + &mut self, + interp: &mut I, + values: Product, + ) -> Result<(), I::Error> + where + I: FrameDriver, + I::Error: From, + { + let slots = self.resume_slots.take().ok_or_else(|| { + I::Error::from(InterpreterError::Custom("body resume without result slots")) + })?; + interp.write_results(self.index, &slots, values) + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs new file mode 100644 index 0000000000..9d1e815224 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs @@ -0,0 +1,117 @@ +use kirin_ir::{Block, CompileStage, Product}; + +use crate::{ + EnvIndex, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, +}; + +use super::block_cursor::BlockCursor; +use super::{CallFrame, Completion, FrameBuild}; + +/// Representation walker for exactly one [`Block`]: bind its parameters, +/// run its statements in order, and surface the exit through the +/// [`Completion`] protocol — `Return` as +/// [`Returned`](Completion::Returned), `Yield` as +/// [`Yielded`](Completion::Yielded). +/// +/// Traversal mechanics only. A `BlockFrame` does not own an activation, is +/// not a call boundary, and does not know whether it is a callable function +/// body ([`CallFrame`] → `BlockFrame`) or a nested structured-operation body +/// (dialect frame → `BlockFrame`): the parent frame defines the role and +/// interprets the completion. CFG transitions (`Jump`/`Branch`) are rejected +/// — a single block owns no CFG edges; multi-block traversal is +/// [`CfgFrame`](super::CfgFrame)'s job. +pub struct BlockFrame { + cursor: BlockCursor, + _marker: std::marker::PhantomData E>, +} + +impl BlockFrame +where + V: Clone, + E: From, +{ + /// Walk `block`, binding `args` to its parameters on the first step. + /// Pure construction — needs no engine access, so a dialect frame can + /// build one as plain values. + pub fn new(stage: CompileStage, index: EnvIndex, block: Block, args: Product) -> Self { + Self { + cursor: BlockCursor::new(stage, index, block, args), + _marker: std::marker::PhantomData, + } + } + + /// Execute the next statement and translate its [`SparseForwardEffect`] + /// into a [`FrameEffect`] over the total frame type `F`. + pub fn step_into(mut self, interp: &mut I) -> Result>, E> + where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + { + if self.cursor.bind_entry(interp)? { + return Ok(FrameEffect::Continue(F::from_block(self))); + } + let Some(statement) = self.cursor.advance(interp)? else { + return Err(E::from(InterpreterError::BlockFellThrough( + self.cursor.block, + ))); + }; + + match interp.run_statement(self.cursor.stage, statement, self.cursor.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_block(self))), + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)) + } + SparseForwardEffect::Push { frame, results } => { + self.cursor.expect_results(results); + Ok(FrameEffect::Push { + parent: F::from_block(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.cursor.stage, self.cursor.index, call); + Ok(FrameEffect::Push { + parent: F::from_block(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Yield(values) => { + Ok(FrameEffect::Complete(Completion::Yielded(values))) + } + SparseForwardEffect::Return(values) => { + Ok(FrameEffect::Complete(Completion::Returned(values))) + } + } + } + + /// A child finished without a payload (its results are already in the + /// shared activation, e.g. a returned call): resume at the advanced + /// cursor. + pub fn resume_done_into(self) -> FrameEffect> + where + F: FrameBuild, + { + FrameEffect::Continue(F::from_block(self)) + } + + /// A child bubbled a completion: a pushed frame's values land in the + /// push's result slots; a `Returned` keeps bubbling toward the nearest + /// [`CallFrame`] (this frame owns no activation to free). + pub fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> + where + I: FrameDriver, + F: FrameBuild, + { + match completion { + Completion::Finished(values) | Completion::Yielded(values) => { + self.cursor.write_child_results(interp, values)?; + Ok(FrameEffect::Continue(F::from_block(self))) + } + Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs new file mode 100644 index 0000000000..9ffea78240 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -0,0 +1,190 @@ +use kirin_ir::{CompileStage, Product, SSAValue}; + +use crate::{Body, CallEffect, Callee, EnvIndex, FrameDriver, FrameEffect, InterpreterError}; + +use super::{BlockFrame, CfgFrame, Completion, DiGraphFrame, FrameBuild, UnGraphEntry}; + +/// The function-call boundary frame: interpreter runtime bookkeeping, not a +/// function dialect operation and not the callable itself. +/// +/// A `CallFrame` owns the whole activation lifecycle that representation +/// walkers deliberately don't: +/// +/// 1. resolve the callee through the engine's [`Linker`](crate::Linker); +/// 2. allocate the callee activation; +/// 3. ask [`FunctionEntry`](crate::FunctionEntry) for the callable body +/// descriptor ([`CallableBody`](crate::CallableBody)); +/// 4. select the entry frame for the closed [`Body`] variant — +/// `Cfg` → [`CfgFrame`], `Block` → [`BlockFrame`], +/// `DiGraph` → [`DiGraphFrame`], `UnGraph` → the dialect/compiler policy +/// ([`FrameBuild::from_ungraph_entry`]); +/// 5. suspend while the callee frame runs; +/// 6. validate the callee's completion kind ([`Returned`](Completion::Returned) +/// or a graph's natural [`Finished`](Completion::Finished) are returns; a +/// structured [`Yielded`](Completion::Yielded) is an error); +/// 7. free the callee activation exactly once; +/// 8. deliver the returned values — into the caller's result slots for a +/// nested call, or as the run's completion for a root call +/// ([`ConcreteInterpreter::call`](crate::ConcreteInterpreter::call) pushes +/// a [`CallFrame::root`], so root and nested calls share this one +/// boundary implementation). +pub struct CallFrame { + state: CallState, +} + +enum CallState { + /// Not yet dispatched: resolve the callee and enter its body. + Pending { + resolve_stage: CompileStage, + callee: Callee, + args: Product, + dest: CallDest, + }, + /// Dispatched: the callee frame is running. Holds the callee activation + /// so the boundary frees it exactly once on completion. + Awaiting { + callee_env: EnvIndex, + dest: CallDest, + }, +} + +/// Where a finished call delivers its returned values. +enum CallDest { + /// Write into result slots of the calling activation and resume the + /// caller. + Caller { + env: EnvIndex, + results: Product, + }, + /// A root call: complete the frame stack with the values. + Root, +} + +impl CallFrame +where + V: Clone, +{ + /// A call issued by a statement ([`SparseForwardEffect::Call`](crate::SparseForwardEffect::Call)): + /// returned values land in `call.results` of the caller's activation. + pub fn pending(scope_stage: CompileStage, caller_env: EnvIndex, call: CallEffect) -> Self { + CallFrame { + state: CallState::Pending { + resolve_stage: call.stage.unwrap_or(scope_stage), + callee: call.callee, + args: call.args, + dest: CallDest::Caller { + env: caller_env, + results: call.results, + }, + }, + } + } + + /// A root call (no calling activation): the returned values complete the + /// frame stack. + pub fn root(stage: CompileStage, callee: Callee, args: Product) -> Self { + CallFrame { + state: CallState::Pending { + resolve_stage: stage, + callee, + args, + dest: CallDest::Root, + }, + } + } + + pub fn step_into(self, interp: &mut I) -> Result>, I::Error> + where + I: FrameDriver, + I::Error: From, + F: FrameBuild, + { + match self.state { + CallState::Pending { + resolve_stage, + callee, + args, + dest, + } => { + let target = interp.resolve_call(resolve_stage, &callee)?; + let index = interp.alloc_env(); + let entry = interp.enter_function(target.stage, target.body, args, index)?; + // The closed `Body` enum is the framework's supported body + // vocabulary, so this match is intentionally exhaustive; + // only the `UnGraph` arm delegates to a language policy. + let child = match entry.body { + Body::Cfg(cfg) => { + F::from_cfg(CfgFrame::new(target.stage, index, cfg, entry.args)) + } + Body::Block(block) => { + F::from_block(BlockFrame::new(target.stage, index, block, entry.args)) + } + Body::DiGraph(graph) => { + F::from_digraph(DiGraphFrame::new(target.stage, index, graph, entry.args)) + } + Body::UnGraph(graph) => F::from_ungraph_entry(UnGraphEntry { + stage: target.stage, + index, + graph, + args: entry.args, + })?, + }; + Ok(FrameEffect::Push { + parent: F::from_call(CallFrame { + state: CallState::Awaiting { + callee_env: index, + dest, + }, + }), + child, + }) + } + CallState::Awaiting { .. } => Err(I::Error::from(InterpreterError::Custom( + "call frame stepped while awaiting a return", + ))), + } + } + + pub fn resume_done_into(self) -> Result>, InterpreterError> { + Err(InterpreterError::Custom( + "call frame resumed without a return", + )) + } + + /// The callee completed: validate the completion kind, free the callee + /// activation exactly once, and deliver the returned values. + pub fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, I::Error> + where + I: FrameDriver, + I::Error: From, + F: FrameBuild, + { + let CallState::Awaiting { callee_env, dest } = self.state else { + return Err(I::Error::from(InterpreterError::Custom( + "call frame resumed before dispatch", + ))); + }; + let values = match completion { + // An explicit `Return`, or a graph body's natural completion + // (a callable DiGraph's outputs are the call's returned values). + Completion::Returned(values) | Completion::Finished(values) => values, + Completion::Yielded(_) => { + return Err(I::Error::from(InterpreterError::Custom( + "structured yield reached a function-call boundary (a callable body must exit with return)", + ))); + } + }; + interp.free_env(callee_env)?; + match dest { + CallDest::Caller { env, results } => { + interp.write_results(env, &results, values)?; + Ok(FrameEffect::Done) + } + CallDest::Root => Ok(FrameEffect::Complete(Completion::Returned(values))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs new file mode 100644 index 0000000000..8dc3ca9eba --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs @@ -0,0 +1,140 @@ +use kirin_ir::{Cfg, CompileStage, Product}; + +use crate::{ + EnvIndex, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, +}; + +use super::block_cursor::BlockCursor; +use super::{CallFrame, Completion, FrameBuild}; + +/// Representation walker for a [`Cfg`]: enter the entry block, run +/// statements, follow `Jump` edges between blocks (binding successor +/// arguments and resetting the cursor), and surface the exit through the +/// [`Completion`] protocol. +/// +/// Traversal mechanics only. A `CfgFrame` does not own an activation, is not +/// a call boundary, and does not decide whether a `Return` belongs to a +/// function call — it completes [`Returned`](Completion::Returned) and lets +/// the completion bubble to the nearest [`CallFrame`]. An undecided concrete +/// `Branch` is an error (single-path execution); exploring branch +/// alternatives is the abstract engine's business. +pub struct CfgFrame { + stage: CompileStage, + index: EnvIndex, + cfg: Cfg, + /// Entry arguments awaiting the first step (the entry block is resolved + /// lazily, so construction needs no engine access). + pending: Option>, + /// The active block cursor; `None` until the entry block is resolved. + cursor: Option>, + _marker: std::marker::PhantomData E>, +} + +impl CfgFrame +where + V: Clone, + E: From, +{ + /// Walk `cfg` from its entry block, binding `args` to the entry block's + /// parameters on the first step. Pure construction — needs no engine + /// access. + pub fn new(stage: CompileStage, index: EnvIndex, cfg: Cfg, args: Product) -> Self { + Self { + stage, + index, + cfg, + pending: Some(args), + cursor: None, + _marker: std::marker::PhantomData, + } + } + + /// Execute the next statement and translate its [`SparseForwardEffect`] + /// into a [`FrameEffect`] over the total frame type `F`. + pub fn step_into(mut self, interp: &mut I) -> Result>, E> + where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + { + // First step: find the entry block and bind the entry arguments. + if let Some(args) = self.pending.take() { + let entry = interp + .cfg_entry(self.stage, self.cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCfg))?; + let mut cursor = BlockCursor::new(self.stage, self.index, entry, args); + cursor.bind_entry(interp)?; + self.cursor = Some(cursor); + return Ok(FrameEffect::Continue(F::from_cfg(self))); + } + let cursor = self + .cursor + .as_mut() + .ok_or_else(|| E::from(InterpreterError::Custom("cfg frame stepped before entry")))?; + let Some(statement) = cursor.advance(interp)? else { + return Err(E::from(InterpreterError::BlockFellThrough(cursor.block))); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_cfg(self))), + SparseForwardEffect::Jump(edge) => { + cursor.enter_block(interp, edge.target, &edge.args)?; + Ok(FrameEffect::Continue(F::from_cfg(self))) + } + SparseForwardEffect::Branch(_) => Err(E::from(InterpreterError::IndeterminateBranch)), + SparseForwardEffect::Push { frame, results } => { + cursor.expect_results(results); + Ok(FrameEffect::Push { + parent: F::from_cfg(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.stage, self.index, call); + Ok(FrameEffect::Push { + parent: F::from_cfg(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Yield(values) => { + Ok(FrameEffect::Complete(Completion::Yielded(values))) + } + SparseForwardEffect::Return(values) => { + Ok(FrameEffect::Complete(Completion::Returned(values))) + } + } + } + + /// A child finished without a payload (its results are already in the + /// shared activation, e.g. a returned call): resume at the advanced + /// cursor. + pub fn resume_done_into(self) -> FrameEffect> + where + F: FrameBuild, + { + FrameEffect::Continue(F::from_cfg(self)) + } + + /// A child bubbled a completion: a pushed frame's values land in the + /// push's result slots; a `Returned` keeps bubbling toward the nearest + /// [`CallFrame`] (this frame owns no activation to free). + pub fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> + where + I: FrameDriver, + F: FrameBuild, + { + match completion { + Completion::Finished(values) | Completion::Yielded(values) => { + let cursor = self.cursor.as_mut().ok_or_else(|| { + E::from(InterpreterError::Custom("cfg frame resumed before entry")) + })?; + cursor.write_child_results(interp, values)?; + Ok(FrameEffect::Continue(F::from_cfg(self))) + } + Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs new file mode 100644 index 0000000000..19f4335346 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs @@ -0,0 +1,178 @@ +use kirin_ir::{CompileStage, Product, SSAValue, Statement}; + +use crate::{ + EnvIndex, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, +}; + +use super::{CallFrame, Completion, FrameBuild}; + +/// Representation-specific walker for a [`DiGraph`](kirin_ir::DiGraph) body: bind +/// entry arguments to the graph's boundary ports, run the node statements in +/// topological (dependency) order, and complete +/// [`Finished`](Completion::Finished) with the graph's declared yields. +/// +/// Traversal mechanics only. A `DiGraphFrame` does not own an activation and +/// does not know whether it is a callable graph-function body +/// ([`CallFrame`] → `DiGraphFrame`, where the yields become the call's +/// returned values) or a graph nested inside another body (dialect frame → +/// `DiGraphFrame`, where the yields return to the pushing operation): the +/// parent interprets the completion. +/// +/// The concrete execution policy requires a DAG: directed cycles are +/// rejected when the walk plan is built +/// ([`GraphHasCycle`](InterpreterError::GraphHasCycle)). This is a property +/// of this walker, not of the IR — a `DiGraph` may represent cycles. +/// +/// Pure construction — the walk plan is fetched and the ports are bound on +/// the first `step`, so a dialect frame can build one without engine access +/// (the same lazy pattern as [`BlockFrame`](super::BlockFrame)). CFG control +/// flow (`Jump`/`Branch`) and `Yield`/`Return` are errors inside a graph: a +/// digraph's outputs are its declared yields, not a statement effect. +pub struct DiGraphFrame { + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + /// Entry arguments not yet bound (bound on the first `step`). + pending: Option>, + /// Remaining schedule, in topological order; `None` until the first step. + schedule: Option>, + yields: Vec, + /// Result slots awaiting a pushed child frame's completion values. + resume_slots: Option>, + _marker: std::marker::PhantomData (V, E)>, +} + +impl DiGraphFrame +where + V: Clone, + E: From, +{ + /// Walk `graph`, binding `args` to its boundary ports on the first step. + pub fn new( + stage: CompileStage, + index: EnvIndex, + graph: kirin_ir::DiGraph, + args: Product, + ) -> Self { + Self { + stage, + index, + graph, + pending: Some(args), + schedule: None, + yields: Vec::new(), + resume_slots: None, + _marker: std::marker::PhantomData, + } + } + + /// Execute the next scheduled node and translate its + /// [`SparseForwardEffect`] into a [`FrameEffect`] over the total frame + /// type `F`. + pub fn step_into(mut self, interp: &mut I) -> Result>, E> + where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + { + // First step: fetch the walk plan and bind the boundary ports. + if let Some(args) = self.pending.take() { + let plan = interp.digraph_walk_plan(self.stage, self.graph)?; + if plan.ports.len() != args.len() { + return Err(E::from(InterpreterError::ProductArityMismatch { + expected: plan.ports.len(), + actual: args.len(), + })); + } + for (port, value) in plan.ports.iter().copied().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + self.schedule = Some(plan.schedule.into()); + self.yields = plan.yields; + return Ok(FrameEffect::Continue(F::from_digraph(self))); + } + + let Some(statement) = self.schedule.as_mut().and_then(|s| s.pop_front()) else { + return self.finish::(interp); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_digraph(self))), + SparseForwardEffect::Push { frame, results } => { + self.resume_slots = Some(results); + Ok(FrameEffect::Push { + parent: F::from_digraph(self), + child: frame, + }) + } + SparseForwardEffect::Call(call) => { + let pending = CallFrame::pending(self.stage, self.index, call); + Ok(FrameEffect::Push { + parent: F::from_digraph(self), + child: F::from_call(pending), + }) + } + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CfgControlFlowInStructuredBody)) + } + SparseForwardEffect::Yield(_) => Err(E::from(InterpreterError::Custom( + "yield inside a digraph body (a digraph's outputs are its declared yields)", + ))), + SparseForwardEffect::Return(_) => Err(E::from(InterpreterError::Custom( + "return inside a digraph body", + ))), + } + } + + /// Schedule exhausted: read the declared yields from the activation and + /// complete `Finished` — the graph's natural completion. The parent + /// decides what the values mean (call returns or push results). + fn finish(self, interp: &mut I) -> Result>, E> + where + I: FrameDriver, + F: FrameBuild, + { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(Completion::Finished(values))) + } + + /// A child finished without a payload (e.g. a returned call whose results + /// are already written): resume the schedule. + pub fn resume_done_into(self) -> FrameEffect> + where + F: FrameBuild, + { + FrameEffect::Continue(F::from_digraph(self)) + } + + /// A child bubbled a completion: a pushed frame's values land in the + /// push's result slots. A `Returned` cannot bubble out of a graph node — + /// a digraph has no function-return convention. + pub fn resume_into( + mut self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> + where + I: FrameDriver, + F: FrameBuild, + { + match completion { + Completion::Finished(values) | Completion::Yielded(values) => { + let slots = self.resume_slots.take().ok_or_else(|| { + E::from(InterpreterError::Custom( + "digraph resume without result slots", + )) + })?; + interp.write_results(self.index, &slots, values)?; + Ok(FrameEffect::Continue(F::from_digraph(self))) + } + Completion::Returned(_) => Err(E::from(InterpreterError::Custom( + "return bubbled into a digraph body", + ))), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs b/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs new file mode 100644 index 0000000000..68c2b20e90 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs @@ -0,0 +1,63 @@ +//! The **concrete** implementation of the shared [`frame`](crate::core::frame) +//! protocol. +//! +//! Two independent axes organize these frames: +//! +//! - **Body representation** — the closed [`Body`](crate::Body) vocabulary +//! (`Cfg` / `Block` / `DiGraph` / `UnGraph`), an intentional IR design +//! decision. Each representation the framework can walk has one +//! representation frame implementing *traversal mechanics only*: +//! [`CfgFrame`] (multi-block, follows jumps), [`BlockFrame`] (one linear +//! block), and [`DiGraphFrame`] (dependency-ordered DAG walk). `UnGraph` +//! has **no** default walker — an undirected graph has no inherent +//! execution order, so traversal is a dialect/compiler-supplied policy +//! ([`FrameBuild::from_ungraph_entry`]). +//! +//! - **Entry context** — *why* a body is being walked: as a callable function +//! body (entered through [`CallFrame`], the call boundary that owns the +//! callee activation and return bookkeeping), as a nested structured +//! operation body (entered through a dialect frame such as `kirin-scf`'s +//! `ScfIfFrame`/`ScfForFrame`, pushed with +//! [`SparseForwardEffect::Push`]), or as an analysis owner (the abstract +//! engines' concern, not this module's). +//! +//! Representation frames never know their entry context: the same +//! [`BlockFrame`] walks a callable Block body and an `scf.if` arm; the parent +//! frame ([`CallFrame`] or the dialect frame) interprets the walker's +//! [`Completion`] and owns activation lifetime. Roles compose instead of +//! multiplying frame types: +//! +//! ```text +//! linear function = CallFrame → BlockFrame +//! CFG function = CallFrame → CfgFrame +//! graph function = CallFrame → DiGraphFrame +//! nested scf block = ScfIfFrame → BlockFrame +//! ``` +//! +//! These are the default total frames for +//! [`ConcreteInterpreter`](crate::ConcreteInterpreter) (bundled as +//! [`StandardFrame`]). Structured-control dialects do not get a framework +//! "scope": they push a frame **they own** through +//! [`SparseForwardEffect::Push`] (that frame may build a [`BlockFrame`] to +//! walk a chosen body — a reusable building block, not framework-owned +//! structured semantics). A language that combines such a dialect defines its +//! own total frame enum embedding these frames via [`FrameBuild`] plus its +//! dialect frames. The forward abstract analogue lives in +//! [`sparse_forward::frames`](crate::engines::sparse_forward::frames). +//! +//! [`SparseForwardEffect::Push`]: crate::SparseForwardEffect::Push + +mod block_cursor; +mod block_frame; +mod call_frame; +mod cfg_frame; +mod digraph_frame; +mod protocol; +mod standard_frame; + +pub use block_frame::BlockFrame; +pub use call_frame::CallFrame; +pub use cfg_frame::CfgFrame; +pub use digraph_frame::DiGraphFrame; +pub use protocol::{Completion, FrameBuild, UnGraphEntry}; +pub use standard_frame::StandardFrame; diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs b/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs new file mode 100644 index 0000000000..cec4f7ead6 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs @@ -0,0 +1,87 @@ +use kirin_ir::{CompileStage, Product, UnGraph}; + +use crate::{Body, EnvIndex, InterpreterError}; + +use super::{BlockFrame, CallFrame, CfgFrame, DiGraphFrame}; + +/// Completion payloads produced by the standard concrete frames. +/// +/// Representation walkers report *what happened*; the parent frame decides +/// what it means. The protocol distinguishes the three relevant exits: +/// +/// - [`Returned`](Completion::Returned): an explicit function `Return` was +/// executed. Every frame between the walker and the call boundary relays it +/// unchanged (dialect frames included), so it bubbles to the nearest +/// [`CallFrame`] — the only frame that frees the callee activation and +/// writes the caller's result slots. +/// - [`Yielded`](Completion::Yielded): a structured `Yield` terminated a +/// block body. The structured-operation frame that pushed the block (e.g. +/// `scf.if`/`scf.for`) consumes the carried values. A [`CallFrame`] +/// rejects it: a callable Block or Cfg must exit with `Return`, never a +/// structured yield. +/// - [`Finished`](Completion::Finished): the body ran to its natural end +/// with these values — a digraph's declared output yields, or a dialect +/// frame's finished sub-computation. A frame that pushed the child writes +/// them into the push's result slots; a [`CallFrame`] accepts them as the +/// call's returned values (a callable DiGraph's outputs are its returns). +pub enum Completion { + /// An explicit function `Return` with these values; bubbles to the + /// enclosing [`CallFrame`]. + Returned(Product), + /// A structured `Yield` with these carried values; consumed by the + /// dialect frame that pushed the block body. + Yielded(Product), + /// Natural completion of a body/sub-computation with these values; + /// delivered to whoever entered it (pusher or [`CallFrame`]). + Finished(Product), +} + +/// The entry context handed to a dialect/compiler-supplied callable-UnGraph +/// policy: the callee stage, the callee activation (owned by the awaiting +/// [`CallFrame`], never by the policy frame), the graph, and the entry +/// arguments for its boundary ports. +pub struct UnGraphEntry { + pub stage: CompileStage, + pub index: EnvIndex, + pub graph: UnGraph, + pub args: Product, +} + +/// Construction trait letting any total frame enum embed the standard +/// concrete frames. +/// +/// The default [`StandardFrame`](super::StandardFrame) implements it +/// trivially; a language that adds structured-control dialects implements it +/// on its own enum to reuse the representation walkers and [`CallFrame`] +/// while adding its own dialect frames. +/// +/// [`Body`](crate::Body) is a deliberately closed enum, so [`CallFrame`] +/// matches it exhaustively and maps each representation to its default +/// walker — except `UnGraph`, whose traversal is a policy this trait's +/// [`from_ungraph_entry`](Self::from_ungraph_entry) hook supplies. +pub trait FrameBuild: Sized { + fn from_block(frame: BlockFrame) -> Self; + fn from_cfg(frame: CfgFrame) -> Self; + fn from_call(frame: CallFrame) -> Self; + fn from_digraph(frame: DiGraphFrame) -> Self; + + /// Build the entry frame for a **callable** `UnGraph` body. + /// + /// There is no framework default: an undirected graph has no inherent + /// producer/consumer direction, control-flow successor, or topological + /// execution order — its semantics (graph rewriting, circuits, constraint + /// propagation, …) belong to the dialect/compiler. A language with such + /// semantics overrides this to construct its own policy frame; everyone + /// else inherits this rejection, so total frame enums carry no meaningless + /// UnGraph boilerplate. (Nested, uncallable UnGraph operations don't come + /// through here — a dialect frame enters them via + /// [`SparseForwardEffect::Push`](crate::SparseForwardEffect::Push).) + fn from_ungraph_entry(entry: UnGraphEntry) -> Result + where + E: From, + { + Err(E::from(InterpreterError::NoDefaultWalker(Body::UnGraph( + entry.graph, + )))) + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs new file mode 100644 index 0000000000..11931376a4 --- /dev/null +++ b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs @@ -0,0 +1,69 @@ +use crate::{Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp}; + +use super::{BlockFrame, CallFrame, CfgFrame, Completion, DiGraphFrame, FrameBuild}; + +/// The standard total concrete frame enum: the representation walkers plus +/// the call boundary, no structured-control dialect frames and no +/// callable-UnGraph policy (so a call into an `UnGraph` body reports +/// [`NoDefaultWalker`](InterpreterError::NoDefaultWalker)). +pub enum StandardFrame { + Block(BlockFrame), + Cfg(CfgFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), +} + +impl FrameBuild for StandardFrame { + fn from_block(frame: BlockFrame) -> Self { + StandardFrame::Block(frame) + } + fn from_cfg(frame: CfgFrame) -> Self { + StandardFrame::Cfg(frame) + } + fn from_call(frame: CallFrame) -> Self { + StandardFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + StandardFrame::DiGraph(frame) + } +} + +impl Frame for StandardFrame +where + I: FrameDriver + SparseForwardInterp>, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step(self, interp: &mut I) -> Result, I::Error> { + match self { + StandardFrame::Block(frame) => frame.step_into::(interp), + StandardFrame::Cfg(frame) => frame.step_into::(interp), + StandardFrame::Call(frame) => frame.step_into::(interp), + StandardFrame::DiGraph(frame) => frame.step_into::(interp), + } + } + + fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + match self { + StandardFrame::Block(frame) => Ok(frame.resume_done_into::()), + StandardFrame::Cfg(frame) => Ok(frame.resume_done_into::()), + StandardFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), + StandardFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), + } + } + + fn resume( + self, + completion: Self::Completion, + interp: &mut I, + ) -> Result, I::Error> { + match self { + StandardFrame::Block(frame) => frame.resume_into::(completion, interp), + StandardFrame::Cfg(frame) => frame.resume_into::(completion, interp), + StandardFrame::Call(frame) => frame.resume_into::(completion, interp), + StandardFrame::DiGraph(frame) => frame.resume_into::(completion, interp), + } + } +} diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index 42315bbd94..b9a7624d8a 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -4,8 +4,8 @@ use kirin_ir::{Block, Cfg, CompileStage, Pipeline, Product, SSAValue, StageMeta, use crate::core::query; use crate::{ - Body, BodyFrame, CallableBody, Callee, Completion, Env, EnvIndex, EnvStackStore, ForwardEval, - Frame, FrameBuild, FrameDriver, FunctionTarget, Interp, InterpDispatch, InterpLocation, + CallFrame, CallableBody, Callee, Completion, Env, EnvIndex, EnvStackStore, ForwardEval, Frame, + FrameBuild, FrameDriver, FunctionTarget, Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, SameStageLinker, SparseForwardEffect, StageQuery, StandardFrame, Store, drive_frames, }; @@ -231,44 +231,20 @@ where } /// Execute a function to completion and return its return product. + /// + /// The root call is an ordinary [`CallFrame`]: the same call boundary + /// that nested `Call` effects go through owns callee resolution, the + /// callee activation, body-kind selection, and completion validation — + /// there is exactly one implementation of that behavior. pub fn call( &mut self, stage: CompileStage, callee: Callee, args: impl IntoIterator, ) -> Result, E> { - let target = self.resolve_call(stage, &callee)?; - let index = self.alloc_env(); let args: Product = args.into_iter().collect(); - let body = self.enter_function(target.stage, target.body, args, index)?; - let frame = match body.body { - Body::Cfg(cfg) => F::from_body(BodyFrame::function( - self, - target.stage, - index, - cfg, - body.args, - )?), - Body::Block(block) => F::from_body(BodyFrame::linear_function( - self, - target.stage, - index, - block, - body.args, - )?), - Body::DiGraph(graph) => { - F::from_digraph(crate::engines::concrete::DiGraphFrame::function( - target.stage, - index, - graph, - body.args, - )) - } - other @ Body::UnGraph(_) => { - return Err(E::from(InterpreterError::NoDefaultWalker(other))); - } - }; - self.frames.push(frame); + self.frames + .push(F::from_call(CallFrame::root(stage, callee, args))); self.run() } @@ -281,9 +257,9 @@ where self.frames = frames; match completion? { Completion::Returned(values) => Ok(values), - Completion::Finished(_) => Err(E::from(InterpreterError::Custom( - "body completion reached the frame-stack root", - ))), + Completion::Yielded(_) | Completion::Finished(_) => Err(E::from( + InterpreterError::Custom("body completion reached the frame-stack root"), + )), } } } diff --git a/crates/kirin-interpreter/src/engines/concrete/mod.rs b/crates/kirin-interpreter/src/engines/concrete/mod.rs index ea1a276d8c..bb77d8cdc4 100644 --- a/crates/kirin-interpreter/src/engines/concrete/mod.rs +++ b/crates/kirin-interpreter/src/engines/concrete/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod frames; pub(crate) mod interp; pub use frames::{ - BlockMode, BodyFrame, CallFrame, Completion, DiGraphFrame, FrameBuild, StandardFrame, + BlockFrame, CallFrame, CfgFrame, Completion, DiGraphFrame, FrameBuild, StandardFrame, + UnGraphEntry, }; pub use interp::ConcreteInterpreter; diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs index d14c120c42..2283e255c3 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs @@ -75,7 +75,7 @@ pub struct AbstractBlockFrame { cursor: Option, mode: BlockMode, /// Entry arguments not yet bound — bound on the first step, so building the - /// frame needs no engine access (see [`BodyFrame`](crate::BodyFrame)). + /// frame needs no engine access (see [`BlockFrame`](crate::BlockFrame)). pending: Option>, resume_slots: Option>, _marker: PhantomData (E, K)>, diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 4b146f030d..8820e7c0be 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -84,10 +84,13 @@ pub use self::core::{ pub use self::core::ForwardDataflowFrameDriver as AbstractFrameDriver; pub use self::core::ForwardFrameDriver as FrameDriver; -// Concrete execution engine + the concrete standard frames. +// Concrete execution engine + the concrete standard frames: the +// representation walkers (`BlockFrame`/`CfgFrame`/`DiGraphFrame` — `UnGraph` +// traversal is a dialect/compiler policy supplied through +// `FrameBuild::from_ungraph_entry`) and the `CallFrame` call boundary. pub use engines::concrete::{ - BlockMode, BodyFrame, CallFrame, Completion, ConcreteInterpreter, DiGraphFrame, FrameBuild, - StandardFrame, + BlockFrame, CallFrame, CfgFrame, Completion, ConcreteInterpreter, DiGraphFrame, FrameBuild, + StandardFrame, UnGraphEntry, }; // Sparse forward engine (`Sem = ForwardEval`) + the abstract standard frames. pub use engines::sparse_forward::{ @@ -161,14 +164,14 @@ pub mod dialect { pub mod engine { pub use crate::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, AbstractInterpreter, BodyFrame, CallContext, CallFrame, Callee, - Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, + AbstractFrameDriver, AbstractInterpreter, BlockFrame, CallContext, CallFrame, Callee, + CfgFrame, Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, DenseBackwardInterpreter, DenseBlockFrame, DenseFrameBuild, DiGraphFrame, Env, ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, FrameDriver, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, InterpreterError, Linker, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, SparseForwardInterp, SparseForwardInterpreter, StandardAbstractFrame, StandardDenseBackwardFrame, StandardFrame, - WideningStrategy, drive_frames, expect_single, + UnGraphEntry, WideningStrategy, drive_frames, expect_single, }; } diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index e9fc9994bb..779f660238 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -10,10 +10,13 @@ //! both arms and joins their results (abstract). //! - `scf.for` -> [`ScfForFrame`] / [`AbstractScfForFrame`], via [`ScfForDispatch`]. //! -//! Both reuse the framework's generic [`BodyFrame`]/[`AbstractBlockFrame`] to +//! Both reuse the framework's generic [`BlockFrame`]/[`AbstractBlockFrame`] to //! *walk* a chosen body block — those are reusable building blocks, not //! framework-owned structured semantics — but the structured *decision* and -//! result binding are owned by the SCF frame. A language that uses `scf` +//! result binding are owned by the SCF frame: the block walker surfaces a +//! structured `Yield` as [`Completion::Yielded`], which the SCF frame consumes, +//! while a function `Return` ([`Completion::Returned`]) is relayed unchanged so +//! it bubbles to the nearest `CallFrame`. A language that uses `scf` //! composes a total frame type embedding these via [`BuildScfIf`]/[`BuildScfFor`] //! (and the abstract equivalents [`BuildAbstractScfIf`]/[`BuildAbstractScfFor`]). @@ -28,7 +31,7 @@ use kirin_interpreter::dialect::{ StrongDemand, }; use kirin_interpreter::{ - AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BodyFrame, + AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BlockFrame, CallContext, Completion, ConcreteInterpreter, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBlockFrame, DenseFrameBuild, EnvIndex, FrameBuild, FrameDriver, FrameEffect, PointFacts, SparseForwardTransfer, @@ -642,10 +645,12 @@ where // Concrete if frame: pick the decided arm, relay its completion. // =========================================================================== -/// Concrete `scf.if` traversal: push the framework [`BodyFrame`] for the decided -/// arm and relay its completion to the pusher. The structured *decision* (which -/// arm) is owned here; an undecided condition is impossible under concrete -/// execution (`IndeterminateBranch`). +/// Concrete `scf.if` traversal: push the framework [`BlockFrame`] for the +/// decided arm, consume the arm's structured `Yield`, and hand the yielded +/// values to the pusher. The structured *decision* (which arm) is owned here; +/// an undecided condition is impossible under concrete execution +/// (`IndeterminateBranch`). A function `Return` inside the arm is relayed +/// unchanged so it bubbles to the nearest `CallFrame`. pub struct ScfIfFrame { stage: CompileStage, env: EnvIndex, @@ -687,10 +692,10 @@ where Some(false) => self.else_body, None => return Err(E::from(InterpreterError::IndeterminateBranch)), }; - let body = BodyFrame::block(self.stage, self.env, arm, Product::new()); + let body = BlockFrame::new(self.stage, self.env, arm, Product::new()); Ok(FrameEffect::Push { parent: F::scf_if(self), - child: F::from_body(body), + child: F::from_block(body), }) } @@ -704,8 +709,16 @@ where self, completion: Completion, ) -> Result>, E> { - // Relay the chosen arm's completion (yield-finish or function return). - Ok(FrameEffect::Complete(completion)) + match completion { + // The arm's structured yield: its values are this operation's + // results, delivered to the pusher as a finished sub-computation. + Completion::Yielded(values) => Ok(FrameEffect::Complete(Completion::Finished(values))), + // A `ret` inside the arm: relay it toward the nearest `CallFrame`. + Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + Completion::Finished(_) => Err(E::from(InterpreterError::Custom( + "scf.if arm completed without a structured yield", + ))), + } } } @@ -875,10 +888,10 @@ where let args: Product = std::iter::once(self.induction.clone()) .chain(self.carried.iter().cloned()) .collect(); - let body = BodyFrame::block(self.stage, self.env, self.body, args); + let body = BlockFrame::new(self.stage, self.env, self.body, args); Ok(FrameEffect::Push { parent: F::scf_for(self), - child: F::from_body(body), + child: F::from_block(body), }) } Some(false) => Ok(FrameEffect::Complete(Completion::Finished(self.carried))), @@ -902,8 +915,9 @@ where F: FrameBuild + BuildScfFor, { match completion { - // The body yielded: advance the induction variable and re-check. - Completion::Finished(yielded) => { + // The body's structured yield: advance the induction variable, + // carry the yielded values forward, and re-check the condition. + Completion::Yielded(yielded) => { let step = interp.env_read(self.env, self.step)?; let next = self .induction @@ -913,8 +927,11 @@ where self.carried = yielded; Ok(FrameEffect::Continue(F::scf_for(self))) } - // A `ret` inside the body returns from the enclosing function. + // A `ret` inside the body: relay it toward the nearest `CallFrame`. Completion::Returned(values) => Ok(FrameEffect::Complete(Completion::Returned(values))), + Completion::Finished(_) => Err(E::from(InterpreterError::Custom( + "scf.for body completed without a structured yield", + ))), } } } diff --git a/crates/kirin-test-languages/src/graph_function_language.rs b/crates/kirin-test-languages/src/graph_function_language.rs index ffb8896f02..6000fc5435 100644 --- a/crates/kirin-test-languages/src/graph_function_language.rs +++ b/crates/kirin-test-languages/src/graph_function_language.rs @@ -8,7 +8,9 @@ use kirin_arith::{Arith, ArithType, ArithValue}; use kirin_cf::ControlFlow; use kirin_constant::Constant; use kirin_function::{Call, Return}; -use kirin_ir::{Block, Cfg, DiGraph, Dialect, Placeholder as _, ResultValue, SSAValue, Signature}; +use kirin_ir::{ + Block, Cfg, DiGraph, Dialect, Placeholder as _, ResultValue, SSAValue, Signature, UnGraph, +}; #[derive(Debug, Clone, PartialEq, Dialect)] #[cfg_attr(feature = "parser", derive(kirin_chumsky::HasParser))] @@ -44,6 +46,18 @@ pub enum GraphFunctionLanguage { body: Block, sig: Signature, }, + /// UnGraph-bodied callable. The framework has no default walker for an + /// undirected graph body: calling one requires the compiler to supply a + /// traversal policy (`FrameBuild::from_ungraph_entry`), otherwise the + /// engine reports `NoDefaultWalker`. + #[cfg_attr( + any(feature = "parser", feature = "pretty"), + chumsky(format = "fn {:name}{sig} {body}") + )] + UnGraphFunction { + body: UnGraph, + sig: Signature, + }, /// Inline graph evaluation: enters its owned digraph via a pushed frame. #[cfg_attr( any(feature = "parser", feature = "pretty"), @@ -99,7 +113,8 @@ mod interpreter { match self { GraphFunctionLanguage::Function { .. } | GraphFunctionLanguage::GraphFunction { .. } - | GraphFunctionLanguage::LinearFunction { .. } => Ok(SparseForwardEffect::Next), + | GraphFunctionLanguage::LinearFunction { .. } + | GraphFunctionLanguage::UnGraphFunction { .. } => Ok(SparseForwardEffect::Next), GraphFunctionLanguage::GraphEval { lhs, rhs, @@ -109,7 +124,10 @@ mod interpreter { let args: Product = [interp.read(*lhs)?, interp.read(*rhs)?] .into_iter() .collect(); - let frame = DiGraphFrame::nested(interp.stage(), interp.index(), *graph, args); + // A nested (uncallable) graph body: no function activation, + // no callee resolution — the operation pushes the walker + // directly into the current activation. + let frame = DiGraphFrame::new(interp.stage(), interp.index(), *graph, args); Ok(SparseForwardEffect::Push { frame: I::Frame::from_digraph(frame), results: [SSAValue::from(*result)].into_iter().collect(), @@ -140,6 +158,9 @@ mod interpreter { GraphFunctionLanguage::LinearFunction { body, .. } => { Ok(CallableBody::new(*body).args(args)) } + GraphFunctionLanguage::UnGraphFunction { body, .. } => { + Ok(CallableBody::new(*body).args(args)) + } _ => Err(I::Error::from(InterpreterError::NotCallable( interp.statement(), ))), diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 7336cf6809..ac46909d10 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -199,7 +199,9 @@ SCF has two such operations: reads the condition value and hands the `Option` decision to the frame; the **frame** picks the arm (concrete; undecided is `IndeterminateBranch`) or explores both arms and **joins** their finish results (abstract). It walks each - arm by pushing the framework `BodyFrame`/`AbstractBlockFrame` building block. + arm by pushing the framework `BlockFrame`/`AbstractBlockFrame` building block, + consumes the arm's `Completion::Yielded` values, and relays a bubbled + `Completion::Returned` unchanged toward the nearest `CallFrame`. - **`scf.for`** → `ScfForFrame` / `AbstractScfForFrame`, built via `ScfForDispatch`. The frame pushes a body frame each iteration, advances the @@ -209,10 +211,11 @@ SCF has two such operations: accumulating finish values across exits — so `scf.for` over a lattice converges, with no framework "scope hook". -The framework `BodyFrame`/`AbstractBlockFrame` (single-block body walkers, -completing on `Yield`) are reusable **building blocks**, not framework-owned -structured semantics: the SCF frames build them to walk a chosen body, but the -structured *decision* and result binding stay in the SCF frame. A language that +The framework `BlockFrame`/`AbstractBlockFrame` (single-block body walkers, +surfacing `Yield` to their parent) are reusable **building blocks**, not +framework-owned structured semantics: the SCF frames build them to walk a +chosen body, but the structured *decision* and result binding stay in the SCF +frame. 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 @@ -294,13 +297,40 @@ belongs to. A generic **frame-stack driver**: it pops the top frame, calls `Frame::step`, and applies the returned `FrameEffect` (`Continue` / `Push` / `Done` / `Complete`) — it owns *no* traversal logic itself. Traversal lives in the -frames. The default total frame type `StandardFrame` wraps the standard -`BodyFrame` (walks a function-body CFG, or a single body block that -completes on `Yield` — `Jump` retargets it, `Return` completes it) and -`CallFrame` (dispatch a callee, await its `Return`). The dialect-produced -`SparseForwardEffect` is consumed by `BodyFrame`, which maps it to a `FrameEffect` -(handling `Push` by pushing the carried frame). `StandardFrame` is -structured-control-free; a custom `F` +frames, organized along two independent axes: + +- **Body representation** (the closed `Body` vocabulary — an intentional IR + design decision): each framework-walkable representation has one + *representation walker* owning traversal mechanics only — `CfgFrame` + (multi-block, follows `Jump`, rejects an undecided `Branch`), `BlockFrame` + (one linear block; `Jump`/`Branch` are errors), and `DiGraphFrame` + (dependency-ordered DAG walk collecting the declared yields). `UnGraph` has + **no default walker**: an undirected graph has no inherent execution order, + so callable-UnGraph traversal is a dialect/compiler-supplied policy + (`FrameBuild::from_ungraph_entry`, defaulting to `NoDefaultWalker`). +- **Entry context**: the same walker serves a *callable* body (entered + through `CallFrame`) and a *nested structured-operation* body (entered + through a dialect frame); analysis owners are the abstract engines' third + context. Walkers never know their role — they surface exits through the + completion protocol (`Completion::Returned` for a function `Return`, + `Completion::Yielded` for a structured `Yield`, `Completion::Finished` for + natural completion such as a digraph's output yields) and the parent frame + decides what each means. + +`CallFrame` is the **call boundary**: it resolves the callee, allocates the +callee activation, selects the entry walker for the closed `Body` variant, +validates the completion kind (`Returned`, or a graph's natural `Finished`; +a structured `Yielded` is an error), frees the callee activation exactly +once, and delivers the values — into the caller's result slots, or as the +run's result for a root call (`ConcreteInterpreter::call` pushes a +`CallFrame::root`, so root and nested calls share one boundary +implementation). Representation walkers never free activations; a `Returned` +bubbles through dialect frames to the nearest `CallFrame`. + +The default total frame type `StandardFrame` bundles the three walkers +plus `CallFrame`. The dialect-produced `SparseForwardEffect` is consumed by +the walkers, which map it to a `FrameEffect` (handling `Push` by pushing the +carried frame). `StandardFrame` is structured-control-free; a custom `F` ([Custom traversal and policies](#custom-traversal-and-policies)) adds dialect frames or replaces traversal without touching the engine. @@ -426,13 +456,17 @@ it**. The concrete and abstract standard frames are two *implementations* of this one protocol — not parallel frameworks. -### Concrete frames — `BodyFrame` / `CallFrame` / `StandardFrame` +### Concrete frames — `BlockFrame` / `CfgFrame` / `DiGraphFrame` / `CallFrame` / `StandardFrame` `ConcreteInterpreter` is generic over the total frame type `F` (default -`StandardFrame`). A custom enum reuses the standard `BodyFrame`/`CallFrame` -single-path traversal through `FrameBuild` (`from_body`/`from_call`) and their -`*_into` delegating methods, adds dialect frames / observation, and instantiates -the engine with that `F`. (Examples: `example/toy-lang`'s `ToyFrame`, which adds +`StandardFrame`). A custom enum reuses the standard single-path traversal — +the representation walkers and the call boundary — through `FrameBuild` +(`from_block`/`from_cfg`/`from_call`/`from_digraph`) and their `*_into` +delegating methods, adds dialect frames / observation, and instantiates the +engine with that `F`. Overriding `FrameBuild::from_ungraph_entry` (default: +`NoDefaultWalker`) supplies a callable-UnGraph traversal policy without +touching the generic logic (see the workspace `tests/body_kinds.rs` policy +test). (Further examples: `example/toy-lang`'s `ToyFrame`, which adds `kirin_scf`'s `ScfIfFrame`/`ScfForFrame` via `BuildScfIf`/`BuildScfFor`; and a `TracingFrame` counting call/body visitation while running the real program — see `example/toy-lang`'s `interpreter::tests::advanced`.) diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 276b491e5a..9931a1b0da 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -3,16 +3,18 @@ //! The toy language uses `kirin-scf`, whose `scf.for` pushes a dialect-owned //! loop frame ([`ScfForFrame`]/[`AbstractScfForFrame`]). A language that uses //! such a dialect composes its own total frame enum embedding the standard -//! framework frames (via [`FrameBuild`]/[`AbstractFrameBuild`]) plus the -//! dialect frames (via [`BuildScfFor`]/[`BuildAbstractScfFor`]). The engine is +//! framework frames — the representation walkers +//! ([`BlockFrame`]/[`CfgFrame`]/[`DiGraphFrame`]) and the [`CallFrame`] call +//! boundary, via [`FrameBuild`]/[`AbstractFrameBuild`] — plus the dialect +//! frames (via [`BuildScfFor`]/[`BuildAbstractScfFor`]). The engine is //! not forked — only the engine's `F` type parameter changes. use std::hash::Hash; use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BodyFrame, CallFrame, Completion, DiGraphFrame, Frame, FrameBuild, - FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp, + AbstractFrameDriver, BlockFrame, CallFrame, CfgFrame, Completion, DiGraphFrame, Frame, + FrameBuild, FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, BuildScfFor, @@ -23,9 +25,11 @@ use kirin_scf::{ // Concrete // =========================================================================== -/// Concrete total frame: standard body/call traversal plus the SCF if/for frames. +/// Concrete total frame: the standard representation walkers and call +/// boundary plus the SCF if/for frames. pub enum ToyFrame { - Body(BodyFrame), + Block(BlockFrame), + Cfg(CfgFrame), Call(CallFrame), DiGraph(DiGraphFrame), ScfIf(ScfIfFrame), @@ -33,8 +37,11 @@ pub enum ToyFrame { } impl FrameBuild for ToyFrame { - fn from_body(frame: BodyFrame) -> Self { - ToyFrame::Body(frame) + fn from_block(frame: BlockFrame) -> Self { + ToyFrame::Block(frame) + } + fn from_cfg(frame: CfgFrame) -> Self { + ToyFrame::Cfg(frame) } fn from_call(frame: CallFrame) -> Self { ToyFrame::Call(frame) @@ -66,7 +73,8 @@ where fn step(self, interp: &mut I) -> Result, I::Error> { match self { - ToyFrame::Body(frame) => frame.step_into::(interp), + ToyFrame::Block(frame) => frame.step_into::(interp), + ToyFrame::Cfg(frame) => frame.step_into::(interp), ToyFrame::Call(frame) => frame.step_into::(interp), ToyFrame::DiGraph(frame) => frame.step_into::(interp), ToyFrame::ScfIf(frame) => frame.step_into::(interp), @@ -76,7 +84,8 @@ where fn resume_done(self, _interp: &mut I) -> Result, I::Error> { match self { - ToyFrame::Body(frame) => Ok(frame.resume_done_into::()), + ToyFrame::Block(frame) => Ok(frame.resume_done_into::()), + ToyFrame::Cfg(frame) => Ok(frame.resume_done_into::()), ToyFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), ToyFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), ToyFrame::ScfIf(frame) => frame.resume_done_into::(), @@ -90,7 +99,8 @@ where interp: &mut I, ) -> Result, I::Error> { match self { - ToyFrame::Body(frame) => frame.resume_into::(completion, interp), + ToyFrame::Block(frame) => frame.resume_into::(completion, interp), + ToyFrame::Cfg(frame) => frame.resume_into::(completion, interp), ToyFrame::Call(frame) => frame.resume_into::(completion, interp), ToyFrame::DiGraph(frame) => frame.resume_into::(completion, interp), ToyFrame::ScfIf(frame) => frame.resume_into::(completion), diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 7057924243..2fae32dc9f 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -566,9 +566,10 @@ mod advanced { use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BodyFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, - CrossStageLinker, DiGraphFrame, Frame, FrameBuild, FrameDriver, FrameEffect, - InterpreterError, SparseForwardInterp, SparseForwardInterpreter, expect_single, + AbstractFrameDriver, BlockFrame, CallContext, CallFrame, CfgFrame, Completion, + ConcreteInterpreter, CrossStageLinker, DiGraphFrame, Frame, FrameBuild, FrameDriver, + FrameEffect, InterpreterError, SparseForwardInterp, SparseForwardInterpreter, + expect_single, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, @@ -581,8 +582,9 @@ mod advanced { // --- A custom total frame enum ----------------------------------------- // - // It reuses the standard `BodyFrame`/`CallFrame` traversal (and the SCF loop - // frame) verbatim via `FrameBuild`/`BuildScfFor` + the delegating `*_into` + // It reuses the standard representation walkers (`BlockFrame`/`CfgFrame`/ + // `DiGraphFrame`), the `CallFrame` call boundary, and the SCF frames + // verbatim via `FrameBuild`/`BuildScfFor` + the delegating `*_into` // methods, and adds *observation*: every call and every body step is counted // in a side log. The engine is not forked — only `ConcreteInterpreter`'s `F` // type parameter changes. @@ -598,7 +600,8 @@ mod advanced { } enum TracingFrame { - Body(BodyFrame), + Block(BlockFrame), + Cfg(CfgFrame), Call(CallFrame), DiGraph(DiGraphFrame), ScfIf(ScfIfFrame), @@ -606,8 +609,11 @@ mod advanced { } impl FrameBuild for TracingFrame { - fn from_body(frame: BodyFrame) -> Self { - TracingFrame::Body(frame) + fn from_block(frame: BlockFrame) -> Self { + TracingFrame::Block(frame) + } + fn from_cfg(frame: CfgFrame) -> Self { + TracingFrame::Cfg(frame) } fn from_call(frame: CallFrame) -> Self { TracingFrame::Call(frame) @@ -639,7 +645,11 @@ mod advanced { fn step(self, interp: &mut I) -> Result, I::Error> { match self { - TracingFrame::Body(frame) => { + TracingFrame::Block(frame) => { + TRACE.with(|t| t.borrow_mut().body_steps += 1); + frame.step_into::(interp) + } + TracingFrame::Cfg(frame) => { TRACE.with(|t| t.borrow_mut().body_steps += 1); frame.step_into::(interp) } @@ -658,7 +668,8 @@ mod advanced { _interp: &mut I, ) -> Result, I::Error> { match self { - TracingFrame::Body(frame) => Ok(frame.resume_done_into::()), + TracingFrame::Block(frame) => Ok(frame.resume_done_into::()), + TracingFrame::Cfg(frame) => Ok(frame.resume_done_into::()), TracingFrame::Call(frame) => { frame.resume_done_into::().map_err(I::Error::from) } @@ -674,7 +685,8 @@ mod advanced { interp: &mut I, ) -> Result, I::Error> { match self { - TracingFrame::Body(frame) => frame.resume_into::(completion, interp), + TracingFrame::Block(frame) => frame.resume_into::(completion, interp), + TracingFrame::Cfg(frame) => frame.resume_into::(completion, interp), TracingFrame::Call(frame) => frame.resume_into::(completion, interp), TracingFrame::DiGraph(frame) => frame.resume_into::(completion, interp), TracingFrame::ScfIf(frame) => frame.resume_into::(completion), @@ -706,15 +718,16 @@ mod advanced { .unwrap(); // (1)+(2): the custom frame ran the real program correctly by reusing - // the standard BodyFrame/CallFrame traversal (no engine fork). + // the standard walker/CallFrame traversal (no engine fork). assert_eq!(result, 120); // (3): traversal is observable through the custom frame. factorial(5) - // makes 4 recursive calls (5→4→3→2→1; the base case at 1 makes none), - // all routed through the custom Call arm; body statements run through - // its Body arm. + // is 5 activations: the root call plus 4 recursive calls (5→4→3→2→1; + // the base case at 1 makes none) — every call, root included, is one + // `CallFrame` routed through the custom Call arm; body statements run + // through its Block/Cfg arms. let trace = TRACE.with(|t| *t.borrow()); - assert_eq!(trace.calls, 4); + assert_eq!(trace.calls, 5); assert!(trace.body_steps > 0); } diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 0b1e9d332a..2a74ef4bb0 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -1,16 +1,42 @@ -//! Acceptance tests for generic interpreter bodies (issue #667): a mixed -//! language where regular Cfg-SSA code and DiGraph computational graphs -//! call into each other, plus linear (Block-bodied) callables. +//! Acceptance tests for generic interpreter bodies (issue #667). +//! +//! Two independent axes organize concrete traversal: +//! +//! - **Body representation** — the closed `Body` vocabulary: `Cfg`, `Block`, +//! `DiGraph`, `UnGraph`. Each framework-walkable representation has one +//! walker (`CfgFrame`/`BlockFrame`/`DiGraphFrame`); `UnGraph` traversal is +//! a compiler-supplied policy (`FrameBuild::from_ungraph_entry`). +//! - **Entry context** — callable (entered through `CallFrame`, which owns +//! the callee activation) vs. nested (entered through a dialect frame +//! pushed with `SparseForwardEffect::Push`, borrowing the current +//! activation). +//! +//! The tests cover the composition matrix: callable Cfg/Block/DiGraph bodies +//! (`CallFrame` → walker), nested DiGraph and scf Blocks (dialect frame → +//! walker), returns bubbling through dialect frames to the nearest +//! `CallFrame`, and the callable-UnGraph policy hook (with and without a +//! policy). + +use std::collections::VecDeque; use kirin::prelude::*; -use kirin_arith::{ArithConversionError, interpreter::DivisionByZero}; +use kirin_arith::{ + Arith, ArithConversionError, ArithType, ArithValue, interpreter::DivisionByZero, +}; +use kirin_cmp::Cmp; +use kirin_constant::Constant; +use kirin_function::Lexical; use kirin_interpreter::{ - ConcreteInterpreter, InterpreterError, SameStageLinker, StandardFrame, expect_single, + BlockFrame, Body, CallFrame, CfgFrame, Completion, ConcreteInterpreter, DiGraphFrame, Env, + EnvIndex, Frame, FrameBuild, FrameDriver, FrameEffect, FunctionEntry, Interpretable, + InterpreterError, SameStageLinker, SparseForwardEffect, StandardFrame, UnGraphEntry, + expect_single, }; +use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; -/// Total error for the test engine: the framework error plus the value -/// conversion/trap errors the mixed language's rules can raise. +/// Total error for the test engines: the framework error plus the value +/// conversion/trap errors the languages' rules can raise. #[derive(Debug)] enum TestError { Core(InterpreterError), @@ -49,9 +75,17 @@ fn run(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result i64 { assert_eq!(run(&pipeline, "main", &[]).unwrap(), 5); } -/// Entry path 2 (the dialect-frame path): a statement inside a Cfg block -/// owns a DiGraph body and enters it with `Push` — the same way `scf.if` -/// enters its Block arms. +// =========================================================================== +// 2. Nested/pushed DiGraph: dialect operation → DiGraphFrame. +// =========================================================================== + +/// A statement inside a Cfg block owns a DiGraph body and enters it with +/// `SparseForwardEffect::Push` — the same way `scf.if` enters its Block +/// arms. Unlike test 1 there is **no** `CallFrame` in the chain: no callee +/// resolution happens, no function activation is allocated or freed — the +/// pushed `DiGraphFrame` runs in the *pusher's* activation, and its +/// `Finished` yields land in the pushing statement's `Push` result slots +/// rather than in call-return slots. #[test] fn cfg_statement_pushes_digraph_body() { let pipeline = parse( @@ -102,8 +144,16 @@ specialize @test fn @main() -> i64 { assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); } +// =========================================================================== +// 3. Callable Block: caller → CallFrame → BlockFrame. +// =========================================================================== + /// A linear (single-Block) callable: the flat-instruction-list function -/// shape (QOS-style compile targets). Exits with `Return`. +/// shape. `Body::Block` maps to the same +/// `BlockFrame` that walks nested structured blocks — there is no separate +/// "linear function frame"; the `CallFrame` parent is what makes this walk a +/// function body. The block exits with `Return`, which the `CallFrame` +/// validates and consumes. #[test] fn linear_block_callable() { let pipeline = parse( @@ -129,8 +179,20 @@ specialize @test fn @main() -> i64 { assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); } -/// Graph nodes run in dependency order, not declaration order: the yield -/// depends on a node declared after its operand producer. +// =========================================================================== +// 4. DiGraph dependency order. +// =========================================================================== + +/// Graph nodes run in dependency order, not declaration order. The graph +/// branches visibly: one input port feeds two independent producers declared +/// *after* their consumer, whose results merge into the yielded node — so a +/// textual/linear walk would read unbound operands. +/// +/// ```text +/// %x ──┬─▶ %c = add %x, %x ──┐ +/// │ ├─▶ %d = mul %c, %e ──▶ yield +/// └─▶ %e = add %x, %one ┘ +/// ``` #[test] fn digraph_runs_in_topological_order() { let pipeline = parse( @@ -139,8 +201,10 @@ stage @test fn @g(i64) -> i64; stage @test fn @main() -> i64; specialize @test fn @g(i64) -> i64 digraph ^g0(%x: i64) { - %d = mul %c, %c -> i64; + %d = mul %c, %e -> i64; %c = add %x, %x -> i64; + %e = add %x, %one -> i64; + %one = constant 1 -> i64; yield %d; } @@ -153,6 +217,489 @@ specialize @test fn @main() -> i64 { } "#, ); - // (3 + 3)^2 = 36 — requires running `add` before `mul` despite text order. - assert_eq!(run(&pipeline, "main", &[]).unwrap(), 36); + // (3 + 3) * (3 + 1) = 24 — requires running both `add`s (and the + // constant) before the `mul` despite the textual order. + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 24); +} + +// =========================================================================== +// An scf-composed language for tests 5 and 6: structured operations enter +// nested Blocks through dialect frames (ScfIfFrame/ScfForFrame → BlockFrame). +// =========================================================================== + +/// Inline language wrapping functions (Cfg bodies), scf, and arithmetic. +/// Specific to this integration suite; shared test dialects live in +/// `kirin-test-languages`. +#[derive( + Debug, Clone, PartialEq, Eq, Hash, Dialect, FunctionEntry, HasParser, PrettyPrint, Interpretable, +)] +#[kirin(builders, type = ArithType)] +enum ScfLanguage { + #[wraps] + #[callable] + Lexical(Lexical), + #[wraps] + Structured(StructuredControlFlow), + #[wraps] + Constant(Constant), + #[wraps] + Arith(Arith), + #[wraps] + Cmp(Cmp), +} + +/// Total frame enum for the scf tests: the standard representation walkers +/// and call boundary plus the dialect-owned SCF frames (composition, not an +/// engine fork). +enum ScfTestFrame { + Block(BlockFrame), + Cfg(CfgFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + ScfIf(ScfIfFrame), + ScfFor(ScfForFrame), +} + +impl FrameBuild for ScfTestFrame { + fn from_block(frame: BlockFrame) -> Self { + ScfTestFrame::Block(frame) + } + fn from_cfg(frame: CfgFrame) -> Self { + ScfTestFrame::Cfg(frame) + } + fn from_call(frame: CallFrame) -> Self { + ScfTestFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + ScfTestFrame::DiGraph(frame) + } +} + +impl BuildScfIf for ScfTestFrame { + fn scf_if(frame: ScfIfFrame) -> Self { + ScfTestFrame::ScfIf(frame) + } +} + +impl BuildScfFor for ScfTestFrame { + fn scf_for(frame: ScfForFrame) -> Self { + ScfTestFrame::ScfFor(frame) + } +} + +impl Frame for ScfTestFrame +where + I: FrameDriver + + kirin_interpreter::SparseForwardInterp>, + V: Clone + kirin_scf::ForLoopValue, + E: From, +{ + type Completion = Completion; + + fn step(self, interp: &mut I) -> Result, I::Error> { + match self { + ScfTestFrame::Block(frame) => frame.step_into::(interp), + ScfTestFrame::Cfg(frame) => frame.step_into::(interp), + ScfTestFrame::Call(frame) => frame.step_into::(interp), + ScfTestFrame::DiGraph(frame) => frame.step_into::(interp), + ScfTestFrame::ScfIf(frame) => frame.step_into::(interp), + ScfTestFrame::ScfFor(frame) => frame.step_into::(interp), + } + } + + fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + match self { + ScfTestFrame::Block(frame) => Ok(frame.resume_done_into::()), + ScfTestFrame::Cfg(frame) => Ok(frame.resume_done_into::()), + ScfTestFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), + ScfTestFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), + ScfTestFrame::ScfIf(frame) => frame.resume_done_into::(), + ScfTestFrame::ScfFor(frame) => frame.resume_done_into::(), + } + } + + fn resume( + self, + completion: Self::Completion, + interp: &mut I, + ) -> Result, I::Error> { + match self { + ScfTestFrame::Block(frame) => frame.resume_into::(completion, interp), + ScfTestFrame::Cfg(frame) => frame.resume_into::(completion, interp), + ScfTestFrame::Call(frame) => frame.resume_into::(completion, interp), + ScfTestFrame::DiGraph(frame) => frame.resume_into::(completion, interp), + ScfTestFrame::ScfIf(frame) => frame.resume_into::(completion), + ScfTestFrame::ScfFor(frame) => frame.resume_into::(completion, interp), + } + } +} + +type ScfL = StageInfo; +type ScfEngine<'ir> = + ConcreteInterpreter<'ir, ScfL, i64, TestError, SameStageLinker, ScfTestFrame>; + +fn parse_scf(program: &str) -> Pipeline { + let mut pipeline: Pipeline = Pipeline::new(); + ParsePipelineText::parse(&mut pipeline, program).expect("program parses"); + pipeline +} + +fn run_scf(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + let mut interp: ScfEngine<'_> = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +// =========================================================================== +// 5. Nested structured Block: ScfIfFrame/ScfForFrame → BlockFrame. +// =========================================================================== + +/// `scf.if` picks the decided arm and pushes the framework `BlockFrame` for +/// it; the arm's `yield` surfaces as `Completion::Yielded`, which the +/// `ScfIfFrame` consumes and hands to the pusher as the operation's results. +#[test] +fn scf_if_arm_yields_to_dialect_frame() { + let pipeline = parse_scf( + r#" +stage @test fn @abs(i64) -> i64; + +specialize @test fn @abs(i64) -> i64 { + ^entry(%x: i64) { + %zero = constant 0 -> i64; + %is_neg = lt %x, %zero -> i64; + %result = if %is_neg then ^then() { + %negated = neg %x -> i64; + yield %negated; + } else ^else() { + yield %x; + } -> i64; + ret %result; + } +} +"#, + ); + assert_eq!(run_scf(&pipeline, "abs", &[-7]).unwrap(), 7); + assert_eq!(run_scf(&pipeline, "abs", &[4]).unwrap(), 4); +} + +/// `scf.for` re-pushes the framework `BlockFrame` per iteration; each +/// `Completion::Yielded` carries the loop-carried values into the next turn, +/// and loop exit completes `Finished` to the pusher. +#[test] +fn scf_for_loop_carries_yielded_values() { + let pipeline = parse_scf( + r#" +stage @test fn @sum_below(i64) -> i64; + +specialize @test fn @sum_below(i64) -> i64 { + ^entry(%n: i64) { + %zero = constant 0 -> i64; + %one = constant 1 -> i64; + %sum = for %zero in %zero..%n step %one iter_args(%zero) do ^body(%i: i64, %acc: i64) { + %next = add %acc, %i -> i64; + yield %next; + } -> i64; + ret %sum; + } +} +"#, + ); + // 0 + 1 + 2 + 3 + 4 = 10. + assert_eq!(run_scf(&pipeline, "sum_below", &[5]).unwrap(), 10); + // Zero iterations: the initial carried value flows through. + assert_eq!(run_scf(&pipeline, "sum_below", &[0]).unwrap(), 0); +} + +// =========================================================================== +// 6. Return through nested structured control. +// =========================================================================== + +/// A function `Return` inside an `scf.if` arm: the arm's `BlockFrame` +/// completes `Returned`, the `ScfIfFrame` relays it (it is not a call +/// boundary), the function's `CfgFrame` relays it too, and the nearest +/// `CallFrame` consumes it — freeing the callee activation exactly once and +/// writing the caller's result slots. Execution must not continue after the +/// return: the statements below the `if` never run on the early-return path. +#[test] +fn return_bubbles_through_scf_frames_to_call_frame() { + let pipeline = parse_scf( + r#" +stage @test fn @clamp0(i64) -> i64; +stage @test fn @twice(i64) -> i64; + +specialize @test fn @clamp0(i64) -> i64 { + ^entry(%x: i64) { + %zero = constant 0 -> i64; + %is_neg = lt %x, %zero -> i64; + %kept = if %is_neg then ^then() { + ret %zero; + } else ^else() { + yield %x; + } -> i64; + %one = constant 1 -> i64; + %r = add %kept, %one -> i64; + ret %r; + } +} + +specialize @test fn @twice(i64) -> i64 { + ^entry(%x: i64) { + %a = call.named @clamp0(%x) -> i64; + %b = call.named @clamp0(%x) -> i64; + %s = add %a, %b -> i64; + ret %s; + } +} +"#, + ); + // Early return: 0, not 0 + 1 — the add after the `if` did not run. + assert_eq!(run_scf(&pipeline, "clamp0", &[-5]).unwrap(), 0); + // Normal path: the arm yields, execution continues after the `if`. + assert_eq!(run_scf(&pipeline, "clamp0", &[5]).unwrap(), 6); + // Two nested calls taking the early-return path in one run: each callee + // activation is freed exactly once and the caller's activation survives, + // otherwise the second call (or the final add) would read freed state. + assert_eq!(run_scf(&pipeline, "twice", &[-5]).unwrap(), 0); + assert_eq!(run_scf(&pipeline, "twice", &[5]).unwrap(), 12); +} + +// =========================================================================== +// 7. Custom callable-UnGraph policy: caller → CallFrame → policy frame. +// =========================================================================== + +// The framework refuses to invent an execution order for an undirected +// graph, so the *compiler* supplies one by overriding +// `FrameBuild::from_ungraph_entry` on its total frame type. This policy +// interprets an ungraph as a sequential dataflow chain: +// +// - **scheduling**: node statements run in the graph's canonical node +// enumeration order (an explicit policy choice — the generic `CallFrame` +// never orders anything); +// - **outputs**: the call returns the result values of the *last* scheduled +// node (an ungraph has no framework output convention such as a digraph's +// declared yields, so the policy defines one). + +/// Total frame enum for the UnGraph-policy engine: the standard frames plus +/// the policy's own walker. Only `from_ungraph_entry` differs from the +/// default composition — the generic traversal logic is untouched. +enum UnPolicyFrame { + Block(BlockFrame), + Cfg(CfgFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + Chain(UnGraphChainFrame), +} + +impl FrameBuild for UnPolicyFrame { + fn from_block(frame: BlockFrame) -> Self { + UnPolicyFrame::Block(frame) + } + fn from_cfg(frame: CfgFrame) -> Self { + UnPolicyFrame::Cfg(frame) + } + fn from_call(frame: CallFrame) -> Self { + UnPolicyFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + UnPolicyFrame::DiGraph(frame) + } + fn from_ungraph_entry(entry: UnGraphEntry) -> Result { + Ok(UnPolicyFrame::Chain(UnGraphChainFrame::new(entry))) + } +} + +/// The compiler-owned callable-UnGraph walker (the policy itself). +struct UnGraphChainFrame { + stage: CompileStage, + /// The callee activation — owned and freed by the awaiting `CallFrame`, + /// merely used here. + index: EnvIndex, + graph: UnGraph, + /// Entry arguments awaiting the boundary-port binding on the first step. + pending: Option>, + /// The policy's schedule (canonical node enumeration order). + schedule: VecDeque, + /// The policy's output convention: the last scheduled node's results. + outputs: Vec, +} + +impl UnGraphChainFrame { + fn new(entry: UnGraphEntry) -> Self { + Self { + stage: entry.stage, + index: entry.index, + graph: entry.graph, + pending: Some(entry.args), + schedule: VecDeque::new(), + outputs: Vec::new(), + } + } + + fn step( + mut self, + interp: &mut UnEngine<'_>, + ) -> Result>, TestError> { + // First step: bind the boundary ports and fix the policy's schedule + // and output convention from the graph's structure. + if let Some(args) = self.pending.take() { + let info = interp + .pipeline() + .stage(self.stage) + .ok_or(InterpreterError::MissingStage(self.stage))?; + let graph_info = self + .graph + .get_info(info) + .ok_or(InterpreterError::Custom("ungraph has no info"))?; + if graph_info.ports().len() != args.len() { + return Err(TestError::Core(InterpreterError::ProductArityMismatch { + expected: graph_info.ports().len(), + actual: args.len(), + })); + } + let ports: Vec<_> = graph_info.ports().to_vec(); + let nodes: Vec = graph_info.graph().node_weights().copied().collect(); + let last = *nodes + .last() + .ok_or(InterpreterError::Custom("empty ungraph body"))?; + self.outputs = last + .definition(info) + .results() + .map(|result| SSAValue::from(*result)) + .collect(); + self.schedule = nodes.into(); + for (port, value) in ports.into_iter().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + return Ok(FrameEffect::Continue(UnPolicyFrame::Chain(self))); + } + + match self.schedule.pop_front() { + Some(statement) => match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(UnPolicyFrame::Chain(self))), + _ => Err(TestError::Core(InterpreterError::Custom( + "the chain policy supports only ordinary dataflow nodes", + ))), + }, + // Natural completion: the policy's outputs become the call's + // returned values (the awaiting CallFrame accepts `Finished`). + None => { + let values: Product = self + .outputs + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(Completion::Finished(values))) + } + } + } +} + +type UnEngine<'ir> = ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, UnPolicyFrame>; + +impl<'ir> Frame> for UnPolicyFrame { + type Completion = Completion; + + fn step( + self, + interp: &mut UnEngine<'ir>, + ) -> Result, TestError> { + match self { + UnPolicyFrame::Block(frame) => frame.step_into::, Self>(interp), + UnPolicyFrame::Cfg(frame) => frame.step_into::, Self>(interp), + UnPolicyFrame::Call(frame) => frame.step_into::, Self>(interp), + UnPolicyFrame::DiGraph(frame) => frame.step_into::, Self>(interp), + UnPolicyFrame::Chain(frame) => frame.step(interp), + } + } + + fn resume_done( + self, + _interp: &mut UnEngine<'ir>, + ) -> Result, TestError> { + match self { + UnPolicyFrame::Block(frame) => Ok(frame.resume_done_into::()), + UnPolicyFrame::Cfg(frame) => Ok(frame.resume_done_into::()), + UnPolicyFrame::Call(frame) => frame.resume_done_into::().map_err(TestError::from), + UnPolicyFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), + UnPolicyFrame::Chain(_) => Err(TestError::Core(InterpreterError::Custom( + "the chain policy pushes no children", + ))), + } + } + + fn resume( + self, + completion: Self::Completion, + interp: &mut UnEngine<'ir>, + ) -> Result, TestError> { + match self { + UnPolicyFrame::Block(frame) => { + frame.resume_into::, Self>(completion, interp) + } + UnPolicyFrame::Cfg(frame) => { + frame.resume_into::, Self>(completion, interp) + } + UnPolicyFrame::Call(frame) => { + frame.resume_into::, Self>(completion, interp) + } + UnPolicyFrame::DiGraph(frame) => { + frame.resume_into::, Self>(completion, interp) + } + UnPolicyFrame::Chain(_) => Err(TestError::Core(InterpreterError::Custom( + "the chain policy pushes no children", + ))), + } + } +} + +const UNGRAPH_PROGRAM: &str = r#" +stage @test fn @usq(i64, i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @usq(i64, i64) -> i64 ungraph ^u0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + %t = mul %s, %s -> i64; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %a = constant 2 -> i64; + %b = constant 3 -> i64; + %r = call.named @usq(%a, %b) -> i64; + ret %r; + } +} +"#; + +/// A language *with* an UnGraph policy: the call goes caller → `CallFrame` → +/// the compiler's `UnGraphChainFrame`. The `CallFrame` still owns the callee +/// activation and return bookkeeping; only the walker construction was +/// delegated. +#[test] +fn custom_ungraph_policy_is_callable() { + let pipeline = parse(UNGRAPH_PROGRAM); + let mut interp: UnEngine<'_> = ConcreteInterpreter::new(&pipeline); + let result: i64 = + expect_single::(interp.call_by_name("test", "main", []).unwrap()).unwrap(); + // (2 + 3)^2 = 25, via the policy's chain schedule and last-node outputs. + assert_eq!(result, 25); +} + +// =========================================================================== +// 8. UnGraph without a policy: a clear no-default-walker error. +// =========================================================================== + +/// The standard frames supply no UnGraph traversal, so calling an +/// UnGraph-bodied function reports `NoDefaultWalker` instead of inventing a +/// node order. +#[test] +fn ungraph_without_policy_reports_no_default_walker() { + let pipeline = parse(UNGRAPH_PROGRAM); + let error = run(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + error, + TestError::Core(InterpreterError::NoDefaultWalker(Body::UnGraph(_))) + ), + "expected NoDefaultWalker(UnGraph), got {error:?}" + ); } From 969914e910ba38cb39e09e05a4817ba9de9ecdbc Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 16 Jul 2026 13:33:42 -0400 Subject: [PATCH 05/21] refactor: simplify liveness analysis by removing demand pre-pass --- crates/kirin-liveness/src/lib.rs | 35 ++++++++-- docs/design/interpreter/index.md | 3 +- example/toy-lang/src/interpreter/mod.rs | 34 +++------ example/toy-lang/src/interpreter/tests.rs | 84 ++++++++++++++++------- example/toy-lang/src/main.rs | 7 +- example/toy-lang/tests/e2e.rs | 25 +++++++ 6 files changed, 132 insertions(+), 56 deletions(-) diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 59ee1bdfe8..30bd1d6344 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -26,7 +26,8 @@ pub use live::{Live, LiveSet}; pub use result::{DemandResult, DenseLivenessResult}; use kirin_interpreter::{ - Body, DenseBackwardInterpreter, DenseBackwardTransfer, InterpDispatch, InterpreterError, + Body, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, + DenseBackwardTransfer, DenseFrameBuild, Frame, InterpDispatch, InterpreterError, SparseBackwardDriver, SparseBackwardInterpreter, StageQuery, StandardDenseBackwardFrame, }; use kirin_ir::{CompileStage, Pipeline, StageMeta}; @@ -59,9 +60,9 @@ where } /// Run classic per-point liveness (dense backward) over `body` in `stage`, -/// with the standard (structured-control-free) frames. Languages with scf -/// compose [`DenseLiveness`] with their own frame type and build the result -/// via [`DenseLivenessResult::from_engine`]. +/// with the standard (structured-control-free) frames. Languages with +/// structured dialects select their total frame through +/// [`analyze_dense_with_frame`] instead. pub fn analyze_dense<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, @@ -79,9 +80,33 @@ where StandardDenseBackwardFrame, >, >, +{ + analyze_dense_with_frame::>( + pipeline, stage, body, + ) +} + +/// Run classic per-point liveness (dense backward) over `body` in `stage` +/// with a caller-selected total frame type `F` — the entry point for +/// languages whose structured dialects require a language-specific total +/// frame. The analysis consumes the finalized IR directly; it neither +/// requires nor computes a demand ([`DemandResult`]) pre-pass. +pub fn analyze_dense_with_frame<'ir, S, F>( + pipeline: &'ir Pipeline, + stage: CompileStage, + body: impl Into, +) -> Result +where + S: StageMeta + + StageQuery + + InterpDispatch>, + F: Frame< + DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, + Completion = DenseBackwardCompletion, + > + DenseFrameBuild, { let body = body.into(); - let mut engine = DenseLiveness::::new(pipeline); + let mut engine = DenseLiveness::::new(pipeline); engine.analyze(stage, body)?; DenseLivenessResult::from_engine(&mut engine, stage, body) } diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 4ec735c6fb..96ef9fd20d 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -563,7 +563,8 @@ and terminating on unknown inputs (both fold to `Top`). Runnable as kill/gen transfer (`gen_uses_kill_defs`, purity-irrelevant). Block owners converge boundary summaries; `live_before`/`live_after` are reconstructed per point on demand (never persisted by the fixpoint); scf owns dense - frames (arm-join, loop fixpoint). + frames (arm-join, loop fixpoint). Classic liveness consumes finalized IR + directly and does not require a sparse-demand pre-pass. Strong per-point sets are the composition `dense ∩ demanded`, not a third analysis. Because the `Semantics` parameter distinguishes impls, one dialect carries all three rules at once, as every shipped dialect demonstrates. diff --git a/example/toy-lang/src/interpreter/mod.rs b/example/toy-lang/src/interpreter/mod.rs index 07b6974516..eebd6e6219 100644 --- a/example/toy-lang/src/interpreter/mod.rs +++ b/example/toy-lang/src/interpreter/mod.rs @@ -22,7 +22,7 @@ use kirin_interpreter::engine::{ CallContext, ConcreteInterpreter, CrossStageLinker, SameStageLinker, SparseForwardInterpreter, expect_single, }; -use kirin_liveness::{DemandResult, DenseLivenessResult, LiveSet}; +use kirin_liveness::{DenseLivenessResult, LiveSet}; use crate::language::{HighLevel, LowLevel}; use crate::stage::Stage; @@ -47,18 +47,6 @@ pub type ToyConstProp<'ir, Lk = CrossStageLinker> = SparseForwardInterpreter< ToyAbstractFrame, >; -/// Classic per-point liveness (dense backward) over toy programs, with a -/// frame type embedding the SCF dense frames (arm join + loop fixpoint). -/// Strong liveness needs no composition — the sparse demand engine has no -/// frames (loop-carried demand converges on the value worklist), so -/// [`kirin_liveness::analyze_demand`] applies directly. -pub type ToyDenseLiveness<'ir> = kirin_liveness::DenseLiveness< - 'ir, - Stage, - InterpreterError, - ToyDenseBackwardFrame, ->; - /// Execute `function_name` starting at `stage_name`, following calls across /// language boundaries. pub fn run_i64( @@ -188,18 +176,18 @@ fn function_cfg( Ok((stage_id, cfg)) } -/// Run both liveness analyses over `function_name`'s body at `stage_name`: -/// strong liveness (the sparse demanded set — DCE-grade) and classic per-point -/// liveness (dense block-boundary sets — regalloc-grade). -pub fn analyze_liveness( +/// Run classic per-point liveness (dense backward — regalloc-grade +/// block-boundary and per-statement sets) over `function_name`'s body at +/// `stage_name`. Consumes the finalized IR directly; strong demand +/// ([`kirin_liveness::analyze_demand`]) is an independent analysis and is +/// not involved. +pub fn analyze_classic_liveness( pipeline: &Pipeline, stage_name: &str, function_name: &str, -) -> Result<(DemandResult, DenseLivenessResult), InterpreterError> { +) -> Result { let (stage, cfg) = function_cfg(pipeline, stage_name, function_name)?; - let demand = kirin_liveness::analyze_demand(pipeline, stage, cfg)?; - let mut engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - engine.analyze(stage, cfg)?; - let dense = DenseLivenessResult::from_engine(&mut engine, stage, cfg)?; - Ok((demand, dense)) + kirin_liveness::analyze_dense_with_frame::<_, ToyDenseBackwardFrame>( + pipeline, stage, cfg, + ) } diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index b4530db1a7..d3d4810874 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -1255,32 +1255,35 @@ specialize @source fn @main(i64, i64) -> i64 { } // =========================================================================== -// Classic (dense, per-point) liveness through scf's dialect-owned dense -// frames: arm-join for `scf.if`, the loop-carried fixpoint for `scf.for`, and -// per-point reconstruction inside structured bodies. +// Classic (dense, per-point) liveness through the toy language's total dense +// frame: arm-join for `scf.if`, the loop-carried fixpoint for `scf.for`, and +// per-point reconstruction inside structured bodies. Runs on the finalized IR +// alone — no demand pre-pass. // =========================================================================== mod dense { use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue, Statement}; use kirin_arith::{Arith, ArithValue}; - use kirin_liveness::{DenseLivenessResult, LiveSet, analyze_demand}; + use kirin_interpreter::InterpreterError; + use kirin_liveness::{DenseLivenessResult, LiveSet}; use super::demand::{FOR_CARRIED_DEMAND, IF_DEAD_RESULT}; use super::demand::{constant_result, entry_params, find_value, parse, source_cfg}; - use crate::interpreter::ToyDenseLiveness; + use crate::interpreter::ToyDenseBackwardFrame; use crate::language::HighLevel; use crate::stage::Stage; - /// Run classic dense liveness with the toy total frame (scf frames - /// embedded). + /// Run classic dense liveness with the toy total frame. fn analyze_dense_toy( pipeline: &Pipeline, stage: CompileStage, cfg: CFG, ) -> DenseLivenessResult { - let mut engine: ToyDenseLiveness<'_> = ToyDenseLiveness::new(pipeline); - engine.analyze(stage, cfg).expect("analysis succeeds"); - DenseLivenessResult::from_engine(&mut engine, stage, cfg).expect("reconstruction succeeds") + kirin_liveness::analyze_dense_with_frame::< + _, + ToyDenseBackwardFrame, + >(pipeline, stage, cfg) + .expect("analysis succeeds") } /// The statement whose definition matches `select` (anywhere in the @@ -1309,15 +1312,15 @@ mod dense { values.iter().copied().collect() } - /// Per-point sets inside an `scf.if` arm follow classic semantics (the - /// yield's operand is live after its def even though the result is dead), - /// and intersecting with the demand set recovers the strong view. + /// Per-point sets inside an `scf.if` arm follow classic semantics: the + /// yield's operand is live after its def even though the result is dead. + /// (The strong view is the `dense ∩ demanded` composition, covered in + /// kirin-liveness — no demand pass runs here.) #[test] fn dense_per_point_inside_scf_if_arm() { let pipeline = parse(IF_DEAD_RESULT); let (stage, cfg) = source_cfg(&pipeline, "if_dead"); let dense = analyze_dense_toy(&pipeline, stage, cfg); - let demand = analyze_demand(&pipeline, stage, cfg).expect("demand succeeds"); let cond = entry_params(&pipeline, cfg)[0]; let a = constant_result(&pipeline, cfg, 1); @@ -1331,20 +1334,55 @@ mod dense { assert_eq!(dense.live_after(a_const), Some(&live_set(&[cond, a]))); assert_eq!(dense.live_before(a_const), Some(&live_set(&[cond]))); - // The if's own points: its dead result is live after it (classic - // records what the walk saw: nothing uses it, so it is NOT live), and - // before it only the condition survives the arm join. + // The if's dead result is not live after it because nothing uses it; + // before it, only the condition survives the arm join. let if_stmt = find_statement(&pipeline, cfg, |definition| { matches!(definition, HighLevel::Structured(_)) }); assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond]))); + } + + const IF_ARMS_DIFFERENT_USES: &str = r#" +stage @source fn @if_arms(i64, i64, i64) -> i64; + +specialize @source fn @if_arms(i64, i64, i64) -> i64 { + ^entry(%cond: i64, %x: i64, %y: i64) { + %r = if %cond then ^then() { + yield %x; + } else ^else() { + yield %y; + } -> i64; + ret %r; + } +} +"#; + + /// The scf.if liveness frame walks BOTH arms backward and joins their + /// live-entry states: the arms use different SSA values (%x vs %y), so + /// the state before the `if` must contain the condition and both. + #[test] + fn dense_scf_if_joins_both_arm_entries() { + let pipeline = parse(IF_ARMS_DIFFERENT_USES); + let (stage, cfg) = source_cfg(&pipeline, "if_arms"); + let dense = analyze_dense_toy(&pipeline, stage, cfg); + + let params = entry_params(&pipeline, cfg); + let (cond, x, y) = (params[0], params[1], params[2]); + let if_stmt = find_statement(&pipeline, cfg, |definition| { + matches!(definition, HighLevel::Structured(_)) + }); + let r = find_value(&pipeline, cfg, |definition| match definition { + HighLevel::Structured(_) => { + use kirin::prelude::HasResults; + definition.results().next().map(|v| SSAValue::from(*v)) + } + _ => None, + }); - // Strong per-point view: %a is classically live after its def but not - // demanded (the if result is dead), so the composition drops it. - let strong = dense - .strong_live_after(a_const, &demand) - .expect("point reconstructed"); - assert_eq!(strong, live_set(&[cond])); + // After the if only its result matters; before it, the then-arm + // contributed %x, the else-arm %y, and the rule genned %cond. + assert_eq!(dense.live_after(if_stmt), Some(&live_set(&[r]))); + assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond, x, y]))); } /// The scf.for dense frame iterates the body walk to the loop-carried diff --git a/example/toy-lang/src/main.rs b/example/toy-lang/src/main.rs index 069a4631de..1a39465b41 100644 --- a/example/toy-lang/src/main.rs +++ b/example/toy-lang/src/main.rs @@ -41,8 +41,8 @@ enum Command { /// Run constant propagation instead of concrete execution. #[arg(long)] constprop: bool, - /// Run liveness analysis (strong demand + classic per-point) instead - /// of concrete execution. + /// Run classic per-program-point liveness analysis (dense backward) + /// instead of concrete execution. #[arg(long)] liveness: bool, /// Restrict execution to the entry stage's language; reject calls @@ -108,8 +108,7 @@ fn run_program( } if liveness { - let (demand, dense) = interpreter::analyze_liveness(&pipeline, stage_name, func_name)?; - println!("demanded: {:?}", demand.demanded()); + let dense = interpreter::analyze_classic_liveness(&pipeline, stage_name, func_name)?; let mut boundaries: Vec<_> = dense .blocks() .map(|(block, live_in, live_out)| format!("{block:?}: in={live_in:?} out={live_out:?}")) diff --git a/example/toy-lang/tests/e2e.rs b/example/toy-lang/tests/e2e.rs index 4fc3f9373f..2156364027 100644 --- a/example/toy-lang/tests/e2e.rs +++ b/example/toy-lang/tests/e2e.rs @@ -177,3 +177,28 @@ fn test_run_missing_stage() { .assert() .failure(); } + +#[test] +fn test_liveness_prints_dense_sets_without_demand() { + // `--liveness` runs exactly one analysis: classic dense-backward + // per-point liveness. It prints block boundary sets and must NOT run or + // print the independent strong-demand analysis. + toy_lang() + .args([ + "run", + "programs/branching.kirin", + "--stage", + "source", + "--function", + "abs", + "--liveness", + ]) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .assert() + .success() + .stdout( + predicate::str::contains("in=") + .and(predicate::str::contains("out=")) + .and(predicate::str::contains("demanded:").not()), + ); +} From 31bd51175e723196021708c246426e392e1880bc Mon Sep 17 00:00:00 2001 From: Dennis Liew <48105496+zhenrongliew@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:19:04 -0400 Subject: [PATCH 06/21] Fixed SSAInfo.uses def-use population at finalize. (#687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builders set `SSAInfo.kind` (use→def) but nobody ever populated `SSAInfo.uses` (def→use). Both finalize paths just copied an always-empty vec through. So finalized IR shipped an empty def-use. - `StageInfo::rebuild_use_index()`: clears every live value's `uses,` then scans each live statement's operands (`HasArguments` order) and records one `Use { stmt, operand_index } `on the value each slot reads. Idempotent, so the future rewriter can re-run it. Skips deleted statements/tombstoned SSAs. - both `finalize()` and `finalize_unchecked()` call it before returning. - `Use` gains `new()` + `stmt()/operand_index()` accessors, `Copy`, and serde parity with its container `SSAInfo`. - `rebuild_use_index` now scan over nodes.digraphs, pushing a `DiGraphYield { graph, index }` into `SSAInfo.uses`. ```rust pub enum Use { StatementOperand { stmt: Statement, index: usize }, DiGraphYield { graph: DiGraph, index: usize }, } ``` --- crates/kirin-ir/src/builder/stage_info.rs | 12 +++-- crates/kirin-ir/src/lib.rs | 2 +- crates/kirin-ir/src/node/mod.rs | 2 +- crates/kirin-ir/src/node/ssa.rs | 31 +++++++++-- crates/kirin-ir/src/stage/info.rs | 66 ++++++++++++++++++++++- crates/kirin-ir/tests/builder_block.rs | 52 ++++++++++++++++++ crates/kirin-ir/tests/builder_graph.rs | 33 ++++++++++++ 7 files changed, 186 insertions(+), 12 deletions(-) diff --git a/crates/kirin-ir/src/builder/stage_info.rs b/crates/kirin-ir/src/builder/stage_info.rs index d121eb2a4a..3393af5da4 100644 --- a/crates/kirin-ir/src/builder/stage_info.rs +++ b/crates/kirin-ir/src/builder/stage_info.rs @@ -236,10 +236,12 @@ impl BuilderStageInfo { }, |_info| None, ); - Ok(StageInfo { + let mut stage = StageInfo { nodes: self.nodes, ssas, - }) + }; + stage.rebuild_use_index(); + Ok(stage) } /// Convert to [`StageInfo`] without validation. @@ -279,10 +281,12 @@ impl BuilderStageInfo { // Deleted items become None tombstones — safe, no zeroed memory. |_info| None, ); - StageInfo { + let mut stage = StageInfo { nodes: self.nodes, ssas, - } + }; + stage.rebuild_use_index(); + stage } } diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index 78659f84c1..ac761e83da 100644 --- a/crates/kirin-ir/src/lib.rs +++ b/crates/kirin-ir/src/lib.rs @@ -36,7 +36,7 @@ pub use node::{ GraphInfo, LinkedList, LinkedListNode, Port, PortParent, ResolutionInfo, ResultValue, SSAInfo, SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, StagedFunction, StagedFunctionInfo, StagedNamePolicy, Statement, StatementInfo, StatementParent, Successor, - Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, UniqueLiveSpecializationError, + Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, UniqueLiveSpecializationError, Use, }; pub use pipeline::Pipeline; pub use product::{HasProduct, Product}; diff --git a/crates/kirin-ir/src/node/mod.rs b/crates/kirin-ir/src/node/mod.rs index 2615537850..bc485be5c7 100644 --- a/crates/kirin-ir/src/node/mod.rs +++ b/crates/kirin-ir/src/node/mod.rs @@ -22,7 +22,7 @@ pub use linked_list::{LinkedList, LinkedListNode}; pub use port::{Port, PortParent}; pub use ssa::{ BlockArgument, BuilderKey, BuilderSSAInfo, BuilderSSAKind, DeletedSSAValue, ResolutionInfo, - ResultValue, SSAInfo, SSAKind, SSAValue, TestSSAValue, + ResultValue, SSAInfo, SSAKind, SSAValue, TestSSAValue, Use, }; pub use stmt::{Statement, StatementInfo, StatementParent}; pub use symbol::{GlobalSymbol, Symbol}; diff --git a/crates/kirin-ir/src/node/ssa.rs b/crates/kirin-ir/src/node/ssa.rs index a3e47cc600..4efe340bbb 100644 --- a/crates/kirin-ir/src/node/ssa.rs +++ b/crates/kirin-ir/src/node/ssa.rs @@ -3,6 +3,7 @@ use crate::identifier; use crate::{Dialect, Symbol}; use smallvec::SmallVec; +use super::digraph::DiGraph; use super::port::{Port, PortParent}; use super::{block::Block, stmt::Statement}; @@ -238,10 +239,32 @@ impl From> for BuilderSSAInfo { } } -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Use { - stmt: Statement, - operand_index: usize, +/// One def-use edge: a position in the IR that reads an SSA value. +/// +/// Stored in [`SSAInfo::uses`] as the reverse index of the authoritative +/// storage. Two kinds of position read a value: +/// +/// - a **statement operand** slot (the usual case — `arith.add`'s operands, +/// CFG branch arguments, `scf.yield` values, function returns), and +/// - a **`DiGraph` body yield** — a value the graph exports across its body +/// boundary. A yield has no backing statement (it lives in +/// [`DiGraphInfo::yields`](crate::DiGraphInfo)), so it cannot be named as a +/// statement operand, but it is a genuine use. +/// +/// `UnGraph` has no analogue: its `Extra` is a list of edge *statements*, whose +/// operands are already ordinary statement-operand uses. +/// +/// Populated by +/// [`StageInfo::rebuild_use_index`](crate::StageInfo::rebuild_use_index) at +/// finalization; a mutation layer (the rewriter) must keep it in sync with +/// every operand and yield change. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Use { + /// The `index`-th operand slot of `stmt`, in `HasArguments` order. + StatementOperand { stmt: Statement, index: usize }, + /// The `index`-th yield slot of the directed graph body `graph`. + DiGraphYield { graph: DiGraph, index: usize }, } /// A lookup key for builder placeholders — resolved at build time to the real SSA value. diff --git a/crates/kirin-ir/src/stage/info.rs b/crates/kirin-ir/src/stage/info.rs index aadf8112cd..abd41b33e0 100644 --- a/crates/kirin-ir/src/stage/info.rs +++ b/crates/kirin-ir/src/stage/info.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; -use crate::arena::Arena; -use crate::node::ssa::SSAInfo; +use crate::arena::{Arena, Id}; +use crate::node::ssa::{SSAInfo, Use}; use crate::{BuilderStageInfo, Dialect, node::*}; use super::arenas::Arenas; @@ -117,6 +117,68 @@ impl StageInfo { &self.ssas } + /// Rebuild the def-use index ([`SSAInfo::uses`](crate::SSAInfo)) from the + /// authoritative storage. + /// + /// Clears every live value's use list, then records one [`Use`](crate::Use) + /// per position that reads a value: + /// + /// - each live statement's operands (in [`HasArguments`](crate::HasArguments) + /// order) → [`Use::StatementOperand`](crate::Use), and + /// - each live `DiGraph` body's yields (in yield order) → + /// [`Use::DiGraphYield`](crate::Use). A yield is a boundary export with no + /// backing statement, so it would otherwise be invisible to the operand + /// scan. + /// + /// The operand and yield slots are the ground truth; this list is a derived + /// reverse index over them. `UnGraph` contributes nothing: its edges are + /// statements whose operands are already covered above; graph ports and + /// block arguments are definitions, not uses. + /// + /// Idempotent — safe to re-run. Called at + /// [`finalize`](crate::BuilderStageInfo::finalize) so finalized IR ships a + /// populated index; a mutation layer must call it (or maintain the index + /// incrementally) after changing operands or yields. + pub fn rebuild_use_index(&mut self) { + let StageInfo { nodes, ssas } = self; + + for item in ssas.iter_mut() { + if let Some(info) = (**item).as_mut() { + info.uses_mut().clear(); + } + } + + for (raw, item) in nodes.statements.items.iter().enumerate() { + if item.deleted() { + continue; + } + let stmt = Statement(Id(raw)); + let operands: Vec = item.data.definition.arguments().copied().collect(); + for (index, operand) in operands.into_iter().enumerate() { + if let Some(slot) = ssas.get_mut(operand) + && let Some(info) = (**slot).as_mut() + { + info.uses_mut().push(Use::StatementOperand { stmt, index }); + } + } + } + + for (raw, item) in nodes.digraphs.items.iter().enumerate() { + if item.deleted() { + continue; + } + let graph = DiGraph::from(Id(raw)); + let yields: Vec = item.data.yields().to_vec(); + for (index, yielded) in yields.into_iter().enumerate() { + if let Some(slot) = ssas.get_mut(yielded) + && let Some(info) = (**slot).as_mut() + { + info.uses_mut().push(Use::DiGraphYield { graph, index }); + } + } + } + } + /// Temporarily convert to a [`BuilderStageInfo`] for construction, then /// convert back. /// diff --git a/crates/kirin-ir/tests/builder_block.rs b/crates/kirin-ir/tests/builder_block.rs index 8ae314b163..31729749cc 100644 --- a/crates/kirin-ir/tests/builder_block.rs +++ b/crates/kirin-ir/tests/builder_block.rs @@ -88,6 +88,58 @@ fn block_builder_substitutes_builder_block_arguments() { assert!(matches!(ssa1.kind(), SSAKind::BlockArgument(_, 1))); } +#[test] +fn finalize_populates_def_use_index() { + let mut stage = new_stage(); + + let arg0 = stage.block_argument().index(0); + let arg1 = stage.block_argument().index(1); + + // arg0 is read twice (add operand 0, use operand 0); arg1 once (add operand 1). + let add_stmt = stage + .statement() + .definition(BuilderDialect::Add(arg0, arg1)) + .new(); + let use_stmt = stage + .statement() + .definition(BuilderDialect::Use(arg0)) + .new(); + + let block = stage + .block() + .argument(TestType::I32) + .argument(TestType::I64) + .stmt(add_stmt) + .stmt(use_stmt) + .new(); + + let stage = stage.finalize().unwrap(); + let block_info = block.expect_info(&stage); + let real_arg0: SSAValue = block_info.arguments[0].into(); + let real_arg1: SSAValue = block_info.arguments[1].into(); + + let uses0 = real_arg0.get_info(&stage).unwrap().uses(); + assert_eq!(uses0.len(), 2, "arg0 is read by two statements"); + assert!(uses0.contains(&Use::StatementOperand { + stmt: add_stmt, + index: 0 + })); + assert!(uses0.contains(&Use::StatementOperand { + stmt: use_stmt, + index: 0 + })); + + let uses1 = real_arg1.get_info(&stage).unwrap().uses(); + assert_eq!(uses1.len(), 1, "arg1 is read only by the add"); + assert_eq!( + uses1[0], + Use::StatementOperand { + stmt: add_stmt, + index: 1 + } + ); +} + #[test] #[should_panic(expected = "is not a terminator")] fn block_builder_terminator_rejects_non_terminator() { diff --git a/crates/kirin-ir/tests/builder_graph.rs b/crates/kirin-ir/tests/builder_graph.rs index feba59349b..ef71dd8be5 100644 --- a/crates/kirin-ir/tests/builder_graph.rs +++ b/crates/kirin-ir/tests/builder_graph.rs @@ -34,6 +34,39 @@ fn digraph_builder_two_node_dag() { assert_eq!(*s1.parent(&stage), Some(StatementParent::DiGraph(dg))); } +#[test] +fn finalize_indexes_digraph_yields_as_uses() { + let mut stage = new_stage(); + + // s0 produces %r; the graph yields %r. No statement reads %r, so its only + // use is the boundary yield — invisible to an operand-only scan. + let s0 = stage.statement().definition(BuilderDialect::Nop).new(); + let result_ssa = stage + .ssa() + .ty(TestType::I32) + .kind(BuilderSSAKind::Result(s0, 0)) + .new(); + + let dg = stage + .digraph() + .node(s0) + .yield_value(result_ssa) + .name("yielder") + .new(); + + let stage = stage.finalize().unwrap(); + + let uses = result_ssa.get_info(&stage).unwrap().uses(); + assert_eq!(uses.len(), 1, "%r is used only by the graph yield"); + assert_eq!( + uses[0], + Use::DiGraphYield { + graph: dg, + index: 0 + } + ); +} + #[test] fn digraph_builder_port_and_capture_creation() { let mut stage = new_stage(); From e0b99cc5554cabbd069ed5fa728e5e2cc6aff3d7 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 30 Jul 2026 13:41:29 -0400 Subject: [PATCH 07/21] refactor: generalize body handling and update topology queries --- crates/kirin-interpreter/src/core/frame.rs | 14 ++++++-- crates/kirin-interpreter/src/core/mod.rs | 5 ++- crates/kirin-interpreter/src/core/query.rs | 2 +- .../src/{facts => core}/topology.rs | 33 ++--------------- .../src/engines/dense_backward/interp.rs | 28 +++++++-------- .../src/engines/sparse_backward/interp.rs | 31 ++++++++-------- .../src/engines/sparse_backward/mod.rs | 2 +- crates/kirin-interpreter/src/facts/anchor.rs | 36 +++++++++++++++---- crates/kirin-interpreter/src/facts/mod.rs | 15 ++++---- crates/kirin-interpreter/src/lib.rs | 16 +++++---- example/toy-lang/src/interpreter/tests.rs | 4 +-- 11 files changed, 95 insertions(+), 91 deletions(-) rename crates/kirin-interpreter/src/{facts => core}/topology.rs (89%) diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 15f6b7713b..2b3d63af10 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -10,7 +10,7 @@ use std::hash::Hash; use kirin_ir::{Block, CFG, CompileStage, Product, SSAValue, Statement}; use crate::{ - CallEffect, CallableBody, Callee, Env, EnvIndex, FunctionTarget, Interp, InterpreterError, + Body, CallEffect, CallableBody, Callee, Env, EnvIndex, FunctionTarget, Interp, InterpreterError, }; /// Structural effect a [`Frame`] returns to the engine driver loop. @@ -143,11 +143,21 @@ pub trait ForwardFrameDriver: Env { /// The default walk plan of a digraph body (ports, toposorted nodes, /// yields). Errors on cyclic digraphs. + /// + /// Digraph bodies are opt-in: an engine that never walks one inherits this + /// rejection rather than inventing a schedule, the same way + /// [`FrameBuild::from_ungraph_entry`](crate::FrameBuild::from_ungraph_entry) + /// rejects a callable `UnGraph` without a compiler-supplied policy. fn digraph_walk_plan( &self, stage: CompileStage, graph: kirin_ir::DiGraph, - ) -> Result; + ) -> Result { + let _ = stage; + Err(Self::Error::from(InterpreterError::NoDefaultWalker( + Body::DiGraph(graph), + ))) + } /// Bind a block's parameters to incoming actuals in `env` (arity-checked). fn bind_block_args( diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index 1b57be2345..7970b999b4 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -1,6 +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 IR queries. +//! protocol, activation storage, calling conventions, errors, and the IR +//! queries ([`query`], [`topology`]) engines run against a stage. //! Everything here is engine-agnostic; the engines compose these pieces. pub(crate) mod dispatch; @@ -11,6 +12,7 @@ pub(crate) mod frame; pub(crate) mod interp; pub(crate) mod linker; pub(crate) mod query; +pub(crate) mod topology; pub(crate) mod value; pub use dispatch::{FunctionEntry, InterpDispatch, Interpretable}; @@ -23,4 +25,5 @@ pub use frame::{ pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use query::{GraphWalkPlan, StageQuery}; +pub use topology::{BlockTopology, BodyTopology, GraphTopology, body_topology}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index 31d5edf9cd..fe958c4896 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -15,7 +15,7 @@ use kirin_ir::{ use crate::Body; use crate::InterpreterError; -use crate::facts::topology::{self, BodyTopology}; +use crate::core::topology::{self, BodyTopology}; /// Block parameters as SSA values. pub struct BlockParams(pub Block); diff --git a/crates/kirin-interpreter/src/facts/topology.rs b/crates/kirin-interpreter/src/core/topology.rs similarity index 89% rename from crates/kirin-interpreter/src/facts/topology.rs rename to crates/kirin-interpreter/src/core/topology.rs index 718af3d1c3..a5addb78a4 100644 --- a/crates/kirin-interpreter/src/facts/topology.rs +++ b/crates/kirin-interpreter/src/core/topology.rs @@ -1,4 +1,5 @@ -//! Dialect-neutral body topology enumeration. +//! Dialect-neutral body topology enumeration: the implementation behind the +//! [`BodyTopologyQuery`](super::query::BodyTopologyQuery) IR query. //! //! Backward analyses need the *shape* of a body: which blocks and graph //! nodes exist (including bodies nested inside structured statements), each @@ -19,7 +20,7 @@ use kirin_ir::{ HasUngraphs, Port, SSAValue, StageInfo, Statement, UnGraph, }; -use crate::Body; +use crate::{Body, PortBoundary}; /// The shape of one block: its statements and CFG successors. #[derive(Clone, Debug)] @@ -48,19 +49,6 @@ pub struct GraphTopology { pub nested: bool, } -/// Where a graph port sits on its owning statement's boundary. -/// -/// This is a **location**, not a value mapping: the owning statement's -/// dialect rule translates port/index into its operands, captures, or -/// results — for values (forward) and demand (backward) alike. -#[derive(Clone, Copy, Debug)] -pub struct PortBoundary { - /// The statement that owns the graph. - pub owner: Statement, - /// Which boundary slot this port occupies. - pub index: usize, -} - /// The shape of a body: all blocks and graph parts (the analyzed body's own /// plus structured bodies, recursively), the block-feeder index, and the /// graph-port boundary index. @@ -72,9 +60,6 @@ pub struct BodyTopology { port_boundary: HashMap, } -/// Deprecated name for [`BodyTopology`]; kept for one release. -pub type CFGTopology = BodyTopology; - impl BodyTopology { /// The statements whose rules can translate demand on `block`'s parameters: /// terminators with an edge into `block`, plus statements owning `block` @@ -131,18 +116,6 @@ where topology } -/// Enumerate the topology of `cfg` in the finalized `stage`. -/// -/// Deprecated spelling of [`body_topology`] over a `CFG`; kept for one -/// release. -pub fn cfg_topology(stage: &StageInfo, cfg: &CFG) -> BodyTopology -where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, -{ - body_topology(stage, Body::CFG(*cfg)) -} - fn collect_block( stage: &StageInfo, block: Block, diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 7d328b484f..22493587f1 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -53,9 +53,9 @@ use kirin_ir::{ use super::frames::{DenseBlockFrame, DenseFrameBuild}; use crate::Body; use crate::core::query; -use crate::engines::sparse_backward::CFGScope; +use crate::engines::sparse_backward::BodyScope; use crate::{ - AbstractInterpreter, BackwardSummaryDeps, CFGTopology, ClassicLiveness, DenseBackwardSemantic, + AbstractInterpreter, BackwardSummaryDeps, BodyTopology, ClassicLiveness, DenseBackwardSemantic, DensePointStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, @@ -322,7 +322,7 @@ where E: From, Sem: DenseBackwardSemantic, { - type SummaryKey = Scoped; + type SummaryKey = Scoped; type Summary = BlockLiveness; type Frame = F; type Completion = DenseBackwardCompletion; @@ -338,11 +338,11 @@ pub enum DenseBackwardCompletion { } /// Analysis-local state carried in the driver's `store` slot: the scope, -/// the CFG topology, and an optional per-point recorder filled by the +/// the body topology, and an optional per-point recorder filled by the /// block frames during [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). pub struct DenseAnalysisState { - scope: Option, - topology: CFGTopology, + scope: Option, + topology: BodyTopology, recorder: Option>, } @@ -350,7 +350,7 @@ impl Default for DenseAnalysisState { fn default() -> Self { Self { scope: None, - topology: CFGTopology::default(), + topology: BodyTopology::default(), recorder: None, } } @@ -363,7 +363,7 @@ pub type DenseBackwardDriver<'ir, S, V, E, F, Sem = ClassicLiveness> = StandardF DenseBackwardTransfer<'ir, S, V, E, F, Sem>, DenseBackwardProfile, DenseAnalysisState, - BackwardSummaryDeps>, + BackwardSummaryDeps>, >; // =========================================================================== @@ -549,7 +549,7 @@ struct DenseBackwardSemantics; impl<'ir, S, V, E, F, Sem> OwnerSemantics< DenseBackwardDriver<'ir, S, V, E, F, Sem>, - Scoped, + Scoped, BlockLiveness, F, DenseBackwardCompletion, @@ -565,7 +565,7 @@ where fn bottom_summary( &mut self, _interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - _owner: &Scoped, + _owner: &Scoped, ) -> Result, E> { Ok(BlockLiveness::bottom()) } @@ -573,7 +573,7 @@ where fn entry_frame( &mut self, interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - owner: &Scoped, + owner: &Scoped, _summary: &BlockLiveness, ) -> Result { let (stage, _cfg) = owner.scope; @@ -586,9 +586,9 @@ where fn complete_owner( &mut self, _interp: &mut DenseBackwardDriver<'ir, S, V, E, F, Sem>, - owner: Scoped, + owner: Scoped, completion: DenseBackwardCompletion, - ) -> Result, BlockLiveness>, E> { + ) -> Result, BlockLiveness>, E> { match completion { DenseBackwardCompletion::Block { live_in, live_out } => Ok(SummaryEffect::Update { owner, @@ -689,7 +689,7 @@ where let body = body.into(); let scope = (stage, body); let topology = query::body_topology(self.driver.inner().pipeline(), stage, body)?; - let owners: Vec> = topology + let owners: Vec> = topology .cfg_blocks() .map(|block| Scoped::new(scope, block.block)) .collect(); diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index 56773de2a2..25a838139b 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -20,11 +20,11 @@ //! the real dispatch location, and the per-rule demand buffer. //! - the **[`StandardFixpointInterpreter`]** driver owns the demand facts //! (summaries keyed by [`Scoped`] SSA values — never bare values), the value -//! worklist, and the analysis state (scope + CFG topology). +//! worklist, and the analysis state (scope + body topology). //! //! # Owners are values; scheduling is demand propagation //! -//! `SummaryKey = Scoped<(CompileStage, CFG), SSAValue>`: the fact anchor is +//! `SummaryKey = Scoped`: the fact anchor is //! the owner. The driver's *default* self-dependent index //! ([`OwnerSummaryDeps`]) is exactly demand propagation — a value whose fact //! rises is rescheduled, and analyzing a value means dispatching the rules @@ -33,7 +33,7 @@ //! - a statement **result** → the defining statement's backward rule; //! - a **block argument** → each of the block's *feeders* (terminators //! targeting the block, statements owning it as a structured body) from the -//! [`CFGTopology`]; +//! [`BodyTopology`]; //! - a graph **port** → unsupported (loud error). //! //! Rules read converged facts ([`DemandInterp::is_demanded`]) and raise new @@ -55,7 +55,7 @@ use kirin_ir::{ use crate::core::query; use crate::{ - AbstractInterpreter, Body, CFGTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, + AbstractInterpreter, Body, BodyTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, Scoped, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, StrongDemand, Summary, SummaryEffect, @@ -67,9 +67,6 @@ use crate::{ /// bodies in one engine keeps their facts distinct. pub type BodyScope = (CompileStage, Body); -/// Deprecated name for [`BodyScope`]; kept for one release. -pub type CFGScope = BodyScope; - // =========================================================================== // Effect + dialect-facing trait // =========================================================================== @@ -278,18 +275,18 @@ where E: From, Sem: SparseBackwardSemantic, { - type SummaryKey = Scoped; + type SummaryKey = Scoped; type Summary = DemandSummary; type Frame = DemandFrame; type Completion = Vec<(SSAValue, V)>; } /// Analysis-local state carried in the driver's `store` slot: the scope facts -/// are qualified with, and the CFG topology (feeders for block arguments). +/// are qualified with, and the body topology (feeders for block arguments). #[derive(Default)] pub struct BackwardAnalysisState { - scope: Option, - topology: CFGTopology, + scope: Option, + topology: BodyTopology, } /// The sparse backward driver: a [`StandardFixpointInterpreter`] over @@ -299,7 +296,7 @@ pub type SparseBackwardDriver<'ir, S, V, E, Sem = StrongDemand> = StandardFixpoi SparseBackwardTransfer<'ir, S, V, E, Sem>, SparseBackwardProfile, BackwardAnalysisState, - OwnerSummaryDeps>, + OwnerSummaryDeps>, >; // =========================================================================== @@ -447,7 +444,7 @@ struct SparseBackwardSemantics; impl<'ir, S, V, E, Sem> OwnerSemantics< SparseBackwardDriver<'ir, S, V, E, Sem>, - Scoped, + Scoped, DemandSummary, DemandFrame, Vec<(SSAValue, V)>, @@ -462,7 +459,7 @@ where fn bottom_summary( &mut self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - _owner: &Scoped, + _owner: &Scoped, ) -> Result, E> { Ok(DemandSummary(V::bottom())) } @@ -470,7 +467,7 @@ where fn entry_frame( &mut self, interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - owner: &Scoped, + owner: &Scoped, _summary: &DemandSummary, ) -> Result, E> { let (stage, _cfg) = owner.scope; @@ -499,9 +496,9 @@ where fn complete_owner( &mut self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, - owner: Scoped, + owner: Scoped, completion: Vec<(SSAValue, V)>, - ) -> Result, DemandSummary>, E> { + ) -> Result, DemandSummary>, E> { // Scope-qualify the bare values the rules demanded. Ok(SummaryEffect::Many( completion diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs index 6fba1b42ad..aee1ad363f 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod interp; pub use interp::{ - BackwardAnalysisState, CFGScope, DemandFrame, DemandInterp, DemandSummary, + BackwardAnalysisState, BodyScope, DemandFrame, DemandInterp, DemandSummary, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; diff --git a/crates/kirin-interpreter/src/facts/anchor.rs b/crates/kirin-interpreter/src/facts/anchor.rs index 41dc249792..394b935345 100644 --- a/crates/kirin-interpreter/src/facts/anchor.rs +++ b/crates/kirin-interpreter/src/facts/anchor.rs @@ -1,5 +1,6 @@ -//! Lattice anchors: *where* dataflow facts attach — plus scope qualification -//! and change detection. +//! Locations in the IR that dataflow reasoning refers to: lattice anchors +//! (*where* facts attach), boundary locations, scope qualification, and change +//! detection. //! //! Following MLIR's terminology, a lattice fact is attached to a *lattice //! anchor*: sparse analyses anchor facts to [`SSAValue`]s; dense analyses @@ -64,6 +65,26 @@ pub enum DenseAnchor { impl LatticeAnchor for DenseAnchor {} +// =========================================================================== +// Boundary locations +// =========================================================================== + +/// Where a graph port sits on its owning statement's boundary. +/// +/// This is a **location**, not a value mapping: the owning statement's +/// dialect rule translates port/index into its operands, captures, or +/// results — for values (forward) and demand (backward) alike. Unlike a +/// [`LatticeAnchor`] no fact attaches here; it is what +/// [`BodyTopology::port_boundary`](crate::BodyTopology::port_boundary) hands +/// a rule so it can reach the statement owning a port. +#[derive(Clone, Copy, Debug)] +pub struct PortBoundary { + /// The statement that owns the graph. + pub owner: Statement, + /// Which boundary slot this port occupies. + pub index: usize, +} + // =========================================================================== // Scope qualification // =========================================================================== @@ -71,11 +92,12 @@ impl LatticeAnchor for DenseAnchor {} /// An anchor or owner qualified by the scope/context it belongs to. /// /// Framework-level summary keys are never bare anchors: the same [`SSAValue`] -/// or [`Block`] under two scopes (two stages, two analyzed cfgs, two call -/// contexts) is two distinct facts, so keys carry their scope. CFG-level -/// analyses use `(CompileStage, CFG)` as the scope; interprocedural -/// analyses generalize `K` to a call-context key (the backward analogue of the -/// forward engine's context-qualified value keys). +/// or [`Block`] under two scopes (two stages, two analyzed bodies, two call +/// contexts) is two distinct facts, so keys carry their scope. Body-level +/// analyses use [`BodyScope`](crate::BodyScope) — `(CompileStage, Body)` — as +/// the scope; interprocedural analyses generalize `K` to a call-context key +/// (the backward analogue of the forward engine's context-qualified value +/// keys). #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Scoped { pub scope: K, diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index 42f9590f62..5653244389 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -1,14 +1,11 @@ -//! Dataflow fact vocabulary: anchors (*where* facts attach), the polymorphic -//! fact stores, and CFG topology enumeration. Fixpoint clients use these, -//! but they are dataflow vocabulary, not the convergence driver itself. +//! Dataflow fact vocabulary: anchors and locations (*where* facts attach) +//! plus the polymorphic fact stores. Fixpoint clients use these, but they are +//! dataflow vocabulary, not the convergence driver itself — and not IR +//! queries either: the body shape an analysis enumerates before it runs is +//! [`core::topology`](crate::core::topology). pub(crate) mod anchor; pub(crate) mod store; -pub(crate) mod topology; -pub use anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; +pub use anchor::{Change, DenseAnchor, LatticeAnchor, PortBoundary, ProgramPoint, Scoped}; pub use store::{DenseBlockStore, DensePointStore, FactStore, ScopedSparseStore, SparseStore}; -pub use topology::{ - BlockTopology, BodyTopology, CFGTopology, GraphTopology, PortBoundary, body_topology, - cfg_topology, -}; diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index fe6da09d79..6b52bde44a 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -75,6 +75,9 @@ pub use self::core::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use self::core::{EnvIndex, EnvStackStore, Store}; pub use self::core::{FunctionEntry, InterpDispatch, Interpretable}; pub use self::core::{InterpreterError, StageQuery}; +// The body-shape IR query: what blocks/graphs a body contains, which +// statements feed a block's parameters, and where a graph port sits. +pub use self::core::{BlockTopology, BodyTopology, GraphTopology, body_topology}; // The shared, direction-neutral frame protocol (`Frame`/`FrameEngine`/ // `FrameEffect`/`drive_frames`) plus the forward frame-driver capability surfaces. pub use self::core::{ @@ -100,7 +103,7 @@ pub use engines::sparse_forward::{ }; // Sparse backward engine (`Sem = StrongDemand`). pub use engines::sparse_backward::{ - BackwardAnalysisState, CFGScope, DemandFrame, DemandInterp, DemandSummary, + BackwardAnalysisState, BodyScope, DemandFrame, DemandInterp, DemandSummary, SparseBackwardDriver, SparseBackwardEffect, SparseBackwardInterp, SparseBackwardInterpreter, SparseBackwardProfile, SparseBackwardTransfer, }; @@ -112,13 +115,12 @@ pub use engines::dense_backward::{ DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, SuccessorEdge, }; -// Lattice anchors (*where* facts attach), scope qualification, the polymorphic -// fact stores, and cfg topology enumeration. Anchor family is a property of -// the solver shape; dispatch meaning lives in `semantics`. +// Lattice anchors (*where* facts attach), scope qualification, and the +// polymorphic fact stores. Anchor family is a property of the solver shape; +// dispatch meaning lives in `semantics`. pub use facts::{ - BlockTopology, BodyTopology, CFGTopology, Change, DenseAnchor, DenseBlockStore, - DensePointStore, FactStore, GraphTopology, LatticeAnchor, PortBoundary, ProgramPoint, Scoped, - ScopedSparseStore, SparseStore, body_topology, cfg_topology, + Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, LatticeAnchor, PortBoundary, + ProgramPoint, Scoped, ScopedSparseStore, SparseStore, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index d3d4810874..268d5aa527 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -978,7 +978,7 @@ mod demand { let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let topology = kirin_interpreter::cfg_topology(info, &cfg); + let topology = kirin_interpreter::body_topology(info, kirin_interpreter::Body::CFG(cfg)); for block in &topology.blocks { for &stmt in &block.stmts { if let Some(value) = select(stmt.definition(info)) { @@ -1297,7 +1297,7 @@ mod dense { let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let topology = kirin_interpreter::cfg_topology(info, &cfg); + let topology = kirin_interpreter::body_topology(info, kirin_interpreter::Body::CFG(cfg)); for block in &topology.blocks { for &stmt in &block.stmts { if select(stmt.definition(info)) { From 8bc66dfe71159b8f6ad0c66f71b480ac1248e79c Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 30 Jul 2026 16:50:47 -0400 Subject: [PATCH 08/21] Refactored the DenseBackwardInterp trait to use point_state and point_state_mut methods for state management. - Introduced the DenseBackwardState trait to handle state renaming and forgetting. - Updated the LiveSet implementation to conform to the DenseBackwardState contract. - Modified various interpreters and frames to utilize the new DenseBackwardState trait. - Enhanced documentation to clarify the role of DenseBackwardState in the context of classic liveness and dense backward analysis. - Added tests to validate the behavior of the new forward dataflow engine with constant propagation. --- AGENTS.md | 2 +- Cargo.lock | 1 + Cargo.toml | 1 + .../src/engines/dense_backward/interp.rs | 111 ++++-- .../src/engines/dense_backward/mod.rs | 4 +- .../src/engines/sparse_backward/interp.rs | 42 +- crates/kirin-interpreter/src/lib.rs | 20 +- crates/kirin-liveness/src/live.rs | 25 +- crates/kirin-scf/src/interpreter.rs | 32 +- docs/design/interpreter/index.md | 16 +- example/toy-lang/src/interpreter/frame.rs | 4 +- tests/body_kinds.rs | 363 ++++++++++++++++-- 12 files changed, 509 insertions(+), 112 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4515247a56..65d2f942a7 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` insert/remove point facts); 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`. 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/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` diff --git a/Cargo.lock b/Cargo.lock index ede9dbd3e9..c8c281304f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,6 +710,7 @@ dependencies = [ "kirin-chumsky", "kirin-cmp", "kirin-constant", + "kirin-constprop", "kirin-function", "kirin-interpreter", "kirin-ir", diff --git a/Cargo.toml b/Cargo.toml index 0788d53acc..3b471d120d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,7 @@ kirin-bitwise = { workspace = true } kirin-cf = { workspace = true } kirin-cmp = { workspace = true } kirin-constant = { workspace = true } +kirin-constprop = { workspace = true } kirin-function = { workspace = true } kirin-scf = { workspace = true } kirin-interpreter = { workspace = true, features = ["derive"] } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 22493587f1..37c9445962 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -19,9 +19,9 @@ //! - **[`DenseBackwardTransfer`]** is the [`Interp`] delegate: pipeline access, //! the real dispatch location, and the *current point state* the dialect //! rules transform through the shape-generic -//! [`DenseBackwardInterp::insert_fact`] / [`DenseBackwardInterp::remove_fact`] -//! (liveness rules use the [`ClassicLivenessInterp`] `gen_live`/`kill_def` -//! spellings). +//! [`DenseBackwardInterp::point_state_mut`] (liveness rules use the +//! [`ClassicLivenessInterp`] `gen_live`/`kill_def` spellings, which is where +//! "a state is a set of values" lives — the engine never assumes it). //! - the **[`StandardFixpointInterpreter`]** driver owns the block-boundary //! summaries ([`BlockLiveness`], keyed by [`Scoped`] blocks), the block //! worklist, and [`BackwardSummaryDeps`] (successor changed → reanalyse @@ -87,10 +87,35 @@ pub enum DenseBackwardEffect { Push { frame: F }, } -/// The point-state contract: a dense backward state is a set of SSA values. +/// How a dense backward state moves across a control edge or out of a scope. /// -/// Implemented by the analysis's state type (e.g. `kirin_liveness::LiveSet`); -/// the join for merge points comes from [`Lattice`] (set union for liveness). +/// A dense backward state names its facts by [`SSAValue`]. Crossing an edge +/// renames them — the target's parameters become the edge's arguments; leaving +/// a scope drops them. Neither operation says what a fact *is*, which is why +/// this is the only state contract the engine and the dialect frames need: +/// [`Lattice`] merges at join points, this moves facts between vocabularies. +/// +/// Pass-through renaming (keep facts the rename does not cover) is the caller's +/// choice, spelled `state.rename(p, a).join(&state.forget(p))` — the CFG edge +/// transfer wants it, a loop back-edge does not. +pub trait DenseBackwardState: Lattice + Sized { + /// Only the facts named by `params`, renamed to the matching `args`. + /// + /// A `params[i]` with no `args[i]` contributes nothing. + fn rename(&self, params: &[SSAValue], args: &[SSAValue]) -> Self; + + /// Everything except the facts named by `values`. + fn forget(&self, values: &[SSAValue]) -> Self; +} + +/// The point-state contract of [`ClassicLiveness`]: its state is a set of live +/// SSA values. +/// +/// This is *semantics*, not shape — it backs +/// [`gen_live`](ClassicLivenessInterp::gen_live) / +/// [`kill_def`](ClassicLivenessInterp::kill_def) and is required by nothing in +/// the engine or the frames. A different dense-backward key brings its own +/// state contract and implements only [`DenseBackwardState`]. pub trait PointFacts { /// Insert `value`; `true` if newly added. fn insert(&mut self, value: SSAValue) -> bool; @@ -116,11 +141,15 @@ pub trait DenseBackwardInterp: /// [`DenseBackwardEffect::Push`]. Ordinary dialects never name it. type Frame; - /// Insert `value` into the current point state. - fn insert_fact(&mut self, value: impl Into) -> Result<(), Self::Error>; + /// The point state being transformed: the state *after* the statement on + /// entry to a rule, *before* it on exit. + /// + /// The engine hands the state over opaquely; how a rule transforms it is + /// the semantics' business. + fn point_state(&self) -> &Self::Value; - /// Remove `value` from the current point state. - fn remove_fact(&mut self, value: impl Into) -> Result<(), Self::Error>; + /// The point state being transformed, mutably. + fn point_state_mut(&mut self) -> &mut Self::Value; } /// [`ClassicLiveness`]'s helper vocabulary on top of the shape-generic @@ -132,16 +161,22 @@ pub trait DenseBackwardInterp: /// Pinned to `Semantics = ClassicLiveness` via the supertrait (rustc /// elaborates supertraits, so rules bounding `I: ClassicLivenessInterp` need /// no extra clauses), and blanket-implemented for every classic-liveness -/// dense-backward engine. -pub trait ClassicLivenessInterp: DenseBackwardInterp + Interp { +/// dense-backward engine. [`PointFacts`] rides in the same supertrait as an +/// associated-type bound for the same reason: elaboration means liveness rules +/// inherit it and never spell it. +pub trait ClassicLivenessInterp: + DenseBackwardInterp + Interp +{ /// Gen: mark `value` live at the current point (a use). fn gen_live(&mut self, value: impl Into) -> Result<(), Self::Error> { - self.insert_fact(value) + self.point_state_mut().insert(value.into()); + Ok(()) } /// Kill: remove `value` (a definition) from the current point state. fn kill_def(&mut self, value: impl Into) -> Result<(), Self::Error> { - self.remove_fact(value) + self.point_state_mut().remove(value.into()); + Ok(()) } /// The classic (weak) liveness transfer for an ordinary statement: kill @@ -161,8 +196,10 @@ pub trait ClassicLivenessInterp: DenseBackwardInterp + Interp ClassicLivenessInterp for I where - I: DenseBackwardInterp + Interp +impl ClassicLivenessInterp for I +where + I: DenseBackwardInterp + Interp, + I::Value: PointFacts, { } @@ -241,20 +278,18 @@ where impl<'ir, S, V, E, F, Sem> DenseBackwardInterp for DenseBackwardTransfer<'ir, S, V, E, F, Sem> where S: StageMeta, - V: Clone + PointFacts, + V: Clone, E: From, Sem: DenseBackwardSemantic, { type Frame = F; - fn insert_fact(&mut self, value: impl Into) -> Result<(), E> { - self.state.insert(value.into()); - Ok(()) + fn point_state(&self) -> &V { + &self.state } - fn remove_fact(&mut self, value: impl Into) -> Result<(), E> { - self.state.remove(value.into()); - Ok(()) + fn point_state_mut(&mut self) -> &mut V { + &mut self.state } } @@ -429,7 +464,7 @@ pub trait DenseBackwardFrameDriver: Interp DenseBackwardFrameDriver for DenseBackwardDriver<'ir, S, V, E, F, Sem> where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + PointFacts, + V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, { @@ -516,21 +551,15 @@ where continue; }; let params = query::block_params(self.inner().pipeline(), stage, edge.target)?; - let mut mapped = V::bottom(); - for value in summary.live_in.values() { - match params.iter().position(|param| *param == value) { - Some(index) => { - if let Some(arg) = edge.args.get(index) { - mapped.insert(*arg); - } - } - // A live-in that is not a parameter of the successor is a - // dominated direct cross-block use: pass it through. - None => { - mapped.insert(value); - } - } - } + // The successor's entry state in this block's vocabulary: its + // parameters renamed to the edge's arguments, joined with the + // facts the rename does not cover — live-ins that are not + // parameters are dominated direct cross-block uses and pass + // through unchanged. + let entry = &summary.live_in; + let mapped = entry + .rename(¶ms, &edge.args) + .join(&entry.forget(¶ms)); out = out.join(&mapped); } @@ -557,7 +586,7 @@ impl<'ir, S, V, E, F, Sem> > for DenseBackwardSemantics where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + PointFacts, + V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, F: DenseFrameBuild, @@ -676,7 +705,7 @@ where impl<'ir, S, V, E, F, Sem> DenseBackwardInterpreter<'ir, S, V, E, F, Sem> where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + PointFacts, + V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, F: Frame, Completion = DenseBackwardCompletion> diff --git a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs index e440ba0b94..5227c7e96c 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs @@ -9,6 +9,6 @@ pub use frames::{DenseBlockFrame, DenseBlockMode, DenseFrameBuild, StandardDense pub use interp::{ BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardTransfer, PointFacts, - SuccessorEdge, + DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, + PointFacts, SuccessorEdge, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index 25a838139b..bc8cac4310 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -100,8 +100,16 @@ pub trait SparseBackwardInterp: /// The converged fact for `value` (bottom if absent). fn fact(&self, value: impl Into) -> Result; - /// Raise `value`'s fact to ⊤ (buffered; returned by [`effect`](Self::effect)). - fn raise_fact(&mut self, value: impl Into) -> Result<(), Self::Error>; + /// Merge `fact` into `value`'s fact (buffered; returned by + /// [`effect`](Self::effect)). + /// + /// The engine moves the fact and never inspects it — which element of the + /// lattice a rule raises is the semantics' business, not the shape's. + fn raise_fact( + &mut self, + value: impl Into, + fact: Self::Value, + ) -> Result<(), Self::Error>; /// Drain the raised-fact buffer into this rule's effect. fn effect(&mut self) -> Self::Effect; @@ -122,10 +130,17 @@ pub trait SparseBackwardInterp: /// Pinned to `Semantics = StrongDemand` via the supertrait (rustc elaborates /// supertraits, so rules bounding `I: DemandInterp` need no extra clauses), /// and blanket-implemented for every strong-demand sparse-backward engine. -pub trait DemandInterp: SparseBackwardInterp + Interp { +/// [`HasTop`] rides in the same supertrait as an associated-type bound rather +/// than in a `where` clause, for the same reason: elaboration means rules +/// bounding `I: DemandInterp` inherit it and never spell it. (A +/// `where Self::Value: HasTop` on the trait would *not* be elaborated — every +/// rule would have to repeat it.) +pub trait DemandInterp: + SparseBackwardInterp + Interp +{ /// Raise `value`'s demand (a demanded value carries the ⊤ fact). fn demand(&mut self, value: impl Into) -> Result<(), Self::Error> { - self.raise_fact(value) + self.raise_fact(value, Self::Value::top()) } /// `true` iff `value` carries a non-bottom demand fact. @@ -161,7 +176,12 @@ pub trait DemandInterp: SparseBackwardInterp + Interp } } -impl DemandInterp for I where I: SparseBackwardInterp + Interp {} +impl DemandInterp for I +where + I: SparseBackwardInterp + Interp, + I::Value: HasTop, +{ +} // =========================================================================== // SparseBackwardTransfer — the summary-free Interp delegate @@ -324,7 +344,7 @@ impl DemandFrame { impl<'ir, S, V, E, Sem> Frame> for DemandFrame where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { @@ -401,7 +421,7 @@ where impl<'ir, S, V, E, Sem> SparseBackwardInterp for SparseBackwardDriver<'ir, S, V, E, Sem> where S: StageMeta + StageQuery, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { @@ -416,9 +436,9 @@ where .unwrap_or_else(V::bottom)) } - fn raise_fact(&mut self, value: impl Into) -> Result<(), E> { + fn raise_fact(&mut self, value: impl Into, fact: V) -> Result<(), E> { let value = value.into(); - self.inner_mut().demands.push((value, V::top())); + self.inner_mut().demands.push((value, fact)); Ok(()) } @@ -452,7 +472,7 @@ impl<'ir, S, V, E, Sem> > for SparseBackwardSemantics where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { @@ -591,7 +611,7 @@ where impl<'ir, S, V, E, Sem> SparseBackwardInterpreter<'ir, S, V, E, Sem> where S: StageMeta + StageQuery + InterpDispatch>, - V: Clone + PartialEq + Lattice + HasBottom + HasTop, + V: Clone + PartialEq + Lattice + HasBottom, E: From, Sem: SparseBackwardSemantic, { diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 6b52bde44a..f45841b05b 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -40,7 +40,7 @@ //! per semantic key (and [`FunctionEntry`] for callable statements). A rule //! receives the engine `interp` directly. Shape-generic mechanics live on //! the engine traits (read/write on [`SparseForwardInterp`]; -//! fact/raise-fact on [`SparseBackwardInterp`]; insert/remove point facts on +//! fact/raise-fact on [`SparseBackwardInterp`]; opaque point-state access on //! [`DenseBackwardInterp`]); semantics-specific vocabulary lives in helper //! traits — demand rules bind [`DemandInterp`] //! (`demand`/`is_demanded`/`demand_uses_if_observable`), classic-liveness rules bind @@ -111,8 +111,9 @@ pub use engines::sparse_backward::{ pub use engines::dense_backward::{ BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardTransfer, DenseBlockFrame, - DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, SuccessorEdge, + DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, + DenseBlockFrame, DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, + SuccessorEdge, }; // Lattice anchors (*where* facts attach), scope qualification, and the @@ -169,11 +170,12 @@ pub mod engine { AbstractFrameDriver, AbstractInterpreter, BlockFrame, CFGFrame, CallContext, CallFrame, Callee, Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBlockFrame, DenseFrameBuild, DiGraphFrame, Env, - ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, FrameDriver, - FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, InterpreterError, Linker, - SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, SparseForwardInterp, - SparseForwardInterpreter, StandardAbstractFrame, StandardDenseBackwardFrame, StandardFrame, - UnGraphEntry, WideningStrategy, drive_frames, expect_single, + DenseBackwardInterpreter, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, + DiGraphFrame, Env, ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, + FrameDriver, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, + InterpreterError, Linker, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, + SparseForwardInterp, SparseForwardInterpreter, StandardAbstractFrame, + StandardDenseBackwardFrame, StandardFrame, UnGraphEntry, WideningStrategy, drive_frames, + expect_single, }; } diff --git a/crates/kirin-liveness/src/live.rs b/crates/kirin-liveness/src/live.rs index 2e062427bb..5d29b8a37b 100644 --- a/crates/kirin-liveness/src/live.rs +++ b/crates/kirin-liveness/src/live.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; -use kirin_interpreter::PointFacts; +use kirin_interpreter::{DenseBackwardState, PointFacts}; use kirin_ir::{HasBottom, HasTop, Lattice, SSAValue}; /// The two-point liveness lattice: `Dead` (bottom) ⊑ `Live` (top). @@ -140,7 +140,28 @@ impl HasBottom for LiveSet { } } -/// The dense backward point-state contract: gen/kill mutate the set. +/// How a live set moves between vocabularies. Renaming keeps only the values +/// the rename covers, so a caller that wants pass-through joins +/// [`forget`](DenseBackwardState::forget) itself. +impl DenseBackwardState for LiveSet { + fn rename(&self, params: &[SSAValue], args: &[SSAValue]) -> Self { + let mut out = LiveSet::new(); + for (index, param) in params.iter().enumerate() { + if self.contains(*param) + && let Some(arg) = args.get(index) + { + out.insert(*arg); + } + } + out + } + + fn forget(&self, values: &[SSAValue]) -> Self { + self.iter().filter(|v| !values.contains(v)).collect() + } +} + +/// The classic-liveness point-state contract: gen/kill mutate the set. impl PointFacts for LiveSet { fn insert(&mut self, value: SSAValue) -> bool { LiveSet::insert(self, value) diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index b0a6401b2c..ac4d5fec69 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -33,8 +33,8 @@ use kirin_interpreter::dialect::{ use kirin_interpreter::{ AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BlockFrame, CallContext, Completion, ConcreteInterpreter, DenseBackwardCompletion, - DenseBackwardFrameDriver, DenseBlockFrame, DenseFrameBuild, EnvIndex, FrameBuild, FrameDriver, - FrameEffect, PointFacts, SparseForwardTransfer, + DenseBackwardFrameDriver, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, EnvIndex, + FrameBuild, FrameDriver, FrameEffect, SparseForwardTransfer, }; use crate::{For, ForLoopValue, If, Yield}; @@ -400,19 +400,18 @@ impl DenseScfForFrame { } } + /// The loop-carried estimate: the state after the loop, plus the body's + /// carried parameters renamed across the back-edge to the slots that yield + /// them. Unlike a CFG edge this does *not* pass anything through — the + /// body's own vocabulary does not escape backwards through the back-edge. + /// + /// `params[0]` is the induction variable, which no yield slot feeds. fn carry(&self, body_entry: &V) -> V where - V: Clone + Lattice + PointFacts, + V: Clone + Lattice + DenseBackwardState, { - let mut next = self.seed.clone().expect("seed captured"); - for (index, param) in self.params.iter().skip(1).enumerate() { - if body_entry.contains(*param) - && let Some(slot) = self.yields.get(index) - { - next.insert(*slot); - } - } - next + let seed = self.seed.clone().expect("seed captured"); + seed.join(&body_entry.rename(&self.params[1..], &self.yields)) } pub fn step_into( @@ -422,7 +421,7 @@ impl DenseScfForFrame { where I: DenseBackwardFrameDriver, F: DenseFrameBuild + BuildDenseScfFor, - V: Clone + PartialEq + Lattice + PointFacts, + V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, { if self.seed.is_none() { @@ -448,7 +447,7 @@ impl DenseScfForFrame { where I: DenseBackwardFrameDriver, F: DenseFrameBuild + BuildDenseScfFor, - V: Clone + PartialEq + Lattice + PointFacts, + V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, { match completion { @@ -463,10 +462,7 @@ impl DenseScfForFrame { } else { // Stable: the final body entry, minus the body-local // parameters, is the state before the loop. - let mut before = body_entry; - for param in &self.params { - before.remove(*param); - } + let before = body_entry.forget(&self.params); interp.replace_state(before); Ok(FrameEffect::Complete(DenseBackwardCompletion::Structured)) } diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 96ef9fd20d..9cb6d6caa9 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -89,14 +89,22 @@ conflict): the shape-generic `SparseBackwardInterp` (`fact`/`raise_fact`/`effect` plus the block-topology queries) serves any sparse-backward key, and `DemandInterp` (pinned to `StrongDemand` via its supertrait) adds the demand - vocabulary. Rules bind `DemandInterp`: read converged facts (`is_demanded`), + vocabulary. `raise_fact` takes the lattice element to merge, so the shape + moves facts without inspecting them; ⊤ is `demand`'s business, and `HasTop` + rides on `DemandInterp`'s supertrait so rules never spell it. Rules bind `DemandInterp`: read converged facts (`is_demanded`), raise demands (`demand`), and end with `interp.effect()` (= `SparseBackwardEffect`); ordinary dialects are the one-liner `interp.demand_uses_if_observable(self)` (purity-aware neededness via `IsPure`). - `ClassicLiveness` (on `DenseBackwardShape`) — likewise split: the - shape-generic `DenseBackwardInterp` (`insert_fact`/`remove_fact` point-state - mechanics) serves any dense-backward key, and `ClassicLivenessInterp` adds - liveness's spellings. Rules bind `ClassicLivenessInterp`: `gen_live`/ + shape-generic `DenseBackwardInterp` (`point_state`/`point_state_mut`, which + hand the state over opaquely) serves any dense-backward key, and + `ClassicLivenessInterp` adds liveness's spellings — `PointFacts` ("a state is + a set of live values") is that key's contract, required by no engine and no + frame. What the shape *does* need of a state is `Lattice` for merges and + `DenseBackwardState` (`rename`/`forget`) for crossing edges and leaving + scopes; the parameter-to-argument substitution the CFG edge transfer and + `scf.for`'s back-edge both perform lives in those two methods, implemented + for `LiveSet` in `kirin-liveness`. Rules bind `ClassicLivenessInterp`: `gen_live`/ `kill_def`; ordinary dialects (and calls — purity is irrelevant to dense sets) are `interp.gen_uses_kill_defs(self)`; CFG terminators name their edges (`Edges`, in `DenseBackwardEffect`), structured dialects push dense frames diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 1de9431d8d..657e3c7216 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -188,7 +188,7 @@ where // Dense backward (classic per-point liveness) // =========================================================================== -use kirin_interpreter::PointFacts; +use kirin_interpreter::DenseBackwardState; use kirin_interpreter::engine::{ DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBlockFrame, DenseFrameBuild, }; @@ -225,7 +225,7 @@ impl BuildDenseScfFor for ToyDenseBackwardFrame { impl Frame for ToyDenseBackwardFrame where I: DenseBackwardFrameDriver>, - V: Clone + PartialEq + Lattice + PointFacts, + V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, { type Completion = DenseBackwardCompletion; diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 2a98b3a649..c9a9ad54db 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -16,8 +16,15 @@ //! walker), returns bubbling through dialect frames to the nearest //! `CallFrame`, and the callable-UnGraph policy hook (with and without a //! policy). +//! +//! A final section runs the *forward dataflow* engine +//! (`SparseForwardInterpreter`, instantiated at the constant-propagation +//! lattice) over the same body vocabulary: `CFG`/`Block` bodies analyze, and +//! `DiGraph`/`UnGraph` bodies are asserted to report `NoDefaultWalker` +//! because no abstract graph walker exists yet. use std::collections::VecDeque; +use std::hash::Hash; use kirin::prelude::*; use kirin_arith::{ @@ -25,12 +32,14 @@ use kirin_arith::{ }; use kirin_cmp::Cmp; use kirin_constant::Constant; +use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_function::Lexical; use kirin_interpreter::{ - BlockFrame, Body, CFGFrame, CallFrame, Completion, ConcreteInterpreter, DiGraphFrame, Env, - EnvIndex, Frame, FrameBuild, FrameDriver, FrameEffect, FunctionEntry, Interpretable, - InterpreterError, SameStageLinker, SparseForwardEffect, StandardFrame, UnGraphEntry, - expect_single, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, + AbstractFrameDriver, BlockFrame, Body, CFGFrame, CallContext, CallFrame, Completion, + ConcreteInterpreter, DiGraphFrame, Env, EnvIndex, Frame, FrameBuild, FrameDriver, FrameEffect, + FunctionEntry, Interpretable, InterpreterError, SameStageLinker, SparseForwardEffect, + SparseForwardInterp, SparseForwardInterpreter, StandardFrame, UnGraphEntry, expect_single, }; use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -59,6 +68,13 @@ impl From for TestError { Self::DivisionByZero } } +// `ConstPropValue: From` is infallible, so the abstract engine's +// `TryFrom` conversion error is uninhabited. +impl From for TestError { + fn from(never: std::convert::Infallible) -> Self { + match never {} + } +} type L = StageInfo; type Engine<'ir> = @@ -86,10 +102,7 @@ fn run(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result i64; stage @test fn @main() -> i64; @@ -106,8 +119,11 @@ specialize @test fn @main() -> i64 { ret %r; } } -"#, - ); +"#; + +#[test] +fn cfg_main_calls_digraph_function() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); assert_eq!(run(&pipeline, "main", &[]).unwrap(), 5); } @@ -122,10 +138,7 @@ specialize @test fn @main() -> i64 { /// pushed `DiGraphFrame` runs in the *pusher's* activation, and its /// `Finished` yields land in the pushing statement's `Push` result slots /// rather than in call-return slots. -#[test] -fn cfg_statement_pushes_digraph_body() { - let pipeline = parse( - r#" +const NESTED_DIGRAPH_PROGRAM: &str = r#" stage @test fn @main() -> i64; specialize @test fn @main() -> i64 { @@ -139,8 +152,11 @@ specialize @test fn @main() -> i64 { ret %r; } } -"#, - ); +"#; + +#[test] +fn cfg_statement_pushes_digraph_body() { + let pipeline = parse(NESTED_DIGRAPH_PROGRAM); assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); } @@ -154,10 +170,7 @@ specialize @test fn @main() -> i64 { /// "linear function frame"; the `CallFrame` parent is what makes this walk a /// function body. The block exits with `Return`, which the `CallFrame` /// validates and consumes. -#[test] -fn linear_block_callable() { - let pipeline = parse( - r#" +const BLOCK_CALLABLE_PROGRAM: &str = r#" stage @test fn @ladd(i64, i64) -> i64; stage @test fn @main() -> i64; @@ -174,8 +187,11 @@ specialize @test fn @main() -> i64 { ret %r; } } -"#, - ); +"#; + +#[test] +fn linear_block_callable() { + let pipeline = parse(BLOCK_CALLABLE_PROGRAM); assert_eq!(run(&pipeline, "main", &[]).unwrap(), 42); } @@ -703,3 +719,306 @@ fn ungraph_without_policy_reports_no_default_walker() { "expected NoDefaultWalker(UnGraph), got {error:?}" ); } + +// =========================================================================== +// 9. The same bodies under forward dataflow (abstract interpretation). +// =========================================================================== + +// The tests above pin *concrete* traversal. These pin what the forward +// dataflow engine does with the same closed `Body` vocabulary, at the +// constant-propagation lattice: +// +// - `CFG` and `Block` bodies analyze: `SparseForwardInterpreter` seeds the +// entry block (`Body::CFG` → its entry, `Body::Block` → itself) and the +// standard abstract frames walk it, joining branch arms and summarizing +// calls interprocedurally. +// - `DiGraph` and `UnGraph` bodies are **refused**. There is no abstract +// graph walker: the engine rejects a graph callable body outright, and the +// abstract frame type has no variant able to walk a pushed graph. Both +// refusals are asserted rather than left implicit, so adding an abstract +// digraph walker fails these tests and forces them to be updated. + +/// Summary key of the constant-propagation context policy. +type CpKey = >::Key; + +/// Total abstract frame for the graph language: the standard abstract +/// traversal, plus a variant recording the absence of a graph walker. +/// +/// The language's `Interpretable` rule bounds `I::Frame: FrameBuild<..>` +/// because its `graph_eval` variant pushes a `DiGraphFrame`, so an abstract +/// frame type for this language must satisfy that bound even though the +/// abstract engine itself only ever calls `AbstractFrameBuild`. The concrete +/// walkers cannot be embedded here — their completion type is +/// `Completion`, not `AbstractCompletion` — so the `FrameBuild` hooks +/// build `NoWalker`, which reports the gap if it is ever stepped instead of +/// silently running concrete traversal over lattice values. +enum GraphAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + /// No abstract walker exists for this body kind; carries the reason. + NoWalker(&'static str), +} + +impl AbstractFrameBuild for GraphAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + GraphAbstractFrame::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + GraphAbstractFrame::Call(frame) + } +} + +impl FrameBuild for GraphAbstractFrame { + fn from_block(_: BlockFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract walker for a concrete Block frame") + } + fn from_cfg(_: CFGFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract walker for a concrete CFG frame") + } + fn from_call(_: CallFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract walker for a concrete call boundary") + } + fn from_digraph(_: DiGraphFrame) -> Self { + GraphAbstractFrame::NoWalker("no abstract digraph walker") + } +} + +impl Frame for GraphAbstractFrame +where + I: AbstractFrameDriver + + SparseForwardInterp>, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step(self, interp: &mut I) -> Result, I::Error> { + match self { + GraphAbstractFrame::Block(frame) => frame.step_into::(interp), + GraphAbstractFrame::Call(frame) => frame.step_into::(interp), + GraphAbstractFrame::NoWalker(reason) => { + Err(I::Error::from(InterpreterError::Custom(reason))) + } + } + } + + fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + match self { + GraphAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), + GraphAbstractFrame::Call(frame) => frame.resume_done_into::(), + GraphAbstractFrame::NoWalker(reason) => { + Err(I::Error::from(InterpreterError::Custom(reason))) + } + } + } + + fn resume( + self, + completion: Self::Completion, + interp: &mut I, + ) -> Result, I::Error> { + match self { + GraphAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), + GraphAbstractFrame::Call(frame) => frame.resume_into::(completion), + GraphAbstractFrame::NoWalker(reason) => { + Err(I::Error::from(InterpreterError::Custom(reason))) + } + } + } +} + +type AbstractEngine<'ir> = SparseForwardInterpreter< + 'ir, + L, + ConstPropValue, + TestError, + SameStageLinker, + ConstPropContext, + GraphAbstractFrame, +>; + +/// Run constant propagation from `function` and return its inferred return +/// value at the fixpoint. +fn analyze( + pipeline: &Pipeline, + function: &str, + args: &[ConstPropValue], +) -> Result { + let mut analysis: AbstractEngine<'_> = + SparseForwardInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(analysis.analyze_by_name("test", function, args.iter().cloned())?) +} + +/// A CFG body whose branch condition is an *unknown* argument, so neither +/// successor can be decided: the abstract block frame explores both and joins +/// their returns. Identical arms fold to a constant; differing arms join to +/// `Top`. +const CFG_BRANCH_PROGRAM: &str = r#" +stage @test fn @same(i64) -> i64; +stage @test fn @diff(i64) -> i64; +stage @test fn @caller(i64) -> i64; + +specialize @test fn @same(i64) -> i64 { + ^entry(%c: i64) { + cond_br %c then=^t() else=^f(); + } + ^t() { + %a = constant 7 -> i64; + ret %a; + } + ^f() { + %b = constant 7 -> i64; + ret %b; + } +} + +specialize @test fn @diff(i64) -> i64 { + ^entry(%c: i64) { + cond_br %c then=^t() else=^f(); + } + ^t() { + %a = constant 7 -> i64; + ret %a; + } + ^f() { + %b = constant 9 -> i64; + ret %b; + } +} + +specialize @test fn @caller(i64) -> i64 { + ^entry(%c: i64) { + %r = call.named @same(%c) -> i64; + %one = constant 1 -> i64; + %s = add %r, %one -> i64; + ret %s; + } +} +"#; + +/// `Body::CFG` under forward dataflow: the entry block is seeded, the +/// undecided `cond_br` explores both successors, and the returns are joined. +#[test] +fn abstract_cfg_body_joins_branch_arms() { + let pipeline = parse(CFG_BRANCH_PROGRAM); + // Both arms return the same constant, so the join stays precise even + // though the condition is unknown. + assert_eq!( + analyze(&pipeline, "same", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Const(7) + ); + // Differing arms join to Top — evidence both were actually explored + // rather than one being picked. + assert_eq!( + analyze(&pipeline, "diff", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Top + ); +} + +/// The interprocedural path: a CFG-bodied caller summarizes a CFG-bodied +/// callee and folds the returned summary into its own arithmetic. +#[test] +fn abstract_cfg_body_summarizes_call() { + let pipeline = parse(CFG_BRANCH_PROGRAM); + assert_eq!( + analyze(&pipeline, "caller", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Const(8) + ); +} + +/// `Body::Block` under forward dataflow: a single-block callable is seeded +/// directly as its own entry block (no CFG entry lookup), and is reached both +/// as an analysis root and as a summarized callee. +#[test] +fn abstract_block_body_callable() { + let pipeline = parse(BLOCK_CALLABLE_PROGRAM); + assert_eq!( + analyze( + &pipeline, + "ladd", + &[ConstPropValue::Const(40), ConstPropValue::Const(2)] + ) + .unwrap(), + ConstPropValue::Const(42) + ); + // Same body, reached through a call from a CFG-bodied caller. + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(42) + ); + // An unknown operand propagates through the block body to Top. + assert_eq!( + analyze( + &pipeline, + "ladd", + &[ConstPropValue::Top, ConstPropValue::Const(2)] + ) + .unwrap(), + ConstPropValue::Top + ); +} + +/// A callable `DiGraph` body has no abstract walker, so the engine refuses it +/// when seeding the function's entry rather than inventing a schedule — both +/// as an analysis root and as a summarized callee. +#[test] +fn abstract_digraph_callable_reports_no_default_walker() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + let root = analyze( + &pipeline, + "gadd", + &[ConstPropValue::Const(2), ConstPropValue::Const(3)], + ) + .unwrap_err(); + assert!( + matches!( + root, + TestError::Core(InterpreterError::NoDefaultWalker(Body::DiGraph(_))) + ), + "expected NoDefaultWalker(DiGraph) as an analysis root, got {root:?}" + ); + + let callee = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + callee, + TestError::Core(InterpreterError::NoDefaultWalker(Body::DiGraph(_))) + ), + "expected NoDefaultWalker(DiGraph) through a call, got {callee:?}" + ); +} + +/// A callable `UnGraph` body is refused on the same path. Note the concrete +/// escape hatch does *not* apply here: `FrameBuild::from_ungraph_entry` is a +/// concrete-frame hook, so an UnGraph policy supplied for execution buys +/// nothing under abstract interpretation. +#[test] +fn abstract_ungraph_callable_reports_no_default_walker() { + let pipeline = parse(UNGRAPH_PROGRAM); + let error = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + error, + TestError::Core(InterpreterError::NoDefaultWalker(Body::UnGraph(_))) + ), + "expected NoDefaultWalker(UnGraph), got {error:?}" + ); +} + +/// A *nested* graph body is refused too, by a different route: the statement's +/// rule pushes a walker, and the abstract frame type has none to give. The +/// callable cases above never reach a frame at all, so this is the only test +/// covering the pushed path. +#[test] +fn abstract_nested_digraph_reports_no_walker() { + let pipeline = parse(NESTED_DIGRAPH_PROGRAM); + let error = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + error, + TestError::Core(InterpreterError::Custom("no abstract digraph walker")) + ), + "expected the pushed-frame walker gap, got {error:?}" + ); +} From e6410e455aa02c41f28e75b7f169b6c7e585fbea Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 30 Jul 2026 17:16:10 -0400 Subject: [PATCH 09/21] refactor: introduce AbstractDiGraphFrame for dependency-ordered graph traversal - Added AbstractDiGraphFrame to handle graph bodies in a single pass. - Updated Owner enum to include Graph variant for executable graph bodies. - Enhanced SparseForwardInterpreter to support DiGraph bodies. - Modified tests to validate the new graph handling and ensure correct execution order. --- .../src/engines/sparse_forward/frames.rs | 218 +++++++++++++++++- .../src/engines/sparse_forward/interp.rs | 207 ++++++++++------- .../src/engines/sparse_forward/mod.rs | 4 +- crates/kirin-interpreter/src/lib.rs | 14 +- docs/design/interpreter/index.md | 31 ++- tests/body_kinds.rs | 142 +++++++----- 6 files changed, 469 insertions(+), 147 deletions(-) diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs index dff10b58e0..11f12a9d2f 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs @@ -18,13 +18,14 @@ //! recursion) stays atomic in the engine behind [`AbstractFrameDriver`]; frames //! only choose what to step next. +use std::collections::VecDeque; use std::hash::Hash; use std::marker::PhantomData; -use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; +use kirin_ir::{Block, CompileStage, DiGraph, Product, SSAValue, Statement}; use crate::{ - AbstractFrameDriver, CallEffect, Edge, EnvIndex, Frame, FrameEffect, InterpreterError, + AbstractFrameDriver, Body, CallEffect, Edge, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, }; @@ -47,6 +48,22 @@ pub enum AbstractCompletion { pub trait AbstractFrameBuild: Sized { fn from_block(frame: AbstractBlockFrame) -> Self; fn from_call(frame: AbstractCallFrame) -> Self; + + /// Embed the standard abstract digraph walker. + /// + /// Graph bodies are opt-in: a total abstract frame enum that carries no + /// [`AbstractDiGraphFrame`] inherits this rejection rather than pretending + /// to analyze one, the same way + /// [`FrameBuild::from_ungraph_entry`](crate::FrameBuild::from_ungraph_entry) + /// rejects a callable `UnGraph` without a compiler-supplied policy. + fn from_digraph(frame: AbstractDiGraphFrame) -> Result + where + E: From, + { + Err(E::from(InterpreterError::NoDefaultWalker(Body::DiGraph( + frame.graph(), + )))) + } } // =========================================================================== @@ -248,6 +265,194 @@ where } } +// =========================================================================== +// DiGraph frame: one dependency-ordered pass over a graph body +// =========================================================================== + +/// Abstract walker for a [`DiGraph`] body: bind the boundary ports, run the +/// node statements in dependency (topological) order, and complete +/// [`Finished`](AbstractCompletion::Finished) with the graph's declared yields. +/// +/// A single pass is **exact** for a DAG — there is no loop inside the graph, so +/// no widening happens here. Convergence pressure comes only from *outside*: +/// the owner's entry product is widened at +/// [`Owner`](crate::Owner) entry when a new call site raises it, and the whole +/// pass is re-run. +/// +/// The one substantive difference from the concrete +/// [`DiGraphFrame`](crate::DiGraphFrame) is call handling: a `Call` effect +/// pushes an [`AbstractCallFrame`], so the call goes through the engine's +/// interprocedural summarization protocol (`summarize_call`) instead of +/// descending into the callee. Descending would neither widen nor terminate on +/// recursion. +/// +/// Like the other frames, construction is pure — the walk plan is fetched and +/// the ports are bound on the first `step`, so a dialect frame can build one +/// without engine access. +pub struct AbstractDiGraphFrame { + stage: CompileStage, + index: EnvIndex, + graph: DiGraph, + /// Entry arguments not yet bound (bound on the first `step`). + pending: Option>, + /// Remaining schedule in dependency order; `None` until the first step. + schedule: Option>, + yields: Vec, + /// Result slots awaiting a pushed child frame's completion values. + resume_slots: Option>, + _marker: PhantomData (E, K)>, +} + +impl AbstractDiGraphFrame { + /// The graph body this frame walks. Available without the walking bounds so + /// [`AbstractFrameBuild::from_digraph`]'s rejecting default can name it. + pub fn graph(&self) -> DiGraph { + self.graph + } +} + +impl AbstractDiGraphFrame +where + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + /// Walk `graph`, binding `args` to its boundary ports on the first step. + pub fn new(stage: CompileStage, index: EnvIndex, graph: DiGraph, args: Product) -> Self { + Self { + stage, + index, + graph, + pending: Some(args), + schedule: None, + yields: Vec::new(), + resume_slots: None, + _marker: PhantomData, + } + } + + pub fn step_into( + mut self, + interp: &mut I, + ) -> Result>, E> + where + I: AbstractFrameDriver + + SparseForwardInterp, + F: AbstractFrameBuild, + { + // First step: fetch the walk plan and bind the boundary ports. + if let Some(args) = self.pending.take() { + let plan = interp.digraph_walk_plan(self.stage, self.graph)?; + if plan.ports.len() != args.len() { + return Err(E::from(InterpreterError::ProductArityMismatch { + expected: plan.ports.len(), + actual: args.len(), + })); + } + for (port, value) in plan.ports.iter().copied().zip(args) { + interp.env_write(self.index, SSAValue::from(port), value)?; + } + self.schedule = Some(plan.schedule.into()); + self.yields = plan.yields; + return Ok(FrameEffect::Continue(F::from_digraph(self)?)); + } + + let Some(statement) = self.schedule.as_mut().and_then(|s| s.pop_front()) else { + return self.finish::(interp); + }; + + match interp.run_statement(self.stage, statement, self.index)? { + SparseForwardEffect::Next => Ok(FrameEffect::Continue(F::from_digraph(self)?)), + SparseForwardEffect::Push { frame, results } => { + self.resume_slots = Some(results); + Ok(FrameEffect::Push { + parent: F::from_digraph(self)?, + child: frame, + }) + } + // Summarize, don't descend: the interprocedural fixpoint + // re-evaluates the callee under its own key. + SparseForwardEffect::Call(call) => { + let call_frame = AbstractCallFrame::new(self.stage, call, self.index); + Ok(FrameEffect::Push { + parent: F::from_digraph(self)?, + child: F::from_call(call_frame), + }) + } + SparseForwardEffect::Jump(_) | SparseForwardEffect::Branch(_) => { + Err(E::from(InterpreterError::CFGControlFlowInStructuredBody)) + } + SparseForwardEffect::Yield(_) => Err(E::from(InterpreterError::Custom( + "yield inside a digraph body (a digraph's outputs are its declared yields)", + ))), + SparseForwardEffect::Return(_) => Err(E::from(InterpreterError::Custom( + "return inside a digraph body", + ))), + } + } + + /// Schedule exhausted: read the declared yields out of the activation and + /// complete. The parent decides what the values mean — a graph **owner** + /// turns them into the function's return, a pushing statement binds them + /// into its result slots. + fn finish(self, interp: &mut I) -> Result>, E> + where + I: AbstractFrameDriver, + F: AbstractFrameBuild, + { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(AbstractCompletion::Finished(Some( + values, + )))) + } + + /// A pushed child finished without a payload (e.g. a summarized call whose + /// results are already written): resume the schedule. + pub fn resume_done_into(self) -> Result>, E> + where + F: AbstractFrameBuild, + { + Ok(FrameEffect::Continue(F::from_digraph(self)?)) + } + + pub fn resume_into( + mut self, + completion: AbstractCompletion, + interp: &mut I, + ) -> Result>, E> + where + I: AbstractFrameDriver, + F: AbstractFrameBuild, + { + match completion { + AbstractCompletion::Finished(Some(values)) => { + let slots = self.resume_slots.take().ok_or_else(|| { + E::from(InterpreterError::Custom( + "digraph resume without result slots", + )) + })?; + interp.write_results(self.index, &slots, values)?; + Ok(FrameEffect::Continue(F::from_digraph(self)?)) + } + // A nested push left via `return`. A digraph has no function-return + // convention, so this cannot be relayed. + AbstractCompletion::Finished(None) => Err(E::from(InterpreterError::Custom( + "return bubbled into a digraph body", + ))), + AbstractCompletion::FunctionDone => Err(E::from(InterpreterError::Custom( + "digraph frame resumed with a function completion", + ))), + AbstractCompletion::CFGBlock { .. } => Err(E::from(InterpreterError::Custom( + "digraph frame resumed with a CFG-block completion", + ))), + } + } +} + // =========================================================================== // Call frame: summarize a call (no descent — the interprocedural fixpoint // re-evaluates the callee). @@ -312,6 +517,7 @@ where pub enum StandardAbstractFrame { Block(AbstractBlockFrame), Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), } impl AbstractFrameBuild for StandardAbstractFrame { @@ -321,6 +527,9 @@ impl AbstractFrameBuild for StandardAbstractFrame { fn from_call(frame: AbstractCallFrame) -> Self { StandardAbstractFrame::Call(frame) } + fn from_digraph(frame: AbstractDiGraphFrame) -> Result { + Ok(StandardAbstractFrame::DiGraph(frame)) + } } impl Frame for StandardAbstractFrame @@ -337,6 +546,7 @@ where match self { StandardAbstractFrame::Block(frame) => frame.step_into::(interp), StandardAbstractFrame::Call(frame) => frame.step_into::(interp), + StandardAbstractFrame::DiGraph(frame) => frame.step_into::(interp), } } @@ -344,6 +554,7 @@ where match self { StandardAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), StandardAbstractFrame::Call(frame) => frame.resume_done_into::(), + StandardAbstractFrame::DiGraph(frame) => frame.resume_done_into::(), } } @@ -355,6 +566,9 @@ where match self { StandardAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), StandardAbstractFrame::Call(frame) => frame.resume_into::(completion), + StandardAbstractFrame::DiGraph(frame) => { + frame.resume_into::(completion, interp) + } } } } diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 391ed74902..90642ba280 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -17,9 +17,10 @@ //! # Owner kinds //! //! [`Owner::Function`] is a **summary/storage** owner — it is *never scheduled*; it -//! records a function context's entry/return/entry-block. [`Owner::Block`] is the -//! **executable** owner: exactly the block owners run frames (one single-pass CFG -//! walk each). CFG convergence is owner-summary convergence: a block emits its +//! records a function context's entry/return/entry-block. [`Owner::Block`] and +//! [`Owner::Graph`] are the **executable** owners: exactly those run frames (one +//! single-pass walk each — a CFG block, or a whole graph body in dependency +//! order). CFG convergence is owner-summary convergence: a block emits its //! successor block-entries, its function return, its outputs, and its external //! read dependencies through the single [`apply_update`](ForwardDriver::apply_update) //! path, which merges via the analysis policy and reschedules owners / value @@ -33,19 +34,19 @@ use std::hash::Hash; use std::marker::PhantomData; use kirin_ir::{ - Block, CFG, CompileStage, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, + Block, CFG, CompileStage, DiGraph, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, StageMeta, Statement, Widen, }; use crate::core::query; use crate::{ - AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, - AbstractInterpreter, Body, CallEffect, CallableBody, Callee, Env, EnvIndex, EnvStackStore, - FixpointProfile, ForwardEval, ForwardFrameDriver, ForwardSummaryDeps, Frame, FunctionTarget, - Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, OwnerSemantics, - SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, StandardAbstractFrame, - StandardFixpointInterpreter, Store, Summary, SummaryDependency, SummaryDependencyIndex, - SummaryEffect, + AbstractBlockFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractFrameBuild, + AbstractFrameDriver, AbstractInterpreter, Body, CallEffect, CallableBody, Callee, Env, + EnvIndex, EnvStackStore, FixpointProfile, ForwardEval, ForwardFrameDriver, ForwardSummaryDeps, + Frame, FunctionTarget, Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, + OwnerSemantics, SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, + StandardAbstractFrame, StandardFixpointInterpreter, Store, Summary, SummaryDependency, + SummaryDependencyIndex, SummaryEffect, }; // =========================================================================== @@ -123,7 +124,8 @@ where /// Owner of a summary in the forward fixpoint. /// /// [`Owner::Function`] is a **summary/storage** owner (never scheduled); -/// [`Owner::Block`] is the **executable** owner (frame-executed). `Owner` is a +/// [`Owner::Block`] and [`Owner::Graph`] are the **executable** owners +/// (frame-executed) — one per unit of re-analysis. `Owner` is a /// dataflow-equation identity — deliberately **not** a /// [`LatticeAnchor`](crate::LatticeAnchor). #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -132,6 +134,22 @@ pub enum Owner { Function(K), /// A CFG block executable owner within function context `K`. Block { function: K, block: Block }, + /// A graph-body executable owner within function context `K`: the whole + /// graph is one unit, re-analyzed as a single dependency-ordered pass + /// whenever its entry product rises. One pass is exact for a DAG, so there + /// is no intra-graph fixpoint to split into finer owners. + Graph { function: K, graph: DiGraph }, +} + +impl Owner { + /// The function context this owner belongs to. + pub fn function(&self) -> &K { + match self { + Owner::Function(function) + | Owner::Block { function, .. } + | Owner::Graph { function, .. } => function, + } + } } /// Per-function summary/storage record: call-site metadata, the joined entry @@ -300,18 +318,15 @@ enum ForwardUpdate { /// Merge a return contribution into a function context's return (join); on /// rise, reschedule its callers. FunctionReturn { key: K, values: Product }, - /// Merge edge args into a block owner's entry (widen by visits); on rise, - /// (re)schedule that block owner. - BlockEntry { - function: K, - block: Block, - args: Product, - }, - /// Merge a block's freshly computed outputs (join); on any value's rise, + /// Merge incoming args into an **executable** owner's entry (widen by + /// visits); on rise, (re)schedule that owner. The incoming args are a CFG + /// edge's arguments for a block owner, or the boundary-port values for a + /// graph owner. + OwnerEntry { owner: Owner, args: Product }, + /// Merge an owner's freshly computed outputs (join); on any value's rise, /// reschedule that value's readers. - BlockOutputs { - function: K, - block: Block, + OwnerOutputs { + owner: Owner, outputs: HashMap, }, } @@ -796,11 +811,7 @@ where } fn current_function_key(&self) -> Option<

>::Key> { - match self.current_owner() { - Some(Owner::Function(key)) => Some(key.clone()), - Some(Owner::Block { function, .. }) => Some(function.clone()), - None => None, - } + self.current_owner().map(|owner| owner.function().clone()) } /// Summarize a call atomically: resolve, merge the callee entry (which seeds @@ -958,12 +969,7 @@ where Ok(()) } - ForwardUpdate::BlockEntry { - function, - block, - args, - } => { - let owner = Owner::Block { function, block }; + ForwardUpdate::OwnerEntry { owner, args } => { let changed = if self.summary(&owner).is_none() { self.summaries_mut().insert( owner.clone(), @@ -1003,15 +1009,8 @@ where Ok(()) } - ForwardUpdate::BlockOutputs { - function, - block, - outputs, - } => { - let owner = Owner::Block { - function: function.clone(), - block, - }; + ForwardUpdate::OwnerOutputs { owner, outputs } => { + let function = owner.function().clone(); let mut risen = Vec::new(); for (value, incoming) in outputs { let old = self @@ -1045,8 +1044,14 @@ where } } - /// Resolve the entry block of `key`'s function (allocating its shared env on - /// first use) and seed the entry block owner with the entry-block arguments. + /// Resolve the executable entry owner of `key`'s function (allocating its + /// shared env 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 + /// hold — a `CFG`'s entry block or a `Block` body become an + /// [`Owner::Block`], a `DiGraph` body becomes an [`Owner::Graph`]. An + /// `UnGraph` has no derivable traversal order at all, so it is rejected. fn seed_entry_block( &mut self, key: &

>::Key, @@ -1067,36 +1072,53 @@ where .map(|function| function.entry.clone()) .expect("function summary present"); let body_info = self.enter_function(stage, body, entry_args, env)?; - let entry_block = match body_info.body { - Body::CFG(cfg) => self - .cfg_entry(stage, cfg)? - .ok_or_else(|| E::from(InterpreterError::EmptyCFG))?, - Body::Block(block) => block, - other @ (Body::DiGraph(_) | Body::UnGraph(_)) => { - return Err(E::from(InterpreterError::NoDefaultWalker(other))); + let owner = match body_info.body { + Body::CFG(cfg) => Owner::Block { + function: key.clone(), + block: self + .cfg_entry(stage, cfg)? + .ok_or_else(|| E::from(InterpreterError::EmptyCFG))?, + }, + Body::Block(block) => Owner::Block { + function: key.clone(), + block, + }, + // A graph body has no blocks: it is its own unit of re-analysis. + Body::DiGraph(graph) => Owner::Graph { + function: key.clone(), + graph, + }, + // An undirected graph has no producer/consumer direction, so no + // traversal order can be derived from its structure. + graph @ Body::UnGraph(_) => { + return Err(E::from(InterpreterError::NoDefaultWalker(graph))); } }; if let Some(function) = self .summary_mut(&Owner::Function(key.clone())) .and_then(|info| info.as_function_mut()) { - function.entry_block = Some(entry_block); + function.entry_block = match &owner { + Owner::Block { block, .. } => Some(*block), + _ => None, + }; } - self.apply_update(ForwardUpdate::BlockEntry { - function: key.clone(), - block: entry_block, + self.apply_update(ForwardUpdate::OwnerEntry { + owner, args: body_info.args, }) } } // =========================================================================== -// Owner semantics: only block owners are executable. +// Owner semantics: block and graph owners are executable. // =========================================================================== -/// The forward owner semantics. Only [`Owner::Block`] owners are analyzed: bind -/// the block-entry, walk the block once, then route its outputs / successor edges / -/// return / read-deps through [`apply_update`](ForwardDriver::apply_update). +/// The forward owner semantics. [`Owner::Block`] and [`Owner::Graph`] owners are +/// analyzed: bind the entry product, walk the unit once, then route its outputs / +/// successor edges / return / read-deps through +/// [`apply_update`](ForwardDriver::apply_update). A graph owner has no successor +/// edges — its declared yields are the function's return instead. struct SparseForwardSemantics { _marker: PhantomData V>, } @@ -1136,7 +1158,11 @@ where // safe default for the dependency-index bookkeeping path. Ok(match owner { Owner::Function(_) => ForwardSummary::Function(FunctionSummary::bottom()), - Owner::Block { .. } => ForwardSummary::Block(BlockSummary::bottom()), + // Both executable owners carry the same shape of summary: a joined + // entry product plus the output facts they define. + Owner::Block { .. } | Owner::Graph { .. } => { + ForwardSummary::Block(BlockSummary::bottom()) + } }) } @@ -1146,8 +1172,8 @@ where owner: &Owner<

>::Key>, summary: &ForwardSummary, ) -> Result { - let (function, block) = match owner { - Owner::Block { function, block } => (function.clone(), *block), + let function = match owner { + Owner::Block { function, .. } | Owner::Graph { function, .. } => function.clone(), Owner::Function(_) => { return Err(E::from(InterpreterError::Custom( "function owners are storage-only and never executed", @@ -1179,12 +1205,22 @@ where )) })?; interp.inner_mut().begin_block_log(); - Ok(F::from_block(AbstractBlockFrame::new_cfg_block( - stage, - env, - block, - block_entry, - ))) + match owner { + Owner::Block { block, .. } => Ok(F::from_block(AbstractBlockFrame::new_cfg_block( + stage, + env, + *block, + block_entry, + ))), + // One dependency-ordered pass over the whole graph. Exact for a DAG, + // so the pass never needs to iterate internally. + Owner::Graph { graph, .. } => { + F::from_digraph(AbstractDiGraphFrame::new(stage, env, *graph, block_entry)) + } + Owner::Function(_) => Err(E::from(InterpreterError::Custom( + "function owners are storage-only and never executed", + ))), + } } fn complete_owner( @@ -1193,22 +1229,29 @@ where owner: Owner<

>::Key>, completion: AbstractCompletion, ) -> Result>::Key>, ForwardSummary>, E> { - let (function, block) = match &owner { - Owner::Block { function, block } => (function.clone(), *block), + let function = match &owner { + Owner::Block { function, .. } | Owner::Graph { function, .. } => function.clone(), Owner::Function(_) => { return Err(E::from(InterpreterError::Custom( "function owners are storage-only and never executed", ))); } }; - let edges = match completion { - AbstractCompletion::CFGBlock { edges } => edges, + // A block owner completes with its outgoing CFG edges. A graph owner has + // no successors at all — it completes with the graph's declared yields, + // which for a callable graph body *are* the function's return values. + let (edges, graph_yields) = match (&owner, completion) { + (Owner::Block { .. }, AbstractCompletion::CFGBlock { edges }) => (edges, None), + (Owner::Graph { .. }, AbstractCompletion::Finished(values)) => (Vec::new(), values), _ => { return Err(E::from(InterpreterError::Custom( - "block owner completed with a non-CFG-block completion", + "executable owner completed with a mismatched completion", ))); } }; + if let Some(values) = graph_yields { + interp.contribute_return(values)?; + } let (reads, writes) = interp.inner_mut().take_logs(); let env = interp.store().env(&function).ok_or_else(|| { @@ -1239,17 +1282,19 @@ where let fact = interp.inner().env_read(env, value)?; outputs.insert(value, fact); } - interp.apply_update(ForwardUpdate::BlockOutputs { - function: function.clone(), - block, + interp.apply_update(ForwardUpdate::OwnerOutputs { + owner: owner.clone(), outputs, })?; - // Propagate CFG successor edges as block-entry updates. + // Propagate CFG successor edges as block-owner entry updates. Empty for a + // returning block and for a graph owner. for edge in edges { - interp.apply_update(ForwardUpdate::BlockEntry { - function: function.clone(), - block: edge.target, + interp.apply_update(ForwardUpdate::OwnerEntry { + owner: Owner::Block { + function: function.clone(), + block: edge.target, + }, args: edge.args, })?; } diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs b/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs index e307766375..04ec2ace23 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/mod.rs @@ -6,8 +6,8 @@ pub(crate) mod frames; pub(crate) mod interp; pub use frames::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - StandardAbstractFrame, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, StandardAbstractFrame, }; pub use interp::{ CallContext, ContextInsensitive, Owner, SparseForwardInterpreter, SparseForwardTransfer, diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index f45841b05b..ac160bf346 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -97,9 +97,9 @@ pub use engines::concrete::{ }; // Sparse forward engine (`Sem = ForwardEval`) + the abstract standard frames. pub use engines::sparse_forward::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, CallContext, - ContextInsensitive, Owner, SparseForwardInterpreter, SparseForwardTransfer, - StandardAbstractFrame, WideningStrategy, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, CallContext, ContextInsensitive, Owner, SparseForwardInterpreter, + SparseForwardTransfer, StandardAbstractFrame, WideningStrategy, }; // Sparse backward engine (`Sem = StrongDemand`). pub use engines::sparse_backward::{ @@ -166,10 +166,10 @@ pub mod dialect { /// Everything a compiler author needs to run engines or customize traversal. pub mod engine { pub use crate::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, AbstractInterpreter, BlockFrame, CFGFrame, CallContext, CallFrame, - Callee, Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, - DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, AbstractFrameDriver, AbstractInterpreter, BlockFrame, CFGFrame, + CallContext, CallFrame, Callee, Completion, ConcreteInterpreter, ContextInsensitive, + CrossStageLinker, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, DenseBackwardInterpreter, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, DiGraphFrame, Env, ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, FrameDriver, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 9cb6d6caa9..7a35529550 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -483,16 +483,41 @@ test). (Further examples: `example/toy-lang`'s `ToyFrame`, which adds `SparseForwardInterpreter` is symmetrically generic over a total abstract frame type `F` (default `StandardAbstractFrame`). The standard abstract frames -(`AbstractFunctionFrame`, `AbstractCFGFrame`, `AbstractBlockFrame`, -`AbstractCallFrame`) implement the *same* +(`AbstractBlockFrame`, `AbstractCallFrame`, `AbstractDiGraphFrame`) implement the +*same* `Frame` protocol, but their traversal is the abstract one: a CFG block worklist that joins/widens at merge points, `Branch` exploration, single-block -body walks that complete on `Yield`, and per-key call summarization. A custom +body walks that complete on `Yield`, dependency-ordered graph passes, and +per-key call summarization. A custom enum reuses them through `AbstractFrameBuild` and the `*_into` methods — exactly mirroring the concrete pattern (see `ToyAbstractFrame`, which adds `AbstractScfIfFrame`/`AbstractScfForFrame`, and `TracingAbstractFrame` in the same test module). +**Executable owners and the body vocabulary.** The forward fixpoint's work items +are `Owner`s, and only *executable* owners run frames: `Owner::Block` (one CFG +block) and `Owner::Graph` (one whole graph body). `Owner::Function` is +storage-only — it accumulates a context's joined entry arguments and joined +return, and is never scheduled. `seed_entry_block` is the single place a callable +body becomes executable work, translating the closed `Body` vocabulary into an +owner: `CFG` → its entry block, `Block` → itself, `DiGraph` → an `Owner::Graph`. +A graph owner is one unit because a single dependency-ordered pass is *exact* for +a DAG — no intra-graph widening is needed, and convergence pressure comes only +from entry widening when a new call site raises the owner's entry product. On +completion a graph owner has no successor edges; its declared yields become the +function's return contribution. `UnGraph` bodies are rejected with +`NoDefaultWalker`: an undirected graph has no derivable traversal order, and +unlike the concrete engine's `FrameBuild::from_ungraph_entry` there is currently +no seam through which a compiler could supply one. Analogously, +`AbstractFrameBuild::from_digraph` defaults to rejecting, so a total abstract +frame enum that carries no `AbstractDiGraphFrame` inherits the refusal rather +than pretending to analyze a graph body. + +`AbstractDiGraphFrame` differs from the concrete `DiGraphFrame` in exactly one +substantive way: a `Call` effect pushes an `AbstractCallFrame`, routing the call +through `summarize_call` instead of descending into the callee. Descending would +neither widen nor terminate on recursion. + Abstract frames need a few capabilities beyond `ForwardFrameDriver`, on `ForwardDataflowFrameDriver: ForwardFrameDriver` (alias: `AbstractFrameDriver`) — `analysis_merge`, `contribute_return`, and diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index c9a9ad54db..533c2fa05c 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -19,9 +19,10 @@ //! //! A final section runs the *forward dataflow* engine //! (`SparseForwardInterpreter`, instantiated at the constant-propagation -//! lattice) over the same body vocabulary: `CFG`/`Block` bodies analyze, and -//! `DiGraph`/`UnGraph` bodies are asserted to report `NoDefaultWalker` -//! because no abstract graph walker exists yet. +//! lattice) over the same body vocabulary: `CFG`, `Block` and `DiGraph` +//! callable bodies analyze — the last as an `Owner::Graph` walked by +//! `AbstractDiGraphFrame` — while `UnGraph` bodies and *nested* graph bodies +//! are asserted to be refused. use std::collections::VecDeque; use std::hash::Hash; @@ -35,11 +36,12 @@ use kirin_constant::Constant; use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_function::Lexical; use kirin_interpreter::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BlockFrame, Body, CFGFrame, CallContext, CallFrame, Completion, - ConcreteInterpreter, DiGraphFrame, Env, EnvIndex, Frame, FrameBuild, FrameDriver, FrameEffect, - FunctionEntry, Interpretable, InterpreterError, SameStageLinker, SparseForwardEffect, - SparseForwardInterp, SparseForwardInterpreter, StandardFrame, UnGraphEntry, expect_single, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, + AbstractFrameBuild, AbstractFrameDriver, BlockFrame, Body, CFGFrame, CallContext, CallFrame, + Completion, ConcreteInterpreter, DiGraphFrame, Env, EnvIndex, Frame, FrameBuild, FrameDriver, + FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, + SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, StandardFrame, + UnGraphEntry, expect_single, }; use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -209,10 +211,7 @@ fn linear_block_callable() { /// │ ├─▶ %d = mul %c, %e ──▶ yield /// └─▶ %e = add %x, %one ┘ /// ``` -#[test] -fn digraph_runs_in_topological_order() { - let pipeline = parse( - r#" +const DIGRAPH_TOPO_PROGRAM: &str = r#" stage @test fn @g(i64) -> i64; stage @test fn @main() -> i64; @@ -231,8 +230,11 @@ specialize @test fn @main() -> i64 { ret %r; } } -"#, - ); +"#; + +#[test] +fn digraph_runs_in_topological_order() { + let pipeline = parse(DIGRAPH_TOPO_PROGRAM); // (3 + 3) * (3 + 1) = 24 — requires running both `add`s (and the // constant) before the `mul` despite the textual order. assert_eq!(run(&pipeline, "main", &[]).unwrap(), 24); @@ -728,33 +730,41 @@ fn ungraph_without_policy_reports_no_default_walker() { // dataflow engine does with the same closed `Body` vocabulary, at the // constant-propagation lattice: // -// - `CFG` and `Block` bodies analyze: `SparseForwardInterpreter` seeds the -// entry block (`Body::CFG` → its entry, `Body::Block` → itself) and the -// standard abstract frames walk it, joining branch arms and summarizing -// calls interprocedurally. -// - `DiGraph` and `UnGraph` bodies are **refused**. There is no abstract -// graph walker: the engine rejects a graph callable body outright, and the -// abstract frame type has no variant able to walk a pushed graph. Both -// refusals are asserted rather than left implicit, so adding an abstract -// digraph walker fails these tests and forces them to be updated. +// - `CFG`, `Block` and `DiGraph` bodies analyze. The engine translates the +// callable body into the executable owner the worklist holds — `Body::CFG` → +// its entry block, `Body::Block` → itself, `Body::DiGraph` → an +// `Owner::Graph` walked by `AbstractDiGraphFrame` as one dependency-ordered +// pass (exact for a DAG, so no intra-graph widening). +// - `UnGraph` bodies are **refused**: an undirected graph has no derivable +// traversal order, and unlike the concrete engine there is no seam through +// which a compiler could supply one. +// - A *nested* graph body (`graph_eval`) is also still refused, for an +// unrelated reason: that dialect rule builds the **concrete** `DiGraphFrame` +// directly instead of selecting one per engine the way scf does, so no +// abstract walker can be substituted. Both refusals are asserted rather than +// left implicit. /// Summary key of the constant-propagation context policy. type CpKey = >::Key; /// Total abstract frame for the graph language: the standard abstract -/// traversal, plus a variant recording the absence of a graph walker. +/// traversal (blocks, calls, graph bodies), plus a variant recording the +/// absence of a walker. /// /// The language's `Interpretable` rule bounds `I::Frame: FrameBuild<..>` -/// because its `graph_eval` variant pushes a `DiGraphFrame`, so an abstract -/// frame type for this language must satisfy that bound even though the -/// abstract engine itself only ever calls `AbstractFrameBuild`. The concrete -/// walkers cannot be embedded here — their completion type is +/// because its `graph_eval` variant pushes a **concrete** `DiGraphFrame`, so an +/// abstract frame type for this language must satisfy that bound even though +/// the abstract engine itself only ever calls `AbstractFrameBuild`. The +/// concrete walkers cannot be embedded here — their completion type is /// `Completion`, not `AbstractCompletion` — so the `FrameBuild` hooks /// build `NoWalker`, which reports the gap if it is ever stepped instead of -/// silently running concrete traversal over lattice values. +/// silently running concrete traversal over lattice values. Giving `graph_eval` +/// a per-engine dispatch trait (as `kirin-scf` does for `scf.if`/`scf.for`) +/// would remove the need for both the bound and this variant. enum GraphAbstractFrame { Block(AbstractBlockFrame), Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), /// No abstract walker exists for this body kind; carries the reason. NoWalker(&'static str), } @@ -766,6 +776,9 @@ impl AbstractFrameBuild for GraphAbstractFrame { fn from_call(frame: AbstractCallFrame) -> Self { GraphAbstractFrame::Call(frame) } + fn from_digraph(frame: AbstractDiGraphFrame) -> Result { + Ok(GraphAbstractFrame::DiGraph(frame)) + } } impl FrameBuild for GraphAbstractFrame { @@ -797,6 +810,7 @@ where match self { GraphAbstractFrame::Block(frame) => frame.step_into::(interp), GraphAbstractFrame::Call(frame) => frame.step_into::(interp), + GraphAbstractFrame::DiGraph(frame) => frame.step_into::(interp), GraphAbstractFrame::NoWalker(reason) => { Err(I::Error::from(InterpreterError::Custom(reason))) } @@ -807,6 +821,7 @@ where match self { GraphAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), GraphAbstractFrame::Call(frame) => frame.resume_done_into::(), + GraphAbstractFrame::DiGraph(frame) => frame.resume_done_into::(), GraphAbstractFrame::NoWalker(reason) => { Err(I::Error::from(InterpreterError::Custom(reason))) } @@ -821,6 +836,7 @@ where match self { GraphAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), GraphAbstractFrame::Call(frame) => frame.resume_into::(completion), + GraphAbstractFrame::DiGraph(frame) => frame.resume_into::(completion, interp), GraphAbstractFrame::NoWalker(reason) => { Err(I::Error::from(InterpreterError::Custom(reason))) } @@ -959,33 +975,55 @@ fn abstract_block_body_callable() { ); } -/// A callable `DiGraph` body has no abstract walker, so the engine refuses it -/// when seeding the function's entry rather than inventing a schedule — both -/// as an analysis root and as a summarized callee. +/// `Body::DiGraph` under forward dataflow: the callable graph body becomes an +/// `Owner::Graph` walked by `AbstractDiGraphFrame` — one dependency-ordered +/// pass binding the boundary ports, with the graph's declared yields becoming +/// the function's return summary. Reached both as an analysis root and as a +/// summarized callee. #[test] -fn abstract_digraph_callable_reports_no_default_walker() { +fn abstract_digraph_callable_analyzes() { let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); - let root = analyze( - &pipeline, - "gadd", - &[ConstPropValue::Const(2), ConstPropValue::Const(3)], - ) - .unwrap_err(); - assert!( - matches!( - root, - TestError::Core(InterpreterError::NoDefaultWalker(Body::DiGraph(_))) - ), - "expected NoDefaultWalker(DiGraph) as an analysis root, got {root:?}" + assert_eq!( + analyze( + &pipeline, + "gadd", + &[ConstPropValue::Const(2), ConstPropValue::Const(3)] + ) + .unwrap(), + ConstPropValue::Const(5) ); + // Same body, reached through a call from a CFG-bodied caller: the graph + // owner's yields flow back as the callee's return summary. + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(5) + ); + // An unknown port value propagates through the graph to Top. + assert_eq!( + analyze( + &pipeline, + "gadd", + &[ConstPropValue::Top, ConstPropValue::Const(3)] + ) + .unwrap(), + ConstPropValue::Top + ); +} - let callee = analyze(&pipeline, "main", &[]).unwrap_err(); - assert!( - matches!( - callee, - TestError::Core(InterpreterError::NoDefaultWalker(Body::DiGraph(_))) - ), - "expected NoDefaultWalker(DiGraph) through a call, got {callee:?}" +/// The abstract walker uses the same dependency schedule as the concrete one: +/// this graph's nodes are declared consumer-before-producer, so a textual walk +/// would read unbound operands. +#[test] +fn abstract_digraph_follows_dependency_order() { + let pipeline = parse(DIGRAPH_TOPO_PROGRAM); + // (3 + 3) * (3 + 1) = 24, matching `digraph_runs_in_topological_order`. + assert_eq!( + analyze(&pipeline, "g", &[ConstPropValue::Const(3)]).unwrap(), + ConstPropValue::Const(24) + ); + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(24) ); } From 2a9e7d629ac3fece95fdeb83ea17fcbe436339b9 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 30 Jul 2026 19:38:33 -0400 Subject: [PATCH 10/21] refactor: enhance body handling and interprocedural analysis in tests --- tests/body_kinds.rs | 363 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 356 insertions(+), 7 deletions(-) diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 533c2fa05c..d64f6873ec 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -17,12 +17,16 @@ //! `CallFrame`, and the callable-UnGraph policy hook (with and without a //! policy). //! -//! A final section runs the *forward dataflow* engine +//! The later sections run the *forward dataflow* engine //! (`SparseForwardInterpreter`, instantiated at the constant-propagation //! lattice) over the same body vocabulary: `CFG`, `Block` and `DiGraph` //! callable bodies analyze — the last as an `Owner::Graph` walked by //! `AbstractDiGraphFrame` — while `UnGraph` bodies and *nested* graph bodies -//! are asserted to be refused. +//! are asserted to be refused. Those sections also pin the graph owner's +//! interprocedural behaviour: calls *inside* a graph body are summarized rather +//! than descended into (so self-recursion converges), entry arguments join and +//! re-run the owner when several call sites share one key, and a directed cycle +//! is rejected identically by both engines. use std::collections::VecDeque; use std::hash::Hash; @@ -38,10 +42,10 @@ use kirin_function::Lexical; use kirin_interpreter::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractFrameBuild, AbstractFrameDriver, BlockFrame, Body, CFGFrame, CallContext, CallFrame, - Completion, ConcreteInterpreter, DiGraphFrame, Env, EnvIndex, Frame, FrameBuild, FrameDriver, - FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, - SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, StandardFrame, - UnGraphEntry, expect_single, + Completion, ConcreteInterpreter, ContextInsensitive, DiGraphFrame, Env, EnvIndex, Frame, + FrameBuild, FrameDriver, FrameEffect, FunctionEntry, Interpretable, InterpreterError, + SameStageLinker, SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, + StandardFrame, UnGraphEntry, expect_single, }; use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -89,8 +93,18 @@ fn parse(program: &str) -> Pipeline { } fn run(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + expect_single(run_product(pipeline, function, args)?) +} + +/// `run`, keeping the whole returned product — for callables that return more +/// than one value. +fn run_product( + pipeline: &Pipeline, + function: &str, + args: &[i64], +) -> Result, TestError> { let mut interp: Engine<'_> = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); - expect_single(interp.call_by_name("test", function, args.iter().copied())?) + interp.call_by_name("test", function, args.iter().copied()) } // =========================================================================== @@ -866,6 +880,33 @@ fn analyze( expect_single(analysis.analyze_by_name("test", function, args.iter().cloned())?) } +/// Summary key of the *context-insensitive* policy: one key per function, so +/// every call site shares one owner and their arguments join. +type CiKey = >::Key; + +type InsensitiveEngine<'ir> = SparseForwardInterpreter< + 'ir, + L, + ConstPropValue, + TestError, + SameStageLinker, + ContextInsensitive, + GraphAbstractFrame, +>; + +/// The same analysis under [`ContextInsensitive`] keying: distinct call sites +/// collapse onto one owner, so entry arguments must join and the owner must be +/// re-analyzed when they rise. +fn analyze_insensitive( + pipeline: &Pipeline, + function: &str, + args: &[ConstPropValue], +) -> Result { + let mut analysis: InsensitiveEngine<'_> = + SparseForwardInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(analysis.analyze_by_name("test", function, args.iter().cloned())?) +} + /// A CFG body whose branch condition is an *unknown* argument, so neither /// successor can be decided: the abstract block frame explores both and joins /// their returns. Identical arms fold to a constant; differing arms join to @@ -1060,3 +1101,311 @@ fn abstract_nested_digraph_reports_no_walker() { "expected the pushed-frame walker gap, got {error:?}" ); } + +// =========================================================================== +// 10. Calls *inside* a graph body. +// =========================================================================== + +/// A digraph node that is itself a call. The dependency edge `%a → %b` runs +/// through two call results, so the graph walker must sequence the calls, and +/// each call must be routed through the engine's call protocol rather than +/// evaluated inline. +const DIGRAPH_CALLS_PROGRAM: &str = r#" +stage @test fn @inc(i64) -> i64; +stage @test fn @gcall(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @inc(i64) -> i64 { + ^entry(%v: i64) { + %one = constant 1 -> i64; + %s = add %v, %one -> i64; + ret %s; + } +} + +specialize @test fn @gcall(i64) -> i64 digraph ^g0(%x: i64) { + %b = call.named @inc(%a) -> i64; + %a = call.named @inc(%x) -> i64; + yield %b; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %c = constant 5 -> i64; + %r = call.named @gcall(%c) -> i64; + ret %r; + } +} +"#; + +/// Concretely: the `DiGraphFrame` pushes a `CallFrame` per call node, in +/// dependency order (`%a` before `%b` despite the textual order). +#[test] +fn digraph_node_calls_run_in_dependency_order() { + let pipeline = parse(DIGRAPH_CALLS_PROGRAM); + assert_eq!(run(&pipeline, "gcall", &[5]).unwrap(), 7); + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 7); +} + +/// Abstractly the same graph must route each call through +/// `summarize_call` — an `AbstractCallFrame`, *not* the concrete `CallFrame`, +/// which would descend into the callee and bypass the interprocedural +/// protocol. This is the one arm where `AbstractDiGraphFrame` differs +/// substantively from the concrete walker. +#[test] +fn abstract_digraph_node_calls_are_summarized() { + let pipeline = parse(DIGRAPH_CALLS_PROGRAM); + assert_eq!( + analyze(&pipeline, "gcall", &[ConstPropValue::Const(5)]).unwrap(), + ConstPropValue::Const(7) + ); + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(7) + ); + // An unknown port flows through both summarized calls to Top. + assert_eq!( + analyze(&pipeline, "gcall", &[ConstPropValue::Top]).unwrap(), + ConstPropValue::Top + ); +} + +/// A graph body that calls *itself*. A digraph cannot branch, so this +/// recursion has no base case and does not terminate concretely — which is +/// exactly why it is analysis-only. It terminates here because the call is +/// summarized: the self-key's return summary starts at `bottom` and the owner +/// re-runs only while it rises. Descending into the callee instead would +/// recurse forever. +const DIGRAPH_RECURSION_PROGRAM: &str = r#" +stage @test fn @grec(i64) -> i64; + +specialize @test fn @grec(i64) -> i64 digraph ^g0(%x: i64) { + %r = call.named @grec(%x) -> i64; + yield %r; +} +"#; + +#[test] +fn abstract_digraph_self_recursion_converges() { + let pipeline = parse(DIGRAPH_RECURSION_PROGRAM); + // Never returns, so the sound fixpoint is `bottom` — and, crucially, the + // analysis reaches it instead of diverging. + assert_eq!( + analyze(&pipeline, "grec", &[ConstPropValue::Const(1)]).unwrap(), + ConstPropValue::Bottom + ); +} + +// =========================================================================== +// 11. Graph-owner re-analysis: two call sites, one owner. +// =========================================================================== + +/// One graph-bodied callee reached from two call sites with different +/// constants. +const DIGRAPH_TWO_CALLERS_PROGRAM: &str = r#" +stage @test fn @gdouble(i64) -> i64; +stage @test fn @twocalls() -> i64; + +specialize @test fn @gdouble(i64) -> i64 digraph ^g0(%x: i64) { + %s = add %x, %x -> i64; + yield %s; +} + +specialize @test fn @twocalls() -> i64 { + ^entry() { + %a = constant 1 -> i64; + %b = constant 2 -> i64; + %p = call.named @gdouble(%a) -> i64; + %q = call.named @gdouble(%b) -> i64; + %s = add %p, %q -> i64; + ret %s; + } +} +"#; + +/// The convergence behaviour of a graph owner, pinned from both sides by +/// running the same program under both keying policies. +/// +/// Under [`ContextInsensitive`] the two call sites share one owner, so its +/// entry product must **join** (`Const(1) ⊔ Const(2)` = `Top`) and the graph +/// must be **re-analyzed** with the wider entry — an owner seeded once and +/// never re-run would leave the second call site reading a stale `Const(4)`. +/// Under `ConstPropContext` the sites key separately and each stays exact. +/// Together these show a graph owner participates in entry widening exactly +/// like a block owner, which is where all of its convergence pressure comes +/// from (one dependency-ordered pass is exact, so nothing widens *inside* the +/// graph). +#[test] +fn abstract_digraph_owner_joins_two_call_sites() { + let pipeline = parse(DIGRAPH_TWO_CALLERS_PROGRAM); + // Shared owner: entry joins to Top, the graph re-runs, both results are Top. + assert_eq!( + analyze_insensitive(&pipeline, "twocalls", &[]).unwrap(), + ConstPropValue::Top + ); + // Distinct keys: 1+1 = 2 and 2+2 = 4, so 2 + 4 = 6. + assert_eq!( + analyze(&pipeline, "twocalls", &[]).unwrap(), + ConstPropValue::Const(6) + ); +} + +// =========================================================================== +// 12. Cyclic DiGraph: rejected when the walk plan is built. +// =========================================================================== + +/// A digraph whose nodes depend on each other. The IR represents this happily — +/// it parses — because a `DiGraph` is not required to be acyclic. +const DIGRAPH_CYCLE_PROGRAM: &str = r#" +stage @test fn @gcycle(i64) -> i64; + +specialize @test fn @gcycle(i64) -> i64 digraph ^g0(%x: i64) { + %a = add %b, %x -> i64; + %b = add %a, %x -> i64; + yield %a; +} +"#; + +/// Both engines reject a directed cycle, with the *same* error and from the +/// *same* place: `digraph_walk_plan` topologically sorts the nodes, and a +/// cyclic graph has no topological order. So the rejection is a property of the +/// walk plan (shared by the concrete and abstract walkers), not of the IR and +/// not of either engine. +/// +/// Supporting cyclic graph bodies is therefore not an extension of the current +/// walkers: it needs a schedule that is not a toposort plus a fixpoint *inside* +/// the graph, which in turn needs a finer unit of re-analysis than +/// `Owner::Graph`'s single exact pass. +#[test] +fn cyclic_digraph_is_rejected_by_both_engines() { + let pipeline = parse(DIGRAPH_CYCLE_PROGRAM); + + let concrete = run(&pipeline, "gcycle", &[1]).unwrap_err(); + assert!( + matches!( + concrete, + TestError::Core(InterpreterError::GraphHasCycle(_)) + ), + "expected GraphHasCycle concretely, got {concrete:?}" + ); + + let abstract_ = analyze(&pipeline, "gcycle", &[ConstPropValue::Const(1)]).unwrap_err(); + assert!( + matches!( + abstract_, + TestError::Core(InterpreterError::GraphHasCycle(_)) + ), + "expected GraphHasCycle abstractly, got {abstract_:?}" + ); +} + +// =========================================================================== +// 13. Multi-yield graphs and boundary arity. +// =========================================================================== + +/// A graph yielding **two** values into a two-result call site. +/// +/// Note the declared signature is `-> i64`, one type, while the function +/// actually returns two values: `Signature` carries a single `ret` type, so it +/// does not constrain return *arity*. The product arity that matters at runtime +/// is the graph's `yield` list versus the call statement's result slots. (This +/// is the same shape as `example/toy-qc/programs/ghz.kirin`, which declares +/// `-> Qubit` and yields three.) +const DIGRAPH_MULTIYIELD_PROGRAM: &str = r#" +stage @test fn @gpair(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @gpair(i64) -> i64 digraph ^g0(%x: i64) { + %a = add %x, %x -> i64; + %b = mul %x, %x -> i64; + yield %a, %b; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %c = constant 3 -> i64; + %p, %q = call.named @gpair(%c) -> i64, i64; + %s = add %p, %q -> i64; + ret %s; + } +} +"#; + +/// Yield order is result-slot order: `%p` gets the first yield, `%q` the +/// second. Asserting the product directly (rather than only their sum) pins +/// that mapping. +#[test] +fn digraph_yields_multiple_values() { + let pipeline = parse(DIGRAPH_MULTIYIELD_PROGRAM); + let values: Vec = run_product(&pipeline, "gpair", &[3]) + .unwrap() + .iter() + .copied() + .collect(); + // 3 + 3 = 6 and 3 * 3 = 9, in yield order. + assert_eq!(values, vec![6, 9]); + // Both land in the caller's two result slots: 6 + 9. + assert_eq!(run(&pipeline, "main", &[]).unwrap(), 15); +} + +/// The abstract walker collects the same product, so a multi-result callee's +/// return summary carries both slots. +#[test] +fn abstract_digraph_yields_multiple_values() { + let pipeline = parse(DIGRAPH_MULTIYIELD_PROGRAM); + assert_eq!( + analyze(&pipeline, "main", &[]).unwrap(), + ConstPropValue::Const(15) + ); +} + +/// A graph whose boundary ports outnumber the call's arguments. Nothing earlier +/// in the pipeline cross-checks the declared signature against the port list, so +/// the graph walkers arity-check when binding the ports — the same check, and +/// the same error, in both engines. +const DIGRAPH_PORT_ARITY_PROGRAM: &str = r#" +stage @test fn @g2(i64) -> i64; +stage @test fn @main() -> i64; + +specialize @test fn @g2(i64) -> i64 digraph ^g0(%x: i64, %y: i64) { + %s = add %x, %y -> i64; + yield %s; +} + +specialize @test fn @main() -> i64 { + ^entry() { + %c = constant 3 -> i64; + %r = call.named @g2(%c) -> i64; + ret %r; + } +} +"#; + +#[test] +fn digraph_port_arity_mismatch_is_reported() { + let pipeline = parse(DIGRAPH_PORT_ARITY_PROGRAM); + + let concrete = run(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + concrete, + TestError::Core(InterpreterError::ProductArityMismatch { + expected: 2, + actual: 1 + }) + ), + "expected a port arity mismatch concretely, got {concrete:?}" + ); + + let abstract_ = analyze(&pipeline, "main", &[]).unwrap_err(); + assert!( + matches!( + abstract_, + TestError::Core(InterpreterError::ProductArityMismatch { + expected: 2, + actual: 1 + }) + ), + "expected a port arity mismatch abstractly, got {abstract_:?}" + ); +} From e4d50b2a6abda5160d85c439a07aab5bd78cba99 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 30 Jul 2026 22:26:42 -0400 Subject: [PATCH 11/21] refactor: update frame handling methods for clarity and consistency --- AGENTS.md | 4 +- crates/kirin-interpreter/src/core/frame.rs | 53 +++-- .../engines/concrete/frames/block_frame.rs | 34 ++-- .../src/engines/concrete/frames/call_frame.rs | 42 ++-- .../src/engines/concrete/frames/cfg_frame.rs | 34 ++-- .../engines/concrete/frames/digraph_frame.rs | 68 +++---- .../engines/concrete/frames/standard_frame.rs | 43 ++-- .../src/engines/concrete/interp.rs | 2 +- .../src/engines/dense_backward/frames.rs | 57 +++--- .../src/engines/dense_backward/interp.rs | 2 +- .../src/engines/sparse_backward/interp.rs | 14 +- .../src/engines/sparse_forward/frames.rs | 166 +++++++-------- .../src/engines/sparse_forward/interp.rs | 2 +- .../kirin-interpreter/src/fixpoint/runner.rs | 2 +- .../kirin-interpreter/src/fixpoint/solver.rs | 10 +- .../src/fixpoint/tests/counter.rs | 10 +- .../src/fixpoint/tests/deps.rs | 10 +- .../src/fixpoint/tests/phase.rs | 12 +- crates/kirin-liveness/src/lib.rs | 1 + crates/kirin-liveness/src/result.rs | 1 + crates/kirin-scf/src/interpreter.rs | 186 ++++++++--------- docs/design/interpreter/index.md | 43 +++- example/toy-lang/src/interpreter/frame.rs | 131 ++++++------ example/toy-lang/src/interpreter/tests.rs | 167 ++++++---------- tests/body_kinds.rs | 189 +++++++++--------- 25 files changed, 659 insertions(+), 624 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 65d2f942a7..6036e75e4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,9 +155,9 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Calling conventions are linkers**: `Linker` 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` 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>` 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>` (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` protocol — pop the top frame, `step_into`, apply the returned `FrameEffect`, owning no traversal logic. **One trait covers both roles**: a *member* (an individual walker) is generic over the total frame type `F` it composes into and names its successors in `F`; a *universe* (a language's total frame enum) implements `Frame` when it is the stack's element type but stays generic over `F`, so it can itself be embedded in a larger enum — `drive_frames` bounds on `F: Frame`. That is how `toy-lang`'s `TracingFrame` is a newtype wrapping `ToyFrame` whole rather than a copy of its variants. `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>` 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>` (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 `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`. +- **Customizing traversal**: `core/frame.rs` defines the shared `Frame`/`FrameEffect`/`drive_frames` protocol plus forward driver traits. Every frame — walker or enum — implements the same three methods (`step_into`/`resume_done_into`/`resume_into`), all returning `Result, I::Error>`, so a total enum's match arms are uniform across variants. 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` 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`. diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 2b3d63af10..70d46c91f5 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -20,10 +20,11 @@ pub enum FrameEffect { /// Push `parent` then `child`; `child` runs next, `parent` resumes after. Push { parent: F, child: F }, /// This frame finished with no payload; its parent's - /// [`Frame::resume_done`] is called. + /// [`Frame::resume_done_into`] is called. Done, - /// This frame produced a completion `C`; its parent's [`Frame::resume`] is - /// called (or, at the root, the run finishes with `C`). + /// This frame produced a completion `C`; its parent's + /// [`Frame::resume_into`] is called (or, at the root, the run finishes with + /// `C`). Complete(C), } @@ -37,21 +38,43 @@ impl FrameEngine for T { type Error = ::Error; } -/// A continuation frame anchored in an IR traversal. +/// A continuation frame anchored in an IR traversal, expressed over the total +/// frame type `F` it composes into. /// -/// Implemented by the total frame enum `F`. Each method consumes `self` and -/// returns the next structural move as a [`FrameEffect`]. -pub trait Frame: Sized { +/// Every method consumes `self` and returns the next structural move as a +/// [`FrameEffect`] **over `F`** — never over `Self`. That single choice is what +/// lets one trait serve both roles a frame stack needs: +/// +/// - a **member** — an individual walker ([`BlockFrame`](crate::BlockFrame), +/// [`CallFrame`](crate::CallFrame), a dialect's own frame). It is one variant +/// of `F` and names its successors in `F`, re-wrapping itself through the +/// relevant `*FrameBuild` hook. Members are generic over `F`, so the same +/// walker composes into any language's frame type. +/// - a **universe** — a language's total frame enum. It implements +/// `Frame` when it is the stack's element type, and stays generic +/// over `F` so that it can *also* be embedded in a larger enum (an +/// instrumenting wrapper, or another language's frame type) without +/// re-enumerating its variants. +/// +/// [`drive_frames`] bounds on `F: Frame`: the stack's element type must be +/// a universe — a type able to represent every frame that can appear on it. +pub trait Frame: Sized { /// The completion payload this frame family bubbles to parents/root. type Completion; - fn step(self, interp: &mut I) -> Result, I::Error>; - fn resume_done(self, interp: &mut I) -> Result, I::Error>; - fn resume( + /// Do this frame's next unit of work. + fn step_into(self, interp: &mut I) -> Result, I::Error>; + + /// A pushed child finished with no payload. + fn resume_done_into(self, interp: &mut I) + -> Result, I::Error>; + + /// A pushed child finished with a completion payload. + fn resume_into( self, completion: Self::Completion, interp: &mut I, - ) -> Result, I::Error>; + ) -> Result, I::Error>; } /// Shared frame-stepping loop. @@ -59,13 +82,13 @@ pub fn drive_frames(engine: &mut I, frames: &mut Vec) -> Result, - F: Frame, + F: Frame, { loop { let frame = frames .pop() .ok_or_else(|| I::Error::from(InterpreterError::EmptyFrameStack))?; - let mut effect = frame.step(engine)?; + let mut effect = frame.step_into(engine)?; loop { match effect { FrameEffect::Continue(frame) => { @@ -81,11 +104,11 @@ where let parent = frames .pop() .ok_or_else(|| I::Error::from(InterpreterError::EmptyFrameStack))?; - effect = parent.resume_done(engine)?; + effect = parent.resume_done_into(engine)?; } FrameEffect::Complete(completion) => match frames.pop() { Some(parent) => { - effect = parent.resume(completion, engine)?; + effect = parent.resume_into(completion, engine)?; } None => return Ok(completion), }, diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs index 603b1fd1bd..c4da6211c6 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs @@ -1,7 +1,8 @@ use kirin_ir::{Block, CompileStage, Product}; use crate::{ - EnvIndex, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, + EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, }; use super::block_cursor::BlockCursor; @@ -39,14 +40,20 @@ where _marker: std::marker::PhantomData, } } +} + +impl Frame for BlockFrame +where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; /// Execute the next statement and translate its [`SparseForwardEffect`] /// into a [`FrameEffect`] over the total frame type `F`. - pub fn step_into(mut self, interp: &mut I) -> Result>, E> - where - I: FrameDriver + SparseForwardInterp, - F: FrameBuild, - { + fn step_into(mut self, interp: &mut I) -> Result>, E> { if self.cursor.bind_entry(interp)? { return Ok(FrameEffect::Continue(F::from_block(self))); } @@ -87,25 +94,18 @@ where /// A child finished without a payload (its results are already in the /// shared activation, e.g. a returned call): resume at the advanced /// cursor. - pub fn resume_done_into(self) -> FrameEffect> - where - F: FrameBuild, - { - FrameEffect::Continue(F::from_block(self)) + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_block(self))) } /// A child bubbled a completion: a pushed frame's values land in the /// push's result slots; a `Returned` keeps bubbling toward the nearest /// [`CallFrame`] (this frame owns no activation to free). - pub fn resume_into( + fn resume_into( mut self, completion: Completion, interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { + ) -> Result>, E> { match completion { Completion::Finished(values) | Completion::Yielded(values) => { self.cursor.write_child_results(interp, values)?; 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 0fae0a2cf5..eade499793 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -1,6 +1,8 @@ use kirin_ir::{CompileStage, Product, SSAValue}; -use crate::{Body, CallEffect, Callee, EnvIndex, FrameDriver, FrameEffect, InterpreterError}; +use crate::{ + Body, CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, +}; use super::{BlockFrame, CFGFrame, Completion, DiGraphFrame, FrameBuild, UnGraphEntry}; @@ -92,13 +94,18 @@ where }, } } +} + +impl Frame for CallFrame +where + I: FrameDriver, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; - pub fn step_into(self, interp: &mut I) -> Result>, I::Error> - where - I: FrameDriver, - I::Error: From, - F: FrameBuild, - { + fn step_into(self, interp: &mut I) -> Result>, E> { match self.state { CallState::Pending { resolve_stage, @@ -139,32 +146,27 @@ where child, }) } - CallState::Awaiting { .. } => Err(I::Error::from(InterpreterError::Custom( + CallState::Awaiting { .. } => Err(E::from(InterpreterError::Custom( "call frame stepped while awaiting a return", ))), } } - pub fn resume_done_into(self) -> Result>, InterpreterError> { - Err(InterpreterError::Custom( + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Err(E::from(InterpreterError::Custom( "call frame resumed without a return", - )) + ))) } /// The callee completed: validate the completion kind, free the callee /// activation exactly once, and deliver the returned values. - pub fn resume_into( + fn resume_into( self, completion: Completion, interp: &mut I, - ) -> Result>, I::Error> - where - I: FrameDriver, - I::Error: From, - F: FrameBuild, - { + ) -> Result>, E> { let CallState::Awaiting { callee_env, dest } = self.state else { - return Err(I::Error::from(InterpreterError::Custom( + return Err(E::from(InterpreterError::Custom( "call frame resumed before dispatch", ))); }; @@ -173,7 +175,7 @@ where // (a callable DiGraph's outputs are the call's returned values). Completion::Returned(values) | Completion::Finished(values) => values, Completion::Yielded(_) => { - return Err(I::Error::from(InterpreterError::Custom( + return Err(E::from(InterpreterError::Custom( "structured yield reached a function-call boundary (a callable body must exit with return)", ))); } diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs index 441a9ebb82..fdcf88c8dd 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs @@ -1,7 +1,8 @@ use kirin_ir::{CFG, CompileStage, Product}; use crate::{ - EnvIndex, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, + EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, }; use super::block_cursor::BlockCursor; @@ -48,14 +49,20 @@ where _marker: std::marker::PhantomData, } } +} + +impl Frame for CFGFrame +where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; /// Execute the next statement and translate its [`SparseForwardEffect`] /// into a [`FrameEffect`] over the total frame type `F`. - pub fn step_into(mut self, interp: &mut I) -> Result>, E> - where - I: FrameDriver + SparseForwardInterp, - F: FrameBuild, - { + fn step_into(mut self, interp: &mut I) -> Result>, E> { // First step: find the entry block and bind the entry arguments. if let Some(args) = self.pending.take() { let entry = interp @@ -107,25 +114,18 @@ where /// A child finished without a payload (its results are already in the /// shared activation, e.g. a returned call): resume at the advanced /// cursor. - pub fn resume_done_into(self) -> FrameEffect> - where - F: FrameBuild, - { - FrameEffect::Continue(F::from_cfg(self)) + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_cfg(self))) } /// A child bubbled a completion: a pushed frame's values land in the /// push's result slots; a `Returned` keeps bubbling toward the nearest /// [`CallFrame`] (this frame owns no activation to free). - pub fn resume_into( + fn resume_into( mut self, completion: Completion, interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { + ) -> Result>, E> { match completion { Completion::Finished(values) | Completion::Yielded(values) => { let cursor = self.cursor.as_mut().ok_or_else(|| { 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 12854ce1ee..79af321db3 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/digraph_frame.rs @@ -1,7 +1,8 @@ use kirin_ir::{CompileStage, Product, SSAValue, Statement}; use crate::{ - EnvIndex, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, SparseForwardInterp, + EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, }; use super::{CallFrame, Completion, FrameBuild}; @@ -66,14 +67,36 @@ where } } - /// Execute the next scheduled node and translate its - /// [`SparseForwardEffect`] into a [`FrameEffect`] over the total frame - /// type `F`. - pub fn step_into(mut self, interp: &mut I) -> Result>, E> + /// Schedule exhausted: read the declared yields from the activation and + /// complete `Finished` — the graph's natural completion. The parent + /// decides what the values mean (call returns or push results). + fn finish(self, interp: &mut I) -> Result>, E> where - I: FrameDriver + SparseForwardInterp, + I: FrameDriver, F: FrameBuild, { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(Completion::Finished(values))) + } +} + +impl Frame for DiGraphFrame +where + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, + V: Clone, + E: From, +{ + type Completion = Completion; + + /// Execute the next scheduled node and translate its + /// [`SparseForwardEffect`] into a [`FrameEffect`] over the total frame + /// type `F`. + fn step_into(mut self, interp: &mut I) -> Result>, E> { // First step: fetch the walk plan and bind the boundary ports. if let Some(args) = self.pending.take() { let plan = interp.digraph_walk_plan(self.stage, self.graph)?; @@ -123,43 +146,20 @@ where } } - /// Schedule exhausted: read the declared yields from the activation and - /// complete `Finished` — the graph's natural completion. The parent - /// decides what the values mean (call returns or push results). - fn finish(self, interp: &mut I) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { - let values: Product = self - .yields - .iter() - .map(|&value| interp.env_read(self.index, value)) - .collect::>()?; - Ok(FrameEffect::Complete(Completion::Finished(values))) - } - /// A child finished without a payload (e.g. a returned call whose results /// are already written): resume the schedule. - pub fn resume_done_into(self) -> FrameEffect> - where - F: FrameBuild, - { - FrameEffect::Continue(F::from_digraph(self)) + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_digraph(self))) } /// A child bubbled a completion: a pushed frame's values land in the /// push's result slots. A `Returned` cannot bubble out of a graph node — /// a digraph has no function-return convention. - pub fn resume_into( + fn resume_into( mut self, completion: Completion, interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild, - { + ) -> Result>, E> { match completion { Completion::Finished(values) | Completion::Yielded(values) => { let slots = self.resume_slots.take().ok_or_else(|| { @@ -167,7 +167,7 @@ where "digraph resume without result slots", )) })?; - interp.write_results(self.index, &slots, values)?; + crate::FrameDriver::write_results(interp, self.index, &slots, values)?; Ok(FrameEffect::Continue(F::from_digraph(self))) } Completion::Returned(_) => Err(E::from(InterpreterError::Custom( diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs index c4082cda11..65dbead4fe 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs @@ -28,42 +28,47 @@ impl FrameBuild for StandardFrame { } } -impl Frame for StandardFrame +/// A *universe* impl: generic over the outer total frame type `F` so that +/// `StandardFrame` can be the stack's element type (`F = Self`, the usual case) +/// **or** be embedded in a larger enum — an instrumenting wrapper, say — without +/// re-enumerating its variants. +impl Frame for StandardFrame where - I: FrameDriver + SparseForwardInterp>, + I: FrameDriver + SparseForwardInterp, + F: FrameBuild, V: Clone, E: From, { type Completion = Completion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - StandardFrame::Block(frame) => frame.step_into::(interp), - StandardFrame::CFG(frame) => frame.step_into::(interp), - StandardFrame::Call(frame) => frame.step_into::(interp), - StandardFrame::DiGraph(frame) => frame.step_into::(interp), + StandardFrame::Block(frame) => frame.step_into(interp), + StandardFrame::CFG(frame) => frame.step_into(interp), + StandardFrame::Call(frame) => frame.step_into(interp), + StandardFrame::DiGraph(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - StandardFrame::Block(frame) => Ok(frame.resume_done_into::()), - StandardFrame::CFG(frame) => Ok(frame.resume_done_into::()), - StandardFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), - StandardFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), + StandardFrame::Block(frame) => frame.resume_done_into(interp), + StandardFrame::CFG(frame) => frame.resume_done_into(interp), + StandardFrame::Call(frame) => frame.resume_done_into(interp), + StandardFrame::DiGraph(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - StandardFrame::Block(frame) => frame.resume_into::(completion, interp), - StandardFrame::CFG(frame) => frame.resume_into::(completion, interp), - StandardFrame::Call(frame) => frame.resume_into::(completion, interp), - StandardFrame::DiGraph(frame) => frame.resume_into::(completion, interp), + StandardFrame::Block(frame) => frame.resume_into(completion, interp), + StandardFrame::CFG(frame) => frame.resume_into(completion, interp), + StandardFrame::Call(frame) => frame.resume_into(completion, interp), + StandardFrame::DiGraph(frame) => frame.resume_into(completion, interp), } } } diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index 14ec25fa66..e15f9dc980 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -210,7 +210,7 @@ where V: Clone, E: From, Lk: Linker, - F: Frame> + FrameBuild, + F: Frame> + FrameBuild, { /// Resolve `stage`/`function` by name and execute it to completion. pub fn call_by_name( diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index 6d26b60ed3..71f659e8f4 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -77,15 +77,21 @@ where pub fn structured_body(stage: CompileStage, block: Block) -> Self { Self::with_mode(stage, block, DenseBlockMode::StructuredBody) } +} - pub fn step_into( +impl Frame for DenseBlockFrame +where + I: DenseBackwardFrameDriver, + F: DenseFrameBuild, + V: Clone, + E: From, +{ + type Completion = DenseBackwardCompletion; + + fn step_into( mut self, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild, - { + ) -> Result>, E> { let statements = match self.statements.as_ref() { Some(statements) => statements, None => { @@ -147,21 +153,20 @@ where } } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into( + self, + _interp: &mut I, + ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "dense block frames resume only with completions", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild, - { + ) -> Result>, E> { match completion { DenseBackwardCompletion::Structured => { if let Some(statement) = self.pending_point.take() { @@ -197,33 +202,39 @@ impl DenseFrameBuild for StandardDenseBackwardFrame { } } -impl Frame for StandardDenseBackwardFrame +/// A *universe* impl, generic over the outer total frame type `F` — see +/// [`Frame`] for what that buys. +impl Frame for StandardDenseBackwardFrame where - I: DenseBackwardFrameDriver>, + I: DenseBackwardFrameDriver, + F: DenseFrameBuild, V: Clone, E: From, { type Completion = DenseBackwardCompletion; - fn step(self, interp: &mut I) -> Result, E> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - Self::Block(frame) => frame.step_into::(interp), + Self::Block(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, E> { + fn resume_done_into( + self, + interp: &mut I, + ) -> Result>, E> { match self { - Self::Block(frame) => frame.resume_done_into::(), + Self::Block(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result, E> { + ) -> Result>, E> { match self { - Self::Block(frame) => frame.resume_into::(completion, interp), + Self::Block(frame) => frame.resume_into(completion, interp), } } } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 37c9445962..b2f5643206 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -708,7 +708,7 @@ where V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, E: From, Sem: DenseBackwardSemantic, - F: Frame, Completion = DenseBackwardCompletion> + F: Frame, F, Completion = DenseBackwardCompletion> + DenseFrameBuild, { /// Run the block-boundary fixpoint over `cfg` in `stage`: seed every diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index bc8cac4310..e3ba46acce 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -341,7 +341,13 @@ impl DemandFrame { } } -impl<'ir, S, V, E, Sem> Frame> for DemandFrame +// A leaf *universe*, deliberately pinned to `F = Self` rather than generic like +// the other frames. `step_into` returns `FrameEffect::Continue(self)`, so being +// generic over `F` would need a conversion `DemandFrame -> F` — i.e. a public +// `DemandFrameBuild` hook that nothing currently calls. Add it if the sparse +// backward engine ever needs a wrapping frame (an instrumenting layer, say); +// until then the pinned signature states the fact that it cannot be embedded. +impl<'ir, S, V, E, Sem> Frame, Self> for DemandFrame where S: StageMeta + StageQuery + InterpDispatch>, V: Clone + PartialEq + Lattice + HasBottom, @@ -350,7 +356,7 @@ where { type Completion = Vec<(SSAValue, V)>; - fn step( + fn step_into( mut self, interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, ) -> Result, E> { @@ -365,7 +371,7 @@ where } } - fn resume_done( + fn resume_done_into( self, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, ) -> Result, E> { @@ -374,7 +380,7 @@ where ))) } - fn resume( + fn resume_into( self, _completion: Self::Completion, _interp: &mut SparseBackwardDriver<'ir, S, V, E, Sem>, diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs index 11f12a9d2f..03bffa6e4f 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs @@ -138,16 +138,19 @@ where _marker: PhantomData, } } +} - pub fn step_into( - mut self, - interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver - + SparseForwardInterp, - F: AbstractFrameBuild, - { +impl Frame for AbstractBlockFrame +where + I: AbstractFrameDriver + SparseForwardInterp, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, interp: &mut I) -> Result>, E> { // Bind entry arguments lazily on the first step. if let Some(args) = self.pending.take() { interp.bind_block_args(self.stage, self.index, self.block, &args)?; @@ -218,22 +221,15 @@ where } /// A pushed call frame finished: continue walking the body. - pub fn resume_done_into(self) -> FrameEffect> - where - F: AbstractFrameBuild, - { - FrameEffect::Continue(F::from_block(self)) + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { + Ok(FrameEffect::Continue(F::from_block(self))) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild, - { + ) -> Result>, E> { match completion { AbstractCompletion::Finished(Some(values)) => { let slots = self.resume_slots.take().ok_or_else(|| { @@ -241,7 +237,7 @@ where "block resume without result slots", )) })?; - interp.write_results(self.index, &slots, values)?; + crate::FrameDriver::write_results(interp, self.index, &slots, values)?; Ok(FrameEffect::Continue(F::from_block(self))) } // A nested push returned without finishing: this pass left via return. @@ -331,15 +327,37 @@ where } } - pub fn step_into( - mut self, - interp: &mut I, - ) -> Result>, E> + /// Schedule exhausted: read the declared yields out of the activation and + /// complete. The parent decides what the values mean — a graph **owner** + /// turns them into the function's return, a pushing statement binds them + /// into its result slots. + fn finish(self, interp: &mut I) -> Result>, E> where - I: AbstractFrameDriver - + SparseForwardInterp, + I: AbstractFrameDriver, F: AbstractFrameBuild, { + let values: Product = self + .yields + .iter() + .map(|&value| interp.env_read(self.index, value)) + .collect::>()?; + Ok(FrameEffect::Complete(AbstractCompletion::Finished(Some( + values, + )))) + } +} + +impl Frame for AbstractDiGraphFrame +where + I: AbstractFrameDriver + SparseForwardInterp, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, interp: &mut I) -> Result>, E> { // First step: fetch the walk plan and bind the boundary ports. if let Some(args) = self.pending.take() { let plan = interp.digraph_walk_plan(self.stage, self.graph)?; @@ -391,43 +409,17 @@ where } } - /// Schedule exhausted: read the declared yields out of the activation and - /// complete. The parent decides what the values mean — a graph **owner** - /// turns them into the function's return, a pushing statement binds them - /// into its result slots. - fn finish(self, interp: &mut I) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild, - { - let values: Product = self - .yields - .iter() - .map(|&value| interp.env_read(self.index, value)) - .collect::>()?; - Ok(FrameEffect::Complete(AbstractCompletion::Finished(Some( - values, - )))) - } - /// A pushed child finished without a payload (e.g. a summarized call whose /// results are already written): resume the schedule. - pub fn resume_done_into(self) -> Result>, E> - where - F: AbstractFrameBuild, - { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Ok(FrameEffect::Continue(F::from_digraph(self)?)) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild, - { + ) -> Result>, E> { match completion { AbstractCompletion::Finished(Some(values)) => { let slots = self.resume_slots.take().ok_or_else(|| { @@ -435,7 +427,7 @@ where "digraph resume without result slots", )) })?; - interp.write_results(self.index, &slots, values)?; + crate::FrameDriver::write_results(interp, self.index, &slots, values)?; Ok(FrameEffect::Continue(F::from_digraph(self)?)) } // A nested push left via `return`. A digraph has no function-return @@ -481,25 +473,33 @@ where _marker: PhantomData, } } +} - pub fn step_into(self, interp: &mut I) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild, - { +impl Frame for AbstractCallFrame +where + I: AbstractFrameDriver, + F: AbstractFrameBuild, + V: Clone + PartialEq, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(self, interp: &mut I) -> Result>, E> { interp.summarize_call(self.stage, self.call, self.index)?; Ok(FrameEffect::Done) } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "call frame resumed without a return", ))) } - pub fn resume_into( + fn resume_into( self, _completion: AbstractCompletion, + _interp: &mut I, ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "call frame resumed with a completion", @@ -532,43 +532,43 @@ impl AbstractFrameBuild for StandardAbstractFrame { } } -impl Frame for StandardAbstractFrame +/// A *universe* impl, generic over the outer total frame type `F` — see +/// [`Frame`] for what that buys. +impl Frame for StandardAbstractFrame where - I: AbstractFrameDriver - + SparseForwardInterp>, + I: AbstractFrameDriver + SparseForwardInterp, + F: AbstractFrameBuild, V: Clone + PartialEq, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - StandardAbstractFrame::Block(frame) => frame.step_into::(interp), - StandardAbstractFrame::Call(frame) => frame.step_into::(interp), - StandardAbstractFrame::DiGraph(frame) => frame.step_into::(interp), + StandardAbstractFrame::Block(frame) => frame.step_into(interp), + StandardAbstractFrame::Call(frame) => frame.step_into(interp), + StandardAbstractFrame::DiGraph(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - StandardAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - StandardAbstractFrame::Call(frame) => frame.resume_done_into::(), - StandardAbstractFrame::DiGraph(frame) => frame.resume_done_into::(), + StandardAbstractFrame::Block(frame) => frame.resume_done_into(interp), + StandardAbstractFrame::Call(frame) => frame.resume_done_into(interp), + StandardAbstractFrame::DiGraph(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - StandardAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), - StandardAbstractFrame::Call(frame) => frame.resume_into::(completion), - StandardAbstractFrame::DiGraph(frame) => { - frame.resume_into::(completion, interp) - } + StandardAbstractFrame::Block(frame) => frame.resume_into(completion, interp), + StandardAbstractFrame::Call(frame) => frame.resume_into(completion, interp), + StandardAbstractFrame::DiGraph(frame) => frame.resume_into(completion, interp), } } } diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 90642ba280..d7cec3c574 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -1475,7 +1475,7 @@ where Lk: Linker, P: CallContext + WideningStrategy, Sem: SparseForwardSemantic, - F: Frame, Completion = AbstractCompletion> + F: Frame, F, Completion = AbstractCompletion> + AbstractFrameBuild>::Key>, { /// Resolve `stage`/`function` by name and analyze. Returns the function's diff --git a/crates/kirin-interpreter/src/fixpoint/runner.rs b/crates/kirin-interpreter/src/fixpoint/runner.rs index 4887df5871..36857206e7 100644 --- a/crates/kirin-interpreter/src/fixpoint/runner.rs +++ b/crates/kirin-interpreter/src/fixpoint/runner.rs @@ -21,7 +21,7 @@ where /// a fresh stack. pub fn run_frame(&mut self, root: P::Frame) -> Result where - P::Frame: Frame, + P::Frame: Frame, { if !self.frame_stack.is_empty() { return Err(I::Error::from(InterpreterError::Custom( diff --git a/crates/kirin-interpreter/src/fixpoint/solver.rs b/crates/kirin-interpreter/src/fixpoint/solver.rs index db4a1f5f53..3f852f13b8 100644 --- a/crates/kirin-interpreter/src/fixpoint/solver.rs +++ b/crates/kirin-interpreter/src/fixpoint/solver.rs @@ -45,7 +45,7 @@ where /// Analyse `entry` and everything it transitively schedules, to a fixpoint. pub fn solve(&mut self, semantics: &mut Sem, entry: P::SummaryKey) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -69,7 +69,7 @@ where entries: impl IntoIterator, ) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -90,7 +90,7 @@ where iterations: usize, ) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -113,7 +113,7 @@ where /// Pop and analyse owners until the worklist is empty. pub fn drain_worklist(&mut self, semantics: &mut Sem) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, @@ -184,7 +184,7 @@ where owner: P::SummaryKey, ) -> Result<(), I::Error> where - P::Frame: Frame, + P::Frame: Frame, Sem: OwnerSemantics, Deps: SummaryDependencyIndex, InterpreterError: From, diff --git a/crates/kirin-interpreter/src/fixpoint/tests/counter.rs b/crates/kirin-interpreter/src/fixpoint/tests/counter.rs index a1d489fd69..d8270094b1 100644 --- a/crates/kirin-interpreter/src/fixpoint/tests/counter.rs +++ b/crates/kirin-interpreter/src/fixpoint/tests/counter.rs @@ -39,22 +39,22 @@ impl Summary for CounterSummary { #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct CounterFrame(u8); -impl> Frame for CounterFrame { +impl, F> Frame for CounterFrame { type Completion = u8; - fn step(self, _interp: &mut D) -> Result, InterpreterError> { + fn step_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Complete(self.0.saturating_add(1).min(2))) } - fn resume_done(self, _interp: &mut D) -> Result, InterpreterError> { + fn resume_done_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Done) } - fn resume( + fn resume_into( self, completion: u8, _interp: &mut D, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Complete(completion)) } } diff --git a/crates/kirin-interpreter/src/fixpoint/tests/deps.rs b/crates/kirin-interpreter/src/fixpoint/tests/deps.rs index 0275493f2c..1793421688 100644 --- a/crates/kirin-interpreter/src/fixpoint/tests/deps.rs +++ b/crates/kirin-interpreter/src/fixpoint/tests/deps.rs @@ -41,22 +41,22 @@ struct DepFrame { owner: u8, } -impl> Frame for DepFrame { +impl, F> Frame for DepFrame { type Completion = u8; - fn step(self, _interp: &mut D) -> Result, InterpreterError> { + fn step_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Complete(self.owner.saturating_add(1))) } - fn resume_done(self, _interp: &mut D) -> Result, InterpreterError> { + fn resume_done_into(self, _interp: &mut D) -> Result, InterpreterError> { Ok(FrameEffect::Done) } - fn resume( + fn resume_into( self, completion: u8, _interp: &mut D, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Complete(completion)) } } diff --git a/crates/kirin-interpreter/src/fixpoint/tests/phase.rs b/crates/kirin-interpreter/src/fixpoint/tests/phase.rs index 8115cf14ae..79966a881e 100644 --- a/crates/kirin-interpreter/src/fixpoint/tests/phase.rs +++ b/crates/kirin-interpreter/src/fixpoint/tests/phase.rs @@ -42,10 +42,10 @@ struct PhaseFrame; type PhaseInterp = StandardFixpointInterpreter>; -impl Frame for PhaseFrame { +impl Frame for PhaseFrame { type Completion = u8; - fn step(self, interp: &mut PhaseInterp) -> Result, InterpreterError> { + fn step_into(self, interp: &mut PhaseInterp) -> Result, InterpreterError> { let completion = match interp.phase() { FixpointPhase::Join => 1, FixpointPhase::Widen => 10, @@ -54,18 +54,18 @@ impl Frame for PhaseFrame { Ok(FrameEffect::Complete(completion)) } - fn resume_done( + fn resume_done_into( self, _interp: &mut PhaseInterp, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Done) } - fn resume( + fn resume_into( self, completion: u8, _interp: &mut PhaseInterp, - ) -> Result, InterpreterError> { + ) -> Result, InterpreterError> { Ok(FrameEffect::Complete(completion)) } } diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 30bd1d6344..87b31748ae 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -102,6 +102,7 @@ where + InterpDispatch>, F: Frame< DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, + F, Completion = DenseBackwardCompletion, > + DenseFrameBuild, { diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs index 192a114ef6..3560e1b765 100644 --- a/crates/kirin-liveness/src/result.rs +++ b/crates/kirin-liveness/src/result.rs @@ -72,6 +72,7 @@ impl DenseLivenessResult { + InterpDispatch>, F: Frame< DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, + F, Completion = DenseBackwardCompletion, > + DenseFrameBuild, { diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index ac4d5fec69..2259f8ebb5 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -21,6 +21,7 @@ //! (and the abstract equivalents [`BuildAbstractScfIf`]/[`BuildAbstractScfFor`]). use std::collections::VecDeque; +use std::hash::Hash; use std::marker::PhantomData; use kirin::prelude::Lattice; @@ -34,7 +35,7 @@ use kirin_interpreter::{ AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BlockFrame, CallContext, Completion, ConcreteInterpreter, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, EnvIndex, - FrameBuild, FrameDriver, FrameEffect, SparseForwardTransfer, + Frame, FrameBuild, FrameDriver, FrameEffect, SparseForwardTransfer, }; use crate::{For, ForLoopValue, If, Yield}; @@ -299,17 +300,21 @@ impl DenseScfIfFrame { _marker: PhantomData, } } +} + +impl Frame for DenseScfIfFrame +where + I: DenseBackwardFrameDriver, + F: DenseFrameBuild + BuildDenseScfIf, + V: Clone + Lattice, + E: From, +{ + type Completion = DenseBackwardCompletion; - pub fn step_into( + fn step_into( mut self, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfIf, - V: Clone + Lattice, - E: From, - { + ) -> Result>, E> { if self.after.is_none() { self.after = Some(interp.state()); } @@ -334,17 +339,11 @@ impl DenseScfIfFrame { } } - pub fn resume_into( + fn resume_into( mut self, completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfIf, - V: Clone + Lattice, - E: From, - { + ) -> Result>, E> { match completion { DenseBackwardCompletion::Structured => { let arm_entry = interp.state(); @@ -360,10 +359,10 @@ impl DenseScfIfFrame { } } - pub fn resume_done_into(self) -> Result>, E> - where - E: From, - { + fn resume_done_into( + self, + _interp: &mut I, + ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.if dense frames resume only with completions", ))) @@ -413,17 +412,21 @@ impl DenseScfForFrame { let seed = self.seed.clone().expect("seed captured"); seed.join(&body_entry.rename(&self.params[1..], &self.yields)) } +} + +impl Frame for DenseScfForFrame +where + I: DenseBackwardFrameDriver, + F: DenseFrameBuild + BuildDenseScfFor, + V: Clone + PartialEq + Lattice + DenseBackwardState, + E: From, +{ + type Completion = DenseBackwardCompletion; - pub fn step_into( + fn step_into( mut self, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfFor, - V: Clone + PartialEq + Lattice + DenseBackwardState, - E: From, - { + ) -> Result>, E> { if self.seed.is_none() { self.seed = Some(interp.state()); self.params = interp.block_params(self.stage, self.body)?; @@ -439,17 +442,11 @@ impl DenseScfForFrame { }) } - pub fn resume_into( + fn resume_into( mut self, completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result>, E> - where - I: DenseBackwardFrameDriver, - F: DenseFrameBuild + BuildDenseScfFor, - V: Clone + PartialEq + Lattice + DenseBackwardState, - E: From, - { + ) -> Result>, E> { match completion { DenseBackwardCompletion::Structured => { let body_entry = interp.state(); @@ -473,10 +470,10 @@ impl DenseScfForFrame { } } - pub fn resume_done_into(self) -> Result>, E> - where - E: From, - { + fn resume_done_into( + self, + _interp: &mut I, + ) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.for dense frames resume only with completions", ))) @@ -677,12 +674,18 @@ where _marker: PhantomData, } } +} - pub fn step_into(self, _interp: &mut I) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild + BuildScfIf, - { +impl Frame for ScfIfFrame +where + I: FrameDriver, + F: FrameBuild + BuildScfIf, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into(self, _interp: &mut I) -> Result>, E> { let arm = match self.decided { Some(true) => self.then_body, Some(false) => self.else_body, @@ -695,15 +698,16 @@ where }) } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.if frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( self, completion: Completion, + _interp: &mut I, ) -> Result>, E> { match completion { // The arm's structured yield: its values are this operation's @@ -772,15 +776,19 @@ where self.acc = Some(merged); Ok(()) } +} - pub fn step_into( - mut self, - _interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfIf, - { +impl Frame for AbstractScfIfFrame +where + I: AbstractFrameDriver, + F: AbstractFrameBuild + BuildAbstractScfIf, + V: Clone + PartialEq + Lattice, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, _interp: &mut I) -> Result>, E> { match self.remaining.pop_front() { None => Ok(FrameEffect::Complete(AbstractCompletion::Finished( self.acc, @@ -795,21 +803,17 @@ where } } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.if frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfIf, - { + ) -> Result>, E> { match completion { AbstractCompletion::Finished(Some(values)) => { self.join_acc(interp, values)?; @@ -872,12 +876,18 @@ where _marker: PhantomData, } } +} - pub fn step_into(self, interp: &mut I) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild + BuildScfFor, - { +impl Frame for ScfForFrame +where + I: FrameDriver, + F: FrameBuild + BuildScfFor, + V: Clone + ForLoopValue, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { let end = interp.env_read(self.env, self.end)?; match self.induction.loop_condition(&end) { Some(true) => { @@ -895,21 +905,17 @@ where } } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.for frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: Completion, interp: &mut I, - ) -> Result>, E> - where - I: FrameDriver, - F: FrameBuild + BuildScfFor, - { + ) -> Result>, E> { match completion { // The body's structured yield: advance the induction variable, // carry the yielded values forward, and re-check the condition. @@ -1011,15 +1017,19 @@ where self.finish = Some(merged); Ok(()) } +} - pub fn step_into( - mut self, - interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfFor, - { +impl Frame for AbstractScfForFrame +where + I: AbstractFrameDriver, + F: AbstractFrameBuild + BuildAbstractScfFor, + V: Clone + PartialEq + ForLoopValue + Lattice, + E: From, + K: Clone + Eq + Hash, +{ + type Completion = AbstractCompletion; + + fn step_into(mut self, interp: &mut I) -> Result>, E> { if !self.entered { self.entered = true; let end = interp.env_read(self.env, self.end)?; @@ -1043,21 +1053,17 @@ where }) } - pub fn resume_done_into(self) -> Result>, E> { + fn resume_done_into(self, _interp: &mut I) -> Result>, E> { Err(E::from(InterpreterError::Custom( "scf.for frame resumed without a body completion", ))) } - pub fn resume_into( + fn resume_into( mut self, completion: AbstractCompletion, interp: &mut I, - ) -> Result>, E> - where - I: AbstractFrameDriver, - F: AbstractFrameBuild + BuildAbstractScfFor, - { + ) -> Result>, E> { let yielded = match completion { AbstractCompletion::Finished(Some(values)) => values, // The body returned: the loop finishes with what it has joined. diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 7a35529550..f400daaed6 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -437,30 +437,51 @@ pub enum FrameEffect { Continue(F), Push { parent: F, child: F }, Done, Co pub trait FrameEngine { type Error; } // direction-neutral anchor (no value domain) impl FrameEngine for T { type Error = ::Error; } -pub trait Frame: Sized { // implemented by the *total* frame enum +// ONE interface, implemented by every frame — individual walkers and total enums alike. +// The effects are over `F`, the total frame type composed into, never over `Self`. +pub trait Frame: Sized { type Completion; - fn step(self, &mut I) -> Result, I::Error>; - fn resume_done(self, &mut I) -> Result, I::Error>; - fn resume(self, Self::Completion, &mut I) -> Result, I::Error>; + fn step_into(self, &mut I) -> Result, I::Error>; + fn resume_done_into(self, &mut I) -> Result, I::Error>; + fn resume_into(self, Self::Completion, &mut I) -> Result, I::Error>; } -// The one shared, direction-neutral driver loop, used by every engine: -pub fn drive_frames>(engine: &mut I, frames: &mut Vec) +// The one shared, direction-neutral driver loop, used by every engine. The +// stack's element type must be a *universe* — `F: Frame`. +pub fn drive_frames>(engine: &mut I, frames: &mut Vec) -> Result; // Forward-specific capability surface (alias: FrameDriver): pub trait ForwardFrameDriver: Env { /* env alloc/free, IR queries, dispatch, resolution */ } ``` +**Members and universes.** The `F` parameter is what lets one trait serve both +roles a frame stack needs: + +- a **member** — an individual walker (`BlockFrame`, `CallFrame`, a dialect's own + frame). It is one variant of `F` and names its successors in `F`, re-wrapping + itself through the relevant `*FrameBuild` hook. Members are generic over `F`, + so the same walker composes into any language's frame type. +- a **universe** — a total frame enum. It implements `Frame` when it is + the stack's element type, and stays generic over `F` so it can *also* be + embedded in a larger enum without re-enumerating its variants. `TracingFrame` + in `toy-lang`'s tests is exactly this: a newtype wrapping `ToyFrame` whole, + counting steps and delegating. (A leaf universe with no members, like the + sparse backward `DemandFrame`, honestly pins `F = Self`.) + +The stack must be homogeneous in *type* while heterogeneous in *kind*, which is +why `F` is a closed sum type rather than `Box`: a run holds a +`CallFrame`, a `CFGFrame`, a `BlockFrame` and a dialect frame simultaneously. + `Frame` is anchored only on `FrameEngine` (a total `Error`), **not** on the forward value engine `Interp` — so the frame protocol is decoupled from forward value interpretation and reusable by other analyses. Every `Interp` is a `FrameEngine` by blanket impl. The engine owns a `Vec` and calls -`drive_frames`, which pops the top frame, `step`s it, and applies the returned -`FrameEffect`. `ForwardFrameDriver: Env` is the richer **forward** capability -surface the *forward* frames call (it requires `Env` because the default -`bind_block_args`/`write_results` use `env_write`); **both forward engines implement -it**. The concrete and +`drive_frames`, which pops the top frame, `step_into`s it, and applies the +returned `FrameEffect`. `ForwardFrameDriver: Env` is the richer **forward** +capability surface the *forward* frames call (it requires `Env` because the +default `bind_block_args`/`write_results` use `env_write`); **both forward +engines implement it**. The concrete and abstract standard frames are two *implementations* of this one protocol — not parallel frameworks. diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 657e3c7216..b2e0e1a4ca 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -63,48 +63,49 @@ impl BuildScfFor for ToyFrame { } } -impl Frame for ToyFrame +impl Frame for ToyFrame where - I: FrameDriver + SparseForwardInterp>, + I: FrameDriver + SparseForwardInterp, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, { type Completion = Completion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ToyFrame::Block(frame) => frame.step_into::(interp), - ToyFrame::CFG(frame) => frame.step_into::(interp), - ToyFrame::Call(frame) => frame.step_into::(interp), - ToyFrame::DiGraph(frame) => frame.step_into::(interp), - ToyFrame::ScfIf(frame) => frame.step_into::(interp), - ToyFrame::ScfFor(frame) => frame.step_into::(interp), + ToyFrame::Block(frame) => frame.step_into(interp), + ToyFrame::CFG(frame) => frame.step_into(interp), + ToyFrame::Call(frame) => frame.step_into(interp), + ToyFrame::DiGraph(frame) => frame.step_into(interp), + ToyFrame::ScfIf(frame) => frame.step_into(interp), + ToyFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - ToyFrame::Block(frame) => Ok(frame.resume_done_into::()), - ToyFrame::CFG(frame) => Ok(frame.resume_done_into::()), - ToyFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), - ToyFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), - ToyFrame::ScfIf(frame) => frame.resume_done_into::(), - ToyFrame::ScfFor(frame) => frame.resume_done_into::(), + ToyFrame::Block(frame) => frame.resume_done_into(interp), + ToyFrame::CFG(frame) => frame.resume_done_into(interp), + ToyFrame::Call(frame) => frame.resume_done_into(interp), + ToyFrame::DiGraph(frame) => frame.resume_done_into(interp), + ToyFrame::ScfIf(frame) => frame.resume_done_into(interp), + ToyFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ToyFrame::Block(frame) => frame.resume_into::(completion, interp), - ToyFrame::CFG(frame) => frame.resume_into::(completion, interp), - ToyFrame::Call(frame) => frame.resume_into::(completion, interp), - ToyFrame::DiGraph(frame) => frame.resume_into::(completion, interp), - ToyFrame::ScfIf(frame) => frame.resume_into::(completion), - ToyFrame::ScfFor(frame) => frame.resume_into::(completion, interp), + ToyFrame::Block(frame) => frame.resume_into(completion, interp), + ToyFrame::CFG(frame) => frame.resume_into(completion, interp), + ToyFrame::Call(frame) => frame.resume_into(completion, interp), + ToyFrame::DiGraph(frame) => frame.resume_into(completion, interp), + ToyFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ToyFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } @@ -142,44 +143,44 @@ impl BuildAbstractScfFor for ToyAbstractFrame { } } -impl Frame for ToyAbstractFrame +impl Frame for ToyAbstractFrame where - I: AbstractFrameDriver - + SparseForwardInterp>, - V: Clone + PartialEq + ForLoopValue, + I: AbstractFrameDriver + SparseForwardInterp, + F: AbstractFrameBuild + BuildAbstractScfIf + BuildAbstractScfFor, + V: Clone + PartialEq + ForLoopValue + Lattice, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ToyAbstractFrame::Block(frame) => frame.step_into::(interp), - ToyAbstractFrame::Call(frame) => frame.step_into::(interp), - ToyAbstractFrame::ScfIf(frame) => frame.step_into::(interp), - ToyAbstractFrame::ScfFor(frame) => frame.step_into::(interp), + ToyAbstractFrame::Block(frame) => frame.step_into(interp), + ToyAbstractFrame::Call(frame) => frame.step_into(interp), + ToyAbstractFrame::ScfIf(frame) => frame.step_into(interp), + ToyAbstractFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - ToyAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - ToyAbstractFrame::Call(frame) => frame.resume_done_into::(), - ToyAbstractFrame::ScfIf(frame) => frame.resume_done_into::(), - ToyAbstractFrame::ScfFor(frame) => frame.resume_done_into::(), + ToyAbstractFrame::Block(frame) => frame.resume_done_into(interp), + ToyAbstractFrame::Call(frame) => frame.resume_done_into(interp), + ToyAbstractFrame::ScfIf(frame) => frame.resume_done_into(interp), + ToyAbstractFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ToyAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), - ToyAbstractFrame::Call(frame) => frame.resume_into::(completion), - ToyAbstractFrame::ScfIf(frame) => frame.resume_into::(completion, interp), - ToyAbstractFrame::ScfFor(frame) => frame.resume_into::(completion, interp), + ToyAbstractFrame::Block(frame) => frame.resume_into(completion, interp), + ToyAbstractFrame::Call(frame) => frame.resume_into(completion, interp), + ToyAbstractFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ToyAbstractFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } @@ -222,41 +223,43 @@ impl BuildDenseScfFor for ToyDenseBackwardFrame { } } -impl Frame for ToyDenseBackwardFrame +impl Frame for ToyDenseBackwardFrame where - I: DenseBackwardFrameDriver>, + I: DenseBackwardFrameDriver, + F: DenseFrameBuild + BuildDenseScfIf + BuildDenseScfFor, V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, { type Completion = DenseBackwardCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ToyDenseBackwardFrame::Block(frame) => frame.step_into::(interp), - ToyDenseBackwardFrame::ScfIf(frame) => frame.step_into::(interp), - ToyDenseBackwardFrame::ScfFor(frame) => frame.step_into::(interp), + ToyDenseBackwardFrame::Block(frame) => frame.step_into(interp), + ToyDenseBackwardFrame::ScfIf(frame) => frame.step_into(interp), + ToyDenseBackwardFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into( + self, + interp: &mut I, + ) -> Result>, E> { match self { - ToyDenseBackwardFrame::Block(frame) => frame.resume_done_into::(), - ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_done_into::(), - ToyDenseBackwardFrame::ScfFor(frame) => frame.resume_done_into::(), + ToyDenseBackwardFrame::Block(frame) => frame.resume_done_into(interp), + ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_done_into(interp), + ToyDenseBackwardFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: DenseBackwardCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ToyDenseBackwardFrame::Block(frame) => frame.resume_into::(completion, interp), - ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_into::(completion, interp), - ToyDenseBackwardFrame::ScfFor(frame) => { - frame.resume_into::(completion, interp) - } + ToyDenseBackwardFrame::Block(frame) => frame.resume_into(completion, interp), + ToyDenseBackwardFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ToyDenseBackwardFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 268d5aa527..b03b61dbfa 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -577,7 +577,9 @@ mod advanced { }; use super::build_pipeline; - use crate::interpreter::ToyError; + use kirin::prelude::Lattice; + + use crate::interpreter::{ToyAbstractFrame, ToyError, ToyFrame}; use crate::stage::Stage; // --- A custom total frame enum ----------------------------------------- @@ -599,99 +601,70 @@ mod advanced { body_steps: usize, } - enum TracingFrame { - Block(BlockFrame), - CFG(CFGFrame), - Call(CallFrame), - DiGraph(DiGraphFrame), - ScfIf(ScfIfFrame), - ScfFor(ScfForFrame), - } + /// A *wrapper* universe: it does not re-enumerate the language's frames, it + /// embeds `ToyFrame` whole and observes it. Only possible because `Frame`'s + /// effects are over the outer total type `F`, not over `Self` — so + /// `ToyFrame: Frame` holds as soon as `TracingFrame` + /// implements the build traits. + struct TracingFrame(ToyFrame); impl FrameBuild for TracingFrame { fn from_block(frame: BlockFrame) -> Self { - TracingFrame::Block(frame) + Self(ToyFrame::Block(frame)) } fn from_cfg(frame: CFGFrame) -> Self { - TracingFrame::CFG(frame) + Self(ToyFrame::CFG(frame)) } fn from_call(frame: CallFrame) -> Self { - TracingFrame::Call(frame) + Self(ToyFrame::Call(frame)) } fn from_digraph(frame: DiGraphFrame) -> Self { - TracingFrame::DiGraph(frame) + Self(ToyFrame::DiGraph(frame)) } } impl BuildScfIf for TracingFrame { fn scf_if(frame: ScfIfFrame) -> Self { - TracingFrame::ScfIf(frame) + Self(ToyFrame::ScfIf(frame)) } } impl BuildScfFor for TracingFrame { fn scf_for(frame: ScfForFrame) -> Self { - TracingFrame::ScfFor(frame) + Self(ToyFrame::ScfFor(frame)) } } - impl Frame for TracingFrame + impl Frame for TracingFrame where - I: FrameDriver + SparseForwardInterp>, + I: FrameDriver + SparseForwardInterp, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, { type Completion = Completion; - fn step(self, interp: &mut I) -> Result, I::Error> { - match self { - TracingFrame::Block(frame) => { - TRACE.with(|t| t.borrow_mut().body_steps += 1); - frame.step_into::(interp) - } - TracingFrame::CFG(frame) => { - TRACE.with(|t| t.borrow_mut().body_steps += 1); - frame.step_into::(interp) + fn step_into(self, interp: &mut I) -> Result>, E> { + match &self.0 { + ToyFrame::Block(_) | ToyFrame::CFG(_) => { + TRACE.with(|t| t.borrow_mut().body_steps += 1) } - TracingFrame::Call(frame) => { - TRACE.with(|t| t.borrow_mut().calls += 1); - frame.step_into::(interp) - } - TracingFrame::DiGraph(frame) => frame.step_into::(interp), - TracingFrame::ScfIf(frame) => frame.step_into::(interp), - TracingFrame::ScfFor(frame) => frame.step_into::(interp), + ToyFrame::Call(_) => TRACE.with(|t| t.borrow_mut().calls += 1), + _ => {} } + self.0.step_into(interp) } - fn resume_done( - self, - _interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingFrame::Block(frame) => Ok(frame.resume_done_into::()), - TracingFrame::CFG(frame) => Ok(frame.resume_done_into::()), - TracingFrame::Call(frame) => { - frame.resume_done_into::().map_err(I::Error::from) - } - TracingFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), - TracingFrame::ScfIf(frame) => frame.resume_done_into::(), - TracingFrame::ScfFor(frame) => frame.resume_done_into::(), - } + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + self.0.resume_done_into(interp) } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingFrame::Block(frame) => frame.resume_into::(completion, interp), - TracingFrame::CFG(frame) => frame.resume_into::(completion, interp), - TracingFrame::Call(frame) => frame.resume_into::(completion, interp), - TracingFrame::DiGraph(frame) => frame.resume_into::(completion, interp), - TracingFrame::ScfIf(frame) => frame.resume_into::(completion), - TracingFrame::ScfFor(frame) => frame.resume_into::(completion, interp), - } + ) -> Result>, E> { + self.0.resume_into(completion, interp) } } @@ -777,91 +750,65 @@ mod advanced { calls: usize, } - enum TracingAbstractFrame { - Block(AbstractBlockFrame), - Call(AbstractCallFrame), - ScfIf(AbstractScfIfFrame), - ScfFor(AbstractScfForFrame), - } + /// The abstract analogue of [`TracingFrame`]: a wrapper universe embedding + /// `ToyAbstractFrame` whole rather than re-listing its variants. + struct TracingAbstractFrame(ToyAbstractFrame); impl AbstractFrameBuild for TracingAbstractFrame { fn from_block(frame: AbstractBlockFrame) -> Self { - TracingAbstractFrame::Block(frame) + Self(ToyAbstractFrame::Block(frame)) } fn from_call(frame: AbstractCallFrame) -> Self { - TracingAbstractFrame::Call(frame) + Self(ToyAbstractFrame::Call(frame)) } } impl BuildAbstractScfIf for TracingAbstractFrame { fn scf_if(frame: AbstractScfIfFrame) -> Self { - TracingAbstractFrame::ScfIf(frame) + Self(ToyAbstractFrame::ScfIf(frame)) } } impl BuildAbstractScfFor for TracingAbstractFrame { fn scf_for(frame: AbstractScfForFrame) -> Self { - TracingAbstractFrame::ScfFor(frame) + Self(ToyAbstractFrame::ScfFor(frame)) } } - impl Frame for TracingAbstractFrame + impl Frame for TracingAbstractFrame where I: AbstractFrameDriver - + SparseForwardInterp>, - V: Clone + PartialEq + ForLoopValue, + + SparseForwardInterp, + F: AbstractFrameBuild + BuildAbstractScfIf + BuildAbstractScfFor, + V: Clone + PartialEq + ForLoopValue + Lattice, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { - match self { - TracingAbstractFrame::Block(frame) => { - ATRACE.with(|t| t.borrow_mut().block_steps += 1); - frame.step_into::(interp) - } - TracingAbstractFrame::Call(frame) => { - ATRACE.with(|t| t.borrow_mut().calls += 1); - frame.step_into::(interp) - } - TracingAbstractFrame::ScfIf(frame) => { - ATRACE.with(|t| t.borrow_mut().if_steps += 1); - frame.step_into::(interp) - } - TracingAbstractFrame::ScfFor(frame) => frame.step_into::(interp), + fn step_into(self, interp: &mut I) -> Result>, E> { + match &self.0 { + ToyAbstractFrame::Block(_) => ATRACE.with(|t| t.borrow_mut().block_steps += 1), + ToyAbstractFrame::Call(_) => ATRACE.with(|t| t.borrow_mut().calls += 1), + ToyAbstractFrame::ScfIf(_) => ATRACE.with(|t| t.borrow_mut().if_steps += 1), + ToyAbstractFrame::ScfFor(_) => {} } + self.0.step_into(interp) } - fn resume_done( + fn resume_done_into( self, - _interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - TracingAbstractFrame::Call(frame) => frame.resume_done_into::(), - TracingAbstractFrame::ScfIf(frame) => frame.resume_done_into::(), - TracingAbstractFrame::ScfFor(frame) => frame.resume_done_into::(), - } + interp: &mut I, + ) -> Result>, E> { + self.0.resume_done_into(interp) } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { - match self { - TracingAbstractFrame::Block(frame) => { - frame.resume_into::(completion, interp) - } - TracingAbstractFrame::Call(frame) => frame.resume_into::(completion), - TracingAbstractFrame::ScfIf(frame) => { - frame.resume_into::(completion, interp) - } - TracingAbstractFrame::ScfFor(frame) => { - frame.resume_into::(completion, interp) - } - } + ) -> Result>, E> { + self.0.resume_into(completion, interp) } } diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index d64f6873ec..1b4c8d773d 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -319,49 +319,49 @@ impl BuildScfFor for ScfTestFrame { } } -impl Frame for ScfTestFrame +impl Frame for ScfTestFrame where - I: FrameDriver - + kirin_interpreter::SparseForwardInterp>, + I: FrameDriver + SparseForwardInterp, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + kirin_scf::ForLoopValue, E: From, { type Completion = Completion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - ScfTestFrame::Block(frame) => frame.step_into::(interp), - ScfTestFrame::CFG(frame) => frame.step_into::(interp), - ScfTestFrame::Call(frame) => frame.step_into::(interp), - ScfTestFrame::DiGraph(frame) => frame.step_into::(interp), - ScfTestFrame::ScfIf(frame) => frame.step_into::(interp), - ScfTestFrame::ScfFor(frame) => frame.step_into::(interp), + ScfTestFrame::Block(frame) => frame.step_into(interp), + ScfTestFrame::CFG(frame) => frame.step_into(interp), + ScfTestFrame::Call(frame) => frame.step_into(interp), + ScfTestFrame::DiGraph(frame) => frame.step_into(interp), + ScfTestFrame::ScfIf(frame) => frame.step_into(interp), + ScfTestFrame::ScfFor(frame) => frame.step_into(interp), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - ScfTestFrame::Block(frame) => Ok(frame.resume_done_into::()), - ScfTestFrame::CFG(frame) => Ok(frame.resume_done_into::()), - ScfTestFrame::Call(frame) => frame.resume_done_into::().map_err(I::Error::from), - ScfTestFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), - ScfTestFrame::ScfIf(frame) => frame.resume_done_into::(), - ScfTestFrame::ScfFor(frame) => frame.resume_done_into::(), + ScfTestFrame::Block(frame) => frame.resume_done_into(interp), + ScfTestFrame::CFG(frame) => frame.resume_done_into(interp), + ScfTestFrame::Call(frame) => frame.resume_done_into(interp), + ScfTestFrame::DiGraph(frame) => frame.resume_done_into(interp), + ScfTestFrame::ScfIf(frame) => frame.resume_done_into(interp), + ScfTestFrame::ScfFor(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - ScfTestFrame::Block(frame) => frame.resume_into::(completion, interp), - ScfTestFrame::CFG(frame) => frame.resume_into::(completion, interp), - ScfTestFrame::Call(frame) => frame.resume_into::(completion, interp), - ScfTestFrame::DiGraph(frame) => frame.resume_into::(completion, interp), - ScfTestFrame::ScfIf(frame) => frame.resume_into::(completion), - ScfTestFrame::ScfFor(frame) => frame.resume_into::(completion, interp), + ScfTestFrame::Block(frame) => frame.resume_into(completion, interp), + ScfTestFrame::CFG(frame) => frame.resume_into(completion, interp), + ScfTestFrame::Call(frame) => frame.resume_into(completion, interp), + ScfTestFrame::DiGraph(frame) => frame.resume_into(completion, interp), + ScfTestFrame::ScfIf(frame) => frame.resume_into(completion, interp), + ScfTestFrame::ScfFor(frame) => frame.resume_into(completion, interp), } } } @@ -565,10 +565,14 @@ impl UnGraphChainFrame { outputs: Vec::new(), } } +} + +impl<'ir> Frame, UnPolicyFrame> for UnGraphChainFrame { + type Completion = Completion; - fn step( + fn step_into( mut self, - interp: &mut UnEngine<'_>, + interp: &mut UnEngine<'ir>, ) -> Result>, TestError> { // First step: bind the boundary ports and fix the policy's schedule // and output convention from the graph's structure. @@ -623,62 +627,73 @@ impl UnGraphChainFrame { } } } + + fn resume_done_into( + self, + _interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + Err(TestError::Core(InterpreterError::Custom( + "the chain policy pushes no children", + ))) + } + + fn resume_into( + self, + _completion: Completion, + _interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { + Err(TestError::Core(InterpreterError::Custom( + "the chain policy pushes no children", + ))) + } } type UnEngine<'ir> = ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, UnPolicyFrame>; -impl<'ir> Frame> for UnPolicyFrame { +// Pinned to `F = Self` rather than generic: the `Chain` variant holds the +// compiler-supplied `UnGraphChainFrame`, which has no `*FrameBuild` hook (it is +// constructed through `FrameBuild::from_ungraph_entry`), so there is no way to +// re-wrap it into an arbitrary outer `F`. +impl<'ir> Frame, Self> for UnPolicyFrame { type Completion = Completion; - fn step( + fn step_into( self, interp: &mut UnEngine<'ir>, - ) -> Result, TestError> { + ) -> Result>, TestError> { match self { - UnPolicyFrame::Block(frame) => frame.step_into::, Self>(interp), - UnPolicyFrame::CFG(frame) => frame.step_into::, Self>(interp), - UnPolicyFrame::Call(frame) => frame.step_into::, Self>(interp), - UnPolicyFrame::DiGraph(frame) => frame.step_into::, Self>(interp), - UnPolicyFrame::Chain(frame) => frame.step(interp), + UnPolicyFrame::Block(frame) => frame.step_into(interp), + UnPolicyFrame::CFG(frame) => frame.step_into(interp), + UnPolicyFrame::Call(frame) => frame.step_into(interp), + UnPolicyFrame::DiGraph(frame) => frame.step_into(interp), + UnPolicyFrame::Chain(frame) => frame.step_into(interp), } } - fn resume_done( + fn resume_done_into( self, - _interp: &mut UnEngine<'ir>, - ) -> Result, TestError> { + interp: &mut UnEngine<'ir>, + ) -> Result>, TestError> { match self { - UnPolicyFrame::Block(frame) => Ok(frame.resume_done_into::()), - UnPolicyFrame::CFG(frame) => Ok(frame.resume_done_into::()), - UnPolicyFrame::Call(frame) => frame.resume_done_into::().map_err(TestError::from), - UnPolicyFrame::DiGraph(frame) => Ok(frame.resume_done_into::()), - UnPolicyFrame::Chain(_) => Err(TestError::Core(InterpreterError::Custom( - "the chain policy pushes no children", - ))), + UnPolicyFrame::Block(frame) => frame.resume_done_into(interp), + UnPolicyFrame::CFG(frame) => frame.resume_done_into(interp), + UnPolicyFrame::Call(frame) => frame.resume_done_into(interp), + UnPolicyFrame::DiGraph(frame) => frame.resume_done_into(interp), + UnPolicyFrame::Chain(frame) => frame.resume_done_into(interp), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: Completion, interp: &mut UnEngine<'ir>, - ) -> Result, TestError> { + ) -> Result>, TestError> { match self { - UnPolicyFrame::Block(frame) => { - frame.resume_into::, Self>(completion, interp) - } - UnPolicyFrame::CFG(frame) => { - frame.resume_into::, Self>(completion, interp) - } - UnPolicyFrame::Call(frame) => { - frame.resume_into::, Self>(completion, interp) - } - UnPolicyFrame::DiGraph(frame) => { - frame.resume_into::, Self>(completion, interp) - } - UnPolicyFrame::Chain(_) => Err(TestError::Core(InterpreterError::Custom( - "the chain policy pushes no children", - ))), + UnPolicyFrame::Block(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::CFG(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::Call(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::DiGraph(frame) => frame.resume_into(completion, interp), + UnPolicyFrame::Chain(frame) => frame.resume_into(completion, interp), } } } @@ -810,50 +825,44 @@ impl FrameBuild for GraphAbstractFrame { } } -impl Frame for GraphAbstractFrame +impl Frame for GraphAbstractFrame where - I: AbstractFrameDriver - + SparseForwardInterp>, + I: AbstractFrameDriver + SparseForwardInterp, + F: AbstractFrameBuild, V: Clone + PartialEq, E: From, K: Clone + Eq + Hash, { type Completion = AbstractCompletion; - fn step(self, interp: &mut I) -> Result, I::Error> { + fn step_into(self, interp: &mut I) -> Result>, E> { match self { - GraphAbstractFrame::Block(frame) => frame.step_into::(interp), - GraphAbstractFrame::Call(frame) => frame.step_into::(interp), - GraphAbstractFrame::DiGraph(frame) => frame.step_into::(interp), - GraphAbstractFrame::NoWalker(reason) => { - Err(I::Error::from(InterpreterError::Custom(reason))) - } + GraphAbstractFrame::Block(frame) => frame.step_into(interp), + GraphAbstractFrame::Call(frame) => frame.step_into(interp), + GraphAbstractFrame::DiGraph(frame) => frame.step_into(interp), + GraphAbstractFrame::NoWalker(reason) => Err(E::from(InterpreterError::Custom(reason))), } } - fn resume_done(self, _interp: &mut I) -> Result, I::Error> { + fn resume_done_into(self, interp: &mut I) -> Result>, E> { match self { - GraphAbstractFrame::Block(frame) => Ok(frame.resume_done_into::()), - GraphAbstractFrame::Call(frame) => frame.resume_done_into::(), - GraphAbstractFrame::DiGraph(frame) => frame.resume_done_into::(), - GraphAbstractFrame::NoWalker(reason) => { - Err(I::Error::from(InterpreterError::Custom(reason))) - } + GraphAbstractFrame::Block(frame) => frame.resume_done_into(interp), + GraphAbstractFrame::Call(frame) => frame.resume_done_into(interp), + GraphAbstractFrame::DiGraph(frame) => frame.resume_done_into(interp), + GraphAbstractFrame::NoWalker(reason) => Err(E::from(InterpreterError::Custom(reason))), } } - fn resume( + fn resume_into( self, - completion: Self::Completion, + completion: AbstractCompletion, interp: &mut I, - ) -> Result, I::Error> { + ) -> Result>, E> { match self { - GraphAbstractFrame::Block(frame) => frame.resume_into::(completion, interp), - GraphAbstractFrame::Call(frame) => frame.resume_into::(completion), - GraphAbstractFrame::DiGraph(frame) => frame.resume_into::(completion, interp), - GraphAbstractFrame::NoWalker(reason) => { - Err(I::Error::from(InterpreterError::Custom(reason))) - } + GraphAbstractFrame::Block(frame) => frame.resume_into(completion, interp), + GraphAbstractFrame::Call(frame) => frame.resume_into(completion, interp), + GraphAbstractFrame::DiGraph(frame) => frame.resume_into(completion, interp), + GraphAbstractFrame::NoWalker(reason) => Err(E::from(InterpreterError::Custom(reason))), } } } From 2b12a2b6c64a149130c8c4415be07acd0a0d0cec Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Fri, 31 Jul 2026 17:31:14 -0400 Subject: [PATCH 12/21] refactored topology queries already answered in IR --- .../src/frame_build.rs | 393 ++++++++++++++++++ crates/kirin-derive-interpreter/src/lib.rs | 36 ++ ...rete_frame_enum_with_dialect_variants.snap | 20 + ...ests__crate_path_override_is_honoured.snap | 20 + ...ts__dense_backward_single_constructor.snap | 11 + ...ward_digraph_is_fallible_when_present.snap | 17 + ...rd_omits_optional_digraph_when_absent.snap | 14 + crates/kirin-interpreter/src/core/mod.rs | 2 +- crates/kirin-interpreter/src/core/query.rs | 5 +- crates/kirin-interpreter/src/core/topology.rs | 104 ++--- .../src/engines/dense_backward/interp.rs | 6 +- crates/kirin-interpreter/src/lib.rs | 6 +- example/toy-lang/src/interpreter/frame.rs | 41 +- tests/body_kinds.rs | 29 +- 14 files changed, 588 insertions(+), 116 deletions(-) create mode 100644 crates/kirin-derive-interpreter/src/frame_build.rs create mode 100644 crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap create mode 100644 crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap create mode 100644 crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap create mode 100644 crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap create mode 100644 crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap diff --git a/crates/kirin-derive-interpreter/src/frame_build.rs b/crates/kirin-derive-interpreter/src/frame_build.rs new file mode 100644 index 0000000000..53baba118d --- /dev/null +++ b/crates/kirin-derive-interpreter/src/frame_build.rs @@ -0,0 +1,393 @@ +//! Code generation for the frame-injection derives: `#[derive(FrameBuild)]`, +//! `#[derive(AbstractFrameBuild)]`, `#[derive(DenseFrameBuild)]`. +//! +//! A language that runs the interpreter declares a **total frame enum** — the +//! one Rust type the driver's `Vec` stack holds. Each framework walker it +//! carries must be injectable into that enum, which is what the `*FrameBuild` +//! traits are for. The impls are pure transcription: one constructor per +//! framework frame, each body `Self::Variant(frame)`. +//! +//! These derives write that transcription. The **derive name selects the +//! family** — `FrameBuild` (concrete), `AbstractFrameBuild` (sparse forward), +//! `DenseFrameBuild` (dense backward) — so no attribute is needed, and the name +//! matches the trait it implements as the other interpreter derives do. +//! +//! Variants are matched to constructors by their **field type**, not their +//! variant name, so renaming a variant cannot silently change what is +//! generated. Variants holding a *dialect* frame (`ScfIfFrame`, …) are ignored: +//! those are injected through the dialect's own `Build*` trait, declared by the +//! dialect and implemented by hand. +//! +//! Not derivable, by design: an enum that supplies a callable-`UnGraph` policy. +//! `FrameBuild::from_ungraph_entry` is a defaulted method and a derive emits the +//! whole impl block, so such an enum keeps its hand-written impl (see +//! `UnPolicyFrame` in the workspace `body_kinds` test). + +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +const DEFAULT_INTERP_CRATE: &str = "::kirin_interpreter"; + +/// One constructor of an injection trait. +struct Ctor { + /// The framework frame type this constructor accepts, matched against a + /// variant's field type by its final path segment. + frame: &'static str, + /// The trait method to generate. + method: &'static str, + /// Whether the trait requires it. Optional constructors have a defaulted + /// implementation in the trait and are simply omitted when no variant + /// carries the frame. + required: bool, + /// Whether the method returns `Result` rather than `Self` — the + /// trait lets these refuse, so the generated body wraps in `Ok`. + fallible: bool, +} + +/// A frame family: its injection trait, that trait's arity, and its +/// constructors. +pub struct Family { + trait_name: &'static str, + /// Number of type parameters the trait takes, which must equal the number + /// the deriving enum declares. + arity: usize, + ctors: &'static [Ctor], +} + +pub const CONCRETE: Family = Family { + trait_name: "FrameBuild", + arity: 2, + ctors: &[ + Ctor { + frame: "BlockFrame", + method: "from_block", + required: true, + fallible: false, + }, + Ctor { + frame: "CFGFrame", + method: "from_cfg", + required: true, + fallible: false, + }, + Ctor { + frame: "CallFrame", + method: "from_call", + required: true, + fallible: false, + }, + Ctor { + frame: "DiGraphFrame", + method: "from_digraph", + required: true, + fallible: false, + }, + ], +}; + +pub const SPARSE_FORWARD: Family = Family { + trait_name: "AbstractFrameBuild", + arity: 3, + ctors: &[ + Ctor { + frame: "AbstractBlockFrame", + method: "from_block", + required: true, + fallible: false, + }, + Ctor { + frame: "AbstractCallFrame", + method: "from_call", + required: true, + fallible: false, + }, + // Graph bodies are opt-in: the trait's default refuses, so an enum + // without this variant simply inherits the refusal. + Ctor { + frame: "AbstractDiGraphFrame", + method: "from_digraph", + required: false, + fallible: true, + }, + ], +}; + +pub const DENSE_BACKWARD: Family = Family { + trait_name: "DenseFrameBuild", + arity: 2, + ctors: &[Ctor { + frame: "DenseBlockFrame", + method: "from_block", + required: true, + fallible: false, + }], +}; + +/// Reads the `#[interpret(crate = ...)]` override, reusing the namespace the +/// other interpreter derives already use. +fn parse_interp_crate_path(input: &DeriveInput) -> syn::Result { + let mut crate_path = None; + for attr in &input.attrs { + if !attr.path().is_ident("interpret") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("crate") { + crate_path = Some(meta.value()?.parse()?); + Ok(()) + } else { + Err(meta.error("unsupported attribute for #[interpret(...)]")) + } + })?; + } + match crate_path { + Some(path) => Ok(path), + None => syn::parse_str(DEFAULT_INTERP_CRATE), + } +} + +/// The final path segment of a type, used to recognize a framework frame. +fn type_head(ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(path) => path.path.segments.last().map(|s| s.ident.to_string()), + _ => None, + } +} + +pub fn generate(input: &DeriveInput, family: &Family) -> syn::Result { + let trait_ident: syn::Ident = syn::parse_str(family.trait_name)?; + let interp_crate = parse_interp_crate_path(input)?; + + let syn::Data::Enum(data) = &input.data else { + return Err(syn::Error::new_spanned( + input, + format!( + "`{}` can only be derived for a total frame enum", + family.trait_name + ), + )); + }; + + // The trait's type arguments are the enum's own type parameters, in order — + // `enum ToyFrame` implements `FrameBuild`. Reject a mismatch + // here rather than emitting an impl that fails to resolve later. + let type_params: Vec<&syn::Ident> = input.generics.type_params().map(|p| &p.ident).collect(); + if type_params.len() != family.arity { + return Err(syn::Error::new_spanned( + input, + format!( + "`{}` expects an enum with {} type parameter(s) to match `{}<{}>`, found {}. \ + An enum whose trait arguments are not its own type parameters must implement the trait by hand.", + family.trait_name, + family.arity, + family.trait_name, + vec!["_"; family.arity].join(", "), + type_params.len(), + ), + )); + } + + // Match each variant to a constructor by its field type. + let mut methods = Vec::new(); + for ctor in family.ctors { + let mut matched = None; + for variant in &data.variants { + let syn::Fields::Unnamed(fields) = &variant.fields else { + continue; + }; + if fields.unnamed.len() != 1 { + continue; + } + let field_ty = &fields.unnamed[0].ty; + if type_head(field_ty).as_deref() == Some(ctor.frame) { + if matched.is_some() { + return Err(syn::Error::new_spanned( + variant, + format!( + "two variants hold a `{}`; `{}::{}` would be ambiguous", + ctor.frame, family.trait_name, ctor.method + ), + )); + } + matched = Some((&variant.ident, field_ty)); + } + } + + let Some((variant_ident, field_ty)) = matched else { + if ctor.required { + return Err(syn::Error::new_spanned( + input, + format!( + "no variant holds a `{}`, which `{}` requires for `{}`. \ + Add such a variant, or implement the trait by hand.", + ctor.frame, family.trait_name, ctor.method + ), + )); + } + continue; + }; + + let method: syn::Ident = syn::parse_str(ctor.method)?; + // The parameter type is copied verbatim from the variant, so the frame + // type's own generic arity never has to be reconstructed here. + methods.push(if ctor.fallible { + let err = type_params[1]; + quote! { + fn #method(frame: #field_ty) -> ::core::result::Result { + ::core::result::Result::Ok(Self::#variant_ident(frame)) + } + } + } else { + quote! { + fn #method(frame: #field_ty) -> Self { + Self::#variant_ident(frame) + } + } + }); + } + + let enum_ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics #interp_crate::#trait_ident<#(#type_params),*> + for #enum_ident #ty_generics #where_clause + { + #(#methods)* + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use kirin_test_utils::rustfmt; + + fn emit(input: syn::DeriveInput, family: &Family) -> String { + rustfmt( + generate(&input, family) + .expect("codegen failed") + .to_string(), + ) + } + + #[test] + fn concrete_frame_enum_with_dialect_variants() { + // The two scf variants are ignored — they are injected through + // `BuildScfIf`/`BuildScfFor`, which the dialect declares. + let input: syn::DeriveInput = syn::parse_quote! { + enum ToyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + ScfIf(ScfIfFrame), + ScfFor(ScfForFrame), + } + }; + insta::assert_snapshot!(emit(input, &CONCRETE)); + } + + #[test] + fn sparse_forward_omits_optional_digraph_when_absent() { + let input: syn::DeriveInput = syn::parse_quote! { + enum ToyAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + ScfIf(AbstractScfIfFrame), + } + }; + insta::assert_snapshot!(emit(input, &SPARSE_FORWARD)); + } + + #[test] + fn sparse_forward_digraph_is_fallible_when_present() { + let input: syn::DeriveInput = syn::parse_quote! { + enum StandardAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), + } + }; + insta::assert_snapshot!(emit(input, &SPARSE_FORWARD)); + } + + #[test] + fn dense_backward_single_constructor() { + let input: syn::DeriveInput = syn::parse_quote! { + enum ToyDenseBackwardFrame { + Block(DenseBlockFrame), + ScfIf(DenseScfIfFrame), + } + }; + insta::assert_snapshot!(emit(input, &DENSE_BACKWARD)); + } + + #[test] + fn crate_path_override_is_honoured() { + let input: syn::DeriveInput = syn::parse_quote! { + #[interpret(crate = crate)] + enum StandardFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + insta::assert_snapshot!(emit(input, &CONCRETE)); + } + + #[test] + fn rejects_missing_required_frame() { + let input: syn::DeriveInput = syn::parse_quote! { + enum Incomplete { + Block(BlockFrame), + } + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!(err.contains("no variant holds a `CFGFrame`"), "{err}"); + } + + #[test] + fn rejects_wrong_type_param_count() { + let input: syn::DeriveInput = syn::parse_quote! { + enum Weird { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!( + err.contains("expects an enum with 2 type parameter(s)"), + "{err}" + ); + } + + #[test] + fn rejects_ambiguous_duplicate_frame() { + let input: syn::DeriveInput = syn::parse_quote! { + enum Dup { + Block(BlockFrame), + AlsoBlock(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!(err.contains("would be ambiguous"), "{err}"); + } + + #[test] + fn rejects_non_enum() { + let input: syn::DeriveInput = syn::parse_quote! { + struct NotAnEnum(BlockFrame); + }; + let err = generate(&input, &CONCRETE).unwrap_err().to_string(); + assert!(err.contains("total frame enum"), "{err}"); + } +} diff --git a/crates/kirin-derive-interpreter/src/lib.rs b/crates/kirin-derive-interpreter/src/lib.rs index 1c80fb6178..07f92859dc 100644 --- a/crates/kirin-derive-interpreter/src/lib.rs +++ b/crates/kirin-derive-interpreter/src/lib.rs @@ -1,5 +1,6 @@ extern crate proc_macro; +mod frame_build; mod function_entry; mod interp_dispatch; mod interpretable; @@ -41,3 +42,38 @@ pub fn derive_interp_dispatch(input: TokenStream) -> TokenStream { Err(e) => e.into_compile_error().into(), } } + +/// Derive `FrameBuild` for a **concrete** total frame enum: one +/// constructor per framework walker it carries, matched by field type. Variants +/// holding dialect frames are ignored — those are injected through the dialect's +/// own `Build*` trait. +#[proc_macro_derive(FrameBuild, attributes(interpret))] +pub fn derive_frame_build(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + match frame_build::generate(&ast, &frame_build::CONCRETE) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Derive `AbstractFrameBuild` for a **sparse forward** total frame +/// enum. `from_digraph` is emitted only when a variant carries an +/// `AbstractDiGraphFrame`; otherwise the trait's refusing default is inherited. +#[proc_macro_derive(AbstractFrameBuild, attributes(interpret))] +pub fn derive_abstract_frame_build(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + match frame_build::generate(&ast, &frame_build::SPARSE_FORWARD) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Derive `DenseFrameBuild` for a **dense backward** total frame enum. +#[proc_macro_derive(DenseFrameBuild, attributes(interpret))] +pub fn derive_dense_frame_build(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + match frame_build::generate(&ast, &frame_build::DENSE_BACKWARD) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap new file mode 100644 index 0000000000..5e2e54cadb --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap @@ -0,0 +1,20 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 256 +expression: "emit(input, &CONCRETE)" +--- +#[automatically_derived] +impl ::kirin_interpreter::FrameBuild for ToyFrame { + fn from_block(frame: BlockFrame) -> Self { + Self::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self::DiGraph(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap new file mode 100644 index 0000000000..d9d36bd9a2 --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap @@ -0,0 +1,20 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 305 +expression: "emit(input, &CONCRETE)" +--- +#[automatically_derived] +impl crate::FrameBuild for StandardFrame { + fn from_block(frame: BlockFrame) -> Self { + Self::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self::DiGraph(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap new file mode 100644 index 0000000000..f0bb0b5f48 --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__dense_backward_single_constructor.snap @@ -0,0 +1,11 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 291 +expression: "emit(input, &DENSE_BACKWARD)" +--- +#[automatically_derived] +impl ::kirin_interpreter::DenseFrameBuild for ToyDenseBackwardFrame { + fn from_block(frame: DenseBlockFrame) -> Self { + Self::Block(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap new file mode 100644 index 0000000000..2fe6d0bcd1 --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_digraph_is_fallible_when_present.snap @@ -0,0 +1,17 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 280 +expression: "emit(input, &SPARSE_FORWARD)" +--- +#[automatically_derived] +impl ::kirin_interpreter::AbstractFrameBuild for StandardAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + Self::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: AbstractDiGraphFrame) -> ::core::result::Result { + ::core::result::Result::Ok(Self::DiGraph(frame)) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap new file mode 100644 index 0000000000..21aa450e0e --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__sparse_forward_omits_optional_digraph_when_absent.snap @@ -0,0 +1,14 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +assertion_line: 268 +expression: "emit(input, &SPARSE_FORWARD)" +--- +#[automatically_derived] +impl ::kirin_interpreter::AbstractFrameBuild for ToyAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + Self::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + Self::Call(frame) + } +} diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index 7970b999b4..308bc44d3f 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -25,5 +25,5 @@ pub use frame::{ pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use query::{GraphWalkPlan, StageQuery}; -pub use topology::{BlockTopology, BodyTopology, GraphTopology, body_topology}; +pub use topology::{BlockTopology, BodyTopology, body_topology}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index fe958c4896..7d9891ba30 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -278,8 +278,9 @@ where } /// The topology of a body: blocks and graph parts (including nested -/// structured bodies), statements per part, CFG successors, block feeders, -/// and graph-port boundaries. +/// structured bodies), statements per part, block feeders, and graph-port +/// boundaries. Forward CFG successors are deliberately absent — those are a +/// local IR query on a block's terminator. pub struct BodyTopologyQuery(pub Body); impl StageAction for BodyTopologyQuery diff --git a/crates/kirin-interpreter/src/core/topology.rs b/crates/kirin-interpreter/src/core/topology.rs index a5addb78a4..825339ee46 100644 --- a/crates/kirin-interpreter/src/core/topology.rs +++ b/crates/kirin-interpreter/src/core/topology.rs @@ -3,15 +3,22 @@ //! //! Backward analyses need the *shape* of a body: which blocks and graph //! nodes exist (including bodies nested inside structured statements), each -//! block's statements, the CFG successor relation, each block's *feeders* — -//! the statements whose rules can translate demand on that block's -//! parameters (terminators targeting it, statements owning it) — and each -//! graph port's *boundary* (the statement owning the graph, and the port's -//! slot index). This is topology only — uses/defs/edge-argument *semantics* -//! stay in dialect [`Interpretable`](crate::Interpretable) rules; the -//! enumeration consumes the generic [`HasSuccessors`]/[`HasBlocks`]/ -//! [`HasCFG`]/[`HasDigraphs`]/[`HasUngraphs`] contract every dialect -//! derives. +//! block's statements, each block's *feeders* — the statements whose rules can +//! translate demand on that block's parameters (terminators targeting it, +//! statements owning it) — and each graph port's *boundary* (the statement +//! owning the graph, and the port's slot index). This is topology only — +//! uses/defs/edge-argument *semantics* stay in dialect +//! [`Interpretable`](crate::Interpretable) rules; the enumeration consumes the +//! generic [`HasSuccessors`]/[`HasBlocks`]/[`HasCFG`]/[`HasDigraphs`]/ +//! [`HasUngraphs`] contract every dialect derives. +//! +//! Note what is deliberately *not* here: the forward CFG successor relation. +//! That is a local IR query — `stmt.definition(stage).successors()` on a +//! block's terminator answers it — so materializing a copy would duplicate IR +//! state that can go stale. `feeders` is the *reverse* relation and is not +//! obtainable from a block alone (finding who targets it requires sweeping the +//! whole body), which is why this prepass materializes that one and not the +//! forward one. use std::collections::{HashMap, HashSet}; @@ -22,40 +29,33 @@ use kirin_ir::{ use crate::{Body, PortBoundary}; -/// The shape of one block: its statements and CFG successors. +/// The shape of one block: which block it is and the statements it contains. #[derive(Clone, Debug)] pub struct BlockTopology { pub block: Block, /// Statements in program order; the terminator, if any, is last. pub stmts: Vec, - /// CFG successor blocks (targets of the block's terminator). - pub successors: Vec, /// `true` for blocks nested inside a statement (structured bodies), /// `false` for the analyzed body's own top-level blocks. pub nested: bool, } -/// The shape of one graph body: its node statements, in declaration order. -/// -/// Order is enumeration order, not a schedule — execution scheduling is the -/// walker's job, and backward prepasses only need *all* statements. -#[derive(Clone, Debug)] -pub struct GraphTopology { - /// `Body::DiGraph(..)` or `Body::UnGraph(..)`. - pub graph: Body, - pub stmts: Vec, - /// `true` for graphs nested inside a statement, `false` for the analyzed - /// body itself. - pub nested: bool, -} - /// The shape of a body: all blocks and graph parts (the analyzed body's own /// plus structured bodies, recursively), the block-feeder index, and the /// graph-port boundary index. +/// +/// Graph parts contribute only their node statements, flattened: nothing +/// consumes per-graph grouping, the graph handle, or a nested flag, so none is +/// recorded. Order is enumeration order, not a schedule — scheduling is the +/// walker's job, and backward prepasses only need *all* statements. #[derive(Clone, Debug, Default)] pub struct BodyTopology { pub blocks: Vec, - pub graphs: Vec, + /// Position of each block within `blocks`, so a lookup by [`Block`] is O(1) + /// rather than a scan. Built during collection; holds no statements of its + /// own. + block_index: HashMap, + graph_stmts: Vec, feeders: HashMap>, port_boundary: HashMap, } @@ -73,6 +73,18 @@ impl BodyTopology { self.blocks.iter().filter(|block| !block.nested) } + /// The statements of `block` in program order (terminator last), if this + /// topology enumerated it. + /// + /// O(1) via `block_index`. Worth indexing: the dense backward engine asks + /// this once per block-owner analysis, and owners are re-analyzed until the + /// fixpoint converges — a scan here is O(blocks) per iteration. + pub fn block_statements(&self, block: Block) -> Option<&[Statement]> { + self.block_index + .get(&block) + .map(|&position| self.blocks[position].stmts.as_slice()) + } + /// Where `port` sits on its owning statement's boundary, if the port /// belongs to a graph enumerated by this topology. pub fn port_boundary(&self, port: impl Into) -> Option<&PortBoundary> { @@ -85,7 +97,7 @@ impl BodyTopology { self.blocks .iter() .flat_map(|block| block.stmts.iter().copied()) - .chain(self.graphs.iter().flat_map(|g| g.stmts.iter().copied())) + .chain(self.graph_stmts.iter().copied()) } } @@ -107,10 +119,10 @@ where collect_block(stage, block, false, &mut topology, &mut visited); } Body::DiGraph(graph) => { - collect_digraph(stage, graph, false, &mut topology, &mut visited); + collect_digraph(stage, graph, &mut topology, &mut visited); } Body::UnGraph(graph) => { - collect_ungraph(stage, graph, false, &mut topology, &mut visited); + collect_ungraph(stage, graph, &mut topology, &mut visited); } } topology @@ -135,20 +147,22 @@ fn collect_block( stmts.push(terminator); } - // CFG successor edges: the target's feeders include the terminator. - let mut successors = Vec::new(); + // Record the *reverse* edge only: a statement with an edge into `target` + // is one of `target`'s feeders. The forward direction is left to the IR. for &stmt in &stmts { for successor in stmt.definition(stage).successors() { - let target = successor.target(); - successors.push(target); - topology.feeders.entry(target).or_default().push(stmt); + topology + .feeders + .entry(successor.target()) + .or_default() + .push(stmt); } } + topology.block_index.insert(block, topology.blocks.len()); topology.blocks.push(BlockTopology { block, stmts: stmts.clone(), - successors, nested, }); @@ -160,7 +174,6 @@ fn collect_block( fn collect_digraph( stage: &StageInfo, graph: DiGraph, - nested: bool, topology: &mut BodyTopology, visited: &mut HashSet, ) where @@ -170,11 +183,7 @@ fn collect_digraph( let info = graph.expect_info(stage); let stmts: Vec = info.graph().node_weights().copied().collect(); record_ports(info.parent(), info.ports(), topology); - topology.graphs.push(GraphTopology { - graph: Body::DiGraph(graph), - stmts: stmts.clone(), - nested, - }); + topology.graph_stmts.extend(stmts.iter().copied()); for stmt in stmts { collect_owned_bodies(stage, stmt, topology, visited); } @@ -183,7 +192,6 @@ fn collect_digraph( fn collect_ungraph( stage: &StageInfo, graph: UnGraph, - nested: bool, topology: &mut BodyTopology, visited: &mut HashSet, ) where @@ -193,11 +201,7 @@ fn collect_ungraph( let info = graph.expect_info(stage); let stmts: Vec = info.graph().node_weights().copied().collect(); record_ports(info.parent(), info.ports(), topology); - topology.graphs.push(GraphTopology { - graph: Body::UnGraph(graph), - stmts: stmts.clone(), - nested, - }); + topology.graph_stmts.extend(stmts.iter().copied()); for stmt in stmts { collect_owned_bodies(stage, stmt, topology, visited); } @@ -231,10 +235,10 @@ fn collect_owned_bodies( } } for owned in owned_digraphs { - collect_digraph(stage, owned, true, topology, visited); + collect_digraph(stage, owned, topology, visited); } for owned in owned_ungraphs { - collect_ungraph(stage, owned, true, topology, visited); + collect_ungraph(stage, owned, topology, visited); } } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index b2f5643206..591ee96538 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -493,10 +493,8 @@ where fn block_statements(&self, block: Block) -> Result, E> { self.store() .topology - .blocks - .iter() - .find(|candidate| candidate.block == block) - .map(|candidate| candidate.stmts.clone()) + .block_statements(block) + .map(<[Statement]>::to_vec) .ok_or_else(|| E::from(InterpreterError::MissingBlock(block))) } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index ac160bf346..109cabac2e 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -77,7 +77,7 @@ pub use self::core::{FunctionEntry, InterpDispatch, Interpretable}; pub use self::core::{InterpreterError, StageQuery}; // The body-shape IR query: what blocks/graphs a body contains, which // statements feed a block's parameters, and where a graph port sits. -pub use self::core::{BlockTopology, BodyTopology, GraphTopology, body_topology}; +pub use self::core::{BlockTopology, BodyTopology, body_topology}; // The shared, direction-neutral frame protocol (`Frame`/`FrameEngine`/ // `FrameEffect`/`drive_frames`) plus the forward frame-driver capability surfaces. pub use self::core::{ @@ -145,7 +145,9 @@ pub use fixpoint::{ }; #[cfg(feature = "derive")] -pub use kirin_derive_interpreter::{FunctionEntry, InterpDispatch, Interpretable}; +pub use kirin_derive_interpreter::{ + AbstractFrameBuild, DenseFrameBuild, FrameBuild, FunctionEntry, InterpDispatch, Interpretable, +}; /// Everything a dialect author needs to implement statement semantics — /// forward evaluation (`Interpretable`), backward demand diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index b2e0e1a4ca..19ac184448 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -27,6 +27,12 @@ use kirin_scf::{ /// Concrete total frame: the standard representation walkers and call /// boundary plus the SCF if/for frames. +/// +/// The framework injections are derived — one constructor per walker, matched by +/// field type. The two scf variants are injected through `kirin-scf`'s own +/// `BuildScfIf`/`BuildScfFor`, which the dialect declares and a language +/// implements by hand. +#[derive(FrameBuild)] pub enum ToyFrame { Block(BlockFrame), CFG(CFGFrame), @@ -36,21 +42,6 @@ pub enum ToyFrame { ScfFor(ScfForFrame), } -impl FrameBuild for ToyFrame { - fn from_block(frame: BlockFrame) -> Self { - ToyFrame::Block(frame) - } - fn from_cfg(frame: CFGFrame) -> Self { - ToyFrame::CFG(frame) - } - fn from_call(frame: CallFrame) -> Self { - ToyFrame::Call(frame) - } - fn from_digraph(frame: DiGraphFrame) -> Self { - ToyFrame::DiGraph(frame) - } -} - impl BuildScfIf for ToyFrame { fn scf_if(frame: ScfIfFrame) -> Self { ToyFrame::ScfIf(frame) @@ -115,6 +106,10 @@ where // =========================================================================== /// Abstract total frame: standard abstract traversal plus the SCF if/for frames. +/// +/// No `AbstractDiGraphFrame` variant, so the derive omits `from_digraph` and the +/// trait's refusing default applies — toy-lang has no graph bodies. +#[derive(AbstractFrameBuild)] pub enum ToyAbstractFrame { Block(AbstractBlockFrame), Call(AbstractCallFrame), @@ -122,15 +117,6 @@ pub enum ToyAbstractFrame { ScfFor(AbstractScfForFrame), } -impl AbstractFrameBuild for ToyAbstractFrame { - fn from_block(frame: AbstractBlockFrame) -> Self { - ToyAbstractFrame::Block(frame) - } - fn from_call(frame: AbstractCallFrame) -> Self { - ToyAbstractFrame::Call(frame) - } -} - impl BuildAbstractScfIf for ToyAbstractFrame { fn scf_if(frame: AbstractScfIfFrame) -> Self { ToyAbstractFrame::ScfIf(frame) @@ -199,18 +185,13 @@ use kirin::prelude::Lattice; /// Dense backward total frame: the standard block walk plus the SCF dense /// frames (arm-join for `scf.if`, the loop-carried fixpoint for `scf.for`). +#[derive(DenseFrameBuild)] pub enum ToyDenseBackwardFrame { Block(DenseBlockFrame), ScfIf(DenseScfIfFrame), ScfFor(DenseScfForFrame), } -impl DenseFrameBuild for ToyDenseBackwardFrame { - fn from_block(frame: DenseBlockFrame) -> Self { - ToyDenseBackwardFrame::Block(frame) - } -} - impl BuildDenseScfIf for ToyDenseBackwardFrame { fn scf_if(frame: DenseScfIfFrame) -> Self { ToyDenseBackwardFrame::ScfIf(frame) diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 1b4c8d773d..99d520ad2e 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -283,6 +283,7 @@ enum ScfLanguage { /// Total frame enum for the scf tests: the standard representation walkers /// and call boundary plus the dialect-owned SCF frames (composition, not an /// engine fork). +#[derive(FrameBuild)] enum ScfTestFrame { Block(BlockFrame), CFG(CFGFrame), @@ -292,21 +293,6 @@ enum ScfTestFrame { ScfFor(ScfForFrame), } -impl FrameBuild for ScfTestFrame { - fn from_block(frame: BlockFrame) -> Self { - ScfTestFrame::Block(frame) - } - fn from_cfg(frame: CFGFrame) -> Self { - ScfTestFrame::CFG(frame) - } - fn from_call(frame: CallFrame) -> Self { - ScfTestFrame::Call(frame) - } - fn from_digraph(frame: DiGraphFrame) -> Self { - ScfTestFrame::DiGraph(frame) - } -} - impl BuildScfIf for ScfTestFrame { fn scf_if(frame: ScfIfFrame) -> Self { ScfTestFrame::ScfIf(frame) @@ -790,6 +776,7 @@ type CpKey = >::Key; /// silently running concrete traversal over lattice values. Giving `graph_eval` /// a per-engine dispatch trait (as `kirin-scf` does for `scf.if`/`scf.for`) /// would remove the need for both the bound and this variant. +#[derive(AbstractFrameBuild)] enum GraphAbstractFrame { Block(AbstractBlockFrame), Call(AbstractCallFrame), @@ -798,18 +785,6 @@ enum GraphAbstractFrame { NoWalker(&'static str), } -impl AbstractFrameBuild for GraphAbstractFrame { - fn from_block(frame: AbstractBlockFrame) -> Self { - GraphAbstractFrame::Block(frame) - } - fn from_call(frame: AbstractCallFrame) -> Self { - GraphAbstractFrame::Call(frame) - } - fn from_digraph(frame: AbstractDiGraphFrame) -> Result { - Ok(GraphAbstractFrame::DiGraph(frame)) - } -} - impl FrameBuild for GraphAbstractFrame { fn from_block(_: BlockFrame) -> Self { GraphAbstractFrame::NoWalker("no abstract walker for a concrete Block frame") From d1801fab50ba69bc53ef888907c44f33ae98b83b Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Mon, 3 Aug 2026 10:38:57 -0400 Subject: [PATCH 13/21] Add call body traversal policy and tests - Added a test to validate the mismatch between callable-body policies when the `#[interpret(body_frames = ...)]` attribute is not specified --- AGENTS.md | 2 + .../src/frame_build.rs | 88 +++- ...sts__body_frames_override_is_honoured.snap | 20 + ...rete_frame_enum_with_dialect_variants.snap | 3 +- ...ests__crate_path_override_is_honoured.snap | 3 +- .../src/engines/concrete/frames/call_frame.rs | 62 ++- .../src/engines/concrete/frames/mod.rs | 4 +- .../src/engines/concrete/frames/protocol.rs | 145 ++++- .../engines/concrete/frames/standard_frame.rs | 10 +- .../src/engines/concrete/mod.rs | 4 +- crates/kirin-interpreter/src/lib.rs | 11 +- docs/design/interpreter/index.md | 46 ++ example/toy-lang/src/interpreter/frame.rs | 7 +- example/toy-lang/src/interpreter/tests.rs | 8 +- tests/body_kinds.rs | 497 +++++++++++++++++- .../call_frame_policy_mismatch.rs | 46 ++ .../call_frame_policy_mismatch.stderr | 13 + 17 files changed, 907 insertions(+), 62 deletions(-) create mode 100644 crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap create mode 100644 tests/compile-fail/call_frame_policy_mismatch.rs create mode 100644 tests/compile-fail/call_frame_policy_mismatch.stderr diff --git a/AGENTS.md b/AGENTS.md index 6036e75e4d..94221888d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -157,6 +157,8 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Engines run frames; traversal lives in frames**: both engines share one driver loop, `drive_frames` (`core/frame.rs`), over the direction-neutral `Frame` protocol — pop the top frame, `step_into`, apply the returned `FrameEffect`, owning no traversal logic. **One trait covers both roles**: a *member* (an individual walker) is generic over the total frame type `F` it composes into and names its successors in `F`; a *universe* (a language's total frame enum) implements `Frame` when it is the stack's element type but stays generic over `F`, so it can itself be embedded in a larger enum — `drive_frames` bounds on `F: Frame`. That is how `toy-lang`'s `TracingFrame` is a newtype wrapping `ToyFrame` whole rather than a copy of its variants. `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>` 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>` (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). +- **Callable-body walkers are a concrete policy**: `CallFrame` owns the call convention (resolve, allocate the activation, enter, suspend, validate the completion, free exactly once, bind results) and delegates *only* which walker enters the callee body to `CallBodyFramePolicy`, selected via `FrameBuild::BodyFrames` (default `DefaultBodyFrames`: `CFG`→`CFGFrame`, `Block`→`BlockFrame`, `DiGraph`→`DiGraphFrame`, `UnGraph`→`FrameBuild::from_ungraph_entry`). `CallFrame` still means `CallFrame`, so no existing language changes. `#[derive(FrameBuild)]` emits the default; `#[interpret(body_frames = MyBodyFrames)]` overrides it. **Concrete execution only** — forward abstract interpretation summarizes calls (`AbstractCallFrame`) and maps a callable body to an `Owner` in `seed_entry_block` instead of descending, and the backward engines never walk callable bodies through a call frame; customizing those would need engine-family-specific policies, and IR owners must never supply walkers. This policy is *not* consulted for nested bodies: `scf.if`/`scf.for` and other structured operations keep choosing their own dialect frames through their dispatch traits. + - **Customizing traversal**: `core/frame.rs` defines the shared `Frame`/`FrameEffect`/`drive_frames` protocol plus forward driver traits. Every frame — walker or enum — implements the same three methods (`step_into`/`resume_done_into`/`resume_into`), all returning `Result, I::Error>`, so a total enum's match arms are uniform across variants. 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` 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`. diff --git a/crates/kirin-derive-interpreter/src/frame_build.rs b/crates/kirin-derive-interpreter/src/frame_build.rs index 53baba118d..832e36ab3e 100644 --- a/crates/kirin-derive-interpreter/src/frame_build.rs +++ b/crates/kirin-derive-interpreter/src/frame_build.rs @@ -53,6 +53,12 @@ pub struct Family { /// the deriving enum declares. arity: usize, ctors: &'static [Ctor], + /// `true` for the concrete family, whose `FrameBuild` carries a + /// `BodyFrames` associated type selecting the callable-body walkers. The + /// abstract and dense families have no such policy — forward abstract + /// interpretation summarizes calls rather than descending into them, and the + /// backward engines do not walk callable bodies through a call frame. + body_frames: bool, } pub const CONCRETE: Family = Family { @@ -84,6 +90,7 @@ pub const CONCRETE: Family = Family { fallible: false, }, ], + body_frames: true, }; pub const SPARSE_FORWARD: Family = Family { @@ -111,6 +118,7 @@ pub const SPARSE_FORWARD: Family = Family { fallible: true, }, ], + body_frames: false, }; pub const DENSE_BACKWARD: Family = Family { @@ -122,12 +130,21 @@ pub const DENSE_BACKWARD: Family = Family { required: true, fallible: false, }], + body_frames: false, }; -/// Reads the `#[interpret(crate = ...)]` override, reusing the namespace the +/// The `#[interpret(..)]` options these derives read, reusing the namespace the /// other interpreter derives already use. -fn parse_interp_crate_path(input: &DeriveInput) -> syn::Result { +struct Options { + crate_path: syn::Path, + /// `#[interpret(body_frames = MyBodyFrames)]` — the callable-body walker + /// policy for the concrete family. `None` means [`DefaultBodyFrames`]. + body_frames: Option, +} + +fn parse_options(input: &DeriveInput) -> syn::Result { let mut crate_path = None; + let mut body_frames = None; for attr in &input.attrs { if !attr.path().is_ident("interpret") { continue; @@ -136,15 +153,21 @@ fn parse_interp_crate_path(input: &DeriveInput) -> syn::Result { if meta.path.is_ident("crate") { crate_path = Some(meta.value()?.parse()?); Ok(()) + } else if meta.path.is_ident("body_frames") { + body_frames = Some(meta.value()?.parse()?); + Ok(()) } else { Err(meta.error("unsupported attribute for #[interpret(...)]")) } })?; } - match crate_path { - Some(path) => Ok(path), - None => syn::parse_str(DEFAULT_INTERP_CRATE), - } + Ok(Options { + crate_path: match crate_path { + Some(path) => path, + None => syn::parse_str(DEFAULT_INTERP_CRATE)?, + }, + body_frames, + }) } /// The final path segment of a type, used to recognize a framework frame. @@ -157,7 +180,18 @@ fn type_head(ty: &syn::Type) -> Option { pub fn generate(input: &DeriveInput, family: &Family) -> syn::Result { let trait_ident: syn::Ident = syn::parse_str(family.trait_name)?; - let interp_crate = parse_interp_crate_path(input)?; + let options = parse_options(input)?; + let interp_crate = &options.crate_path; + + if !family.body_frames && options.body_frames.is_some() { + return Err(syn::Error::new_spanned( + input, + format!( + "`body_frames` applies only to the concrete `FrameBuild` family; `{}` has no callable-body policy", + family.trait_name + ), + )); + } let syn::Data::Enum(data) = &input.data else { return Err(syn::Error::new_spanned( @@ -250,11 +284,22 @@ pub fn generate(input: &DeriveInput, family: &Family) -> syn::Result quote! { #path }, + None => quote! { #interp_crate::DefaultBodyFrames }, + }; + quote! { type BodyFrames = #policy; } + } else { + quote! {} + }; + Ok(quote! { #[automatically_derived] impl #impl_generics #interp_crate::#trait_ident<#(#type_params),*> for #enum_ident #ty_generics #where_clause { + #assoc_body_frames #(#methods)* } }) @@ -339,6 +384,35 @@ mod tests { insta::assert_snapshot!(emit(input, &CONCRETE)); } + #[test] + fn body_frames_override_is_honoured() { + let input: syn::DeriveInput = syn::parse_quote! { + #[interpret(body_frames = MyBodyFrames)] + enum MyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + } + }; + insta::assert_snapshot!(emit(input, &CONCRETE)); + } + + #[test] + fn rejects_body_frames_on_a_family_without_a_call_policy() { + // Only the concrete family descends into a callee, so only it has a + // callable-body policy to configure. + let input: syn::DeriveInput = syn::parse_quote! { + #[interpret(body_frames = MyBodyFrames)] + enum MyAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + } + }; + let err = generate(&input, &SPARSE_FORWARD).unwrap_err().to_string(); + assert!(err.contains("applies only to the concrete"), "{err}"); + } + #[test] fn rejects_missing_required_frame() { let input: syn::DeriveInput = syn::parse_quote! { diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap new file mode 100644 index 0000000000..1482c724da --- /dev/null +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__body_frames_override_is_honoured.snap @@ -0,0 +1,20 @@ +--- +source: crates/kirin-derive-interpreter/src/frame_build.rs +expression: "emit(input, &CONCRETE)" +--- +#[automatically_derived] +impl ::kirin_interpreter::FrameBuild for MyFrame { + type BodyFrames = MyBodyFrames; + fn from_block(frame: BlockFrame) -> Self { + Self::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + Self::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + Self::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + Self::DiGraph(frame) + } +} diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap index 5e2e54cadb..b5c0e4600d 100644 --- a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__concrete_frame_enum_with_dialect_variants.snap @@ -1,10 +1,11 @@ --- source: crates/kirin-derive-interpreter/src/frame_build.rs -assertion_line: 256 +assertion_line: 335 expression: "emit(input, &CONCRETE)" --- #[automatically_derived] impl ::kirin_interpreter::FrameBuild for ToyFrame { + type BodyFrames = ::kirin_interpreter::DefaultBodyFrames; fn from_block(frame: BlockFrame) -> Self { Self::Block(frame) } diff --git a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap index d9d36bd9a2..0f9e5de746 100644 --- a/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap +++ b/crates/kirin-derive-interpreter/src/snapshots/kirin_derive_interpreter__frame_build__tests__crate_path_override_is_honoured.snap @@ -1,10 +1,11 @@ --- source: crates/kirin-derive-interpreter/src/frame_build.rs -assertion_line: 305 +assertion_line: 384 expression: "emit(input, &CONCRETE)" --- #[automatically_derived] impl crate::FrameBuild for StandardFrame { + type BodyFrames = crate::DefaultBodyFrames; fn from_block(frame: BlockFrame) -> Self { Self::Block(frame) } 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 eade499793..b42920fca2 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -4,7 +4,7 @@ use crate::{ Body, CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, }; -use super::{BlockFrame, CFGFrame, Completion, DiGraphFrame, FrameBuild, UnGraphEntry}; +use super::{BodyFrameEntry, CallBodyFramePolicy, Completion, DefaultBodyFrames, FrameBuild}; /// The function-call boundary frame: interpreter runtime bookkeeping, not a /// function dialect operation and not the callable itself. @@ -16,10 +16,12 @@ use super::{BlockFrame, CFGFrame, Completion, DiGraphFrame, FrameBuild, UnGraphE /// 2. allocate the callee activation; /// 3. ask [`FunctionEntry`](crate::FunctionEntry) for the callable body /// descriptor ([`CallableBody`](crate::CallableBody)); -/// 4. select the entry frame for the closed [`Body`] variant — -/// `CFG` → [`CFGFrame`], `Block` → [`BlockFrame`], -/// `DiGraph` → [`DiGraphFrame`], `UnGraph` → the dialect/compiler policy -/// ([`FrameBuild::from_ungraph_entry`]); +/// 4. select the entry frame for the closed [`Body`] variant — **delegated to +/// the `P` policy** ([`CallBodyFramePolicy`]), which defaults to +/// [`DefaultBodyFrames`]: `CFG` → `CFGFrame`, `Block` → `BlockFrame`, +/// `DiGraph` → `DiGraphFrame`, `UnGraph` → the dialect/compiler hook +/// ([`FrameBuild::from_ungraph_entry`]). Everything else in this list is +/// fixed: a custom policy chooses walkers, never the lifecycle; /// 5. suspend while the callee frame runs; /// 6. validate the callee's completion kind ([`Returned`](Completion::Returned) /// or a graph's natural [`Finished`](Completion::Finished) are returns; a @@ -30,8 +32,11 @@ use super::{BlockFrame, CFGFrame, Completion, DiGraphFrame, FrameBuild, UnGraphE /// ([`ConcreteInterpreter::call`](crate::ConcreteInterpreter::call) pushes /// a [`CallFrame::root`], so root and nested calls share this one /// boundary implementation). -pub struct CallFrame { +pub struct CallFrame { state: CallState, + /// Which walkers enter the callee body. Selected by the total frame type + /// via [`FrameBuild::BodyFrames`]; the lifecycle above is unaffected. + _policy: std::marker::PhantomData P>, } enum CallState { @@ -62,7 +67,7 @@ enum CallDest { Root, } -impl CallFrame +impl CallFrame where V: Clone, { @@ -79,6 +84,7 @@ where results: call.results, }, }, + _policy: std::marker::PhantomData, } } @@ -92,14 +98,16 @@ where args, dest: CallDest::Root, }, + _policy: std::marker::PhantomData, } } } -impl Frame for CallFrame +impl Frame for CallFrame where I: FrameDriver, - F: FrameBuild, + F: FrameBuild, + P: CallBodyFramePolicy, V: Clone, E: From, { @@ -119,20 +127,33 @@ where // The closed `Body` enum is the framework's supported body // vocabulary, so this match is intentionally exhaustive; // only the `UnGraph` arm delegates to a language policy. + // `Body` is a closed vocabulary, so this match stays + // exhaustive; only *which frame* each arm builds is + // configurable, via the `P` policy. Activation ownership and + // completion handling deliberately stay out of the policy. let child = match entry.body { - Body::CFG(cfg) => { - F::from_cfg(CFGFrame::new(target.stage, index, cfg, entry.args)) - } - Body::Block(block) => { - F::from_block(BlockFrame::new(target.stage, index, block, entry.args)) - } - Body::DiGraph(graph) => { - F::from_digraph(DiGraphFrame::new(target.stage, index, graph, entry.args)) - } - Body::UnGraph(graph) => F::from_ungraph_entry(UnGraphEntry { + Body::CFG(cfg) => P::from_cfg(BodyFrameEntry { stage: target.stage, index, - graph, + body: cfg, + args: entry.args, + })?, + Body::Block(block) => P::from_block(BodyFrameEntry { + stage: target.stage, + index, + body: block, + args: entry.args, + })?, + Body::DiGraph(graph) => P::from_digraph(BodyFrameEntry { + stage: target.stage, + index, + body: graph, + args: entry.args, + })?, + Body::UnGraph(graph) => P::from_ungraph(BodyFrameEntry { + stage: target.stage, + index, + body: graph, args: entry.args, })?, }; @@ -142,6 +163,7 @@ where callee_env: index, dest, }, + _policy: std::marker::PhantomData, }), child, }) diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs b/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs index 6f5833966c..3b438037f5 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/mod.rs @@ -59,5 +59,7 @@ pub use block_frame::BlockFrame; pub use call_frame::CallFrame; pub use cfg_frame::CFGFrame; pub use digraph_frame::DiGraphFrame; -pub use protocol::{Completion, FrameBuild, UnGraphEntry}; +pub use protocol::{ + BodyFrameEntry, CallBodyFramePolicy, Completion, DefaultBodyFrames, FrameBuild, UnGraphEntry, +}; pub use standard_frame::StandardFrame; diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs b/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs index 213c935a4b..62bf897bd4 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/protocol.rs @@ -1,4 +1,4 @@ -use kirin_ir::{CompileStage, Product, UnGraph}; +use kirin_ir::{Block, CFG, CompileStage, DiGraph, Product, UnGraph}; use crate::{Body, EnvIndex, InterpreterError}; @@ -47,6 +47,119 @@ pub struct UnGraphEntry { pub args: Product, } +/// The entry context for **one** callable body representation `B`, handed to a +/// [`CallBodyFramePolicy`]. +/// +/// Generalizes [`UnGraphEntry`], which is the `B = UnGraph` case retained for +/// the existing escape hatch. Note what is *not* here: the callee activation is +/// only *borrowed* — `index` names the activation the awaiting [`CallFrame`] +/// allocated and will free exactly once. A policy builds a walker over it; it +/// never owns its lifetime. +pub struct BodyFrameEntry { + pub stage: CompileStage, + pub index: EnvIndex, + pub body: B, + pub args: Product, +} + +impl From> for UnGraphEntry { + fn from(entry: BodyFrameEntry) -> Self { + UnGraphEntry { + stage: entry.stage, + index: entry.index, + graph: entry.body, + args: entry.args, + } + } +} + +/// Which walker enters a **callable** body of each representation. +/// +/// This is the *body-entry* half of [`CallFrame`], split out so the two +/// concerns are separately replaceable: +/// +/// - the **call convention** — resolve the callee, allocate its activation, +/// ask [`FunctionEntry`](crate::FunctionEntry) for the body, suspend, +/// validate the completion kind, free the activation exactly once, bind the +/// results — stays in [`CallFrame`] and is *not* configurable. It is where +/// double-frees would live. +/// - the **walker choice** — which frame traverses that body — is this trait. +/// +/// So a language can say "walk my CFGs with my own scheduler" without forking +/// the lifecycle. [`Body`](crate::Body) stays a closed vocabulary; only the +/// frame chosen per variant becomes configurable. +/// +/// **Concrete execution only.** Forward abstract interpretation does not +/// descend into a callee — `AbstractCallFrame` *summarizes* the call and the +/// fixpoint engine separately maps a callable body to an +/// [`Owner`](crate::Owner). Customizing that would be an abstract +/// body-entry/owner policy, not this one. The backward engines don't walk +/// callable bodies through a call frame at all. +/// +/// Selected by the compiler/language author through the concrete total frame +/// type's [`FrameBuild::BodyFrames`]. A dialect crate may *offer* reusable +/// walkers or policies, but a callable dialect should not permanently fix one +/// traversal for every engine. +pub trait CallBodyFramePolicy { + fn from_cfg(entry: BodyFrameEntry) -> Result; + fn from_block(entry: BodyFrameEntry) -> Result; + fn from_digraph(entry: BodyFrameEntry) -> Result; + fn from_ungraph(entry: BodyFrameEntry) -> Result; +} + +/// The framework's default callable-body walkers — today's exact behaviour: +/// +/// | body | walker | +/// |---|---| +/// | `CFG` | [`CFGFrame`] | +/// | `Block` | [`BlockFrame`] | +/// | `DiGraph` | [`DiGraphFrame`] | +/// | `UnGraph` | [`FrameBuild::from_ungraph_entry`] — `NoDefaultWalker` unless overridden | +/// +/// `CallFrame` means `CallFrame`, so nothing changes +/// for a language that does not opt in. +pub struct DefaultBodyFrames; + +impl CallBodyFramePolicy for DefaultBodyFrames +where + V: Clone, + E: From, + F: FrameBuild, +{ + fn from_cfg(entry: BodyFrameEntry) -> Result { + Ok(F::from_cfg(CFGFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_block(entry: BodyFrameEntry) -> Result { + Ok(F::from_block(BlockFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_digraph(entry: BodyFrameEntry) -> Result { + Ok(F::from_digraph(DiGraphFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + /// Delegates to the pre-existing escape hatch, so a language that already + /// supplies a callable-UnGraph walker keeps working untouched. + fn from_ungraph(entry: BodyFrameEntry) -> Result { + F::from_ungraph_entry(entry.into()) + } +} + /// Construction trait letting any total frame enum embed the standard /// concrete frames. /// @@ -55,14 +168,36 @@ pub struct UnGraphEntry { /// on its own enum to reuse the representation walkers and [`CallFrame`] /// while adding its own dialect frames. /// +/// Two traits sit next to each other here; they answer different questions: +/// +/// - **`FrameBuild`** — *injection*: how is an already-built frame wrapped into +/// the total frame type `F`? One constructor per framework frame, so a member +/// frame can re-wrap itself without knowing what `F` is. +/// - **[`CallBodyFramePolicy`]** — *selection*: which walker enters a **callable** +/// body of a given representation? Chosen per language via +/// [`BodyFrames`](Self::BodyFrames). +/// /// [`Body`](crate::Body) is a deliberately closed enum, so [`CallFrame`] -/// matches it exhaustively and maps each representation to its default -/// walker — except `UnGraph`, whose traversal is a policy this trait's -/// [`from_ungraph_entry`](Self::from_ungraph_entry) hook supplies. +/// matches it exhaustively; the policy decides only *which frame* each arm +/// builds. `UnGraph` keeps its dedicated +/// [`from_ungraph_entry`](Self::from_ungraph_entry) hook, which +/// [`DefaultBodyFrames`] delegates to, so languages that already supply a +/// callable-UnGraph walker are unaffected. pub trait FrameBuild: Sized { + /// Which walkers this frame type uses to enter a **callable** body. + /// + /// Defaults to [`DefaultBodyFrames`] for every enum that does not opt in; + /// see [`CallBodyFramePolicy`]. Deliberately *unbounded* here: bounding it + /// as `CallBodyFramePolicy` makes checking + /// `type BodyFrames = DefaultBodyFrames` require `Self: FrameBuild`, + /// i.e. the very impl being checked. The obligation is instead attached + /// where the policy is *used*, in `CallFrame`'s [`Frame`](crate::Frame) + /// impl. + type BodyFrames; + fn from_block(frame: BlockFrame) -> Self; fn from_cfg(frame: CFGFrame) -> Self; - fn from_call(frame: CallFrame) -> Self; + fn from_call(frame: CallFrame) -> Self; fn from_digraph(frame: DiGraphFrame) -> Self; /// Build the entry frame for a **callable** `UnGraph` body. diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs index 65dbead4fe..a83878acf1 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs @@ -1,6 +1,8 @@ use crate::{Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp}; -use super::{BlockFrame, CFGFrame, CallFrame, Completion, DiGraphFrame, FrameBuild}; +use super::{ + BlockFrame, CFGFrame, CallFrame, Completion, DefaultBodyFrames, DiGraphFrame, FrameBuild, +}; /// The standard total concrete frame enum: the representation walkers plus /// the call boundary, no structured-control dialect frames and no @@ -14,6 +16,8 @@ pub enum StandardFrame { } impl FrameBuild for StandardFrame { + type BodyFrames = DefaultBodyFrames; + fn from_block(frame: BlockFrame) -> Self { StandardFrame::Block(frame) } @@ -35,7 +39,9 @@ impl FrameBuild for StandardFrame { impl Frame for StandardFrame where I: FrameDriver + SparseForwardInterp, - F: FrameBuild, + // The `Call` variant is spelled `CallFrame`, i.e. the default policy, so + // an outer universe embedding `StandardFrame` must use that policy too. + F: FrameBuild, V: Clone, E: From, { diff --git a/crates/kirin-interpreter/src/engines/concrete/mod.rs b/crates/kirin-interpreter/src/engines/concrete/mod.rs index 11fa511c76..16290e0e78 100644 --- a/crates/kirin-interpreter/src/engines/concrete/mod.rs +++ b/crates/kirin-interpreter/src/engines/concrete/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod frames; pub(crate) mod interp; pub use frames::{ - BlockFrame, CFGFrame, CallFrame, Completion, DiGraphFrame, FrameBuild, StandardFrame, - UnGraphEntry, + BlockFrame, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, CallFrame, Completion, + DefaultBodyFrames, DiGraphFrame, FrameBuild, StandardFrame, UnGraphEntry, }; pub use interp::ConcreteInterpreter; diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 109cabac2e..9790b97000 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -92,8 +92,8 @@ pub use self::core::ForwardFrameDriver as FrameDriver; // traversal is a dialect/compiler policy supplied through // `FrameBuild::from_ungraph_entry`) and the `CallFrame` call boundary. pub use engines::concrete::{ - BlockFrame, CFGFrame, CallFrame, Completion, ConcreteInterpreter, DiGraphFrame, FrameBuild, - StandardFrame, UnGraphEntry, + BlockFrame, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, CallFrame, Completion, + ConcreteInterpreter, DefaultBodyFrames, DiGraphFrame, FrameBuild, StandardFrame, UnGraphEntry, }; // Sparse forward engine (`Sem = ForwardEval`) + the abstract standard frames. pub use engines::sparse_forward::{ @@ -169,9 +169,10 @@ pub mod dialect { pub mod engine { pub use crate::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, - AbstractFrameBuild, AbstractFrameDriver, AbstractInterpreter, BlockFrame, CFGFrame, - CallContext, CallFrame, Callee, Completion, ConcreteInterpreter, ContextInsensitive, - CrossStageLinker, DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, + AbstractFrameBuild, AbstractFrameDriver, AbstractInterpreter, BlockFrame, BodyFrameEntry, + CFGFrame, CallBodyFramePolicy, CallContext, CallFrame, Callee, Completion, + ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DefaultBodyFrames, + DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, DenseBackwardInterpreter, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, DiGraphFrame, Env, ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, FrameDriver, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index f400daaed6..28cf1f4f3b 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -500,6 +500,52 @@ test). (Further examples: `example/toy-lang`'s `ToyFrame`, which adds `TracingFrame` counting call/body visitation while running the real program — see `example/toy-lang`'s `interpreter::tests::advanced`.) +### Callable-body walkers — `CallBodyFramePolicy` / `DefaultBodyFrames` + +`CallFrame` bundles two separable concerns, and only the second is +configurable: + +| Concern | Where | Configurable? | +|---|---|---| +| **call convention** — resolve the callee, allocate its activation, ask `FunctionEntry` for the body, suspend, validate the completion kind, free the activation *exactly once*, bind results | `CallFrame` itself | **no** — this is where double-frees would live | +| **walker choice** — which frame traverses the callee body | `CallBodyFramePolicy` | **yes** | + +`Body` stays a closed vocabulary, so `CallFrame::step_into` still matches it +exhaustively; only the frame each arm builds is chosen by the policy. The +default reproduces today's behaviour exactly: + +| body | `DefaultBodyFrames` | a custom `MyBodyFrames` might use | +|---|---|---| +| `CFG` | `CFGFrame` | `MyCustomCFGFrame` | +| `Block` | `BlockFrame` | `BlockFrame` (delegate to the default) | +| `DiGraph` | `DiGraphFrame` | `MyScheduledGraphFrame` | +| `UnGraph` | `FrameBuild::from_ungraph_entry` → `NoDefaultWalker` unless overridden | `MyCircuitWalker` | + +The policy is selected by the **compiler/language author** through the concrete +total frame type's `FrameBuild::BodyFrames`; `#[derive(FrameBuild)]` emits +`DefaultBodyFrames` unless given +`#[interpret(body_frames = MyBodyFrames)]`. `CallFrame` continues to mean +`CallFrame`. A dialect crate may *offer* reusable walkers +or policies, but a callable dialect should not permanently fix one traversal for +every engine. + +**Concrete execution only, and deliberately so.** Concrete execution descends +into a callee — `CallFrame` → body walker → completion → `CallFrame`. Forward +abstract interpretation does not: `AbstractCallFrame` *summarizes* the call while +the fixpoint engine separately maps a callable body to an `Owner::Block` or +`Owner::Graph` in `seed_entry_block`. Customizing that would be an abstract +body-entry/owner policy, not this one. The backward engines differ further — +sparse backward uses SSA values as owners and never walks callable bodies through +a call frame; dense backward uses block owners and reverse walks. If those ever +need configurable representation traversal, add engine-family-specific policies; +do not make IR owners supply walkers. + +**Not consulted for nested bodies.** `scf.if`/`scf.for` enter their Blocks +through their own dialect frames (chosen per engine by `ScfIfDispatch` / +`ScfForDispatch`), which then reuse a framework `BlockFrame`. Those are *nested* +bodies — they borrow the caller's activation and exit by `Yield` — so the +callable-body policy plays no part. + ### Abstract frames — `StandardAbstractFrame` / `AbstractFrameBuild` / `ForwardDataflowFrameDriver` `SparseForwardInterpreter` is symmetrically generic over a total abstract frame type diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 19ac184448..1d1082ecf1 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -13,8 +13,9 @@ use std::hash::Hash; use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BlockFrame, CFGFrame, CallFrame, Completion, DiGraphFrame, Frame, - FrameBuild, FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp, + AbstractFrameDriver, BlockFrame, CFGFrame, CallFrame, Completion, DefaultBodyFrames, + DiGraphFrame, Frame, FrameBuild, FrameDriver, FrameEffect, InterpreterError, + SparseForwardInterp, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, BuildScfFor, @@ -57,7 +58,7 @@ impl BuildScfFor for ToyFrame { impl Frame for ToyFrame where I: FrameDriver + SparseForwardInterp, - F: FrameBuild + BuildScfIf + BuildScfFor, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, { diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index b03b61dbfa..1332f2c0eb 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -567,8 +567,8 @@ mod advanced { use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BlockFrame, CFGFrame, CallContext, CallFrame, Completion, - ConcreteInterpreter, CrossStageLinker, DiGraphFrame, Frame, FrameBuild, FrameDriver, - FrameEffect, InterpreterError, SparseForwardInterp, SparseForwardInterpreter, + ConcreteInterpreter, CrossStageLinker, DefaultBodyFrames, DiGraphFrame, Frame, FrameBuild, + FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp, SparseForwardInterpreter, expect_single, }; use kirin_scf::{ @@ -609,6 +609,8 @@ mod advanced { struct TracingFrame(ToyFrame); impl FrameBuild for TracingFrame { + type BodyFrames = DefaultBodyFrames; + fn from_block(frame: BlockFrame) -> Self { Self(ToyFrame::Block(frame)) } @@ -638,7 +640,7 @@ mod advanced { impl Frame for TracingFrame where I: FrameDriver + SparseForwardInterp, - F: FrameBuild + BuildScfIf + BuildScfFor, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, { diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 99d520ad2e..4bc09b2333 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -28,6 +28,7 @@ //! re-run the owner when several call sites share one key, and a directed cycle //! is rejected identically by both engines. +use std::cell::RefCell; use std::collections::VecDeque; use std::hash::Hash; @@ -41,11 +42,12 @@ use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_function::Lexical; use kirin_interpreter::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, - AbstractFrameBuild, AbstractFrameDriver, BlockFrame, Body, CFGFrame, CallContext, CallFrame, - Completion, ConcreteInterpreter, ContextInsensitive, DiGraphFrame, Env, EnvIndex, Frame, - FrameBuild, FrameDriver, FrameEffect, FunctionEntry, Interpretable, InterpreterError, - SameStageLinker, SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, - StandardFrame, UnGraphEntry, expect_single, + AbstractFrameBuild, AbstractFrameDriver, BlockFrame, Body, BodyFrameEntry, CFGFrame, + CallBodyFramePolicy, CallContext, CallFrame, Completion, ConcreteInterpreter, + ContextInsensitive, DefaultBodyFrames, DiGraphFrame, Env, EnvIndex, Frame, FrameBuild, + FrameDriver, FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, + SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, StandardFrame, + UnGraphEntry, expect_single, }; use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -308,7 +310,7 @@ impl BuildScfFor for ScfTestFrame { impl Frame for ScfTestFrame where I: FrameDriver + SparseForwardInterp, - F: FrameBuild + BuildScfIf + BuildScfFor, + F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + kirin_scf::ForLoopValue, E: From, { @@ -374,10 +376,7 @@ fn run_scf(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result i64; specialize @test fn @abs(i64) -> i64 { @@ -393,8 +392,11 @@ specialize @test fn @abs(i64) -> i64 { ret %result; } } -"#, - ); +"#; + +#[test] +fn scf_if_arm_yields_to_dialect_frame() { + let pipeline = parse_scf(SCF_ABS_PROGRAM); assert_eq!(run_scf(&pipeline, "abs", &[-7]).unwrap(), 7); assert_eq!(run_scf(&pipeline, "abs", &[4]).unwrap(), 4); } @@ -508,6 +510,8 @@ enum UnPolicyFrame { } impl FrameBuild for UnPolicyFrame { + type BodyFrames = DefaultBodyFrames; + fn from_block(frame: BlockFrame) -> Self { UnPolicyFrame::Block(frame) } @@ -786,6 +790,8 @@ enum GraphAbstractFrame { } impl FrameBuild for GraphAbstractFrame { + type BodyFrames = DefaultBodyFrames; + fn from_block(_: BlockFrame) -> Self { GraphAbstractFrame::NoWalker("no abstract walker for a concrete Block frame") } @@ -1393,3 +1399,470 @@ fn digraph_port_arity_mismatch_is_reported() { "expected a port arity mismatch abstractly, got {abstract_:?}" ); } + +// =========================================================================== +// 14. A custom callable-body walker policy. +// =========================================================================== + +// `CallFrame` owns the call convention — resolve, allocate, enter, suspend, +// validate the completion, free the activation exactly once, bind results — and +// delegates only *which walker enters the callee body* to a +// `CallBodyFramePolicy`. These tests show a language replacing that choice for +// two body kinds without reimplementing any of the lifecycle, and confirm the +// choice does not leak into `scf.if`, which picks its own dialect frame. + +thread_local! { + /// Which body kinds the custom policy was asked for, in order. + static POLICY_LOG: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// A custom policy: instrument `CFG` and `DiGraph` entry, delegate `Block` and +/// `UnGraph` to the framework default. Each arm still builds the *standard* +/// walker — the point is that the language chose it, not that it walks +/// differently. +struct LoggingBodyFrames; + +impl CallBodyFramePolicy for LoggingBodyFrames { + fn from_cfg(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("cfg")); + Ok(PolicyFrame::CFG(CFGFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_block(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("block")); + >::from_block(entry) + } + + fn from_digraph(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("digraph")); + Ok(PolicyFrame::DiGraph(DiGraphFrame::new( + entry.stage, + entry.index, + entry.body, + entry.args, + ))) + } + + fn from_ungraph(entry: BodyFrameEntry) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().push("ungraph")); + >::from_ungraph(entry) + } +} + +/// A total frame type that selects `LoggingBodyFrames`. Note the `Call` variant +/// carries the policy, and `FrameBuild::BodyFrames` names it — the derive would +/// emit exactly this via `#[interpret(body_frames = LoggingBodyFrames)]`. +enum PolicyFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + ScfIf(ScfIfFrame), + ScfFor(ScfForFrame), +} + +impl FrameBuild for PolicyFrame { + type BodyFrames = LoggingBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + PolicyFrame::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + PolicyFrame::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + PolicyFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + PolicyFrame::DiGraph(frame) + } +} + +impl BuildScfIf for PolicyFrame { + fn scf_if(frame: ScfIfFrame) -> Self { + PolicyFrame::ScfIf(frame) + } +} + +impl BuildScfFor for PolicyFrame { + fn scf_for(frame: ScfForFrame) -> Self { + PolicyFrame::ScfFor(frame) + } +} + +impl Frame for PolicyFrame +where + I: FrameDriver + SparseForwardInterp, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, TestError> { + match self { + PolicyFrame::Block(frame) => frame.step_into(interp), + PolicyFrame::CFG(frame) => frame.step_into(interp), + PolicyFrame::Call(frame) => frame.step_into(interp), + PolicyFrame::DiGraph(frame) => frame.step_into(interp), + PolicyFrame::ScfIf(frame) => frame.step_into(interp), + PolicyFrame::ScfFor(frame) => frame.step_into(interp), + } + } + + fn resume_done_into( + self, + interp: &mut I, + ) -> Result>, TestError> { + match self { + PolicyFrame::Block(frame) => frame.resume_done_into(interp), + PolicyFrame::CFG(frame) => frame.resume_done_into(interp), + PolicyFrame::Call(frame) => frame.resume_done_into(interp), + PolicyFrame::DiGraph(frame) => frame.resume_done_into(interp), + PolicyFrame::ScfIf(frame) => frame.resume_done_into(interp), + PolicyFrame::ScfFor(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, TestError> { + match self { + PolicyFrame::Block(frame) => frame.resume_into(completion, interp), + PolicyFrame::CFG(frame) => frame.resume_into(completion, interp), + PolicyFrame::Call(frame) => frame.resume_into(completion, interp), + PolicyFrame::DiGraph(frame) => frame.resume_into(completion, interp), + PolicyFrame::ScfIf(frame) => frame.resume_into(completion, interp), + PolicyFrame::ScfFor(frame) => frame.resume_into(completion, interp), + } + } +} + +type PolicyEngine<'ir> = ConcreteInterpreter<'ir, L, i64, TestError, SameStageLinker, PolicyFrame>; + +fn run_with_policy(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + POLICY_LOG.with(|log| log.borrow_mut().clear()); + let mut interp: PolicyEngine<'_> = + ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +fn policy_log() -> Vec<&'static str> { + POLICY_LOG.with(|log| log.borrow().clone()) +} + +/// A root call and a nested call, both routed through the custom policy. The +/// returned values are unchanged — only the *selection* of the walker moved. +#[test] +fn custom_body_policy_enters_callable_bodies() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + + // Root call into a CFG body, which then calls a DiGraph body. + assert_eq!(run_with_policy(&pipeline, "main", &[]).unwrap(), 5); + assert_eq!(policy_log(), vec!["cfg", "digraph"]); + + // Root call straight into the DiGraph body. + assert_eq!(run_with_policy(&pipeline, "gadd", &[2, 3]).unwrap(), 5); + assert_eq!(policy_log(), vec!["digraph"]); +} + +/// The `Block` arm delegates to `DefaultBodyFrames`, so a policy can override +/// only the body kinds it cares about. +#[test] +fn custom_body_policy_can_delegate_to_the_default() { + let pipeline = parse(BLOCK_CALLABLE_PROGRAM); + assert_eq!(run_with_policy(&pipeline, "main", &[]).unwrap(), 42); + assert_eq!(policy_log(), vec!["cfg", "block"]); +} + +/// Isolation: `scf.if` builds its *own* dialect frame via `ScfIfDispatch` and +/// walks the chosen arm with a framework `BlockFrame`. It is a nested body, not +/// a callable one, so the call-body policy must never be consulted for it. +#[test] +fn scf_if_does_not_use_the_call_body_policy() { + let pipeline = parse_scf(SCF_ABS_PROGRAM); + let mut interp: ConcreteInterpreter< + '_, + ScfL, + i64, + TestError, + SameStageLinker, + ScfTestFrame, + > = ConcreteInterpreter::new(&pipeline).with_linker(SameStageLinker); + POLICY_LOG.with(|log| log.borrow_mut().clear()); + let result = + expect_single::(interp.call_by_name("test", "abs", [-7]).unwrap()).unwrap(); + assert_eq!(result, 7); + // `ScfTestFrame` uses the default policy, and in any case the scf arm never + // reaches a call boundary — the log stays empty. + assert!(policy_log().is_empty(), "got {:?}", policy_log()); +} + +// =========================================================================== +// 15. A *genuine* replacement walker, selected through the derive. +// =========================================================================== + +// Section 14 proves the policy is consulted, but each arm still built a +// standard walker and the total frame type implemented `FrameBuild` by hand. This +// section closes both gaps: +// +// - `MyCfgWalker` is a **distinct frame type** with its own `Frame` impl and its +// own enum variant. A callable `CFG` body enters through it, never through +// `DerivedFrame::CFG`. +// - the policy is selected by `#[interpret(body_frames = MyBodyFrames)]`, so the +// derive is **compiled and executed** here rather than only snapshotted. +// +// What this does *not* claim: `MyCfgWalker` delegates the actual block-to-block +// traversal to a `CFGFrame` it owns, rather than reimplementing CFG walking. It +// hands off after the entry step, which is why the assertions count *entries*. +// The substitutable thing Roger asked for is the frame type the language chooses +// at the callable-body boundary, and that is what is replaced. + +#[derive(Clone, Copy, Default, Debug, PartialEq)] +struct CustomTrace { + /// `MyBodyFrames::from_cfg` calls — one per callable CFG activation. + policy_selections: usize, + /// Steps taken *by* `MyCfgWalker`. + my_steps: usize, + /// Steps taken by the standard `DerivedFrame::CFG` variant. Must stay `0`: + /// if the custom walker ever handed the walk back to the framework variant, + /// this counts it. + standard_cfg_steps: usize, +} + +thread_local! { + static CUSTOM: RefCell = const { RefCell::new(CustomTrace { + policy_selections: 0, + my_steps: 0, + standard_cfg_steps: 0, + }) }; +} + +/// A language's own callable-CFG walker: distinct type, distinct variant. +struct MyCfgWalker { + inner: CFGFrame, +} + +impl MyCfgWalker { + /// The inner `CFGFrame` re-wraps *itself* through `FrameBuild::from_cfg`, + /// which lands in `DerivedFrame::CFG`. Lift those back so this walker stays + /// resident for the whole traversal rather than only its entry step. + /// + /// Only the frame that represents *self* is lifted: `Push.child` is a + /// different frame (a call boundary or a pushed dialect frame) and must be + /// left alone. + fn stay_resident( + effect: FrameEffect, Completion>, + ) -> FrameEffect, Completion> { + fn lift(frame: DerivedFrame) -> DerivedFrame { + match frame { + DerivedFrame::CFG(inner) => DerivedFrame::MyCfg(MyCfgWalker { inner }), + other => other, + } + } + match effect { + FrameEffect::Continue(frame) => FrameEffect::Continue(lift(frame)), + FrameEffect::Push { parent, child } => FrameEffect::Push { + parent: lift(parent), + child, + }, + // `Done`/`Complete` carry no frame. + other => other, + } + } +} + +impl Frame> for MyCfgWalker +where + I: FrameDriver + SparseForwardInterp>, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into( + self, + interp: &mut I, + ) -> Result, Completion>, E> { + CUSTOM.with(|t| t.borrow_mut().my_steps += 1); + self.inner.step_into(interp).map(Self::stay_resident) + } + + fn resume_done_into( + self, + interp: &mut I, + ) -> Result, Completion>, E> { + CUSTOM.with(|t| t.borrow_mut().my_steps += 1); + self.inner.resume_done_into(interp).map(Self::stay_resident) + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result, Completion>, E> { + CUSTOM.with(|t| t.borrow_mut().my_steps += 1); + self.inner + .resume_into(completion, interp) + .map(Self::stay_resident) + } +} + +/// Replaces the `CFG` walker outright; delegates the other three body kinds to +/// the framework default. +struct MyBodyFrames; + +impl CallBodyFramePolicy> for MyBodyFrames +where + V: Clone, + E: From, +{ + fn from_cfg(entry: BodyFrameEntry) -> Result, E> { + // Not `F::from_cfg` — the language's own walker, in its own variant. + CUSTOM.with(|t| t.borrow_mut().policy_selections += 1); + Ok(DerivedFrame::MyCfg(MyCfgWalker { + inner: CFGFrame::new(entry.stage, entry.index, entry.body, entry.args), + })) + } + + fn from_block(entry: BodyFrameEntry) -> Result, E> { + >>::from_block(entry) + } + + fn from_digraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_digraph(entry) + } + + fn from_ungraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_ungraph(entry) + } +} + +/// The policy is chosen by the attribute; the derive emits +/// `type BodyFrames = MyBodyFrames` and the four injection constructors. +#[derive(FrameBuild)] +#[interpret(body_frames = MyBodyFrames)] +enum DerivedFrame { + Block(BlockFrame), + /// Required by `FrameBuild`, and reached only when `MyCfgWalker` hands off + /// mid-walk — never as the entry frame for a callable body. + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), + MyCfg(MyCfgWalker), +} + +impl Frame for DerivedFrame +where + I: FrameDriver + SparseForwardInterp>, + V: Clone, + E: From, +{ + type Completion = Completion; + + fn step_into(self, interp: &mut I) -> Result>, E> { + match self { + DerivedFrame::Block(frame) => frame.step_into(interp), + DerivedFrame::CFG(frame) => { + CUSTOM.with(|t| t.borrow_mut().standard_cfg_steps += 1); + frame.step_into(interp) + } + DerivedFrame::Call(frame) => frame.step_into(interp), + DerivedFrame::DiGraph(frame) => frame.step_into(interp), + DerivedFrame::MyCfg(frame) => frame.step_into(interp), + } + } + + fn resume_done_into(self, interp: &mut I) -> Result>, E> { + match self { + DerivedFrame::Block(frame) => frame.resume_done_into(interp), + DerivedFrame::CFG(frame) => frame.resume_done_into(interp), + DerivedFrame::Call(frame) => frame.resume_done_into(interp), + DerivedFrame::DiGraph(frame) => frame.resume_done_into(interp), + DerivedFrame::MyCfg(frame) => frame.resume_done_into(interp), + } + } + + fn resume_into( + self, + completion: Completion, + interp: &mut I, + ) -> Result>, E> { + match self { + DerivedFrame::Block(frame) => frame.resume_into(completion, interp), + DerivedFrame::CFG(frame) => frame.resume_into(completion, interp), + DerivedFrame::Call(frame) => frame.resume_into(completion, interp), + DerivedFrame::DiGraph(frame) => frame.resume_into(completion, interp), + DerivedFrame::MyCfg(frame) => frame.resume_into(completion, interp), + } + } +} + +fn run_derived(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { + CUSTOM.with(|t| *t.borrow_mut() = CustomTrace::default()); + let mut interp: ConcreteInterpreter< + '_, + L, + i64, + TestError, + SameStageLinker, + DerivedFrame, + > = ConcreteInterpreter::new(pipeline).with_linker(SameStageLinker); + expect_single(interp.call_by_name("test", function, args.iter().copied())?) +} + +/// The custom walker is selected by `#[interpret(body_frames = ..)]` and stays +/// resident for the **whole** CFG traversal, because it re-wraps every frame the +/// inner walker hands back (see [`MyCfgWalker::stay_resident`]). +/// +/// Two assertions carry the claim: `standard_cfg_steps == 0` proves the +/// framework's `CFGFrame` variant is never stepped, and `my_steps` well above +/// `policy_selections` proves the walker kept going rather than handing off after +/// entry. Deleting the re-wrap flips this to +/// `my_steps: 1, standard_cfg_steps: 4` — i.e. entry-only — so the assertions +/// have teeth. (Observed here: `my_steps: 6, standard_cfg_steps: 0`.) +#[test] +fn derived_policy_substitutes_a_custom_cfg_walker() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + + // `main` is CFG-bodied → the custom walker. It calls `gadd`, a DiGraph body, + // which the policy delegates to the framework's `DiGraphFrame`. + assert_eq!(run_derived(&pipeline, "main", &[]).unwrap(), 5); + let trace = CUSTOM.with(|t| *t.borrow()); + assert_eq!(trace.policy_selections, 1, "{trace:?}"); + assert_eq!( + trace.standard_cfg_steps, 0, + "custom walker leaked: {trace:?}" + ); + assert!( + trace.my_steps > trace.policy_selections, + "walker handed off after entry: {trace:?}" + ); + + // A root call straight into the DiGraph body: delegated, so no custom CFG + // walker is ever built. + assert_eq!(run_derived(&pipeline, "gadd", &[2, 3]).unwrap(), 5); + assert_eq!(CUSTOM.with(|t| *t.borrow()), CustomTrace::default()); +} + +/// Two nested CFG-bodied callables: the policy is consulted once per activation, +/// and neither activation ever falls back to the standard variant. +#[test] +fn derived_policy_walker_stays_resident_across_activations() { + let pipeline = parse(CFG_BRANCH_PROGRAM); + // `caller` (CFG) calls `same` (CFG) → two callable CFG activations. + assert_eq!(run_derived(&pipeline, "caller", &[0]).unwrap(), 8); + let trace = CUSTOM.with(|t| *t.borrow()); + assert_eq!(trace.policy_selections, 2, "{trace:?}"); + assert_eq!( + trace.standard_cfg_steps, 0, + "custom walker leaked: {trace:?}" + ); + // Both bodies are multi-statement, so residency means many more steps than + // the two entries. + assert!(trace.my_steps > 4, "{trace:?}"); +} diff --git a/tests/compile-fail/call_frame_policy_mismatch.rs b/tests/compile-fail/call_frame_policy_mismatch.rs new file mode 100644 index 0000000000..a56da56e78 --- /dev/null +++ b/tests/compile-fail/call_frame_policy_mismatch.rs @@ -0,0 +1,46 @@ +//! A total frame type whose `Call` variant carries one callable-body policy +//! while `#[derive(FrameBuild)]` is configured with another (here: the default, +//! because no `#[interpret(body_frames = ..)]` was given). +//! +//! The derive deliberately does not try to reconcile the two — it emits +//! `type BodyFrames = DefaultBodyFrames` and lets the generated impl produce a +//! type error naming both policies, which is more informative than anything the +//! macro could say about a path it cannot resolve. + +use kirin_interpreter::{ + BlockFrame, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, CallFrame, DefaultBodyFrames, + DiGraphFrame, FrameBuild, InterpreterError, +}; +use kirin_ir::{Block, CFG, DiGraph, UnGraph}; + +struct MyBodyFrames; + +impl CallBodyFramePolicy> for MyBodyFrames +where + V: Clone, + E: From, +{ + fn from_cfg(entry: BodyFrameEntry) -> Result, E> { + >>::from_cfg(entry) + } + fn from_block(entry: BodyFrameEntry) -> Result, E> { + >>::from_block(entry) + } + fn from_digraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_digraph(entry) + } + fn from_ungraph(entry: BodyFrameEntry) -> Result, E> { + >>::from_ungraph(entry) + } +} + +// Missing: #[interpret(body_frames = MyBodyFrames)] +#[derive(FrameBuild)] +enum MismatchedFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), +} + +fn main() {} diff --git a/tests/compile-fail/call_frame_policy_mismatch.stderr b/tests/compile-fail/call_frame_policy_mismatch.stderr new file mode 100644 index 0000000000..135dd41dc9 --- /dev/null +++ b/tests/compile-fail/call_frame_policy_mismatch.stderr @@ -0,0 +1,13 @@ +error[E0053]: method `from_call` has an incompatible type for trait + --> tests/compile-fail/call_frame_policy_mismatch.rs:42:10 + | +42 | Call(CallFrame), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `DefaultBodyFrames`, found `MyBodyFrames` + | + = note: expected signature `fn(CallFrame) -> MismatchedFrame` + found signature `fn(CallFrame) -> MismatchedFrame` +help: change the parameter type to match the trait + | +42 - Call(CallFrame), +42 + Call(CallFrame), + | From 6ec0a31bcab998976b436b35ed447d1f2a7d4405 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Mon, 3 Aug 2026 12:02:58 -0400 Subject: [PATCH 14/21] Decoupled the monolithic trait `ForwardFrameDriver` into smaller traits split by consumer. --- AGENTS.md | 8 +- crates/kirin-interpreter/src/core/frame.rs | 274 ++++++--- crates/kirin-interpreter/src/core/interp.rs | 28 + crates/kirin-interpreter/src/core/mod.rs | 3 +- .../engines/concrete/frames/block_cursor.rs | 15 +- .../engines/concrete/frames/block_frame.rs | 6 +- .../src/engines/concrete/frames/call_frame.rs | 6 +- .../src/engines/concrete/frames/cfg_frame.rs | 6 +- .../engines/concrete/frames/digraph_frame.rs | 13 +- .../engines/concrete/frames/standard_frame.rs | 4 +- .../src/engines/concrete/interp.rs | 68 ++- .../src/engines/dense_backward/frames.rs | 6 +- .../src/engines/dense_backward/interp.rs | 8 +- .../src/engines/dense_backward/mod.rs | 2 +- .../src/engines/sparse_forward/frames.rs | 25 +- .../src/engines/sparse_forward/interp.rs | 156 ++++- crates/kirin-interpreter/src/lib.rs | 34 +- crates/kirin-scf/src/interpreter.rs | 31 +- .../design/formalism/operational-semantics.md | 2 +- docs/design/interpreter/index.md | 159 ++++- example/toy-lang/src/interpreter/frame.rs | 16 +- example/toy-lang/src/interpreter/tests.rs | 12 +- tests/body_kinds.rs | 22 +- tests/frame_engine_capabilities.rs | 575 ++++++++++++++++++ 24 files changed, 1242 insertions(+), 237 deletions(-) create mode 100644 tests/frame_engine_capabilities.rs diff --git a/AGENTS.md b/AGENTS.md index 94221888d4..15104ea739 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -159,7 +159,13 @@ For user-defined dialects not in this table, ask the user for domain context dur - **Callable-body walkers are a concrete policy**: `CallFrame` owns the call convention (resolve, allocate the activation, enter, suspend, validate the completion, free exactly once, bind results) and delegates *only* which walker enters the callee body to `CallBodyFramePolicy`, selected via `FrameBuild::BodyFrames` (default `DefaultBodyFrames`: `CFG`→`CFGFrame`, `Block`→`BlockFrame`, `DiGraph`→`DiGraphFrame`, `UnGraph`→`FrameBuild::from_ungraph_entry`). `CallFrame` still means `CallFrame`, so no existing language changes. `#[derive(FrameBuild)]` emits the default; `#[interpret(body_frames = MyBodyFrames)]` overrides it. **Concrete execution only** — forward abstract interpretation summarizes calls (`AbstractCallFrame`) and maps a callable body to an `Owner` in `seed_entry_block` instead of descending, and the backward engines never walk callable bodies through a call frame; customizing those would need engine-family-specific policies, and IR owners must never supply walkers. This policy is *not* consulted for nested bodies: `scf.if`/`scf.for` and other structured operations keep choosing their own dialect frames through their dispatch traits. -- **Customizing traversal**: `core/frame.rs` defines the shared `Frame`/`FrameEffect`/`drive_frames` protocol plus forward driver traits. Every frame — walker or enum — implements the same three methods (`step_into`/`resume_done_into`/`resume_into`), all returning `Result, I::Error>`, so a total enum's match arms are uniform across variants. 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`. +- **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 storage, linking, callable-entry dispatch; kept whole because `CallFrame` consumes all four and their pairing is a safety property — `CallFrame` still owns the *convention*). **A member frame bounds only what it consumes** (`ScfIfFrame` needs just `FrameEngine`; `ScfForFrame` just `Env`); a *universe* — a total frame enum — keeps an umbrella (`ForwardFrameEngine + SparseForwardInterp` concrete, `ForwardDataflowFrameEngine + SparseForwardInterp` abstract), because its engine must support the union of all its variants. Do not mechanically narrow universe bounds. `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` = whole-universe capability sets. 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. + +- **`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`. + +- **Customizing traversal**: Every frame — walker or enum — implements the same three methods (`step_into`/`resume_done_into`/`resume_into`), all returning `Result, I::Error>`, so a total enum's match arms are uniform across variants. 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` 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`. diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 70d46c91f5..2f2b1cfa11 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -1,9 +1,63 @@ -//! Shared frame protocol plus forward frame-driver capabilities. +//! Shared frame protocol plus the engine capabilities frames require. //! //! [`Frame`], [`FrameEngine`], [`FrameEffect`], and [`drive_frames`] are -//! direction-neutral. Forward engines add [`ForwardFrameDriver`] and -//! [`ForwardDataflowFrameDriver`] for env access, IR queries, calls, and abstract -//! merge/summarization. +//! direction-neutral. Forward engines add the capability traits below. +//! +//! # Three levels of "engine" +//! +//! The word means something different at each level, so the names are kept +//! distinct: +//! +//! - **[`drive_frames`]** is the *frame-stack driver* — the loop. Nothing else +//! is a "driver"; the concrete objects named `ForwardDriver` / +//! `DenseBackwardDriver` are fixpoint-driver structs, not capability traits. +//! - **[`FrameEngine`]** is the minimal engine contract the generic frame stack +//! needs: a total `Error` type, and nothing more. +//! - 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. +//! +//! # The capability model +//! +//! Capabilities are split by **what one frame needs**, not by what one engine +//! happens to provide. Each trait is the requirement of a specific kind of +//! traversal, so a frame's bound documents exactly which engine operations it +//! can reach — and an engine that implements only some of them still runs the +//! frames it can support. +//! +//! | trait | capability | consumed by | +//! |---|---|---| +//! | [`StatementDispatch`] | dispatch a statement to its dialect rule | every executing frame | +//! | [`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) | +//! +//! 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 +//! actuals — lives on the crate-private `BlockBinding` extension instead of +//! hiding inside [`BlockQueries`], so no query trait's name conceals a store +//! mutation. +//! +//! [`StatementDispatch`] is the engine side of dialect dispatch, and is easy to +//! confuse with [`InterpDispatch`](crate::InterpDispatch) — they face opposite +//! directions. `InterpDispatch` is implemented by a **stage/language** to +//! route a statement to the right dialect rule. `StatementDispatch` is +//! implemented by the **engine** and is what a *frame* calls: it stashes the +//! current location (`stage`/`statement`/`index`) so the rule can read it back +//! through [`Interp`], then delegates to `InterpDispatch`. +//! +//! Two umbrellas compose the components for the two *engine families*. A total +//! 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, +//! blanket-implemented. +//! - [`ForwardDataflowFrameEngine`] — abstract dataflow: the traversal +//! components abstract execution *shares*, plus merge/summarization. It does +//! **not** inherit [`CallServices`] or [`CFGQueries`], because an abstract +//! engine summarizes calls rather than entering them. use std::hash::Hash; @@ -117,38 +171,43 @@ where } } -/// Capabilities required by forward frames. +/// Engine capability for dispatching a statement to its dialect rule. /// -/// Re-exported as [`FrameDriver`](crate::FrameDriver). -pub trait ForwardFrameDriver: Env { - /// Allocate a fresh SSA activation record. - fn alloc_env(&mut self) -> EnvIndex; - /// Free an activation record. - fn free_env(&mut self, index: EnvIndex) -> Result<(), Self::Error>; - /// Resolve a callee to a concrete function target via the engine's linker. - fn resolve_call( - &self, - stage: CompileStage, - callee: &Callee, - ) -> Result; +/// The one capability *every* frame that executes statements needs, and the only +/// one shared by concrete execution and abstract dataflow. +/// +/// Not to be confused with [`InterpDispatch`](crate::InterpDispatch), which +/// faces the other way: a *stage/language* implements `InterpDispatch` to route +/// a statement to its dialect rule, while an *engine* implements +/// `StatementDispatch` to expose location-aware dispatch to frames. +pub trait StatementDispatch: Interp { /// Dispatch one statement to its dialect [`Interpretable`](crate::Interpretable) /// rule, producing this engine's [`Effect`](Interp::Effect) (a /// [`SparseForwardEffect`](crate::SparseForwardEffect) for the value engines). + /// + /// The engine stashes `stage`/`statement`/`index` as its current location + /// first, so the rule can read it back through [`Interp`]. fn run_statement( &mut self, stage: CompileStage, statement: Statement, index: EnvIndex, ) -> Result; - /// Build the [`CallableBody`] a callable statement enters on invocation. - fn enter_function( - &mut self, - stage: CompileStage, - body: Statement, - args: Product, - index: EnvIndex, - ) -> Result, Self::Error>; +} +/// Read-only structural queries needed to traverse a single [`Block`]. +/// +/// The requirement of [`BlockFrame`](crate::BlockFrame), its internal +/// `BlockCursor`, and every frame that steps through a block's statements. +/// +/// **Read-only by construction**: only [`Interp`] is required, not [`Env`], so +/// nothing on this trait can touch SSA storage. Entering a block also *binds* +/// its parameters, which needs a write — that operation lives on the +/// crate-private `BlockBinding` extension (bounded `Env + BlockQueries`) +/// rather than here, so this name cannot hide a store mutation. Engine-internal +/// callers wanting the same queries outside a frame use +/// [`StageQuery`](crate::StageQuery). +pub trait BlockQueries: Interp { fn block_params(&self, stage: CompileStage, block: Block) -> Result, Self::Error>; fn first_statement( @@ -162,27 +221,26 @@ pub trait ForwardFrameDriver: Env { block: Block, after: Statement, ) -> Result, Self::Error>; - fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, Self::Error>; +} - /// The default walk plan of a digraph body (ports, toposorted nodes, - /// yields). Errors on cyclic digraphs. - /// - /// Digraph bodies are opt-in: an engine that never walks one inherits this - /// rejection rather than inventing a schedule, the same way - /// [`FrameBuild::from_ungraph_entry`](crate::FrameBuild::from_ungraph_entry) - /// rejects a callable `UnGraph` without a compiler-supplied policy. - fn digraph_walk_plan( - &self, - stage: CompileStage, - graph: kirin_ir::DiGraph, - ) -> Result { - let _ = stage; - Err(Self::Error::from(InterpreterError::NoDefaultWalker( - Body::DiGraph(graph), - ))) - } +/// Structural queries needed to enter and traverse a [`CFG`]. +/// +/// Extends [`BlockQueries`] because walking a CFG *is* walking its blocks and +/// following jumps between them; `cfg_entry` only adds finding where to start. +pub trait CFGQueries: BlockQueries { + fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, Self::Error>; +} - /// Bind a block's parameters to incoming actuals in `env` (arity-checked). +/// Crate-private block-entry binding: the one operation that needs a +/// [`BlockQueries`] read *and* an [`Env`] write. +/// +/// 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 { + /// Positionally bind a block's parameters to incoming actuals in `index`, + /// checking arity. fn bind_block_args( &mut self, stage: CompileStage, @@ -203,35 +261,112 @@ pub trait ForwardFrameDriver: Env { } Ok(()) } +} - /// Destructure `values` into `results` slots in `env` (arity-checked). - fn write_results( +impl BlockBinding for T {} + +/// Structural/scheduling queries needed to traverse a +/// [`DiGraph`](kirin_ir::DiGraph) body. +/// +/// Split out from the block/CFG queries because a digraph walk shares none of +/// their mechanics: there are no blocks, no jumps, and no entry block — only a +/// dependency order. +pub trait DiGraphQueries: Interp { + /// The default walk plan of a digraph body (ports, toposorted nodes, + /// yields). Errors on cyclic digraphs. + /// + /// Digraph bodies are opt-in: an engine that never walks one inherits this + /// rejection rather than inventing a schedule, the same way + /// [`FrameBuild::from_ungraph_entry`](crate::FrameBuild::from_ungraph_entry) + /// rejects a callable `UnGraph` without a compiler-supplied policy. + fn digraph_walk_plan( + &self, + stage: CompileStage, + graph: kirin_ir::DiGraph, + ) -> Result { + let _ = stage; + Err(Self::Error::from(InterpreterError::NoDefaultWalker( + Body::DiGraph(graph), + ))) + } +} + +/// Engine services used by [`CallFrame`](crate::CallFrame): activation storage, +/// linking, and callable-entry dispatch. +/// +/// **[`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 all four 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 { + /// Allocate a fresh SSA activation record. + fn alloc_env(&mut self) -> EnvIndex; + /// Free an activation record. + fn free_env(&mut self, index: EnvIndex) -> Result<(), Self::Error>; + /// Resolve a callee to a concrete function target via the engine's linker. + fn resolve_call( + &self, + stage: CompileStage, + callee: &Callee, + ) -> Result; + /// Build the [`CallableBody`] a callable statement enters on invocation. + fn enter_function( &mut self, + stage: CompileStage, + body: Statement, + args: Product, index: EnvIndex, - results: &Product, - values: Product, - ) -> Result<(), Self::Error> { - if results.len() != values.len() { - return Err(Self::Error::from(InterpreterError::ProductArityMismatch { - expected: results.len(), - actual: values.len(), - })); - } - for (slot, value) in results.iter().copied().zip(values) { - self.env_write(index, slot, value)?; - } - Ok(()) - } + ) -> Result, Self::Error>; +} + +/// An interpreter engine capable of running the complete standard **concrete** +/// forward-frame universe. +/// +/// 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 *universe* level, where a total +/// frame enum's engine must support the union of all its variants +/// ([`StandardFrame`](crate::StandardFrame) and downstream frame enums do). +/// Individual member frames should bound only the components they use, so a +/// partial engine can still run them. +pub trait ForwardFrameEngine: + StatementDispatch + CFGQueries + DiGraphQueries + CallServices +{ +} + +impl ForwardFrameEngine for T where + T: StatementDispatch + CFGQueries + DiGraphQueries + CallServices +{ } -/// The **forward dataflow** frame-driver capability surface: what the forward -/// abstract frames need from the engine, beyond the [`ForwardFrameDriver`] IR -/// queries. +/// An interpreter engine capable of running the standard **forward abstract** +/// frame universe: the traversal capabilities it shares with concrete execution, +/// plus merge/summarization. +/// +/// It extends [`Env`] + [`StatementDispatch`] + [`BlockQueries`] + +/// [`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, +/// `resolve_call`, and `enter_function` 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 +/// in the fixpoint driver, not by asking a frame to enter a CFG. A frame that +/// *does* want either capability can name it in addition — see the +/// abstract-body-traversal follow-up. /// /// Implemented by [`SparseForwardInterpreter`](crate::SparseForwardInterpreter). -/// The standard abstract frames are generic over `I: ForwardDataflowFrameDriver`, -/// so a custom forward-dataflow frame can drive any engine providing these -/// capabilities. +/// The standard abstract frames are generic over +/// `I: ForwardDataflowFrameEngine`, so a custom forward-dataflow frame can drive +/// any engine providing these capabilities. /// /// The interprocedural protocol stays **atomic in the engine**: `summarize_call` /// performs the whole call-summarization step (resolve, key, join arguments into @@ -239,10 +374,9 @@ pub trait ForwardFrameDriver: Env { /// self-recursion* — and read the current return summary or `bottom`), so a /// custom frame cannot reorder it and break soundness. Frames only decide /// *traversal*: which frame to step next. -/// -/// Re-exported as [`AbstractFrameDriver`](crate::AbstractFrameDriver) for backward -/// compatibility. -pub trait ForwardDataflowFrameDriver: ForwardFrameDriver { +pub trait ForwardDataflowFrameEngine: + Env + StatementDispatch + BlockQueries + DiGraphQueries +{ /// The key under which function entry/return summaries are tracked /// (the analysis [`CallContext::Key`](crate::CallContext::Key)). type SummaryKey: Clone + Eq + Hash; diff --git a/crates/kirin-interpreter/src/core/interp.rs b/crates/kirin-interpreter/src/core/interp.rs index 2916cfc4e2..a28136ba4d 100644 --- a/crates/kirin-interpreter/src/core/interp.rs +++ b/crates/kirin-interpreter/src/core/interp.rs @@ -69,6 +69,34 @@ pub trait Env: Interp { 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 diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index 308bc44d3f..d2afeaf493 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -20,7 +20,8 @@ pub use effect::{Body, CallEffect, CallableBody, Callee, Edge, SparseForwardEffe pub use env::{EnvIndex, EnvStackStore, Store}; pub use error::InterpreterError; pub use frame::{ - ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameEffect, FrameEngine, drive_frames, + BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, }; pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; 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 261f50b444..d220ff3270 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_cursor.rs @@ -1,6 +1,7 @@ use kirin_ir::{Block, CompileStage, Product, SSAValue, Statement}; -use crate::{EnvIndex, FrameDriver, InterpreterError}; +use crate::core::frame::BlockBinding; +use crate::{BlockQueries, Env, EnvIndex, InterpreterError}; /// Block-cursor mechanics shared by the block-shaped walkers /// ([`BlockFrame`](super::BlockFrame) and [`CFGFrame`](super::CFGFrame)): @@ -15,7 +16,7 @@ pub(super) struct BlockCursor { cursor: Option, /// Entry arguments not yet bound. A frame built by a dialect frame is /// constructed without engine access — it binds on its first `step`, so - /// construction needs no [`FrameDriver`]. + /// construction needs no engine access. pending: Option>, /// Result slots awaiting a pushed child frame's completion values. resume_slots: Option>, @@ -43,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: FrameDriver, + I: Env + BlockQueries, { match self.pending.take() { Some(args) => { @@ -58,7 +59,7 @@ impl BlockCursor { /// Take the current statement, advancing the cursor past it. pub(super) fn advance(&mut self, interp: &I) -> Result, I::Error> where - I: FrameDriver, + I: BlockQueries, { let Some(statement) = self.cursor else { return Ok(None); @@ -76,7 +77,7 @@ impl BlockCursor { args: &Product, ) -> Result<(), I::Error> where - I: FrameDriver, + I: Env + BlockQueries, { interp.bind_block_args(self.stage, self.index, target, args)?; self.cursor = interp.first_statement(self.stage, target)?; @@ -96,12 +97,12 @@ impl BlockCursor { values: Product, ) -> Result<(), I::Error> where - I: FrameDriver, + I: Env, I::Error: From, { let slots = self.resume_slots.take().ok_or_else(|| { I::Error::from(InterpreterError::Custom("body resume without result slots")) })?; - interp.write_results(self.index, &slots, values) + interp.bind_values(self.index, slots.as_slice(), values) } } diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs index c4da6211c6..01d182fdee 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/block_frame.rs @@ -1,8 +1,8 @@ use kirin_ir::{Block, CompileStage, Product}; use crate::{ - EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, - SparseForwardInterp, + BlockQueries, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, StatementDispatch, }; use super::block_cursor::BlockCursor; @@ -44,7 +44,7 @@ where impl Frame for BlockFrame where - I: FrameDriver + SparseForwardInterp, + I: BlockQueries + StatementDispatch + SparseForwardInterp, F: FrameBuild, V: Clone, E: From, 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 b42920fca2..5d63080b50 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -1,7 +1,7 @@ use kirin_ir::{CompileStage, Product, SSAValue}; use crate::{ - Body, CallEffect, Callee, EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, + Body, CallEffect, CallServices, Callee, EnvIndex, Frame, FrameEffect, InterpreterError, }; use super::{BodyFrameEntry, CallBodyFramePolicy, Completion, DefaultBodyFrames, FrameBuild}; @@ -105,7 +105,7 @@ where impl Frame for CallFrame where - I: FrameDriver, + I: CallServices, F: FrameBuild, P: CallBodyFramePolicy, V: Clone, @@ -205,7 +205,7 @@ where interp.free_env(callee_env)?; match dest { CallDest::Caller { env, results } => { - interp.write_results(env, &results, values)?; + interp.bind_values(env, results.as_slice(), values)?; Ok(FrameEffect::Done) } CallDest::Root => Ok(FrameEffect::Complete(Completion::Returned(values))), diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs index fdcf88c8dd..fb3bb4d9df 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/cfg_frame.rs @@ -1,8 +1,8 @@ use kirin_ir::{CFG, CompileStage, Product}; use crate::{ - EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, - SparseForwardInterp, + CFGQueries, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, StatementDispatch, }; use super::block_cursor::BlockCursor; @@ -53,7 +53,7 @@ where impl Frame for CFGFrame where - I: FrameDriver + SparseForwardInterp, + I: CFGQueries + StatementDispatch + SparseForwardInterp, F: FrameBuild, 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 79af321db3..147de26d3a 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::{ - EnvIndex, Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardEffect, - SparseForwardInterp, + DiGraphQueries, Env, EnvIndex, Frame, FrameEffect, InterpreterError, SparseForwardEffect, + SparseForwardInterp, StatementDispatch, }; use super::{CallFrame, Completion, FrameBuild}; @@ -70,9 +70,12 @@ where /// Schedule exhausted: read the declared yields from the activation and /// complete `Finished` — the graph's natural completion. The parent /// decides what the values mean (call returns or push results). + /// + /// Reading the yields is all this step needs, so it asks for [`Env`] alone — + /// not [`DiGraphQueries`], whose schedule was already consumed. fn finish(self, interp: &mut I) -> Result>, E> where - I: FrameDriver, + I: Env, F: FrameBuild, { let values: Product = self @@ -86,7 +89,7 @@ where impl Frame for DiGraphFrame where - I: FrameDriver + SparseForwardInterp, + I: DiGraphQueries + StatementDispatch + SparseForwardInterp, F: FrameBuild, V: Clone, E: From, @@ -167,7 +170,7 @@ where "digraph resume without result slots", )) })?; - crate::FrameDriver::write_results(interp, self.index, &slots, values)?; + interp.bind_values(self.index, slots.as_slice(), values)?; Ok(FrameEffect::Continue(F::from_digraph(self))) } Completion::Returned(_) => Err(E::from(InterpreterError::Custom( diff --git a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs index a83878acf1..66ae05e39b 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/standard_frame.rs @@ -1,4 +1,4 @@ -use crate::{Frame, FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp}; +use crate::{ForwardFrameEngine, Frame, FrameEffect, InterpreterError, SparseForwardInterp}; use super::{ BlockFrame, CFGFrame, CallFrame, Completion, DefaultBodyFrames, DiGraphFrame, FrameBuild, @@ -38,7 +38,7 @@ impl FrameBuild for StandardFrame { /// re-enumerating its variants. impl Frame for StandardFrame where - I: FrameDriver + SparseForwardInterp, + I: ForwardFrameEngine + SparseForwardInterp, // The `Call` variant is spelled `CallFrame`, i.e. the default policy, so // an outer universe embedding `StandardFrame` must use that policy too. F: FrameBuild, diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index e15f9dc980..be2437aeda 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -4,10 +4,10 @@ use kirin_ir::{Block, CFG, CompileStage, Pipeline, Product, SSAValue, StageMeta, use crate::core::query; use crate::{ - CallFrame, CallableBody, Callee, Completion, Env, EnvIndex, EnvStackStore, ForwardEval, Frame, - FrameBuild, FrameDriver, FunctionTarget, Interp, InterpDispatch, InterpLocation, - InterpreterError, Linker, SameStageLinker, SparseForwardEffect, StageQuery, StandardFrame, - Store, drive_frames, + BlockQueries, CFGQueries, CallFrame, CallServices, CallableBody, Callee, Completion, + DiGraphQueries, Env, EnvIndex, EnvStackStore, ForwardEval, Frame, FrameBuild, FunctionTarget, + Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, SameStageLinker, + SparseForwardEffect, StageQuery, StandardFrame, StatementDispatch, Store, drive_frames, }; /// Concrete executor: runs IR over a concrete value domain with an explicit @@ -112,7 +112,11 @@ where } } -impl<'ir, S, V, E, Lk, F> FrameDriver for ConcreteInterpreter<'ir, S, V, E, Lk, F> +// The concrete engine provides the whole forward capability surface; it is split +// into one impl block per capability so the components stay individually +// nameable, and the blanket impl gives it `ForwardFrameEngine`/`ForwardFrameEngine`. + +impl<'ir, S, V, E, Lk, F> CallServices for ConcreteInterpreter<'ir, S, V, E, Lk, F> where S: StageQuery + InterpDispatch, V: Clone, @@ -133,47 +137,63 @@ where .map_err(E::from) } - fn run_statement( + fn enter_function( &mut self, stage: CompileStage, - statement: Statement, + body: Statement, + args: Product, index: EnvIndex, - ) -> Result { + ) -> Result, E> { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement, + statement: body, index, }); - let result = info.dispatch_statement(statement, self); + let result = info.dispatch_function_entry(body, args, self); self.location = previous; result } +} - fn enter_function( +impl<'ir, S, V, E, Lk, F> StatementDispatch for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ + fn run_statement( &mut self, stage: CompileStage, - body: Statement, - args: Product, + statement: Statement, index: EnvIndex, - ) -> Result, E> { + ) -> Result { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement: body, + statement, index, }); - let result = info.dispatch_function_entry(body, args, self); + let result = info.dispatch_statement(statement, self); self.location = previous; result } +} +impl<'ir, S, V, E, Lk, F> BlockQueries for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { query::block_params(self.pipeline, stage, block).map_err(E::from) } @@ -190,11 +210,27 @@ where ) -> Result, E> { query::next_statement(self.pipeline, stage, block, after).map_err(E::from) } +} +impl<'ir, S, V, E, Lk, F> CFGQueries for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, E> { query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } +} +impl<'ir, S, V, E, Lk, F> DiGraphQueries for ConcreteInterpreter<'ir, S, V, E, Lk, F> +where + S: StageQuery + InterpDispatch, + V: Clone, + E: From, + Lk: Linker, +{ fn digraph_walk_plan( &self, stage: CompileStage, diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index 71f659e8f4..97f3caa173 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -15,7 +15,7 @@ use std::marker::PhantomData; use kirin_ir::{Block, CompileStage, Statement}; use crate::{ - DenseBackwardCompletion, DenseBackwardEffect, DenseBackwardFrameDriver, Frame, FrameEffect, + DenseBackwardCompletion, DenseBackwardEffect, DenseBackwardFrameEngine, Frame, FrameEffect, InterpreterError, }; @@ -81,7 +81,7 @@ where impl Frame for DenseBlockFrame where - I: DenseBackwardFrameDriver, + I: DenseBackwardFrameEngine, F: DenseFrameBuild, V: Clone, E: From, @@ -206,7 +206,7 @@ impl DenseFrameBuild for StandardDenseBackwardFrame { /// [`Frame`] for what that buys. impl Frame for StandardDenseBackwardFrame where - I: DenseBackwardFrameDriver, + I: DenseBackwardFrameEngine, F: DenseFrameBuild, V: Clone, E: From, diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 591ee96538..06385f403f 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -26,7 +26,7 @@ //! summaries ([`BlockLiveness`], keyed by [`Scoped`] blocks), the block //! worklist, and [`BackwardSummaryDeps`] (successor changed → reanalyse //! predecessor, registered self-discoveringly by -//! [`absorb_edges`](DenseBackwardFrameDriver::absorb_edges)). +//! [`absorb_edges`](DenseBackwardFrameEngine::absorb_edges)). //! //! # Owners are blocks; one owner analysis is one backward walk //! @@ -405,9 +405,9 @@ pub type DenseBackwardDriver<'ir, S, V, E, F, Sem = ClassicLiveness> = StandardF // Driver capabilities (frames run on the driver) // =========================================================================== -/// The dense-backward frame-driver capability surface: what the dense frames +/// The dense-backward engine-capability surface: what the dense frames /// need from the engine. Implemented on the driver (it needs the summaries). -pub trait DenseBackwardFrameDriver: Interp> { +pub trait DenseBackwardFrameEngine: Interp> { /// The engine's total backward frame type. type Frame; @@ -461,7 +461,7 @@ pub trait DenseBackwardFrameDriver: Interp Result; } -impl<'ir, S, V, E, F, Sem> DenseBackwardFrameDriver for DenseBackwardDriver<'ir, S, V, E, F, Sem> +impl<'ir, S, V, E, F, Sem> DenseBackwardFrameEngine for DenseBackwardDriver<'ir, S, V, E, F, Sem> where S: StageMeta + StageQuery + InterpDispatch>, V: Clone + PartialEq + Lattice + HasBottom + DenseBackwardState, diff --git a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs index 5227c7e96c..ffffe9890b 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs @@ -8,7 +8,7 @@ pub(crate) mod interp; pub use frames::{DenseBlockFrame, DenseBlockMode, DenseFrameBuild, StandardDenseBackwardFrame}; pub use interp::{ BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, - DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameDriver, DenseBackwardInterp, + DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, PointFacts, SuccessorEdge, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs index 03bffa6e4f..8b6013e187 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/frames.rs @@ -15,7 +15,7 @@ //! policy lives in that dialect frame (it may reuse [`AbstractBlockFrame`] to //! walk a chosen body). The interprocedural //! *policy* (summary keying, join/widen, caller recording — including same-key -//! recursion) stays atomic in the engine behind [`AbstractFrameDriver`]; frames +//! recursion) stays atomic in the engine behind [`ForwardDataflowFrameEngine`]; frames //! only choose what to step next. use std::collections::VecDeque; @@ -24,9 +24,10 @@ use std::marker::PhantomData; use kirin_ir::{Block, CompileStage, DiGraph, Product, SSAValue, Statement}; +use crate::core::frame::BlockBinding; use crate::{ - AbstractFrameDriver, Body, CallEffect, Edge, EnvIndex, Frame, FrameEffect, InterpreterError, - SparseForwardEffect, SparseForwardInterp, + Body, CallEffect, Edge, Env, EnvIndex, ForwardDataflowFrameEngine, Frame, FrameEffect, + InterpreterError, SparseForwardEffect, SparseForwardInterp, }; /// Completion payloads produced by the standard abstract frames. @@ -142,7 +143,8 @@ where impl Frame for AbstractBlockFrame where - I: AbstractFrameDriver + SparseForwardInterp, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, F: AbstractFrameBuild, V: Clone + PartialEq, E: From, @@ -237,7 +239,7 @@ where "block resume without result slots", )) })?; - crate::FrameDriver::write_results(interp, self.index, &slots, values)?; + interp.bind_values(self.index, slots.as_slice(), values)?; Ok(FrameEffect::Continue(F::from_block(self))) } // A nested push returned without finishing: this pass left via return. @@ -331,9 +333,10 @@ where /// complete. The parent decides what the values mean — a graph **owner** /// turns them into the function's return, a pushing statement binds them /// into its result slots. + /// Reading the yields needs [`Env`] alone, not the whole dataflow surface. fn finish(self, interp: &mut I) -> Result>, E> where - I: AbstractFrameDriver, + I: Env, F: AbstractFrameBuild, { let values: Product = self @@ -349,7 +352,8 @@ where impl Frame for AbstractDiGraphFrame where - I: AbstractFrameDriver + SparseForwardInterp, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, F: AbstractFrameBuild, V: Clone + PartialEq, E: From, @@ -427,7 +431,7 @@ where "digraph resume without result slots", )) })?; - crate::FrameDriver::write_results(interp, self.index, &slots, values)?; + interp.bind_values(self.index, slots.as_slice(), values)?; Ok(FrameEffect::Continue(F::from_digraph(self)?)) } // A nested push left via `return`. A digraph has no function-return @@ -477,7 +481,7 @@ where impl Frame for AbstractCallFrame where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, F: AbstractFrameBuild, V: Clone + PartialEq, E: From, @@ -536,7 +540,8 @@ impl AbstractFrameBuild for StandardAbstractFrame { /// [`Frame`] for what that buys. impl Frame for StandardAbstractFrame where - I: AbstractFrameDriver + SparseForwardInterp, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, F: AbstractFrameBuild, V: Clone + PartialEq, E: From, diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index d7cec3c574..8b5ca33f5b 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -8,7 +8,9 @@ //! //! - **[`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 ([`ForwardFrameDriver`]). +//! 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 @@ -41,11 +43,12 @@ use kirin_ir::{ use crate::core::query; use crate::{ AbstractBlockFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractFrameBuild, - AbstractFrameDriver, AbstractInterpreter, Body, CallEffect, CallableBody, Callee, Env, - EnvIndex, EnvStackStore, FixpointProfile, ForwardEval, ForwardFrameDriver, ForwardSummaryDeps, - Frame, FunctionTarget, Interp, InterpDispatch, InterpLocation, InterpreterError, Linker, - OwnerSemantics, SameStageLinker, SparseForwardEffect, SparseForwardSemantic, StageQuery, - StandardAbstractFrame, StandardFixpointInterpreter, Store, Summary, SummaryDependency, + AbstractInterpreter, BlockQueries, Body, CFGQueries, CallEffect, CallServices, CallableBody, + Callee, DiGraphQueries, Env, EnvIndex, EnvStackStore, FixpointProfile, + ForwardDataflowFrameEngine, ForwardEval, ForwardSummaryDeps, Frame, FunctionTarget, Interp, + InterpDispatch, InterpLocation, InterpreterError, Linker, OwnerSemantics, SameStageLinker, + SparseForwardEffect, SparseForwardSemantic, StageQuery, StandardAbstractFrame, + StandardFixpointInterpreter, StatementDispatch, Store, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, }; @@ -500,7 +503,7 @@ where } // Policy-driven merge + return accumulation, kept on the transfer (the analysis `P` -// lives here). The driver's `AbstractFrameDriver` impl delegates to these. +// lives here). The driver's `ForwardDataflowFrameEngine` impl delegates to these. impl<'ir, S: StageMeta, V, E, Lk, P, F, Sem> SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> where V: Clone + PartialEq + Widen, @@ -616,8 +619,12 @@ where { } -// The IR-query / dispatch capability surface. Dialect rules dispatch on the transfer. -impl<'ir, S, V, E, Lk, P, F, Sem> ForwardFrameDriver +// The IR-query / dispatch capability surface. Dialect rules dispatch on the +// transfer. The transfer implements the *concrete* call lifecycle too, even +// though the abstract frames never use it: `SparseForwardTransfer` is also the +// engine a concrete-shaped caller can drive, and keeping it whole preserves the +// existing delegation to `ForwardDriver` unchanged. +impl<'ir, S, V, E, Lk, P, F, Sem> CallServices for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> where S: StageQuery + InterpDispatch, @@ -641,47 +648,69 @@ where .map_err(E::from) } - fn run_statement( + fn enter_function( &mut self, stage: CompileStage, - statement: Statement, + body: Statement, + args: Product, index: EnvIndex, - ) -> Result { + ) -> Result, E> { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement, + statement: body, index, }); - let result = info.dispatch_statement(statement, self); + let result = info.dispatch_function_entry(body, args, self); self.location = previous; result } +} - fn enter_function( +impl<'ir, S, V, E, Lk, P, F, Sem> StatementDispatch + for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ + fn run_statement( &mut self, stage: CompileStage, - body: Statement, - args: Product, + statement: Statement, index: EnvIndex, - ) -> Result, E> { + ) -> Result { let pipeline = self.pipeline; let info = pipeline .stage(stage) .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement: body, + statement, index, }); - let result = info.dispatch_function_entry(body, args, self); + let result = info.dispatch_statement(statement, self); self.location = previous; result } +} +impl<'ir, S, V, E, Lk, P, F, Sem> BlockQueries + for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { query::block_params(self.pipeline, stage, block).map_err(E::from) } @@ -698,11 +727,32 @@ where ) -> Result, E> { query::next_statement(self.pipeline, stage, block, after).map_err(E::from) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> CFGQueries for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, E> { query::cfg_entry(self.pipeline, stage, cfg).map_err(E::from) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> DiGraphQueries + for SparseForwardTransfer<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn digraph_walk_plan( &self, stage: CompileStage, @@ -716,7 +766,8 @@ where // Driver capability impls (frames run on the driver, which delegates to the transfer) // =========================================================================== -impl<'ir, S, V, E, Lk, P, F, Sem> ForwardFrameDriver for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +// Delegation is unchanged; only the trait each group of methods belongs to. +impl<'ir, S, V, E, Lk, P, F, Sem> CallServices for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> where S: StageQuery + InterpDispatch>, V: Clone + HasBottom, @@ -737,15 +788,6 @@ where self.inner().resolve_call(stage, callee) } - fn run_statement( - &mut self, - stage: CompileStage, - statement: Statement, - index: EnvIndex, - ) -> Result { - self.inner_mut().run_statement(stage, statement, index) - } - fn enter_function( &mut self, stage: CompileStage, @@ -755,7 +797,36 @@ where ) -> Result, E> { self.inner_mut().enter_function(stage, body, args, index) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> StatementDispatch for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ + fn run_statement( + &mut self, + stage: CompileStage, + statement: Statement, + index: EnvIndex, + ) -> Result { + self.inner_mut().run_statement(stage, statement, index) + } +} + +impl<'ir, S, V, E, Lk, P, F, Sem> BlockQueries for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { self.inner().block_params(stage, block) } @@ -772,11 +843,31 @@ where ) -> Result, E> { self.inner().next_statement(stage, block, after) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> CFGQueries for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn cfg_entry(&self, stage: CompileStage, cfg: CFG) -> Result, E> { self.inner().cfg_entry(stage, cfg) } +} +impl<'ir, S, V, E, Lk, P, F, Sem> DiGraphQueries for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +where + S: StageQuery + InterpDispatch>, + V: Clone + HasBottom, + E: From, + Lk: Linker, + P: CallContext, + Sem: SparseForwardSemantic, +{ fn digraph_walk_plan( &self, stage: CompileStage, @@ -786,7 +877,8 @@ where } } -impl<'ir, S, V, E, Lk, P, F, Sem> AbstractFrameDriver for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> +impl<'ir, S, V, E, Lk, P, F, Sem> ForwardDataflowFrameEngine + for ForwardDriver<'ir, S, V, E, Lk, P, F, Sem> where S: StageQuery + InterpDispatch>, V: Clone + PartialEq + Widen + HasBottom, @@ -853,7 +945,7 @@ where .and_then(|info| info.as_function()) .and_then(|function| function.ret.clone()); match ret { - Some(values) => self.write_results(index, &results, values), + Some(values) => self.bind_values(index, results.as_slice(), values), None => { for slot in results.iter().copied() { self.env_write(index, slot, V::bottom())?; diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 9790b97000..d21f512c0f 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -78,14 +78,18 @@ pub use self::core::{InterpreterError, StageQuery}; // The body-shape IR query: what blocks/graphs a body contains, which // statements feed a block's parameters, and where a graph port sits. pub use self::core::{BlockTopology, BodyTopology, body_topology}; -// The shared, direction-neutral frame protocol (`Frame`/`FrameEngine`/ -// `FrameEffect`/`drive_frames`) plus the forward frame-driver capability surfaces. +// 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::{ - ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameEffect, FrameEngine, drive_frames, + BlockQueries, CFGQueries, CallServices, DiGraphQueries, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameEffect, FrameEngine, StatementDispatch, drive_frames, }; -// Backward-compatible aliases for the forward frame-driver capability surfaces. -pub use self::core::ForwardDataflowFrameDriver as AbstractFrameDriver; -pub use self::core::ForwardFrameDriver as FrameDriver; // Concrete execution engine + the concrete standard frames: the // representation walkers (`BlockFrame`/`CFGFrame`/`DiGraphFrame` — `UnGraph` @@ -110,7 +114,7 @@ pub use engines::sparse_backward::{ // Dense backward engine (`Sem = ClassicLiveness`) + the dense standard frames. pub use engines::dense_backward::{ BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, - DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameDriver, DenseBackwardInterp, + DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, DenseBlockFrame, DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, SuccessorEdge, @@ -169,16 +173,16 @@ pub mod dialect { pub mod engine { pub use crate::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, - AbstractFrameBuild, AbstractFrameDriver, AbstractInterpreter, BlockFrame, BodyFrameEntry, - CFGFrame, CallBodyFramePolicy, CallContext, CallFrame, Callee, Completion, - ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DefaultBodyFrames, - DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBackwardInterp, + AbstractFrameBuild, AbstractInterpreter, BlockFrame, BlockQueries, BodyFrameEntry, + CFGFrame, CFGQueries, CallBodyFramePolicy, CallContext, CallFrame, CallServices, Callee, + Completion, ConcreteInterpreter, ContextInsensitive, CrossStageLinker, DefaultBodyFrames, + DenseBackwardCompletion, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, - DiGraphFrame, Env, ForwardDataflowFrameDriver, ForwardFrameDriver, Frame, FrameBuild, - FrameDriver, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, + DiGraphFrame, DiGraphQueries, Env, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, + FrameBuild, FrameEffect, FrameEngine, FunctionTarget, Interp, InterpDispatch, InterpreterError, Linker, SameStageLinker, SparseBackwardInterp, SparseBackwardInterpreter, SparseForwardInterp, SparseForwardInterpreter, StandardAbstractFrame, - StandardDenseBackwardFrame, StandardFrame, UnGraphEntry, WideningStrategy, drive_frames, - expect_single, + StandardDenseBackwardFrame, StandardFrame, StatementDispatch, UnGraphEntry, + WideningStrategy, drive_frames, expect_single, }; } diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index 2259f8ebb5..dd216db1ec 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -32,10 +32,10 @@ use kirin_interpreter::dialect::{ StrongDemand, }; use kirin_interpreter::{ - AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, AbstractFrameDriver, BlockFrame, - CallContext, Completion, ConcreteInterpreter, DenseBackwardCompletion, - DenseBackwardFrameDriver, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, EnvIndex, - Frame, FrameBuild, FrameDriver, FrameEffect, SparseForwardTransfer, + AbstractBlockFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, CallContext, + Completion, ConcreteInterpreter, DenseBackwardCompletion, DenseBackwardFrameEngine, + DenseBackwardState, DenseBlockFrame, DenseFrameBuild, Env, EnvIndex, + ForwardDataflowFrameEngine, Frame, FrameBuild, FrameEffect, FrameEngine, SparseForwardTransfer, }; use crate::{For, ForLoopValue, If, Yield}; @@ -304,7 +304,7 @@ impl DenseScfIfFrame { impl Frame for DenseScfIfFrame where - I: DenseBackwardFrameDriver, + I: DenseBackwardFrameEngine, F: DenseFrameBuild + BuildDenseScfIf, V: Clone + Lattice, E: From, @@ -416,7 +416,7 @@ impl DenseScfForFrame { impl Frame for DenseScfForFrame where - I: DenseBackwardFrameDriver, + I: DenseBackwardFrameEngine, F: DenseFrameBuild + BuildDenseScfFor, V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, @@ -676,9 +676,14 @@ where } } +/// `scf.if` decides its arm *before* the frame is built (the rule reads the +/// condition), so stepping it touches no engine capability at all — only the +/// error type. This is the narrowest bound in the codebase, and it is the point +/// of splitting the driver: a dialect frame that makes a decision and delegates +/// the walking should not have to name an engine that can allocate activations. impl Frame for ScfIfFrame where - I: FrameDriver, + I: FrameEngine, F: FrameBuild + BuildScfIf, V: Clone, E: From, @@ -767,7 +772,7 @@ where fn join_acc(&mut self, interp: &mut I, values: Product) -> Result<(), E> where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, { let merged = match self.acc.take() { None => values, @@ -780,7 +785,7 @@ where impl Frame for AbstractScfIfFrame where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, F: AbstractFrameBuild + BuildAbstractScfIf, V: Clone + PartialEq + Lattice, E: From, @@ -878,9 +883,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. impl Frame for ScfForFrame where - I: FrameDriver, + I: Env, F: FrameBuild + BuildScfFor, V: Clone + ForLoopValue, E: From, @@ -1008,7 +1015,7 @@ where fn join_finish(&mut self, interp: &mut I, values: Product) -> Result<(), E> where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, { let merged = match self.finish.take() { None => values, @@ -1021,7 +1028,7 @@ where impl Frame for AbstractScfForFrame where - I: AbstractFrameDriver, + I: ForwardDataflowFrameEngine, F: AbstractFrameBuild + BuildAbstractScfFor, V: Clone + PartialEq + ForLoopValue + Lattice, E: From, diff --git a/docs/design/formalism/operational-semantics.md b/docs/design/formalism/operational-semantics.md index 7fc3ab8335..10fb1b8fe2 100644 --- a/docs/design/formalism/operational-semantics.md +++ b/docs/design/formalism/operational-semantics.md @@ -18,7 +18,7 @@ | Loop transition strategy | `ScopeHook`, `ScopeStep` | [`crates/kirin-interpreter/src/effect.rs`](../../../crates/kirin-interpreter/src/effect.rs) | | Statement dispatch | `Interpretable`, `InterpDispatch` | [`crates/kirin-interpreter/src/dispatch.rs`](../../../crates/kirin-interpreter/src/dispatch.rs) | | Current statement location | `InterpLocation` | [`crates/kirin-interpreter/src/interp.rs`](../../../crates/kirin-interpreter/src/interp.rs) | -| Frame protocol | `Frame`, `FrameDriver` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | +| Frame protocol | `Frame`, `ForwardFrameEngine` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | | Scope continuation frame | `ScopeFrame` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | | Call continuation frame | `CallFrame` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | | Total frame enum | `StandardFrame` | [`crates/kirin-interpreter/src/frame.rs`](../../../crates/kirin-interpreter/src/frame.rs) | diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index 28cf1f4f3b..e8eee49fd1 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -352,7 +352,7 @@ and one *specialization* of the shared framework in the forward direction (it se lattice-valued abstract engines. `SparseForwardInterpreter` is the forward engine; `SparseBackwardInterpreter` (per-SSA demand / strong liveness) and `DenseBackwardInterpreter` (classic per-point liveness) are the backward -specializations — each with its own fact store, effect, and frame-driver +specializations — each with its own fact store, effect, and engine-capability capability, reusing the same framework (fixpoint driver + `*Transfer` inner `Interp`) and also implementing `AbstractInterpreter`. @@ -416,20 +416,105 @@ type `F` is the engine's generic; it is named in `Interpretable` *only* by a structured dialect building `SparseForwardEffect::Push` (through `SparseForwardInterp::Frame`) — ordinary dialects never mention it. -### Shared protocol vs. forward frame drivers +### Shared protocol vs. forward engine capabilities `Frame`, `FrameEngine`, `FrameEffect`, and `drive_frames` are **shared and direction-neutral** — they say nothing about a value domain or direction, and the backward engines reuse them as-is. On top of that neutral protocol sit the -per-direction frame-driver capability surfaces: `ForwardFrameDriver` / -`ForwardDataflowFrameDriver` for the forward engines (they require `Env`, run -`SparseForwardEffect`, bind block args, write forward results, and summarize -forward calls; `FrameDriver` and `AbstractFrameDriver` are retained as -compatibility aliases), and `DenseBackwardFrameDriver` for the dense backward -engine (statement dispatch, point-state access, edge absorption against the -converged summaries, per-point recording). The sparse backward engine needs no -frame-driver surface at all — its `DemandFrame` dispatches rules directly on -the driver. +per-direction engine-capability surfaces: the forward **component traits** +below, composed by the `ForwardFrameEngine` / `ForwardDataflowFrameEngine` +umbrellas, and `DenseBackwardFrameEngine` for the dense backward engine +(statement dispatch, point-state access, edge absorption against the converged +summaries, per-point recording). The sparse backward engine needs no capability +surface at all — its `DemandFrame` dispatches rules directly on the transfer. + +**"Engine" means three different things**, so the names are kept distinct: + +| name | what it is | +|---|---| +| `drive_frames` | the **frame-stack driver** — the loop. The only thing called a driver at this layer; `ForwardDriver`/`DenseBackwardDriver` are fixpoint-driver *structs*, not capability traits. | +| `FrameEngine` | the **minimal engine contract** the generic frame stack needs: a total `Error` type, nothing more. | +| `ForwardFrameEngine` | the **full engine capability set** for the standard concrete frame universe. | +| `ForwardDataflowFrameEngine` | the capability set for the standard forward-abstract frame universe. | +| component traits | narrowly scoped **services used by individual frames**. | + +#### The forward capability model + +Forward capabilities are split by **what one frame needs**, not by what one +engine happens to provide. A frame's bound is then a precise statement of which +engine operations it can reach, and an engine that implements only part of the +surface still runs the frames it can support. + +| trait | capability | required by | +|---|---|---| +| `StatementDispatch: Interp` | `run_statement` — dispatch to the dialect rule | every executing frame | +| `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_call`/`enter_function` | `CallFrame` | + +**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 +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. + +`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. It is deliberately **not** split further: the standard `CallFrame` +consumes all four 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. + +`StatementDispatch` and `InterpDispatch` face opposite directions and are easy +to confuse. `InterpDispatch` is implemented by a **stage/language** to route a +statement to the right dialect rule. `StatementDispatch` is implemented by the +**engine** and is what a *frame* calls: it stashes the current location +(`stage`/`statement`/`index`) so the rule can read it back through `Interp`, then +delegates to `InterpDispatch`. + +Two umbrellas compose them, one per engine family. Use an umbrella at the +*universe* level — a total frame enum's engine must support the union of all its +variants — and the components at the *member* level: + +```rust +// Full concrete surface. Adds no methods; blanket-implemented. +pub trait ForwardFrameEngine: + StatementDispatch + CFGQueries + DiGraphQueries + CallServices {} +impl ForwardFrameEngine for T +where T: StatementDispatch + 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 +{ + type SummaryKey: Clone + Eq + Hash; + fn analysis_merge(..); fn contribute_return(..); fn current_function_key(..); + fn summarize_call(..); fn max_iterations(..); +} +``` + +An abstract engine therefore **no longer inherits the concrete call lifecycle**. +That follows the semantics: forward abstract interpretation *summarizes* a call +(`summarize_call` → `AbstractCallFrame`) rather than descending into it, and +reaches a callable body's entry block through `Owner` seeding in the fixpoint +driver rather than `cfg_entry`. Requiring it to expose `alloc_env`, `free_env`, +`enter_function`, `resolve_call`, 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. + +Binding values into an **explicitly selected** activation is +`Env::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 — +so neither name mentions the `Product` container it happens to accept. ```rust pub enum FrameEffect { Continue(F), Push { parent: F, child: F }, Done, Complete(C) } @@ -451,8 +536,14 @@ pub trait Frame: Sized { pub fn drive_frames>(engine: &mut I, frames: &mut Vec) -> Result; -// Forward-specific capability surface (alias: FrameDriver): -pub trait ForwardFrameDriver: Env { /* env alloc/free, IR queries, dispatch, resolution */ } +// Forward-specific capability surface: one component trait per kind of +// traversal, plus two umbrellas — see "The forward capability model" above. +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_call, enter_function */ } +pub(crate) trait BlockBinding: Env + BlockQueries { /* bind_block_args */ } ``` **Members and universes.** The `F` parameter is what lets one trait serve both @@ -478,12 +569,27 @@ forward value engine `Interp` — so the frame protocol is decoupled from forwar value interpretation and reusable by other analyses. Every `Interp` is a `FrameEngine` by blanket impl. The engine owns a `Vec` and calls `drive_frames`, which pops the top frame, `step_into`s it, and applies the -returned `FrameEffect`. `ForwardFrameDriver: Env` is the richer **forward** -capability surface the *forward* frames call (it requires `Env` because the -default `bind_block_args`/`write_results` use `env_write`); **both forward -engines implement it**. The concrete and -abstract standard frames are two *implementations* of this one protocol — not -parallel frameworks. +returned `FrameEffect`. The forward component traits above are the richer +capability surfaces the *forward* frames call; each forward frame bounds only the +components it uses, and the concrete engine implements all of them (so it also +gets `ForwardFrameEngine` by blanket impl). The concrete and abstract standard +frames are two *implementations* of this one protocol — not parallel frameworks. + +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 | +| `BlockFrame` | `BlockQueries + StatementDispatch + SparseForwardInterp` | +| `CFGFrame` | `CFGQueries + StatementDispatch + SparseForwardInterp` | +| `DiGraphFrame` | `DiGraphQueries + StatementDispatch + SparseForwardInterp` | +| `AbstractBlockFrame`, `AbstractCallFrame`, `AbstractDiGraphFrame` | `ForwardDataflowFrameEngine` (+ `SparseForwardInterp` for the walkers) | +| `StandardFrame`, `ToyFrame`, other total concrete enums | `ForwardFrameEngine + SparseForwardInterp` — correct at the universe level | +| `StandardAbstractFrame`, other total abstract enums | `ForwardDataflowFrameEngine + SparseForwardInterp` | ### Concrete frames — `BlockFrame` / `CFGFrame` / `DiGraphFrame` / `CallFrame` / `StandardFrame` @@ -546,7 +652,7 @@ through their own dialect frames (chosen per engine by `ScfIfDispatch` / bodies — they borrow the caller's activation and exit by `Yield` — so the callable-body policy plays no part. -### Abstract frames — `StandardAbstractFrame` / `AbstractFrameBuild` / `ForwardDataflowFrameDriver` +### Abstract frames — `StandardAbstractFrame` / `AbstractFrameBuild` / `ForwardDataflowFrameEngine` `SparseForwardInterpreter` is symmetrically generic over a total abstract frame type `F` (default `StandardAbstractFrame`). The standard abstract frames @@ -585,10 +691,15 @@ substantive way: a `Call` effect pushes an `AbstractCallFrame`, routing the call through `summarize_call` instead of descending into the callee. Descending would neither widen nor terminate on recursion. -Abstract frames need a few capabilities beyond `ForwardFrameDriver`, on -`ForwardDataflowFrameDriver: ForwardFrameDriver` (alias: `AbstractFrameDriver`) — -`analysis_merge`, `contribute_return`, and -`summarize_call`. The interprocedural protocol stays **atomic in the engine**: +Abstract frames need a few capabilities beyond the traversal they share with +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 +`CFGQueries`, since the entry block of a callable body arrives via `Owner` +seeding rather than `cfg_entry`. The interprocedural protocol stays **atomic in +the engine**: `summarize_call` performs resolve → key → join-into-callee-entry → record-caller (*including same-key recursion*) → read-return-summary in one step, so a custom frame chooses *what to traverse* but cannot reorder the summary protocol and diff --git a/example/toy-lang/src/interpreter/frame.rs b/example/toy-lang/src/interpreter/frame.rs index 1d1082ecf1..e7096c039f 100644 --- a/example/toy-lang/src/interpreter/frame.rs +++ b/example/toy-lang/src/interpreter/frame.rs @@ -12,10 +12,9 @@ use std::hash::Hash; use kirin_interpreter::engine::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BlockFrame, CFGFrame, CallFrame, Completion, DefaultBodyFrames, - DiGraphFrame, Frame, FrameBuild, FrameDriver, FrameEffect, InterpreterError, - SparseForwardInterp, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, + CFGFrame, CallFrame, Completion, DefaultBodyFrames, DiGraphFrame, ForwardDataflowFrameEngine, + ForwardFrameEngine, Frame, FrameBuild, FrameEffect, InterpreterError, SparseForwardInterp, }; use kirin_scf::{ AbstractScfForFrame, AbstractScfIfFrame, BuildAbstractScfFor, BuildAbstractScfIf, BuildScfFor, @@ -57,7 +56,7 @@ impl BuildScfFor for ToyFrame { impl Frame for ToyFrame where - I: FrameDriver + SparseForwardInterp, + I: ForwardFrameEngine + SparseForwardInterp, F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, @@ -132,7 +131,8 @@ impl BuildAbstractScfFor for ToyAbstractFrame { impl Frame for ToyAbstractFrame where - I: AbstractFrameDriver + SparseForwardInterp, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, F: AbstractFrameBuild + BuildAbstractScfIf + BuildAbstractScfFor, V: Clone + PartialEq + ForLoopValue + Lattice, E: From, @@ -178,7 +178,7 @@ where use kirin_interpreter::DenseBackwardState; use kirin_interpreter::engine::{ - DenseBackwardCompletion, DenseBackwardFrameDriver, DenseBlockFrame, DenseFrameBuild, + DenseBackwardCompletion, DenseBackwardFrameEngine, DenseBlockFrame, DenseFrameBuild, }; use kirin_scf::{BuildDenseScfFor, BuildDenseScfIf, DenseScfForFrame, DenseScfIfFrame}; @@ -207,7 +207,7 @@ impl BuildDenseScfFor for ToyDenseBackwardFrame { impl Frame for ToyDenseBackwardFrame where - I: DenseBackwardFrameDriver, + I: DenseBackwardFrameEngine, F: DenseFrameBuild + BuildDenseScfIf + BuildDenseScfFor, V: Clone + PartialEq + Lattice + DenseBackwardState, E: From, diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 1332f2c0eb..1ad99108b4 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -565,10 +565,10 @@ mod advanced { use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_interpreter::engine::{ - AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, - AbstractFrameDriver, BlockFrame, CFGFrame, CallContext, CallFrame, Completion, - ConcreteInterpreter, CrossStageLinker, DefaultBodyFrames, DiGraphFrame, Frame, FrameBuild, - FrameDriver, FrameEffect, InterpreterError, SparseForwardInterp, SparseForwardInterpreter, + AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, + CFGFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, CrossStageLinker, + DefaultBodyFrames, DiGraphFrame, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, + FrameBuild, FrameEffect, InterpreterError, SparseForwardInterp, SparseForwardInterpreter, expect_single, }; use kirin_scf::{ @@ -639,7 +639,7 @@ mod advanced { impl Frame for TracingFrame where - I: FrameDriver + SparseForwardInterp, + I: ForwardFrameEngine + SparseForwardInterp, F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + ForLoopValue, E: From, @@ -779,7 +779,7 @@ mod advanced { impl Frame for TracingAbstractFrame where - I: AbstractFrameDriver + I: ForwardDataflowFrameEngine + SparseForwardInterp, F: AbstractFrameBuild + BuildAbstractScfIf + BuildAbstractScfFor, V: Clone + PartialEq + ForLoopValue + Lattice, diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 4bc09b2333..2d99523ec1 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -42,12 +42,12 @@ use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_function::Lexical; use kirin_interpreter::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, - AbstractFrameBuild, AbstractFrameDriver, BlockFrame, Body, BodyFrameEntry, CFGFrame, - CallBodyFramePolicy, CallContext, CallFrame, Completion, ConcreteInterpreter, - ContextInsensitive, DefaultBodyFrames, DiGraphFrame, Env, EnvIndex, Frame, FrameBuild, - FrameDriver, FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, + AbstractFrameBuild, BlockFrame, Body, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, + CallContext, CallFrame, Completion, ConcreteInterpreter, ContextInsensitive, DefaultBodyFrames, + DiGraphFrame, Env, EnvIndex, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, FrameBuild, + FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, StandardFrame, - UnGraphEntry, expect_single, + StatementDispatch, UnGraphEntry, expect_single, }; use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -309,7 +309,7 @@ impl BuildScfFor for ScfTestFrame { impl Frame for ScfTestFrame where - I: FrameDriver + SparseForwardInterp, + I: ForwardFrameEngine + SparseForwardInterp, F: FrameBuild + BuildScfIf + BuildScfFor, V: Clone + kirin_scf::ForLoopValue, E: From, @@ -808,7 +808,8 @@ impl FrameBuild for GraphAbstractFrame { impl Frame for GraphAbstractFrame where - I: AbstractFrameDriver + SparseForwardInterp, + I: ForwardDataflowFrameEngine + + SparseForwardInterp, F: AbstractFrameBuild, V: Clone + PartialEq, E: From, @@ -1497,7 +1498,8 @@ impl BuildScfFor for PolicyFrame { impl Frame for PolicyFrame where - I: FrameDriver + SparseForwardInterp, + I: ForwardFrameEngine + + SparseForwardInterp, { type Completion = Completion; @@ -1678,7 +1680,7 @@ impl MyCfgWalker { impl Frame> for MyCfgWalker where - I: FrameDriver + SparseForwardInterp>, + I: ForwardFrameEngine + SparseForwardInterp>, V: Clone, E: From, { @@ -1758,7 +1760,7 @@ enum DerivedFrame { impl Frame for DerivedFrame where - I: FrameDriver + SparseForwardInterp>, + I: ForwardFrameEngine + SparseForwardInterp>, V: Clone, E: From, { diff --git a/tests/frame_engine_capabilities.rs b/tests/frame_engine_capabilities.rs new file mode 100644 index 0000000000..73e9eb29bf --- /dev/null +++ b/tests/frame_engine_capabilities.rs @@ -0,0 +1,575 @@ +//! Compile-time regression tests for the **engine-capability split**. +//! +//! 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_call`/ +//! `enter_function`/`cfg_entry`/`digraph_walk_plan`, and an abstract dataflow +//! engine had to expose a concrete call convention it never performs. +//! +//! | mock engine | pins | +//! |---|---| +//! | `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 | +//! +//! 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. + +// Everything here exists to be *type-checked*, not read: the frame variants +// prove the universes are constructible 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, AbstractFrameBuild, BlockFrame, + BlockQueries, CFGFrame, CFGQueries, CallEffect, CallFrame, CallServices, CallableBody, Callee, + DefaultBodyFrames, DiGraphFrame, DiGraphQueries, Env, EnvIndex, ForwardDataflowFrameEngine, + ForwardEval, ForwardFrameEngine, Frame, FrameBuild, FunctionTarget, Interp, InterpreterError, + 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, + T: Frame, +{ +} + +fn assert_dataflow_engine() {} + +/// The `*Queries` traits must be satisfiable **without** [`Env`] — that is what +/// makes their names truthful. +fn assert_read_only_queries() {} + +// =========================================================================== +// 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_call`/`enter_function`), [`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, +} + +impl Interp for BlockOnlyEngine { + type Value = i64; + type Error = InterpreterError; + type Effect = SparseForwardEffect; + 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") + } +} + +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 }) + } + + fn env_write( + &mut self, + index: EnvIndex, + value: SSAValue, + data: i64, + ) -> Result<(), InterpreterError> { + self.store.0.insert((index.raw(), value), data); + Ok(()) + } +} + +impl StatementDispatch for BlockOnlyEngine { + fn run_statement( + &mut self, + _stage: CompileStage, + _statement: Statement, + _index: EnvIndex, + ) -> Result { + unimplemented!("type-level mock") + } +} + +impl BlockQueries for BlockOnlyEngine { + fn block_params( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn first_statement( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn next_statement( + &self, + _stage: CompileStage, + _block: Block, + _after: Statement, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +/// A total concrete frame type for the mocks. +/// +/// Note it *carries* a [`CallFrame`] variant and implements +/// [`FrameBuild::from_call`]: **building** the universe is independent of whether +/// a given engine can **step** every variant. `BlockOnlyEngine` can step the +/// block walker but could never step this `Call` variant — and that is exactly +/// the separation the capability split expresses, so no `Frame` impl is +/// asserted for `MockFrame` itself. +enum MockFrame { + Block(BlockFrame), + CFG(CFGFrame), + Call(CallFrame), + DiGraph(DiGraphFrame), +} + +impl FrameBuild for MockFrame { + type BodyFrames = DefaultBodyFrames; + + fn from_block(frame: BlockFrame) -> Self { + MockFrame::Block(frame) + } + fn from_cfg(frame: CFGFrame) -> Self { + MockFrame::CFG(frame) + } + fn from_call(frame: CallFrame) -> Self { + MockFrame::Call(frame) + } + fn from_digraph(frame: DiGraphFrame) -> Self { + MockFrame::DiGraph(frame) + } +} + +#[test] +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, +} + +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") + } +} + +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 }) + } + + fn env_write( + &mut self, + index: EnvIndex, + value: SSAValue, + data: i64, + ) -> Result<(), InterpreterError> { + self.store.0.insert((index.raw(), value), data); + Ok(()) + } +} + +impl CallServices for CallOnlyEngine { + fn alloc_env(&mut self) -> EnvIndex { + unimplemented!("type-level mock") + } + fn free_env(&mut self, _index: EnvIndex) -> Result<(), InterpreterError> { + unimplemented!("type-level mock") + } + fn resolve_call( + &self, + _stage: CompileStage, + _callee: &Callee, + ) -> Result { + unimplemented!("type-level mock") + } + fn enter_function( + &mut self, + _stage: CompileStage, + _body: Statement, + _args: Product, + _index: EnvIndex, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +#[test] +fn call_frame_runs_on_an_engine_with_only_call_services() { + 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, `enter_function`, `resolve_call`, +/// 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; + + fn stage(&self) -> CompileStage { + unimplemented!("type-level mock") + } + fn statement(&self) -> Statement { + unimplemented!("type-level mock") + } + fn index(&self) -> EnvIndex { + unimplemented!("type-level mock") + } +} + +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 }) + } + + fn env_write( + &mut self, + index: EnvIndex, + value: SSAValue, + data: i64, + ) -> Result<(), InterpreterError> { + self.store.0.insert((index.raw(), value), data); + Ok(()) + } +} + +impl StatementDispatch for AbstractOnlyEngine { + fn run_statement( + &mut self, + _stage: CompileStage, + _statement: Statement, + _index: EnvIndex, + ) -> Result { + unimplemented!("type-level mock") + } +} + +impl BlockQueries for AbstractOnlyEngine { + fn block_params( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn first_statement( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn next_statement( + &self, + _stage: CompileStage, + _block: Block, + _after: Statement, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +/// 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 { + type SummaryKey = (); + + fn analysis_merge( + &self, + _current: &Product, + _incoming: &Product, + _visits: usize, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + + fn contribute_return(&mut self, _values: Product) -> Result<(), InterpreterError> { + unimplemented!("type-level mock") + } + + fn current_function_key(&self) -> Option<()> { + unimplemented!("type-level mock") + } + + fn summarize_call( + &mut self, + _stage: CompileStage, + _call: CallEffect, + _index: EnvIndex, + ) -> Result<(), InterpreterError> { + unimplemented!("type-level mock") + } + + fn max_iterations(&self) -> usize { + unimplemented!("type-level mock") + } +} + +/// The abstract counterpart of [`MockFrame`], again carrying the call variant it +/// can build but this engine could never step. +enum MockAbstractFrame { + Block(AbstractBlockFrame), + Call(AbstractCallFrame), + DiGraph(AbstractDiGraphFrame), +} + +impl AbstractFrameBuild for MockAbstractFrame { + fn from_block(frame: AbstractBlockFrame) -> Self { + MockAbstractFrame::Block(frame) + } + fn from_call(frame: AbstractCallFrame) -> Self { + MockAbstractFrame::Call(frame) + } + fn from_digraph( + frame: AbstractDiGraphFrame, + ) -> Result { + Ok(MockAbstractFrame::DiGraph(frame)) + } +} + +#[test] +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, + AbstractBlockFrame, + >(); + assert_frame::< + AbstractOnlyEngine, + MockAbstractFrame, + AbstractCallFrame, + >(); + assert_frame::< + AbstractOnlyEngine, + MockAbstractFrame, + AbstractDiGraphFrame, + >(); +} + +// =========================================================================== +// 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") + } +} + +impl BlockQueries for QueriesOnlyEngine { + fn block_params( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn first_statement( + &self, + _stage: CompileStage, + _block: Block, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } + fn next_statement( + &self, + _stage: CompileStage, + _block: Block, + _after: Statement, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +impl CFGQueries for QueriesOnlyEngine { + fn cfg_entry( + &self, + _stage: CompileStage, + _cfg: kirin_ir::CFG, + ) -> Result, InterpreterError> { + unimplemented!("type-level mock") + } +} + +impl DiGraphQueries for QueriesOnlyEngine {} + +#[test] +fn query_traits_are_satisfiable_without_env() { + assert_read_only_queries::(); +} From a61fb1449c62dd4a8d8139933d3a1fe21b1b5fea Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Mon, 10 Aug 2026 15:12:47 -0400 Subject: [PATCH 15/21] Refactor block and graph ownership semantics in IR - Introduced `BlockParent` enum to represent the immediate owner of a block, allowing for clearer ownership semantics between CFGs and statements. - Updated `BlockInfo` to include a `predecessors` field for reverse control-flow indexing, facilitating demand analysis. - Modified builders to correctly assign parents to blocks and graphs, ensuring ownership integrity during construction. - Removed unused boundary location structures and related code to streamline the anchor module. --- crates/kirin-interpreter/src/core/effect.rs | 4 +- crates/kirin-interpreter/src/core/mod.rs | 4 +- crates/kirin-interpreter/src/core/query.rs | 249 +++++++++++++++-- crates/kirin-interpreter/src/core/topology.rs | 252 ------------------ .../src/engines/dense_backward/frames.rs | 2 +- .../src/engines/dense_backward/interp.rs | 44 ++- .../src/engines/sparse_backward/interp.rs | 74 +++-- crates/kirin-interpreter/src/facts/anchor.rs | 23 +- crates/kirin-interpreter/src/facts/mod.rs | 10 +- crates/kirin-interpreter/src/lib.rs | 7 +- crates/kirin-ir/src/builder/block.rs | 3 +- crates/kirin-ir/src/builder/cfg.rs | 12 +- crates/kirin-ir/src/builder/stage_info.rs | 2 + crates/kirin-ir/src/builder/staged.rs | 47 ++++ crates/kirin-ir/src/detach.rs | 94 ++++--- crates/kirin-ir/src/lib.rs | 13 +- crates/kirin-ir/src/node/block.rs | 24 +- crates/kirin-ir/src/node/mod.rs | 2 +- crates/kirin-ir/src/query/info.rs | 4 +- crates/kirin-ir/src/stage/info.rs | 52 +++- crates/kirin-ir/tests/builder_block.rs | 106 ++++++++ crates/kirin-ir/tests/builder_graph.rs | 44 +++ crates/kirin-ir/tests/common.rs | 87 ++++-- crates/kirin-scf/src/interpreter.rs | 4 +- 24 files changed, 698 insertions(+), 465 deletions(-) delete mode 100644 crates/kirin-interpreter/src/core/topology.rs diff --git a/crates/kirin-interpreter/src/core/effect.rs b/crates/kirin-interpreter/src/core/effect.rs index 5ef55aa3b0..87a1429454 100644 --- a/crates/kirin-interpreter/src/core/effect.rs +++ b/crates/kirin-interpreter/src/core/effect.rs @@ -7,8 +7,8 @@ use kirin_ir::{ /// /// 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 +/// the moment a body is handed to the interpreter (callable entry, +/// body-containment 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 { diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index d2afeaf493..5dc2577d6d 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`], [`topology`]) engines run against a stage. +//! queries ([`query`]) engines run against a stage. //! Everything here is engine-agnostic; the engines compose these pieces. pub(crate) mod dispatch; @@ -12,7 +12,6 @@ pub(crate) mod frame; pub(crate) mod interp; pub(crate) mod linker; pub(crate) mod query; -pub(crate) mod topology; pub(crate) mod value; pub use dispatch::{FunctionEntry, InterpDispatch, Interpretable}; @@ -26,5 +25,4 @@ pub use frame::{ pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use query::{GraphWalkPlan, StageQuery}; -pub use topology::{BlockTopology, BodyTopology, body_topology}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index 7d9891ba30..8c8e35ed25 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -7,15 +7,14 @@ //! bound that any well-formed stage enum satisfies automatically. use kirin_ir::{ - Block, CFG, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasCFG, HasStageInfo, - HasSuccessors, Pipeline, SSAKind, SSAValue, SpecializedFunction, StageAction, StageInfo, - StageMeta, StagedFunction, Statement, SupportsStageDispatch, Symbol, - UniqueLiveSpecializationError, + Block, BlockParent, CFG, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasCFG, + HasDigraphs, HasStageInfo, HasUngraphs, Pipeline, PortParent, SSAKind, SSAValue, + SpecializedFunction, StageAction, StageInfo, StageMeta, StagedFunction, Statement, + SupportsStageDispatch, Symbol, UniqueLiveSpecializationError, }; use crate::Body; use crate::InterpreterError; -use crate::core::topology::{self, BodyTopology}; /// Block parameters as SSA values. pub struct BlockParams(pub Block); @@ -46,6 +45,39 @@ where } } +/// A block's statements in program order, with the terminator last. +pub struct BlockStatements(pub Block); + +impl BlockStatements { + fn collect(&self, info: &StageInfo) -> Result, InterpreterError> { + self.0 + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.0))?; + let mut statements: Vec = self.0.statements(info).collect(); + if let Some(terminator) = self.0.terminator(info) { + statements.push(terminator); + } + Ok(statements) + } +} + +impl StageAction for BlockStatements +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Vec; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + self.collect(info) + } +} + /// First statement of a block (head of the statement list, or the cached /// terminator for terminator-only blocks). pub struct FirstStatement(pub Block); @@ -277,23 +309,60 @@ where } } -/// The topology of a body: blocks and graph parts (including nested -/// structured bodies), statements per part, block feeders, and graph-port -/// boundaries. Forward CFG successors are deliberately absent — those are a -/// local IR query on a block's terminator. -pub struct BodyTopologyQuery(pub Body); +/// Statements whose backward rules translate demand on a block argument. +/// +/// A directly owned single-block body is translated by its structural owner. +/// A CFG block is translated by the terminator of each block in its finalized +/// predecessor index. +pub struct BlockArgumentPredecessors(pub Block); + +impl StageAction for BlockArgumentPredecessors +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Vec; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + let block = self + .0 + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.0))?; + + match block.parent { + Some(BlockParent::Statement(owner)) => Ok(vec![owner]), + Some(BlockParent::CFG(_)) => block + .predecessors + .iter() + .map(|predecessor| { + predecessor.terminator(info).ok_or(InterpreterError::Custom( + "CFG predecessor block has no terminator", + )) + }) + .collect(), + None => Ok(Vec::new()), + } + } +} + +/// The statement structurally owning a graph port's parent graph. +/// +/// The port's [`PortParent`] identifies the authoritative graph, whose +/// `GraphInfo::parent` field identifies the statement whose dialect rule +/// translates values and demand across the graph boundary. +pub struct GraphPortOwner(pub PortParent); -impl StageAction for BodyTopologyQuery +impl StageAction for GraphPortOwner where S: StageMeta + HasStageInfo, L: Dialect, - for<'a> L: HasSuccessors<'a> - + HasBlocks<'a> - + HasCFG<'a> - + kirin_ir::HasDigraphs<'a> - + kirin_ir::HasUngraphs<'a>, { - type Output = BodyTopology; + type Output = Statement; type Error = InterpreterError; fn run( @@ -301,7 +370,101 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - Ok(topology::body_topology(info, self.0)) + let owner = match self.0 { + PortParent::DiGraph(graph) => graph.get_info(info).and_then(|graph| graph.parent()), + PortParent::UnGraph(graph) => graph.get_info(info).and_then(|graph| graph.parent()), + }; + owner.ok_or(InterpreterError::Custom( + "graph port has no owning statement", + )) + } +} + +/// Blocks directly selected as dense fixpoint owners by an analysis root. +pub struct DirectBodyBlocks(pub Body); + +impl StageAction for DirectBodyBlocks +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Vec; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + Ok(match self.0 { + Body::CFG(cfg) => cfg.blocks(info).collect(), + Body::Block(block) => vec![block], + Body::DiGraph(_) | Body::UnGraph(_) => Vec::new(), + }) + } +} + +/// One step of a body-containment walk: the statements directly in this body +/// part and the child body parts reached from it. +pub struct BodyContents { + pub statements: Vec, + pub children: Vec, +} + +pub struct BodyContentsQuery(pub Body); + +impl StageAction for BodyContentsQuery +where + S: StageMeta + HasStageInfo, + L: Dialect, + for<'a> L: HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, +{ + type Output = BodyContents; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + let (statements, mut children) = match self.0 { + Body::CFG(cfg) => ( + Vec::new(), + cfg.blocks(info).map(Body::Block).collect::>(), + ), + Body::Block(block) => (BlockStatements(block).collect(info)?, Vec::new()), + Body::DiGraph(graph) => ( + graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + Vec::new(), + ), + Body::UnGraph(graph) => ( + graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + Vec::new(), + ), + }; + + for &statement in &statements { + let definition = statement.definition(info); + children.extend(definition.blocks().copied().map(Body::Block)); + children.extend(definition.cfgs().copied().map(Body::CFG)); + children.extend(definition.digraphs().copied().map(Body::DiGraph)); + children.extend(definition.ungraphs().copied().map(Body::UnGraph)); + } + + Ok(BodyContents { + statements, + children, + }) } } @@ -333,6 +496,7 @@ where pub trait StageQuery: StageMeta + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> @@ -344,7 +508,10 @@ pub trait StageQuery: + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch + SupportsStageDispatch { } @@ -352,6 +519,7 @@ pub trait StageQuery: impl StageQuery for S where S: StageMeta + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> @@ -363,7 +531,10 @@ impl StageQuery for S where + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch + SupportsStageDispatch { } @@ -392,6 +563,14 @@ pub(crate) fn block_params( dispatch(pipeline, stage, BlockParams(block)) } +pub(crate) fn block_statements( + pipeline: &Pipeline, + stage: CompileStage, + block: Block, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, BlockStatements(block)) +} + pub(crate) fn first_statement( pipeline: &Pipeline, stage: CompileStage, @@ -457,6 +636,22 @@ pub(crate) fn terminator_arguments( dispatch(pipeline, stage, TerminatorArguments(block)) } +pub(crate) fn block_argument_predecessors( + pipeline: &Pipeline, + stage: CompileStage, + block: Block, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, BlockArgumentPredecessors(block)) +} + +pub(crate) fn graph_port_owner( + pipeline: &Pipeline, + stage: CompileStage, + parent: PortParent, +) -> Result { + dispatch(pipeline, stage, GraphPortOwner(parent)) +} + pub(crate) fn digraph_walk_plan( pipeline: &Pipeline, stage: CompileStage, @@ -465,10 +660,18 @@ pub(crate) fn digraph_walk_plan( dispatch(pipeline, stage, DiGraphWalkQuery(graph)) } -pub(crate) fn body_topology( +pub(crate) fn direct_body_blocks( + pipeline: &Pipeline, + stage: CompileStage, + body: Body, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, DirectBodyBlocks(body)) +} + +pub(crate) fn body_contents( pipeline: &Pipeline, stage: CompileStage, body: Body, -) -> Result { - dispatch(pipeline, stage, BodyTopologyQuery(body)) +) -> Result { + dispatch(pipeline, stage, BodyContentsQuery(body)) } diff --git a/crates/kirin-interpreter/src/core/topology.rs b/crates/kirin-interpreter/src/core/topology.rs deleted file mode 100644 index 825339ee46..0000000000 --- a/crates/kirin-interpreter/src/core/topology.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Dialect-neutral body topology enumeration: the implementation behind the -//! [`BodyTopologyQuery`](super::query::BodyTopologyQuery) IR query. -//! -//! Backward analyses need the *shape* of a body: which blocks and graph -//! nodes exist (including bodies nested inside structured statements), each -//! block's statements, each block's *feeders* — the statements whose rules can -//! translate demand on that block's parameters (terminators targeting it, -//! statements owning it) — and each graph port's *boundary* (the statement -//! owning the graph, and the port's slot index). This is topology only — -//! uses/defs/edge-argument *semantics* stay in dialect -//! [`Interpretable`](crate::Interpretable) rules; the enumeration consumes the -//! generic [`HasSuccessors`]/[`HasBlocks`]/[`HasCFG`]/[`HasDigraphs`]/ -//! [`HasUngraphs`] contract every dialect derives. -//! -//! Note what is deliberately *not* here: the forward CFG successor relation. -//! That is a local IR query — `stmt.definition(stage).successors()` on a -//! block's terminator answers it — so materializing a copy would duplicate IR -//! state that can go stale. `feeders` is the *reverse* relation and is not -//! obtainable from a block alone (finding who targets it requires sweeping the -//! whole body), which is why this prepass materializes that one and not the -//! forward one. - -use std::collections::{HashMap, HashSet}; - -use kirin_ir::{ - Block, CFG, DiGraph, Dialect, GetInfo, HasBlocks, HasCFG, HasDigraphs, HasSuccessors, - HasUngraphs, Port, SSAValue, StageInfo, Statement, UnGraph, -}; - -use crate::{Body, PortBoundary}; - -/// The shape of one block: which block it is and the statements it contains. -#[derive(Clone, Debug)] -pub struct BlockTopology { - pub block: Block, - /// Statements in program order; the terminator, if any, is last. - pub stmts: Vec, - /// `true` for blocks nested inside a statement (structured bodies), - /// `false` for the analyzed body's own top-level blocks. - pub nested: bool, -} - -/// The shape of a body: all blocks and graph parts (the analyzed body's own -/// plus structured bodies, recursively), the block-feeder index, and the -/// graph-port boundary index. -/// -/// Graph parts contribute only their node statements, flattened: nothing -/// consumes per-graph grouping, the graph handle, or a nested flag, so none is -/// recorded. Order is enumeration order, not a schedule — scheduling is the -/// walker's job, and backward prepasses only need *all* statements. -#[derive(Clone, Debug, Default)] -pub struct BodyTopology { - pub blocks: Vec, - /// Position of each block within `blocks`, so a lookup by [`Block`] is O(1) - /// rather than a scan. Built during collection; holds no statements of its - /// own. - block_index: HashMap, - graph_stmts: Vec, - feeders: HashMap>, - port_boundary: HashMap, -} - -impl BodyTopology { - /// The statements whose rules can translate demand on `block`'s parameters: - /// terminators with an edge into `block`, plus statements owning `block` - /// as a structured body. - pub fn feeders(&self, block: Block) -> &[Statement] { - self.feeders.get(&block).map(Vec::as_slice).unwrap_or(&[]) - } - - /// The analyzed body's own top-level blocks (excluding nested bodies). - pub fn cfg_blocks(&self) -> impl Iterator { - self.blocks.iter().filter(|block| !block.nested) - } - - /// The statements of `block` in program order (terminator last), if this - /// topology enumerated it. - /// - /// O(1) via `block_index`. Worth indexing: the dense backward engine asks - /// this once per block-owner analysis, and owners are re-analyzed until the - /// fixpoint converges — a scan here is O(blocks) per iteration. - pub fn block_statements(&self, block: Block) -> Option<&[Statement]> { - self.block_index - .get(&block) - .map(|&position| self.blocks[position].stmts.as_slice()) - } - - /// Where `port` sits on its owning statement's boundary, if the port - /// belongs to a graph enumerated by this topology. - pub fn port_boundary(&self, port: impl Into) -> Option<&PortBoundary> { - self.port_boundary.get(&port.into()) - } - - /// Every statement enumerated by this topology: block statements first, - /// then graph node statements. - pub fn statements(&self) -> impl Iterator + '_ { - self.blocks - .iter() - .flat_map(|block| block.stmts.iter().copied()) - .chain(self.graph_stmts.iter().copied()) - } -} - -/// Enumerate the topology of `body` in the finalized `stage`. -pub fn body_topology(stage: &StageInfo, body: Body) -> BodyTopology -where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, -{ - let mut topology = BodyTopology::default(); - let mut visited = HashSet::new(); - match body { - Body::CFG(cfg) => { - for block in cfg.blocks(stage) { - collect_block(stage, block, false, &mut topology, &mut visited); - } - } - Body::Block(block) => { - collect_block(stage, block, false, &mut topology, &mut visited); - } - Body::DiGraph(graph) => { - collect_digraph(stage, graph, &mut topology, &mut visited); - } - Body::UnGraph(graph) => { - collect_ungraph(stage, graph, &mut topology, &mut visited); - } - } - topology -} - -fn collect_block( - stage: &StageInfo, - block: Block, - nested: bool, - topology: &mut BodyTopology, - visited: &mut HashSet, -) where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, -{ - if !visited.insert(block) { - return; - } - - let mut stmts: Vec = block.statements(stage).collect(); - if let Some(terminator) = block.terminator(stage) { - stmts.push(terminator); - } - - // Record the *reverse* edge only: a statement with an edge into `target` - // is one of `target`'s feeders. The forward direction is left to the IR. - for &stmt in &stmts { - for successor in stmt.definition(stage).successors() { - topology - .feeders - .entry(successor.target()) - .or_default() - .push(stmt); - } - } - - topology.block_index.insert(block, topology.blocks.len()); - topology.blocks.push(BlockTopology { - block, - stmts: stmts.clone(), - nested, - }); - - for &stmt in &stmts { - collect_owned_bodies(stage, stmt, topology, visited); - } -} - -fn collect_digraph( - stage: &StageInfo, - graph: DiGraph, - topology: &mut BodyTopology, - visited: &mut HashSet, -) where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, -{ - let info = graph.expect_info(stage); - let stmts: Vec = info.graph().node_weights().copied().collect(); - record_ports(info.parent(), info.ports(), topology); - topology.graph_stmts.extend(stmts.iter().copied()); - for stmt in stmts { - collect_owned_bodies(stage, stmt, topology, visited); - } -} - -fn collect_ungraph( - stage: &StageInfo, - graph: UnGraph, - topology: &mut BodyTopology, - visited: &mut HashSet, -) where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, -{ - let info = graph.expect_info(stage); - let stmts: Vec = info.graph().node_weights().copied().collect(); - record_ports(info.parent(), info.ports(), topology); - topology.graph_stmts.extend(stmts.iter().copied()); - for stmt in stmts { - collect_owned_bodies(stage, stmt, topology, visited); - } -} - -/// Descend into every body owned by `stmt`: structured blocks/cfgs (the -/// owning statement feeds each owned block) and owned graphs (their ports' -/// boundary is recorded). -fn collect_owned_bodies( - stage: &StageInfo, - stmt: Statement, - topology: &mut BodyTopology, - visited: &mut HashSet, -) where - L: Dialect, - for<'a> L: HasSuccessors<'a> + HasBlocks<'a> + HasCFG<'a> + HasDigraphs<'a> + HasUngraphs<'a>, -{ - let definition = stmt.definition(stage); - let owned_blocks: Vec = definition.blocks().copied().collect(); - let owned_cfgs: Vec = definition.cfgs().copied().collect(); - let owned_digraphs: Vec = definition.digraphs().copied().collect(); - let owned_ungraphs: Vec = definition.ungraphs().copied().collect(); - for owned in owned_blocks { - topology.feeders.entry(owned).or_default().push(stmt); - collect_block(stage, owned, true, topology, visited); - } - for owned_cfg in owned_cfgs { - for owned in owned_cfg.blocks(stage) { - topology.feeders.entry(owned).or_default().push(stmt); - collect_block(stage, owned, true, topology, visited); - } - } - for owned in owned_digraphs { - collect_digraph(stage, owned, topology, visited); - } - for owned in owned_ungraphs { - collect_ungraph(stage, owned, topology, visited); - } -} - -fn record_ports(owner: Option, ports: &[Port], topology: &mut BodyTopology) { - let Some(owner) = owner else { return }; - for (index, &port) in ports.iter().enumerate() { - topology - .port_boundary - .insert(SSAValue::from(port), PortBoundary { owner, index }); - } -} diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index 97f3caa173..51f9424b91 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -95,7 +95,7 @@ where let statements = match self.statements.as_ref() { Some(statements) => statements, None => { - let statements = interp.block_statements(self.block)?; + let statements = interp.block_statements(self.stage, self.block)?; self.remaining = statements.len(); self.statements.insert(statements) } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 06385f403f..66cdbc6e73 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -55,7 +55,7 @@ use crate::Body; use crate::core::query; use crate::engines::sparse_backward::BodyScope; use crate::{ - AbstractInterpreter, BackwardSummaryDeps, BodyTopology, ClassicLiveness, DenseBackwardSemantic, + AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, DensePointStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, @@ -372,12 +372,12 @@ pub enum DenseBackwardCompletion { Structured, } -/// Analysis-local state carried in the driver's `store` slot: the scope, -/// the body topology, and an optional per-point recorder filled by the -/// block frames during [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). +/// Analysis-local state carried in the driver's `store` slot: the scope, the +/// root block owners, and an optional per-point recorder filled by block frames +/// during [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). pub struct DenseAnalysisState { scope: Option, - topology: BodyTopology, + blocks: Vec, recorder: Option>, } @@ -385,7 +385,7 @@ impl Default for DenseAnalysisState { fn default() -> Self { Self { scope: None, - topology: BodyTopology::default(), + blocks: Vec::new(), recorder: None, } } @@ -419,7 +419,11 @@ pub trait DenseBackwardFrameEngine: Interp Result; /// A block's statements in program order (terminator, if any, last). - fn block_statements(&self, block: Block) -> Result, Self::Error>; + fn block_statements( + &self, + stage: CompileStage, + block: Block, + ) -> Result, Self::Error>; /// The parameters of `block` (structured frames map carried demand). fn block_params(&self, stage: CompileStage, block: Block) @@ -490,12 +494,8 @@ where result } - fn block_statements(&self, block: Block) -> Result, E> { - self.store() - .topology - .block_statements(block) - .map(<[Statement]>::to_vec) - .ok_or_else(|| E::from(InterpreterError::MissingBlock(block))) + fn block_statements(&self, stage: CompileStage, block: Block) -> Result, E> { + query::block_statements(self.inner().pipeline(), stage, block).map_err(E::from) } fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { @@ -691,12 +691,7 @@ where /// The analyzed CFG's own top-level blocks (post-`analyze`). pub fn cfg_blocks(&self) -> Vec { - self.driver - .store() - .topology - .cfg_blocks() - .map(|block| block.block) - .collect() + self.driver.store().blocks.clone() } } @@ -715,14 +710,15 @@ where pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { let body = body.into(); let scope = (stage, body); - let topology = query::body_topology(self.driver.inner().pipeline(), stage, body)?; - let owners: Vec> = topology - .cfg_blocks() - .map(|block| Scoped::new(scope, block.block)) + let blocks = query::direct_body_blocks(self.driver.inner().pipeline(), stage, body)?; + let owners: Vec> = blocks + .iter() + .copied() + .map(|block| Scoped::new(scope, block)) .collect(); *self.driver.store_mut() = DenseAnalysisState { scope: Some(scope), - topology, + blocks, recorder: None, }; diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index e3ba46acce..c866df6074 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -20,7 +20,7 @@ //! the real dispatch location, and the per-rule demand buffer. //! - the **[`StandardFixpointInterpreter`]** driver owns the demand facts //! (summaries keyed by [`Scoped`] SSA values — never bare values), the value -//! worklist, and the analysis state (scope + body topology). +//! worklist, and the analysis scope. //! //! # Owners are values; scheduling is demand propagation //! @@ -31,20 +31,20 @@ //! that can translate its demand: //! //! - a statement **result** → the defining statement's backward rule; -//! - a **block argument** → each of the block's *feeders* (terminators -//! targeting the block, statements owning it as a structured body) from the -//! [`BodyTopology`]; -//! - a graph **port** → unsupported (loud error). +//! - a **block argument** → its directly owning structured statement, or each +//! indexed CFG predecessor block's terminator; +//! - a graph **port** → the statement owning the graph boundary. //! //! Rules read converged facts ([`DemandInterp::is_demanded`]) and raise new //! demands ([`DemandInterp::demand`], strong liveness's spelling of the //! shape-generic [`SparseBackwardInterp::raise_fact`]); each rule returns the //! facts it raised as its [`SparseBackwardEffect`]. All fact mutation flows //! through the driver's single merge path. Facts only rise in a finite-height -//! lattice, so the fixpoint terminates with O(feeders) rule runs per rise — +//! lattice, so the fixpoint terminates with O(predecessors) rule runs per rise — //! no block re-walks, no widening, and no frames for structured control: //! loop-carried demand (e.g. `scf.for`) converges through the value worklist. +use std::collections::{HashSet, VecDeque}; use std::marker::PhantomData; use std::mem; @@ -55,7 +55,7 @@ use kirin_ir::{ use crate::core::query; use crate::{ - AbstractInterpreter, Body, BodyTopology, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, + AbstractInterpreter, Body, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, Scoped, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, StrongDemand, Summary, SummaryEffect, @@ -88,7 +88,7 @@ pub enum SparseBackwardEffect { /// sparse-backward semantics ([`StrongDemand`] today, downstream keys /// tomorrow) shares this surface: read a converged per-value fact, raise a /// fact, drain the raised facts into the rule's effect, and query the block -/// topology facts terminator/structured rules map across. Rules are +/// boundary facts terminator/structured rules map across. Rules are /// scope-blind: they name bare SSA values, and the engine qualifies them with /// the current analysis scope. /// @@ -302,11 +302,10 @@ where } /// Analysis-local state carried in the driver's `store` slot: the scope facts -/// are qualified with, and the body topology (feeders for block arguments). +/// are qualified with. #[derive(Default)] pub struct BackwardAnalysisState { scope: Option, - topology: BodyTopology, } /// The sparse backward driver: a [`StandardFixpointInterpreter`] over @@ -500,21 +499,14 @@ where let kind = query::value_kind(interp.inner().pipeline(), stage, owner.item)?; let work = match kind { SSAKind::Result(statement, _) => vec![statement], - SSAKind::BlockArgument(block, _) => interp.store().topology.feeders(block).to_vec(), - SSAKind::Port(..) => { - // A port is a boundary SSA value: the statement owning the - // graph translates demand across the boundary (its rule maps - // port/index to operands, captures, or results). - let boundary = interp.store().topology.port_boundary(owner.item); - match boundary { - Some(boundary) => vec![boundary.owner], - None => { - return Err(E::from(InterpreterError::Custom( - "graph port outside the analyzed body", - ))); - } - } + SSAKind::BlockArgument(block, _) => { + query::block_argument_predecessors(interp.inner().pipeline(), stage, block)? } + SSAKind::Port(parent, _) => vec![query::graph_port_owner( + interp.inner().pipeline(), + stage, + parent, + )?], }; Ok(DemandFrame::new(stage, work)) } @@ -623,30 +615,34 @@ where { /// Run the demand fixpoint over `body` in `stage`. /// - /// **Prepass**: enumerate the body topology (blocks and graph parts, - /// including structured bodies, statements, feeders, port boundaries), - /// then run every statement's rule once with nothing demanded — impure - /// statements and terminators contribute the demand roots. + /// **Prepass**: walk the body's containment hierarchy, running every + /// statement's rule once with nothing demanded — impure statements and + /// terminators contribute the demand roots. /// **Propagation**: drain the value worklist; each risen value dispatches /// the rules that translate its demand. pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { let body = body.into(); let scope = (stage, body); - let topology = query::body_topology(self.driver.inner().pipeline(), stage, body)?; - let statements: Vec = topology.statements().collect(); - *self.driver.store_mut() = BackwardAnalysisState { - scope: Some(scope), - topology, - }; + *self.driver.store_mut() = BackwardAnalysisState { scope: Some(scope) }; let mut semantics = SparseBackwardSemantics; - // Prepass: collect the demand roots. + // Prepass: visit each contained body part once and collect all demand + // roots before merging any of them, so every rule observes bottom. + let mut bodies = VecDeque::from([body]); + let mut visited = HashSet::new(); let mut seeds: Vec<(SSAValue, V)> = Vec::new(); - for statement in statements { - let SparseBackwardEffect::Demands(demands) = - self.driver.run_statement(stage, statement)?; - seeds.extend(demands); + while let Some(body) = bodies.pop_front() { + if !visited.insert(body) { + continue; + } + let contents = query::body_contents(self.driver.inner().pipeline(), stage, body)?; + bodies.extend(contents.children); + for statement in contents.statements { + let SparseBackwardEffect::Demands(demands) = + self.driver.run_statement(stage, statement)?; + seeds.extend(demands); + } } // Propagate to the fixpoint. diff --git a/crates/kirin-interpreter/src/facts/anchor.rs b/crates/kirin-interpreter/src/facts/anchor.rs index 394b935345..706b271591 100644 --- a/crates/kirin-interpreter/src/facts/anchor.rs +++ b/crates/kirin-interpreter/src/facts/anchor.rs @@ -1,6 +1,5 @@ //! Locations in the IR that dataflow reasoning refers to: lattice anchors -//! (*where* facts attach), boundary locations, scope qualification, and change -//! detection. +//! (*where* facts attach), scope qualification, and change detection. //! //! Following MLIR's terminology, a lattice fact is attached to a *lattice //! anchor*: sparse analyses anchor facts to [`SSAValue`]s; dense analyses @@ -65,26 +64,6 @@ pub enum DenseAnchor { impl LatticeAnchor for DenseAnchor {} -// =========================================================================== -// Boundary locations -// =========================================================================== - -/// Where a graph port sits on its owning statement's boundary. -/// -/// This is a **location**, not a value mapping: the owning statement's -/// dialect rule translates port/index into its operands, captures, or -/// results — for values (forward) and demand (backward) alike. Unlike a -/// [`LatticeAnchor`] no fact attaches here; it is what -/// [`BodyTopology::port_boundary`](crate::BodyTopology::port_boundary) hands -/// a rule so it can reach the statement owning a port. -#[derive(Clone, Copy, Debug)] -pub struct PortBoundary { - /// The statement that owns the graph. - pub owner: Statement, - /// Which boundary slot this port occupies. - pub index: usize, -} - // =========================================================================== // Scope qualification // =========================================================================== diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index 5653244389..d5d7c5afbd 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -1,11 +1,9 @@ -//! Dataflow fact vocabulary: anchors and locations (*where* facts attach) -//! plus the polymorphic fact stores. Fixpoint clients use these, but they are -//! dataflow vocabulary, not the convergence driver itself — and not IR -//! queries either: the body shape an analysis enumerates before it runs is -//! [`core::topology`](crate::core::topology). +//! Dataflow fact vocabulary: anchors (*where* facts attach) plus the +//! polymorphic fact stores. Fixpoint clients use these, but they are dataflow +//! vocabulary, not the convergence driver itself — and not IR queries either. pub(crate) mod anchor; pub(crate) mod store; -pub use anchor::{Change, DenseAnchor, LatticeAnchor, PortBoundary, ProgramPoint, Scoped}; +pub use anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; pub use store::{DenseBlockStore, DensePointStore, FactStore, ScopedSparseStore, SparseStore}; diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index d21f512c0f..29794f2dac 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -75,9 +75,6 @@ pub use self::core::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use self::core::{EnvIndex, EnvStackStore, Store}; pub use self::core::{FunctionEntry, InterpDispatch, Interpretable}; pub use self::core::{InterpreterError, StageQuery}; -// The body-shape IR query: what blocks/graphs a body contains, which -// statements feed a block's parameters, and where a graph port sits. -pub use self::core::{BlockTopology, BodyTopology, body_topology}; // 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 @@ -124,8 +121,8 @@ pub use engines::dense_backward::{ // polymorphic fact stores. Anchor family is a property of the solver shape; // dispatch meaning lives in `semantics`. pub use facts::{ - Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, LatticeAnchor, PortBoundary, - ProgramPoint, Scoped, ScopedSparseStore, SparseStore, + Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, LatticeAnchor, ProgramPoint, + Scoped, ScopedSparseStore, SparseStore, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` diff --git a/crates/kirin-ir/src/builder/block.rs b/crates/kirin-ir/src/builder/block.rs index 84429c3345..63449c1b9e 100644 --- a/crates/kirin-ir/src/builder/block.rs +++ b/crates/kirin-ir/src/builder/block.rs @@ -182,10 +182,11 @@ impl<'a, L: Dialect> BlockBuilder<'a, L> { } let block = BlockInfo::builder() - .maybe_parent(self.parent) + .maybe_parent(self.parent.map(BlockParent::CFG)) .maybe_name(self.name.map(|n| self.stage.symbols.intern(n))) .node(LinkedListNode::new(id)) .arguments(block_args) + .predecessors(Vec::new()) .statements(self.stage.link_statements(&self.statements)) .maybe_terminator(self.terminator) .new(); diff --git a/crates/kirin-ir/src/builder/cfg.rs b/crates/kirin-ir/src/builder/cfg.rs index efecf644a4..80644a9f5f 100644 --- a/crates/kirin-ir/src/builder/cfg.rs +++ b/crates/kirin-ir/src/builder/cfg.rs @@ -1,4 +1,4 @@ -use crate::{Block, BuilderStageInfo, CFG, Dialect, Statement, node::CFGInfo}; +use crate::{Block, BlockParent, BuilderStageInfo, CFG, Dialect, Statement, node::CFGInfo}; pub struct CFGBuilder<'a, L: Dialect> { pub(super) stage: &'a mut BuilderStageInfo, @@ -31,6 +31,16 @@ impl<'a, L: Dialect> CFGBuilder<'a, L> { #[allow(clippy::wrong_self_convention, clippy::new_ret_no_self)] pub fn new(self) -> CFG { let id = self.stage.cfgs.next_id(); + for &block in &self.blocks { + let parent = self.stage.blocks[block].parent; + assert!( + parent.is_none() || parent == Some(BlockParent::CFG(id)), + "Block `{block}` already has a different parent" + ); + } + for &block in &self.blocks { + self.stage.blocks[block].parent = Some(BlockParent::CFG(id)); + } let info = CFGInfo::builder() .id(id) .blocks(self.stage.link_blocks(&self.blocks)) diff --git a/crates/kirin-ir/src/builder/stage_info.rs b/crates/kirin-ir/src/builder/stage_info.rs index 3393af5da4..5614a1af3d 100644 --- a/crates/kirin-ir/src/builder/stage_info.rs +++ b/crates/kirin-ir/src/builder/stage_info.rs @@ -241,6 +241,7 @@ impl BuilderStageInfo { ssas, }; stage.rebuild_use_index(); + stage.rebuild_predecessor_index(); Ok(stage) } @@ -286,6 +287,7 @@ impl BuilderStageInfo { ssas, }; stage.rebuild_use_index(); + stage.rebuild_predecessor_index(); stage } } diff --git a/crates/kirin-ir/src/builder/staged.rs b/crates/kirin-ir/src/builder/staged.rs index 88f839c261..775d0b1ae7 100644 --- a/crates/kirin-ir/src/builder/staged.rs +++ b/crates/kirin-ir/src/builder/staged.rs @@ -104,6 +104,40 @@ impl BuilderStageInfo { #[builder(finish_fn = new)] pub fn statement(&mut self, #[builder(into)] definition: L) -> Statement { let id = self.statements.next_id(); + let owned_blocks: Vec = definition.blocks().copied().collect(); + let owned_cfgs: Vec = definition.cfgs().copied().collect(); + let owned_digraphs: Vec = definition.digraphs().copied().collect(); + let owned_ungraphs: Vec = definition.ungraphs().copied().collect(); + + for &block in &owned_blocks { + let parent = self.blocks[block].parent; + assert!( + parent.is_none() || parent == Some(BlockParent::Statement(id)), + "Block `{block}` already has a different parent" + ); + } + for &cfg in &owned_cfgs { + let parent = self.cfgs[cfg].parent; + assert!( + parent.is_none() || parent == Some(id), + "CFG `{cfg:?}` already has a different parent" + ); + } + for &graph in &owned_digraphs { + let parent = self.digraphs[graph].parent; + assert!( + parent.is_none() || parent == Some(id), + "DiGraph `{graph:?}` already has a different parent" + ); + } + for &graph in &owned_ungraphs { + let parent = self.ungraphs[graph].parent; + assert!( + parent.is_none() || parent == Some(id), + "UnGraph `{graph:?}` already has a different parent" + ); + } + let statement = StatementInfo { node: LinkedListNode::new(id), parent: None, @@ -111,6 +145,19 @@ impl BuilderStageInfo { }; let _ = self.statements.alloc(statement); + for block in owned_blocks { + self.blocks[block].parent = Some(BlockParent::Statement(id)); + } + for cfg in owned_cfgs { + self.cfgs[cfg].parent = Some(id); + } + for graph in owned_digraphs { + self.digraphs[graph].parent = Some(id); + } + for graph in owned_ungraphs { + self.ungraphs[graph].parent = Some(id); + } + // Resolve Unresolved(Result(idx)) SSAs now that the statement ID is known let result_ssas: Vec = self.statements[id] .definition diff --git a/crates/kirin-ir/src/detach.rs b/crates/kirin-ir/src/detach.rs index 13ad97a198..0ec2d503a1 100644 --- a/crates/kirin-ir/src/detach.rs +++ b/crates/kirin-ir/src/detach.rs @@ -1,6 +1,6 @@ use crate::arena::GetInfo; use crate::node::stmt::StatementParent; -use crate::node::{Block, Statement}; +use crate::node::{Block, BlockParent, Statement}; use crate::query::{LinkedListElem, LinkedListInfo, ParentInfo}; use crate::{Dialect, StageInfo}; @@ -64,55 +64,51 @@ impl Detach for Statement { } } -macro_rules! impl_detach { - ($ty:ty) => { - impl Detach for $ty { - fn detach(&self, stage: &mut StageInfo) { - let (prev, next, parent) = if let Some(info) = self.get_info_mut(stage) { - let prev = info.get_prev_mut().take(); - let next = info.get_next_mut().take(); - let parent = info.get_parent_mut().take(); - (prev, next, parent) - } else { - (None, None, None) - }; - - if let Some(prev) = prev { - let prev_info = prev.expect_info_mut(stage); - prev_info.node.next = next; - } - if let Some(next) = next { - let next_info = next.expect_info_mut(stage); - *next_info.get_prev_mut() = prev; - } - - if let Some(parent) = parent { - let parent_info = parent.expect_info_mut(stage); - // if prev is None, set head of parent block to next - if prev.is_none() { - debug_assert!( - *parent_info.get_head() == Some(*self), - "Parent block's head does not match the statement being detached" - ); - *parent_info.get_head_mut() = next; - } +impl Detach for Block { + fn detach(&self, stage: &mut StageInfo) { + let (prev, next, parent) = if let Some(info) = self.get_info_mut(stage) { + assert!( + !matches!(info.parent, Some(BlockParent::Statement(_))), + "Cannot detach a block directly owned by a statement" + ); + let prev = info.get_prev_mut().take(); + let next = info.get_next_mut().take(); + let parent = info.get_parent_mut().take(); + (prev, next, parent) + } else { + (None, None, None) + }; - // if next is None, set tail of parent block to prev - if next.is_none() { - debug_assert!( - *parent_info.get_tail() == Some(*self), - "Parent block's tail does not match the statement being detached" - ); - *parent_info.get_tail_mut() = prev; - } + if let Some(prev) = prev { + let prev_info = prev.expect_info_mut(stage); + prev_info.node.next = next; + } + if let Some(next) = next { + let next_info = next.expect_info_mut(stage); + *next_info.get_prev_mut() = prev; + } - *parent_info.get_len_mut() = parent_info.get_len().checked_sub(1).expect( - "linked list length underflow: detaching from a parent with zero length", - ); - } - } + let Some(BlockParent::CFG(parent)) = parent else { + return; + }; + let parent_info = parent.expect_info_mut(stage); + if prev.is_none() { + debug_assert!( + *parent_info.get_head() == Some(*self), + "Parent CFG's head does not match the block being detached" + ); + *parent_info.get_head_mut() = next; } - }; + if next.is_none() { + debug_assert!( + *parent_info.get_tail() == Some(*self), + "Parent CFG's tail does not match the block being detached" + ); + *parent_info.get_tail_mut() = prev; + } + *parent_info.get_len_mut() = parent_info + .get_len() + .checked_sub(1) + .expect("linked list length underflow: detaching from a parent with zero length"); + } } - -impl_detach!(Block); diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index ac761e83da..7c08936857 100644 --- a/crates/kirin-ir/src/lib.rs +++ b/crates/kirin-ir/src/lib.rs @@ -31,12 +31,13 @@ pub use language::{ }; pub use lattice::{FiniteLattice, HasBottom, HasTop, Lattice, TypeLattice, Widen}; pub use node::{ - Block, BlockArgument, BlockInfo, BuilderKey, BuilderSSAInfo, BuilderSSAKind, CFG, CompileStage, - DeletedSSAValue, DiGraph, DiGraphExtra, DiGraphInfo, Function, FunctionInfo, GlobalSymbol, - GraphInfo, LinkedList, LinkedListNode, Port, PortParent, ResolutionInfo, ResultValue, SSAInfo, - SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, StagedFunction, - StagedFunctionInfo, StagedNamePolicy, Statement, StatementInfo, StatementParent, Successor, - Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, UniqueLiveSpecializationError, Use, + Block, BlockArgument, BlockInfo, BlockParent, BuilderKey, BuilderSSAInfo, BuilderSSAKind, CFG, + CompileStage, DeletedSSAValue, DiGraph, DiGraphExtra, DiGraphInfo, Function, FunctionInfo, + GlobalSymbol, GraphInfo, LinkedList, LinkedListNode, Port, PortParent, ResolutionInfo, + ResultValue, SSAInfo, SSAKind, SSAValue, SpecializedFunction, SpecializedFunctionInfo, + StagedFunction, StagedFunctionInfo, StagedNamePolicy, Statement, StatementInfo, + StatementParent, Successor, Symbol, TestSSAValue, UnGraph, UnGraphExtra, UnGraphInfo, + UniqueLiveSpecializationError, Use, }; pub use pipeline::Pipeline; pub use product::{HasProduct, Product}; diff --git a/crates/kirin-ir/src/node/block.rs b/crates/kirin-ir/src/node/block.rs index eb90ef25e6..e6796738ce 100644 --- a/crates/kirin-ir/src/node/block.rs +++ b/crates/kirin-ir/src/node/block.rs @@ -2,10 +2,10 @@ use crate::{ Dialect, Symbol, arena::{GetInfo, Id, Item}, identifier, - node::cfg::CFG, }; use super::{ + cfg::CFG, linked_list::{LinkedList, LinkedListNode}, ssa::BlockArgument, stmt::Statement, @@ -48,13 +48,26 @@ impl std::fmt::Display for Successor { } } +/// The immediate structural owner of a block. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum BlockParent { + /// The block belongs to a block-list control-flow body. + CFG(CFG), + /// The block is a single-block body owned directly by a statement. + Statement(Statement), +} + #[derive(Clone, Debug, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlockInfo { - pub parent: Option, + pub parent: Option, pub name: Option, pub node: LinkedListNode, pub arguments: Vec, + /// Reverse control-flow index: blocks whose terminators may transfer + /// control to this block. + pub predecessors: Vec, pub statements: LinkedList, pub terminator: Option, _marker: std::marker::PhantomData, @@ -64,14 +77,16 @@ pub struct BlockInfo { impl BlockInfo { #[builder(finish_fn = new)] pub(crate) fn new( - /// The parent cfg of this block. - parent: Option, + /// The immediate CFG or statement parent of this block. + parent: Option, /// The name of this block. name: Option, /// The linked list node for this block. node: LinkedListNode, /// The arguments of this block. arguments: Vec, + /// The predecessor blocks in the reverse control-flow index. + predecessors: Vec, /// The statements contained in this block. statements: Option>, /// The terminator statement of this block, if any. @@ -82,6 +97,7 @@ impl BlockInfo { name, node, arguments, + predecessors, statements: statements.unwrap_or_default(), terminator, _marker: std::marker::PhantomData, diff --git a/crates/kirin-ir/src/node/mod.rs b/crates/kirin-ir/src/node/mod.rs index bc485be5c7..98ed75c2f9 100644 --- a/crates/kirin-ir/src/node/mod.rs +++ b/crates/kirin-ir/src/node/mod.rs @@ -10,7 +10,7 @@ pub mod stmt; pub mod symbol; pub(crate) mod ungraph; -pub use block::{Block, BlockInfo, Successor}; +pub use block::{Block, BlockInfo, BlockParent, Successor}; pub use cfg::{CFG, CFGInfo}; pub use digraph::{DiGraph, DiGraphInfo}; pub use function::{ diff --git a/crates/kirin-ir/src/query/info.rs b/crates/kirin-ir/src/query/info.rs index 7a6845c425..c3fc7aab5a 100644 --- a/crates/kirin-ir/src/query/info.rs +++ b/crates/kirin-ir/src/query/info.rs @@ -1,7 +1,7 @@ use crate::{ Dialect, LinkedList, node::{ - Block, BlockInfo, CFG, CFGInfo, LinkedListNode, Statement, StatementInfo, + Block, BlockInfo, BlockParent, CFGInfo, LinkedListNode, Statement, StatementInfo, stmt::StatementParent, }, }; @@ -26,7 +26,7 @@ impl ParentInfo for StatementInfo { } impl ParentInfo for BlockInfo { - type ParentPtr = CFG; + type ParentPtr = BlockParent; fn get_parent(&self) -> &Option { &self.parent } diff --git a/crates/kirin-ir/src/stage/info.rs b/crates/kirin-ir/src/stage/info.rs index abd41b33e0..3158aff588 100644 --- a/crates/kirin-ir/src/stage/info.rs +++ b/crates/kirin-ir/src/stage/info.rs @@ -1,4 +1,7 @@ -use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashSet, + ops::{Deref, DerefMut}, +}; use crate::arena::{Arena, Id}; use crate::node::ssa::{SSAInfo, Use}; @@ -179,6 +182,53 @@ impl StageInfo { } } + /// Rebuild the reverse control-flow index stored in + /// [`BlockInfo::predecessors`](crate::BlockInfo::predecessors). + /// + /// Successor references on statements are the authoritative forward + /// edges. This method clears every live block's cached predecessors, then + /// scans each live statement whose structural parent is a block. For every + /// successor target, the source block is recorded as a predecessor of that + /// target. + /// + /// Multiple successor edges from one source block to the same target still + /// represent one predecessor block, so duplicate `(source, target)` pairs + /// are recorded only once. Blocks directly owned by statements do not need + /// synthetic predecessor entries: backward traversal reaches their owner + /// through [`BlockParent::Statement`](crate::BlockParent::Statement). + /// + /// Idempotent — safe to re-run after successor edges change. Called during + /// finalization so finalized IR ships with a populated reverse index. + pub fn rebuild_predecessor_index(&mut self) { + let StageInfo { nodes, .. } = self; + let (blocks, statements) = (&mut nodes.blocks, &nodes.statements); + + for block in blocks.iter_mut() { + block.predecessors.clear(); + } + + let mut seen = HashSet::new(); + for statement in statements.iter() { + let Some(StatementParent::Block(source)) = statement.parent else { + continue; + }; + + for successor in statement.definition.successors() { + let target = successor.target(); + if !seen.insert((source, target)) { + continue; + } + + let Some(target_info) = blocks.get_mut(target) else { + continue; + }; + if !target_info.deleted() { + target_info.predecessors.push(source); + } + } + } + } + /// Temporarily convert to a [`BuilderStageInfo`] for construction, then /// convert back. /// diff --git a/crates/kirin-ir/tests/builder_block.rs b/crates/kirin-ir/tests/builder_block.rs index 31729749cc..4b27af03a4 100644 --- a/crates/kirin-ir/tests/builder_block.rs +++ b/crates/kirin-ir/tests/builder_block.rs @@ -249,6 +249,7 @@ fn empty_block_iteration() { assert_eq!(block.first_statement(&stage), None); assert_eq!(block.last_statement(&stage), None); assert_eq!(block.terminator(&stage), None); + assert!(block.expect_info(&stage).predecessors.is_empty()); } #[test] @@ -295,15 +296,120 @@ fn cfg_builder_creates_cfg_with_ordered_blocks() { assert_eq!(blocks, vec![b0, b1, b2]); let b0_info = b0.expect_info(&stage); + assert_eq!(b0_info.parent, Some(BlockParent::CFG(cfg))); assert_eq!(b0_info.node.next, Some(b1)); let b1_info = b1.expect_info(&stage); + assert_eq!(b1_info.parent, Some(BlockParent::CFG(cfg))); assert_eq!(b1_info.node.prev, Some(b0)); assert_eq!(b1_info.node.next, Some(b2)); let b2_info = b2.expect_info(&stage); + assert_eq!(b2_info.parent, Some(BlockParent::CFG(cfg))); assert_eq!(b2_info.node.prev, Some(b1)); assert_eq!(b2_info.node.next, None); } +#[test] +fn finalize_populates_block_predecessor_index() { + let mut stage = new_stage(); + let target = stage.block().new(); + + let branch0 = stage + .statement() + .definition(BuilderDialect::Branch(Successor::from_block(target))) + .new(); + let branch1 = stage + .statement() + .definition(BuilderDialect::Branch(Successor::from_block(target))) + .new(); + let source0 = stage.block().terminator(branch0).new(); + let source1 = stage.block().terminator(branch1).new(); + let _cfg = stage + .cfg() + .add_block(source0) + .add_block(source1) + .add_block(target) + .new(); + + let stage = stage.finalize().unwrap(); + assert_eq!( + target.expect_info(&stage).predecessors, + vec![source0, source1] + ); + assert!(source0.expect_info(&stage).predecessors.is_empty()); + assert!(source1.expect_info(&stage).predecessors.is_empty()); +} + +#[test] +fn predecessor_index_deduplicates_edges_from_the_same_block() { + let mut stage = new_stage(); + let target = stage.block().new(); + let successor = Successor::from_block(target); + let branch = stage + .statement() + .definition(BuilderDialect::CondBranch(successor, successor)) + .new(); + let source = stage.block().terminator(branch).new(); + let _cfg = stage.cfg().add_block(source).add_block(target).new(); + + let stage = stage.finalize().unwrap(); + assert_eq!(target.expect_info(&stage).predecessors, vec![source]); +} + +#[test] +fn statement_builder_assigns_parent_to_directly_owned_blocks() { + let mut stage = new_stage(); + let then_block = stage.block().new(); + let else_block = stage.block().new(); + + let owner = stage + .statement() + .definition(BuilderDialect::OwnBlocks(then_block, else_block)) + .new(); + + let stage = stage.finalize().unwrap(); + assert_eq!( + then_block.expect_info(&stage).parent, + Some(BlockParent::Statement(owner)) + ); + assert_eq!( + else_block.expect_info(&stage).parent, + Some(BlockParent::Statement(owner)) + ); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_records_cfg_parent() { + let mut stage = new_stage(); + let cfg = stage.cfg().new(); + + let _owner = stage + .statement() + .definition(BuilderDialect::OwnCFG(cfg)) + .new(); + + // A second owner is rejected only if the first statement recorded itself + // in the crate-private `CFGInfo.parent` field. + let _other_owner = stage + .statement() + .definition(BuilderDialect::OwnCFG(cfg)) + .new(); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_rejects_block_owned_by_cfg() { + let mut stage = new_stage(); + let cfg_block = stage.block().new(); + let other_block = stage.block().new(); + let _cfg = stage.cfg().add_block(cfg_block).new(); + + let _owner = stage + .statement() + .definition(BuilderDialect::OwnBlocks(cfg_block, other_block)) + .new(); +} + #[test] fn has_cfg_body_entry_block_returns_first_block() { let mut stage = new_stage(); diff --git a/crates/kirin-ir/tests/builder_graph.rs b/crates/kirin-ir/tests/builder_graph.rs index ef71dd8be5..9b5be82a41 100644 --- a/crates/kirin-ir/tests/builder_graph.rs +++ b/crates/kirin-ir/tests/builder_graph.rs @@ -105,6 +105,50 @@ fn digraph_builder_port_and_capture_creation() { assert!(ssa_cap.name().is_some()); } +#[test] +fn statement_builder_assigns_parent_to_directly_owned_graphs() { + let mut stage = new_stage(); + let digraph = stage.digraph().new(); + let ungraph = stage.ungraph().new(); + + let owner = stage + .statement() + .definition(BuilderDialect::OwnGraphs(digraph, ungraph)) + .new(); + + let stage = stage.finalize().unwrap(); + assert_eq!(digraph.expect_info(&stage).parent(), Some(owner)); + assert_eq!(ungraph.expect_info(&stage).parent(), Some(owner)); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_rejects_digraph_with_existing_owner() { + let mut stage = new_stage(); + let existing_owner = stage.statement().definition(BuilderDialect::Nop).new(); + let digraph = stage.digraph().parent(existing_owner).new(); + let ungraph = stage.ungraph().new(); + + let _other_owner = stage + .statement() + .definition(BuilderDialect::OwnGraphs(digraph, ungraph)) + .new(); +} + +#[test] +#[should_panic(expected = "already has a different parent")] +fn statement_builder_rejects_ungraph_with_existing_owner() { + let mut stage = new_stage(); + let existing_owner = stage.statement().definition(BuilderDialect::Nop).new(); + let digraph = stage.digraph().new(); + let ungraph = stage.ungraph().parent(existing_owner).new(); + + let _other_owner = stage + .statement() + .definition(BuilderDialect::OwnGraphs(digraph, ungraph)) + .new(); +} + #[test] fn digraph_builder_resolves_builder_port_placeholders() { let mut stage = new_stage(); diff --git a/crates/kirin-ir/tests/common.rs b/crates/kirin-ir/tests/common.rs index 8e90c759b2..5cecd270b9 100644 --- a/crates/kirin-ir/tests/common.rs +++ b/crates/kirin-ir/tests/common.rs @@ -42,6 +42,11 @@ impl Placeholder for TestType { /// - `Gate(a, b)`: two SSAValue operands (ungraph node) /// - `Wire(r)`: edge that produces a ResultValue (ungraph edge) /// - `Isolated`: no operands, no results (ungraph isolated node) +/// - `Branch(target)`: one-successor control-flow terminator +/// - `CondBranch(a, b)`: two-successor control-flow terminator +/// - `OwnBlocks(a, b)`: structurally owns two blocks +/// - `OwnCFG(cfg)`: structurally owns one CFG +/// - `OwnGraphs(dg, ug)`: structurally owns one directed and one undirected graph #[allow(dead_code)] #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum BuilderDialect { @@ -52,6 +57,11 @@ pub enum BuilderDialect { Gate(SSAValue, SSAValue), Wire(ResultValue), Isolated, + Branch(Successor), + CondBranch(Successor, Successor), + OwnBlocks(Block, Block), + OwnCFG(CFG), + OwnGraphs(DiGraph, UnGraph), } impl<'a> HasArguments<'a> for BuilderDialect { @@ -97,50 +107,73 @@ impl<'a> HasResultsMut<'a> for BuilderDialect { } impl<'a> HasBlocks<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a Block>; + type Iter = std::vec::IntoIter<&'a Block>; fn blocks(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnBlocks(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasBlocksMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut Block>; + type IterMut = std::vec::IntoIter<&'a mut Block>; fn blocks_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnBlocks(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasSuccessors<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a Successor>; + type Iter = std::vec::IntoIter<&'a Successor>; fn successors(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::Branch(target) => vec![target].into_iter(), + BuilderDialect::CondBranch(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasSuccessorsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut Successor>; + type IterMut = std::vec::IntoIter<&'a mut Successor>; fn successors_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::Branch(target) => vec![target].into_iter(), + BuilderDialect::CondBranch(a, b) => vec![a, b].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasCFG<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a CFG>; + type Iter = std::vec::IntoIter<&'a CFG>; fn cfgs(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnCFG(cfg) => vec![cfg].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasCFGMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut CFG>; + type IterMut = std::vec::IntoIter<&'a mut CFG>; fn cfgs_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnCFG(cfg) => vec![cfg].into_iter(), + _ => vec![].into_iter(), + } } } impl IsTerminator for BuilderDialect { fn is_terminator(&self) -> bool { - matches!(self, BuilderDialect::Return) + matches!( + self, + BuilderDialect::Return | BuilderDialect::Branch(_) | BuilderDialect::CondBranch(_, _) + ) } } @@ -163,30 +196,42 @@ impl IsSpeculatable for BuilderDialect { } impl<'a> HasDigraphs<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a DiGraph>; + type Iter = std::vec::IntoIter<&'a DiGraph>; fn digraphs(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(graph, _) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasDigraphsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut DiGraph>; + type IterMut = std::vec::IntoIter<&'a mut DiGraph>; fn digraphs_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(graph, _) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasUngraphs<'a> for BuilderDialect { - type Iter = std::iter::Empty<&'a UnGraph>; + type Iter = std::vec::IntoIter<&'a UnGraph>; fn ungraphs(&'a self) -> Self::Iter { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(_, graph) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } impl<'a> HasUngraphsMut<'a> for BuilderDialect { - type IterMut = std::iter::Empty<&'a mut UnGraph>; + type IterMut = std::vec::IntoIter<&'a mut UnGraph>; fn ungraphs_mut(&'a mut self) -> Self::IterMut { - std::iter::empty() + match self { + BuilderDialect::OwnGraphs(_, graph) => vec![graph].into_iter(), + _ => vec![].into_iter(), + } } } diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index dd216db1ec..77a2e798ff 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -111,8 +111,8 @@ where // // Demand converges value-by-value on the sparse backward engine's worklist, // so structured bodies need no walk and loops need no frame fixpoint: this -// rule re-runs whenever a result or a body block parameter it feeds rises -// (the owning statement is the body's *feeder* in the cfg topology). +// rule re-runs whenever a result or a body block parameter it owns rises (the +// owning statement is recorded as the body's structural parent). /// Backward demand for `scf.if`: the condition is an unconditional control /// root (consistent with `cf.cond_br`); a body's yield slot is demanded iff From 3979057a69d104fde89be63671ee6576391dc12b Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Mon, 10 Aug 2026 20:46:06 -0400 Subject: [PATCH 16/21] Refactor liveness to store both block and statement liveness facts. --- .../src/engines/dense_backward/frames.rs | 25 +++- .../src/engines/dense_backward/interp.rs | 133 ++++++++---------- crates/kirin-interpreter/src/facts/anchor.rs | 34 ++--- crates/kirin-interpreter/src/facts/mod.rs | 4 +- crates/kirin-interpreter/src/facts/store.rs | 99 ++----------- crates/kirin-interpreter/src/lib.rs | 4 +- crates/kirin-liveness/src/lib.rs | 2 +- crates/kirin-liveness/src/result.rs | 64 ++++----- crates/kirin-liveness/tests/cfg.rs | 5 + example/toy-lang/src/interpreter/tests.rs | 124 +++++++++++----- 10 files changed, 228 insertions(+), 266 deletions(-) diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index 51f9424b91..c49edf5331 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -16,7 +16,7 @@ use kirin_ir::{Block, CompileStage, Statement}; use crate::{ DenseBackwardCompletion, DenseBackwardEffect, DenseBackwardFrameEngine, Frame, FrameEffect, - InterpreterError, + InterpreterError, ProgramPoint, }; /// How a [`DenseBlockFrame`] treats its block. @@ -95,6 +95,10 @@ where let statements = match self.statements.as_ref() { Some(statements) => statements, None => { + if self.mode == DenseBlockMode::StructuredBody { + let facts = interp.state(); + interp.record_point(ProgramPoint::BlockExit(self.block), facts); + } let statements = interp.block_statements(self.stage, self.block)?; self.remaining = statements.len(); self.statements.insert(statements) @@ -103,10 +107,12 @@ where let total = statements.len(); if self.remaining == 0 { + let live_in = interp.state(); + interp.record_point(ProgramPoint::BlockEntry(self.block), live_in.clone()); return Ok(FrameEffect::Complete(match self.mode { DenseBlockMode::CFGOwner => DenseBackwardCompletion::Block { - live_in: interp.state(), - live_out: self.live_out.take().unwrap_or_else(|| interp.state()), + live_in: live_in.clone(), + live_out: self.live_out.take().unwrap_or(live_in), }, DenseBlockMode::StructuredBody => DenseBackwardCompletion::Structured, })); @@ -117,10 +123,12 @@ where let statement = self.statements.as_ref().expect("materialized")[index]; self.remaining = index; - interp.record_after(statement); + let after = interp.state(); + interp.record_point(ProgramPoint::After(statement), after); match interp.run_statement(self.stage, statement)? { DenseBackwardEffect::Next => { - interp.record_before(statement); + let before = interp.state(); + interp.record_point(ProgramPoint::Before(statement), before); Ok(FrameEffect::Continue(F::from_block(self))) } DenseBackwardEffect::Edges(edges) => { @@ -132,8 +140,10 @@ where match self.mode { DenseBlockMode::CFGOwner => { let out = interp.absorb_edges(self.stage, &edges)?; + interp.record_point(ProgramPoint::BlockExit(self.block), out.clone()); self.live_out = Some(out); - interp.record_before(statement); + let before = interp.state(); + interp.record_point(ProgramPoint::Before(statement), before); Ok(FrameEffect::Continue(F::from_block(self))) } DenseBlockMode::StructuredBody => Err(E::from(InterpreterError::Custom( @@ -170,7 +180,8 @@ where match completion { DenseBackwardCompletion::Structured => { if let Some(statement) = self.pending_point.take() { - interp.record_before(statement); + let before = interp.state(); + interp.record_point(ProgramPoint::Before(statement), before); } Ok(FrameEffect::Continue(F::from_block(self))) } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 66cdbc6e73..cc32728575 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -39,9 +39,9 @@ //! argument, pass-through for non-parameters) — which both seeds the walk //! state and records the block's `live_out`. Structured dialects push //! dialect-owned frames ([`DenseBackwardEffect::Push`]) that walk their bodies -//! against the same point state. Per-statement states are not persisted: -//! reconstruct them on demand with -//! [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). +//! against the same point state. Every block walk records its block and +//! statement program-point facts; later fixpoint iterations overwrite earlier +//! approximations, leaving the final stable facts in the analysis store. use std::marker::PhantomData; @@ -56,7 +56,7 @@ use crate::core::query; use crate::engines::sparse_backward::BodyScope; use crate::{ AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, - DensePointStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, + DenseFactStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, }; @@ -372,21 +372,18 @@ pub enum DenseBackwardCompletion { Structured, } -/// Analysis-local state carried in the driver's `store` slot: the scope, the -/// root block owners, and an optional per-point recorder filled by block frames -/// during [`reconstruct_points`](DenseBackwardInterpreter::reconstruct_points). +/// Analysis-local state carried in the driver's `store` slot: the active scope +/// and the latest fact recorded at every visited program point. pub struct DenseAnalysisState { scope: Option, - blocks: Vec, - recorder: Option>, + points: DenseFactStore, } impl Default for DenseAnalysisState { fn default() -> Self { Self { scope: None, - blocks: Vec::new(), - recorder: None, + points: DenseFactStore::new(), } } } @@ -443,13 +440,9 @@ pub trait DenseBackwardFrameEngine: Interp Self::Value; - /// Record the current state as the point *before* `statement` (no-op - /// unless a per-point reconstruction is running). - fn record_before(&mut self, statement: Statement); - - /// Record the current state as the point *after* `statement` (no-op - /// unless a per-point reconstruction is running). - fn record_after(&mut self, statement: Statement); + /// Store `facts` at a block or statement program point, overwriting the + /// approximation recorded by any earlier fixpoint iteration. + fn record_point(&mut self, point: ProgramPoint, facts: Self::Value); /// Absorb a CFG terminator's edges atomically: for each successor, map /// its converged live-in across the edge (parameter → matching edge @@ -514,18 +507,8 @@ where std::mem::replace(&mut self.inner_mut().state, state) } - fn record_before(&mut self, statement: Statement) { - let state = self.inner().state.clone(); - if let Some(recorder) = self.store_mut().recorder.as_mut() { - recorder.set(ProgramPoint::Before(statement), state); - } - } - - fn record_after(&mut self, statement: Statement) { - let state = self.inner().state.clone(); - if let Some(recorder) = self.store_mut().recorder.as_mut() { - recorder.set(ProgramPoint::After(statement), state); - } + fn record_point(&mut self, point: ProgramPoint, facts: V) { + self.store_mut().points.set(point, facts); } fn absorb_edges(&mut self, stage: CompileStage, edges: &[SuccessorEdge]) -> Result { @@ -689,9 +672,14 @@ where .summary(&Scoped::new((stage, body.into()), block)) } - /// The analyzed CFG's own top-level blocks (post-`analyze`). - pub fn cfg_blocks(&self) -> Vec { - self.driver.store().blocks.clone() + /// The latest fact recorded at `point` for the active dense analysis. + pub fn point_facts(&self, point: ProgramPoint) -> Option<&V> { + self.driver.store().points.get(point) + } + + /// All block- and statement-boundary facts for the active analysis. + pub fn fact_store(&self) -> &DenseFactStore { + &self.driver.store().points } } @@ -704,13 +692,25 @@ where F: Frame, F, Completion = DenseBackwardCompletion> + DenseFrameBuild, { + /// The blocks directly selected as fixpoint owners for `body`. + /// + /// This reads the current IR rather than returning cached analysis state. + pub fn direct_body_blocks( + &self, + stage: CompileStage, + body: impl Into, + ) -> Result, E> { + query::direct_body_blocks(self.driver.inner().pipeline(), stage, body.into()) + .map_err(E::from) + } + /// Run the block-boundary fixpoint over `cfg` in `stage`: seed every /// CFG block (a backward analysis must visit them all) and drain the /// worklist; dependencies are discovered from the terminators' edges. pub fn analyze(&mut self, stage: CompileStage, body: impl Into) -> Result<(), E> { let body = body.into(); let scope = (stage, body); - let blocks = query::direct_body_blocks(self.driver.inner().pipeline(), stage, body)?; + let blocks = self.direct_body_blocks(stage, body)?; let owners: Vec> = blocks .iter() .copied() @@ -718,48 +718,35 @@ where .collect(); *self.driver.store_mut() = DenseAnalysisState { scope: Some(scope), - blocks, - recorder: None, + points: DenseFactStore::new(), }; let mut semantics = DenseBackwardSemantics; - self.driver.solve_many(&mut semantics, owners) - } - - /// Reconstruct every per-statement state — including statements inside - /// structured bodies, at any nesting depth — by re-walking each converged - /// CFG block with the recorder enabled. Per-point states are never - /// persisted by the fixpoint itself; loop bodies record their final - /// (stable) iteration. - pub fn reconstruct_points( - &mut self, - stage: CompileStage, - body: impl Into, - ) -> Result, E> { - let scope = (stage, body.into()); - self.driver.store_mut().recorder = Some(DensePointStore::new()); - for block in self.cfg_blocks() { - // The CFGOwner walk re-absorbs the converged successor summaries, - // so it replays exactly the fixpoint's final states. - let _ = scope; - self.driver.replace_state(V::bottom()); - match self - .driver - .run_frame(F::from_block(DenseBlockFrame::cfg_owner(stage, block)))? - { - DenseBackwardCompletion::Block { .. } => {} - DenseBackwardCompletion::Structured => { - return Err(E::from(InterpreterError::Custom( - "a CFG block walk completed as a structured frame", - ))); - } - } + self.driver.solve_many(&mut semantics, owners)?; + + // Block summaries are the authoritative converged boundary facts. + // Refresh their program points after the worklist drains; structured + // block boundaries were recorded by their final containing walk. + let boundaries: Vec<_> = blocks + .into_iter() + .filter_map(|block| { + self.driver + .summary(&Scoped::new(scope, block)) + .cloned() + .map(|summary| (block, summary)) + }) + .collect(); + for (block, summary) in boundaries { + self.driver + .store_mut() + .points + .set(ProgramPoint::BlockEntry(block), summary.live_in); + self.driver + .store_mut() + .points + .set(ProgramPoint::BlockExit(block), summary.live_out); } - Ok(self - .driver - .store_mut() - .recorder - .take() - .expect("recorder installed above")) + + Ok(()) } } diff --git a/crates/kirin-interpreter/src/facts/anchor.rs b/crates/kirin-interpreter/src/facts/anchor.rs index 706b271591..61e5422da6 100644 --- a/crates/kirin-interpreter/src/facts/anchor.rs +++ b/crates/kirin-interpreter/src/facts/anchor.rs @@ -3,8 +3,8 @@ //! //! Following MLIR's terminology, a lattice fact is attached to a *lattice //! anchor*: sparse analyses anchor facts to [`SSAValue`]s; dense analyses -//! anchor facts to blocks, program points, or edges. Which anchor family an -//! analysis uses is part of its solver *shape* +//! anchor facts to [`ProgramPoint`]s. Which anchor family an analysis uses is +//! part of its solver *shape* //! ([`AnalysisShape`](crate::AnalysisShape) — see //! [`semantics`](crate::semantics)); anchors themselves carry no dispatch //! meaning. What a rule *means* is a separate concern entirely: the @@ -27,20 +27,25 @@ use kirin_ir::{Block, SSAValue, Statement}; /// /// Anchors key fact stores ([`FactStore`](crate::FactStore)) and summaries, so /// they must be cheap to clone, compare, and hash. Sparse anchors are -/// [`SSAValue`]s; dense anchors are [`Block`]s, [`ProgramPoint`]s, or -/// [`DenseAnchor`]s; [`Scoped`] qualifies any anchor with its scope. +/// [`SSAValue`]s; dense anchors are [`ProgramPoint`]s; [`Scoped`] qualifies any +/// anchor with its scope. pub trait LatticeAnchor: Clone + Eq + Hash {} impl LatticeAnchor for SSAValue {} impl LatticeAnchor for Block {} -/// A program point: immediately before or after a statement. +/// A location at which a dense dataflow fact is defined. /// -/// Never anchor a fact to a raw statement without saying *before* or *after* — -/// the two carry different facts for any non-trivial analysis. +/// Blocks and statements each have two distinct boundary points. Whole CFG and +/// graph bodies are deliberately not points: unlike a block, they do not have +/// one unambiguous entry/exit fact. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ProgramPoint { + /// State on entry to a CFG-owned or statement-owned block. + BlockEntry(Block), + /// State on exit from a CFG-owned or statement-owned block. + BlockExit(Block), /// The point immediately before `stmt` executes. Before(Statement), /// The point immediately after `stmt` executes. @@ -49,21 +54,6 @@ pub enum ProgramPoint { impl LatticeAnchor for ProgramPoint {} -/// A dense lattice anchor: a block boundary, program point, or CFG edge. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum DenseAnchor { - /// State on entry to a block. - BlockEntry(Block), - /// State on exit from a block. - BlockExit(Block), - /// State at a specific [`ProgramPoint`]. - Point(ProgramPoint), - /// State on a specific CFG edge. - Edge { from: Block, to: Block }, -} - -impl LatticeAnchor for DenseAnchor {} - // =========================================================================== // Scope qualification // =========================================================================== diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index d5d7c5afbd..cad30050ea 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -5,5 +5,5 @@ pub(crate) mod anchor; pub(crate) mod store; -pub use anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; -pub use store::{DenseBlockStore, DensePointStore, FactStore, ScopedSparseStore, SparseStore}; +pub use anchor::{Change, LatticeAnchor, ProgramPoint, Scoped}; +pub use store::{DenseFactStore, FactStore, ScopedSparseStore, SparseStore}; diff --git a/crates/kirin-interpreter/src/facts/store.rs b/crates/kirin-interpreter/src/facts/store.rs index b6dc0ad809..7298b0d35b 100644 --- a/crates/kirin-interpreter/src/facts/store.rs +++ b/crates/kirin-interpreter/src/facts/store.rs @@ -8,14 +8,14 @@ //! 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`]), dense analyses to program points -//! ([`DensePointStore`]) or block boundaries ([`DenseBlockStore`]). +//! [`ScopedSparseStore`]), while dense analyses anchor facts to block and +//! statement boundaries ([`DenseFactStore`]). use std::collections::HashMap; use kirin_ir::SSAValue; -use super::anchor::{Change, DenseAnchor, LatticeAnchor, ProgramPoint, Scoped}; +use super::anchor::{Change, LatticeAnchor, ProgramPoint, Scoped}; /// One dataflow fact per lattice anchor. /// @@ -105,68 +105,8 @@ pub type SparseStore = FactStore; /// under two scopes is two distinct facts. pub type ScopedSparseStore = FactStore, F>; -/// A dense store keyed by program points, for analyses that need per-point -/// state as a queryable fact (e.g. reconstructed per-statement live sets). -pub type DensePointStore = FactStore; - -/// A dense store keyed by block boundaries (entry/exit), for dense analyses. -/// -/// This is the rustc-style default: store block-boundary states and -/// reconstruct statement-local states on demand rather than persisting every -/// before/after state. A thin convenience wrapper over -/// `FactStore` using the -/// [`BlockEntry`](DenseAnchor::BlockEntry)/[`BlockExit`](DenseAnchor::BlockExit) -/// anchors. -#[derive(Clone, Debug)] -pub struct DenseBlockStore { - facts: FactStore, -} - -impl Default for DenseBlockStore { - fn default() -> Self { - Self::new() - } -} - -impl DenseBlockStore { - pub fn new() -> Self { - Self { - facts: FactStore::new(), - } - } - - pub fn entry(&self, block: kirin_ir::Block) -> Option<&F> { - self.facts.get(DenseAnchor::BlockEntry(block)) - } - - pub fn exit(&self, block: kirin_ir::Block) -> Option<&F> { - self.facts.get(DenseAnchor::BlockExit(block)) - } - - pub fn set_entry(&mut self, block: kirin_ir::Block, fact: F) { - self.facts.set(DenseAnchor::BlockEntry(block), fact); - } - - pub fn set_exit(&mut self, block: kirin_ir::Block, fact: F) { - self.facts.set(DenseAnchor::BlockExit(block), fact); - } - - /// Iterate `(block, entry fact)` pairs (order unspecified). - pub fn entries(&self) -> impl Iterator { - self.facts.iter().filter_map(|(anchor, fact)| match anchor { - DenseAnchor::BlockEntry(block) => Some((block, fact)), - _ => None, - }) - } - - /// Iterate `(block, exit fact)` pairs (order unspecified). - pub fn exits(&self) -> impl Iterator { - self.facts.iter().filter_map(|(anchor, fact)| match anchor { - DenseAnchor::BlockExit(block) => Some((block, fact)), - _ => None, - }) - } -} +/// A dense store keyed uniformly by block and statement program points. +pub type DenseFactStore = FactStore; #[cfg(test)] mod tests { @@ -227,35 +167,22 @@ mod tests { } #[test] - fn dense_block_store_maps_entry_exit_through_dense_anchor() { + fn dense_fact_store_keeps_block_and_statement_boundaries_distinct() { let block = Block::from(Id::from(ssa(0))); let other = Block::from(Id::from(ssa(1))); - - let mut store: DenseBlockStore<&'static str> = DenseBlockStore::new(); - store.set_entry(block, "in"); - store.set_exit(block, "out"); - - // Entry and exit of the same block are distinct anchors. - assert_eq!(store.entry(block), Some(&"in")); - assert_eq!(store.exit(block), Some(&"out")); - assert_eq!(store.entry(other), None); - - let entries: Vec<_> = store.entries().collect(); - let exits: Vec<_> = store.exits().collect(); - assert_eq!(entries, vec![(block, &"in")]); - assert_eq!(exits, vec![(block, &"out")]); - } - - #[test] - fn dense_point_store_keeps_before_and_after_distinct() { let statement = Statement::from(Id::from(ssa(3))); - let mut store: DensePointStore<&'static str> = FactStore::new(); + let mut store: DenseFactStore<&'static str> = FactStore::new(); + store.set(ProgramPoint::BlockEntry(block), "in"); + store.set(ProgramPoint::BlockExit(block), "out"); store.set(ProgramPoint::Before(statement), "before"); store.set(ProgramPoint::After(statement), "after"); + assert_eq!(store.get(ProgramPoint::BlockEntry(block)), Some(&"in")); + assert_eq!(store.get(ProgramPoint::BlockExit(block)), Some(&"out")); + assert_eq!(store.get(ProgramPoint::BlockEntry(other)), None); assert_eq!(store.get(ProgramPoint::Before(statement)), Some(&"before")); assert_eq!(store.get(ProgramPoint::After(statement)), Some(&"after")); - assert_eq!(store.len(), 2); + assert_eq!(store.len(), 4); } } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 29794f2dac..e69fe5f7f8 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -121,8 +121,8 @@ pub use engines::dense_backward::{ // polymorphic fact stores. Anchor family is a property of the solver shape; // dispatch meaning lives in `semantics`. pub use facts::{ - Change, DenseAnchor, DenseBlockStore, DensePointStore, FactStore, LatticeAnchor, ProgramPoint, - Scoped, ScopedSparseStore, SparseStore, + Change, DenseFactStore, FactStore, LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, + SparseStore, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index 87b31748ae..b2d9404ff4 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -109,5 +109,5 @@ where let body = body.into(); let mut engine = DenseLiveness::::new(pipeline); engine.analyze(stage, body)?; - DenseLivenessResult::from_engine(&mut engine, stage, body) + Ok(DenseLivenessResult::from_engine(&engine)) } diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs index 3560e1b765..5c5ae026ae 100644 --- a/crates/kirin-liveness/src/result.rs +++ b/crates/kirin-liveness/src/result.rs @@ -2,9 +2,8 @@ //! per-point sets (classic liveness), plus their composition. use kirin_interpreter::{ - Body, DenseBackwardCompletion, DenseBackwardDriver, DenseBackwardInterpreter, - DenseBackwardTransfer, DenseBlockStore, DenseFrameBuild, DensePointStore, Frame, - InterpDispatch, InterpreterError, ProgramPoint, SparseBackwardInterpreter, StageQuery, + Body, DenseBackwardInterpreter, DenseFactStore, InterpreterError, ProgramPoint, + SparseBackwardInterpreter, }; use kirin_ir::{Block, CompileStage, Lattice, SSAValue, StageMeta, Statement}; @@ -44,8 +43,8 @@ impl DemandResult { } } -/// The result of [`analyze_dense`](crate::analyze_dense): classic per-point -/// liveness — block-boundary sets plus reconstructed per-statement sets. +/// The result of [`analyze_dense`](crate::analyze_dense): classic liveness at +/// every block and statement program point. /// /// These sets carry the conventional (regalloc-grade) meaning: every use gens, /// purity-irrelevant. Strong per-point sets are the composition @@ -53,69 +52,58 @@ impl DemandResult { /// intersected with the demand set. #[derive(Clone, Debug)] pub struct DenseLivenessResult { - blocks: DenseBlockStore, - points: DensePointStore, + facts: DenseFactStore, } impl DenseLivenessResult { - /// Build the result from a converged dense engine: copy the boundary - /// summaries and reconstruct every per-statement state by replaying each - /// block through the dialect rules. + /// Copy the facts recorded by a converged dense engine. pub fn from_engine<'ir, S, F>( - engine: &mut DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F>, - stage: CompileStage, - body: impl Into, - ) -> Result + engine: &DenseBackwardInterpreter<'ir, S, LiveSet, InterpreterError, F>, + ) -> Self where - S: StageMeta - + StageQuery - + InterpDispatch>, - F: Frame< - DenseBackwardDriver<'ir, S, LiveSet, InterpreterError, F>, - F, - Completion = DenseBackwardCompletion, - > + DenseFrameBuild, + S: StageMeta, { - let body = body.into(); - let mut blocks = DenseBlockStore::new(); - for block in engine.cfg_blocks() { - if let Some(summary) = engine.block_summary(stage, body, block) { - blocks.set_entry(block, summary.live_in.clone()); - blocks.set_exit(block, summary.live_out.clone()); - } + Self { + facts: engine.fact_store().clone(), } - let points = engine.reconstruct_points(stage, body)?; - Ok(Self { blocks, points }) + } + + /// The liveness fact recorded at `point`. + pub fn point_facts(&self, point: ProgramPoint) -> Option<&LiveSet> { + self.facts.get(point) } /// Iterate `(block, live_in, live_out)` triples (order unspecified). pub fn blocks(&self) -> impl Iterator { - self.blocks.entries().filter_map(|(block, live_in)| { - self.blocks - .exit(block) + self.facts.iter().filter_map(|(point, live_in)| { + let ProgramPoint::BlockEntry(block) = point else { + return None; + }; + self.facts + .get(ProgramPoint::BlockExit(block)) .map(|live_out| (block, live_in, live_out)) }) } /// The set of values live on entry to `block`. pub fn live_in(&self, block: Block) -> Option<&LiveSet> { - self.blocks.entry(block) + self.point_facts(ProgramPoint::BlockEntry(block)) } /// The set of values live on exit from `block` (excludes the terminator's /// own uses, e.g. the branch condition). pub fn live_out(&self, block: Block) -> Option<&LiveSet> { - self.blocks.exit(block) + self.point_facts(ProgramPoint::BlockExit(block)) } /// The set of values live immediately before `statement`. pub fn live_before(&self, statement: Statement) -> Option<&LiveSet> { - self.points.get(ProgramPoint::Before(statement)) + self.point_facts(ProgramPoint::Before(statement)) } /// The set of values live immediately after `statement`. pub fn live_after(&self, statement: Statement) -> Option<&LiveSet> { - self.points.get(ProgramPoint::After(statement)) + self.point_facts(ProgramPoint::After(statement)) } /// Strong per-point set: the classic set intersected with the demand set diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 879c5cbeec..3e25bb2fe2 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -4,6 +4,7 @@ use kirin::prelude::{GetInfo, ParsePipelineText, Pipeline, SSAValue, StageInfo}; use kirin_arith::Arith; +use kirin_interpreter::ProgramPoint; use kirin_liveness::analyze_demand; use kirin_test_languages::ArithFunctionLanguage; @@ -315,6 +316,10 @@ fn classic_liveness_boundary_sets() { // live_in(entry): %x (used by add and both edges) and %cond (branch use). assert_eq!(result.live_in(entry), Some(&live_set(&[x, cond]))); + assert_eq!( + result.point_facts(ProgramPoint::BlockEntry(entry)), + Some(&live_set(&[x, cond])) + ); // live_out(entry): both successors' live-ins mapped across the edges — // {%a} → {%x}, {%b} → {%x}; the branch condition is a terminator *use*, // not part of the boundary set. diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 1ad99108b4..8d97335a90 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -865,11 +865,15 @@ mod advanced { // =========================================================================== mod demand { + use std::collections::HashSet; + use kirin::prelude::{ - CFG, CompileStage, GetInfo, HasCFGBody, HasResults, ParsePipelineText, Pipeline, SSAValue, + CFG, CompileStage, GetInfo, HasBlocks, HasCFG, HasCFGBody, HasDigraphs, HasResults, + HasUngraphs, ParsePipelineText, Pipeline, SSAValue, Statement, }; use kirin_arith::{Arith, ArithValue}; use kirin_function::Lexical; + use kirin_interpreter::Body; use kirin_liveness::analyze_demand; use crate::language::HighLevel; @@ -915,29 +919,85 @@ mod demand { .collect() } - /// Find something by matching statement definitions anywhere in the - /// cfg (including scf bodies, via the topology's nested-block - /// enumeration). - pub(super) fn find_value( + /// Find something by walking statement definitions anywhere in the CFG, + /// including bodies nested under statements. + fn find_in_body( pipeline: &Pipeline, cfg: CFG, - select: impl Fn(&HighLevel) -> Option, + mut select: impl FnMut(Statement, &HighLevel) -> Option, ) -> R { let stage_id = pipeline.stage_by_name("source").expect("source stage"); let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { panic!("source stage holds HighLevel"); }; - let topology = kirin_interpreter::body_topology(info, kirin_interpreter::Body::CFG(cfg)); - for block in &topology.blocks { - for &stmt in &block.stmts { - if let Some(value) = select(stmt.definition(info)) { + + let mut bodies = vec![Body::CFG(cfg)]; + let mut visited = HashSet::new(); + while let Some(body) = bodies.pop() { + if !visited.insert(body) { + continue; + } + + let statements: Vec = match body { + Body::CFG(cfg) => { + bodies.extend(cfg.blocks(info).map(Body::Block)); + continue; + } + Body::Block(block) => { + let mut statements: Vec = block.statements(info).collect(); + if let Some(terminator) = block.terminator(info) { + statements.push(terminator); + } + statements + } + Body::DiGraph(graph) => graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + Body::UnGraph(graph) => graph + .expect_info(info) + .graph() + .node_weights() + .copied() + .collect(), + }; + + for statement in statements { + let definition = statement.definition(info); + if let Some(value) = select(statement, definition) { return value; } + bodies.extend(definition.blocks().copied().map(Body::Block)); + bodies.extend(definition.cfgs().copied().map(Body::CFG)); + bodies.extend(definition.digraphs().copied().map(Body::DiGraph)); + bodies.extend(definition.ungraphs().copied().map(Body::UnGraph)); } } panic!("no matching statement in cfg"); } + pub(super) fn find_value( + pipeline: &Pipeline, + cfg: CFG, + select: impl FnMut(&HighLevel) -> Option, + ) -> R { + let mut select = select; + find_in_body(pipeline, cfg, |_, definition| select(definition)) + } + + pub(super) fn find_statement( + pipeline: &Pipeline, + cfg: CFG, + select: impl FnMut(&HighLevel) -> bool, + ) -> Statement { + let mut select = select; + find_in_body(pipeline, cfg, |statement, definition| { + select(definition).then_some(statement) + }) + } + /// The result of the `constant -> i64` statement. pub(super) fn constant_result(pipeline: &Pipeline, cfg: CFG, value: i64) -> SSAValue { find_value(pipeline, cfg, |definition| match definition { @@ -1211,13 +1271,16 @@ specialize @source fn @main(i64, i64) -> i64 { // =========================================================================== mod dense { - use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue, Statement}; + use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue}; use kirin_arith::{Arith, ArithValue}; - use kirin_interpreter::InterpreterError; + use kirin_interpreter::{InterpreterError, ProgramPoint}; use kirin_liveness::{DenseLivenessResult, LiveSet}; + use kirin_scf::StructuredControlFlow; use super::demand::{FOR_CARRIED_DEMAND, IF_DEAD_RESULT}; - use super::demand::{constant_result, entry_params, find_value, parse, source_cfg}; + use super::demand::{ + constant_result, entry_params, find_statement, find_value, parse, source_cfg, + }; use crate::interpreter::ToyDenseBackwardFrame; use crate::language::HighLevel; use crate::stage::Stage; @@ -1235,28 +1298,6 @@ mod dense { .expect("analysis succeeds") } - /// The statement whose definition matches `select` (anywhere in the - /// cfg, including scf bodies). - fn find_statement( - pipeline: &Pipeline, - cfg: CFG, - select: impl Fn(&HighLevel) -> bool, - ) -> Statement { - let stage_id = pipeline.stage_by_name("source").expect("source stage"); - let Stage::Source(info) = pipeline.stage(stage_id).expect("stage info") else { - panic!("source stage holds HighLevel"); - }; - let topology = kirin_interpreter::body_topology(info, kirin_interpreter::Body::CFG(cfg)); - for block in &topology.blocks { - for &stmt in &block.stmts { - if select(stmt.definition(info)) { - return stmt; - } - } - } - panic!("no matching statement in cfg"); - } - fn live_set(values: &[SSAValue]) -> LiveSet { values.iter().copied().collect() } @@ -1289,6 +1330,19 @@ mod dense { matches!(definition, HighLevel::Structured(_)) }); assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond]))); + + let then_block = find_value(&pipeline, cfg, |definition| match definition { + HighLevel::Structured(StructuredControlFlow::If(if_op)) => Some(if_op.then_block()), + _ => None, + }); + assert_eq!( + dense.point_facts(ProgramPoint::BlockEntry(then_block)), + Some(&live_set(&[cond])) + ); + assert_eq!( + dense.point_facts(ProgramPoint::BlockExit(then_block)), + Some(&live_set(&[cond])) + ); } const IF_ARMS_DIFFERENT_USES: &str = r#" From d4235695f5c99646147164568ad24983c1fc8cc0 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Tue, 11 Aug 2026 09:44:22 -0400 Subject: [PATCH 17/21] Refactor dense backward engine to use scoped fact stores and improve point fact retrieval --- .../src/engines/dense_backward/frames.rs | 7 +- .../src/engines/dense_backward/interp.rs | 151 ++++++++---------- .../src/engines/dense_backward/mod.rs | 7 +- crates/kirin-interpreter/src/facts/mod.rs | 2 +- crates/kirin-interpreter/src/facts/store.rs | 51 +++--- crates/kirin-interpreter/src/lib.rs | 12 +- crates/kirin-liveness/src/result.rs | 67 ++------ crates/kirin-liveness/tests/cfg.rs | 97 +++++++++-- example/toy-lang/src/interpreter/mod.rs | 10 +- example/toy-lang/src/interpreter/tests.rs | 46 ++++-- example/toy-lang/src/main.rs | 24 ++- 11 files changed, 270 insertions(+), 204 deletions(-) diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index c49edf5331..d5baecf740 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -23,7 +23,7 @@ use crate::{ #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DenseBlockMode { /// A CFG block owner: the terminator's [`Edges`](DenseBackwardEffect::Edges) - /// are absorbed (seeding the state and recording `live_out`); completes + /// are absorbed (seeding the state and producing `live_out`); completes /// with [`DenseBackwardCompletion::Block`]. CFGOwner, /// A structured body walked by a dialect frame against the current point @@ -108,7 +108,9 @@ where if self.remaining == 0 { let live_in = interp.state(); - interp.record_point(ProgramPoint::BlockEntry(self.block), live_in.clone()); + if self.mode == DenseBlockMode::StructuredBody { + interp.record_point(ProgramPoint::BlockEntry(self.block), live_in.clone()); + } return Ok(FrameEffect::Complete(match self.mode { DenseBlockMode::CFGOwner => DenseBackwardCompletion::Block { live_in: live_in.clone(), @@ -140,7 +142,6 @@ where match self.mode { DenseBlockMode::CFGOwner => { let out = interp.absorb_edges(self.stage, &edges)?; - interp.record_point(ProgramPoint::BlockExit(self.block), out.clone()); self.live_out = Some(out); let before = interp.state(); interp.record_point(ProgramPoint::Before(statement), before); diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index cc32728575..62ce428db2 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -39,9 +39,11 @@ //! argument, pass-through for non-parameters) — which both seeds the walk //! state and records the block's `live_out`. Structured dialects push //! dialect-owned frames ([`DenseBackwardEffect::Push`]) that walk their bodies -//! against the same point state. Every block walk records its block and -//! statement program-point facts; later fixpoint iterations overwrite earlier -//! approximations, leaving the final stable facts in the analysis store. +//! against the same point state. Each block walk records statement points and +//! nested structured-block boundaries; later fixpoint iterations overwrite +//! earlier approximations. CFG-owner boundaries remain canonical in their +//! converged summaries. The public fact view merges those disjoint sources +//! into one scope-qualified program-point store. use std::marker::PhantomData; @@ -55,10 +57,10 @@ use crate::Body; use crate::core::query; use crate::engines::sparse_backward::BodyScope; use crate::{ - AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, - DenseFactStore, EnvIndex, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, - InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, - StandardFixpointInterpreter, Summary, SummaryDependency, SummaryDependencyIndex, SummaryEffect, + AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, EnvIndex, + FactStore, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, + OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, + SummaryDependency, SummaryDependencyIndex, SummaryEffect, }; // =========================================================================== @@ -372,29 +374,13 @@ pub enum DenseBackwardCompletion { Structured, } -/// Analysis-local state carried in the driver's `store` slot: the active scope -/// and the latest fact recorded at every visited program point. -pub struct DenseAnalysisState { - scope: Option, - points: DenseFactStore, -} - -impl Default for DenseAnalysisState { - fn default() -> Self { - Self { - scope: None, - points: DenseFactStore::new(), - } - } -} - /// The dense backward driver: a [`StandardFixpointInterpreter`] over /// [`DenseBackwardTransfer`] with scope-qualified block owners and /// successor→predecessor dependencies. pub type DenseBackwardDriver<'ir, S, V, E, F, Sem = ClassicLiveness> = StandardFixpointInterpreter< DenseBackwardTransfer<'ir, S, V, E, F, Sem>, DenseBackwardProfile, - DenseAnalysisState, + FactStore, V>, BackwardSummaryDeps>, >; @@ -508,25 +494,28 @@ where } fn record_point(&mut self, point: ProgramPoint, facts: V) { - self.store_mut().points.set(point, facts); + let scope = self + .current_owner() + .map(|owner| owner.scope) + .expect("dense frames only run while analyzing an owner"); + self.store_mut().set(Scoped::new(scope, point), facts); } fn absorb_edges(&mut self, stage: CompileStage, edges: &[SuccessorEdge]) -> Result { - let scope = self - .store() - .scope - .ok_or_else(|| E::from(InterpreterError::Custom("no active backward analysis")))?; + let current = self + .current_owner() + .cloned() + .ok_or_else(|| E::from(InterpreterError::Custom("no active backward owner")))?; + let scope = current.scope; let mut out = V::bottom(); for edge in edges { let owner = Scoped::new(scope, edge.target); // Successor changed → reanalyse the current block. - if let Some(current) = self.current_owner().cloned() { - self.dependency_index_mut() - .register(&owner, SummaryDependency::Reanalyze(current)) - .expect("backward dependency index is infallible"); - } + self.dependency_index_mut() + .register(&owner, SummaryDependency::Reanalyze(current.clone())) + .expect("backward dependency index is infallible"); let Some(summary) = self.summary(&owner) else { continue; @@ -620,7 +609,8 @@ where /// ```ignore /// let mut analysis = DenseBackwardInterpreter::::new(&pipeline); /// analysis.analyze(stage, cfg)?; -/// let boundary = analysis.block_summary(stage, cfg, block); +/// let point = Scoped::new((stage, Body::CFG(cfg)), ProgramPoint::BlockEntry(block)); +/// let live_in = analysis.point_facts(point); /// ``` pub struct DenseBackwardInterpreter< 'ir, @@ -649,7 +639,7 @@ where Self { driver: StandardFixpointInterpreter::with_dependency_index( DenseBackwardTransfer::new(pipeline), - DenseAnalysisState::default(), + FactStore::new(), (), BackwardSummaryDeps::new(), ), @@ -660,26 +650,46 @@ where self.driver.inner().pipeline() } - /// The converged boundary states of `block` under the `(stage, cfg)` - /// scope. - pub fn block_summary( - &self, - stage: CompileStage, - body: impl Into, - block: Block, - ) -> Option<&BlockLiveness> { - self.driver - .summary(&Scoped::new((stage, body.into()), block)) - } - - /// The latest fact recorded at `point` for the active dense analysis. - pub fn point_facts(&self, point: ProgramPoint) -> Option<&V> { - self.driver.store().points.get(point) + /// The converged fact at a scope-qualified program point. + /// + /// CFG-owner boundaries come directly from their fixpoint summaries; + /// statement and nested structured-block points come from the point store. + /// Each fact therefore has one mutable source during solving. + pub fn point_facts(&self, point: Scoped) -> Option<&V> { + let summary_fact = match point.item { + ProgramPoint::BlockEntry(block) => self + .driver + .summary(&Scoped::new(point.scope, block)) + .map(|summary| &summary.live_in), + ProgramPoint::BlockExit(block) => self + .driver + .summary(&Scoped::new(point.scope, block)) + .map(|summary| &summary.live_out), + ProgramPoint::Before(_) | ProgramPoint::After(_) => None, + }; + summary_fact.or_else(|| self.driver.store().get(point)) } - /// All block- and statement-boundary facts for the active analysis. - pub fn fact_store(&self) -> &DenseFactStore { - &self.driver.store().points + /// Snapshot the active analysis as one scope-qualified program-point fact + /// store. + /// + /// The solver keeps CFG-owner boundaries in summaries because they drive + /// convergence. This copies each final boundary into the returned result; + /// it does not create a second mutable representation inside the engine. + pub fn facts(&self) -> FactStore, V> { + let mut facts = self.driver.store().clone(); + + for (owner, summary) in self.driver.summaries() { + facts.set( + Scoped::new(owner.scope, ProgramPoint::BlockEntry(owner.item)), + summary.live_in.clone(), + ); + facts.set( + Scoped::new(owner.scope, ProgramPoint::BlockExit(owner.item)), + summary.live_out.clone(), + ); + } + facts } } @@ -716,37 +726,10 @@ where .copied() .map(|block| Scoped::new(scope, block)) .collect(); - *self.driver.store_mut() = DenseAnalysisState { - scope: Some(scope), - points: DenseFactStore::new(), - }; + let pipeline = self.driver.inner().pipeline(); + self.driver = Self::new(pipeline).driver; let mut semantics = DenseBackwardSemantics; - self.driver.solve_many(&mut semantics, owners)?; - - // Block summaries are the authoritative converged boundary facts. - // Refresh their program points after the worklist drains; structured - // block boundaries were recorded by their final containing walk. - let boundaries: Vec<_> = blocks - .into_iter() - .filter_map(|block| { - self.driver - .summary(&Scoped::new(scope, block)) - .cloned() - .map(|summary| (block, summary)) - }) - .collect(); - for (block, summary) in boundaries { - self.driver - .store_mut() - .points - .set(ProgramPoint::BlockEntry(block), summary.live_in); - self.driver - .store_mut() - .points - .set(ProgramPoint::BlockExit(block), summary.live_out); - } - - Ok(()) + self.driver.solve_many(&mut semantics, owners) } } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs index ffffe9890b..a15ace34f2 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/mod.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/mod.rs @@ -7,8 +7,7 @@ pub(crate) mod interp; pub use frames::{DenseBlockFrame, DenseBlockMode, DenseFrameBuild, StandardDenseBackwardFrame}; pub use interp::{ - BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, - DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, - PointFacts, SuccessorEdge, + BlockLiveness, ClassicLivenessInterp, DenseBackwardCompletion, DenseBackwardDriver, + DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, + DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, PointFacts, SuccessorEdge, }; diff --git a/crates/kirin-interpreter/src/facts/mod.rs b/crates/kirin-interpreter/src/facts/mod.rs index cad30050ea..9684cdfec9 100644 --- a/crates/kirin-interpreter/src/facts/mod.rs +++ b/crates/kirin-interpreter/src/facts/mod.rs @@ -6,4 +6,4 @@ pub(crate) mod anchor; pub(crate) mod store; pub use anchor::{Change, LatticeAnchor, ProgramPoint, Scoped}; -pub use store::{DenseFactStore, FactStore, ScopedSparseStore, SparseStore}; +pub use store::{FactStore, ScopedSparseStore, SparseStore}; diff --git a/crates/kirin-interpreter/src/facts/store.rs b/crates/kirin-interpreter/src/facts/store.rs index 7298b0d35b..e7699735de 100644 --- a/crates/kirin-interpreter/src/facts/store.rs +++ b/crates/kirin-interpreter/src/facts/store.rs @@ -8,14 +8,14 @@ //! 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 anchor facts to block and -//! statement boundaries ([`DenseFactStore`]). +//! [`ScopedSparseStore`]), while dense analyses use +//! `FactStore, F>` directly. use std::collections::HashMap; use kirin_ir::SSAValue; -use super::anchor::{Change, LatticeAnchor, ProgramPoint, Scoped}; +use super::anchor::{Change, LatticeAnchor, Scoped}; /// One dataflow fact per lattice anchor. /// @@ -105,11 +105,9 @@ pub type SparseStore = FactStore; /// under two scopes is two distinct facts. pub type ScopedSparseStore = FactStore, F>; -/// A dense store keyed uniformly by block and statement program points. -pub type DenseFactStore = FactStore; - #[cfg(test)] mod tests { + use crate::{Body, BodyScope, ProgramPoint}; use kirin_ir::{Block, CFG, CompileStage, Id, Statement, TestSSAValue}; use super::*; @@ -167,22 +165,39 @@ mod tests { } #[test] - fn dense_fact_store_keeps_block_and_statement_boundaries_distinct() { + fn scoped_dense_facts_keep_block_and_statement_boundaries_distinct() { let block = Block::from(Id::from(ssa(0))); let other = Block::from(Id::from(ssa(1))); let statement = Statement::from(Id::from(ssa(3))); - let mut store: DenseFactStore<&'static str> = FactStore::new(); - store.set(ProgramPoint::BlockEntry(block), "in"); - store.set(ProgramPoint::BlockExit(block), "out"); - store.set(ProgramPoint::Before(statement), "before"); - store.set(ProgramPoint::After(statement), "after"); - - assert_eq!(store.get(ProgramPoint::BlockEntry(block)), Some(&"in")); - assert_eq!(store.get(ProgramPoint::BlockExit(block)), Some(&"out")); - assert_eq!(store.get(ProgramPoint::BlockEntry(other)), None); - assert_eq!(store.get(ProgramPoint::Before(statement)), Some(&"before")); - assert_eq!(store.get(ProgramPoint::After(statement)), Some(&"after")); + let scope: BodyScope = ( + CompileStage::from(Id::from(ssa(10))), + Body::CFG(CFG::from(Id::from(ssa(11)))), + ); + let point = |item| Scoped::new(scope, item); + let mut store: FactStore, &'static str> = FactStore::new(); + store.set(point(ProgramPoint::BlockEntry(block)), "in"); + store.set(point(ProgramPoint::BlockExit(block)), "out"); + store.set(point(ProgramPoint::Before(statement)), "before"); + store.set(point(ProgramPoint::After(statement)), "after"); + + assert_eq!( + store.get(point(ProgramPoint::BlockEntry(block))), + Some(&"in") + ); + assert_eq!( + store.get(point(ProgramPoint::BlockExit(block))), + Some(&"out") + ); + assert_eq!(store.get(point(ProgramPoint::BlockEntry(other))), None); + assert_eq!( + store.get(point(ProgramPoint::Before(statement))), + Some(&"before") + ); + assert_eq!( + store.get(point(ProgramPoint::After(statement))), + Some(&"after") + ); assert_eq!(store.len(), 4); } } diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index e69fe5f7f8..718c6bb9f1 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -110,19 +110,17 @@ pub use engines::sparse_backward::{ }; // Dense backward engine (`Sem = ClassicLiveness`) + the dense standard frames. pub use engines::dense_backward::{ - BlockLiveness, ClassicLivenessInterp, DenseAnalysisState, DenseBackwardCompletion, - DenseBackwardDriver, DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, - DenseBackwardInterpreter, DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, - DenseBlockFrame, DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, - SuccessorEdge, + BlockLiveness, ClassicLivenessInterp, DenseBackwardCompletion, DenseBackwardDriver, + DenseBackwardEffect, DenseBackwardFrameEngine, DenseBackwardInterp, DenseBackwardInterpreter, + DenseBackwardProfile, DenseBackwardState, DenseBackwardTransfer, DenseBlockFrame, + DenseBlockMode, DenseFrameBuild, PointFacts, StandardDenseBackwardFrame, SuccessorEdge, }; // Lattice anchors (*where* facts attach), scope qualification, and the // polymorphic fact stores. Anchor family is a property of the solver shape; // dispatch meaning lives in `semantics`. pub use facts::{ - Change, DenseFactStore, FactStore, LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, - SparseStore, + Change, FactStore, LatticeAnchor, ProgramPoint, Scoped, ScopedSparseStore, SparseStore, }; // Semantic keys (*what* a rule means — the `Interpretable`/`Interp::Semantics` diff --git a/crates/kirin-liveness/src/result.rs b/crates/kirin-liveness/src/result.rs index 5c5ae026ae..6d7dbc42d8 100644 --- a/crates/kirin-liveness/src/result.rs +++ b/crates/kirin-liveness/src/result.rs @@ -2,10 +2,10 @@ //! per-point sets (classic liveness), plus their composition. use kirin_interpreter::{ - Body, DenseBackwardInterpreter, DenseFactStore, InterpreterError, ProgramPoint, + Body, BodyScope, DenseBackwardInterpreter, FactStore, InterpreterError, ProgramPoint, Scoped, SparseBackwardInterpreter, }; -use kirin_ir::{Block, CompileStage, Lattice, SSAValue, StageMeta, Statement}; +use kirin_ir::{CompileStage, Lattice, SSAValue, StageMeta}; use crate::live::{Live, LiveSet}; @@ -47,12 +47,12 @@ impl DemandResult { /// every block and statement program point. /// /// These sets carry the conventional (regalloc-grade) meaning: every use gens, -/// purity-irrelevant. Strong per-point sets are the composition -/// [`strong_live_before`](Self::strong_live_before) — the classic set +/// purity-irrelevant. Strong per-point sets are +/// [`strong_point_facts`](Self::strong_point_facts): the classic set /// intersected with the demand set. #[derive(Clone, Debug)] pub struct DenseLivenessResult { - facts: DenseFactStore, + facts: FactStore, LiveSet>, } impl DenseLivenessResult { @@ -64,66 +64,23 @@ impl DenseLivenessResult { S: StageMeta, { Self { - facts: engine.fact_store().clone(), + facts: engine.facts(), } } /// The liveness fact recorded at `point`. - pub fn point_facts(&self, point: ProgramPoint) -> Option<&LiveSet> { + pub fn point_facts(&self, point: Scoped) -> Option<&LiveSet> { self.facts.get(point) } - /// Iterate `(block, live_in, live_out)` triples (order unspecified). - pub fn blocks(&self) -> impl Iterator { - self.facts.iter().filter_map(|(point, live_in)| { - let ProgramPoint::BlockEntry(block) = point else { - return None; - }; - self.facts - .get(ProgramPoint::BlockExit(block)) - .map(|live_out| (block, live_in, live_out)) - }) - } - - /// The set of values live on entry to `block`. - pub fn live_in(&self, block: Block) -> Option<&LiveSet> { - self.point_facts(ProgramPoint::BlockEntry(block)) - } - - /// The set of values live on exit from `block` (excludes the terminator's - /// own uses, e.g. the branch condition). - pub fn live_out(&self, block: Block) -> Option<&LiveSet> { - self.point_facts(ProgramPoint::BlockExit(block)) - } - - /// The set of values live immediately before `statement`. - pub fn live_before(&self, statement: Statement) -> Option<&LiveSet> { - self.point_facts(ProgramPoint::Before(statement)) - } - - /// The set of values live immediately after `statement`. - pub fn live_after(&self, statement: Statement) -> Option<&LiveSet> { - self.point_facts(ProgramPoint::After(statement)) - } - - /// Strong per-point set: the classic set intersected with the demand set - /// (values live here *and* transitively needed by a root). - pub fn strong_live_before( - &self, - statement: Statement, - demand: &DemandResult, - ) -> Option { - self.live_before(statement) - .map(|set| set.meet(demand.demanded())) - } - - /// See [`strong_live_before`](Self::strong_live_before). - pub fn strong_live_after( + /// Strong fact at `point`: the classic set intersected with the demand set + /// (values live there *and* transitively needed by a root). + pub fn strong_point_facts( &self, - statement: Statement, + point: Scoped, demand: &DemandResult, ) -> Option { - self.live_after(statement) + self.point_facts(point) .map(|set| set.meet(demand.demanded())) } } diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 3e25bb2fe2..529ae27544 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -4,8 +4,8 @@ use kirin::prelude::{GetInfo, ParsePipelineText, Pipeline, SSAValue, StageInfo}; use kirin_arith::Arith; -use kirin_interpreter::ProgramPoint; -use kirin_liveness::analyze_demand; +use kirin_interpreter::{Body, InterpreterError, ProgramPoint, Scoped}; +use kirin_liveness::{DenseLiveness, analyze_demand}; use kirin_test_languages::ArithFunctionLanguage; const PROGRAM: &str = r#" @@ -313,21 +313,84 @@ fn classic_liveness_boundary_sets() { let entry = nth_block(&pipeline, cfg, 0); let then_block = nth_block(&pipeline, cfg, 1); let else_block = nth_block(&pipeline, cfg, 2); + let scope = (stage, Body::CFG(cfg)); + let point = |item| Scoped::new(scope, item); // live_in(entry): %x (used by add and both edges) and %cond (branch use). - assert_eq!(result.live_in(entry), Some(&live_set(&[x, cond]))); assert_eq!( - result.point_facts(ProgramPoint::BlockEntry(entry)), + result.point_facts(point(ProgramPoint::BlockEntry(entry))), Some(&live_set(&[x, cond])) ); + assert_eq!( + result.point_facts(Scoped::new( + (stage, Body::Block(entry)), + ProgramPoint::BlockEntry(entry), + )), + None, + "the same point under another body scope is a different fact" + ); // live_out(entry): both successors' live-ins mapped across the edges — // {%a} → {%x}, {%b} → {%x}; the branch condition is a terminator *use*, // not part of the boundary set. - assert_eq!(result.live_out(entry), Some(&live_set(&[x]))); - assert_eq!(result.live_in(then_block), Some(&live_set(&[then_param]))); - assert_eq!(result.live_out(then_block), Some(&live_set(&[]))); - assert_eq!(result.live_in(else_block), Some(&live_set(&[else_param]))); - assert_eq!(result.live_out(else_block), Some(&live_set(&[]))); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockExit(entry))), + Some(&live_set(&[x])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockEntry(then_block))), + Some(&live_set(&[then_param])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockExit(then_block))), + Some(&live_set(&[])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockEntry(else_block))), + Some(&live_set(&[else_param])) + ); + assert_eq!( + result.point_facts(point(ProgramPoint::BlockExit(else_block))), + Some(&live_set(&[])) + ); +} + +#[test] +fn reusing_dense_engine_replaces_the_previous_scoped_result() { + let pipeline = parse(PROGRAM); + let (stage, cfg) = main_cfg(&pipeline); + let entry = nth_block(&pipeline, cfg, 0); + let then_block = nth_block(&pipeline, cfg, 1); + let mut engine = DenseLiveness::<_, InterpreterError>::new(&pipeline); + + engine.analyze(stage, cfg).expect("CFG analysis succeeds"); + assert!( + engine + .point_facts(Scoped::new( + (stage, Body::CFG(cfg)), + ProgramPoint::BlockEntry(entry), + )) + .is_some() + ); + + engine + .analyze(stage, then_block) + .expect("block analysis succeeds"); + assert_eq!( + engine.point_facts(Scoped::new( + (stage, Body::CFG(cfg)), + ProgramPoint::BlockEntry(entry), + )), + None, + "facts from the previous analysis are not retained" + ); + assert!( + engine + .point_facts(Scoped::new( + (stage, Body::Block(then_block)), + ProgramPoint::BlockEntry(then_block), + )) + .is_some() + ); } #[test] @@ -341,12 +404,19 @@ fn classic_per_point_sets_gen_dead_uses() { let add = find_stmt(&pipeline, cfg, |definition| { matches!(definition, ArithFunctionLanguage::Arith(Arith::Add { .. })) }); + let scope = (stage, Body::CFG(cfg)); // Classic semantics: the dead add still GENS its operands, so %b is live // before it — this is the conventional per-point meaning (the old strong // expectations were demand projections, not dense liveness). - assert_eq!(result.live_before(add), Some(&live_set(&[a, b]))); - assert_eq!(result.live_after(add), Some(&live_set(&[a]))); + assert_eq!( + result.point_facts(Scoped::new(scope, ProgramPoint::Before(add))), + Some(&live_set(&[a, b])) + ); + assert_eq!( + result.point_facts(Scoped::new(scope, ProgramPoint::After(add))), + Some(&live_set(&[a])) + ); } #[test] @@ -365,7 +435,10 @@ fn strong_per_point_sets_are_classic_intersect_demanded() { // The composition recovers the strong (needed) per-point view: %b is // classically live before the dead add but not demanded, so it drops out. let strong = dense - .strong_live_before(add, &demand) + .strong_point_facts( + Scoped::new((stage, Body::CFG(cfg)), ProgramPoint::Before(add)), + &demand, + ) .expect("point reconstructed"); assert_eq!(strong, live_set(&[a])); assert!(!strong.contains(b)); diff --git a/example/toy-lang/src/interpreter/mod.rs b/example/toy-lang/src/interpreter/mod.rs index eebd6e6219..3071453ef7 100644 --- a/example/toy-lang/src/interpreter/mod.rs +++ b/example/toy-lang/src/interpreter/mod.rs @@ -185,9 +185,11 @@ pub fn analyze_classic_liveness( pipeline: &Pipeline, stage_name: &str, function_name: &str, -) -> Result { +) -> Result<(CompileStage, CFG, DenseLivenessResult), InterpreterError> { let (stage, cfg) = function_cfg(pipeline, stage_name, function_name)?; - kirin_liveness::analyze_dense_with_frame::<_, ToyDenseBackwardFrame>( - pipeline, stage, cfg, - ) + let result = kirin_liveness::analyze_dense_with_frame::< + _, + ToyDenseBackwardFrame, + >(pipeline, stage, cfg)?; + Ok((stage, cfg, result)) } diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 8d97335a90..f9e2bb5ed0 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -1273,7 +1273,7 @@ specialize @source fn @main(i64, i64) -> i64 { mod dense { use kirin::prelude::{CFG, CompileStage, Pipeline, SSAValue}; use kirin_arith::{Arith, ArithValue}; - use kirin_interpreter::{InterpreterError, ProgramPoint}; + use kirin_interpreter::{Body, InterpreterError, ProgramPoint, Scoped}; use kirin_liveness::{DenseLivenessResult, LiveSet}; use kirin_scf::StructuredControlFlow; @@ -1311,6 +1311,8 @@ mod dense { let pipeline = parse(IF_DEAD_RESULT); let (stage, cfg) = source_cfg(&pipeline, "if_dead"); let dense = analyze_dense_toy(&pipeline, stage, cfg); + let scope = (stage, Body::CFG(cfg)); + let point = |item| Scoped::new(scope, item); let cond = entry_params(&pipeline, cfg)[0]; let a = constant_result(&pipeline, cfg, 1); @@ -1321,26 +1323,35 @@ mod dense { // Classic: after `%a = constant 1`, %a is live (the yield uses it) // and %cond flows through the arm; before it, %a is killed. - assert_eq!(dense.live_after(a_const), Some(&live_set(&[cond, a]))); - assert_eq!(dense.live_before(a_const), Some(&live_set(&[cond]))); + assert_eq!( + dense.point_facts(point(ProgramPoint::After(a_const))), + Some(&live_set(&[cond, a])) + ); + assert_eq!( + dense.point_facts(point(ProgramPoint::Before(a_const))), + Some(&live_set(&[cond])) + ); // The if's dead result is not live after it because nothing uses it; // before it, only the condition survives the arm join. let if_stmt = find_statement(&pipeline, cfg, |definition| { matches!(definition, HighLevel::Structured(_)) }); - assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond]))); + assert_eq!( + dense.point_facts(point(ProgramPoint::Before(if_stmt))), + Some(&live_set(&[cond])) + ); let then_block = find_value(&pipeline, cfg, |definition| match definition { HighLevel::Structured(StructuredControlFlow::If(if_op)) => Some(if_op.then_block()), _ => None, }); assert_eq!( - dense.point_facts(ProgramPoint::BlockEntry(then_block)), + dense.point_facts(point(ProgramPoint::BlockEntry(then_block))), Some(&live_set(&[cond])) ); assert_eq!( - dense.point_facts(ProgramPoint::BlockExit(then_block)), + dense.point_facts(point(ProgramPoint::BlockExit(then_block))), Some(&live_set(&[cond])) ); } @@ -1368,6 +1379,7 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 { let pipeline = parse(IF_ARMS_DIFFERENT_USES); let (stage, cfg) = source_cfg(&pipeline, "if_arms"); let dense = analyze_dense_toy(&pipeline, stage, cfg); + let scope = (stage, Body::CFG(cfg)); let params = entry_params(&pipeline, cfg); let (cond, x, y) = (params[0], params[1], params[2]); @@ -1384,8 +1396,14 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 { // After the if only its result matters; before it, the then-arm // contributed %x, the else-arm %y, and the rule genned %cond. - assert_eq!(dense.live_after(if_stmt), Some(&live_set(&[r]))); - assert_eq!(dense.live_before(if_stmt), Some(&live_set(&[cond, x, y]))); + assert_eq!( + dense.point_facts(Scoped::new(scope, ProgramPoint::After(if_stmt))), + Some(&live_set(&[r])) + ); + assert_eq!( + dense.point_facts(Scoped::new(scope, ProgramPoint::Before(if_stmt))), + Some(&live_set(&[cond, x, y])) + ); } /// The scf.for dense frame iterates the body walk to the loop-carried @@ -1397,6 +1415,7 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 { let pipeline = parse(FOR_CARRIED_DEMAND); let (stage, cfg) = source_cfg(&pipeline, "loop_sum"); let dense = analyze_dense_toy(&pipeline, stage, cfg); + let scope = (stage, Body::CFG(cfg)); let params = entry_params(&pipeline, cfg); let (lo, hi, step) = (params[0], params[1], params[2]); @@ -1423,9 +1442,12 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 { }); // Around the loop. - assert_eq!(dense.live_after(for_stmt), Some(&live_set(&[sum]))); assert_eq!( - dense.live_before(for_stmt), + dense.point_facts(Scoped::new(scope, ProgramPoint::After(for_stmt))), + Some(&live_set(&[sum])) + ); + assert_eq!( + dense.point_facts(Scoped::new(scope, ProgramPoint::Before(for_stmt))), Some(&live_set(&[lo, hi, step, init])) ); @@ -1433,11 +1455,11 @@ specialize @source fn @if_arms(i64, i64, i64) -> i64 { // constant are live before the add; the yield slot is live after it // (it feeds the next iteration through the carry). assert_eq!( - dense.live_before(add_stmt), + dense.point_facts(Scoped::new(scope, ProgramPoint::Before(add_stmt))), Some(&live_set(&[lo, hi, step, init, acc, one])) ); assert_eq!( - dense.live_after(add_stmt), + dense.point_facts(Scoped::new(scope, ProgramPoint::After(add_stmt))), Some(&live_set(&[lo, hi, step, init, next])) ); } diff --git a/example/toy-lang/src/main.rs b/example/toy-lang/src/main.rs index 1a39465b41..7c29f843d6 100644 --- a/example/toy-lang/src/main.rs +++ b/example/toy-lang/src/main.rs @@ -5,6 +5,7 @@ mod stage; use clap::{Parser, Subcommand}; use kirin::prelude::*; use kirin::pretty::PipelinePrintExt; +use kirin_interpreter::{Body, ProgramPoint, Scoped}; use stage::Stage; @@ -108,10 +109,25 @@ fn run_program( } if liveness { - let dense = interpreter::analyze_classic_liveness(&pipeline, stage_name, func_name)?; - let mut boundaries: Vec<_> = dense - .blocks() - .map(|(block, live_in, live_out)| format!("{block:?}: in={live_in:?} out={live_out:?}")) + let (stage, cfg, dense) = + interpreter::analyze_classic_liveness(&pipeline, stage_name, func_name)?; + let blocks: Vec<_> = match pipeline + .stage(stage) + .ok_or_else(|| anyhow::anyhow!("resolved stage is missing"))? + { + Stage::Source(info) => cfg.blocks(info).collect(), + Stage::Lowered(info) => cfg.blocks(info).collect(), + }; + let scope = (stage, Body::CFG(cfg)); + let mut boundaries: Vec<_> = blocks + .into_iter() + .filter_map(|block| { + let live_in = + dense.point_facts(Scoped::new(scope, ProgramPoint::BlockEntry(block)))?; + let live_out = + dense.point_facts(Scoped::new(scope, ProgramPoint::BlockExit(block)))?; + Some(format!("{block:?}: in={live_in:?} out={live_out:?}")) + }) .collect(); boundaries.sort(); for line in boundaries { From 5d01225b3f5f36f2a6dd569cbfefc66196b5a798 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Wed, 12 Aug 2026 12:52:12 -0400 Subject: [PATCH 18/21] Refactor block predecessor handling to use SmallVec for improved memory efficiency --- crates/kirin-interpreter/src/core/query.rs | 2 +- crates/kirin-ir/src/builder/block.rs | 4 +++- crates/kirin-ir/src/node/block.rs | 11 +++++++++-- crates/kirin-ir/tests/builder_block.rs | 6 +++--- crates/kirin-liveness/tests/cfg.rs | 1 + 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index 8c8e35ed25..c8ec626be7 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -538,7 +538,7 @@ impl StageQuery for S where + SupportsStageDispatch { } - +/// TODO: add caching (with red-green tree) to avoid repeated queries for the same stage and block/statement. /// Run a stage action against the stage with id `stage`. pub(crate) fn dispatch( pipeline: &Pipeline, diff --git a/crates/kirin-ir/src/builder/block.rs b/crates/kirin-ir/src/builder/block.rs index 63449c1b9e..6c28adb9e5 100644 --- a/crates/kirin-ir/src/builder/block.rs +++ b/crates/kirin-ir/src/builder/block.rs @@ -1,3 +1,5 @@ +use smallvec::SmallVec; + use crate::node::ssa::{BuilderSSAInfo, BuilderSSAKind, ResolutionInfo, SSAValue}; use crate::node::stmt::StatementParent; use crate::node::*; @@ -186,7 +188,7 @@ impl<'a, L: Dialect> BlockBuilder<'a, L> { .maybe_name(self.name.map(|n| self.stage.symbols.intern(n))) .node(LinkedListNode::new(id)) .arguments(block_args) - .predecessors(Vec::new()) + .predecessors(SmallVec::new()) .statements(self.stage.link_statements(&self.statements)) .maybe_terminator(self.terminator) .new(); diff --git a/crates/kirin-ir/src/node/block.rs b/crates/kirin-ir/src/node/block.rs index e6796738ce..1e348603d2 100644 --- a/crates/kirin-ir/src/node/block.rs +++ b/crates/kirin-ir/src/node/block.rs @@ -1,3 +1,5 @@ +use smallvec::SmallVec; + use crate::{ Dialect, Symbol, arena::{GetInfo, Id, Item}, @@ -67,7 +69,12 @@ pub struct BlockInfo { pub arguments: Vec, /// Reverse control-flow index: blocks whose terminators may transfer /// control to this block. - pub predecessors: Vec, + /// + /// Inline capacity 4: a straight-line block has one predecessor, an + /// if-merge or loop header has two, and a small switch join or a loop + /// with a couple of `break`s stays under four. Wider joins spill to the + /// heap rather than making every block pay for the worst case. + pub predecessors: SmallVec<[Block; 4]>, pub statements: LinkedList, pub terminator: Option, _marker: std::marker::PhantomData, @@ -86,7 +93,7 @@ impl BlockInfo { /// The arguments of this block. arguments: Vec, /// The predecessor blocks in the reverse control-flow index. - predecessors: Vec, + predecessors: SmallVec<[Block; 4]>, /// The statements contained in this block. statements: Option>, /// The terminator statement of this block, if any. diff --git a/crates/kirin-ir/tests/builder_block.rs b/crates/kirin-ir/tests/builder_block.rs index 4b27af03a4..f1c06bb991 100644 --- a/crates/kirin-ir/tests/builder_block.rs +++ b/crates/kirin-ir/tests/builder_block.rs @@ -332,8 +332,8 @@ fn finalize_populates_block_predecessor_index() { let stage = stage.finalize().unwrap(); assert_eq!( - target.expect_info(&stage).predecessors, - vec![source0, source1] + target.expect_info(&stage).predecessors.as_slice(), + [source0, source1] ); assert!(source0.expect_info(&stage).predecessors.is_empty()); assert!(source1.expect_info(&stage).predecessors.is_empty()); @@ -352,7 +352,7 @@ fn predecessor_index_deduplicates_edges_from_the_same_block() { let _cfg = stage.cfg().add_block(source).add_block(target).new(); let stage = stage.finalize().unwrap(); - assert_eq!(target.expect_info(&stage).predecessors, vec![source]); + assert_eq!(target.expect_info(&stage).predecessors.as_slice(), [source]); } #[test] diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 529ae27544..865a4f13e5 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -49,6 +49,7 @@ fn parse(program: &str) -> Pipeline> { } /// The finalized stage id and the body cfg of `@main`. +/// TODO: `analyze` method is wrong, calling demand analysis. fn main_cfg( pipeline: &Pipeline>, ) -> (kirin::prelude::CompileStage, kirin_ir::CFG) { From 27e901f0215d845a6130448df6e85d10110e6732 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Wed, 12 Aug 2026 19:01:34 -0400 Subject: [PATCH 19/21] Changed to use SmallVec. Liveness Analysis now uses block cursor: LastStatement + PreviousStatement --- crates/kirin-interpreter/src/core/mod.rs | 2 +- crates/kirin-interpreter/src/core/query.rs | 171 ++++++++++++------ .../src/engines/dense_backward/frames.rs | 48 ++--- .../src/engines/dense_backward/interp.rs | 36 +++- .../src/engines/sparse_backward/interp.rs | 7 +- crates/kirin-interpreter/src/lib.rs | 2 +- crates/kirin-scf/src/interpreter.rs | 5 +- 7 files changed, 175 insertions(+), 96 deletions(-) diff --git a/crates/kirin-interpreter/src/core/mod.rs b/crates/kirin-interpreter/src/core/mod.rs index 5dc2577d6d..b05588c4cf 100644 --- a/crates/kirin-interpreter/src/core/mod.rs +++ b/crates/kirin-interpreter/src/core/mod.rs @@ -24,5 +24,5 @@ pub use frame::{ }; pub use interp::{AbstractInterpreter, Env, Interp, InterpLocation, SparseForwardInterp}; pub use linker::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; -pub use query::{GraphWalkPlan, StageQuery}; +pub use query::{GraphWalkPlan, StageQuery, TerminatorArgs}; pub use value::{BranchCondition, HasProductValue, expect_single}; diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index c8ec626be7..1d6cc98f91 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -6,15 +6,26 @@ //! kirin-ir's `StageDispatch` machinery; [`StageQuery`] bundles them into one //! bound that any well-formed stage enum satisfies automatically. +use crate::Body; +use crate::InterpreterError; use kirin_ir::{ Block, BlockParent, CFG, CompileStage, Dialect, GetInfo, HasArguments, HasBlocks, HasCFG, HasDigraphs, HasStageInfo, HasUngraphs, Pipeline, PortParent, SSAKind, SSAValue, SpecializedFunction, StageAction, StageInfo, StageMeta, StagedFunction, Statement, SupportsStageDispatch, Symbol, UniqueLiveSpecializationError, }; +use smallvec::{SmallVec, smallvec}; -use crate::Body; -use crate::InterpreterError; +/// Operands yielded by a structured body's terminator. +/// +/// Most structured operations yield zero, one, or two values. Wider products +/// remain supported by spilling to the heap. +pub type TerminatorArgs = SmallVec<[SSAValue; 2]>; + +/// Statements that can translate demand on a block argument. +/// +/// Capacity matches [`kirin_ir::BlockInfo`]'s inline predecessor capacity. +pub(crate) type BlockArgumentPredecessorStatements = SmallVec<[Statement; 4]>; /// Block parameters as SSA values. pub struct BlockParams(pub Block); @@ -45,28 +56,39 @@ where } } -/// A block's statements in program order, with the terminator last. -pub struct BlockStatements(pub Block); +/// First statement of a block (head of the statement list, or the cached +/// terminator for terminator-only blocks). +pub struct FirstStatement(pub Block); -impl BlockStatements { - fn collect(&self, info: &StageInfo) -> Result, InterpreterError> { - self.0 - .get_info(info) - .ok_or(InterpreterError::MissingBlock(self.0))?; - let mut statements: Vec = self.0.statements(info).collect(); - if let Some(terminator) = self.0.terminator(info) { - statements.push(terminator); - } - Ok(statements) +impl StageAction for FirstStatement +where + S: StageMeta + HasStageInfo, + L: Dialect, +{ + type Output = Option; + type Error = InterpreterError; + + fn run( + &mut self, + _stage: CompileStage, + info: &StageInfo, + ) -> Result { + Ok(self.0.first_statement(info)) } } -impl StageAction for BlockStatements +/// Statement after `after` within `block`, ending with the terminator. +pub struct NextStatement { + pub block: Block, + pub after: Statement, +} + +impl StageAction for NextStatement where S: StageMeta + HasStageInfo, L: Dialect, { - type Output = Vec; + type Output = Option; type Error = InterpreterError; fn run( @@ -74,15 +96,21 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - self.collect(info) + match *self.after.next(info) { + Some(next) => Ok(Some(next)), + None if self.block.last_statement(info) != Some(self.after) => { + Ok(self.block.last_statement(info)) + } + None => Ok(None), + } } } -/// First statement of a block (head of the statement list, or the cached -/// terminator for terminator-only blocks). -pub struct FirstStatement(pub Block); +/// Last statement of a block (the cached terminator when present, otherwise +/// the tail of the statement list). +pub struct LastStatement(pub Block); -impl StageAction for FirstStatement +impl StageAction for LastStatement where S: StageMeta + HasStageInfo, L: Dialect, @@ -95,17 +123,21 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - Ok(self.0.first_statement(info)) + self.0 + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.0))?; + Ok(self.0.last_statement(info)) } } -/// Statement after `after` within `block`, ending with the terminator. -pub struct NextStatement { +/// Statement before `before` within `block`, starting after the cached +/// terminator and then walking the statement list backwards. +pub struct PreviousStatement { pub block: Block, - pub after: Statement, + pub before: Statement, } -impl StageAction for NextStatement +impl StageAction for PreviousStatement where S: StageMeta + HasStageInfo, L: Dialect, @@ -118,12 +150,13 @@ where _stage: CompileStage, info: &StageInfo, ) -> Result { - match *self.after.next(info) { - Some(next) => Ok(Some(next)), - None if self.block.last_statement(info) != Some(self.after) => { - Ok(self.block.last_statement(info)) - } - None => Ok(None), + self.block + .get_info(info) + .ok_or(InterpreterError::MissingBlock(self.block))?; + if self.block.terminator(info) == Some(self.before) { + Ok(self.block.statements(info).next_back()) + } else { + Ok(*self.before.prev(info)) } } } @@ -287,7 +320,7 @@ where L: Dialect, for<'a> L: HasArguments<'a>, { - type Output = Vec; + type Output = TerminatorArgs; type Error = InterpreterError; fn run( @@ -303,7 +336,7 @@ where .definition(info) .arguments() .copied() - .collect::>() + .collect::() }) .unwrap_or_default()) } @@ -321,7 +354,7 @@ where S: StageMeta + HasStageInfo, L: Dialect, { - type Output = Vec; + type Output = BlockArgumentPredecessorStatements; type Error = InterpreterError; fn run( @@ -335,7 +368,7 @@ where .ok_or(InterpreterError::MissingBlock(self.0))?; match block.parent { - Some(BlockParent::Statement(owner)) => Ok(vec![owner]), + Some(BlockParent::Statement(owner)) => Ok(smallvec![owner]), Some(BlockParent::CFG(_)) => block .predecessors .iter() @@ -345,7 +378,7 @@ where )) }) .collect(), - None => Ok(Vec::new()), + None => Ok(SmallVec::new()), } } } @@ -432,7 +465,16 @@ where Vec::new(), cfg.blocks(info).map(Body::Block).collect::>(), ), - Body::Block(block) => (BlockStatements(block).collect(info)?, Vec::new()), + Body::Block(block) => { + block + .get_info(info) + .ok_or(InterpreterError::MissingBlock(block))?; + let mut statements: Vec = block.statements(info).collect(); + if let Some(terminator) = block.terminator(info) { + statements.push(terminator); + } + (statements, Vec::new()) + } Body::DiGraph(graph) => ( graph .expect_info(info) @@ -496,9 +538,10 @@ where pub trait StageQuery: StageMeta + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch< UniqueSpecialization, @@ -507,9 +550,12 @@ pub trait StageQuery: > + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch - + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch + + SupportsStageDispatch< + BlockArgumentPredecessors, + BlockArgumentPredecessorStatements, + InterpreterError, + > + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch @@ -519,9 +565,10 @@ pub trait StageQuery: impl StageQuery for S where S: StageMeta + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch< UniqueSpecialization, @@ -530,9 +577,12 @@ impl StageQuery for S where > + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch - + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch + + SupportsStageDispatch + + SupportsStageDispatch< + BlockArgumentPredecessors, + BlockArgumentPredecessorStatements, + InterpreterError, + > + SupportsStageDispatch + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch @@ -563,14 +613,6 @@ pub(crate) fn block_params( dispatch(pipeline, stage, BlockParams(block)) } -pub(crate) fn block_statements( - pipeline: &Pipeline, - stage: CompileStage, - block: Block, -) -> Result, InterpreterError> { - dispatch(pipeline, stage, BlockStatements(block)) -} - pub(crate) fn first_statement( pipeline: &Pipeline, stage: CompileStage, @@ -588,6 +630,23 @@ pub(crate) fn next_statement( dispatch(pipeline, stage, NextStatement { block, after }) } +pub(crate) fn last_statement( + pipeline: &Pipeline, + stage: CompileStage, + block: Block, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, LastStatement(block)) +} + +pub(crate) fn previous_statement( + pipeline: &Pipeline, + stage: CompileStage, + block: Block, + before: Statement, +) -> Result, InterpreterError> { + dispatch(pipeline, stage, PreviousStatement { block, before }) +} + pub(crate) fn cfg_entry( pipeline: &Pipeline, stage: CompileStage, @@ -632,7 +691,7 @@ pub(crate) fn terminator_arguments( pipeline: &Pipeline, stage: CompileStage, block: Block, -) -> Result, InterpreterError> { +) -> Result { dispatch(pipeline, stage, TerminatorArguments(block)) } @@ -640,7 +699,7 @@ pub(crate) fn block_argument_predecessors( pipeline: &Pipeline, stage: CompileStage, block: Block, -) -> Result, InterpreterError> { +) -> Result { dispatch(pipeline, stage, BlockArgumentPredecessors(block)) } diff --git a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs index d5baecf740..f2ef4c4502 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/frames.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/frames.rs @@ -37,10 +37,14 @@ pub enum DenseBlockMode { pub struct DenseBlockFrame { stage: CompileStage, block: Block, - /// Materialized on the first step (needs the driver). - statements: Option>, - /// Number of statements not yet walked (walks from the end). - remaining: usize, + /// Current position in the reverse statement walk. + cursor: Option, + /// `false` until the cursor has been positioned at the block's last + /// logical statement. + initialized: bool, + /// `true` only while visiting the first reverse position (the block's + /// logical terminator position). + at_block_exit: bool, mode: DenseBlockMode, /// The absorbed edge mapping (`CFGOwner` only). live_out: Option, @@ -59,8 +63,9 @@ where Self { stage, block, - statements: None, - remaining: 0, + cursor: None, + initialized: false, + at_block_exit: false, mode, live_out: None, pending_point: None, @@ -92,21 +97,17 @@ where mut self, interp: &mut I, ) -> Result>, E> { - let statements = match self.statements.as_ref() { - Some(statements) => statements, - None => { - if self.mode == DenseBlockMode::StructuredBody { - let facts = interp.state(); - interp.record_point(ProgramPoint::BlockExit(self.block), facts); - } - let statements = interp.block_statements(self.stage, self.block)?; - self.remaining = statements.len(); - self.statements.insert(statements) + if !self.initialized { + if self.mode == DenseBlockMode::StructuredBody { + let facts = interp.state(); + interp.record_point(ProgramPoint::BlockExit(self.block), facts); } - }; - let total = statements.len(); + self.cursor = interp.last_statement(self.stage, self.block)?; + self.initialized = true; + self.at_block_exit = true; + } - if self.remaining == 0 { + let Some(statement) = self.cursor else { let live_in = interp.state(); if self.mode == DenseBlockMode::StructuredBody { interp.record_point(ProgramPoint::BlockEntry(self.block), live_in.clone()); @@ -118,12 +119,11 @@ where }, DenseBlockMode::StructuredBody => DenseBackwardCompletion::Structured, })); - } + }; - let index = self.remaining - 1; - let is_terminator_position = self.remaining == total; - let statement = self.statements.as_ref().expect("materialized")[index]; - self.remaining = index; + let is_terminator_position = self.at_block_exit; + self.cursor = interp.previous_statement(self.stage, self.block, statement)?; + self.at_block_exit = false; let after = interp.state(); interp.record_point(ProgramPoint::After(statement), after); diff --git a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs index 62ce428db2..3a59968e36 100644 --- a/crates/kirin-interpreter/src/engines/dense_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/dense_backward/interp.rs @@ -60,7 +60,7 @@ use crate::{ AbstractInterpreter, BackwardSummaryDeps, ClassicLiveness, DenseBackwardSemantic, EnvIndex, FactStore, FixpointProfile, Frame, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, ProgramPoint, Scoped, StageQuery, StandardFixpointInterpreter, Summary, - SummaryDependency, SummaryDependencyIndex, SummaryEffect, + SummaryDependency, SummaryDependencyIndex, SummaryEffect, TerminatorArgs, }; // =========================================================================== @@ -401,12 +401,21 @@ pub trait DenseBackwardFrameEngine: Interp Result; - /// A block's statements in program order (terminator, if any, last). - fn block_statements( + /// The last logical statement of a block (the terminator when present). + fn last_statement( &self, stage: CompileStage, block: Block, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; + + /// The statement immediately before `before` in the block's logical + /// statement order. + fn previous_statement( + &self, + stage: CompileStage, + block: Block, + before: Statement, + ) -> Result, Self::Error>; /// The parameters of `block` (structured frames map carried demand). fn block_params(&self, stage: CompileStage, block: Block) @@ -417,7 +426,7 @@ pub trait DenseBackwardFrameEngine: Interp Result, Self::Error>; + ) -> Result; /// The current point state (cloned). fn state(&self) -> Self::Value; @@ -473,15 +482,24 @@ where result } - fn block_statements(&self, stage: CompileStage, block: Block) -> Result, E> { - query::block_statements(self.inner().pipeline(), stage, block).map_err(E::from) + fn last_statement(&self, stage: CompileStage, block: Block) -> Result, E> { + query::last_statement(self.inner().pipeline(), stage, block).map_err(E::from) + } + + fn previous_statement( + &self, + stage: CompileStage, + block: Block, + before: Statement, + ) -> Result, E> { + query::previous_statement(self.inner().pipeline(), stage, block, before).map_err(E::from) } fn block_params(&self, stage: CompileStage, block: Block) -> Result, E> { query::block_params(self.inner().pipeline(), stage, block).map_err(E::from) } - fn terminator_args(&self, stage: CompileStage, block: Block) -> Result, E> { + fn terminator_args(&self, stage: CompileStage, block: Block) -> Result { query::terminator_arguments(self.inner().pipeline(), stage, block).map_err(E::from) } @@ -705,7 +723,7 @@ where /// The blocks directly selected as fixpoint owners for `body`. /// /// This reads the current IR rather than returning cached analysis state. - pub fn direct_body_blocks( + fn direct_body_blocks( &self, stage: CompileStage, body: impl Into, diff --git a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs index c866df6074..961e7feca3 100644 --- a/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_backward/interp.rs @@ -58,7 +58,7 @@ use crate::{ AbstractInterpreter, Body, EnvIndex, FixpointProfile, Frame, FrameEffect, Interp, InterpDispatch, InterpLocation, InterpreterError, OwnerSemantics, OwnerSummaryDeps, Scoped, SparseBackwardSemantic, SparseStore, StageQuery, StandardFixpointInterpreter, StrongDemand, - Summary, SummaryEffect, + Summary, SummaryEffect, TerminatorArgs, }; /// The scope a body-level backward analysis qualifies its facts with. @@ -119,7 +119,7 @@ pub trait SparseBackwardInterp: fn block_params(&self, block: Block) -> Result, Self::Error>; /// The operands of `block`'s terminator — a structured body's yield slots. - fn terminator_args(&self, block: Block) -> Result, Self::Error>; + fn terminator_args(&self, block: Block) -> Result; } /// [`StrongDemand`]'s helper vocabulary on top of the shape-generic @@ -455,7 +455,7 @@ where query::block_params(self.inner().pipeline(), self.stage(), block).map_err(E::from) } - fn terminator_args(&self, block: Block) -> Result, E> { + fn terminator_args(&self, block: Block) -> Result { query::terminator_arguments(self.inner().pipeline(), self.stage(), block).map_err(E::from) } } @@ -501,6 +501,7 @@ where SSAKind::Result(statement, _) => vec![statement], SSAKind::BlockArgument(block, _) => { query::block_argument_predecessors(interp.inner().pipeline(), stage, block)? + .into_vec() } SSAKind::Port(parent, _) => vec![query::graph_port_owner( interp.inner().pipeline(), diff --git a/crates/kirin-interpreter/src/lib.rs b/crates/kirin-interpreter/src/lib.rs index 718c6bb9f1..34aa28bd52 100644 --- a/crates/kirin-interpreter/src/lib.rs +++ b/crates/kirin-interpreter/src/lib.rs @@ -74,7 +74,7 @@ pub use self::core::{BranchCondition, HasProductValue, expect_single}; pub use self::core::{CrossStageLinker, FunctionTarget, Linker, SameStageLinker}; pub use self::core::{EnvIndex, EnvStackStore, Store}; pub use self::core::{FunctionEntry, InterpDispatch, Interpretable}; -pub use self::core::{InterpreterError, StageQuery}; +pub use self::core::{InterpreterError, StageQuery, TerminatorArgs}; // 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 diff --git a/crates/kirin-scf/src/interpreter.rs b/crates/kirin-scf/src/interpreter.rs index 77a2e798ff..d245face6a 100644 --- a/crates/kirin-scf/src/interpreter.rs +++ b/crates/kirin-scf/src/interpreter.rs @@ -36,6 +36,7 @@ use kirin_interpreter::{ Completion, ConcreteInterpreter, DenseBackwardCompletion, DenseBackwardFrameEngine, DenseBackwardState, DenseBlockFrame, DenseFrameBuild, Env, EnvIndex, ForwardDataflowFrameEngine, Frame, FrameBuild, FrameEffect, FrameEngine, SparseForwardTransfer, + TerminatorArgs, }; use crate::{For, ForLoopValue, If, Yield}; @@ -380,7 +381,7 @@ pub struct DenseScfForFrame { /// Captured on the first step. seed: Option, params: Vec, - yields: Vec, + yields: TerminatorArgs, /// The current body-exit state estimate. entry: Option, _marker: PhantomData E>, @@ -393,7 +394,7 @@ impl DenseScfForFrame { body, seed: None, params: Vec::new(), - yields: Vec::new(), + yields: TerminatorArgs::new(), entry: None, _marker: PhantomData, } From f6439fc9e6a772cacc12c557acf123b41c697cb1 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Thu, 13 Aug 2026 10:37:53 -0400 Subject: [PATCH 20/21] Refactor CallContext trait to accept FunctionTarget instead of individual parameters --- crates/kirin-constprop/src/context.rs | 13 ++++--- crates/kirin-interpreter/src/core/query.rs | 4 +-- .../src/engines/sparse_forward/interp.rs | 35 ++++++------------- crates/kirin-liveness/src/lib.rs | 5 ++- crates/kirin-liveness/tests/cfg.rs | 3 +- example/toy-lang/src/interpreter/tests.rs | 5 +-- 6 files changed, 27 insertions(+), 38 deletions(-) diff --git a/crates/kirin-constprop/src/context.rs b/crates/kirin-constprop/src/context.rs index b62b6d42dc..1483488cbc 100644 --- a/crates/kirin-constprop/src/context.rs +++ b/crates/kirin-constprop/src/context.rs @@ -21,7 +21,9 @@ use std::collections::{HashMap, HashSet}; -use kirin_interpreter::{CallContext, ContextInsensitive, InterpreterError, WideningStrategy}; +use kirin_interpreter::{ + CallContext, ContextInsensitive, FunctionTarget, InterpreterError, WideningStrategy, +}; use kirin_ir::{CompileStage, Product, SpecializedFunction}; use crate::ConstPropValue; @@ -67,12 +69,9 @@ impl Default for ConstPropContext { impl CallContext for ConstPropContext { type Key = (CompileStage, SpecializedFunction, CallCtx); - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - args: &Product, - ) -> Self::Key { + fn key(&mut self, target: &FunctionTarget, args: &Product) -> Self::Key { + let stage = target.stage; + let function = target.function; let ctx = match all_const(args) { Some(consts) => { let admitted = self.admitted.entry((stage, function)).or_default(); diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index 1d6cc98f91..565e5ba848 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -588,8 +588,8 @@ impl StageQuery for S where + SupportsStageDispatch { } -/// TODO: add caching (with red-green tree) to avoid repeated queries for the same stage and block/statement. -/// Run a stage action against the stage with id `stage`. +// TODO: add caching (with red-green tree) to avoid repeated queries for the same stage and block/statement. +// Run a stage action against the stage with id `stage`. pub(crate) fn dispatch( pipeline: &Pipeline, stage: CompileStage, diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 8b5ca33f5b..6808fe7dab 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -61,12 +61,7 @@ use crate::{ pub trait CallContext { type Key: Clone + Eq + Hash; - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - args: &Product, - ) -> Self::Key; + fn key(&mut self, target: &FunctionTarget, args: &Product) -> Self::Key; } /// Explore/join strategy: combines an `incoming` abstract state into the @@ -96,13 +91,8 @@ impl Default for ContextInsensitive { impl CallContext for ContextInsensitive { type Key = (CompileStage, SpecializedFunction); - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - _args: &Product, - ) -> Self::Key { - (stage, function) + fn key(&mut self, target: &FunctionTarget, _args: &Product) -> Self::Key { + (target.stage, target.function) } } @@ -523,13 +513,8 @@ where } /// Key a resolved call target through the analysis. - fn key( - &mut self, - stage: CompileStage, - function: SpecializedFunction, - args: &Product, - ) ->

>::Key { - self.analysis.key(stage, function, args) + fn key(&mut self, target: &FunctionTarget, args: &Product) ->

>::Key { + self.analysis.key(target, args) } fn take_ret_acc(&mut self) -> Option> { @@ -924,7 +909,7 @@ where } = call; let resolve_stage = call_stage.unwrap_or(stage); let target = self.inner().resolve_call(resolve_stage, &callee)?; - let key = self.inner_mut().key(target.stage, target.function, &args); + let key = self.inner_mut().key(&target, &args); self.apply_update(ForwardUpdate::FunctionEntry { key: key.clone(), @@ -1603,10 +1588,7 @@ where ) -> Result, E> { let target = self.driver.inner().resolve_call(stage, &callee)?; let args: Product = args.into_iter().collect(); - let key = self - .driver - .inner_mut() - .key(target.stage, target.function, &args); + let key = self.driver.inner_mut().key(&target, &args); self.driver.apply_update(ForwardUpdate::FunctionEntry { key: key.clone(), @@ -1615,6 +1597,9 @@ where args, })?; + // TODO: Rename this, "semantics" is a bit misleading, sounds like the + // Semantic Keys, e.g. ForwardEval. + // Alternative: Rename the keys to: SparseForwardKey / SparseBackwardKey / DenseBackwardKey. let mut semantics = SparseForwardSemantics::new(); self.driver.drain_worklist(&mut semantics)?; diff --git a/crates/kirin-liveness/src/lib.rs b/crates/kirin-liveness/src/lib.rs index b2d9404ff4..fd76c16e69 100644 --- a/crates/kirin-liveness/src/lib.rs +++ b/crates/kirin-liveness/src/lib.rs @@ -42,7 +42,10 @@ pub type Demand<'ir, S, E = InterpreterError> = SparseBackwardInterpreter<'ir, S pub type DenseLiveness<'ir, S, E = InterpreterError, F = StandardDenseBackwardFrame> = DenseBackwardInterpreter<'ir, S, LiveSet, E, F>; -/// Run strong liveness (sparse backward demand) over `body` in `stage`. +/// Run strong liveness (sparse backward demand) over `body` in `stage`. TODO: +/// analyze() should accept Callee similar to concrete and constprop's +/// analyze(CompileStage, Callee, args) instead of Body, so that the caller can +/// select a specialization and pass its args. pub fn analyze_demand<'ir, S>( pipeline: &'ir Pipeline, stage: CompileStage, diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 865a4f13e5..50e134f7ae 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -49,7 +49,8 @@ fn parse(program: &str) -> Pipeline> { } /// The finalized stage id and the body cfg of `@main`. -/// TODO: `analyze` method is wrong, calling demand analysis. +// TODO: `analyze` method is wrong, calling demand analysis. +// TODO: `analyze` should use the same entry point. i.e. Callee not CFG/Body. fn main_cfg( pipeline: &Pipeline>, ) -> (kirin::prelude::CompileStage, kirin_ir::CFG) { diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index f9e2bb5ed0..313b268dc1 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -564,7 +564,8 @@ mod advanced { use std::hash::Hash; use kirin_constprop::{ConstPropContext, ConstPropValue}; - use kirin_interpreter::engine::{ + use kirin_interpreter::SameStageLinker; +use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, CFGFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, CrossStageLinker, DefaultBodyFrames, DiGraphFrame, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, @@ -717,7 +718,7 @@ mod advanced { let pipeline = build_pipeline(include_str!("../../programs/factorial.kirin")); let mut analysis = crate::interpreter::ToyConstProp::new(&pipeline) .with_policy(ConstPropContext::with_budget(2)) - .with_linker(CrossStageLinker); + .with_linker(SameStageLinker); let result = expect_single::( analysis .analyze_by_name("source", "factorial", [ConstPropValue::Const(5)]) From 22bcc7aa4734a0a714cb59c3773cf42fc1041cd7 Mon Sep 17 00:00:00 2001 From: Dennis Liew Date: Mon, 17 Aug 2026 12:01:37 -0400 Subject: [PATCH 21/21] added Symbol entry points for forward interpreter. Renamed function body to be function definition. Actual body terminology remains unchanged for CFG, Block, graphs, Function.body, Lambda.body, and CallableBody.body. --- AGENTS.md | 2 +- .../kirin-chumsky/src/function_text/error.rs | 6 +- .../src/function_text/parse_text.rs | 34 ++++---- .../kirin-chumsky/src/function_text/syntax.rs | 18 ++-- .../kirin-chumsky/src/function_text/tests.rs | 46 +++++----- crates/kirin-chumsky/src/tests.rs | 10 +-- crates/kirin-derive-chumsky/src/format.rs | 2 +- .../src/interp_dispatch.rs | 4 +- crates/kirin-derive-ir/src/lib.rs | 4 +- .../src/parse_dispatch.rs | 8 +- ...sts__parse_dispatch_duplicate_dialect.snap | 9 +- ...__tests__parse_dispatch_multi_dialect.snap | 9 +- .../src/{body.rs => function.rs} | 2 +- crates/kirin-function/src/lib.rs | 7 +- crates/kirin-interpreter/src/core/dispatch.rs | 8 +- crates/kirin-interpreter/src/core/effect.rs | 6 ++ crates/kirin-interpreter/src/core/frame.rs | 2 +- crates/kirin-interpreter/src/core/linker.rs | 8 +- crates/kirin-interpreter/src/core/query.rs | 28 +++--- .../src/engines/concrete/frames/call_frame.rs | 2 +- .../src/engines/concrete/interp.rs | 22 ++++- .../src/engines/sparse_forward/interp.rs | 46 ++++++---- crates/kirin-ir/src/builder/error.rs | 4 +- crates/kirin-ir/src/builder/redefine.rs | 2 +- crates/kirin-ir/src/builder/staged.rs | 6 +- crates/kirin-ir/src/language.rs | 2 +- crates/kirin-ir/src/node/function/mod.rs | 2 +- .../kirin-ir/src/node/function/specialized.rs | 19 +++-- crates/kirin-ir/src/pipeline.rs | 8 +- .../kirin-ir/src/signature/has_signature.rs | 10 +-- crates/kirin-ir/src/stage/info.rs | 8 +- crates/kirin-ir/tests/builder_staged.rs | 49 ++++++----- crates/kirin-liveness/tests/cfg.rs | 6 +- .../src/document/ir_render.rs | 10 +-- .../kirin-prettyless/src/tests/edge_cases.rs | 22 +++-- crates/kirin-prettyless/src/tests/impls.rs | 2 +- crates/kirin-prettyless/src/tests/mod.rs | 2 +- crates/kirin-prettyless/src/tests/pipeline.rs | 18 +++- .../src/tests/sprint_with_globals.rs | 2 +- docs/design/formalism/syntax.md | 2 +- docs/design/interpreter/index.md | 2 +- example/toy-lang/src/interpreter/mod.rs | 22 ++--- example/toy-lang/src/interpreter/tests.rs | 12 +-- example/toy-qc/src/circuit.rs | 2 +- example/toy-qc/src/zx.rs | 2 +- tests/body_kinds.rs | 85 +++++++++++++++++-- tests/frame_engine_capabilities.rs | 2 +- .../roundtrip/composable_existing_dialects.rs | 4 +- tests/roundtrip/digraph.rs | 20 ++--- tests/simple.rs | 2 +- 50 files changed, 378 insertions(+), 232 deletions(-) rename crates/kirin-function/src/{body.rs => function.rs} (89%) diff --git a/AGENTS.md b/AGENTS.md index 15104ea739..c6d54430ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,7 +153,7 @@ For user-defined dialects not in this table, ask the user for domain context dur - **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` 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. +- **Calling conventions are linkers**: `Linker` resolves `Callee` to a `(stage, specialization, definition)` 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` protocol — pop the top frame, `step_into`, apply the returned `FrameEffect`, owning no traversal logic. **One trait covers both roles**: a *member* (an individual walker) is generic over the total frame type `F` it composes into and names its successors in `F`; a *universe* (a language's total frame enum) implements `Frame` when it is the stack's element type but stays generic over `F`, so it can itself be embedded in a larger enum — `drive_frames` bounds on `F: Frame`. That is how `toy-lang`'s `TracingFrame` is a newtype wrapping `ToyFrame` whole rather than a copy of its variants. `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>` 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>` (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). diff --git a/crates/kirin-chumsky/src/function_text/error.rs b/crates/kirin-chumsky/src/function_text/error.rs index a7fef9087d..b347437c23 100644 --- a/crates/kirin-chumsky/src/function_text/error.rs +++ b/crates/kirin-chumsky/src/function_text/error.rs @@ -9,7 +9,7 @@ pub enum FunctionParseErrorKind { UnknownStage, InconsistentFunctionName, MissingStageDeclaration, - BodyParseFailed, + DefinitionParseFailed, EmitFailed, } @@ -24,7 +24,9 @@ impl Display for FunctionParseErrorKind { FunctionParseErrorKind::MissingStageDeclaration => { write!(f, "missing stage declaration") } - FunctionParseErrorKind::BodyParseFailed => write!(f, "function body parse failed"), + FunctionParseErrorKind::DefinitionParseFailed => { + write!(f, "function definition parse failed") + } FunctionParseErrorKind::EmitFailed => write!(f, "IR emission failed"), } } diff --git a/crates/kirin-chumsky/src/function_text/parse_text.rs b/crates/kirin-chumsky/src/function_text/parse_text.rs index 44edf13e96..4bf1c3d0f0 100644 --- a/crates/kirin-chumsky/src/function_text/parse_text.rs +++ b/crates/kirin-chumsky/src/function_text/parse_text.rs @@ -15,10 +15,10 @@ //! - collect `(stage, function) -> staged_function` mappings; //! - record offsets of `specialize` declarations for pass 2. //! -//! 2. **Pass 2 (specialize bodies)** +//! 2. **Pass 2 (specialization definitions)** //! - re-parse only the previously recorded `specialize` declarations; //! - resolve the target staged function from the pass-1 lookup; -//! - emit specialization bodies into the resolved stage dialect. +//! - emit specialization definitions into the resolved stage dialect. //! //! This separation guarantees that specialization emission sees a complete //! staged-function header set, which keeps behavior deterministic even when @@ -27,7 +27,7 @@ //! ## Why stage dispatch is central //! //! A pipeline can contain different dialects per stage (for example stage `A` -//! with `FunctionBody`, stage `B` with `LowerBody`). The parser does not guess +//! with `FunctionDefinition`, stage `B` with `LowerDefinition`). The parser does not guess //! which dialect to use from text alone. Instead it: //! //! - resolves/creates the stage symbol first (`@A`, `@B`, ...); @@ -39,7 +39,7 @@ //! //! ## Illustrative examples //! -//! Same-stage header + body: +//! Same-stage header + definition: //! //! ```text //! stage @A fn @foo(()) -> (); @@ -275,7 +275,7 @@ where let Declaration::Specialize { stage: _stage_sym, - body_span, + definition_span, span, } = declaration else { @@ -286,7 +286,7 @@ where )); }; - let body_text = &ctx.src[body_span.start..body_span.end]; + let definition_text = &ctx.src[definition_span.start..definition_span.end]; // Use the function name from parse_declaration_head (always available), // not from the chumsky Declaration (empty for dialect-controlled format). @@ -300,7 +300,7 @@ where stage_id, &function_name, ctx.function_symbol, - body_text, + definition_text, span, &mut *ctx.function_lookup, &mut *ctx.staged_lookup, @@ -607,7 +607,7 @@ fn apply_specialize_declaration( stage_id: CompileStage, function_name: &SymbolName<'_>, function_symbol: GlobalSymbol, - body_text: &str, + definition_text: &str, span: SimpleSpan, function_lookup: &mut FxHashMap, staged_lookup: &mut FxHashMap, @@ -617,14 +617,14 @@ where L: Dialect + ParseEmit + kirin_ir::HasSignature, L::Type: kirin_ir::Placeholder, { - // Parse and emit the body first — we need it to extract signature if needed - let body_statement = stage + // Parse and emit the definition first — we need it to extract the signature. + let definition = stage .with_builder(|builder| { let mut emit_ctx = EmitContext::new(builder); - L::parse_and_emit(body_text, &mut emit_ctx).map_err(|err| { + L::parse_and_emit(definition_text, &mut emit_ctx).map_err(|err| { let (kind, message) = match &err { crate::ChumskyError::Parse(errs) => ( - FunctionParseErrorKind::BodyParseFailed, + FunctionParseErrorKind::DefinitionParseFailed, errs.iter() .map(|e| e.to_string()) .collect::>() @@ -645,11 +645,11 @@ where ) })?; - // Get signature from HasSignature on the body statement. + // Get the signature from HasSignature on the definition statement. // The Signature field is populated by the statement parser from format string elements. - let def = body_statement.expect_info(stage).definition(); - let signature = def.signature().unwrap_or_else(|| { - // Fallback: create a placeholder signature if the body type + let definition_value = definition.expect_info(stage).definition(); + let signature = definition_value.signature().unwrap_or_else(|| { + // Fallback: create a placeholder signature if the definition type // doesn't carry one (e.g., no Signature field). kirin_ir::Signature::placeholder() }); @@ -673,7 +673,7 @@ where .specialize() .staged_func(staged_function) .signature(signature.clone()) - .body(body_statement) + .definition(definition) .new() .map_err(|err| { FunctionParseError::new( diff --git a/crates/kirin-chumsky/src/function_text/syntax.rs b/crates/kirin-chumsky/src/function_text/syntax.rs index 94110b3bfe..f89ff37a9d 100644 --- a/crates/kirin-chumsky/src/function_text/syntax.rs +++ b/crates/kirin-chumsky/src/function_text/syntax.rs @@ -24,8 +24,8 @@ pub(super) enum Declaration<'src, T> { Stage(Header<'src, T>), Specialize { stage: SymbolName<'src>, - /// Span of the body portion (from keyword through closing `}`). - body_span: SimpleSpan, + /// Span of the definition (from keyword through closing `}`). + definition_span: SimpleSpan, /// Span of the entire specialize declaration. span: SimpleSpan, }, @@ -69,11 +69,11 @@ where .labelled("function signature") } -/// Body span scanner. Matches an optional keyword prefix (e.g. `digraph`, +/// Definition span scanner. Matches an optional keyword prefix (e.g. `digraph`, /// `ungraph`) followed by a brace-balanced `{ ... }` CFG. Returns the /// span covering everything from the first non-brace token (or the opening -/// brace) through the matching closing brace. Does not parse body contents. -fn body_span<'src, I>() -> impl Parser<'src, I, SimpleSpan, ParserError<'src>> +/// brace) through the matching closing brace. Does not parse the definition. +fn definition_span<'src, I>() -> impl Parser<'src, I, SimpleSpan, ParserError<'src>> where I: TokenInput<'src>, { @@ -88,7 +88,7 @@ where None => { return Err(Rich::custom( input.span_since(&start), - "expected '{' in body", + "expected '{' in function definition", )); } } @@ -132,10 +132,10 @@ where // The function name is extracted post-parse from EmitContext::function_name(). let specialize_decl = identifier("specialize") .ignore_then(symbol()) - .then(body_span::()) // captures from keyword (e.g. `fn`) through closing `}` - .map_with(|(stage, body_span), extra| Declaration::Specialize { + .then(definition_span::()) // captures from keyword (e.g. `fn`) through closing `}` + .map_with(|(stage, definition_span), extra| Declaration::Specialize { stage, - body_span, + definition_span, span: extra.span(), }); diff --git a/crates/kirin-chumsky/src/function_text/tests.rs b/crates/kirin-chumsky/src/function_text/tests.rs index 0d55bf6483..d601cfa2be 100644 --- a/crates/kirin-chumsky/src/function_text/tests.rs +++ b/crates/kirin-chumsky/src/function_text/tests.rs @@ -88,7 +88,7 @@ trivial_type_lattice!(I32Type, "i32", just(Token::Identifier("i32"))); #[derive(Clone, Debug, PartialEq, Eq, Hash, kirin_ir::Dialect, HasParser, PrettyPrint)] #[kirin(builders, type = UnitType, crate = kirin_ir)] #[chumsky(crate = crate, format = "fn {:name}{sig} {body}")] -struct FunctionBody { +struct FunctionDefinition { body: CFG, sig: Signature, } @@ -109,9 +109,9 @@ struct LowerBody { #[stage(crate = "kirin_ir", chumsky_crate = "crate")] enum StageBucket { #[stage(name = "A")] - Parse(StageInfo), + Parse(StageInfo), #[stage(name = "B")] - Lower(StageInfo), + Lower(StageInfo), } // --------------------------------------------------------------------------- @@ -122,7 +122,7 @@ enum StageBucket { #[stage(crate = "kirin_ir", chumsky_crate = "crate")] enum MixedStage { #[stage(name = "A")] - StageA(StageInfo), + StageA(StageInfo), #[stage(name = "B")] StageB(StageInfo), } @@ -155,7 +155,7 @@ fn parsed_names(pipeline: &Pipeline, functions: Vec) -> BTreeSet #[test] fn test_pipeline_parse_accepts_mixed_function_names() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let input = format!( "stage @A fn @foo(()) -> (); specialize @A fn @foo(()) -> () {BODY} \ stage @B fn @bar(()) -> (); specialize @B fn @bar(()) -> () {BODY}" @@ -171,7 +171,7 @@ fn test_pipeline_parse_accepts_mixed_function_names() { #[test] fn test_pipeline_parse_uses_pipeline_global_table() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let input = format!("stage @A fn @foo(()) -> (); specialize @A fn @foo(()) -> () {BODY}"); let parsed = pipeline.parse(&input).unwrap(); @@ -217,14 +217,14 @@ fn test_stage_enum_pipeline_parse_suggests_declared_name() { #[test] fn test_stage_requires_semicolon() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let err = pipeline.parse("stage @A fn @foo(()) -> ()").unwrap_err(); assert_eq!(err.kind, crate::FunctionParseErrorKind::InvalidHeader); } #[test] fn test_specialize_requires_body() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let err = pipeline .parse("specialize @A fn @foo(()) -> ();") .unwrap_err(); @@ -233,7 +233,7 @@ fn test_specialize_requires_body() { #[test] fn test_global_symbol_prefix_is_required() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let err = pipeline.parse("stage 1 fn @foo(()) -> ();").unwrap_err(); assert_eq!(err.kind, crate::FunctionParseErrorKind::InvalidHeader); } @@ -242,7 +242,7 @@ fn test_global_symbol_prefix_is_required() { fn test_specialize_without_stage_auto_creates() { // With auto-creation, specialize without a prior stage declaration // succeeds by auto-creating the staged function. - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); pipeline .add_stage() .stage(StageInfo::default()) @@ -259,17 +259,17 @@ fn test_specialize_without_stage_auto_creates() { #[test] fn test_comments_and_whitespace_are_accepted() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let input = format!( "/* stage declaration */ stage @A fn @foo(()) -> (); \ - // specialization body\n specialize @A fn @foo(()) -> () /* body */ {BODY}" + // specialization definition\n specialize @A fn @foo(()) -> () /* definition */ {BODY}" ); pipeline.parse(&input).unwrap(); } #[test] fn test_pipeline_roundtrip_print_parse_print() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let stage_a = pipeline .add_stage() .stage(StageInfo::default()) @@ -287,18 +287,18 @@ fn test_pipeline_roundtrip_print_parse_print() { pipeline.stage_mut(stage_a).unwrap().with_builder(|b| { let block = b.block().new(); let cfg = b.cfg().add_block(block).new(); - let body = FunctionBody::new(b, cfg, Signature::new(vec![], UnitType, ())); + let definition = FunctionDefinition::new(b, cfg, Signature::new(vec![], UnitType, ())); b.specialize() .staged_func(staged_function) .signature(unit_sig()) - .body(body) + .definition(definition) .new() .unwrap(); }); let rendered = function.sprint(&pipeline); - let mut parsed_pipeline: Pipeline> = Pipeline::new(); + let mut parsed_pipeline: Pipeline> = Pipeline::new(); let parsed_functions = parsed_pipeline.parse(&rendered).unwrap(); let parsed_function = parsed_functions .into_iter() @@ -355,7 +355,7 @@ fn test_pipeline_parse_uses_stage_language_dispatch() { #[test] fn test_pipeline_parse_empty_input() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let err = pipeline.parse("").unwrap_err(); assert_eq!(err.kind, crate::FunctionParseErrorKind::InvalidHeader); assert!(err.message.contains("expected at least one declaration")); @@ -363,7 +363,7 @@ fn test_pipeline_parse_empty_input() { #[test] fn test_pipeline_parse_whitespace_only() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let err = pipeline.parse(" \n\t ").unwrap_err(); assert_eq!(err.kind, crate::FunctionParseErrorKind::InvalidHeader); } @@ -374,7 +374,7 @@ fn test_pipeline_parse_whitespace_only() { #[test] fn test_pipeline_parse_numeric_stage_symbol_rejected() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); // Numeric tokens like `1` are not prefixed with `@`, so `stage 1` should fail let err = pipeline.parse("stage 1 fn @foo(()) -> ();").unwrap_err(); assert_eq!(err.kind, crate::FunctionParseErrorKind::InvalidHeader); @@ -383,7 +383,7 @@ fn test_pipeline_parse_numeric_stage_symbol_rejected() { #[test] fn test_pipeline_numeric_stage_lookup_by_existing_id() { // When a stage already exists in the pipeline, @ can find it by raw ID - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let stage_id = pipeline .add_stage() .stage(StageInfo::default()) @@ -442,7 +442,7 @@ fn test_stage_suggestion_very_distant_name() { #[test] fn test_invalid_body_parse_has_source() { use std::error::Error; - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); // Valid header but invalid body tokens let err = pipeline .parse("stage @A fn @foo(()) -> (); specialize @A fn @foo(()) -> () { invalid }") @@ -458,7 +458,7 @@ fn test_invalid_body_parse_has_source() { #[test] fn test_duplicate_stage_declaration_same_signature() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let input = format!( "stage @A fn @foo(()) -> (); \ stage @A fn @foo(()) -> (); \ @@ -474,7 +474,7 @@ fn test_duplicate_stage_declaration_same_signature() { #[test] fn test_invalid_declaration_keyword() { - let mut pipeline: Pipeline> = Pipeline::new(); + let mut pipeline: Pipeline> = Pipeline::new(); let err = pipeline.parse("define @A fn @foo(()) -> ();").unwrap_err(); assert_eq!(err.kind, crate::FunctionParseErrorKind::InvalidHeader); } diff --git a/crates/kirin-chumsky/src/tests.rs b/crates/kirin-chumsky/src/tests.rs index c2cd207401..649706ef79 100644 --- a/crates/kirin-chumsky/src/tests.rs +++ b/crates/kirin-chumsky/src/tests.rs @@ -538,8 +538,8 @@ fn test_function_parse_error_kind_display() { "missing stage declaration" ); assert_eq!( - format!("{}", crate::FunctionParseErrorKind::BodyParseFailed), - "function body parse failed" + format!("{}", crate::FunctionParseErrorKind::DefinitionParseFailed), + "function definition parse failed" ); assert_eq!( format!("{}", crate::FunctionParseErrorKind::EmitFailed), @@ -559,7 +559,7 @@ fn test_function_parse_error_source() { // With source let source_err = std::io::Error::other("inner"); let err = crate::FunctionParseError::new( - crate::FunctionParseErrorKind::BodyParseFailed, + crate::FunctionParseErrorKind::DefinitionParseFailed, None, "outer", ) @@ -1117,7 +1117,7 @@ fn test_function_parse_error_all_kinds_display() { crate::FunctionParseErrorKind::UnknownStage, crate::FunctionParseErrorKind::InconsistentFunctionName, crate::FunctionParseErrorKind::MissingStageDeclaration, - crate::FunctionParseErrorKind::BodyParseFailed, + crate::FunctionParseErrorKind::DefinitionParseFailed, crate::FunctionParseErrorKind::EmitFailed, ]; let mut displays: Vec = kinds.iter().map(|k| format!("{k}")).collect(); @@ -1328,7 +1328,7 @@ fn test_function_parse_error_chained_source() { let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "not found"); let middle = crate::FunctionParseError::new( - crate::FunctionParseErrorKind::BodyParseFailed, + crate::FunctionParseErrorKind::DefinitionParseFailed, None, "body failed", ) diff --git a/crates/kirin-derive-chumsky/src/format.rs b/crates/kirin-derive-chumsky/src/format.rs index 3a5f477776..54154f87c8 100644 --- a/crates/kirin-derive-chumsky/src/format.rs +++ b/crates/kirin-derive-chumsky/src/format.rs @@ -77,7 +77,7 @@ //! // Quantum gate with multiple results: //! "$cnot {ctrl}, {tgt} -> {ctrl_out:type}, {tgt_out:type}" //! -//! // Function body with signature projections and context name: +//! // Function definition with signature projections and context name: //! "fn {:name}({sig:inputs}) -> {sig:return} ({body:ports}) captures ({body:captures}) {{ {body:body} }}" //! //! // Block field with args/body projections: diff --git a/crates/kirin-derive-interpreter/src/interp_dispatch.rs b/crates/kirin-derive-interpreter/src/interp_dispatch.rs index 7eb02631da..fcc3291822 100644 --- a/crates/kirin-derive-interpreter/src/interp_dispatch.rs +++ b/crates/kirin-derive-interpreter/src/interp_dispatch.rs @@ -87,7 +87,7 @@ pub fn generate(input: &DeriveInput) -> Result { let entry_arms = build_arms(&variants, enum_ident, |_| { quote! { #interp_crate::InterpDispatch::dispatch_function_entry( - stage_info, body, args, interp, + stage_info, definition, args, interp, ) } }); @@ -112,7 +112,7 @@ pub fn generate(input: &DeriveInput) -> Result { fn dispatch_function_entry( &self, - body: #ir_crate::Statement, + definition: #ir_crate::Statement, args: #ir_crate::Product<<__InterpI as #interp_crate::Interp>::Value>, interp: &mut __InterpI, ) -> Result< diff --git a/crates/kirin-derive-ir/src/lib.rs b/crates/kirin-derive-ir/src/lib.rs index 2886d3bcf5..1c2c12a58c 100644 --- a/crates/kirin-derive-ir/src/lib.rs +++ b/crates/kirin-derive-ir/src/lib.rs @@ -113,9 +113,9 @@ pub fn derive_stage_meta(input: TokenStream) -> TokenStream { /// #[stage(crate = "kirin_ir", chumsky_crate = "kirin_chumsky")] /// enum MixedStage { /// #[stage(name = "A")] -/// StageA(StageInfo), +/// StageA(StageInfo), /// #[stage(name = "B")] -/// StageB(StageInfo), +/// StageB(StageInfo), /// } /// ``` #[proc_macro_derive(ParseDispatch, attributes(stage))] diff --git a/crates/kirin-derive-toolkit/src/parse_dispatch.rs b/crates/kirin-derive-toolkit/src/parse_dispatch.rs index 72ebcf67a2..f8c447f4bf 100644 --- a/crates/kirin-derive-toolkit/src/parse_dispatch.rs +++ b/crates/kirin-derive-toolkit/src/parse_dispatch.rs @@ -169,9 +169,9 @@ mod tests { #[stage(crate = "kirin_ir", chumsky_crate = "kirin_chumsky")] enum MixedStage { #[stage(name = "A")] - StageA(StageInfo), + StageA(StageInfo), #[stage(name = "B")] - StageB(StageInfo), + StageB(StageInfo), } }; insta::assert_snapshot!(generate_parse_dispatch_code(input)); @@ -183,9 +183,9 @@ mod tests { #[stage(crate = "kirin_ir", chumsky_crate = "kirin_chumsky")] enum StageBucket { #[stage(name = "A")] - Parse(StageInfo), + Parse(StageInfo), #[stage(name = "B")] - Lower(StageInfo), + Lower(StageInfo), } }; insta::assert_snapshot!(generate_parse_dispatch_code(input)); diff --git a/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_duplicate_dialect.snap b/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_duplicate_dialect.snap index 47aedf9d29..2c28681bdc 100644 --- a/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_duplicate_dialect.snap +++ b/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_duplicate_dialect.snap @@ -1,6 +1,5 @@ --- source: crates/kirin-derive-toolkit/src/parse_dispatch.rs -assertion_line: 191 expression: generate_parse_dispatch_code(input) --- #[automatically_derived] @@ -15,11 +14,11 @@ impl kirin_chumsky::ParseDispatch for StageBucket { > { match self { StageBucket::Parse(stage_info) => { - kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } StageBucket::Lower(stage_info) => { - kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } } @@ -32,11 +31,11 @@ impl kirin_chumsky::ParseDispatch for StageBucket { { match self { StageBucket::Parse(stage_info) => { - kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } StageBucket::Lower(stage_info) => { - kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } } diff --git a/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_multi_dialect.snap b/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_multi_dialect.snap index ca59c440ee..0545ea675f 100644 --- a/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_multi_dialect.snap +++ b/crates/kirin-derive-toolkit/src/snapshots/kirin_derive_toolkit__parse_dispatch__tests__parse_dispatch_multi_dialect.snap @@ -1,6 +1,5 @@ --- source: crates/kirin-derive-toolkit/src/parse_dispatch.rs -assertion_line: 177 expression: generate_parse_dispatch_code(input) --- #[automatically_derived] @@ -15,11 +14,11 @@ impl kirin_chumsky::ParseDispatch for MixedStage { > { match self { MixedStage::StageA(stage_info) => { - kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } MixedStage::StageB(stage_info) => { - kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::first_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } } @@ -32,11 +31,11 @@ impl kirin_chumsky::ParseDispatch for MixedStage { { match self { MixedStage::StageA(stage_info) => { - kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } MixedStage::StageB(stage_info) => { - kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) + kirin_chumsky::second_pass_concrete::(stage_info, stage_id, ctx) .map(::core::option::Option::Some) } } diff --git a/crates/kirin-function/src/body.rs b/crates/kirin-function/src/function.rs similarity index 89% rename from crates/kirin-function/src/body.rs rename to crates/kirin-function/src/function.rs index baf0e1ecc7..00521be92b 100644 --- a/crates/kirin-function/src/body.rs +++ b/crates/kirin-function/src/function.rs @@ -1,6 +1,6 @@ use kirin::prelude::*; -/// Structural function-body statement used by function text parsing. +/// Structural function-definition statement used by function text parsing. /// /// The `sig` field stores the function's type signature (`(T, T) -> T`), /// parsed from the format string. `derive(Dialect)` generates `HasSignature` diff --git a/crates/kirin-function/src/lib.rs b/crates/kirin-function/src/lib.rs index 0dfd20a0db..8a41b1d11f 100644 --- a/crates/kirin-function/src/lib.rs +++ b/crates/kirin-function/src/lib.rs @@ -18,20 +18,17 @@ use kirin::prelude::*; use kirin_interpreter::{FunctionEntry, Interpretable}; pub mod bind; -pub mod body; pub mod call; +pub mod function; pub mod lambda; pub mod ret; pub use bind::Bind; -pub use body::Function; pub use call::{Call, CallFunction, CallLike, CallNamed, CallSpecialized, CallStaged}; +pub use function::Function; pub use lambda::Lambda; pub use ret::Return; -#[deprecated(note = "use Function")] -pub type FunctionBody = Function; - pub mod interpreter; #[cfg(test)] diff --git a/crates/kirin-interpreter/src/core/dispatch.rs b/crates/kirin-interpreter/src/core/dispatch.rs index 19f6428f87..bf2cb45934 100644 --- a/crates/kirin-interpreter/src/core/dispatch.rs +++ b/crates/kirin-interpreter/src/core/dispatch.rs @@ -51,7 +51,7 @@ pub trait InterpDispatch: StageMeta { fn dispatch_function_entry( &self, - body: Statement, + definition: Statement, args: Product, interp: &mut I, ) -> Result, I::Error>; @@ -73,11 +73,11 @@ where fn dispatch_function_entry( &self, - body: Statement, + definition: Statement, args: Product, interp: &mut I, ) -> Result, I::Error> { - let definition = body.definition(self).clone(); - definition.function_entry(args, interp) + let callable = definition.definition(self).clone(); + callable.function_entry(args, interp) } } diff --git a/crates/kirin-interpreter/src/core/effect.rs b/crates/kirin-interpreter/src/core/effect.rs index 87a1429454..866a3f22d6 100644 --- a/crates/kirin-interpreter/src/core/effect.rs +++ b/crates/kirin-interpreter/src/core/effect.rs @@ -125,6 +125,12 @@ pub enum Callee { Specialized(SpecializedFunction), } +impl From for Callee { + fn from(symbol: Symbol) -> Self { + Self::Named(symbol) + } +} + /// The body a callable statement enters when invoked, plus the entry /// arguments bound to its boundary (block parameters / graph ports). /// diff --git a/crates/kirin-interpreter/src/core/frame.rs b/crates/kirin-interpreter/src/core/frame.rs index 2f2b1cfa11..ca8eff8d68 100644 --- a/crates/kirin-interpreter/src/core/frame.rs +++ b/crates/kirin-interpreter/src/core/frame.rs @@ -321,7 +321,7 @@ pub trait CallServices: Env { fn enter_function( &mut self, stage: CompileStage, - body: Statement, + definition: Statement, args: Product, index: EnvIndex, ) -> Result, Self::Error>; diff --git a/crates/kirin-interpreter/src/core/linker.rs b/crates/kirin-interpreter/src/core/linker.rs index ea531e8a8c..24a30c63ce 100644 --- a/crates/kirin-interpreter/src/core/linker.rs +++ b/crates/kirin-interpreter/src/core/linker.rs @@ -4,12 +4,12 @@ use super::query; use crate::{Callee, InterpreterError, StageQuery}; /// A fully resolved call target: the stage to execute in, the specialization, -/// and its body statement. +/// and its callable definition statement. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct FunctionTarget { pub stage: CompileStage, pub function: SpecializedFunction, - pub body: Statement, + pub definition: Statement, } /// The calling-convention component of an engine. @@ -76,11 +76,11 @@ fn target_at_stage( Callee::Staged(staged) => query::unique_specialization(pipeline, stage, staged)?, Callee::Specialized(specialized) => specialized, }; - let body = query::function_body(pipeline, stage, specialized)?; + let definition = query::function_definition(pipeline, stage, specialized)?; Ok(FunctionTarget { stage, function: specialized, - body, + definition, }) } diff --git a/crates/kirin-interpreter/src/core/query.rs b/crates/kirin-interpreter/src/core/query.rs index 565e5ba848..401df3b985 100644 --- a/crates/kirin-interpreter/src/core/query.rs +++ b/crates/kirin-interpreter/src/core/query.rs @@ -262,10 +262,10 @@ where } } -/// Body statement of a specialized function. -pub struct FunctionBody(pub SpecializedFunction); +/// Definition statement of a specialized function. +pub struct FunctionDefinition(pub SpecializedFunction); -impl StageAction for FunctionBody +impl StageAction for FunctionDefinition where S: StageMeta + HasStageInfo, L: Dialect, @@ -281,8 +281,10 @@ where Ok(self .0 .get_info(info) - .map(|info| *info.body()) - .ok_or(InterpreterError::Custom("specialized function has no body"))) + .map(|info| *info.definition()) + .ok_or(InterpreterError::Custom( + "specialized function has no definition", + ))) } } @@ -535,8 +537,7 @@ where /// Satisfied automatically by any stage enum built from `StageInfo` /// variants (and by `StageInfo` itself for single-language pipelines); /// compiler authors never implement it by hand. -pub trait StageQuery: - StageMeta +pub trait StageQuery: StageMeta + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> @@ -547,7 +548,7 @@ pub trait StageQuery: UniqueSpecialization, Result, InterpreterError, - > + SupportsStageDispatch, InterpreterError> + > + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch @@ -574,8 +575,11 @@ impl StageQuery for S where UniqueSpecialization, Result, InterpreterError, - > + SupportsStageDispatch, InterpreterError> - + SupportsStageDispatch, InterpreterError> + > + SupportsStageDispatch< + FunctionDefinition, + Result, + InterpreterError, + > + SupportsStageDispatch, InterpreterError> + SupportsStageDispatch + SupportsStageDispatch + SupportsStageDispatch< @@ -663,12 +667,12 @@ pub(crate) fn unique_specialization( dispatch(pipeline, stage, UniqueSpecialization(staged))? } -pub(crate) fn function_body( +pub(crate) fn function_definition( pipeline: &Pipeline, stage: CompileStage, specialized: SpecializedFunction, ) -> Result { - dispatch(pipeline, stage, FunctionBody(specialized))? + dispatch(pipeline, stage, FunctionDefinition(specialized))? } pub(crate) fn resolve_symbol_name( 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 5d63080b50..8d49d62f8e 100644 --- a/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs +++ b/crates/kirin-interpreter/src/engines/concrete/frames/call_frame.rs @@ -123,7 +123,7 @@ where } => { let target = interp.resolve_call(resolve_stage, &callee)?; let index = interp.alloc_env(); - let entry = interp.enter_function(target.stage, target.body, args, index)?; + let entry = interp.enter_function(target.stage, target.definition, args, index)?; // The closed `Body` enum is the framework's supported body // vocabulary, so this match is intentionally exhaustive; // only the `UnGraph` arm delegates to a language policy. diff --git a/crates/kirin-interpreter/src/engines/concrete/interp.rs b/crates/kirin-interpreter/src/engines/concrete/interp.rs index be2437aeda..ff40f33735 100644 --- a/crates/kirin-interpreter/src/engines/concrete/interp.rs +++ b/crates/kirin-interpreter/src/engines/concrete/interp.rs @@ -1,6 +1,8 @@ use std::marker::PhantomData; -use kirin_ir::{Block, CFG, CompileStage, Pipeline, Product, SSAValue, StageMeta, Statement}; +use kirin_ir::{ + Block, CFG, CompileStage, Pipeline, Product, SSAValue, StageMeta, Statement, Symbol, +}; use crate::core::query; use crate::{ @@ -140,7 +142,7 @@ where fn enter_function( &mut self, stage: CompileStage, - body: Statement, + definition: Statement, args: Product, index: EnvIndex, ) -> Result, E> { @@ -150,10 +152,10 @@ where .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement: body, + statement: definition, index, }); - let result = info.dispatch_function_entry(body, args, self); + let result = info.dispatch_function_entry(definition, args, self); self.location = previous; result } @@ -266,6 +268,18 @@ where self.call(stage, Callee::Function(function), args) } + /// Resolve a stage-local `symbol` through the linker and execute the + /// selected callable to completion. This is the convenience form of + /// [`call`](Self::call) with [`Callee::Named`]. + pub fn call_by_symbol( + &mut self, + stage: CompileStage, + symbol: Symbol, + args: impl IntoIterator, + ) -> Result, E> { + self.call(stage, symbol.into(), args) + } + /// Execute a function to completion and return its return product. /// /// The root call is an ordinary [`CallFrame`]: the same call boundary diff --git a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs index 6808fe7dab..283c8bf999 100644 --- a/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs +++ b/crates/kirin-interpreter/src/engines/sparse_forward/interp.rs @@ -37,7 +37,7 @@ use std::marker::PhantomData; use kirin_ir::{ Block, CFG, CompileStage, DiGraph, HasBottom, Pipeline, Product, SSAValue, SpecializedFunction, - StageMeta, Statement, Widen, + StageMeta, Statement, Symbol, Widen, }; use crate::core::query; @@ -305,7 +305,7 @@ enum ForwardUpdate { FunctionEntry { key: K, stage: CompileStage, - body: Statement, + definition: Statement, args: Product, }, /// Merge a return contribution into a function context's return (join); on @@ -636,7 +636,7 @@ where fn enter_function( &mut self, stage: CompileStage, - body: Statement, + definition: Statement, args: Product, index: EnvIndex, ) -> Result, E> { @@ -646,10 +646,10 @@ where .ok_or_else(|| E::from(InterpreterError::MissingStage(stage)))?; let previous = self.location.replace(InterpLocation { stage, - statement: body, + statement: definition, index, }); - let result = info.dispatch_function_entry(body, args, self); + let result = info.dispatch_function_entry(definition, args, self); self.location = previous; result } @@ -776,11 +776,12 @@ where fn enter_function( &mut self, stage: CompileStage, - body: Statement, + definition: Statement, args: Product, index: EnvIndex, ) -> Result, E> { - self.inner_mut().enter_function(stage, body, args, index) + self.inner_mut() + .enter_function(stage, definition, args, index) } } @@ -914,7 +915,7 @@ where self.apply_update(ForwardUpdate::FunctionEntry { key: key.clone(), stage: target.stage, - body: target.body, + definition: target.definition, args, })?; @@ -964,7 +965,7 @@ where ForwardUpdate::FunctionEntry { key, stage, - body, + definition, args, } => { let owner = Owner::Function(key.clone()); @@ -972,7 +973,7 @@ where self.summaries_mut().insert( owner.clone(), ForwardSummary::Function(FunctionSummary { - meta: Some((stage, body)), + meta: Some((stage, definition)), entry: args, entry_joins: 0, ret: None, @@ -1004,7 +1005,7 @@ where changed }; if changed { - self.seed_entry_block(&key, stage, body)?; + self.seed_entry_block(&key, stage, definition)?; } Ok(()) } @@ -1133,7 +1134,7 @@ where &mut self, key: &

>::Key, stage: CompileStage, - body: Statement, + definition: Statement, ) -> Result<(), E> { let env = match self.store().env(key) { Some(env) => env, @@ -1148,8 +1149,8 @@ where .and_then(|info| info.as_function()) .map(|function| function.entry.clone()) .expect("function summary present"); - let body_info = self.enter_function(stage, body, entry_args, env)?; - let owner = match body_info.body { + let entry = self.enter_function(stage, definition, entry_args, env)?; + let owner = match entry.body { Body::CFG(cfg) => Owner::Block { function: key.clone(), block: self @@ -1182,7 +1183,7 @@ where } self.apply_update(ForwardUpdate::OwnerEntry { owner, - args: body_info.args, + args: entry.args, }) } } @@ -1578,6 +1579,19 @@ where self.analyze(stage, Callee::Function(function), args) } + /// Resolve a stage-local `symbol` through the linker and analyze the + /// selected callable. This is the convenience form of + /// [`analyze`](Self::analyze) with [`Callee::Named`], and returns its + /// inferred return product at the fixpoint. + pub fn analyze_by_symbol( + &mut self, + stage: CompileStage, + symbol: Symbol, + args: impl IntoIterator, + ) -> Result, E> { + self.analyze(stage, symbol.into(), args) + } + /// Run the fixpoint from a single entry. Seeds the entry function's entry block /// owner and drains the owner worklist. pub fn analyze( @@ -1593,7 +1607,7 @@ where self.driver.apply_update(ForwardUpdate::FunctionEntry { key: key.clone(), stage: target.stage, - body: target.body, + definition: target.definition, args, })?; diff --git a/crates/kirin-ir/src/builder/error.rs b/crates/kirin-ir/src/builder/error.rs index 8c1705f5cd..69a4a39a41 100644 --- a/crates/kirin-ir/src/builder/error.rs +++ b/crates/kirin-ir/src/builder/error.rs @@ -86,8 +86,8 @@ pub struct SpecializeError { pub signature: Signature, /// Existing non-invalidated specializations with matching signatures. pub conflicting: Vec, - /// Preserved body statement for the new specialization. - pub body: Statement, + /// Preserved definition statement for the new specialization. + pub definition: Statement, /// Preserved backedges for the new specialization. pub backedges: Option>, } diff --git a/crates/kirin-ir/src/builder/redefine.rs b/crates/kirin-ir/src/builder/redefine.rs index 2e6477267a..e83092bc22 100644 --- a/crates/kirin-ir/src/builder/redefine.rs +++ b/crates/kirin-ir/src/builder/redefine.rs @@ -34,7 +34,7 @@ impl BuilderStageInfo { let specialized_function = SpecializedFunctionInfo::builder() .id(id) .signature(error.signature) - .body(error.body) + .definition(error.definition) .maybe_backedges(error.backedges) .new(); staged_function_info diff --git a/crates/kirin-ir/src/builder/staged.rs b/crates/kirin-ir/src/builder/staged.rs index 775d0b1ae7..25fb9907df 100644 --- a/crates/kirin-ir/src/builder/staged.rs +++ b/crates/kirin-ir/src/builder/staged.rs @@ -255,7 +255,7 @@ impl BuilderStageInfo { &mut self, #[builder(name = staged_func)] func: StagedFunction, signature: Option>, - #[builder(into)] body: Statement, + #[builder(into)] definition: Statement, backedges: Option>, ) -> Result> { let staged_function_info = &mut self.staged_functions[func]; @@ -274,7 +274,7 @@ impl BuilderStageInfo { staged_function: func, signature, conflicting, - body, + definition, backedges, }); } @@ -284,7 +284,7 @@ impl BuilderStageInfo { let specialized_function = SpecializedFunctionInfo::builder() .id(id) .signature(signature) - .body(body) + .definition(definition) .maybe_backedges(backedges) .new(); staged_function_info diff --git a/crates/kirin-ir/src/language.rs b/crates/kirin-ir/src/language.rs index 4c8f4b333c..50fd1b6a24 100644 --- a/crates/kirin-ir/src/language.rs +++ b/crates/kirin-ir/src/language.rs @@ -75,7 +75,7 @@ pub trait HasUngraphsMut<'a> { /// Structural trait for dialect operations that have a single CFG body. /// /// This trait is intentionally not a supertrait of `Dialect` — it applies to -/// individual operations (e.g., `FunctionBody`, `Lambda`) that contain a single +/// individual operations (e.g., `Function`, `Lambda`) that contain a single /// `CFG`, not to the dialect enum itself. It enables shared helper functions /// for interpreter and analysis code that operate on CFG-bearing operations. pub trait HasCFGBody { diff --git a/crates/kirin-ir/src/node/function/mod.rs b/crates/kirin-ir/src/node/function/mod.rs index 8f87af4381..a71db283d6 100644 --- a/crates/kirin-ir/src/node/function/mod.rs +++ b/crates/kirin-ir/src/node/function/mod.rs @@ -20,7 +20,7 @@ //! //! - [`SpecializedFunction`] / [`SpecializedFunctionInfo`] — A concrete //! instantiation of a staged function for a particular (possibly narrower) -//! signature. Owns the IR body. Dispatch selects the most specific +//! signature. Owns the IR definition statement. Dispatch selects the most specific //! non-invalidated specialization via [`SignatureSemantics`]. //! //! Each level can be *invalidated* (staged or specialized) when the function is diff --git a/crates/kirin-ir/src/node/function/specialized.rs b/crates/kirin-ir/src/node/function/specialized.rs index 625704468a..3b02e70e7f 100644 --- a/crates/kirin-ir/src/node/function/specialized.rs +++ b/crates/kirin-ir/src/node/function/specialized.rs @@ -18,14 +18,15 @@ impl SpecializedFunction { /// A concrete instantiation of a staged function for a specific signature. /// /// The specialized signature is a subset of the parent [`StagedFunctionInfo`](super::staged::StagedFunctionInfo)'s -/// generic signature. This is the level that owns the IR [`body`](Self::body). +/// generic signature. This is the level that owns the IR +/// [`definition`](Self::definition). /// Like staged functions, specializations can be invalidated and are then /// excluded from dispatch while remaining available for backedge tracking. #[derive(Clone, Debug)] pub struct SpecializedFunctionInfo { id: SpecializedFunction, signature: Signature, - body: Statement, + definition: Statement, /// Functions that call this function (used for inter-procedural analyses). backedges: Vec, /// Whether this specialization has been invalidated by a redefinition. @@ -42,15 +43,15 @@ impl SpecializedFunctionInfo { id: SpecializedFunction, /// The signature of this specialized function. signature: Signature, - /// The body of this specialized function. - body: Statement, + /// The definition statement of this specialized function. + definition: Statement, /// The functions that call this specialized function. backedges: Option>, ) -> Self { Self { id, signature, - body, + definition, backedges: backedges.unwrap_or_default(), invalidated: false, } @@ -68,12 +69,12 @@ impl SpecializedFunctionInfo { self.id } - pub fn body(&self) -> &Statement { - &self.body + pub fn definition(&self) -> &Statement { + &self.definition } - pub fn body_mut(&mut self) -> &mut Statement { - &mut self.body + pub fn definition_mut(&mut self) -> &mut Statement { + &mut self.definition } pub fn return_type(&self) -> &L::Type { diff --git a/crates/kirin-ir/src/pipeline.rs b/crates/kirin-ir/src/pipeline.rs index 0c62533d21..37d34a1fac 100644 --- a/crates/kirin-ir/src/pipeline.rs +++ b/crates/kirin-ir/src/pipeline.rs @@ -361,7 +361,7 @@ impl Pipeline { /// /// This is a convenience shorthand for the common case of creating the full /// three-level function hierarchy (Function → StagedFunction → SpecializedFunction) - /// with a single body. + /// with a single definition. /// /// # Errors /// @@ -374,7 +374,7 @@ impl Pipeline { /// ```ignore /// let (func, sf, spec) = pipeline.define_function::() /// .stage(stage_id) - /// .body(body_stmt) + /// .definition(definition) /// .name("my_func") /// .signature(sig) /// .new() @@ -386,7 +386,7 @@ impl Pipeline { #[builder(into)] name: Option, stage: CompileStage, signature: Option>, - #[builder(into)] body: Statement, + #[builder(into)] definition: Statement, ) -> Result<(Function, StagedFunction, SpecializedFunction), PipelineStagedError> where S: HasStageInfo, @@ -409,7 +409,7 @@ impl Pipeline { // Omit signature — specialize defaults to the staged function's signature. let spec = stage_info - .with_builder(|b| b.specialize().staged_func(sf).body(body).new()) + .with_builder(|b| b.specialize().staged_func(sf).definition(definition).new()) .expect("specialization conflict on newly created staged function"); Ok((func, sf, spec)) diff --git a/crates/kirin-ir/src/signature/has_signature.rs b/crates/kirin-ir/src/signature/has_signature.rs index 87fc0fa20a..4e31868d2f 100644 --- a/crates/kirin-ir/src/signature/has_signature.rs +++ b/crates/kirin-ir/src/signature/has_signature.rs @@ -2,11 +2,11 @@ use crate::Dialect; use super::Signature; -/// Extract the function signature from a parsed function-body statement. +/// Extract the function signature from a parsed function-definition statement. /// -/// Implemented by dialect types that serve as function bodies (e.g., `FunctionBody`, -/// `CircuitFunction`). The framework calls this after parsing to construct the -/// `SpecializedFunction`. +/// Implemented by dialect types that serve as function definitions (e.g., +/// `Function`, `CircuitFunction`). The framework calls this after parsing to +/// construct the `SpecializedFunction`. /// /// With RFC 0004, the signature is a field on the statement type — `derive(Dialect)` /// generates this trait automatically. Types with a `Signature` field return @@ -17,6 +17,6 @@ use super::Signature; /// - `L`: The dialect whose `Type` is used in the signature. pub trait HasSignature { /// Returns the function signature from this statement, or `None` - /// if the type does not carry a signature (e.g. non-function-body statements). + /// if the type does not carry a signature (e.g. non-definition statements). fn signature(&self) -> Option>; } diff --git a/crates/kirin-ir/src/stage/info.rs b/crates/kirin-ir/src/stage/info.rs index 3158aff588..7b2b527000 100644 --- a/crates/kirin-ir/src/stage/info.rs +++ b/crates/kirin-ir/src/stage/info.rs @@ -59,9 +59,13 @@ use super::arenas::Arenas; /// let ret = b.statement().definition(MyDialect::Return(arg)).new(); /// let block = b.block().argument(MyType::I64).terminator(ret).new(); /// let cfg = b.cfg().add_block(block).new(); -/// let body = b.statement().definition(MyDialect::FuncBody(cfg)).new(); +/// let definition = b.statement().definition(MyDialect::Function(cfg)).new(); /// -/// b.specialize().staged_func(sf).body(body).new().unwrap(); +/// b.specialize() +/// .staged_func(sf) +/// .definition(definition) +/// .new() +/// .unwrap(); /// }); /// // stage is back to StageInfo with the new function added /// ``` diff --git a/crates/kirin-ir/tests/builder_staged.rs b/crates/kirin-ir/tests/builder_staged.rs index 99ce5b29c6..c8c1d75faa 100644 --- a/crates/kirin-ir/tests/builder_staged.rs +++ b/crates/kirin-ir/tests/builder_staged.rs @@ -100,19 +100,19 @@ fn specialize_success_and_duplicate_error() { let sf = stage.staged_function().new().unwrap(); - let body1 = stage.statement().definition(BuilderDialect::Return).new(); + let definition1 = stage.statement().definition(BuilderDialect::Return).new(); let _spec1 = stage .specialize() .staged_func(sf) - .body(body1) + .definition(definition1) .new() .expect("first specialize should succeed"); - let body2 = stage.statement().definition(BuilderDialect::Return).new(); + let definition2 = stage.statement().definition(BuilderDialect::Return).new(); let err = stage .specialize() .staged_func(sf) - .body(body2) + .definition(definition2) .new() .expect_err("duplicate signature should fail"); @@ -124,19 +124,19 @@ fn redefine_specialization_invalidates_and_registers() { let mut stage = new_stage(); let sf = stage.staged_function().new().unwrap(); - let body1 = stage.statement().definition(BuilderDialect::Return).new(); + let definition1 = stage.statement().definition(BuilderDialect::Return).new(); let spec1 = stage .specialize() .staged_func(sf) - .body(body1) + .definition(definition1) .new() .unwrap(); - let body2 = stage.statement().definition(BuilderDialect::Return).new(); + let definition2 = stage.statement().definition(BuilderDialect::Return).new(); let err = stage .specialize() .staged_func(sf) - .body(body2) + .definition(definition2) .new() .expect_err("duplicate"); @@ -181,23 +181,23 @@ fn staged_function_all_matching_returns_most_specific() { let mut stage = new_stage(); let sf = stage.staged_function().new().unwrap(); - let body1 = stage.statement().definition(BuilderDialect::Return).new(); + let definition1 = stage.statement().definition(BuilderDialect::Return).new(); let sig_i32 = Signature::new(vec![TestType::I32], TestType::Any, ()); let _spec1 = stage .specialize() .staged_func(sf) .signature(sig_i32.clone()) - .body(body1) + .definition(definition1) .new() .unwrap(); - let body2 = stage.statement().definition(BuilderDialect::Return).new(); + let definition2 = stage.statement().definition(BuilderDialect::Return).new(); let sig_i64 = Signature::new(vec![TestType::I64], TestType::Any, ()); let _spec2 = stage .specialize() .staged_func(sf) .signature(sig_i64) - .body(body2) + .definition(definition2) .new() .unwrap(); @@ -214,21 +214,21 @@ fn staged_function_all_matching_excludes_invalidated() { let mut stage = new_stage(); let sf = stage.staged_function().new().unwrap(); - let body1 = stage.statement().definition(BuilderDialect::Return).new(); + let definition1 = stage.statement().definition(BuilderDialect::Return).new(); let default_sig: Signature = Signature::placeholder(); let spec1 = stage .specialize() .staged_func(sf) - .body(body1) + .definition(definition1) .new() .unwrap(); // Invalidate spec1 by redefining - let body2 = stage.statement().definition(BuilderDialect::Return).new(); + let definition2 = stage.statement().definition(BuilderDialect::Return).new(); let err = stage .specialize() .staged_func(sf) - .body(body2) + .definition(definition2) .new() .expect_err("duplicate"); let _spec2 = stage.redefine_specialization(err); @@ -248,8 +248,13 @@ fn staged_function_unique_live_specialization_returns_only_live_specialization() let mut stage = new_stage(); let sf = stage.staged_function().new().unwrap(); - let body = stage.statement().definition(BuilderDialect::Return).new(); - let spec = stage.specialize().staged_func(sf).body(body).new().unwrap(); + let definition = stage.statement().definition(BuilderDialect::Return).new(); + let spec = stage + .specialize() + .staged_func(sf) + .definition(definition) + .new() + .unwrap(); let stage = stage.finalize().unwrap(); let sf_info = sf.get_info(&stage).unwrap(); @@ -262,23 +267,23 @@ fn staged_function_unique_live_specialization_rejects_ambiguous_live_set() { let mut stage = new_stage(); let sf = stage.staged_function().new().unwrap(); - let body1 = stage.statement().definition(BuilderDialect::Return).new(); + let definition1 = stage.statement().definition(BuilderDialect::Return).new(); let sig_i32 = Signature::new(vec![TestType::I32], TestType::Any, ()); stage .specialize() .staged_func(sf) .signature(sig_i32) - .body(body1) + .definition(definition1) .new() .unwrap(); - let body2 = stage.statement().definition(BuilderDialect::Return).new(); + let definition2 = stage.statement().definition(BuilderDialect::Return).new(); let sig_i64 = Signature::new(vec![TestType::I64], TestType::Any, ()); stage .specialize() .staged_func(sf) .signature(sig_i64) - .body(body2) + .definition(definition2) .new() .unwrap(); diff --git a/crates/kirin-liveness/tests/cfg.rs b/crates/kirin-liveness/tests/cfg.rs index 50e134f7ae..65615d9c89 100644 --- a/crates/kirin-liveness/tests/cfg.rs +++ b/crates/kirin-liveness/tests/cfg.rs @@ -62,11 +62,11 @@ fn main_cfg( .expect("@main is staged at @test"); let sf_info = sf.get_info(stage).expect("staged function info"); let spec = &sf_info.specializations()[0]; - let body = *spec.body(); + let definition = *spec.definition(); - let cfg = match body.definition(stage) { + let cfg = match definition.definition(stage) { ArithFunctionLanguage::Function { body, .. } => *body, - other => panic!("expected a function body, got {other:?}"), + other => panic!("expected a function definition, got {other:?}"), }; (stage_id, cfg) } diff --git a/crates/kirin-prettyless/src/document/ir_render.rs b/crates/kirin-prettyless/src/document/ir_render.rs index 4c284cb57d..d08251864c 100644 --- a/crates/kirin-prettyless/src/document/ir_render.rs +++ b/crates/kirin-prettyless/src/document/ir_render.rs @@ -234,17 +234,17 @@ where let staged_info = staged_fn.expect_info(self.stage); let spec = &staged_info.specializations()[idx]; - // Set function context so the body's PrettyPrint can access it + // Set function context so the definition's PrettyPrint can access it // via doc.print_function_name() and doc.print_return_types(). let prev_name = self.function_name(); self.set_function_name(staged_info.name()); let header = self.text("specialize @") + self.text(self.stage_symbol_text()); - let body = self.print_statement(spec.body()); + let definition = self.print_statement(spec.definition()); self.set_function_name(prev_name); - header + self.text(" ") + body + header + self.text(" ") + definition } /// Pretty print a staged function with all its non-invalidated specializations. @@ -271,14 +271,14 @@ where return doc; } - // Set function context for body PrettyPrint projections. + // Set function context for definition PrettyPrint projections. let prev_name = self.function_name(); self.set_function_name(info.name()); for spec in active { doc += self.line_(); doc += self.text("specialize @") + self.text(self.stage_symbol_text()); - doc += self.text(" ") + self.print_statement(spec.body()); + doc += self.text(" ") + self.print_statement(spec.definition()); } self.set_function_name(prev_name); diff --git a/crates/kirin-prettyless/src/tests/edge_cases.rs b/crates/kirin-prettyless/src/tests/edge_cases.rs index 8eb0c25749..265ec9b82e 100644 --- a/crates/kirin-prettyless/src/tests/edge_cases.rs +++ b/crates/kirin-prettyless/src/tests/edge_cases.rs @@ -311,7 +311,11 @@ fn test_staged_function_unnamed() { let block = ctx.block().stmt(a).terminator(ret).new(); let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); - ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); + ctx.specialize() + .staged_func(sf) + .definition(fdef) + .new() + .unwrap(); }); let output = PrintExt::sprint(&func, &pipeline); @@ -341,7 +345,7 @@ fn test_staged_function_no_params() { let _ = stage .specialize() .staged_func(staged_function) - .body(fdef) + .definition(fdef) .new() .unwrap(); @@ -392,7 +396,11 @@ fn test_pipeline_render_builder_write_to() { let block = ctx.block().stmt(a).terminator(ret).new(); let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); - ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); + ctx.specialize() + .staged_func(sf) + .definition(fdef) + .new() + .unwrap(); }); let mut output = Vec::new(); @@ -428,7 +436,11 @@ fn test_function_render_builder_write_to() { let block = ctx.block().stmt(a).terminator(ret).new(); let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); - ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); + ctx.specialize() + .staged_func(sf) + .definition(fdef) + .new() + .unwrap(); }); let mut output = Vec::new(); @@ -462,7 +474,7 @@ fn test_render_very_narrow_width() { let _ = stage .specialize() .staged_func(sf) - .body(fdef) + .definition(fdef) .new() .unwrap(); diff --git a/crates/kirin-prettyless/src/tests/impls.rs b/crates/kirin-prettyless/src/tests/impls.rs index dd0550ed4a..312f2dd226 100644 --- a/crates/kirin-prettyless/src/tests/impls.rs +++ b/crates/kirin-prettyless/src/tests/impls.rs @@ -432,7 +432,7 @@ fn test_render_builder_config() { let f = stage .specialize() .staged_func(sf) - .body(fdef) + .definition(fdef) .new() .unwrap(); diff --git a/crates/kirin-prettyless/src/tests/mod.rs b/crates/kirin-prettyless/src/tests/mod.rs index 8a5ccca40b..d7060454fb 100644 --- a/crates/kirin-prettyless/src/tests/mod.rs +++ b/crates/kirin-prettyless/src/tests/mod.rs @@ -76,7 +76,7 @@ fn create_test_function() -> ( let f = stage .specialize() .staged_func(staged_function) - .body(fdef) + .definition(fdef) .new() .unwrap(); diff --git a/crates/kirin-prettyless/src/tests/pipeline.rs b/crates/kirin-prettyless/src/tests/pipeline.rs index 04636aa454..45f596d297 100644 --- a/crates/kirin-prettyless/src/tests/pipeline.rs +++ b/crates/kirin-prettyless/src/tests/pipeline.rs @@ -23,7 +23,11 @@ fn test_pipeline_function_print() { let block = ctx0.block().stmt(a).terminator(ret).new(); let body = ctx0.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx0, body); - ctx0.specialize().staged_func(sf0).body(fdef).new().unwrap(); + ctx0.specialize() + .staged_func(sf0) + .definition(fdef) + .new() + .unwrap(); }); // --- Stage B: a different version with two constants --- @@ -48,7 +52,11 @@ fn test_pipeline_function_print() { let block = ctx1.block().stmt(a).stmt(b).stmt(c).terminator(ret).new(); let body = ctx1.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx1, body); - ctx1.specialize().staged_func(sf1).body(fdef).new().unwrap(); + ctx1.specialize() + .staged_func(sf1) + .definition(fdef) + .new() + .unwrap(); }); // Print the function across both stages @@ -80,7 +88,11 @@ fn test_pipeline_unnamed_stage() { let block = ctx.block().stmt(a).terminator(ret).new(); let body = ctx.cfg().add_block(block).new(); let fdef = SimpleLanguage::op_function(ctx, body); - ctx.specialize().staged_func(sf).body(fdef).new().unwrap(); + ctx.specialize() + .staged_func(sf) + .definition(fdef) + .new() + .unwrap(); }); // Should fall back to numeric symbol form: "stage @0" diff --git a/crates/kirin-prettyless/src/tests/sprint_with_globals.rs b/crates/kirin-prettyless/src/tests/sprint_with_globals.rs index 800af41212..5d16eff7bc 100644 --- a/crates/kirin-prettyless/src/tests/sprint_with_globals.rs +++ b/crates/kirin-prettyless/src/tests/sprint_with_globals.rs @@ -19,7 +19,7 @@ fn test_sprint_with_globals() { let _ = stage .specialize() .staged_func(staged_function) - .body(fdef) + .definition(fdef) .new() .unwrap(); diff --git a/docs/design/formalism/syntax.md b/docs/design/formalism/syntax.md index 3c5e2c2446..158c2b927e 100644 --- a/docs/design/formalism/syntax.md +++ b/docs/design/formalism/syntax.md @@ -48,7 +48,7 @@ Language ::= DialectEnumVariant* Function ::= FunctionInfo + staged variants StagedFunction ::= stage-specific callable variant -Specialized ::= concrete specialization with body Statement +Specialized ::= concrete specialization with definition Statement Statement ::= dialect definition + operands + results + nested blocks/cfgs/successors CFG ::= Block* diff --git a/docs/design/interpreter/index.md b/docs/design/interpreter/index.md index e8eee49fd1..126efd7376 100644 --- a/docs/design/interpreter/index.md +++ b/docs/design/interpreter/index.md @@ -284,7 +284,7 @@ pub trait Linker { ``` A linker resolves `Callee::{Named, Function, Staged, Specialized}` to a -`(stage, specialization, body)` target. It is a *field of the engine*, never +`(stage, specialization, definition)` target. It is a *field of the engine*, never a trait the user implements on the engine type — this is a deliberate coherence rule: policies must be swappable without newtype-cloning a driver. diff --git a/example/toy-lang/src/interpreter/mod.rs b/example/toy-lang/src/interpreter/mod.rs index 3071453ef7..5cef5132a9 100644 --- a/example/toy-lang/src/interpreter/mod.rs +++ b/example/toy-lang/src/interpreter/mod.rs @@ -134,15 +134,16 @@ fn function_cfg( }); } }; - let spec_info = spec - .get_info(info) - .ok_or(InterpreterError::Custom("specialized function has no body"))?; - match spec_info.body().definition(info) { + let spec_info = spec.get_info(info).ok_or(InterpreterError::Custom( + "specialized function has no definition", + ))?; + let definition = *spec_info.definition(); + match definition.definition(info) { HighLevel::Lexical(Lexical::Function(function)) => { use kirin::prelude::HasCFGBody; *function.cfg() } - _ => return Err(InterpreterError::Custom("expected a function body")), + _ => return Err(InterpreterError::Custom("expected a function definition")), } } Stage::Lowered(info) => { @@ -161,15 +162,16 @@ fn function_cfg( }); } }; - let spec_info = spec - .get_info(info) - .ok_or(InterpreterError::Custom("specialized function has no body"))?; - match spec_info.body().definition(info) { + let spec_info = spec.get_info(info).ok_or(InterpreterError::Custom( + "specialized function has no definition", + ))?; + let definition = *spec_info.definition(); + match definition.definition(info) { LowLevel::Lifted(Lifted::Function(function)) => { use kirin::prelude::HasCFGBody; *function.cfg() } - _ => return Err(InterpreterError::Custom("expected a function body")), + _ => return Err(InterpreterError::Custom("expected a function definition")), } } }; diff --git a/example/toy-lang/src/interpreter/tests.rs b/example/toy-lang/src/interpreter/tests.rs index 313b268dc1..970bdb3973 100644 --- a/example/toy-lang/src/interpreter/tests.rs +++ b/example/toy-lang/src/interpreter/tests.rs @@ -263,7 +263,7 @@ fn build_cross_stage_specialized_pipeline() -> Pipeline { .terminator(ret) .new(); let cfg = builder.cfg().add_block(block).new(); - let body = Function::::new( + let definition = Function::::new( builder, cfg, Signature::new(vec![ArithType::I64], ArithType::I64, ()), @@ -271,7 +271,7 @@ fn build_cross_stage_specialized_pipeline() -> Pipeline { builder .specialize() .staged_func(caller) - .body(body) + .definition(definition) .new() .unwrap(); }); @@ -565,7 +565,7 @@ mod advanced { use kirin_constprop::{ConstPropContext, ConstPropValue}; use kirin_interpreter::SameStageLinker; -use kirin_interpreter::engine::{ + use kirin_interpreter::engine::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractFrameBuild, BlockFrame, CFGFrame, CallContext, CallFrame, Completion, ConcreteInterpreter, CrossStageLinker, DefaultBodyFrames, DiGraphFrame, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, @@ -896,10 +896,10 @@ mod demand { .resolve_staged_function(name, stage_id) .expect("staged function"); let sf_info = sf.get_info(info).expect("staged function info"); - let body = *sf_info.specializations()[0].body(); - let cfg = match body.definition(info) { + let definition = *sf_info.specializations()[0].definition(); + let cfg = match definition.definition(info) { HighLevel::Lexical(Lexical::Function(function)) => *function.cfg(), - other => panic!("expected a function body, got {other:?}"), + other => panic!("expected a function definition, got {other:?}"), }; (stage_id, cfg) } diff --git a/example/toy-qc/src/circuit.rs b/example/toy-qc/src/circuit.rs index c328b4bd0b..a2c2a970d1 100644 --- a/example/toy-qc/src/circuit.rs +++ b/example/toy-qc/src/circuit.rs @@ -1,7 +1,7 @@ use crate::types::QubitType; use kirin::prelude::*; -/// Function body holding a DiGraph for circuit-stage programs. +/// Function definition whose body is a DiGraph for circuit-stage programs. /// Circuits are naturally directed acyclic graphs: qubit values flow /// forward through gates. #[derive(Clone, Debug, PartialEq, Dialect, HasParser, PrettyPrint)] diff --git a/example/toy-qc/src/zx.rs b/example/toy-qc/src/zx.rs index 7ea160d960..3a328a2673 100644 --- a/example/toy-qc/src/zx.rs +++ b/example/toy-qc/src/zx.rs @@ -1,7 +1,7 @@ use crate::types::QubitType; use kirin::prelude::*; -/// Function body holding an UnGraph for ZX-stage programs. +/// Function definition whose body is an UnGraph for ZX-stage programs. /// ZX calculus diagrams are undirected graphs: wires are edges /// and spiders/boxes are nodes connected by those edges. #[derive(Clone, Debug, PartialEq, Dialect, HasParser, PrettyPrint)] diff --git a/tests/body_kinds.rs b/tests/body_kinds.rs index 2d99523ec1..5448c04e16 100644 --- a/tests/body_kinds.rs +++ b/tests/body_kinds.rs @@ -43,11 +43,11 @@ use kirin_function::Lexical; use kirin_interpreter::{ AbstractBlockFrame, AbstractCallFrame, AbstractCompletion, AbstractDiGraphFrame, AbstractFrameBuild, BlockFrame, Body, BodyFrameEntry, CFGFrame, CallBodyFramePolicy, - CallContext, CallFrame, Completion, ConcreteInterpreter, ContextInsensitive, DefaultBodyFrames, - DiGraphFrame, Env, EnvIndex, ForwardDataflowFrameEngine, ForwardFrameEngine, Frame, FrameBuild, - FrameEffect, FunctionEntry, Interpretable, InterpreterError, SameStageLinker, - SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, StandardFrame, - StatementDispatch, UnGraphEntry, expect_single, + CallContext, CallFrame, Callee, Completion, ConcreteInterpreter, ContextInsensitive, + DefaultBodyFrames, DiGraphFrame, Env, EnvIndex, ForwardDataflowFrameEngine, ForwardFrameEngine, + Frame, FrameBuild, FrameEffect, FunctionEntry, Interpretable, InterpreterError, + SameStageLinker, SparseForwardEffect, SparseForwardInterp, SparseForwardInterpreter, + StandardFrame, StatementDispatch, UnGraphEntry, expect_single, }; use kirin_scf::{BuildScfFor, BuildScfIf, ScfForFrame, ScfIfFrame, StructuredControlFlow}; use kirin_test_languages::GraphFunctionLanguage; @@ -94,6 +94,23 @@ fn parse(program: &str) -> Pipeline { pipeline } +fn local_function_symbol( + pipeline: &Pipeline, + stage_name: &str, + function_name: &str, +) -> (CompileStage, Symbol) { + let stage = pipeline + .stage_by_name(stage_name) + .expect("named stage exists"); + let symbol = pipeline + .stage(stage) + .expect("stage info exists") + .symbol_table() + .lookup(function_name) + .expect("function name is interned in the stage-local symbol table"); + (stage, symbol) +} + fn run(pipeline: &Pipeline, function: &str, args: &[i64]) -> Result { expect_single(run_product(pipeline, function, args)?) } @@ -898,6 +915,64 @@ fn analyze_insensitive( expect_single(analysis.analyze_by_name("test", function, args.iter().cloned())?) } +#[test] +fn concrete_symbol_entry_matches_name_and_direct_callee() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + let (stage, symbol) = local_function_symbol(&pipeline, "test", "gadd"); + assert_eq!(Callee::from(symbol), Callee::Named(symbol)); + + let mut by_name: Engine<'_> = ConcreteInterpreter::new(&pipeline); + let by_name = + expect_single::(by_name.call_by_name("test", "gadd", [2, 3]).unwrap()) + .unwrap(); + + let mut by_symbol: Engine<'_> = ConcreteInterpreter::new(&pipeline); + let by_symbol = + expect_single::(by_symbol.call_by_symbol(stage, symbol, [2, 3]).unwrap()) + .unwrap(); + + let mut by_callee: Engine<'_> = ConcreteInterpreter::new(&pipeline); + let by_callee = expect_single::( + by_callee + .call(stage, Callee::Named(symbol), [2, 3]) + .unwrap(), + ) + .unwrap(); + + assert_eq!((by_name, by_symbol, by_callee), (5, 5, 5)); +} + +#[test] +fn sparse_forward_symbol_entry_matches_name_and_direct_callee() { + let pipeline = parse(DIGRAPH_CALLABLE_PROGRAM); + let (stage, symbol) = local_function_symbol(&pipeline, "test", "gadd"); + let args = || [ConstPropValue::Const(2), ConstPropValue::Const(3)]; + + let mut by_name: AbstractEngine<'_> = SparseForwardInterpreter::new(&pipeline); + let by_name = expect_single::( + by_name.analyze_by_name("test", "gadd", args()).unwrap(), + ) + .unwrap(); + + let mut by_symbol: AbstractEngine<'_> = SparseForwardInterpreter::new(&pipeline); + let by_symbol = expect_single::( + by_symbol.analyze_by_symbol(stage, symbol, args()).unwrap(), + ) + .unwrap(); + + let mut by_callee: AbstractEngine<'_> = SparseForwardInterpreter::new(&pipeline); + let by_callee = expect_single::( + by_callee + .analyze(stage, Callee::Named(symbol), args()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(by_name, ConstPropValue::Const(5)); + assert_eq!(by_symbol, by_name); + assert_eq!(by_callee, by_name); +} + /// A CFG body whose branch condition is an *unknown* argument, so neither /// successor can be decided: the abstract block frame explores both and joins /// their returns. Identical arms fold to a constant; differing arms join to diff --git a/tests/frame_engine_capabilities.rs b/tests/frame_engine_capabilities.rs index 73e9eb29bf..65e8cedc0d 100644 --- a/tests/frame_engine_capabilities.rs +++ b/tests/frame_engine_capabilities.rs @@ -270,7 +270,7 @@ impl CallServices for CallOnlyEngine { fn enter_function( &mut self, _stage: CompileStage, - _body: Statement, + _definition: Statement, _args: Product, _index: EnvIndex, ) -> Result, InterpreterError> { diff --git a/tests/roundtrip/composable_existing_dialects.rs b/tests/roundtrip/composable_existing_dialects.rs index 663da5a628..03a7d440ed 100644 --- a/tests/roundtrip/composable_existing_dialects.rs +++ b/tests/roundtrip/composable_existing_dialects.rs @@ -2,7 +2,7 @@ use kirin::prelude::*; use kirin_arith::{Arith, ArithType, ArithValue}; use kirin_cmp::Cmp; use kirin_constant::Constant; -use kirin_function::{Function as FunctionBody, Lexical, Return}; +use kirin_function::{Function, Lexical, Return}; use kirin_scf::StructuredControlFlow; use kirin_test_utils::roundtrip; @@ -27,7 +27,7 @@ enum ComposedSourceLanguage { #[chumsky(crate = kirin::parsers)] enum WrappedConstantLanguage { #[wraps] - Function(FunctionBody), + Function(Function), #[wraps] Constant(Constant), #[wraps] diff --git a/tests/roundtrip/digraph.rs b/tests/roundtrip/digraph.rs index 512bb58e1e..2e125a2a4a 100644 --- a/tests/roundtrip/digraph.rs +++ b/tests/roundtrip/digraph.rs @@ -110,7 +110,7 @@ fn test_projected_digraph_empty_body_roundtrip() { // --- Pipeline-level projected format e2e test --- -/// A dialect where the function body uses projected DiGraph format. +/// A dialect where the function definition uses projected DiGraph format. /// The body format is `({body:ports}) {{ {body:body} }}` — ports and body /// are parsed from projections, and the function signature is extracted /// from the IR after emit. @@ -129,11 +129,11 @@ enum ProjectedFuncLang { #[kirin(into)] kirin_test_languages::Value, #[kirin(type = SimpleType::F64)] ResultValue, ), - /// Function body: `fn {:name}{sig} ({graph:ports}) captures ({graph:captures}) {{ {graph:body} }}` + /// Function definition: `fn {:name}{sig} ({graph:ports}) captures ({graph:captures}) {{ {graph:body} }}` #[chumsky( format = "fn {:name}{sig} ({graph:ports}) captures ({graph:captures}) {{ {graph:body} }}" )] - FuncBody { + FunctionDefinition { graph: DiGraph, sig: Signature, }, @@ -180,7 +180,7 @@ specialize @test fn @foo(f64) -> f64 (%p0: f64) captures () { %r = add %p0, %p0; // --- Use Case 5: Block projections pipeline test --- -/// A dialect using Block projections for the function body. +/// A dialect using Block projections for the function definition. #[derive(Debug, Clone, PartialEq, Eq, Hash, Dialect, HasParser, PrettyPrint)] #[kirin(builders, type = SimpleType, crate = kirin::ir)] #[chumsky(crate = kirin::parsers)] @@ -194,9 +194,9 @@ enum BlockProjectedLang { #[chumsky(format = "$ret {0}")] #[kirin(terminator)] Ret(SSAValue), - /// Function body: `fn {:name}{sig} ({body:args}) {{ {body:body} }}` + /// Function definition: `fn {:name}{sig} ({body:args}) {{ {body:body} }}` #[chumsky(format = "fn {:name}{sig} ({body:args}) {{ {body:body} }}")] - FuncBody { + FunctionDefinition { body: Block, sig: Signature, }, @@ -244,9 +244,9 @@ enum CFGProjectedLang { #[chumsky(format = "$ret {0}")] #[kirin(terminator)] Ret(SSAValue), - /// Function body: `fn {:name}{sig} {{ {body:body} }}` + /// Function definition: `fn {:name}{sig} {{ {body:body} }}` #[chumsky(format = "fn {:name}{sig} {{ {body:body} }}")] - FuncBody { + FunctionDefinition { body: CFG, sig: Signature, }, @@ -296,11 +296,11 @@ enum DialectControlledLang { #[kirin(into)] kirin_test_languages::Value, #[kirin(type = SimpleType::F64)] ResultValue, ), - /// Function body: `fn {:name}{sig} ({graph:ports}) captures ({graph:captures}) {{ {graph:body} }}` + /// Function definition: `fn {:name}{sig} ({graph:ports}) captures ({graph:captures}) {{ {graph:body} }}` #[chumsky( format = "fn {:name}{sig} ({graph:ports}) captures ({graph:captures}) {{ {graph:body} }}" )] - FuncBody { + FunctionDefinition { graph: DiGraph, sig: Signature, }, diff --git a/tests/simple.rs b/tests/simple.rs index 44a9de9917..10a5b079ec 100644 --- a/tests/simple.rs +++ b/tests/simple.rs @@ -49,7 +49,7 @@ fn test_block() { let f = stage .specialize() .staged_func(staged_function) - .body(fdef) + .definition(fdef) .new() .unwrap();