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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ jobs:
path: target/nextest/ci/junit.xml
if-no-files-found: ignore

# ── Codegen compile oracle (the #[ignore] gates) ────────────────────
# `cargo nextest`/`cargo test` above do NOT run #[ignore] tests, so the
# compiler-enforced codegen oracles (emitted_crate_cargo_checks,
# emitted_crate_builds_to_wasm32_wasip2) would otherwise never gate in CI.
# They shell out to cargo to build a generated crate — including to
# wasm32-wasip2 — so this job installs that target and runs the ignored set.
# REQ-CODEGEN-WIT-RECORDS-001/002 (#319).
codegen-oracle:
name: Codegen compile oracle
runs-on: [self-hosted, linux, x64, rust-cpu]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
targets: wasm32-wasip2
- uses: Swatinem/rust-cache@v2
- name: Run generated-crate compile oracles (ignored tests)
run: cargo test -p spar-codegen --test lifecycle_bindings -- --ignored

# ── Bench compile smoke (fast regression gate) ──────────────────────
bench-smoke:
name: Bench compile smoke
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2649,6 +2649,120 @@ artifacts:
- type: traces-to
target: REQ-CODEGEN-WIT

- id: REQ-CODEGEN-WIT-RECORDS-001
type: requirement
title: "Structured WIT records + wit-bindgen-wired Rust from AADL"
description: >
Codegen shall emit no_alloc-representable structured interfaces from real
AADL components, closing the #319 gaps where a `data implementation` with
subcomponents degraded to an opaque `type x = list<u8>` (forcing
cabi_realloc) and the Rust skeleton had no wit-bindgen wiring. Delivered
and oracle-verified in this requirement:
(1) a `data implementation` with scalar subcomponents becomes a WIT
`record { field: type }`, recursively for nested data implementations,
emitted dependency-first (#319 items 1);
(2) scalar fields map to precise WIT scalars honoring signedness/float
(Data_Representation => Float -> f32/f64; Number_Representation => Signed
-> s8..s64; else u8..u64), and a field that cannot be a no_alloc scalar
(non-power-of-two size, sub-32-bit float, opaque type) is a HARD ERROR
naming the field — codegen refuses rather than emit an allocating fallback
(user-chosen policy);
(3) `wit_ident` breaks PascalCase word boundaries (EepromSnapshot ->
eeprom-snapshot, #319 item 6);
(4) each thread's WIT world exports the lifecycle its `Dispatch_Protocol`
implies — Periodic -> a single `{instance}-compute`; other protocols ->
`{instance}-initialize/compute/finalize` — replacing the old opaque single
`{thread}: func()` (#319 item 7); and
(5) a per-process binding crate root (`crates/{proc}/src/lib.rs`) wires the
world to Rust: `wit_bindgen::generate!` + `impl Guest` (methods matching the
world exports) + `export!`, and the per-thread `{Struct}Component` trait is
lifecycle-derived from the same single source (#319 item 4). All five share
`crate::Lifecycle`/`lifecycle_for`/`dispatch_protocol` so the WIT world and
the Rust Guest cannot drift — and because wit-bindgen derives the `Guest`
trait from the world, a `cargo check` of the emitted crate is a
compiler-enforced alignment oracle, not a string match.
OUT OF SCOPE (successor REQ-CODEGEN-WIT-RECORDS-002): defining the Rust
`*Impl` record structs + typed ports and module-wiring the per-thread files
so the WHOLE crate (not just the binding lib.rs) compiles, and the
wasm-component zero-`cabi_realloc` symbol check. Reported as GitHub #319.
status: implemented
tags: [codegen, wit, rust, wit-bindgen, interop]
fields:
release: v0.24.0
links:
- type: traces-to
target: REQ-CODEGEN-WIT-TYPES

- id: REQ-CODEGEN-WIT-RECORDS-002
type: requirement
title: "Whole generated component crate compiles + no unbounded ABI types"
description: >
Successor to REQ-CODEGEN-WIT-RECORDS-001: make the ENTIRE generated
component crate compile, not just the WIT binding lib.rs, and give the
no_alloc guarantee a sound oracle. Three parts:
(a) resolve each classifier-typed port to a real Rust type via the SAME
`DataShape` `wit_gen` uses (scalar -> u8..u64/i8..i64/f32/f64; record ->
a generated `#[derive(Default)]` struct in a shared per-crate `types`
module, nested-first and de-duplicated; opaque -> `Vec<u8>`), so the Rust
struct and the WIT record cannot drift — today `feature_to_rust_type` names
PascalCase structs that are never defined (#319 item 3);
(b) relocate the per-thread files UNDER the crate (`crates/{proc}/src/`) and
module-wire them from the binding lib.rs, with the process `Guest` impl
delegating each export to the per-thread `{Struct}Component` (construct-on-
call) instead of `todo!()`;
(c) NO-ALLOC ORACLE at the WIT level, not the wasm symbol level. A probe
(2026-07-08) showed `cabi_realloc` is UNCONDITIONAL wit-bindgen-rt glue —
present and called in both a scalar-record component and a `list<u8>` one —
so an `nm`/symbol "zero cabi_realloc" check cannot discriminate and is
unsound. The sound, by-construction signal is that `list`/`string` are the
only unbounded canonical-ABI types (the ones forcing dynamic `cabi_realloc`);
everything bounded uses a static return area. So the oracle asserts the
generated WIT for an all-scalar/record model contains no `list<`/`string`,
with an opaque-port model as the negative control (it deliberately keeps the
P2a `list<u8>` fallback and MUST contain it — proving the check can fire).
SCOPE: the no-alloc guarantee holds only for models whose ports are all
scalar/record; an opaque top-level port is out of the guarantee by design.
Deferred to a further requirement (data plane): populating ports from the
imported WIT functions and persistent component state (Guest methods are
free fns). Optional stronger evidence, not a completion gate: the crate also
builds to `wasm32-wasip2` (toolchain present locally).
status: implemented
tags: [codegen, wit, rust, wit-bindgen, no-alloc]
fields:
release: v0.24.0
links:
- type: traces-to
target: REQ-CODEGEN-WIT-RECORDS-001

- id: REQ-CODEGEN-VERIFY-WORKSPACE-001
type: requirement
title: "spar codegen --verify test/build emits inert artifacts"
description: >
Bug / honesty gap surfaced during REQ-CODEGEN-WIT-RECORDS-002: the
`--verify test` and `--verify build` (hence `--verify all`) outputs are
structurally inert in the generated multi-crate workspace and therefore
self-certify nothing. (1) test_gen emits `tests/<proc>/<thread>_test.rs`
with `use super::<thread>::*;` at the WORKSPACE ROOT — outside every crate
member, and `super::` is meaningless for a top-level file — so the harnesses
are never compiled or run. (2) build_gen emits `build.rs` + `aadl-contract.toml`
at the workspace ROOT, but workspace_gen makes a VIRTUAL workspace (a
`[workspace]`-only root with no `[package]`), and cargo never runs a build
script for a virtual manifest — so the timing-contract verifier never fires.
Both predate REQ-002 (they assume a flat single-crate layout that
workspace_gen does not produce). Fix: relocate the test harnesses under each
process crate (`crates/<proc>/tests/` or inline `#[cfg(test)]` modules using
the real crate paths, not `super::`), and move the contract build.rs +
manifest into each process crate so the build script actually runs. Until
then, `spar codegen --verify test/build/all` must not be read as producing a
runnable test/verification harness. Related to #319.
status: proposed
tags: [codegen, verify, workspace, bug]
fields:
release: v0.24.0
links:
- type: traces-to
target: REQ-CODEGEN-WIT-RECORDS-002

- id: REQ-PROOF-SCHED-001
type: requirement
title: "Lean scheduling proofs (EDF, RMBound, RTA) wired into CI"
Expand Down
89 changes: 89 additions & 0 deletions artifacts/verification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3900,6 +3900,95 @@ artifacts:
- type: satisfies
target: REQ-CODEGEN-WIT-TYPES

- id: TEST-CODEGEN-WIT-RECORDS
type: feature
title: AADL data implementations become no_alloc WIT records
description: >
wit_gen.rs unit oracles for REQ-CODEGEN-WIT-RECORDS-001 records path:
data_implementation_becomes_wit_record (EepromSnapshot.Impl = u32+u32+u8
-> `record eeprom-snapshot-impl { addr: u32, value: u32, flags: u8 }`, NOT
list<u8>), non_encodable_record_field_is_a_hard_error (3-byte + opaque
fields both reported, representable field not flagged),
record_fields_honor_signedness_and_float (temp: f32, offset: s32,
count: u16), and nested_record_emits_inner_record_and_references_it
(Header.Impl emitted dependency-first, referenced by name from Packet.Impl).
Each also feeds the output through wit-parser's Resolve::push_str, so the
records are valid bindable WIT, not just string-present.
fields:
method: automated-test
steps:
- run: cargo test -p spar-codegen --lib -- wit_gen
status: passing
tags: [codegen, wit, rust, interop, v0240]
links:
- type: satisfies
target: REQ-CODEGEN-WIT-RECORDS-001

- id: TEST-CODEGEN-WIT-BINDINGS
type: feature
title: WIT world <-> Rust Guest lifecycle wiring (compiler-enforced)
description: >
tests/lifecycle_bindings.rs for REQ-CODEGEN-WIT-RECORDS-001 items 4 & 7.
world_exports_dispatch_derived_lifecycle asserts the WIT world exports the
Dispatch_Protocol-derived lifecycle per subcomponent instance (Periodic
`pw` -> compute only; Sporadic `sw` -> initialize/compute/finalize),
confirming Dispatch_Protocol resolves per-instance and the old opaque
single-func export is gone. bindings_wire_generate_guest_and_export asserts
the emitted crate root has `wit_bindgen::generate!` + `impl Guest for
Component` (methods matching the world) + `export!(Component)`.
The COMPILER-ENFORCED gate emitted_crate_cargo_checks (#[ignore], run with
`-- --ignored`) writes the full generated workspace to a tempdir and runs
`cargo check`: wit-bindgen derives the Guest trait from the world, so a
name-mangling/keyword/collision mismatch in the emitter is a compile error
(verified GREEN: `Checking ctrl v0.1.0`; a deliberately renamed Guest
method fails with E0046/E0407). This closes the string-match gap that a
pure assertion oracle would leave. The `#[ignore]` gate runs in CI via the
`codegen-oracle` job (ci.yml) — `cargo nextest`/`cargo test` do NOT run
ignored tests, so without that job this oracle would not gate.
fields:
method: automated-test
steps:
- run: cargo test -p spar-codegen --test lifecycle_bindings
- run: cargo test -p spar-codegen --test lifecycle_bindings -- --ignored
status: passing
tags: [codegen, wit, rust, wit-bindgen, interop, v0240]
links:
- type: satisfies
target: REQ-CODEGEN-WIT-RECORDS-001

- id: TEST-CODEGEN-WIT-WHOLECRATE
type: feature
title: Whole generated crate compiles + no unbounded ABI types
description: >
tests/lifecycle_bindings.rs for REQ-CODEGEN-WIT-RECORDS-002. The NO-ALLOC
oracle is at the WIT level (a probe proved the wasm `cabi_realloc` symbol is
unconditional wit-bindgen-rt glue and cannot discriminate):
no_alloc_all_scalar_record_model_has_no_unbounded_wit_types asserts a
record + multi-process model emits no `list<`/`string`, and the negative
control no_alloc_opaque_port_model_does_contain_list_fallback asserts an
opaque top-level port DOES emit `list<u8>` (so the check can fire; opaque
ports are out of the no-alloc guarantee by design). The compiler-enforced
gate emitted_crate_cargo_checks (#[ignore]) now `cargo check`s the WHOLE
crate — the relocated per-thread modules (crates/{proc}/src/{thread}.rs),
the crate-local `types` record structs (derived from the same DataShape as
the WIT records), and the process `Guest` delegating to each per-thread
component — verified GREEN. emitted_crate_builds_to_wasm32_wasip2 (#[ignore],
stronger evidence, not a completion gate) builds the record + multi-process
crate to wasm32-wasip2 warning-clean. Both `#[ignore]` oracles run in CI via
the `codegen-oracle` job (ci.yml), which installs wasm32-wasip2 and runs
`-- --ignored` — plain `cargo nextest`/`cargo test` skip ignored tests, so
that job is what makes these compile oracles actually gate.
fields:
method: automated-test
steps:
- run: cargo test -p spar-codegen --test lifecycle_bindings
- run: cargo test -p spar-codegen --test lifecycle_bindings -- --ignored
status: passing
tags: [codegen, wit, rust, wit-bindgen, no-alloc, v0240]
links:
- type: satisfies
target: REQ-CODEGEN-WIT-RECORDS-002

- id: TEST-NC-TFA
type: feature
title: FP-TFA / TFA++ total-flow bounds, golden + soundness
Expand Down
5 changes: 4 additions & 1 deletion crates/spar-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2074,7 +2074,10 @@ fn cmd_codegen(args: &[String]) {
dry_run,
};

let result = spar_codegen::generate(&inst, &config);
let result = spar_codegen::generate(&inst, &scope, &config).unwrap_or_else(|e| {
eprint!("{e}");
process::exit(1);
});

if dry_run {
eprintln!(
Expand Down
19 changes: 13 additions & 6 deletions crates/spar-codegen/benches/codegen_benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,18 @@ fn synth_aadl(n_threads: usize) -> String {
}

/// Build a `SystemInstance` from an AADL source string.
fn build_instance(aadl: &str) -> SystemInstance {
fn build_instance(aadl: &str) -> (GlobalScope, SystemInstance) {
let db = HirDefDatabase::default();
let sf = SourceFile::new(&db, "bench.aadl".to_string(), aadl.to_string());
let tree = file_item_tree(&db, sf);
let scope = GlobalScope::from_trees(vec![tree]);
SystemInstance::instantiate(
let inst = SystemInstance::instantiate(
&scope,
&Name::new("BenchSystem"),
&Name::new("BenchRoot"),
&Name::new("impl"),
)
);
(scope, inst)
}

fn bench_codegen_emit(c: &mut Criterion) {
Expand All @@ -105,7 +106,7 @@ fn bench_codegen_emit(c: &mut Criterion) {

let n_threads = 64;
let aadl = synth_aadl(n_threads);
let instance = build_instance(&aadl);
let (scope, instance) = build_instance(&aadl);
let config = CodegenConfig {
root_name: "bench_root".into(),
output_dir: "output".into(),
Expand All @@ -121,7 +122,8 @@ fn bench_codegen_emit(c: &mut Criterion) {
// workspace, concatenated into in-memory strings.
group.bench_function("generate_full_64", |b| {
b.iter(|| {
let output = generate(black_box(&instance), black_box(&config));
let output = generate(black_box(&instance), black_box(&scope), black_box(&config))
.expect("bench codegen");
black_box(output);
});
});
Expand All @@ -136,7 +138,12 @@ fn bench_codegen_emit(c: &mut Criterion) {
};
group.bench_function("generate_rust_only_64", |b| {
b.iter(|| {
let output = generate(black_box(&instance), black_box(&rust_only_config));
let output = generate(
black_box(&instance),
black_box(&scope),
black_box(&rust_only_config),
)
.expect("bench codegen");
black_box(output);
});
});
Expand Down
12 changes: 9 additions & 3 deletions crates/spar-codegen/src/build_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ pub fn generate_build_verifier() -> GeneratedFile {
// constants. Divergence => panic! => compilation failure.
let script = r#"//! AADL build-time contract verification — generated by spar codegen.
//! DO NOT EDIT. Fails the build when a generated timing constant in
//! src/<process>/<thread>.rs diverges from aadl-contract.toml.
//! crates/<process>/src/<thread>.rs diverges from aadl-contract.toml.

use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

fn main() {
println!("cargo:rerun-if-changed=aadl-contract.toml");
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=crates");

let manifest = match fs::read_to_string("aadl-contract.toml") {
Ok(m) => m,
Expand Down Expand Up @@ -110,7 +110,13 @@ fn main() {
}

for (thread, expected) in &contract {
let src = Path::new("src").join(format!("{thread}.rs"));
// Manifest keys are "<process>/<thread>"; the source lives under that
// process crate at crates/<process>/src/<thread>.rs.
let (proc_dir, th_name) = thread.split_once('/').unwrap_or(("", thread.as_str()));
let src = Path::new("crates")
.join(proc_dir)
.join("src")
.join(format!("{th_name}.rs"));
let code = match fs::read_to_string(&src) {
Ok(c) => c,
Err(_) => panic!(
Expand Down
Loading
Loading