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
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,47 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.41.0] - 2026-07-15

MCU-dissolve completion + a const-expr correctness fix. v0.40.0 made
`--memory shared` *correct* (addresses rebased, #326); this release makes the
fused node *cleanly dissolvable* on a real MCU and fixes a silent miscompile of
the position-independent const-expr shape that lowering relies on. Together with
#326, gale's gust:os node fuses **and** dissolves with no hand WAT surgery.

**Falsification:** if SP-coalescing regressed, `shared_stack_pointer_runtime_non_clobber`
(two providers' frames alias) fails. If the `cabi_realloc` drop deferred a *live*
grow, `live_internal_grow_keeps_realloc_and_hard_errors` fails. If the const-expr
truncation returned, `test_338_multimodule_const_first_embedded_globalget_remaps`
(reads the un-remapped base 5100 instead of 1100) fails.

### Added

- **`--memory shared` MCU-dissolve fixups (#334).** A post-merge pass coalesces
the N per-component `__stack_pointer` globals into one shared shadow stack
(leaving them separate is unsound — providers clobber each other's frames) and
drops the dead wac lowered-import shim trampoline's keep-alive export so synth
DCEs it. The fused node now lowers with zero closed-world/shadow-stack
warnings. SP detection uses only the authoritative `__stack_pointer` name (an
init-value heuristic was rejected — it could mis-coalesce unrelated globals).
- **Vestigial `cabi_realloc` drop (#298).** When a component boundary is fully
internalised (core output, no adapters, all lifts scalar) AND the allocator is
provably dead, `meld fuse` drops the `cabi_realloc*` exports and defers the
dead `memory.grow` — unblocking the lean `--memory shared --address-rebase`
fuse (which otherwise hard-errors on `memory.grow`). A call-graph reachability
gate ensures a component that allocates internally keeps its allocator and the
clean compile error rather than trapping at runtime.

### Fixed

- **Silent miscompile of `global.get` extended-const exprs (#338).** `meld fuse`
truncated any wasm-2.0 extended-const expression containing a `global.get`
(both `base + N` and `N + base`) to just `global.get $base`, dropping the
arithmetic — corrupting global initializers and data/element-segment offsets
(the PIE `__memory_base + N` shape). The full expression is now preserved and
re-emitted with global indices remapped; a load-bearing ordering fix ensures
the imported-global remap is populated before init-exprs are converted.

## [0.40.0] - 2026-07-11

MCU-lowering release: `fuse --memory shared` is now **sound** for relocatable
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.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exclude = [
]

[workspace.package]
version = "0.40.0"
version = "0.41.0"
authors = ["PulseEngine <https://github.com/pulseengine>"]
edition = "2024"
license = "Apache-2.0"
Expand Down
66 changes: 39 additions & 27 deletions safety/requirements/safety-requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1737,7 +1737,7 @@ artifacts:
budget-fitting relocatable object — gale's gust:os v0.4.0 buffer node fits
the STM32F100 8 KiB SRAM budget without the hand WAT surgery gale currently
applies.
status: proposed
status: verified
tags: [memory-strategy, shared-memory, mcu, dissolve, dce, v0.41.0]
links:
- type: derives-from
Expand All @@ -1755,15 +1755,19 @@ artifacts:
- meld-core/src/rewriter.rs
verification-method: test
verification-description: >
PLANNED. (1) A fused two-provider shared-memory module exposes exactly
one mutable `i32` `__stack_pointer`-class global (init == sp_init), and
every use of a coalesced duplicate is redirected to the survivor
(global-index remap verified; a runtime check that two providers' frames
do not alias). (2) The fused output contains no exported `call_indirect`
trampoline over an import table — the dead shim's keep-alive export is
dropped so it DCEs. End-to-end oracle on the gale app-tl repro: the fused
module lowers under `synth ... --native-pointer-abi --shadow-stack-size`
with zero closed-world/shadow-stack warnings and fits the 8 KiB budget.
VERIFIED (#343, merged 2026-07-14). meld-core/tests/mcu_dissolve.rs:
(1) `stack_pointer_globals_coalesced` — a fused module ends with exactly
one mutable-i32 SP-class global and every `global.get`/`global.set` of a
coalesced duplicate is redirected to the survivor;
`shared_stack_pointer_runtime_non_clobber` (wasmtime) — pre-coalesce two
providers' frames alias, post-coalesce they do not.
(2) `dead_import_trampoline_export_dropped` — the dead numeric-name
`call_indirect`-over-import trampoline export is dropped while real/vtable
exports are kept. `conservative_no_coalesce_when_unsure` — single-SP,
differing-init, AND unnamed same-init globals are NOT coalesced (Mythos
review: coalescing uses only the authoritative `__stack_pointer` name).
End-to-end synth-dissolve on the gale app-tl node is gale-side downstream
validation (gale#168), pending gale's probe re-run.

- id: SR-50
type: sw-req
Expand All @@ -1788,7 +1792,7 @@ artifacts:
hard-failing), so the lean single-address-space fuse succeeds. Over-drop
is silent marshalling corruption, so the verdict's conservatism is
load-bearing and must not be weakened.
status: proposed
status: verified
tags: [memory-strategy, shared-memory, mcu, dce, realloc, v0.41.0]
links:
- type: derives-from
Expand All @@ -1807,16 +1811,19 @@ artifacts:
- meld-core/src/rewriter.rs
verification-method: test
verification-description: >
PLANNED. Positive: a fused fully-internalised core (scalar lifts, no
adapters) exports no `cabi_realloc*` and, under `--memory shared
--address-rebase`, fuses successfully (the vestigial `memory.grow` is
deferred, not a hard error) — verified by re-parsing the output exports
and asserting the fuse returns Ok. Negative controls (the verdict's
conservatism): a component with a NON-scalar lift (string/list param or
result), a P2 component-wrapped output, or a fuse with adapter sites all
KEEP `cabi_realloc` and do not defer grow. A live-allocator fuse still
rejects `memory.grow` under rebasing (no behaviour change off the
provably-safe path).
VERIFIED (#345, merged 2026-07-14). meld-core/tests/drop_realloc.rs +
memory_probe unit tests. Positive `scalar_boundary_drops_realloc_and_defers_grow`:
a fused fully-internalised core exports no `cabi_realloc*` and fuses Ok
under `--memory shared --address-rebase` (the vestigial `memory.grow` is
deferred, not a hard error). Negative controls (verdict conservatism):
`string_lift`/`component_output`/`adapter_site`_keeps_realloc_and_hard_errors
all KEEP `cabi_realloc` and hard-error on grow. Mythos-added
`live_internal_grow_keeps_realloc_and_hard_errors`: a scalar-interface
component that allocates internally KEEPS realloc (the call-graph
reachability gate `module_has_reachable_memory_grow` — 11 unit tests —
prevents deferring a LIVE grow to a runtime trap).
`tight_match_preserves_realloc_lookalike_export` guards the digits-only
name match. `non_shared_fuse_preserves_realloc` guards the off-path.
- id: SR-51
type: sw-req
title: Extended-const exprs beginning with global.get are preserved, not truncated
Expand All @@ -1832,7 +1839,7 @@ artifacts:
`global.get`-first case. A truncated offset is a silent miscompile: the
module still validates but places data / initialises globals at `base + 0`
instead of `base + N`.
status: proposed
status: verified
tags: [const-expr, miscompile, pie, correctness, v0.41.0]
links:
- type: derives-from
Expand All @@ -1850,9 +1857,14 @@ artifacts:
- meld-core/src/merger.rs
verification-method: test
verification-description: >
PLANNED. Executed-vs-wasmtime oracles reproducing #338: a global
initializer `g = base + 100` (base=1000) yields 1100 (not 1000) after
fuse; a data-segment active offset `base + N` places the segment at
base+N. Negative: the const-first case and a bare `global.get` (no
trailing arithmetic) are unchanged.
VERIFIED (#346, merged 2026-07-15). meld-core/tests/const_expr_globalget.rs,
executed-vs-wasmtime: a global initializer `g = base + 100` (base=1000)
yields 1100 (not the pre-fix 1000); the operand-swapped
`test_338_multimodule_const_first_embedded_globalget_remaps` — a
multi-module fuse shifting `__memory_base` off import-index 0 — reads the
correct base (1100), not the pre-fix un-remapped `__stack_pointer` (5100);
data-segment offsets `base+N` and `N+base` land at base+N (pre-fix
truncated to base+0 / hard-errored). Negative: bare `global.get` and the
const-first pure fold are unchanged. Two Mythos passes (the second caught
the operand-swapped sibling + the imported-global index-map ordering bug).

Loading