Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,31 @@ jobs:
env:
RUSTDOCFLAGS: '--cfg docsrs -D warnings'

cargo-rdme:
name: cargo rdme
# Checks that each crate's README.md is in sync with its crate-level docs (src/lib.rs).
# To fix a failure, edit the doc comment in src/lib.rs and regenerate the README by running
# the same command without `--check`.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: install stable toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_STABLE_VER }}

- name: install cargo-rdme
uses: taiki-e/install-action@v2
with:
tool: cargo-rdme

- name: check execution_tape README
run: cargo rdme --workspace-project=execution_tape --heading-base-level=0 --check

- name: check execution_graph README
run: cargo rdme --workspace-project=execution_graph --heading-base-level=0 --check

# If this fails, consider changing your text or adding something to .typos.toml.
typos:
runs-on: ubuntu-latest
Expand Down
35 changes: 22 additions & 13 deletions execution_graph/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# execution_graph

<!-- We use cargo-rdme to keep this README in sync with the crate-level docs in src/lib.rs.
To update the section below, edit the doc comment in src/lib.rs, then run:
cargo rdme --workspace-project=execution_graph --heading-base-level=0
Full documentation at https://github.com/orium/cargo-rdme -->

<!-- cargo-rdme start -->

Incremental execution graph built on `execution_tape`.

This crate provides a small `no_std` graph that executes verified `execution_tape` programs as
Expand Down Expand Up @@ -52,8 +59,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let program = Arc::new(builder.build_verified()?);

let mut graph = ExecutionGraph::new(NoHost, Limits::default());
let node = graph.add_node(program, entry, vec!["x".into()]);
graph.set_input_value(node, "x", Value::I64(41));
let node = graph.add_node(program, entry, vec!["x".into()])?;
graph.set_input_value(node, "x", Value::I64(41))?;

let summary = graph.run_all()?;
assert_eq!(summary.executed_nodes, 1);
Expand All @@ -73,22 +80,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
`ResourceKey::Input("foo")` dirty, which may trigger re-execution of transitive dependents.

