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
1 change: 1 addition & 0 deletions meld-core/src/adapter/fact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12883,6 +12883,7 @@ mod tests {
tables: Vec::new(),
memories: Vec::new(),
globals: Vec::new(),
defined_global_i32_const: std::collections::HashMap::new(),
exports: Vec::new(),
start_function: None,
elements: Vec::new(),
Expand Down
29 changes: 29 additions & 0 deletions meld-core/src/merger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ pub struct MergedModule {
/// Merged globals
pub globals: Vec<MergedGlobal>,

/// #353 (static PIC): merged (absolute) global index → constant i32 value,
/// for **defined** globals whose init folds to a constant i32 (e.g. a
/// `__memory_base` a `$main` module provides). Consumed by the data/element
/// offset fold in `ParsedConstExpr::reindex` via `IndexMaps`.
pub defined_global_i32_const: std::collections::HashMap<u32, i32>,

/// Merged exports
pub exports: Vec<MergedExport>,

Expand Down Expand Up @@ -1011,6 +1017,7 @@ impl Merger {
tables: Vec::new(),
memories: Vec::new(),
globals: Vec::new(),
defined_global_i32_const: std::collections::HashMap::new(),
exports: Vec::new(),
start_function: None,
elements: Vec::new(),
Expand Down Expand Up @@ -1665,6 +1672,20 @@ impl Merger {
merged,
&global.content_type,
);
// #353: record this DEFINED global's constant i32 value (if any) so a
// data/element offset that `global.get`s it (a post-fusion
// `__memory_base`) can be folded to `i32.const` — a data const-expr
// cannot `global.get` a defined global. Restricted to IMMUTABLE
// globals: a `__memory_base` base is immutable, and folding an init
// value is only unambiguously the segment-init-time value for a
// constant, non-mutable global. (Active segments are initialised
// before any start function, so even a mutable const-init would read
// its init value — but immutable removes all doubt.)
if !global.mutable
&& let Some(v) = crate::segments::const_i32_init_value(&global.init_expr_bytes)
{
merged.defined_global_i32_const.insert(new_idx, v);
}
let ty = convert_global_type(global, comp_idx, mod_idx, merged);
merged.globals.push(MergedGlobal { ty, init_expr });
}
Expand Down Expand Up @@ -1974,6 +1995,12 @@ impl Merger {
elem_segment_base,
code_addr_relocs,
);
// #353: hand the data/element offset reindex the set of DEFINED
// constant-i32 globals so a `global.get` of a post-fusion `__memory_base`
// in an offset is folded to `i32.const` (imported globals stay verbatim).
index_maps
.defined_global_i32_consts
.clone_from(&merged.defined_global_i32_const);
// #298: only under the upstream vestigial-allocator verdict does a
// `memory.grow` reached during rebasing become `unreachable` (the
// allocator is provably dead) instead of a hard error. Inert when
Expand Down Expand Up @@ -3954,6 +3981,7 @@ mod tests {
tables: Vec::new(),
memories: Vec::new(),
globals: Vec::new(),
defined_global_i32_const: std::collections::HashMap::new(),
exports: Vec::new(),
start_function: None,
elements: Vec::new(),
Expand Down Expand Up @@ -6011,6 +6039,7 @@ mod tests {
tables: Vec::new(),
memories: Vec::new(),
globals: Vec::new(),
defined_global_i32_const: std::collections::HashMap::new(),
exports: Vec::new(),
start_function: None,
elements: Vec::new(),
Expand Down
9 changes: 9 additions & 0 deletions meld-core/src/rewriter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ pub struct IndexMaps {
/// `None` preserves the zero-cost legacy path exactly: no const rebasing,
/// and blanket `memarg` rebasing (a no-op when `memory_base_offset == 0`).
pub code_addr_relocs: Option<std::collections::HashSet<u32>>,
/// #353 (static PIC): merged (post-remap) global index → constant i32 value,
/// for **defined** globals whose init folds to a constant i32 (e.g. a
/// `__memory_base` provided by a `$main` module). A data/element segment
/// offset may `global.get` only an *imported* global; after fusion such a
/// base global becomes *defined*, so `ParsedConstExpr::reindex` folds a
/// `global.get` of one of these to `i32.const <value>` (imported globals are
/// absent from this map and stay verbatim, preserving the #338 behaviour).
/// Empty by default → no folding, so every other caller is unaffected.
pub defined_global_i32_consts: std::collections::HashMap<u32, i32>,
}

#[derive(Debug, Clone, Copy)]
Expand Down
38 changes: 37 additions & 1 deletion meld-core/src/segments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,30 @@ pub enum ParsedConstExpr {
ExtendedGlobalGet(Vec<ExtConstOp>),
}

/// If a global's init const-expr bytes (WITHOUT the trailing `End`) fold to a
/// constant `i32` — a bare `i32.const N`, or the wasm-2.0 extended-const
/// `i32.const N (± M)*` with no embedded `global.get` — return that value.
/// Anything with a `global.get` (runtime-dependent) or a non-i32 type → `None`.
///
/// Used by the merger to record which merged **defined** globals are constant
/// i32s, so a data/element offset that `global.get`s one (a post-fusion
/// `__memory_base`) can be folded to `i32.const` (#353).
pub(crate) fn const_i32_init_value(init_bytes: &[u8]) -> Option<i32> {
let mut full = init_bytes.to_vec();
full.push(0x0B); // append End so wasmparser sees a complete const-expr
let reader = wasmparser::BinaryReader::new(&full, 0);
let expr = wasmparser::ConstExpr::new(reader);
let mut ops = expr.get_operators_reader();
match ops.read().ok()? {
Operator::I32Const { value } => match fold_extended_const_i32(&mut ops, value).ok()? {
ExtConstFold::Value(v) => Some(v),
// Embeds a `global.get` (`base + N`) → not a pure constant here.
ExtConstFold::Extended(_) => None,
},
_ => None,
}
}

impl ParsedConstExpr {
/// Convert this parsed const expression into a `wasm_encoder::ConstExpr`
pub fn to_const_expr(&self) -> ConstExpr {
Expand All @@ -192,7 +216,19 @@ impl ParsedConstExpr {
pub fn reindex(&self, maps: &IndexMaps) -> ParsedConstExpr {
match self {
ParsedConstExpr::RefFunc(idx) => ParsedConstExpr::RefFunc(maps.remap_func(*idx)),
ParsedConstExpr::GlobalGet(idx) => ParsedConstExpr::GlobalGet(maps.remap_global(*idx)),
ParsedConstExpr::GlobalGet(idx) => {
// #353 (static PIC): a data/element offset may `global.get` only
// an IMPORTED global. After fusion a `__memory_base`-style base
// (imported by a dylib, defined by `$main`) becomes DEFINED, so a
// `global.get` of it here would be rejected by wasmtime. Fold it
// to the concrete `i32.const` base. Imported globals are absent
// from the map and stay a verbatim `global.get` (#338).
let new = maps.remap_global(*idx);
match maps.defined_global_i32_consts.get(&new) {
Some(&value) => ParsedConstExpr::I32Const(value),
None => ParsedConstExpr::GlobalGet(new),
}
}
ParsedConstExpr::ExtendedGlobalGet(ops) => ParsedConstExpr::ExtendedGlobalGet(
ops.iter()
.map(|o| o.remap_global(|i| maps.remap_global(i)))
Expand Down
1 change: 1 addition & 0 deletions meld-core/tests/mcu_dissolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn merged_base() -> MergedModule {
tables: Vec::new(),
memories: Vec::new(),
globals: Vec::new(),
defined_global_i32_const: std::collections::HashMap::new(),
exports: Vec::new(),
start_function: None,
elements: Vec::new(),
Expand Down
159 changes: 159 additions & 0 deletions meld-core/tests/shared_everything_topology.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! ADR-7 path-H inc 4 (#353) — static PIC / shared-everything flattening.
//!
//! BASELINE + FINDING. Against a real `wasm-tools component link` shared-everything
//! output (a PIC dylib with `(data (global.get $__memory_base) …)` linked so
//! `$main` owns+exports the memory and `$__init` imports it), current meld
//! (post ADR-7 inc 1–3) already:
//! - models the instance-level memory sharing → the fused core has ONE memory
//! (NOT the two the #353 spike observed on its fixture), and
//! - folds `global.get $__memory_base` → `i32.const <base>` in globals/data via
//! the #338 extended-const machinery, producing a VALID single core module.
//!
//! So the spike's "mints 2 memories / needs new topology modeling" premise does
//! not reproduce here. The one thing this cannot yet assert is end-to-end address
//! *correctness* (does the dylib's base-relative data read back right at runtime):
//! the linked component lifts no exports, so there is nothing to execute — exactly
//! the gap the spike flagged as "the one remaining verification". Closing it needs
//! a WIT-lifted executable PIC fixture. This test pins the structural baseline so
//! any regression to 2 memories / invalid output is caught meanwhile.

use meld_core::{Fuser, FuserConfig, MemoryStrategy};

fn base_config() -> FuserConfig {
FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
custom_sections: meld_core::CustomSectionHandling::Drop,
dwarf_handling: meld_core::DwarfHandling::Strip,
output_format: meld_core::OutputFormat::CoreModule,
opaque_resources: Vec::new(),
}
}

#[test]
fn shared_everything_fuses_to_valid_single_memory_core() {
// Skip gracefully if the fixture isn't present (same convention as
// nested_component.rs) so CI never breaks on a missing fixture.
let Ok(component) = std::fs::read("../tests/pic-fixtures/shared_everything_linked.wasm") else {
eprintln!("skipping: shared_everything_linked.wasm fixture not present");
return;
};
let mut fuser = Fuser::new(base_config());
fuser
.add_component_named(&component, Some("shared-everything"))
.unwrap();
let (fused, _) = fuser
.fuse_with_stats()
.expect("shared-everything PIC component must fuse");

// Structural baseline: exactly one memory (the shared-everything memory is
// unified, not duplicated).
let mut mems = 0;
let mut has_base_folded_data = false;
for p in wasmparser::Parser::new(0).parse_all(&fused) {
match p {
Ok(wasmparser::Payload::MemorySection(r)) => mems = r.count(),
Ok(wasmparser::Payload::DataSection(r)) => {
// At least one active data segment sits at the folded base
// constant (0x100000), i.e. `global.get __memory_base` was
// folded to an i32.const rather than left as an (invalid at
// runtime) locally-defined global.get.
for d in r.into_iter().flatten() {
if let wasmparser::DataKind::Active { offset_expr, .. } = d.kind {
let mut ops = offset_expr.get_operators_reader();
if let Ok(wasmparser::Operator::I32Const { value }) = ops.read()
&& value == 0x0010_0000
{
has_base_folded_data = true;
}
}
}
}
_ => {}
}
}
assert_eq!(mems, 1, "shared-everything memory must be unified to one");
assert!(
has_base_folded_data,
"expected a data segment folded to the base constant 0x100000 \
(global.get __memory_base folded to i32.const)"
);
assert!(
wasmparser::Validator::new().validate_all(&fused).is_ok(),
"fused shared-everything core must validate"
);
}

/// Executable base-fold oracle (hand-written PIC-pattern, no toolchain needed):
/// `$main` sets `__memory_base` = 0x10000 and owns the memory; `$lib` imports
/// both and holds a base-relative data segment `(data (global.get $base) …)` +
/// a `read` loading from the base. meld must fold `global.get __memory_base` →
/// `i32.const 0x10000` in BOTH the data offset and the load, so `read` returns
/// the data at the folded base. Proves end-to-end address CORRECTNESS (not just
/// validity) for the static-PIC base-folding path.
fn pic_exec_component() -> Vec<u8> {
let wat = r#"
(component
(core module $main
(global (export "__memory_base") i32 (i32.const 65536))
(memory (export "memory") 2))
(core module $lib
(import "env" "memory" (memory 1))
(import "env" "__memory_base" (global $base i32))
(data (global.get $base) "\aa\bb\cc\dd")
(func (export "read") (result i32) (i32.load (global.get $base))))
(core instance $mi (instantiate $main))
(core instance $li (instantiate $lib (with "env" (instance $mi))))
(alias core export $li "read" (core func $f))
(func $lift (result u32) (canon lift (core func $f)))
(export "read" (func $lift)))
"#;
wat::parse_str(wat).expect("PIC-pattern component WAT must assemble")
}

// Static-PIC data-offset fold (#353). Before the fix, meld emitted the fused
// data-segment offset as `(data (global.get $__memory_base) …)` verbatim — valid
// under wasm-tools (lenient) but REJECTED by wasmtime ("constant expression
// required: global.get of locally defined global"), because after fusion the
// dylib's imported `__memory_base` becomes a DEFINED global and a data const-expr
// may `global.get` only an imported one. The fix folds a `global.get` of a
// defined constant-i32 global to `i32.const <value>` in offset emission
// (`ParsedConstExpr::reindex`), so the base-relative data lands at — and is read
// from — the concrete folded base. Imported globals stay verbatim (#338).
#[test]
fn pic_base_relative_data_reads_correctly_after_fold() {
use wasmtime::{Config, Engine, Instance, Module as RuntimeModule, Store};
let component = pic_exec_component();
let mut fuser = Fuser::new(base_config());
fuser
.add_component_named(&component, Some("pic-exec"))
.unwrap();
let (fused, _) = fuser
.fuse_with_stats()
.expect("PIC-pattern component must fuse");
assert!(
wasmparser::Validator::new().validate_all(&fused).is_ok(),
"fused PIC output must validate"
);

let mut cfg = Config::new();
cfg.wasm_multi_memory(true);
let engine = Engine::new(&cfg).unwrap();
let module = RuntimeModule::new(&engine, &fused).unwrap();
let mut store = Store::new(&engine, ());
let inst = Instance::new(&mut store, &module, &[]).unwrap();
let read = inst
.get_typed_func::<(), i32>(&mut store, "read")
.expect("fused module should export read");
// "\aa\bb\cc\dd" little-endian i32 = 0xddccbbaa. Correct iff the base-relative
// data landed at the folded base and the load reads the same folded base.
assert_eq!(
read.call(&mut store, ()).unwrap() as u32,
0xddcc_bbaa,
"base-relative data must read back correctly after __memory_base fold"
);
}
Binary file added tests/pic-fixtures/shared_everything_linked.wasm
Binary file not shown.
Loading