Skip to main content

CallIndirectGuards

Struct CallIndirectGuards 

Source
pub struct CallIndirectGuards {
    pub tables: Vec<TableGuards>,
    pub type_ids_byte_offset: Option<u32>,
    pub type_ids_image: Vec<u32>,
    pub type_class_ids: Vec<u32>,
}
Expand description

#642/#650: everything the call_indirect lowering needs to emit its guards — computed once per module by DecodedModule::call_indirect_guards and threaded to the instruction selector via CompileConfig.

§The R11 multi-table layout contract (#650)

The runtime/harness links every funcref table as ONE contiguous region of raw 4-byte code pointers based at R11, in declaration order (imported tables first): table 0 at R11 + 0, table N at R11 + sum(size(0..N)) * 4. The offsets are compile-time constants because tables are provably fixed-size (table.grow/table.set are unsupported ops whose functions loud-skip at decode — #642). A single-table module degenerates to the pre-#650 contract (table 0 at R11, offset 0) BY CONSTRUCTION, keeping its emitted bytes identical.

WASM Core §4.4.8 requires call_indirect to trap when index >= table.size and when the callee’s type does not match the instruction’s expected type. The region stores no size fields and no type ids, so, per table:

  • the BOUNDS check is emitted at runtime against THAT table’s compile-time table_size immediate (sound: fixed-size, see above), and

  • the TYPE check is discharged at COMPILE time: for expected type t, tables[n].type_reject[t] is None only when every INITIALIZED slot of table n verifiably holds a function whose signature structurally equals type t (the closed-world property — no runtime mismatch is then possible). Otherwise it holds the reason, and the lowering declines LOUDLY rather than emit an unchecked indirect branch, and

  • a NULL (uninitialized) slot traps at RUNTIME (#664): the layout contract requires the runtime/harness to link every uninitialized slot as a ZERO word (null funcref has no code address; 0 is never a valid function pointer in the region), and when has_null_slots is set the dispatch emits a null check on the loaded pointer (CMP #0 → trap) between the bounds guard and the indirect branch. A fully-initialized table (has_null_slots == false) keeps the pre-#664 dispatch bytes identical BY CONSTRUCTION, and

  • a HETEROGENEOUS table (mixed signatures — the closed-world property cannot hold for ANY expected type) is dispatched through a runtime type check against the type-id sidecar (#676): a parallel u32 array the layout contract places at R11 + type_ids_byte_offset (immediately after the LAST table’s pointer words, i.e. at sum(size(0..num_tables)) * 4), mirroring the pointer region slot for slot — table N’s type-ids start at R11 + type_ids_byte_offset + base_byte_offset(N). Each word is the slot’s STRUCTURAL signature class id: structurally-equal function types share one dense id (1-based, first-occurrence order over the type section); id 0 is reserved for null slots, so the type compare (expected ids are always >= 1) subsumes the #664 null trap in the same CMP. The dispatch loads type_id[idx], compares it against the expected type’s class id (compile-time immediate) and traps (UDF) on mismatch — WASM Core §4.4.8’s runtime type check — before the pointer load and BLX. The sidecar words are emitted into the relocatable object as the .synth.table_type_ids section (non-ALLOC metadata, like .meld_import_table): the runtime/harness that links the pointer region copies them to R11 + type_ids_byte_offset verbatim — it never re-derives ids. A module with NO heterogeneous table emits no sidecar and no runtime type check anywhere: homogeneous dispatch bytes stay identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check: false trick).

    pre-#664 dispatch bytes identical BY CONSTRUCTION.

§Companion: the self-contained SRAM layout contract (#687)

The R11 register above is ALSO the linear-memory base register, whose placement inside SRAM is governed by the self-contained image’s stack layout: --stack-layout=high (default) keeps linmem at the SRAM start with the stack growing down from the top; --stack-layout=low reserves the stack at the SRAM BOTTOM and shifts linmem/globals (and the optimized path’s absolute 0x2000_0100 base) up by the stack size, so an overflow BusFaults below SRAM instead of silently corrupting them. The full layout tables live on build_multi_func_cortex_m_elf in synth-cli (the builder that owns the addresses). Relocatable/host-linked objects are NOT covered — their linker script owns the layout, and the flag is refused there.

Fields§

§tables: Vec<TableGuards>

Per-table guard inputs, indexed by table index (imports first). The default (empty — no module context) DECLINES every call_indirect.

§type_ids_byte_offset: Option<u32>

#676: byte offset of the type-id sidecar within the R11 region — the total pointer-region size, sum(size(0..num_tables)) * 4. Some only when a sidecar exists: at least one table is heterogeneous (see TableGuards::runtime_type_check) AND every table’s size is compile-time known (otherwise the sidecar base is not a constant and heterogeneous dispatches keep declining). None = no sidecar.

§type_ids_image: Vec<u32>

#676: the sidecar image — one u32 structural class id per slot across ALL tables in region order (0 = null slot). Emitted into the object as .synth.table_type_ids; empty exactly when type_ids_byte_offset is None. A table whose image is not statically known contributes ZERO words (it declines at the lowering, and 0 never equals an expected class id, so even a rogue dispatch would trap, not branch).

§type_class_ids: Vec<u32>

#676: per module type index, that type’s structural class id (1-based, dense; structurally-equal duplicate types share an id). The expected-type immediate the dispatch compares against. Empty when type_ids_byte_offset is None (no sidecar — never consulted).

Implementations§

Source§

impl CallIndirectGuards

Source

pub fn single_table( table_size: Option<u32>, type_reject: Vec<Option<String>>, ) -> Self

Single-table (table 0 at R11 offset 0) guards — the pre-#650 shape, used by tests and single-table call sites.

Trait Implementations§

Source§

impl Clone for CallIndirectGuards

Source§

fn clone(&self) -> CallIndirectGuards

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for CallIndirectGuards

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for CallIndirectGuards

Source§

fn default() -> CallIndirectGuards

Returns the “default value” for a type. Read more
Source§

impl Eq for CallIndirectGuards

Source§

impl PartialEq for CallIndirectGuards

Source§

fn eq(&self, other: &CallIndirectGuards) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for CallIndirectGuards

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.