Input names are part of the dependency key space: the string you pass to `set_input_value(node,
"foo", ..)` must match the string you pass to `invalidate_input("foo")` for incremental scheduling
to work.
"foo", ..)` must match the string you pass to `invalidate_input("foo")` for incremental
scheduling to work.

Host state invalidation uses the same key space: if a host op records a
`ResourceKeyRef::HostState { op, key }` read during execution, you can invalidate that state later
via `ExecutionGraph::invalidate_tape_key(...)` (or by constructing the corresponding owned
`ResourceKeyRef::HostState { op, key }` read during execution, you can invalidate that state
later via `ExecutionGraph::invalidate_tape_key(...)` (or by constructing the corresponding owned
`execution_graph::ResourceKey` and calling `ExecutionGraph::invalidate(...)`).

Graph construction is checked at the public API boundary: `add_node`, `set_input_value`, and
`connect` return `GraphError` values for unknown entry functions, input arity mismatches, unknown
input names, and unknown output names.
`connect` return `GraphError` values for unknown entry functions, input arity mismatches,
unknown input names, and unknown output names.

## Execution behavior

`run_node` drains and executes only the dirty work within the dependency closure of the target
nodes outputs, leaving unrelated dirty work dirty to be handled by a later `run_all`.
node's outputs, leaving unrelated dirty work dirty to be handled by a later `run_all`.

For low overhead telemetry, `run_all` / `run_node` return only an executed-node summary.

Expand All @@ -113,12 +120,14 @@ cargo run -p execution_graph_examples --bin tax -- --dot

## Current limitations

- `execution_graph` intentionally stays close to the VM: traps expose `execution_tape::vm::TrapInfo`
rather than source-language diagnostics.
- `execution_graph` intentionally stays close to the VM: traps expose
`execution_tape::vm::TrapInfo` rather than source-language diagnostics.
- VM traps are still collapsed to `GraphError::Trap` at the graph boundary. Missing inputs,
missing upstream outputs, bad output arity, and strict-deps failures are reported with context.
- Graph nodes are currently `execution_tape` entrypoints only; custom dispatch can be layered later
without changing the resource-key model.
- Graph nodes are currently `execution_tape` entrypoints only; custom dispatch can be layered
later without changing the resource-key model.

<!-- cargo-rdme end -->

## Minimum supported Rust Version (MSRV)

Expand Down
130 changes: 92 additions & 38 deletions execution_graph/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
// Copyright 2026 the Execution Tape Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

// After you edit the crate's doc comment, regenerate README.md by running:
// cargo rdme --workspace-project=execution_graph --heading-base-level=0

//! Incremental execution graph built on `execution_tape`.
//!
//! This crate will provide a graph whose nodes are verified "tapes" (program entrypoints) and
//! whose edges represent data dependencies, enabling sound incremental re-execution via dirty
//! tracking.
//!
//! ## Input key semantics
//! This crate provides a small `no_std` graph that executes verified `execution_tape` programs as
//! nodes and re-executes only the nodes that are affected by changes.
//!
//! Incremental invalidation is keyed by [`ResourceKey::Input`]. The same input name string must be
//! used consistently: the name passed to [`ExecutionGraph::set_input_value`] must match the name
//! passed to [`ExecutionGraph::invalidate_input`] (otherwise the invalidation will not affect the
//! reads recorded by runs).
//! ## Quick Start
//!
//! ## Example
//! Use `execution_tape` to build verified programs, then wire them as graph nodes:
//!
//! ```
//! ```rust
//! use std::sync::Arc;
//!
//! use execution_graph::ExecutionGraph;
Expand All @@ -41,34 +38,91 @@
//! }
//! }
//!
//! let mut asm = Asm::new();
//! asm.const_i64(2, 1);
//! asm.i64_add(3, 1, 2);
//! asm.ret(0, &[3]);
//!
//! let mut builder = ProgramBuilder::new();
//! let entry = builder.push_function_checked(
//! asm,
//! FunctionSig {
//! arg_types: vec![ValueType::I64],
//! ret_types: vec![ValueType::I64],
//! },
//! )?;
//! builder.set_function_output_name(entry, 0, "y")?;
//! let program = Arc::new(builder.build_verified()?);
//!
//! let mut graph = ExecutionGraph::new(NoHost, Limits::default());
//! let node = graph.add_node(program, entry, vec!["x".into()])?;
//! graph.set_input_value(node, "x", Value::I64(41))?;
//!
//! let summary = graph.run_all()?;
//! assert_eq!(summary.executed_nodes, 1);
//! assert_eq!(
//! graph.node_outputs(node).unwrap().get("y"),
//! Some(&Value::I64(42))
//! );
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut asm = Asm::new();
//! asm.const_i64(2, 1);
//! asm.i64_add(3, 1, 2);
//! asm.ret(0, &[3]);
//!
//! let mut builder = ProgramBuilder::new();
//! let entry = builder.push_function_checked(
//! asm,
//! FunctionSig {
//! arg_types: vec![ValueType::I64],
//! ret_types: vec![ValueType::I64],
//! },
//! )?;
//! builder.set_function_output_name(entry, 0, "y")?;
//! let program = Arc::new(builder.build_verified()?);
//!
//! let mut graph = ExecutionGraph::new(NoHost, Limits::default());
//! let node = graph.add_node(program, entry, vec!["x".into()])?;
//! graph.set_input_value(node, "x", Value::I64(41))?;
//!
//! let summary = graph.run_all()?;
//! assert_eq!(summary.executed_nodes, 1);
//! assert_eq!(graph.node_outputs(node).unwrap().get("y"), Some(&Value::I64(42)));
//! Ok(())
//! }
//! ```
//!
//! ## Model
//!
//! - **Nodes** are `(VerifiedProgram, entry FuncId)` pairs.
//! - **Edges** represent data dependencies; they are recorded dynamically from each node run:
//! - reading an external input records `ResourceKey::Input(name)`
//! - reading another node's output records `ResourceKey::NodeOutput { node, output }`
//! - host ops can record additional dependencies via `execution_tape::host::AccessSink`
//! - **Invalidation** is done by name: calling `invalidate_input("foo")` marks the input key
//! `ResourceKey::Input("foo")` dirty, which may trigger re-execution of transitive dependents.
//!
//! Input names are part of the dependency key space: the string you pass to `set_input_value(node,
//! "foo", ..)` must match the string you pass to `invalidate_input("foo")` for incremental
//! scheduling to work.
//!
//! Host state invalidation uses the same key space: if a host op records a
//! `ResourceKeyRef::HostState { op, key }` read during execution, you can invalidate that state
//! later via `ExecutionGraph::invalidate_tape_key(...)` (or by constructing the corresponding owned
//! `execution_graph::ResourceKey` and calling `ExecutionGraph::invalidate(...)`).
//!
//! Graph construction is checked at the public API boundary: `add_node`, `set_input_value`, and
//! `connect` return `GraphError` values for unknown entry functions, input arity mismatches,
//! unknown input names, and unknown output names.
//!
//! ## Execution behavior
//!
//! `run_node` drains and executes only the dirty work within the dependency closure of the target
//! node's outputs, leaving unrelated dirty work dirty to be handled by a later `run_all`.
//!
//! For low overhead telemetry, `run_all` / `run_node` return only an executed-node summary.
//!
//! For debugging and instrumentation:
//! - `run_all_with_report` / `run_node_with_report` accept a `ReportDetailMask` so you can choose
//! cheaper detail levels (for example, node + immediate cause key without path tracing).
//! - Use `ReportDetailMask::FULL` when you want full per-node cause paths.
//!
//! ## Demo
//!
//! Run the demo with:
//!
//! ```sh
//! cargo run -p execution_graph_examples --bin tax
//! ```
//!
//! Emit Graphviz DOT for the same graph:
//!
//! ```sh
//! cargo run -p execution_graph_examples --bin tax -- --dot
//! ```
//!
//! ## Current limitations
//!
//! - `execution_graph` intentionally stays close to the VM: traps expose
//! `execution_tape::vm::TrapInfo` rather than source-language diagnostics.
//! - VM traps are still collapsed to `GraphError::Trap` at the graph boundary. Missing inputs,
//! missing upstream outputs, bad output arity, and strict-deps failures are reported with context.
//! - Graph nodes are currently `execution_tape` entrypoints only; custom dispatch can be layered
//! later without changing the resource-key model.

