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

### Fixed

- **LS-A-9 regression coverage** (`meld-core/src/adapter/fact.rs`).
PR fixed the callback-mode `if code == WAIT` branch that silently
treated `POLL (3)` as a YIELD fall-through (dropping host-ready
events on the callback handshake), but landed without a dedicated
test. The LS-N verification gate surfaced this gap. Adds
`ls_a_9_callback_adapter_dispatches_both_wait_and_poll`, which
drives `generate_async_callback_adapter` end-to-end against a
minimal merged-module fixture (lift func + `[callback]` companion
+ `[waitable-set-poll]` import) and asserts the emitted body
contains the canonical `i32.const 2 / i32.eq / local.get … /
i32.const 3 / i32.eq / i32.or` byte sequence in order. Gate
verdict moves from 17/19 verified to **18/19** — only LS-A-8 left
in the missing bucket. **First PR to touch a Tier-5 file** since
`mythos-auto.yml`'s plumbing fixes (#164), so this also serves as
the auto-runner's first true end-to-end matrix-scan smoke test.

- **LS-CP-4 regression coverage + LS-N gate integration-test scan**
(`tools/run_ls_verification.py`,
`meld-core/tests/dwarf_strip.rs`). The LS-N verification gate was
Expand Down
112 changes: 112 additions & 0 deletions meld-core/src/adapter/fact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5640,4 +5640,116 @@ mod tests {
fn ls_a_10_cabi_align_retptr_writeback() {
cabi_alignment_stackful_retptr_writes_i64_at_offset_8();
}

/// LS-A-9: `generate_async_callback_adapter` must dispatch
/// `[waitable-set-poll]` on **both** `WAIT (2)` and `POLL (3)`.
/// The pre-fix `if code == WAIT` branch let POLL fall through to
/// the YIELD path, which sent `(EVENT_NONE, 0, 0)` to `[callback]`
/// and dropped any event the host had ready — silent semantic
/// drift between fused and composed modules with no host trap.
///
/// Asserts the WAIT/POLL OR-pattern is present in the emitted
/// adapter body: the byte sequence
///
/// local.get l_code (0x20 <leb128>)
/// i32.const WAIT=2 (0x41 0x02)
/// i32.eq (0x46)
/// local.get l_code (0x20 <leb128>)
/// i32.const POLL=3 (0x41 0x03)
/// i32.eq (0x46)
/// i32.or (0x72)
///
/// Pin-by-substring is robust against unrelated body changes
/// (locals layout, surrounding control flow) — what we care about
/// is that `i32.const 2 / i32.eq / i32.const 3 / i32.eq / i32.or`
/// appears in that order somewhere in the loop body.
#[test]
fn ls_a_9_callback_adapter_dispatches_both_wait_and_poll() {
let gen_ = FactStyleGenerator::new(AdapterConfig::default());
let mut merged = empty_merged();

// Type 0: () -> i32 — minimal lift signature matching the
// callback-mode return convention (packed callback code i32).
merged.types.push(crate::merger::MergedFuncType {
params: Vec::new(),
results: vec![wasm_encoder::ValType::I32],
});

// The lift function lives at merged index 0.
merged.functions.push(crate::merger::MergedFunction {
type_idx: 0,
body: wasm_encoder::Function::new([]),
origin: (1, 0, 0),
});
merged.function_index_map.insert((1, 0, 0), 0);

// The [callback] companion export — the callback adapter
// resolves this by name (`[callback]<export_name>`).
let export_name = "[async-lift]async_export";
merged.exports.push(crate::merger::MergedExport {
name: format!("[callback]{export_name}"),
kind: wasm_encoder::ExportKind::Func,
index: 0,
});

// Required host import — the adapter looks up
// [waitable-set-poll] by name prefix.
merged.imports.push(crate::merger::MergedImport {
module: "$root".into(),
name: "[waitable-set-poll]".into(),
entity_type: wasm_encoder::EntityType::Function(0),
component_idx: None,
});

let mut site = async_lift_site(export_name);
site.import_func_type_idx = Some(0);
site.crosses_memory = false;

let adapter = gen_
.generate_async_callback_adapter(&site, &merged)
.expect("callback emitter must succeed with [callback] + [waitable-set-poll] wired");

let body = adapter.body.into_raw_body();

// The WAIT/POLL OR-pattern as raw bytes. WAIT=2 / POLL=3 both
// fit in single-byte sleb128. We omit the `local.get l_code`
// bytes from the pattern (their leb128 encoding depends on
// local index) and assert the constant+compare+or skeleton
// appears in order.
const WAIT_POLL_OR_TAIL: &[u8] = &[
0x41, 0x02, // i32.const WAIT (2)
0x46, // i32.eq
0x20, // local.get … (l_code; one-byte leb when index<128)
];
const POLL_OR: &[u8] = &[
0x41, 0x03, // i32.const POLL (3)
0x46, // i32.eq
0x72, // i32.or
];

let wait_idx = body
.windows(WAIT_POLL_OR_TAIL.len())
.position(|w| w == WAIT_POLL_OR_TAIL)
.unwrap_or_else(|| {
panic!(
"callback adapter body must contain WAIT(2)/eq/local.get \
prefix of the OR-pattern; body={body:?}"
)
});
let poll_idx = body[wait_idx..]
.windows(POLL_OR.len())
.position(|w| w == POLL_OR)
.unwrap_or_else(|| {
panic!(
"callback adapter body must contain POLL(3)/eq/or \
tail of the OR-pattern AFTER the WAIT match at \
offset {wait_idx}; body={body:?}"
)
});
assert!(
poll_idx > 0,
"POLL_OR pattern must come after WAIT pattern in body \
(locals interleave between them); poll_idx={poll_idx}"
);
}
}
Loading