Skip to content

Commit ac9d6a2

Browse files
avrabeclaude
andauthored
feat(cli): reachable-graph closure follows call_indirect into table functions (#275) (#276)
--all-exports compiled the closure of the exports over direct `call` (#235), but a function reached only through `call_indirect` (a table entry) was invisible to that closure and omitted from the ELF — so a module dispatching through a function table is not self-contained (gale's falcon: 280 functions → 24 symbols). - Decoder: parse the Element section (`elem_func_indices` on DecodedModule) — the function indices that populate the table = the possible call_indirect targets. Empty for modules with no element section (byte-identical output). - Closure: reachable_from_exports unions in every table function once a reachable function performs a call_indirect (sound over-approximation), then follows their direct calls transitively. Verified on a minimal indirect-call module: the via-table-only target is now compiled in (was absent). Behavior-frozen: control_step 0x00210A55 / flight_seam 0x07FDF307 / div_const 338/338 all result-identical (no element section → closure unchanged). SCOPE: this is the reachability half. call_indirect DISPATCH in the select_with_stack path is still incomplete — the indirect call is dropped in selection, and the encoder's table-base uses R11 (collides with R11=mem-base). So a module using indirect dispatch gets its table functions present but the indirect call is not yet wired end-to-end. Direct-call modules are fully self-contained. Tracked as a follow-up. v0.11.33. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 890b99b commit ac9d6a2

15 files changed

Lines changed: 194 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.11.33] - 2026-06-05
11+
12+
**Whole-reachable-graph closure now follows `call_indirect` into table functions
13+
(#275, foundation — see the honest scope note).**
14+
15+
`--all-exports` compiled the closure of the exports over **direct** `call`
16+
(#235), but a function reached only through `call_indirect` (a table entry) was
17+
invisible to that closure and left out of the ELF — so a module that dispatches
18+
through a function table is not self-contained.
19+
20+
- **Decoder:** parse the Element section (`elem_func_indices` on `DecodedModule`)
21+
— the function indices that populate the table, i.e. the possible
22+
`call_indirect` targets. Empty for modules with no element section, so their
23+
output is byte-identical.
24+
- **Closure:** `reachable_from_exports` now unions in every table function once a
25+
reachable function performs a `call_indirect` (a sound over-approximation —
26+
any table entry could be the dynamic callee), then keeps following their direct
27+
calls transitively. Verified on a minimal indirect-call module: the
28+
call-only-via-table target is now compiled into the object (was absent).
29+
- **Behavior-frozen:** the three differential fixtures stay result-identical
30+
(`control_step` 0x00210A55, `flight_seam` 0x07FDF307, `div_const` 338/338) —
31+
they use no element section, so the closure is unchanged.
32+
33+
**Scope / known remaining (not in this release).** This lands the *reachability*
34+
half. `call_indirect` **dispatch** in the `select_with_stack` (`--all-exports`)
35+
path is still incomplete: the indirect call is currently dropped during
36+
selection, and the encoder's table-base expansion uses R11 — which collides with
37+
synth's R11 = linear-memory-base ABI. So a module using indirect dispatch now has
38+
its table functions *present* in the ELF but the indirect call itself is not yet
39+
wired end-to-end. Modules using only **direct** calls are fully self-contained.
40+
Tracked for a follow-up.
41+
42+
**Falsification:** wrong if a module using only direct `call` still omits a
43+
reachable internal callee, or if any fixture differential diverges.
44+
1045
## [0.11.32] - 2026-06-05
1146

1247
**`mul`+`add``mla` fusion now fires on the real `flat_flight` (#257 follow-up).**

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ resolver = "2"
2727
# semver to publish, so the convention now catches up: workspace
2828
# version follows the release tag, bumped pre-tag in the release
2929
# checklist. See docs/release-process.md.
30-
version = "0.11.32"
30+
version = "0.11.33"
3131
edition = "2024"
3232
rust-version = "1.88"
3333
authors = ["PulseEngine Team"]

MODULE.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module(
77
name = "synth",
88
# Kept in lockstep with [workspace.package] version in Cargo.toml.
99
# Both are bumped pre-tag — see docs/release-process.md.
10-
version = "0.11.32",
10+
version = "0.11.33",
1111
)
1212

1313
# Bazel dependencies

crates/synth-backend-awsm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ categories.workspace = true
1111
description = "aWsm backend integration for the Synth compiler"
1212

1313
[dependencies]
14-
synth-core = { path = "../synth-core", version = "0.11.32" }
14+
synth-core = { path = "../synth-core", version = "0.11.33" }
1515
anyhow.workspace = true
1616
thiserror.workspace = true

crates/synth-backend-riscv/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ categories.workspace = true
1111
description = "RISC-V encoder, ELF builder, PMP allocator, and bare-metal startup for synth"
1212

1313
[dependencies]
14-
synth-core = { path = "../synth-core", version = "0.11.32" }
15-
synth-synthesis = { path = "../synth-synthesis", version = "0.11.32" }
14+
synth-core = { path = "../synth-core", version = "0.11.33" }
15+
synth-synthesis = { path = "../synth-synthesis", version = "0.11.33" }
1616
anyhow.workspace = true
1717
thiserror.workspace = true
1818
tracing.workspace = true

crates/synth-backend-wasker/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ categories.workspace = true
1111
description = "Wasker backend integration for the Synth compiler"
1212

1313
[dependencies]
14-
synth-core = { path = "../synth-core", version = "0.11.32" }
14+
synth-core = { path = "../synth-core", version = "0.11.33" }
1515
anyhow.workspace = true
1616
thiserror.workspace = true

crates/synth-backend/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ default = ["arm-cortex-m"]
1515
arm-cortex-m = ["synth-synthesis"]
1616

1717
[dependencies]
18-
synth-core = { path = "../synth-core", version = "0.11.32" }
19-
synth-synthesis = { path = "../synth-synthesis", version = "0.11.32", optional = true }
18+
synth-core = { path = "../synth-core", version = "0.11.33" }
19+
synth-synthesis = { path = "../synth-synthesis", version = "0.11.33", optional = true }
2020
anyhow.workspace = true
2121
thiserror.workspace = true

crates/synth-cli/Cargo.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@ verify = ["synth-verify"]
2727
# Path deps carry `version` so `cargo publish` rewrites them to the
2828
# crates.io coordinate. Bumping the workspace version requires
2929
# updating these in lockstep — see docs/release-process.md.
30-
synth-core = { path = "../synth-core", version = "0.11.32" }
31-
synth-frontend = { path = "../synth-frontend", version = "0.11.32" }
32-
synth-synthesis = { path = "../synth-synthesis", version = "0.11.32" }
33-
synth-backend = { path = "../synth-backend", version = "0.11.32" }
30+
synth-core = { path = "../synth-core", version = "0.11.33" }
31+
synth-frontend = { path = "../synth-frontend", version = "0.11.33" }
32+
synth-synthesis = { path = "../synth-synthesis", version = "0.11.33" }
33+
synth-backend = { path = "../synth-backend", version = "0.11.33" }
3434

3535
# Optional external backends
36-
synth-backend-awsm = { path = "../synth-backend-awsm", version = "0.11.32", optional = true }
37-
synth-backend-wasker = { path = "../synth-backend-wasker", version = "0.11.32", optional = true }
38-
synth-backend-riscv = { path = "../synth-backend-riscv", version = "0.11.32", optional = true }
36+
synth-backend-awsm = { path = "../synth-backend-awsm", version = "0.11.33", optional = true }
37+
synth-backend-wasker = { path = "../synth-backend-wasker", version = "0.11.33", optional = true }
38+
synth-backend-riscv = { path = "../synth-backend-riscv", version = "0.11.33", optional = true }
3939

4040
# Optional verification (requires z3)
41-
synth-verify = { path = "../synth-verify", version = "0.11.32", optional = true, features = ["z3-solver", "arm"] }
41+
synth-verify = { path = "../synth-verify", version = "0.11.33", optional = true, features = ["z3-solver", "arm"] }
4242

4343
# Optional PulseEngine WASM optimizer
4444
# Uncomment when loom crate is available:

0 commit comments

Comments
 (0)