diff --git a/docs/verification-matrix.md b/docs/verification-matrix.md
index 96e4005..c3d9796 100644
--- a/docs/verification-matrix.md
+++ b/docs/verification-matrix.md
@@ -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`
`adapter_safety::test_sr17_utf8_to_utf16_supplementary_plane_transcoding`
`adapter_safety::test_sr17_utf16_to_utf8_supplementary_plane_transcoding`
`adapter_safety::test_sr17_utf16_to_utf8_lone_high_surrogate_replacement`
`adapter_safety::test_sr17_utf16_to_utf8_midstring_lone_surrogate_replacement`
`adapter_safety::test_sr17_utf16_to_utf8_malformed_surrogate_matrix`
`adapter_safety::test_sr17_utf8_to_utf16_malformed_matrix`
`adapter_safety::test_sr17_latin1_to_utf16_transcoding`
`adapter_safety::test_253_read_latin1_tagclear_to_utf16`
`adapter_safety::test_253_read_latin1_tagclear_to_utf8`
`adapter_safety::test_253_read_latin1_tagset_to_utf16`
`adapter_safety::test_253_read_latin1_tagset_to_utf8_supplementary`
`adapter_safety::test_253_write_utf8_to_latin1_fits`
`adapter_safety::test_253_write_utf8_to_latin1_needs_utf16`
`adapter_safety::test_253_write_utf16_to_latin1_fits`
`adapter_safety::test_253_write_utf16_to_latin1_needs_utf16_supplementary`
`adapter_safety::ls_p_21_latin1_utf16_tag_honored_roundtrip`
`resolver::tests::test_sr17_all_encoding_pairs_transcoding_matrix` | — |
+| SR-17 | Correct string transcoding | verified | `adapter_safety::test_sr17_utf8_to_utf16_string_transcoding`
`adapter_safety::test_sr17_utf8_to_utf16_supplementary_plane_transcoding`
`adapter_safety::test_sr17_utf16_to_utf8_supplementary_plane_transcoding`
`adapter_safety::test_sr17_utf16_to_utf8_lone_high_surrogate_replacement`
`adapter_safety::test_sr17_utf16_to_utf8_midstring_lone_surrogate_replacement`
`adapter_safety::test_sr17_utf16_to_utf8_malformed_surrogate_matrix`
`adapter_safety::test_sr17_utf8_to_utf16_malformed_matrix`
`adapter_safety::test_sr17_latin1_to_utf16_transcoding`
`adapter_safety::test_253_read_latin1_tagclear_to_utf16`
`adapter_safety::test_253_read_latin1_tagclear_to_utf8`
`adapter_safety::test_253_read_latin1_tagset_to_utf16`
`adapter_safety::test_253_read_latin1_tagset_to_utf8_supplementary`
`adapter_safety::test_253_write_utf8_to_latin1_fits`
`adapter_safety::test_253_write_utf8_to_latin1_needs_utf16`
`adapter_safety::test_253_write_utf16_to_latin1_fits`
`adapter_safety::test_253_write_utf16_to_latin1_needs_utf16_supplementary`
`adapter_safety::ls_p_21_latin1_utf16_tag_honored_roundtrip`
`resolver::tests::test_sr17_all_encoding_pairs_transcoding_matrix`
`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`
`resolver::tests::test_resolver_preserves_order_stability` | — |
| SR-20 | Fail-fast on unresolvable state | partial | `tests::test_fuser_empty_components_error`
`tests::test_fuser_rejects_core_module_input`
`tests::test_fuser_address_rebasing_requires_shared_memory`
`tests::test_fuser_rejects_invalid_wasm` | — |
diff --git a/meld-core/src/adapter/call_lowering.rs b/meld-core/src/adapter/call_lowering.rs
index db3efcb..e4615fd 100644
--- a/meld-core/src/adapter/call_lowering.rs
+++ b/meld-core/src/adapter/call_lowering.rs
@@ -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)
@@ -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 {
- // #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
@@ -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]
diff --git a/meld-core/tests/adapter_safety.rs b/meld-core/tests/adapter_safety.rs
index 7c328b9..5252407 100644
--- a/meld-core/tests/adapter_safety.rs
+++ b/meld-core/tests/adapter_safety.rs
@@ -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