Skip to content

Commit f151f5a

Browse files
hyperpolymathclaude
andcommitted
feat(arch): make typed-wasm-verify optional + own the ownership codec in-tree
Group 2 of the estate disentangling: ephapax no longer has a hard mandatory dep on the typed-wasm repo. After this PR, ephapax builds + tests cleanly with `cargo build --no-default-features`, satisfying the architectural rule that typed-wasm must be removable from either language with no impact on the language itself. Design (user-decided 2026-05-26): 1. Section emission is ALWAYS ON — ownership is ephapax's own discipline (linear types are its core feature), not a typed-wasm concern. Ephapax now owns its own copy of the section codec. 2. typed-wasm-verify is an OPTIONAL dep, default-on, behind a feature flag of the same name. Powers the post-codegen `ephapax compile --verify-ownership` flag. 3. With the feature OFF, `--verify-ownership` becomes a no-op (warns and continues); the emitted wasm still carries the `typedwasm.ownership` section so any later off-tree verifier can pick it up. Changes: - New `src/ephapax-wasm/src/ownership.rs` (~210 LoC including 5 unit tests): * `OWNERSHIP_SECTION_NAME` constant (= "typedwasm.ownership") * `OwnershipKind` enum + `from_byte`/`to_byte` * `OwnershipEntry` struct * `build_ownership_section_payload` / `parse_ownership_section_payload` * `LenientReader` helper Byte-for-byte parity with `typed_wasm_verify::section`; any future wire-format change must be coordinated. - `src/ephapax-wasm/src/lib.rs`: 13 use sites flipped from `typed_wasm_verify::*` to `ownership::*` (production + test). Module re-exports the public surface so existing callers stay unchanged. - `src/ephapax-wasm/Cargo.toml`: removed `typed-wasm-verify` dep entirely. ephapax-wasm now has zero git deps on sibling repos. - `src/ephapax-cli/Cargo.toml`: `typed-wasm-verify` is now `{ workspace = true, optional = true }`; new `[features]` section with `default = ["typed-wasm-verify"]`. - `src/ephapax-cli/src/main.rs`: `report_ownership_verification` gated by `#[cfg(feature = "typed-wasm-verify")]`; call site at `compile_file` runs the verifier when feature ON, prints a stderr warning + continues when OFF. - `src/ephapax-cli/tests/cli_verify_ownership.rs`: the 2 positive tests (assert verifier output) gated by `#[cfg(feature = "typed-wasm-verify")]`. The negative test (assert verifier absence without `--verify-ownership`) stays unconditional. Build matrix verified locally: - `cargo build --workspace` → clean (default ON) - `cargo build -p ephapax-cli --no-default-features` → clean - `cargo test -p ephapax-wasm` → 84/84 passing (includes the 5 new ownership-codec unit tests) - `cargo test -p ephapax-cli` → 13/13 passing - `cargo test -p ephapax-cli --no-default-features` → 13/13 passing (the 2 verifier tests are cfg-skipped; non-verifier test still asserts the absence-of-verifier-output) Stacks on #137 (the typedwasm.ownership rename). When #137 merges, this PR will auto-retarget to main. Follow-up: add a `--no-default-features` CI job to ephapax's workflow to keep the dep-free build green automatically. Refs: estate disentangling Group 2. Closes the architectural violation flagged in `project_estate_disentangle_typedwasm.md`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1d95929 commit f151f5a

7 files changed

Lines changed: 298 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ephapax-cli/Cargo.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,28 @@ ephapax-wasm.workspace = true
2626
ephapax-interp.workspace = true
2727
ephapax-repl.workspace = true
2828
ephapax-package.workspace = true
29-
typed-wasm-verify.workspace = true
29+
# `typed-wasm-verify` is optional: it powers `ephapax compile
30+
# --verify-ownership`, which is a post-codegen L7+L10 verifier. With
31+
# the feature OFF the CLI builds + runs without any reference to the
32+
# typed-wasm repo; emitted wasm still carries the `typedwasm.ownership`
33+
# section (codec lives in ephapax-wasm/src/ownership.rs), but the
34+
# `--verify-ownership` flag becomes a no-op (warns and continues).
35+
typed-wasm-verify = { workspace = true, optional = true }
3036
smol_str.workspace = true
3137
clap.workspace = true
3238
colored.workspace = true
3339
ariadne.workspace = true
3440
serde_json.workspace = true
3541

42+
[features]
43+
default = ["typed-wasm-verify"]
44+
# Enabling this feature pulls in `typed-wasm-verify` (a git dep on
45+
# hyperpolymath/typed-wasm) and activates the post-codegen verifier
46+
# behind `ephapax compile --verify-ownership`. Disabling it removes
47+
# all references to typed-wasm and makes ephapax fully buildable
48+
# without any sibling-repo network access.
49+
"typed-wasm-verify" = ["dep:typed-wasm-verify"]
50+
3651
[dev-dependencies]
3752
wasmtime.workspace = true
3853
proptest = { workspace = true }

