diff --git a/majit/majit-backend-wasm/src/codegen.rs b/majit/majit-backend-wasm/src/codegen.rs index afae932f3b8..950fbeee84e 100644 --- a/majit/majit-backend-wasm/src/codegen.rs +++ b/majit/majit-backend-wasm/src/codegen.rs @@ -8,12 +8,9 @@ /// offset 8: slot[0] (i64) /// offset 16: slot[1] (i64) /// ... -/// CALL_AREA_OFS: func_ptr (i64) — used by jit_call trampoline -/// CALL_AREA_OFS+8: num_args (i64) -/// CALL_AREA_OFS+16: arg[0] (i64) -/// CALL_AREA_OFS+24: arg[1] (i64) -/// ... -/// CALL_RESULT_OFS: result (i64) — written by host after call +/// +/// The residual-call trampoline scratch is stored separately at the static +/// base returned by `jit_call_area_addr`. use std::collections::HashMap; use majit_backend::BackendError; @@ -39,14 +36,18 @@ const CALL_FUNC_OFS: u64 = 2008; const CALL_NARGS_OFS: u64 = 2016; const CALL_ARGS_OFS: u64 = 2024; +const STATIC_CALL_RESULT_OFS: u64 = 0; +const STATIC_CALL_FUNC_OFS: u64 = SLOT_SIZE; +const STATIC_CALL_NARGS_OFS: u64 = 2 * SLOT_SIZE; +const STATIC_CALL_ARGS_OFS: u64 = 3 * SLOT_SIZE; + /// Minimum frame allocation size in bytes to accommodate the call area. pub const MIN_FRAME_BYTES: usize = 2024 + 16 * 8; // 16 max call args -/// Per-token layout of a wasm execution frame. Every frozen geometry carries -/// the host-trampoline call area, so a later chained bridge can use it without -/// changing its source token's frame offsets. CA callee frames alone allocate -/// the prefix ending after the Ref homes; the tail is protected by the -/// trampoline-decline floor in `compile_bridge`. +/// Per-token layout of a wasm execution frame. Every frozen geometry retains +/// the historical host-trampoline call area even though emitted code uses the +/// module-static scratch area. CA callee frames allocate only the prefix ending +/// after the Ref homes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FrameGeometry { /// Number of value slots before the dispatch key (including frame[0]). @@ -69,7 +70,7 @@ pub struct FrameGeometry { /// use the low slots. The whole home region remains covered by jf_gcmap. pub label_ref_slots: usize, /// Bytes through the end of Ref homes. CA callee frames allocate exactly - /// this many item bytes; the tail call area is intentionally omitted. + /// this many item bytes; the unused tail call area is intentionally omitted. pub ca_frame_bytes: u32, /// Full bytes in the frame layout, including the tail call area. Host entry /// frames and every chained bridge use this geometry and allocation size. @@ -77,7 +78,7 @@ pub struct FrameGeometry { } impl FrameGeometry { - const CALL_AREA_SLOTS: usize = 3 + 16; // result, function, nargs, args + pub const CALL_AREA_SLOTS: usize = 3 + 16; // result, function, nargs, args /// Historical fixed geometry, used by direct codegen tests and by callers /// that deliberately need the arena-compatible layout. @@ -178,14 +179,17 @@ fn memarg(offset: u64, align: u32) -> MemArg { } } -/// Invoke the residual-call trampoline. The historical import receives only a -/// frame pointer and therefore reads the fixed call area; compact frames use a -/// second import carrying their call-area base. The old trampoline remains -/// unchanged for fixed-layout frames. -fn emit_jit_call(sink: &mut InstructionSink<'_>, jit_call_idx: u32, frame: FrameGeometry) { - if frame.call_result_ofs != CALL_RESULT_OFS { - sink.i32_const(frame.call_result_ofs as i32); - } +fn emit_call_area_addr(sink: &mut InstructionSink<'_>) { + sink.i32_const(crate::jit_call_area_addr() as i32); +} + +/// Invoke the residual-call trampoline, which reads its scratch at +/// `base + offset`. The scratch no longer lives in the frame, so the pair is +/// always the static call area at offset zero, and the base-only import — whose +/// host side adds a baked `CALL_RESULT_OFS` — can no longer be used. +fn emit_jit_call(sink: &mut InstructionSink<'_>, jit_call_idx: u32) { + emit_call_area_addr(sink); + sink.i32_const(0); sink.call(jit_call_idx); } @@ -743,7 +747,6 @@ fn emit_write_barrier( residual_type_base: Option, wb_fn_ptr: i64, base_ref: OpRef, - frame: FrameGeometry, ) { if let Some(base) = residual_type_base { // Header word is a u64 at `obj - GcHeader::SIZE` with the flags in @@ -776,21 +779,20 @@ fn emit_write_barrier( return; }; // func_ptr = wasm_jit_write_barrier - sink.local_get(0); + emit_call_area_addr(sink); sink.i64_const(wb_fn_ptr); - sink.i64_store(mem64(frame.call_func_ofs)); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); // num_args = 1 (the trampoline reflects arity from the wasm signature; // written for protocol symmetry with the alloc/call paths) - sink.local_get(0); + emit_call_area_addr(sink); sink.i64_const(1); - sink.i64_store(mem64(frame.call_nargs_ofs)); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); // arg0 = base object pointer - sink.local_get(0); + emit_call_area_addr(sink); emit_resolve(sink, constants, value_types, base_ref); - sink.i64_store(mem64(frame.call_args_ofs)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS)); // call trampoline; void result ignored - sink.local_get(0); - emit_jit_call(sink, jit_call, frame); + emit_jit_call(sink, jit_call); } /// Per-value def / last-use op positions over the trace, used to filter the @@ -1249,17 +1251,14 @@ fn has_call_ops(ops: &[Op]) -> bool { } /// Whether this trace emits a host `jit_call` / `jit_call_compact` trampoline -/// invocation. CA frames are movable nursery objects, while the host -/// trampoline writes its result back through the pre-call frame pointer, so -/// `compile_bridge` uses this exact lowering census to keep such traces off a -/// live CA frame. +/// invocation and therefore needs the corresponding function import. /// /// Keep this in lockstep with the individual emission arms below: the uniform /// i64 and typed float residual families, `New*`, and write barriers are direct /// under `WASM_DIRECT_RESIDUAL_CALL`; non-uniform CALLs and string allocation /// retain the trampoline. When the direct family is disabled, all of the /// existing call-area users return to the trampoline baseline. -pub fn has_trampoline_calls(inputargs: &[InputArg], ops: &[Op], emit_ca: bool) -> bool { +fn has_trampoline_calls(inputargs: &[InputArg], ops: &[Op], emit_ca: bool) -> bool { let ref_values = RefValues::collect(inputargs, ops); if !WASM_DIRECT_RESIDUAL_CALL { return has_call_ops(ops) || has_ref_store_op(ops, &ref_values); @@ -1469,11 +1468,8 @@ pub struct CaTarget { pub dispatch_entry: u32, /// Bytes to reserve per CA callee frame (the GC `JitFrame`'s data region, /// i.e. its Signed item area). This is the source geometry's prefix through - /// the Ref homes, excluding its tail call area. The trampoline-decline - /// floor in `WasmBackend::compile_bridge` (`source_ca_active && - /// bridge_has_trampoline_calls`) guarantees that no trampoline-lowered op - /// runs on this movable frame, so the omitted tail is unreachable. The alloc - /// trampoline derives the JitFrame item count from this exact byte count. + /// the Ref homes, excluding its unused tail call area. The alloc trampoline + /// derives the JitFrame item count from this exact byte count. pub callee_frame_bytes: u32, /// Leaked per-bridge `jf_gcmap` (`lib.rs::build_callee_gcmap`) marking the /// callee frame's CA input + home Ref slots; baked into each frame's @@ -1713,14 +1709,10 @@ pub fn build_wasm_module( // Type 0: trace function (param i32) -> (result i32) types.ty().function(vec![ValType::I32], vec![ValType::I32]); if needs_call { - // Type 1: fixed `jit_call(frame)` or compact - // `jit_call_compact(frame, call_area_ofs)` trampoline. - let params = if frame.call_result_ofs == CALL_RESULT_OFS { - vec![ValType::I32] - } else { - vec![ValType::I32, ValType::I32] - }; - types.ty().function(params, vec![]); + // Type 1: `jit_call_compact(base, call_area_ofs)` trampoline. + types + .ty() + .function(vec![ValType::I32, ValType::I32], vec![]); } // Residual-call types follow: `(i64×n) -> i64` for arity `n`, indexed by // `residual_type_base + n`. `residual_type_base` = the count of types above. @@ -1774,15 +1766,7 @@ pub fn build_wasm_module( ); if needs_call { // Import jit_call trampoline as function index 0 - imports.import( - "env", - if frame.call_result_ofs == CALL_RESULT_OFS { - "jit_call" - } else { - "jit_call_compact" - }, - EntityType::Function(1), - ); + imports.import("env", "jit_call_compact", EntityType::Function(1)); } if needs_table { // Import the host's shared indirect function table as table index 0. @@ -2858,7 +2842,6 @@ fn build_function( residual_type_base, wb_fn_ptr, base, - frame, ); } emit_resolve(&mut sink, constants, value_types, op.arg(0).to_opref()); // struct ptr @@ -2953,7 +2936,6 @@ fn build_function( residual_type_base, wb_fn_ptr, base, - frame, ); } emit_array_addr(&mut sink, constants, value_types, op); @@ -3623,22 +3605,21 @@ fn build_function( } else { let jit_call = jit_call_idx.expect("CA arm needs jit_call for the frame trampolines"); - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(ca.ca_alloc_fn_ptr); - sink.i64_store(mem64(frame.call_func_ofs)); - sink.local_get(0); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); + emit_call_area_addr(&mut sink); sink.i64_const(2); - sink.i64_store(mem64(frame.call_nargs_ofs)); - sink.local_get(0); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); + emit_call_area_addr(&mut sink); sink.i64_const(tgt.callee_frame_bytes as i64); - sink.i64_store(mem64(frame.call_args_ofs)); - sink.local_get(0); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS)); + emit_call_area_addr(&mut sink); sink.i64_const(tgt.callee_gcmap_ptr); - sink.i64_store(mem64(frame.call_args_ofs + SLOT_SIZE)); - sink.local_get(0); - emit_jit_call(&mut sink, jit_call, frame); - sink.local_get(0); - sink.i64_load(mem64(frame.call_result_ofs)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + SLOT_SIZE)); + emit_jit_call(&mut sink, jit_call); + emit_call_area_addr(&mut sink); + sink.i64_load(mem64(STATIC_CALL_RESULT_OFS)); } sink.i32_wrap_i64(); sink.i32_const(majit_backend::jitframe::FIRST_ITEM_OFFSET as i32); @@ -3648,8 +3629,6 @@ fn build_function( // own frame was the shadow-stack top. Now that the callee is // pushed, reload local 0 from the entry beneath it before // resolving inputs through local-0-relative homes. The - // trampoline path intentionally keeps the earlier assumption: - // its scratch writes themselves dereference stale local 0. if let (Some(_base), Some(inline)) = (residual_type_base, ca.inline) { emit_ca_reload_caller(&mut sink, inline.jf_top_addr); sink.local_set(0); @@ -3701,16 +3680,15 @@ fn build_function( } else { let jit_call = jit_call_idx.expect("CA arm needs jit_call for the frame trampolines"); - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(ca.ca_reload_fn_ptr); - sink.i64_store(mem64(frame.call_func_ofs)); - sink.local_get(0); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); + emit_call_area_addr(&mut sink); sink.i64_const(0); - sink.i64_store(mem64(frame.call_nargs_ofs)); - sink.local_get(0); - emit_jit_call(&mut sink, jit_call, frame); - sink.local_get(0); - sink.i64_load(mem64(frame.call_result_ofs)); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); + emit_jit_call(&mut sink, jit_call); + emit_call_area_addr(&mut sink); + sink.i64_load(mem64(STATIC_CALL_RESULT_OFS)); } sink.i32_wrap_i64(); sink.local_set(ca_cfp_local); @@ -3790,18 +3768,17 @@ fn build_function( } else { let jit_call = jit_call_idx.expect("CA arm needs jit_call for the frame trampolines"); - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(ca.ca_pop_fn_ptr); - sink.i64_store(mem64(frame.call_func_ofs)); - sink.local_get(0); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); + emit_call_area_addr(&mut sink); sink.i64_const(1); - sink.i64_store(mem64(frame.call_nargs_ofs)); - sink.local_get(0); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); + emit_call_area_addr(&mut sink); sink.local_get(ca_cfp_local); sink.i64_extend_i32_u(); - sink.i64_store(mem64(frame.call_args_ofs)); - sink.local_get(0); - emit_jit_call(&mut sink, jit_call, frame); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS)); + emit_jit_call(&mut sink, jit_call); } // The callee recursion minor-collected; this bridge's other live // Ref locals are now stale. Reload them from the forwarded homes. @@ -3955,25 +3932,24 @@ fn build_function( let call_args = &op.getarglist()[1..]; // Store func_ptr to call area - sink.local_get(0); + emit_call_area_addr(&mut sink); emit_resolve(&mut sink, constants, value_types, func_ptr_ref); - sink.i64_store(mem64(frame.call_func_ofs)); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); // Store num_args - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(call_args.len() as i64); - sink.i64_store(mem64(frame.call_nargs_ofs)); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); // Store each arg for (i, arg) in call_args.iter().enumerate() { - sink.local_get(0); + emit_call_area_addr(&mut sink); emit_resolve(&mut sink, constants, value_types, arg.to_opref()); - sink.i64_store(mem64(frame.call_args_ofs + i as u64 * SLOT_SIZE)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + i as u64 * SLOT_SIZE)); } // Call trampoline - sink.local_get(0); - emit_jit_call(&mut sink, jit_call, frame); + emit_jit_call(&mut sink, jit_call); // Read result (for non-void calls) let is_void = matches!( @@ -3986,8 +3962,8 @@ fn build_function( | OpCode::CallLoopinvariantN ); if !OpRef::raw_is_constant(vi) && !is_void { - sink.local_get(0); - sink.i64_load(mem64(frame.call_result_ofs)); + emit_call_area_addr(&mut sink); + sink.i64_load(mem64(STATIC_CALL_RESULT_OFS)); if value_types[vi as usize] == ValType::F64 { sink.f64_reinterpret_i64(); } @@ -4158,29 +4134,28 @@ fn build_function( } else { let jit_call = jit_call_idx.expect("New op present but jit_call not imported"); // func_ptr = wasm_jit_alloc - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(alloc_fn_ptr); - sink.i64_store(mem64(frame.call_func_ofs)); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); // num_args = 2 - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(2); - sink.i64_store(mem64(frame.call_nargs_ofs)); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); // arg0 = type_id - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(type_id); - sink.i64_store(mem64(frame.call_args_ofs)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS)); // arg1 = size - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(size); - sink.i64_store(mem64(frame.call_args_ofs + SLOT_SIZE)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + SLOT_SIZE)); // call trampoline - sink.local_get(0); - emit_jit_call(&mut sink, jit_call, frame); + emit_jit_call(&mut sink, jit_call); if !OpRef::raw_is_constant(vi) { // result pointer - sink.local_get(0); - sink.i64_load(mem64(frame.call_result_ofs)); + emit_call_area_addr(&mut sink); + sink.i64_load(mem64(STATIC_CALL_RESULT_OFS)); sink.local_set(1 + vi); } } @@ -4555,40 +4530,39 @@ fn build_function( let jit_call = jit_call_idx.expect("NewArray op present but jit_call not imported"); // func_ptr = wasm_jit_alloc_array - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(alloc_array_fn_ptr); - sink.i64_store(mem64(frame.call_func_ofs)); + sink.i64_store(mem64(STATIC_CALL_FUNC_OFS)); // num_args = 5 - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(5); - sink.i64_store(mem64(frame.call_nargs_ofs)); + sink.i64_store(mem64(STATIC_CALL_NARGS_OFS)); // arg0 = type_id - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(type_id); - sink.i64_store(mem64(frame.call_args_ofs)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS)); // arg1 = base_size - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(base_size); - sink.i64_store(mem64(frame.call_args_ofs + SLOT_SIZE)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + SLOT_SIZE)); // arg2 = item_size - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(item_size); - sink.i64_store(mem64(frame.call_args_ofs + 2 * SLOT_SIZE)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + 2 * SLOT_SIZE)); // arg3 = length (op.arg(0)) - sink.local_get(0); + emit_call_area_addr(&mut sink); emit_resolve(&mut sink, constants, value_types, op.arg(0).to_opref()); - sink.i64_store(mem64(frame.call_args_ofs + 3 * SLOT_SIZE)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + 3 * SLOT_SIZE)); // arg4 = len_offset - sink.local_get(0); + emit_call_area_addr(&mut sink); sink.i64_const(len_offset); - sink.i64_store(mem64(frame.call_args_ofs + 4 * SLOT_SIZE)); + sink.i64_store(mem64(STATIC_CALL_ARGS_OFS + 4 * SLOT_SIZE)); // call trampoline - sink.local_get(0); - emit_jit_call(&mut sink, jit_call, frame); + emit_jit_call(&mut sink, jit_call); if !OpRef::raw_is_constant(vi) { - sink.local_get(0); - sink.i64_load(mem64(frame.call_result_ofs)); + emit_call_area_addr(&mut sink); + sink.i64_load(mem64(STATIC_CALL_RESULT_OFS)); sink.local_set(1 + vi); } } diff --git a/majit/majit-backend-wasm/src/failguard.rs b/majit/majit-backend-wasm/src/failguard.rs index 1d2829c6fbf..58242391de4 100644 --- a/majit/majit-backend-wasm/src/failguard.rs +++ b/majit/majit-backend-wasm/src/failguard.rs @@ -178,7 +178,6 @@ pub struct CallAssemblerTarget { pub callee_gcmap_ptr: i64, pub loop_finish_fi: u32, pub compiled_ptr: u64, - pub has_trampoline_calls: bool, } /// Compiled loop targets keyed by their `JitCellToken` number. Unlike label @@ -315,7 +314,6 @@ pub fn register_pending_call_assembler_target(number: u64, input_types: Vec>, - /// This bridge contains no host-trampoline lowering, so a nested bridge's - /// movable CALL_ASSEMBLER callee cannot strand a stale source-frame - /// pointer when control returns here. - pub ca_reentry_safe: bool, } /// Compiled wasm loop metadata, stored in `JitCellToken.compiled`. @@ -483,11 +475,6 @@ pub struct CompiledWasmLoop { /// Geometry frozen when this token was first compiled. Every bridge /// chained onto it is emitted against this exact layout. pub frame: crate::codegen::FrameGeometry, - /// True when this loop or any successfully chained bridge uses the host - /// residual-call trampoline. A CA callee frame is movable, but that - /// trampoline retains the pre-call frame pointer, so `compile_bridge` must - /// not enable the CA arm for this source token. - pub has_trampoline_calls: Cell, /// Base address (shared linear memory) of this loop's per-guard bridge-slot /// cell array — one i32 per `fail_index`, `0` = no bridge. The trace's /// epilogue reads `cells[fail_index]` and `compile_bridge` writes a bridge's @@ -599,15 +586,6 @@ impl CompiledWasmLoop { Ok(handle) } } - - /// Incorporate the normal (non-CA unless this bridge is the candidate) - /// codegen census for a bridge after it has been chained onto this token. - /// Every earlier bridge remains reachable from a later CA recursion's - /// guard exits, so its host trampoline use also rules out CA. - pub fn record_chained_bridge_trampoline_calls(&self, bridge_has_trampoline_calls: bool) { - self.has_trampoline_calls - .set(self.has_trampoline_calls.get() || bridge_has_trampoline_calls); - } } impl Drop for CompiledWasmLoop { @@ -637,51 +615,3 @@ impl Drop for CompiledWasmLoop { } } } - -#[cfg(test)] -mod tests { - use super::*; - - fn token_with_trampoline_census(has_trampoline_calls: bool) -> CompiledWasmLoop { - CompiledWasmLoop { - token_number: 0, - trace_id: 0, - input_types: Vec::new(), - func_handle: Cell::new(0), - pending_wasm_bytes: RefCell::new(None), - fail_descrs: RefCell::new(Vec::new()), - num_inputs: 0, - max_output_slots: 0, - num_ref_homes: 0, - frame: crate::codegen::FrameGeometry::fixed(), - has_trampoline_calls: Cell::new(has_trampoline_calls), - bridge_cells_base: 0, - num_guard_cells: 0, - has_preamble: false, - label_descrs: Vec::new(), - guard_fail_arg_advanced: Vec::new(), - bridge_descr_ranges: RefCell::new(Vec::new()), - chained_trace_meta: RefCell::new(std::collections::HashMap::new()), - _bridge_cells_owner: None, - _bridge_owned_cells: RefCell::new(Vec::new()), - ca_active: Cell::new(false), - ca_terminal_declined: Cell::new(false), - ca_callers: RefCell::new(Vec::new()), - } - } - - #[test] - fn chained_bridge_trampoline_census_is_orred_into_token() { - let token = token_with_trampoline_census(false); - token.record_chained_bridge_trampoline_calls(false); - assert!(!token.has_trampoline_calls.get()); - - token.record_chained_bridge_trampoline_calls(true); - assert!(token.has_trampoline_calls.get()); - - // A later clean bridge cannot erase an earlier chained bridge's - // trampoline census before a CA bridge is considered. - token.record_chained_bridge_trampoline_calls(false); - assert!(token.has_trampoline_calls.get()); - } -} diff --git a/majit/majit-backend-wasm/src/lib.rs b/majit/majit-backend-wasm/src/lib.rs index a5d8e471459..aed9f4e6cc5 100644 --- a/majit/majit-backend-wasm/src/lib.rs +++ b/majit/majit-backend-wasm/src/lib.rs @@ -116,14 +116,11 @@ fn diag_bump(i: usize) { const FROZEN_CHAIN_VALUE_SLOTS: usize = 64; const FROZEN_CHAIN_REF_HOMES: usize = 64; -/// An arithmetic op whose result advances a loop-carried numeric value (the -/// `IntAdd`/`IntSub`/… and float-arithmetic block plus the overflow-checked -/// variants and the unary `IntNeg`/`IntInvert`). Excludes copies (`SameAs*`), -/// casts, comparisons, and allocations: those feed a JUMP arg without making -/// the loop's induction walk toward its exit condition. Used to tell a -/// state-advancing loop-closing bridge from a guard side-trace that re-presents -/// the same loop state every pass. -fn is_inductive_arith(opcode: majit_ir::OpCode) -> bool { +/// An op whose result advances loop-carried state. A value produced inside the +/// re-running region by arithmetic or by a heap load is fresh on each pass, so +/// a JUMP carrying it advances the loop. Copies (`SameAs*`), casts, +/// comparisons, and allocations do not. +fn advances_loop_state(opcode: majit_ir::OpCode) -> bool { use majit_ir::OpCode::*; matches!( opcode, @@ -153,16 +150,36 @@ fn is_inductive_arith(opcode: majit_ir::OpCode) -> bool { | IntAddOvf | IntSubOvf | IntMulOvf + | GetfieldGcR + | GetfieldGcI + | GetfieldGcF + | GetfieldRawI + | GetfieldRawR + | GetfieldRawF + | GetarrayitemGcR + | GetarrayitemGcI + | GetarrayitemGcF + | GetarrayitemRawI + | GetarrayitemRawR + | GetarrayitemRawF + | GcLoadI + | GcLoadR + | GcLoadF + | GcLoadIndexedI + | GcLoadIndexedR + | GcLoadIndexedF + | RawLoadI + | RawLoadF ) } /// Per-guard (per-trace order), per-fail-arg: whether the value was produced -/// by induction-advancing arithmetic in the part of the trace that re-runs on -/// every pass — the ops after the loop-header (last) LABEL, or the WHOLE trace -/// when it has no LABEL (a bridge, or a Label-less recursion loop, whose body -/// runs in full each pass). Such a fail arg is fresh in the failing iteration, -/// so a loop-closing bridge that JUMPs it verbatim still advances the chained -/// loop⇄bridge cycle (`compile_bridge`'s livelock check). +/// by loop-state-advancing arithmetic or a heap load in the part of the trace +/// that re-runs on every pass — the ops after the loop-header (last) LABEL, or +/// the WHOLE trace when it has no LABEL (a bridge, or a Label-less recursion +/// loop, whose body runs in full each pass). Such a fail arg is fresh in the +/// failing iteration, so a loop-closing bridge that JUMPs it verbatim still +/// advances the chained loop⇄bridge cycle (`compile_bridge`'s livelock check). fn guard_fail_args_advanced( ops: &[majit_ir::Op], guard_exits: &[codegen::GuardExit], @@ -173,7 +190,7 @@ fn guard_fail_args_advanced( .map_or(0, |p| p + 1); let advanced_ids: std::collections::HashSet = ops[start..] .iter() - .filter(|op| is_inductive_arith(op.opcode)) + .filter(|op| advances_loop_state(op.opcode)) .map(|op| op.pos.get()) .filter(|r| *r != majit_ir::OpRef::NONE && !r.is_constant()) .map(|r| r.raw()) @@ -210,6 +227,14 @@ use majit_ir::{FailDescr, GcRef, InputArg, Op, OpRc, Value}; static JIT_EXC_VALUE: AtomicI64 = AtomicI64::new(0); static JIT_EXC_TYPE: AtomicI64 = AtomicI64::new(0); +/// Residual-call scratch shared by emitted wasm and the host trampoline. +/// Trampoline use is strictly LIFO: the host materialises every argument +/// before invoking the callee, and the guest loads the result immediately on +/// return, so a nested guest trampoline call cannot observe an outer call's +/// live data. +static JIT_CALL_AREA: [AtomicI64; codegen::FrameGeometry::CALL_AREA_SLOTS] = + [const { AtomicI64::new(0) }; codegen::FrameGeometry::CALL_AREA_SLOTS]; + /// llmodel.py:194-199 _store_exception parity: set JIT exception state. /// `value` is a valid OBJECTPTR (or 0); the exception class is read from /// `value.typeptr` (offset 0). @@ -258,6 +283,11 @@ pub fn jit_exc_type_addr() -> usize { &JIT_EXC_TYPE as *const _ as usize } +/// Address of `JIT_CALL_AREA`, embedded as an immediate in JIT-emitted wasm. +pub fn jit_call_area_addr() -> usize { + &JIT_CALL_AREA as *const _ as usize +} + thread_local! { /// llmodel.py self.gc_ll_descr — owned by the active wasm /// backend on this thread. Stored as a thread-local so the @@ -1492,7 +1522,6 @@ struct PendingSelfCa<'a> { token_number: u64, input_types: &'a [majit_ir::Type], frame: codegen::FrameGeometry, - has_trampoline_calls: bool, } /// Resolve every distinct compiled target used by CALL_ASSEMBLER ops in this @@ -1538,13 +1567,8 @@ fn general_int_call_assembler_target( && ca_dispatch_exists(target_token) }) { // Keep the ordinary input validation, then use the current loop's - // frozen geometry. Its direct-lowering census is the pending - // equivalent of the normal live-loop census below: a movable self - // frame must not run a host trampoline. - if registered.input_types.as_slice() != arg_types - || self_.frame.ca_frame_bytes == 0 - || self_.has_trampoline_calls - { + // frozen geometry. + if registered.input_types.as_slice() != arg_types || self_.frame.ca_frame_bytes == 0 { return None; } let callee_gcmap_ptr = @@ -1560,7 +1584,6 @@ fn general_int_call_assembler_target( callee_gcmap_ptr, loop_finish_fi: failguard::WASM_CA_FINISH_FI_UNKNOWN, compiled_ptr: 0, - has_trampoline_calls: false, } } else { // A straight-line function trace may have deferred host module @@ -1586,20 +1609,16 @@ fn general_int_call_assembler_target( || registered.callee_frame_bytes == 0 || registered.callee_gcmap_ptr == 0 || registered.compiled_ptr == 0 - || registered.has_trampoline_calls { return None; } // A successfully compiled loop is retained by its token while it - // is registered. Its chained bridges can subsequently add - // trampoline calls or become terminally declined, so read the - // live census before baking every CA entry. + // is registered. It can subsequently become terminally declined, + // so read the live state before baking every CA entry. let live = unsafe { (registered.compiled_ptr as *const CompiledWasmLoop) .as_ref() - .is_some_and(|loop_| { - !loop_.has_trampoline_calls.get() && !loop_.ca_terminal_declined.get() - }) + .is_some_and(|loop_| !loop_.ca_terminal_declined.get()) }; if !live { return None; @@ -1968,26 +1987,19 @@ impl majit_backend::Backend for WasmBackend { ), }; let input_types: Vec = inputargs.iter().map(|ia| ia.tp).collect(); - // Count with CA direct-lowering enabled. This is the safety census - // for a pending self target: no CompiledWasmLoop exists yet to inspect. - let pending_self_has_trampoline_calls = codegen::has_trampoline_calls(inputargs, ops, true); // A general CALL_ASSEMBLER can enter already-compiled loops through // the shared table. Codegen keeps each callee's geometry keyed by its - // operation's target token. The pending self token is admitted with - // the current frame; distinct sibling targets retain their live census. + // operation's target token. The pending self token is admitted with + // the current frame. let ca_targets = general_int_call_assembler_target( ops, Some(PendingSelfCa { token_number: token.number, input_types: &input_types, frame, - has_trampoline_calls: pending_self_has_trampoline_calls, }), ); let allow_ca = ca_deopt_helper_slot() != 0 && ca_targets.is_some(); - // This must use the same direct-vs-trampoline predicates as codegen: - // a CA callee runs this source-loop body on a movable nursery frame. - let has_trampoline_calls = codegen::has_trampoline_calls(inputargs, ops, allow_ca); // Decline traces the wasm backend cannot compile correctly, so the // metainterp falls back to the interpreter (correct, if unaccelerated) @@ -2276,7 +2288,6 @@ impl majit_backend::Backend for WasmBackend { max_output_slots, num_ref_homes, frame, - has_trampoline_calls: std::cell::Cell::new(has_trampoline_calls), bridge_cells_base, num_guard_cells: guard_exits.len(), has_preamble, @@ -2342,7 +2353,6 @@ impl majit_backend::Backend for WasmBackend { callee_gcmap_ptr, loop_finish_fi, compiled_ptr: compiled as *const CompiledWasmLoop as usize as u64, - has_trampoline_calls: compiled.has_trampoline_calls.get(), }, ); if let Some(targets) = ca_targets.as_ref() { @@ -2407,11 +2417,6 @@ impl majit_backend::Backend for WasmBackend { // lift (the host round-trip path still handles the CALL_ASSEMBLER). let ca_targets = bridge_int_call_assembler_target(ops); let ca_candidate = ca_deopt_helper_slot() != 0 && ca_targets.is_some(); - // The CA candidate is a dedicated direct arm; all other ops are - // scanned against their normal emission paths. - let bridge_has_trampoline_calls = - codegen::has_trampoline_calls(inputargs, ops, ca_candidate); - // The source guard this bridge attaches to. `fail_index` is its index in // the source loop's `fail_descrs` / cell array; `trace_id` identifies the // owning trace. @@ -2420,15 +2425,7 @@ impl majit_backend::Backend for WasmBackend { // Scalars read from the source loop up front, so the immutable borrow of // `original_token` is released before the `&mut self` codegen calls. - let ( - source_guard, - source_ca_reentry_safe, - source_func_handle, - source_has_preamble, - source_frame, - source_ca_active, - source_has_trampoline_calls, - ) = { + let (source_guard, source_func_handle, source_has_preamble, source_frame) = { let source_loop = original_token .compiled .get() @@ -2446,47 +2443,38 @@ impl majit_backend::Backend for WasmBackend { // per-fail-arg advance flags. `None` = foreign trace (declined // below, diag 3). let is_direct = source_trace_id == source_loop.trace_id; - let (guard, ca_reentry_safe) = if is_direct { - ( - Some(( - source_loop.bridge_cells_base, - source_loop.num_guard_cells, - source_loop - .guard_fail_arg_advanced - .get(source_fail_index as usize) - .cloned() - .unwrap_or_default(), - )), - true, - ) + let guard = if is_direct { + Some(( + source_loop.bridge_cells_base, + source_loop.num_guard_cells, + source_loop + .guard_fail_arg_advanced + .get(source_fail_index as usize) + .cloned() + .unwrap_or_default(), + )) } else { match source_loop .chained_trace_meta .borrow() .get(&source_trace_id) { - Some(m) => ( - Some(( - m.cells_base, - m.num_cells, - m.guard_fail_arg_advanced - .get(source_fail_index as usize) - .cloned() - .unwrap_or_default(), - )), - m.ca_reentry_safe, - ), - None => (None, false), + Some(m) => Some(( + m.cells_base, + m.num_cells, + m.guard_fail_arg_advanced + .get(source_fail_index as usize) + .cloned() + .unwrap_or_default(), + )), + None => None, } }; ( guard, - ca_reentry_safe, source_loop.materialize_func_handle()?, source_loop.has_preamble, source_loop.frame, - source_loop.ca_active.get(), - source_loop.has_trampoline_calls.get(), ) }; @@ -2508,34 +2496,10 @@ impl majit_backend::Backend for WasmBackend { "wasm backend: bridge source guard index has no dispatch cell".into(), )); } - // A nested bridge may compose CALL_ASSEMBLER only when its owning - // bridge published that it has no host-trampoline lowering. Merely - // finding the nested guard's cell is insufficient: after a movable - // callee returns, a trampoline-bearing source would retain a stale - // frame pointer. Direct loop guards satisfy the same condition through - // the token-wide trampoline census below. - let mut allow_ca = ca_candidate && source_ca_reentry_safe; - let ca_trampoline_decline = if allow_ca && source_has_trampoline_calls { - Some( - "wasm backend: self-recursive CA source token or chained bridge \ - uses the host call trampoline", - ) - } else if allow_ca && bridge_has_trampoline_calls { - Some("wasm backend: self-recursive CA bridge uses the host call trampoline") - } else { - None - }; - if ca_trampoline_decline.is_some() { - // Let the ordinary non-CA CALL_ASSEMBLER decline path retain the - // interpreter fallback, but make this soundness floor observable. - diag_bump(15); - allow_ca = false; - } + let allow_ca = ca_candidate; if let Some(reason) = wasm_unsupported_trace_reason(ops, allow_ca) { diag_bump(1); // declined: CALL_ASSEMBLER - return Err(BackendError::Unsupported( - ca_trampoline_decline.unwrap_or(reason.as_str()).to_string(), - )); + return Err(BackendError::Unsupported(reason)); } if allow_ca { diag_bump(14); // accepted CALL_ASSEMBLER bridge @@ -2551,19 +2515,7 @@ impl majit_backend::Backend for WasmBackend { let bridge_ref_homes = codegen::count_ref_homes(inputargs, ops); if bridge_value_slots > source_frame.value_slots || bridge_ref_homes > source_frame.ordinary_home_slots() - || (source_ca_active && bridge_has_trampoline_calls) { - if source_ca_active && bridge_has_trampoline_calls { - // Guard exits in a CA-active token execute on movable callee - // frames. Do not chain a later bridge whose own body would - // re-enter the stale-pointer host trampoline. - diag_bump(15); - return Err(BackendError::Unsupported( - "wasm backend: CA-active source cannot chain a bridge that \ - uses the host call trampoline" - .into(), - )); - } diag_bump(4); return Err(BackendError::Unsupported(format!( "wasm backend: bridge frame needs values={bridge_value_slots}, homes={bridge_ref_homes}; \ @@ -2621,8 +2573,8 @@ impl majit_backend::Backend for WasmBackend { // A loop-closing bridge carries the source loop's loop-carried state in // its terminal JUMP args and tail-calls the loop to iterate again. If no - // JUMP arg is the result of an induction-advancing arithmetic op — i.e. - // every loop-carried value is a verbatim input reload, a fresh + // JUMP arg is the result of a loop-state-advancing arithmetic or load op + // — i.e. every loop-carried value is a verbatim input reload, a fresh // allocation, or a baked constant — the bridge re-presents byte-identical // induction/guard state on every pass, so the loop's exit guard never // flips and the loop⇄bridge cycle spins forever (a control-flow @@ -2646,8 +2598,8 @@ impl majit_backend::Backend for WasmBackend { // verbatim reload of source fail arg `k`. The advance for such an // arg may have happened in the SOURCE loop's body before the guard // (an `i += 1` preceding the failing branch): the source recorded - // per-fail-arg whether the value was produced by induction- - // advancing arithmetic within the failing iteration + // per-fail-arg whether the value was produced by a loop-state- + // advancing op within the failing iteration // (`guard_fail_arg_advanced`), so consult that alongside the // in-bridge producers. let input_pos: std::collections::HashMap = inputargs @@ -2662,7 +2614,7 @@ impl majit_backend::Backend for WasmBackend { .is_some_and(|jump| { jump.getarglist().iter().any(|arg| match arg { majit_ir::operand::Operand::Op(producer) => { - is_inductive_arith(producer.opcode) + advances_loop_state(producer.opcode) } majit_ir::operand::Operand::InputArg(ia) => { input_pos.get(&ia.index).is_some_and(|&k| { @@ -2843,7 +2795,6 @@ impl majit_backend::Backend for WasmBackend { count, )); } - source_loop.record_chained_bridge_trampoline_calls(bridge_has_trampoline_calls); // Publish this bridge's own guard-dispatch metadata so a hot guard // INSIDE it can chain a nested sub-bridge (same resolution the // loop's own guards get, keyed by this bridge's trace_id). @@ -2853,7 +2804,6 @@ impl majit_backend::Backend for WasmBackend { cells_base: bridge_cells_base, num_cells: guard_exits.len(), guard_fail_arg_advanced: guard_fail_args_advanced(ops, &guard_exits), - ca_reentry_safe: !bridge_has_trampoline_calls, }, ); // The bridge module lives as long as this source loop, so hand its @@ -3382,13 +3332,10 @@ impl majit_backend::Backend for WasmBackend { let movable_callee = new_target.callee_frame_bytes != 0 && new_target.callee_gcmap_ptr != 0 && new_target.compiled_ptr != 0 - && !new_target.has_trampoline_calls && unsafe { (new_target.compiled_ptr as *const CompiledWasmLoop) .as_ref() - .is_some_and(|loop_| { - !loop_.has_trampoline_calls.get() && !loop_.ca_terminal_declined.get() - }) + .is_some_and(|loop_| !loop_.ca_terminal_declined.get()) }; if !movable_callee { return Err(BackendError::Unsupported(format!( diff --git a/majit/majit-backend-wasm/tests/codegen_test.rs b/majit/majit-backend-wasm/tests/codegen_test.rs index d904529e634..c1919d992ae 100644 --- a/majit/majit-backend-wasm/tests/codegen_test.rs +++ b/majit/majit-backend-wasm/tests/codegen_test.rs @@ -562,21 +562,23 @@ fn test_call_generates_import() { validate_wasm(&bytes); assert_eq!(guards.len(), 1); - // Verify the module has jit_call import + // The residual-call trampoline reads its scratch at `base + offset`, and + // the scratch is the module-static call area, so every emitting module + // takes the two-argument import. let parser = wasmparser::Parser::new(0); let mut has_jit_call = false; for payload in parser.parse_all(&bytes) { if let Ok(wasmparser::Payload::ImportSection(imports)) = payload { for import in imports { if let Ok(import) = import { - if import.name == "jit_call" { + if import.name == "jit_call_compact" { has_jit_call = true; } } } } } - assert!(has_jit_call, "module should import jit_call"); + assert!(has_jit_call, "module should import jit_call_compact"); } #[test] diff --git a/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats b/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats index eebb788ea08..aef30c8349a 100644 --- a/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats +++ b/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats @@ -1,9 +1,9 @@ -bridges_compiled=14 +bridges_compiled=16 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=1 -guard_failures=3062 +guard_failures=2581 internal_compile_panics=0 loops_aborted=1 loops_compiled=3 diff --git a/pyre/bench/synth/exception_catching_frame_tb_node.wasm.jitstats b/pyre/bench/synth/exception_catching_frame_tb_node.wasm.jitstats index d0d4d994fd7..b81f0068647 100644 --- a/pyre/bench/synth/exception_catching_frame_tb_node.wasm.jitstats +++ b/pyre/bench/synth/exception_catching_frame_tb_node.wasm.jitstats @@ -1,8 +1,8 @@ -bridges_compiled=3 +bridges_compiled=2 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=601 +guard_failures=401 internal_compile_panics=0 loops_aborted=0 loops_compiled=4 diff --git a/pyre/bench/synth/exception_inline_callee_tb_frames.wasm.jitstats b/pyre/bench/synth/exception_inline_callee_tb_frames.wasm.jitstats index a7184f7b1ba..3fd375af835 100644 --- a/pyre/bench/synth/exception_inline_callee_tb_frames.wasm.jitstats +++ b/pyre/bench/synth/exception_inline_callee_tb_frames.wasm.jitstats @@ -1,8 +1,9 @@ -bridges_compiled=4 +bridges_compiled=3 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=1562 +fbw_rolled_back_with_effects=0 +guard_failures=604 internal_compile_panics=0 loops_aborted=0 loops_compiled=6 diff --git a/pyre/bench/synth/exception_reentry_guard_finally_residual.wasm.jitstats b/pyre/bench/synth/exception_reentry_guard_finally_residual.wasm.jitstats index 1c0ee484b73..dea497cac0a 100644 --- a/pyre/bench/synth/exception_reentry_guard_finally_residual.wasm.jitstats +++ b/pyre/bench/synth/exception_reentry_guard_finally_residual.wasm.jitstats @@ -1,8 +1,8 @@ -bridges_compiled=12 +bridges_compiled=11 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=2459 +guard_failures=2261 internal_compile_panics=0 loops_aborted=0 loops_compiled=3 diff --git a/pyre/bench/synth/exception_traceback_lineno_chain.wasm.jitstats b/pyre/bench/synth/exception_traceback_lineno_chain.wasm.jitstats index b76d61c038d..d0247cf3063 100644 --- a/pyre/bench/synth/exception_traceback_lineno_chain.wasm.jitstats +++ b/pyre/bench/synth/exception_traceback_lineno_chain.wasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=4 +bridges_compiled=2 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 diff --git a/pyre/bench/synth/gc_bug_bridge_flavor_traceback_names.wasm.jitstats b/pyre/bench/synth/gc_bug_bridge_flavor_traceback_names.wasm.jitstats index 89605bf928f..f186f209a39 100644 --- a/pyre/bench/synth/gc_bug_bridge_flavor_traceback_names.wasm.jitstats +++ b/pyre/bench/synth/gc_bug_bridge_flavor_traceback_names.wasm.jitstats @@ -1,8 +1,9 @@ -bridges_compiled=9 +bridges_compiled=8 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=2038 +fbw_rolled_back_with_effects=0 +guard_failures=1838 internal_compile_panics=0 loops_aborted=0 loops_compiled=4 diff --git a/pyre/bench/synth/recursion_memo_branch.wasm.jitstats b/pyre/bench/synth/recursion_memo_branch.wasm.jitstats index e4ed144eb59..c984ccc88fe 100644 --- a/pyre/bench/synth/recursion_memo_branch.wasm.jitstats +++ b/pyre/bench/synth/recursion_memo_branch.wasm.jitstats @@ -1,8 +1,8 @@ -bridges_compiled=14 +bridges_compiled=16 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=3083 +guard_failures=2602 internal_compile_panics=0 loops_aborted=2 loops_compiled=3 diff --git a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs index 63a00f61d4a..4a7a75edd72 100644 --- a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs +++ b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs @@ -173,9 +173,13 @@ pub enum TerminatorKind { pub struct Terminator { /// mapdict.py:307 `w_cls`. pub w_cls: PyObjectRef, - /// mapdict.py:308 `allow_unboxing` (quasi-immutable; cleared when an - /// attribute that was unboxed is reassigned a differently-typed value, - /// mapdict.py:685). + /// mapdict.py:308 `allow_unboxing` — declared quasi-immutable upstream and + /// cleared when an unboxed attribute is reassigned a differently-typed + /// value (mapdict.py:685). Pyre installs no quasi-immutable watcher here; + /// as with the `Function` fields documented in `descr.rs`, it keeps the + /// field live/mutable and pairs each read with a `GuardValue` on the + /// instance map in `walker_guard_mapdict_instance_shape`, so a map-node + /// change deopts before a stale value can be folded. pub allow_unboxing: Cell, /// Which Terminator subclass this is. pub kind: TerminatorKind, @@ -233,7 +237,10 @@ pub struct PlainAttribute { pub num_attributes: usize, /// mapdict.py:429 `back`. pub back: MapRef, - /// mapdict.py:430 `ever_mutated` (quasi-immutable). + /// mapdict.py:430 `ever_mutated` — declared quasi-immutable upstream; pyre + /// installs no watcher and uses the per-read instance-map `GuardValue` + /// described on `Terminator::allow_unboxing` (the `descr.rs` `Function` + /// precedent). pub ever_mutated: Cell, /// mapdict.py:431 `order`. pub order: usize, @@ -3129,9 +3136,15 @@ unsafe fn materialize_dict(obj: PyObjectRef, w_dict: PyObjectRef) { pub struct CachedAttributeHolder { /// mapdict.py:670 `order` (= number of prior children of `back`). pub order: usize, - /// mapdict.py:675 `attr` (quasi-immutable). + /// mapdict.py:675 `attr` — the cached child map, declared quasi-immutable + /// upstream; pyre installs no watcher and uses the per-read instance-map + /// `GuardValue` described on `Terminator::allow_unboxing` (the `descr.rs` + /// `Function` precedent). pub attr: Cell, - /// mapdict.py:676 `typ` (quasi-immutable unbox type, `None` = boxed). + /// mapdict.py:676 `typ` — the unbox type (`None` = boxed), declared + /// quasi-immutable upstream; pyre installs no watcher and uses the per-read + /// instance-map `GuardValue` described on `Terminator::allow_unboxing` (the + /// `descr.rs` `Function` precedent). pub typ: Cell>, }