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
52 changes: 48 additions & 4 deletions majit/majit-backend-wasm/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2376,7 +2376,10 @@ fn value_id_end(inputargs: &[InputArg], ops: &[Op]) -> u32 {
///
/// `TempVar` ids live in a reserved high strip and constants in their own
/// namespace; neither indexes a value local, so both pass through unchanged.
fn rebase_region_value_ids(bridge: &InlinedBridge, offset: u32) -> (InlinedBridge, u32) {
fn rebase_region_value_ids(
bridge: &InlinedBridge,
offset: u32,
) -> Result<(InlinedBridge, u32), BackendError> {
use majit_ir::operand::Operand;

let shift = |r: OpRef| -> OpRef {
Expand All @@ -2388,6 +2391,20 @@ fn rebase_region_value_ids(bridge: &InlinedBridge, offset: u32) -> (InlinedBridg
};

let width = value_id_end(&bridge.inputargs, &bridge.ops);
// `with_raw` keeps the variant, but the emitters classify by raw payload
// (`OpRef::raw_is_constant`), so an id shifted to or past the limit reads
// as a constant and its result is skipped. Decline instead: the merged
// stream is an optimization, and no renumbering is correct once the
// region's range no longer fits below the limit.
if offset
.checked_add(width)
.is_none_or(|end| end > OpRef::VALUE_ID_LIMIT)
{
return Err(BackendError::Unsupported(format!(
"wasm backend: inlined bridge value ids exceed the value-id space \
(offset {offset}, width {width})"
)));
}
let inputargs: Vec<InputArg> = bridge
.inputargs
.iter()
Expand Down Expand Up @@ -2423,7 +2440,7 @@ fn rebase_region_value_ids(bridge: &InlinedBridge, offset: u32) -> (InlinedBridg
}
}

(
Ok((
InlinedBridge {
source_fail_index: bridge.source_fail_index,
trace_id: bridge.trace_id,
Expand All @@ -2433,7 +2450,7 @@ fn rebase_region_value_ids(bridge: &InlinedBridge, offset: u32) -> (InlinedBridg
constants: bridge.constants.clone(),
},
width,
)
))
}

/// Build a wasm module from majit IR.
Expand Down Expand Up @@ -2486,7 +2503,7 @@ pub fn build_wasm_module(
rebased_constants = constants.clone();
let mut next_value_id = value_id_end(inputargs, ops);
for bridge in inlined_bridges {
let (bridge, width) = rebase_region_value_ids(bridge, next_value_id);
let (bridge, width) = rebase_region_value_ids(bridge, next_value_id)?;
// The pool is keyed by value position for a folded value with no
// producing op, so rebasing the region's ids moved its reads off
// its own entries. Replay that window at the offset, and drop a
Expand Down Expand Up @@ -2545,6 +2562,33 @@ pub fn build_wasm_module(
"wasm backend: inlined bridge stream has an empty region".into(),
));
}
// A region can carry a CALL_ASSEMBLER this build has no arm for. The
// dedicated arm is selected by `ca.emit_ca`, which is decided when the
// OWNER is compiled, and it reads the callee's geometry out of
// `ca.targets`; a region merged in later brings its own callee. An op
// that misses that arm does not fail — it falls through to the ordinary
// residual-call arm, which lowers arg 0 as an
// `__indirect_function_table` slot, and a CALL_ASSEMBLER's arg 0 is the
// callee's first frame slot. That calls whatever the slot happens to
// index and returns its result as the callee's, which is a silent wrong
// answer rather than a trap. `wasm_unsupported_trace_reason` asks this
// question of every trace's own ops; the merged stream is the one place
// it is never re-asked, so ask it here.
for op in &bridge.ops {
if !op.opcode.is_call_assembler() {
continue;
}
let target = op
.getdescr()
.and_then(|descr| descr.as_call_descr().and_then(|d| d.call_target_token()));
if !ca.emit_ca || target.is_none_or(|token| !ca.targets.contains_key(&token)) {
return Err(BackendError::Unsupported(format!(
"wasm backend: inlined bridge carries {:?}, which the owner \
build has no CALL_ASSEMBLER arm for",
op.opcode
)));
}
}
let source_guard = guards
.get(bridge.source_fail_index as usize)
.ok_or_else(|| {
Expand Down
168 changes: 92 additions & 76 deletions majit/majit-backend-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ use std::sync::{Arc, Mutex};
/// because the source module has frame-only dispatch; 46 = parameter entry
/// declined because the source guard and bridge input arities disagree; 47 =
/// LABEL publication suppressed because the bridge entry has nonzero parameters.
/// 48 = an inline trial's LABEL-resume storage exceeds the frozen frame.
pub static BRIDGE_DIAG: [AtomicU64; 49] = [const { AtomicU64::new(0) }; 49];
/// 48 = an inline trial's LABEL-resume storage exceeds the frozen frame; 49 =
/// the region carries a CALL_ASSEMBLER the owner build emits no arm for.
pub static BRIDGE_DIAG: [AtomicU64; 50] = [const { AtomicU64::new(0) }; 50];

#[repr(u8)]
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -117,6 +118,10 @@ impl FrameShortage {
/// shortage without changing the compile result.
static INLINE_GEOMETRY: [AtomicU64; 3] = [const { AtomicU64::new(0) }; 3];
static INLINE_GEOMETRY_COUNT: AtomicU64 = AtomicU64::new(0);
/// The first three reasons an inline-bridge install was refused, verbatim.
/// The names carry "trial" because they are a guest export the runner looks up
/// by string; the errors themselves come from the install itself, which is the
/// only build there is.
static INLINE_TRIAL_ERRORS: Mutex<Vec<String>> = Mutex::new(Vec::new());

pub(crate) fn record_inline_geometry(kind: FrameShortageKind, needed: usize, available: usize) {
Expand Down Expand Up @@ -157,6 +162,39 @@ fn record_inline_trial_error(error: &BackendError) {
}
}

/// Sort a refused inline install into the decline tallies the host prints.
/// `replace_module` rejecting the bytes, or a build with no host binding to
/// replace them through, is a re-emission outcome and stays on its own counter;
/// every other reason is the merged module declining to emit, which is what the
/// per-shortage buckets are for.
fn classify_inline_install_error(error: &BackendError) {
let BackendError::Unsupported(reason) = error else {
diag_bump(37);
diag_bump(43);
return;
};
if reason.contains("wasm host rejected the re-emitted trace module")
|| reason.contains("no host replacement binding")
{
diag_bump(30);
return;
}
diag_bump(37);
if reason.contains("frame value slots exceed frozen frame layout") {
diag_bump(40);
} else if reason.contains("ordinary ref homes") {
diag_bump(41);
} else if reason.contains("label resume layout") {
diag_bump(48);
} else if reason.contains("no CALL_ASSEMBLER arm for") {
diag_bump(49);
} else if reason.contains("inlined bridge stream has no local loop LABEL") {
diag_bump(42);
} else {
diag_bump(43);
}
}

static REEMIT_ENABLED: AtomicBool = AtomicBool::new(false);
static INLINE_BRIDGE_ENABLED: AtomicBool = AtomicBool::new(false);
static BRIDGE_PARAMS_ENABLED: AtomicBool = AtomicBool::new(true);
Expand Down Expand Up @@ -3349,84 +3387,62 @@ impl majit_backend::Backend for WasmBackend {
candidate.classptr_to_typeid = self.collect_classptr_typeid_table(&merged_ops);
candidate.guard_gc_type_info = self.collect_guard_gc_type_info(&merged_ops);
candidate.nursery = nursery_alloc_params(&merged_ops);
match codegen::build_wasm_module(&candidate) {
Err(ref error @ BackendError::Unsupported(ref reason)) => {
record_inline_trial_error(error);
diag_bump(37);
if reason.contains("frame value slots exceed frozen frame layout") {
diag_bump(40);
} else if reason.contains("ordinary ref homes") {
diag_bump(41);
} else if reason.contains("label resume layout") {
diag_bump(48);
} else if reason
.contains("inlined bridge stream has no local loop LABEL")
{
diag_bump(42);
} else {
diag_bump(43);
let source_loop = original_token
.compiled
.get()
.and_then(|c| c.downcast_ref::<CompiledWasmLoop>())
.expect("source loop disappeared before inline install");
// The local branch supersedes any previous direct-cell
// dispatch for this guard. Remove it before reemit so
// the fresh array cannot replay a contradictory slot.
let old_bridge_slot = source_loop
.bridge_slots
.borrow_mut()
.remove(&source_fail_index);
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
if source_cells_base != 0 {
let cell = (source_cells_base as usize + source_fail_index as usize * 4)
as *mut u32;
unsafe { core::ptr::write(cell, 0) };
}
// Eligibility IS the emission: `reemit_loop` runs the same
// `build_wasm_module` over the same candidate, and nothing
// it does before that call mutates state a failure would
// have to unwind — it reads the fail-index base and
// allocates a cell array that is dropped on the error path.
// So install directly and let the build answer, instead of
// asking it once as a trial and once for real.
let old_inputs = source_loop.reemit.replace(Some(candidate));
match self.reemit_loop(original_token) {
Ok(()) => {
self.trace_counter += 1;
if let Some(table) = gc_table {
Self::register_gc_table(original_token, table);
}
diag_bump(31);
diag_bump(32);
return Ok(AsmInfo {
code_addr: 0,
code_size: 0,
});
}
Err(ref error @ BackendError::CompilationFailed(_)) => {
record_inline_trial_error(error);
diag_bump(37);
diag_bump(43);
}
Ok(_) => {
let source_loop = original_token
.compiled
.get()
.and_then(|c| c.downcast_ref::<CompiledWasmLoop>())
.expect("source loop disappeared before inline install");
// The local branch supersedes any previous direct-cell
// dispatch for this guard. Remove it before reemit so
// the fresh array cannot replay a contradictory slot.
let old_bridge_slot = source_loop
.bridge_slots
.borrow_mut()
.remove(&source_fail_index);
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
if source_cells_base != 0 {
let cell = (source_cells_base as usize
+ source_fail_index as usize * 4)
as *mut u32;
unsafe { core::ptr::write(cell, 0) };
}
let old_inputs = source_loop.reemit.replace(Some(candidate));
match self.reemit_loop(original_token) {
Ok(()) => {
self.trace_counter += 1;
if let Some(table) = gc_table {
Self::register_gc_table(original_token, table);
}
diag_bump(31);
diag_bump(32);
return Ok(AsmInfo {
code_addr: 0,
code_size: 0,
});
}
Err(_) => {
source_loop.reemit.replace(old_inputs);
if let Some(slot) = old_bridge_slot {
source_loop
.bridge_slots
.borrow_mut()
.insert(source_fail_index, slot);
#[cfg(all(
target_arch = "wasm32",
not(target_os = "wasi")
))]
if source_cells_base != 0 {
let cell = (source_cells_base as usize
+ source_fail_index as usize * 4)
as *mut u32;
unsafe { core::ptr::write(cell, slot) };
}
}
diag_bump(30);
Err(error) => {
source_loop.reemit.replace(old_inputs);
if let Some(slot) = old_bridge_slot {
source_loop
.bridge_slots
.borrow_mut()
.insert(source_fail_index, slot);
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
if source_cells_base != 0 {
let cell = (source_cells_base as usize
+ source_fail_index as usize * 4)
as *mut u32;
unsafe { core::ptr::write(cell, slot) };
}
}
record_inline_trial_error(&error);
classify_inline_install_error(&error);
}
}
}
Expand Down
Loading
Loading