src/ephapax-cli/src/main.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,9 +567,23 @@ fn finish_compile(
567567

568568
// Run the typed-wasm L7+L10 verifier on the emitted module if
569569
// requested. Fails the build with a non-zero exit if any
570-
// ownership violation is found.
570+
// ownership violation is found. Gated by the `typed-wasm-verify`
571+
// feature: when off, --verify-ownership warns and continues.
571572
if verify_ownership {
572-
report_ownership_verification(&wasm_bytes, verbose)?;
573+
#[cfg(feature = "typed-wasm-verify")]
574+
{
575+
report_ownership_verification(&wasm_bytes, verbose)?;
576+
}
577+
#[cfg(not(feature = "typed-wasm-verify"))]
578+
{
579+
eprintln!(
580+
"{} --verify-ownership requested but this build was \
581+
compiled without the `typed-wasm-verify` feature; \
582+
skipping verification (emitted wasm still carries the \
583+
typedwasm.ownership section).",
584+
"warning:".yellow().bold()
585+
);
586+
}
573587
}
574588

575589
// Write output
@@ -611,6 +625,11 @@ fn finish_compile(
611625
/// has no ownership section, which both verifiers treat as fully
612626
/// unconstrained). Returns `Err` with a user-facing message if any
613627
/// violation is found — the caller surfaces this as a non-zero exit.
628+
///
629+
/// Gated by `feature = "typed-wasm-verify"`. With the feature off, the
630+
/// call site at `compile_file` skips this function entirely and emits
631+
/// a stderr warning instead.
632+
#[cfg(feature = "typed-wasm-verify")]
614633
fn report_ownership_verification(wasm_bytes: &[u8], verbose: bool) -> Result<(), String> {
615634
use typed_wasm_verify::{verify_from_module, VerifyError};
616635

src/ephapax-cli/tests/cli_verify_ownership.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88
// in `typed-wasm-verify`; this test exists to prove the CLI plumbing
99
// works — flag parsing → post-compile verification call → exit code
1010
// → user-facing output.
11+
//
12+
// The two positive-assertion tests are gated by the
13+
// `typed-wasm-verify` feature: with the feature OFF, the CLI prints a
14+
// warning and skips verification, so the "verification: clean" line
15+
// the tests look for is absent (by design). The third test —
16+
// `compile_without_verify_flag_does_not_run_verifier` — works
17+
// regardless because it asserts an *absence* of verifier output.
1118

1219
use std::process::Command;
1320

1421
fn ephapax_bin() -> String {
1522
env!("CARGO_BIN_EXE_ephapax").to_string()
1623
}
1724

25+
#[cfg(feature = "typed-wasm-verify")]
1826
#[test]
1927
fn verify_ownership_clean_module() {
2028
let src = tempfile::NamedTempFile::with_suffix(".eph").expect("tempfile");
@@ -48,6 +56,7 @@ fn verify_ownership_clean_module() {
4856
);
4957
}
5058

59+
#[cfg(feature = "typed-wasm-verify")]
5160
#[test]
5261
fn verify_ownership_linear_program() {
5362
// A program that takes a linear String param and consumes it

src/ephapax-wasm/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@ categories.workspace = true
1717
ephapax-syntax = { workspace = true }
1818
ephapax-ir = { workspace = true }
1919
smol_str = { workspace = true }
20-
typed-wasm-verify = { workspace = true }
2120
wasm-encoder = { workspace = true }
2221
gimli = { version = "0.31", features = ["write"] }
2322
object = "0.36"
2423
serde = { version = "1", features = ["derive"] }
2524
serde_json = "1"
2625

26+
# NOTE: this crate intentionally does NOT depend on `typed-wasm-verify`.
27+
# Ephapax owns its own copy of the `typedwasm.ownership` section codec
28+
# in `src/ownership.rs`. Ownership annotations are ephapax's own
29+
# discipline (linear types); `typed-wasm-verify` is an optional
30+
# post-codegen verifier consumed only by `ephapax-cli` behind a
31+
# feature flag. See `src/ownership.rs` header for the full rationale.
32+
2733
[dev-dependencies]
2834
wasmparser = "0.221"

src/ephapax-wasm/src/lib.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,13 @@
4545
//! Mode is set per-module compilation and affects how unused linear values are handled.
4646
4747
mod debug;
48+
pub mod ownership;
4849

4950
pub use debug::{DebugInfo, VariableMetadata};
51+
pub use ownership::{
52+
build_ownership_section_payload, parse_ownership_section_payload, OwnershipEntry,
53+
OwnershipKind, OWNERSHIP_SECTION_NAME,
54+
};
5055

5156
use ephapax_ir::module_from_sexpr;
5257
use ephapax_syntax::{
@@ -1277,7 +1282,7 @@ impl Codegen {
12771282
/// missing entries as fully Unrestricted, so the smaller payload
12781283
/// has identical semantics.
12791284
fn emit_ownership_section(&mut self) {
1280-
let mut entries: Vec<typed_wasm_verify::OwnershipEntry> = self
1285+
let mut entries: Vec<ownership::OwnershipEntry> = self
12811286
.user_fns
12821287
.values()
12831288
.filter(|info| info.param_linear.iter().any(|&l| l))
@@ -1287,16 +1292,16 @@ impl Codegen {
12871292
.iter()
12881293
.map(|&is_linear| {
12891294
if is_linear {
1290-
typed_wasm_verify::OwnershipKind::Linear
1295+
ownership::OwnershipKind::Linear
12911296
} else {
1292-
typed_wasm_verify::OwnershipKind::Unrestricted
1297+
ownership::OwnershipKind::Unrestricted
12931298
}
12941299
})
12951300
.collect();
1296-
typed_wasm_verify::OwnershipEntry {
1301+
ownership::OwnershipEntry {
12971302
func_idx: info.wasm_fn_idx,
12981303
param_kinds,
1299-
ret_kind: typed_wasm_verify::OwnershipKind::Unrestricted,
1304+
ret_kind: ownership::OwnershipKind::Unrestricted,
13001305
}
13011306
})
13021307
.collect();
@@ -1306,9 +1311,9 @@ impl Codegen {
13061311
// `user_fns` is a HashMap; sort by func_idx for deterministic output.
13071312
entries.sort_by_key(|e| e.func_idx);
13081313

1309-
let payload = typed_wasm_verify::build_ownership_section_payload(&entries);
1314+
let payload = ownership::build_ownership_section_payload(&entries);
13101315
let custom = wasm_encoder::CustomSection {
1311-
name: typed_wasm_verify::OWNERSHIP_SECTION_NAME.into(),
1316+
name: ownership::OWNERSHIP_SECTION_NAME.into(),
13121317
data: payload.as_slice().into(),
13131318
};
13141319
self.module.section(&custom);
@@ -4580,7 +4585,7 @@ mod tests {
45804585
for payload in WP::new(0).parse_all(wasm) {
45814586
if let Payload::CustomSection(reader) = payload.expect("wasm parse")
45824587
{
4583-
if reader.name() == typed_wasm_verify::OWNERSHIP_SECTION_NAME {
4588+
if reader.name() == ownership::OWNERSHIP_SECTION_NAME {
45844589
return Some(reader.data().to_vec());
45854590
}
45864591
}
@@ -4609,15 +4614,15 @@ mod tests {
46094614
assert_wasm_header(&wasm);
46104615

46114616
let payload = ownership_payload(&wasm).expect("ownership section missing");
4612-
let entries = typed_wasm_verify::parse_ownership_section_payload(&payload);
4617+
let entries = ownership::parse_ownership_section_payload(&payload);
46134618
assert_eq!(entries.len(), 1, "expected one ownership entry");
46144619
assert_eq!(
46154620
entries[0].param_kinds,
4616-
vec![typed_wasm_verify::OwnershipKind::Linear]
4621+
vec![ownership::OwnershipKind::Linear]
46174622
);
46184623
assert_eq!(
46194624
entries[0].ret_kind,
4620-
typed_wasm_verify::OwnershipKind::Unrestricted
4625+
ownership::OwnershipKind::Unrestricted
46214626
);
46224627
}
46234628

@@ -4683,7 +4688,7 @@ mod tests {
46834688
};
46844689
let wasm = compile_module(&module).expect("compilation failed");
46854690
let payload = ownership_payload(&wasm).expect("ownership section missing");
4686-
let entries = typed_wasm_verify::parse_ownership_section_payload(&payload);
4691+
let entries = ownership::parse_ownership_section_payload(&payload);
46874692
assert_eq!(entries.len(), 2);
46884693
assert!(
46894694
entries[0].func_idx < entries[1].func_idx,
@@ -4720,11 +4725,11 @@ mod tests {
47204725
};
47214726
let wasm = compile_module(&module).expect("compilation failed");
47224727
let payload = ownership_payload(&wasm).expect("ownership section missing");
4723-
let entries = typed_wasm_verify::parse_ownership_section_payload(&payload);
4728+
let entries = ownership::parse_ownership_section_payload(&payload);
47244729
assert_eq!(entries.len(), 1, "only the Linear fn should be listed");
47254730
assert_eq!(
47264731
entries[0].param_kinds,
4727-
vec![typed_wasm_verify::OwnershipKind::Linear]
4732+
vec![ownership::OwnershipKind::Linear]
47284733
);
47294734
}
47304735

0 commit comments

Comments
 (0)