#![no_std]

Expand Down
21 changes: 16 additions & 5 deletions execution_tape/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# `execution_tape`

<!-- We use cargo-rdme to keep this README in sync with the crate-level docs in src/lib.rs.
To update the section below, edit the doc comment in src/lib.rs, then run:
cargo rdme --workspace-project=execution_tape --heading-base-level=0
Full documentation at https://github.com/orium/cargo-rdme -->

<!-- cargo-rdme start -->

Portable, verifiable bytecode container format and register VM runtime (draft).

`execution_tape` is the low-level execution layer for already-lowered programs. It owns the
portable program format, verifier, register VM, host-call ABI, aggregate values, tracing hooks, and
disassembly tools. It does not own language semantics, graph authoring, or host object lifetimes.
portable program format, verifier, register VM, host-call ABI, aggregate values, tracing hooks,
and disassembly tools. It does not own language semantics, graph authoring, or host object
lifetimes.

The crate is `no_std + alloc` by default. The `std` feature is currently reserved for integrations
that need standard-library support.
The crate is `no_std + alloc` by default. The `std` feature is currently reserved for
integrations that need standard-library support.

## Quick Start

Expand Down Expand Up @@ -73,7 +81,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
- `program`: serialized program model, type tables, constants, host signatures, and names.
- `verifier`: validation and lowering into an execution-ready `VerifiedProgram`.
- `vm`: bounded interpreter for verified programs.
- `host`: host-call trait, borrowed argument views, aggregate readers, and access recording hooks.
- `host`: host-call trait, borrowed argument views, aggregate readers, and access recording
hooks.
- `trace`: low-overhead tracing events for profiling and diagnostics.
- `disasm`: human-readable disassembly for verified programs.

Expand All @@ -92,6 +101,8 @@ Print disassembly for a small branching program:
cargo run -p execution_tape --example disasm
```

<!-- cargo-rdme end -->

## Minimum supported Rust Version (MSRV)

This crate has been verified to compile with **Rust 1.88** and later.
Loading
Loading