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
2 changes: 1 addition & 1 deletion docs/verification-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
| SR-14 | Correct memory index usage in adapters | partial | — | `proofs/adapter/` |
| SR-15 | Correct list copy length | partial | `adapter_safety::test_sr15_list_copy_length` | — |
| SR-16 | Recursive inner pointer fixup | partial | `adapter_safety::test_sr16_inner_pointer_fixup_list_string` | — |
| SR-17 | Correct string transcoding | verified | `adapter_safety::test_sr17_utf8_to_utf16_string_transcoding`<br>`adapter_safety::test_sr17_utf8_to_utf16_supplementary_plane_transcoding`<br>`adapter_safety::test_sr17_utf16_to_utf8_supplementary_plane_transcoding`<br>`adapter_safety::test_sr17_utf16_to_utf8_lone_high_surrogate_replacement`<br>`adapter_safety::test_sr17_utf16_to_utf8_midstring_lone_surrogate_replacement`<br>`adapter_safety::test_sr17_utf16_to_utf8_malformed_surrogate_matrix`<br>`adapter_safety::test_sr17_utf8_to_utf16_malformed_matrix`<br>`adapter_safety::test_sr17_latin1_to_utf16_transcoding`<br>`adapter_safety::test_253_read_latin1_tagclear_to_utf16`<br>`adapter_safety::test_253_read_latin1_tagclear_to_utf8`<br>`adapter_safety::test_253_read_latin1_tagset_to_utf16`<br>`adapter_safety::test_253_read_latin1_tagset_to_utf8_supplementary`<br>`adapter_safety::test_253_write_utf8_to_latin1_fits`<br>`adapter_safety::test_253_write_utf8_to_latin1_needs_utf16`<br>`adapter_safety::test_253_write_utf16_to_latin1_fits`<br>`adapter_safety::test_253_write_utf16_to_latin1_needs_utf16_supplementary`<br>`adapter_safety::ls_p_21_latin1_utf16_tag_honored_roundtrip`<br>`resolver::tests::test_sr17_all_encoding_pairs_transcoding_matrix` | — |
| SR-17 | Correct string transcoding | verified | `adapter_safety::test_sr17_utf8_to_utf16_string_transcoding`<br>`adapter_safety::test_sr17_utf8_to_utf16_supplementary_plane_transcoding`<br>`adapter_safety::test_sr17_utf16_to_utf8_supplementary_plane_transcoding`<br>`adapter_safety::test_sr17_utf16_to_utf8_lone_high_surrogate_replacement`<br>`adapter_safety::test_sr17_utf16_to_utf8_midstring_lone_surrogate_replacement`<br>`adapter_safety::test_sr17_utf16_to_utf8_malformed_surrogate_matrix`<br>`adapter_safety::test_sr17_utf8_to_utf16_malformed_matrix`<br>`adapter_safety::test_sr17_latin1_to_utf16_transcoding`<br>`adapter_safety::test_253_read_latin1_tagclear_to_utf16`<br>`adapter_safety::test_253_read_latin1_tagclear_to_utf8`<br>`adapter_safety::test_253_read_latin1_tagset_to_utf16`<br>`adapter_safety::test_253_read_latin1_tagset_to_utf8_supplementary`<br>`adapter_safety::test_253_write_utf8_to_latin1_fits`<br>`adapter_safety::test_253_write_utf8_to_latin1_needs_utf16`<br>`adapter_safety::test_253_write_utf16_to_latin1_fits`<br>`adapter_safety::test_253_write_utf16_to_latin1_needs_utf16_supplementary`<br>`adapter_safety::ls_p_21_latin1_utf16_tag_honored_roundtrip`<br>`resolver::tests::test_sr17_all_encoding_pairs_transcoding_matrix`<br>`adapter_safety::test_361_same_memory_utf8_to_utf16_transcoding` (same-memory, #361) | — |
| SR-18 | Adapter instruction ordering | not-verified | — | — |
| SR-19 | Deterministic output | partial | `tests::test_deterministic_output`<br>`resolver::tests::test_resolver_preserves_order_stability` | — |
| SR-20 | Fail-fast on unresolvable state | partial | `tests::test_fuser_empty_components_error`<br>`tests::test_fuser_rejects_core_module_input`<br>`tests::test_fuser_address_rebasing_requires_shared_memory`<br>`tests::test_fuser_rejects_invalid_wasm` | — |
Expand Down
66 changes: 24 additions & 42 deletions meld-core/src/adapter/call_lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
//! loud on the identical case (#272), so this closes the sync gap consistently.

use super::AdapterClass;
use crate::{Error, Result};
use crate::Result;

/// The facts about one call boundary that determine how it is lowered. All are
/// cheap booleans read from the resolved [`AdapterSite`](crate::resolver::AdapterSite)
Expand Down Expand Up @@ -83,41 +83,25 @@ pub struct CallLoweringPlan {

/// Resolve how a single cross-component call boundary is lowered.
///
/// Class selection (unchanged from the pre-extraction `generate_adapter`):
/// - cross-memory + encoding mismatch → `Transcode`
/// Class selection:
/// - encoding mismatch (needs transcoding) → `Transcode`, whether or not the
/// boundary crosses memory (#361 — the transcode adapter is
/// memory-index-parameterised, so it transcodes correctly within one shared
/// memory as well as across two)
/// - cross-memory, same encoding → `MemoryCopy`
/// - same memory → `Direct`
/// - same memory, same encoding → `Direct`
///
/// and `inline_eligible` iff inlining is on, the class is `Direct`, and the call
/// carries no resource conversions and no post-return.
///
/// # Errors
///
/// Hard-fails ([`Error::AdapterGeneration`](crate::Error::AdapterGeneration))
/// when the boundary needs transcoding but does **not** cross memory
/// (`needs_transcoding && !crosses_memory`). A same-memory boundary is lowered
/// as a `Direct` shim that does not transcode, so emitting one for a
/// mixed-encoding call would deliver string bytes verbatim in the wrong encoding
/// — a silent miscompile. This is the sync twin of `guard_async_cross_encoding_strings`
/// (the async path already fails loud here, #272). Supporting same-memory
/// transcoding is tracked in #361; until then we refuse rather than corrupt.
pub fn resolve_call_lowering_plan(facts: BoundaryFacts) -> Result<CallLoweringPlan> {
// #361: a boundary that needs transcoding but does not cross memory has no
// lowering that transcodes — `Direct` (the same-memory class) is a thin
// shim. Fail loud instead of silently mis-transcoding (mirrors the async
// guard, #272).
if facts.needs_transcoding && !facts.crosses_memory {
return Err(Error::AdapterGeneration(
"same-memory string transcoding is not supported: this call requires \
transcoding (caller and callee use different string encodings) but \
does not cross a memory boundary, so it would be lowered as a Direct \
shim that copies the bytes verbatim — silently mis-transcoding. The \
async path already fails loud here (#272); see #361 to support it"
.to_string(),
));
}

let class = if facts.crosses_memory && facts.needs_transcoding {
// #361: transcoding is required whenever the encodings differ, regardless of
// whether memory is crossed. Route to `Transcode`; the adapter reads from
// `caller_memory` and writes the transcoded bytes to `callee_memory` (both
// the one shared memory under `--memory shared`), so a same-memory boundary
// transcodes correctly rather than falling through to a verbatim `Direct`
// copy. (This lifts the #360 hard-fail, which was the loud-not-silent
// placeholder until this support landed.)
let class = if facts.needs_transcoding {
AdapterClass::Transcode
} else if facts.crosses_memory {
AdapterClass::MemoryCopy
Expand Down Expand Up @@ -190,20 +174,18 @@ mod tests {
}

#[test]
fn same_memory_transcoding_hard_fails() {
// #361: a boundary that needs transcoding but does not cross memory has
// no lowering that transcodes (Direct is a thin shim). Emitting one
// would silently mis-transcode, so the seam refuses — the sync twin of
// the async cross-encoding guard (#272).
let err = resolve_call_lowering_plan(BoundaryFacts {
fn same_memory_transcoding_routes_to_transcode() {
// #361: a boundary that needs transcoding but does NOT cross memory now
// routes to `Transcode` (the transcode adapter is memory-index-
// parameterised and transcodes within one shared memory), rather than
// hard-failing or falling through to a verbatim `Direct` copy.
let plan = resolve_call_lowering_plan(BoundaryFacts {
needs_transcoding: true,
..direct()
})
.unwrap_err();
assert!(
matches!(err, Error::AdapterGeneration(_)),
"expected AdapterGeneration hard-fail, got {err:?}"
);
.unwrap();
assert_eq!(plan.class, AdapterClass::Transcode);
assert!(!plan.inline_eligible, "Transcode is never inlinable");
}

#[test]
Expand Down
59 changes: 59 additions & 0 deletions meld-core/tests/adapter_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2095,6 +2095,65 @@ fn test_sr17_utf8_to_utf16_string_transcoding() {
);
}

/// #361: the SAME transcoding, but under `SharedMemory` — the caller and callee
/// share one linear memory, so `crosses_memory` is false. #360 hard-failed this
/// (the class dispatch only reached `Transcode` when memory was crossed); this
/// proves same-memory transcoding now works: the transcode adapter is
/// memory-index-parameterised (src reads `caller_memory`, dst writes
/// `callee_memory` — both 0 here), so it transcodes UTF-8 → UTF-16 within the
/// one shared memory. `run()` must still return 500.
#[test]
fn test_361_same_memory_utf8_to_utf16_transcoding() {
let callee = build_callee_utf16_string_component();
let caller = build_caller_string_component();

let config = FuserConfig {
memory_strategy: MemoryStrategy::SharedMemory,
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(),
};

let mut fuser = Fuser::new(config);
fuser
.add_component_named(&callee, Some("callee-utf16"))
.expect("callee should parse");
fuser
.add_component_named(&caller, Some("caller-utf8"))
.expect("caller should parse");

let (fused, _stats) = fuser
.fuse_with_stats()
.expect("#361: same-memory cross-encoding fusion should now succeed");

let mut validator = wasmparser::Validator::new();
validator
.validate_all(&fused)
.expect("#361: fused output should validate");

let mut engine_config = Config::new();
engine_config.wasm_multi_memory(true);
let engine = Engine::new(&engine_config).unwrap();
let module = RuntimeModule::new(&engine, &fused).unwrap();
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[]).unwrap();
let run = instance
.get_typed_func::<(), i32>(&mut store, "run")
.expect("#361: fused module should export 'run'");
let result = run.call(&mut store, ()).unwrap();
assert_eq!(
result, 500,
"#361: same-memory UTF-8→UTF-16 'Hello' must transcode to 500, not \
silently mis-copy"
);
}

/// SR-17 (surrogate-pair clause): a supplementary-plane code point
/// (U+10000 and above) transcoded UTF-8 → UTF-16 must be encoded as a
/// surrogate PAIR — two code units — not a single truncated unit. The
Expand Down
Loading