use crate::wasm_op::WasmOp;
use anyhow::{Context, Result};
use std::collections::HashMap;
use wasmparser::{ExternalKind, Parser, Payload};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportKind {
Function(u32),
Memory,
Table,
Global,
}
#[derive(Debug, Clone)]
pub struct ImportEntry {
pub module: String,
pub name: String,
pub kind: ImportKind,
pub index: u32,
}
#[derive(Debug, Clone)]
pub struct WasmMemory {
pub index: u32,
pub initial_pages: u32,
pub max_pages: Option<u32>,
pub shared: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlobalInit {
I32(i32),
I64(i64),
}
#[derive(Debug, Clone)]
pub struct WasmGlobal {
pub index: u32,
pub init: Option<GlobalInit>,
pub mutable: bool,
pub slot_bytes: u32,
}
impl WasmMemory {
pub fn initial_bytes(&self) -> u32 {
self.initial_pages * 65536
}
pub fn max_bytes(&self) -> u32 {
self.max_pages.unwrap_or(self.initial_pages) * 65536
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElemSegmentInfo {
pub table_index: u32,
pub offset: Option<u32>,
pub funcs: Option<Vec<u32>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TableGuards {
pub table_size: Option<u32>,
pub base_byte_offset: Option<u32>,
pub type_reject: Vec<Option<String>>,
pub has_null_slots: bool,
pub runtime_type_check: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
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>,
}
impl CallIndirectGuards {
pub fn single_table(table_size: Option<u32>, type_reject: Vec<Option<String>>) -> Self {
Self {
tables: vec![TableGuards {
table_size,
base_byte_offset: Some(0),
type_reject,
has_null_slots: false,
runtime_type_check: false,
}],
..Self::default()
}
}
}
#[derive(Debug, Clone)]
pub struct DecodedModule {
pub functions: Vec<FunctionOps>,
pub memories: Vec<WasmMemory>,
pub data_segments: Vec<(u32, Vec<u8>)>,
pub imports: Vec<ImportEntry>,
pub num_imported_funcs: u32,
pub func_arg_counts: Vec<u32>,
pub type_arg_counts: Vec<u32>,
pub func_ret_i64: Vec<bool>,
pub type_ret_i64: Vec<bool>,
pub func_params_i64: Vec<Vec<bool>>,
pub globals: Vec<WasmGlobal>,
pub elem_func_indices: Vec<u32>,
pub table_size: Option<u32>,
pub table_sizes: Vec<Option<u32>>,
pub elem_segments: Vec<ElemSegmentInfo>,
pub func_type_indices: Vec<u32>,
pub type_signatures: Vec<String>,
pub wsc_facts: Vec<crate::wsc_facts::WscFact>,
}
impl DecodedModule {
pub fn call_indirect_guards(&self) -> CallIndirectGuards {
let n_types = self.type_signatures.len();
let global_poison: Option<&'static str> = self
.elem_segments
.iter()
.any(|seg| seg.offset.is_none() || seg.table_index as usize >= self.table_sizes.len())
.then_some(
"element segment is not statically verifiable (passive/declared \
segment, non-const offset, out-of-range table, or non-`ref.func` \
entry)",
);
let mut class_of_sig: std::collections::HashMap<&str, u32> =
std::collections::HashMap::new();
let mut type_class_ids: Vec<u32> = Vec::with_capacity(n_types);
for sig in &self.type_signatures {
let next = class_of_sig.len() as u32 + 1;
type_class_ids.push(*class_of_sig.entry(sig.as_str()).or_insert(next));
}
let mut tables = Vec::with_capacity(self.table_sizes.len());
let mut per_table_slot_ids: Vec<Option<Vec<u32>>> =
Vec::with_capacity(self.table_sizes.len());
let mut base_words: Option<u32> = Some(0);
for (n, &size) in self.table_sizes.iter().enumerate() {
let base_byte_offset = base_words.and_then(|w| w.checked_mul(4));
let (type_reject, has_null_slots, slot_class_ids) =
self.table_type_reject(n as u32, size, global_poison, n_types, &type_class_ids);
let runtime_type_check = slot_class_ids.as_ref().is_some_and(|ids| {
let mut distinct: Vec<u32> = ids.iter().copied().filter(|&c| c != 0).collect();
distinct.sort_unstable();
distinct.dedup();
distinct.len() >= 2
});
per_table_slot_ids.push(slot_class_ids);
tables.push(TableGuards {
table_size: size,
base_byte_offset,
type_reject,
has_null_slots,
runtime_type_check,
});
base_words = match (base_words, size) {
(Some(w), Some(s)) => w.checked_add(s),
_ => None,
};
}
let any_hetero = tables.iter().any(|t| t.runtime_type_check);
let type_ids_byte_offset = base_words
.filter(|_| any_hetero)
.and_then(|w| w.checked_mul(4));
let type_ids_image = if type_ids_byte_offset.is_some() {
self.table_sizes
.iter()
.zip(&per_table_slot_ids)
.flat_map(|(&size, ids)| match ids {
Some(ids) => ids.clone(),
None => vec![0u32; size.unwrap_or(0) as usize],
})
.collect()
} else {
for t in &mut tables {
t.runtime_type_check = false;
}
Vec::new()
};
CallIndirectGuards {
tables,
type_ids_byte_offset,
type_ids_image,
type_class_ids: if type_ids_byte_offset.is_some() {
type_class_ids
} else {
Vec::new()
},
}
}
fn table_type_reject(
&self,
n: u32,
size: Option<u32>,
global_poison: Option<&str>,
n_types: usize,
type_class_ids: &[u32],
) -> (Vec<Option<String>>, bool, Option<Vec<u32>>) {
let reject_all = |reason: String| (vec![Some(reason); n_types], false, None);
if let Some(reason) = global_poison {
return reject_all(reason.to_string());
}
let Some(size) = size else {
return reject_all(format!(
"table {n} has no compile-time-fixed size (imported table with \
growable limits)"
));
};
let mut slots: Vec<Option<u32>> = vec![None; size as usize];
for seg in self.elem_segments.iter().filter(|s| s.table_index == n) {
let (Some(off), Some(funcs)) = (seg.offset, seg.funcs.as_ref()) else {
return reject_all(format!(
"element segment targeting table {n} is not statically \
verifiable (non-`ref.func` entry)"
));
};
for (k, &f) in funcs.iter().enumerate() {
let Some(slot) = slots.get_mut(off as usize + k) else {
return reject_all(format!(
"element segment (offset {off}, {} entries) writes past \
table {n}'s declared size {size}",
funcs.len()
));
};
*slot = Some(f);
}
}
let has_null_slots = slots.iter().any(|s| s.is_none());
let rejects = (0..n_types)
.map(|t| {
for f in slots.iter().flatten() {
let Some(&fty) = self.func_type_indices.get(*f as usize) else {
return Some(format!(
"table {n} entry references function {f} with no known type"
));
};
if self.type_signatures.get(fty as usize) != self.type_signatures.get(t) {
return Some(format!(
"table {n} entry (function {f}, type {fty}) has a different \
signature than expected type {t}"
));
}
}
None
})
.collect();
let slot_class_ids: Option<Vec<u32>> = slots
.iter()
.map(|s| match s {
None => Some(0u32),
Some(f) => self
.func_type_indices
.get(*f as usize)
.and_then(|&fty| type_class_ids.get(fty as usize).copied()),
})
.collect();
(rejects, has_null_slots, slot_class_ids)
}
}
pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
let mut functions = Vec::new();
let mut memories = Vec::new();
let mut data_segments = Vec::new();
let mut globals: Vec<WasmGlobal> = Vec::new();
let mut imports = Vec::new();
let mut func_index = 0u32;
let mut num_imported_funcs = 0u32;
let mut export_names: HashMap<u32, String> = HashMap::new();
let mut type_arg_counts: Vec<u32> = Vec::new();
let mut func_arg_counts: Vec<u32> = Vec::new();
let mut type_ret_i64: Vec<bool> = Vec::new();
let mut func_ret_i64: Vec<bool> = Vec::new();
let mut type_params_i64: Vec<Vec<bool>> = Vec::new();
let mut func_params_i64: Vec<Vec<bool>> = Vec::new();
let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
let mut elem_func_indices: Vec<u32> = Vec::new();
let mut table_sizes: Vec<Option<u32>> = Vec::new();
let mut elem_segments: Vec<ElemSegmentInfo> = Vec::new();
let mut func_type_indices: Vec<u32> = Vec::new();
let mut type_signatures: Vec<String> = Vec::new();
let mut name_section_names: HashMap<u32, String> = HashMap::new();
let mut wsc_facts: Option<Vec<crate::wsc_facts::WscFact>> = None;
let mut num_imported_globals = 0u32;
let mut float_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut v128_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut type_has_v128: Vec<bool> = Vec::new();
let mut func_sig_has_v128: Vec<bool> = Vec::new();
for payload in Parser::new(0).parse_all(wasm_bytes) {
let payload = payload.context("Failed to parse WASM payload")?;
match payload {
Payload::TypeSection(reader) => {
for rec_group in reader {
let rec_group = rec_group.context("Failed to parse type")?;
for sub_ty in rec_group.types() {
type_block_arity.push(match &sub_ty.composite_type.inner {
wasmparser::CompositeInnerType::Func(f) => (
u8::try_from(f.params().len()).unwrap_or(u8::MAX),
u8::try_from(f.results().len()).unwrap_or(u8::MAX),
),
_ => (u8::MAX, u8::MAX),
});
let (count, ret_i64, params_i64) = match &sub_ty.composite_type.inner {
wasmparser::CompositeInnerType::Func(func_ty) => (
func_ty.params().len() as u32,
func_ty
.results()
.first()
.is_some_and(|t| *t == wasmparser::ValType::I64),
func_ty
.params()
.iter()
.map(|t| {
matches!(
t,
wasmparser::ValType::I64 | wasmparser::ValType::F64
)
})
.collect::<Vec<bool>>(),
),
_ => (0, false, Vec::new()),
};
type_arg_counts.push(count);
type_ret_i64.push(ret_i64);
type_params_i64.push(params_i64);
type_has_v128.push(match &sub_ty.composite_type.inner {
wasmparser::CompositeInnerType::Func(f) => f
.params()
.iter()
.chain(f.results())
.any(|t| *t == wasmparser::ValType::V128),
_ => false,
});
type_signatures.push(match &sub_ty.composite_type.inner {
wasmparser::CompositeInnerType::Func(f) => {
format!("{:?}->{:?}", f.params(), f.results())
}
other => format!("non-func:{other:?}"),
});
}
}
}
Payload::ImportSection(reader) => {
for import in reader.into_imports() {
let import = import.context("Failed to parse import")?;
let (kind, idx) = match import.ty {
wasmparser::TypeRef::Func(type_idx) => {
let idx = num_imported_funcs;
num_imported_funcs += 1;
func_type_indices.push(type_idx); func_arg_counts
.push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
func_ret_i64.push(
type_ret_i64
.get(type_idx as usize)
.copied()
.unwrap_or(false),
);
func_params_i64.push(
type_params_i64
.get(type_idx as usize)
.cloned()
.unwrap_or_default(),
);
(ImportKind::Function(type_idx), idx)
}
wasmparser::TypeRef::Memory(_) => (ImportKind::Memory, 0),
wasmparser::TypeRef::Table(t) => {
table_sizes.push(match (u32::try_from(t.initial), t.maximum) {
(Ok(init), Some(max)) if u64::from(init) == max => Some(init),
_ => None,
});
(ImportKind::Table, 0)
}
wasmparser::TypeRef::Global(g) => {
if matches!(
g.content_type,
wasmparser::ValType::F32 | wasmparser::ValType::F64
) {
float_globals.insert(num_imported_globals);
}
if g.content_type == wasmparser::ValType::V128 {
v128_globals.insert(num_imported_globals);
}
num_imported_globals += 1;
(ImportKind::Global, 0)
}
_ => continue,
};
imports.push(ImportEntry {
module: import.module.to_string(),
name: import.name.to_string(),
kind,
index: idx,
});
}
}
Payload::FunctionSection(reader) => {
for ty in reader {
let type_idx = ty.context("Failed to parse function type index")?;
func_type_indices.push(type_idx); func_arg_counts
.push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
func_ret_i64.push(
type_ret_i64
.get(type_idx as usize)
.copied()
.unwrap_or(false),
);
func_params_i64.push(
type_params_i64
.get(type_idx as usize)
.cloned()
.unwrap_or_default(),
);
func_sig_has_v128.push(
type_has_v128
.get(type_idx as usize)
.copied()
.unwrap_or(false),
);
}
}
Payload::TableSection(reader) => {
for table in reader {
let table = table.context("Failed to parse table")?;
table_sizes.push(u32::try_from(table.ty.initial).ok());
}
}
Payload::MemorySection(reader) => {
for (idx, memory) in reader.into_iter().enumerate() {
let mem = memory.context("Failed to parse memory")?;
memories.push(WasmMemory {
index: idx as u32,
initial_pages: mem.initial as u32,
max_pages: mem.maximum.map(|m| m as u32),
shared: mem.shared,
});
}
}
Payload::GlobalSection(reader) => {
for (idx, global) in reader.into_iter().enumerate() {
let global = global.context("Failed to parse global")?;
let mut ops = global.init_expr.get_operators_reader();
let init = match ops.read() {
Ok(wasmparser::Operator::I32Const { value }) => {
Some(GlobalInit::I32(value))
}
Ok(wasmparser::Operator::I64Const { value }) => {
Some(GlobalInit::I64(value))
}
_ => None,
};
let slot_bytes = match global.ty.content_type {
wasmparser::ValType::I64 | wasmparser::ValType::F64 => 8,
wasmparser::ValType::V128 => 16,
_ => 4,
};
if matches!(
global.ty.content_type,
wasmparser::ValType::F32 | wasmparser::ValType::F64
) {
float_globals.insert(num_imported_globals + idx as u32);
}
if global.ty.content_type == wasmparser::ValType::V128 {
v128_globals.insert(num_imported_globals + idx as u32);
}
globals.push(WasmGlobal {
index: idx as u32,
init,
mutable: global.ty.mutable,
slot_bytes,
});
}
}
Payload::DataSection(reader) => {
for data in reader {
let data = data.context("Failed to parse data segment")?;
if let wasmparser::DataKind::Active {
memory_index: 0,
offset_expr,
} = data.kind
{
let mut ops = offset_expr.get_operators_reader();
if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
data_segments.push((value as u32, data.data.to_vec()));
}
}
}
}
Payload::ElementSection(reader) => {
for elem in reader {
let elem = elem.context("Failed to parse element segment")?;
let (seg_table, seg_offset): (u32, Option<u32>) = match &elem.kind {
wasmparser::ElementKind::Active {
table_index,
offset_expr,
} => {
let mut ops = offset_expr.get_operators_reader();
let off = match ops.read() {
Ok(wasmparser::Operator::I32Const { value }) => {
u32::try_from(value).ok()
}
_ => None,
};
(table_index.unwrap_or(0), off)
}
_ => (0, None),
};
let mut seg_funcs: Option<Vec<u32>> = Some(Vec::new());
match elem.items {
wasmparser::ElementItems::Functions(funcs) => {
for f in funcs {
let f = f.context("Failed to parse element func index")?;
elem_func_indices.push(f);
if let Some(v) = seg_funcs.as_mut() {
v.push(f);
}
}
}
wasmparser::ElementItems::Expressions(_, exprs) => {
for expr in exprs {
let expr = expr.context("Failed to parse element expr")?;
let mut entry_func: Option<u32> = None;
let mut plain = true;
for (k, op) in expr.get_operators_reader().into_iter().enumerate() {
match (k, op.context("Failed to parse element op")?) {
(0, wasmparser::Operator::RefFunc { function_index }) => {
elem_func_indices.push(function_index);
entry_func = Some(function_index);
}
(_, wasmparser::Operator::End) => {}
(_, wasmparser::Operator::RefFunc { function_index }) => {
elem_func_indices.push(function_index);
plain = false;
}
_ => plain = false,
}
}
match (plain, entry_func, seg_funcs.as_mut()) {
(true, Some(f), Some(v)) => v.push(f),
_ => seg_funcs = None,
}
}
}
}
elem_segments.push(ElemSegmentInfo {
table_index: seg_table,
offset: seg_offset,
funcs: seg_funcs,
});
}
}
Payload::ExportSection(exports) => {
for export in exports {
let export = export.context("Failed to parse export")?;
if export.kind == ExternalKind::Func {
export_names.insert(export.index, export.name.to_string());
}
}
}
Payload::CodeSectionEntry(body) => {
let (ops, op_offsets, block_arity, mut unsupported) =
decode_function_body(&body, &type_block_arity, &float_globals, &v128_globals)?;
if unsupported.is_none()
&& func_sig_has_v128
.get(func_index as usize)
.copied()
.unwrap_or(false)
{
unsupported = Some(
"signature has a v128 param/result — no SIMD lowering \
for this target (#680)"
.to_string(),
);
}
let actual_index = num_imported_funcs + func_index;
let export_name = export_names.get(&actual_index).cloned();
functions.push(FunctionOps {
index: actual_index,
export_name,
debug_name: None, ops,
op_offsets,
unsupported,
block_arity,
});
func_index += 1;
}
Payload::CustomSection(c) => {
if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
parse_name_section_func_names(reader, &mut name_section_names);
}
if c.name() == crate::wsc_facts::WSC_FACTS_SECTION_NAME && wsc_facts.is_none() {
let parsed = crate::wsc_facts::parse_wsc_facts(c.data());
if let Some(reason) = &parsed.section_ignored {
eprintln!(
"warning: ignoring unparseable `wsc.facts` custom section \
({reason}) — facts are optional accelerators, compilation \
is unaffected (#494 fail-safe skew rule)"
);
} else if parsed.records_skipped > 0 {
eprintln!(
"warning: skipped {} unknown/undecodable `wsc.facts` \
record(s) (likely a newer loom emitter); {} known fact(s) \
kept, compilation is unaffected (#494 fail-safe skew rule)",
parsed.records_skipped,
parsed.facts.len()
);
}
wsc_facts = Some(parsed.facts);
}
}
_ => {}
}
}
apply_name_section(&mut functions, &name_section_names);
Ok(DecodedModule {
functions,
memories,
data_segments,
imports,
num_imported_funcs,
func_arg_counts,
type_arg_counts,
func_ret_i64,
type_ret_i64,
func_params_i64,
globals,
elem_func_indices,
table_size: table_sizes.first().copied().flatten(),
table_sizes,
elem_segments,
func_type_indices,
type_signatures,
wsc_facts: wsc_facts.unwrap_or_default(),
})
}
fn parse_name_section_func_names(
reader: wasmparser::NameSectionReader<'_>,
out: &mut HashMap<u32, String>,
) {
for subsection in reader.into_iter().flatten() {
if let wasmparser::Name::Function(map) = subsection {
for naming in map.into_iter().flatten() {
out.insert(naming.index, naming.name.to_string());
}
}
}
}
fn apply_name_section(functions: &mut [FunctionOps], names: &HashMap<u32, String>) {
if names.is_empty() {
return;
}
for f in functions {
f.debug_name = names.get(&f.index).cloned();
}
}
pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
let mut functions = Vec::new();
let mut func_index = 0u32;
let mut num_imported_funcs = 0u32;
let mut export_names: HashMap<u32, String> = HashMap::new();
let mut name_section_names: HashMap<u32, String> = HashMap::new();
let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
let mut num_imported_globals = 0u32;
let mut float_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut v128_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut type_has_v128: Vec<bool> = Vec::new();
let mut func_sig_has_v128: Vec<bool> = Vec::new();
for payload in Parser::new(0).parse_all(wasm_bytes) {
let payload = payload.context("Failed to parse WASM payload")?;
match payload {
Payload::TypeSection(reader) => {
for rec_group in reader {
let rec_group = rec_group.context("Failed to parse type")?;
for sub_ty in rec_group.types() {
type_block_arity.push(match &sub_ty.composite_type.inner {
wasmparser::CompositeInnerType::Func(f) => (
u8::try_from(f.params().len()).unwrap_or(u8::MAX),
u8::try_from(f.results().len()).unwrap_or(u8::MAX),
),
_ => (u8::MAX, u8::MAX),
});
type_has_v128.push(match &sub_ty.composite_type.inner {
wasmparser::CompositeInnerType::Func(f) => f
.params()
.iter()
.chain(f.results())
.any(|t| *t == wasmparser::ValType::V128),
_ => false,
});
}
}
}
Payload::ImportSection(imports) => {
for import in imports.into_imports() {
let import = import.context("Failed to parse import")?;
match import.ty {
wasmparser::TypeRef::Func(_) => num_imported_funcs += 1,
wasmparser::TypeRef::Global(g) => {
if matches!(
g.content_type,
wasmparser::ValType::F32 | wasmparser::ValType::F64
) {
float_globals.insert(num_imported_globals);
}
if g.content_type == wasmparser::ValType::V128 {
v128_globals.insert(num_imported_globals);
}
num_imported_globals += 1;
}
_ => {}
}
}
}
Payload::FunctionSection(reader) => {
for ty in reader {
let type_idx = ty.context("Failed to parse function type index")?;
func_sig_has_v128.push(
type_has_v128
.get(type_idx as usize)
.copied()
.unwrap_or(false),
);
}
}
Payload::GlobalSection(reader) => {
for (idx, global) in reader.into_iter().enumerate() {
let global = global.context("Failed to parse global")?;
if matches!(
global.ty.content_type,
wasmparser::ValType::F32 | wasmparser::ValType::F64
) {
float_globals.insert(num_imported_globals + idx as u32);
}
if global.ty.content_type == wasmparser::ValType::V128 {
v128_globals.insert(num_imported_globals + idx as u32);
}
}
}
Payload::ExportSection(exports) => {
for export in exports {
let export = export.context("Failed to parse export")?;
if export.kind == ExternalKind::Func {
export_names.insert(export.index, export.name.to_string());
}
}
}
Payload::CodeSectionEntry(body) => {
let (ops, op_offsets, block_arity, mut unsupported) =
decode_function_body(&body, &type_block_arity, &float_globals, &v128_globals)?;
if unsupported.is_none()
&& func_sig_has_v128
.get(func_index as usize)
.copied()
.unwrap_or(false)
{
unsupported = Some(
"signature has a v128 param/result — no SIMD lowering \
for this target (#680)"
.to_string(),
);
}
let actual_index = num_imported_funcs + func_index;
let export_name = export_names.get(&actual_index).cloned();
functions.push(FunctionOps {
index: actual_index,
export_name,
debug_name: None, ops,
op_offsets,
unsupported,
block_arity,
});
func_index += 1;
}
Payload::CustomSection(c) => {
if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
parse_name_section_func_names(reader, &mut name_section_names);
}
}
_ => {}
}
}
apply_name_section(&mut functions, &name_section_names);
Ok(functions)
}
#[derive(Debug, Clone)]
pub struct FunctionOps {
pub index: u32,
pub export_name: Option<String>,
pub debug_name: Option<String>,
pub ops: Vec<WasmOp>,
pub op_offsets: Vec<u32>,
pub unsupported: Option<String>,
pub block_arity: Vec<(u8, u8)>,
}
fn blocktype_arity(bt: &wasmparser::BlockType, type_block_arity: &[(u8, u8)]) -> (u8, u8) {
match bt {
wasmparser::BlockType::Empty => (0, 0),
wasmparser::BlockType::Type(_) => (0, 1),
wasmparser::BlockType::FuncType(i) => type_block_arity
.get(*i as usize)
.copied()
.unwrap_or((u8::MAX, u8::MAX)),
}
}
type DecodedBody = (Vec<WasmOp>, Vec<u32>, Vec<(u8, u8)>, Option<String>);
fn decode_function_body(
body: &wasmparser::FunctionBody,
type_block_arity: &[(u8, u8)],
float_globals: &std::collections::HashSet<u32>,
v128_globals: &std::collections::HashSet<u32>,
) -> Result<DecodedBody> {
let mut ops = Vec::new();
let mut op_offsets = Vec::new();
let mut block_arity: Vec<(u8, u8)> = Vec::new();
let mut unsupported: Option<String> = None;
for local in body.get_locals_reader()? {
let (count, ty) = local.context("Failed to read local declaration")?;
if unsupported.is_none() && count > 0 && ty == wasmparser::ValType::V128 {
unsupported = Some(
"declares a v128-typed local — no SIMD lowering for this \
target, accesses would silently truncate the 16-byte value \
(#680)"
.to_string(),
);
}
}
let ops_reader = body.get_operators_reader()?;
for item in ops_reader.into_iter_with_offsets() {
let (op, offset) = item.context("Failed to read operator")?;
if unsupported.is_none() && is_simd_operator(&op) {
unsupported = Some(format!(
"{op:?}: no SIMD lowering for this target — the op would be \
silently dropped to a no-op (WASM SIMD proposal, #680)"
));
}
if let Some(wasm_op) = convert_operator(&op) {
if let wasmparser::Operator::Block { blockty }
| wasmparser::Operator::Loop { blockty }
| wasmparser::Operator::If { blockty } = &op
{
block_arity.push(blocktype_arity(blockty, type_block_arity));
}
if unsupported.is_none()
&& let WasmOp::GlobalGet(i) | WasmOp::GlobalSet(i) = &wasm_op
&& float_globals.contains(i)
{
unsupported = Some(format!(
"{wasm_op:?} on an f32/f64-typed global — float globals \
have no lowering, the initializer would be silently \
zeroed (GI-FPU-001)"
));
}
if unsupported.is_none()
&& let WasmOp::GlobalGet(i) | WasmOp::GlobalSet(i) = &wasm_op
&& v128_globals.contains(i)
{
unsupported = Some(format!(
"{wasm_op:?} on a v128-typed global — no SIMD lowering \
for this target (#680)"
));
}
ops.push(wasm_op);
op_offsets.push(offset as u32);
} else if unsupported.is_none() && !is_intentionally_ignored(&op) {
unsupported = Some(format!("{op:?}"));
}
}
Ok((ops, op_offsets, block_arity, unsupported))
}
fn is_intentionally_ignored(op: &wasmparser::Operator) -> bool {
use wasmparser::Operator::*;
matches!(op, Nop)
}
fn is_simd_operator(op: &wasmparser::Operator) -> bool {
macro_rules! define_match_operator {
($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {
match op {
$(
wasmparser::Operator::$op { .. } => {
define_match_operator!(impl_one @$proposal)
}
)*
_ => false,
}
};
(impl_one @simd) => { true };
(impl_one @relaxed_simd) => { true };
(impl_one @$proposal:ident) => { false };
}
wasmparser::for_each_operator!(define_match_operator)
}
fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {
use wasmparser::Operator::*;
match op {
I32Const { value } => Some(WasmOp::I32Const(*value)),
I32Add => Some(WasmOp::I32Add),
I32Sub => Some(WasmOp::I32Sub),
I32Mul => Some(WasmOp::I32Mul),
I32DivS => Some(WasmOp::I32DivS),
I32DivU => Some(WasmOp::I32DivU),
I32RemS => Some(WasmOp::I32RemS),
I32RemU => Some(WasmOp::I32RemU),
I64Const { value } => Some(WasmOp::I64Const(*value)),
I64Add => Some(WasmOp::I64Add),
I64Sub => Some(WasmOp::I64Sub),
I64Mul => Some(WasmOp::I64Mul),
I64DivS => Some(WasmOp::I64DivS),
I64DivU => Some(WasmOp::I64DivU),
I64RemS => Some(WasmOp::I64RemS),
I64RemU => Some(WasmOp::I64RemU),
I64And => Some(WasmOp::I64And),
I64Or => Some(WasmOp::I64Or),
I64Xor => Some(WasmOp::I64Xor),
I64Shl => Some(WasmOp::I64Shl),
I64ShrS => Some(WasmOp::I64ShrS),
I64ShrU => Some(WasmOp::I64ShrU),
I64Rotl => Some(WasmOp::I64Rotl),
I64Rotr => Some(WasmOp::I64Rotr),
I64Clz => Some(WasmOp::I64Clz),
I64Ctz => Some(WasmOp::I64Ctz),
I64Popcnt => Some(WasmOp::I64Popcnt),
I64Extend8S => Some(WasmOp::I64Extend8S),
I64Extend16S => Some(WasmOp::I64Extend16S),
I64Extend32S => Some(WasmOp::I64Extend32S),
I64ExtendI32U => Some(WasmOp::I64ExtendI32U),
I64ExtendI32S => Some(WasmOp::I64ExtendI32S),
I32WrapI64 => Some(WasmOp::I32WrapI64),
I64Eqz => Some(WasmOp::I64Eqz),
I64Eq => Some(WasmOp::I64Eq),
I64Ne => Some(WasmOp::I64Ne),
I64LtS => Some(WasmOp::I64LtS),
I64LtU => Some(WasmOp::I64LtU),
I64LeS => Some(WasmOp::I64LeS),
I64LeU => Some(WasmOp::I64LeU),
I64GtS => Some(WasmOp::I64GtS),
I64GtU => Some(WasmOp::I64GtU),
I64GeS => Some(WasmOp::I64GeS),
I64GeU => Some(WasmOp::I64GeU),
I32And => Some(WasmOp::I32And),
I32Or => Some(WasmOp::I32Or),
I32Xor => Some(WasmOp::I32Xor),
I32Shl => Some(WasmOp::I32Shl),
I32ShrS => Some(WasmOp::I32ShrS),
I32ShrU => Some(WasmOp::I32ShrU),
I32Rotl => Some(WasmOp::I32Rotl),
I32Rotr => Some(WasmOp::I32Rotr),
I32Clz => Some(WasmOp::I32Clz),
I32Ctz => Some(WasmOp::I32Ctz),
I32Popcnt => Some(WasmOp::I32Popcnt),
I32Extend8S => Some(WasmOp::I32Extend8S),
I32Extend16S => Some(WasmOp::I32Extend16S),
I32Eqz => Some(WasmOp::I32Eqz),
I32Eq => Some(WasmOp::I32Eq),
I32Ne => Some(WasmOp::I32Ne),
I32LtS => Some(WasmOp::I32LtS),
I32LtU => Some(WasmOp::I32LtU),
I32LeS => Some(WasmOp::I32LeS),
I32LeU => Some(WasmOp::I32LeU),
I32GtS => Some(WasmOp::I32GtS),
I32GtU => Some(WasmOp::I32GtU),
I32GeS => Some(WasmOp::I32GeS),
I32GeU => Some(WasmOp::I32GeU),
I32Load { memarg } => Some(WasmOp::I32Load {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Store { memarg } => Some(WasmOp::I32Store {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Load { memarg } => Some(WasmOp::I64Load {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Store { memarg } => Some(WasmOp::I64Store {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Load8S { memarg } => Some(WasmOp::I32Load8S {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Load8U { memarg } => Some(WasmOp::I32Load8U {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Load16S { memarg } => Some(WasmOp::I32Load16S {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Load16U { memarg } => Some(WasmOp::I32Load16U {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Store8 { memarg } => Some(WasmOp::I32Store8 {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I32Store16 { memarg } => Some(WasmOp::I32Store16 {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
LocalGet { local_index } => Some(WasmOp::LocalGet(*local_index)),
LocalSet { local_index } => Some(WasmOp::LocalSet(*local_index)),
LocalTee { local_index } => Some(WasmOp::LocalTee(*local_index)),
GlobalGet { global_index } => Some(WasmOp::GlobalGet(*global_index)),
GlobalSet { global_index } => Some(WasmOp::GlobalSet(*global_index)),
Block { .. } => Some(WasmOp::Block),
Loop { .. } => Some(WasmOp::Loop),
Br { relative_depth } => Some(WasmOp::Br(*relative_depth)),
BrIf { relative_depth } => Some(WasmOp::BrIf(*relative_depth)),
BrTable { targets } => {
let default = targets.default();
let tgts: Vec<u32> = targets.targets().filter_map(Result::ok).collect();
Some(WasmOp::BrTable {
targets: tgts,
default,
})
}
Return => Some(WasmOp::Return),
Call { function_index } => Some(WasmOp::Call(*function_index)),
CallIndirect {
type_index,
table_index,
..
} => Some(WasmOp::CallIndirect {
type_index: *type_index,
table_index: *table_index,
}),
End => Some(WasmOp::End),
Unreachable => Some(WasmOp::Unreachable),
Nop => None,
Drop => Some(WasmOp::Drop),
Select => Some(WasmOp::Select),
If { .. } => Some(WasmOp::If),
Else => Some(WasmOp::Else),
I64Load8S { memarg } => Some(WasmOp::I64Load8S {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Load8U { memarg } => Some(WasmOp::I64Load8U {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Load16S { memarg } => Some(WasmOp::I64Load16S {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Load16U { memarg } => Some(WasmOp::I64Load16U {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Load32S { memarg } => Some(WasmOp::I64Load32S {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Load32U { memarg } => Some(WasmOp::I64Load32U {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Store8 { memarg } => Some(WasmOp::I64Store8 {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Store16 { memarg } => Some(WasmOp::I64Store16 {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
I64Store32 { memarg } => Some(WasmOp::I64Store32 {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
MemorySize { mem, .. } => Some(WasmOp::MemorySize(*mem)),
MemoryGrow { mem, .. } => Some(WasmOp::MemoryGrow(*mem)),
MemoryCopy {
dst_mem: 0,
src_mem: 0,
} => Some(WasmOp::MemoryCopy),
MemoryFill { mem: 0 } => Some(WasmOp::MemoryFill),
V128Const { value } => {
let mut bytes = [0u8; 16];
bytes.copy_from_slice(value.bytes());
Some(WasmOp::V128Const(bytes))
}
V128Load { memarg } => Some(WasmOp::V128Load {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
V128Store { memarg } => Some(WasmOp::V128Store {
offset: memarg.offset as u32,
align: memarg.align as u32,
}),
V128And => Some(WasmOp::V128And),
V128Or => Some(WasmOp::V128Or),
V128Xor => Some(WasmOp::V128Xor),
V128Not => Some(WasmOp::V128Not),
V128AndNot => Some(WasmOp::V128AndNot),
I8x16Add => Some(WasmOp::I8x16Add),
I8x16Sub => Some(WasmOp::I8x16Sub),
I8x16Neg => Some(WasmOp::I8x16Neg),
I8x16Eq => Some(WasmOp::I8x16Eq),
I8x16Ne => Some(WasmOp::I8x16Ne),
I8x16LtS => Some(WasmOp::I8x16LtS),
I8x16LtU => Some(WasmOp::I8x16LtU),
I8x16GtS => Some(WasmOp::I8x16GtS),
I8x16GtU => Some(WasmOp::I8x16GtU),
I8x16LeS => Some(WasmOp::I8x16LeS),
I8x16LeU => Some(WasmOp::I8x16LeU),
I8x16GeS => Some(WasmOp::I8x16GeS),
I8x16GeU => Some(WasmOp::I8x16GeU),
I8x16Splat => Some(WasmOp::I8x16Splat),
I8x16ExtractLaneS { lane } => Some(WasmOp::I8x16ExtractLaneS(*lane)),
I8x16ExtractLaneU { lane } => Some(WasmOp::I8x16ExtractLaneU(*lane)),
I8x16ReplaceLane { lane } => Some(WasmOp::I8x16ReplaceLane(*lane)),
I8x16Shuffle { lanes } => Some(WasmOp::I8x16Shuffle(*lanes)),
I8x16Swizzle => Some(WasmOp::I8x16Swizzle),
I16x8Add => Some(WasmOp::I16x8Add),
I16x8Sub => Some(WasmOp::I16x8Sub),
I16x8Mul => Some(WasmOp::I16x8Mul),
I16x8Neg => Some(WasmOp::I16x8Neg),
I16x8Eq => Some(WasmOp::I16x8Eq),
I16x8Ne => Some(WasmOp::I16x8Ne),
I16x8LtS => Some(WasmOp::I16x8LtS),
I16x8LtU => Some(WasmOp::I16x8LtU),
I16x8GtS => Some(WasmOp::I16x8GtS),
I16x8GtU => Some(WasmOp::I16x8GtU),
I16x8LeS => Some(WasmOp::I16x8LeS),
I16x8LeU => Some(WasmOp::I16x8LeU),
I16x8GeS => Some(WasmOp::I16x8GeS),
I16x8GeU => Some(WasmOp::I16x8GeU),
I16x8Splat => Some(WasmOp::I16x8Splat),
I16x8ExtractLaneS { lane } => Some(WasmOp::I16x8ExtractLaneS(*lane)),
I16x8ExtractLaneU { lane } => Some(WasmOp::I16x8ExtractLaneU(*lane)),
I16x8ReplaceLane { lane } => Some(WasmOp::I16x8ReplaceLane(*lane)),
I32x4Add => Some(WasmOp::I32x4Add),
I32x4Sub => Some(WasmOp::I32x4Sub),
I32x4Mul => Some(WasmOp::I32x4Mul),
I32x4Neg => Some(WasmOp::I32x4Neg),
I32x4Eq => Some(WasmOp::I32x4Eq),
I32x4Ne => Some(WasmOp::I32x4Ne),
I32x4LtS => Some(WasmOp::I32x4LtS),
I32x4LtU => Some(WasmOp::I32x4LtU),
I32x4GtS => Some(WasmOp::I32x4GtS),
I32x4GtU => Some(WasmOp::I32x4GtU),
I32x4LeS => Some(WasmOp::I32x4LeS),
I32x4LeU => Some(WasmOp::I32x4LeU),
I32x4GeS => Some(WasmOp::I32x4GeS),
I32x4GeU => Some(WasmOp::I32x4GeU),
I32x4Splat => Some(WasmOp::I32x4Splat),
I32x4ExtractLane { lane } => Some(WasmOp::I32x4ExtractLane(*lane)),
I32x4ReplaceLane { lane } => Some(WasmOp::I32x4ReplaceLane(*lane)),
I64x2Add => Some(WasmOp::I64x2Add),
I64x2Sub => Some(WasmOp::I64x2Sub),
I64x2Mul => Some(WasmOp::I64x2Mul),
I64x2Neg => Some(WasmOp::I64x2Neg),
I64x2Eq => Some(WasmOp::I64x2Eq),
I64x2Ne => Some(WasmOp::I64x2Ne),
I64x2LtS => Some(WasmOp::I64x2LtS),
I64x2GtS => Some(WasmOp::I64x2GtS),
I64x2LeS => Some(WasmOp::I64x2LeS),
I64x2GeS => Some(WasmOp::I64x2GeS),
I64x2Splat => Some(WasmOp::I64x2Splat),
I64x2ExtractLane { lane } => Some(WasmOp::I64x2ExtractLane(*lane)),
I64x2ReplaceLane { lane } => Some(WasmOp::I64x2ReplaceLane(*lane)),
F32x4Add => Some(WasmOp::F32x4Add),
F32x4Sub => Some(WasmOp::F32x4Sub),
F32x4Mul => Some(WasmOp::F32x4Mul),
F32x4Div => Some(WasmOp::F32x4Div),
F32x4Abs => Some(WasmOp::F32x4Abs),
F32x4Neg => Some(WasmOp::F32x4Neg),
F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
F32x4Eq => Some(WasmOp::F32x4Eq),
F32x4Ne => Some(WasmOp::F32x4Ne),
F32x4Lt => Some(WasmOp::F32x4Lt),
F32x4Le => Some(WasmOp::F32x4Le),
F32x4Gt => Some(WasmOp::F32x4Gt),
F32x4Ge => Some(WasmOp::F32x4Ge),
F32x4Splat => Some(WasmOp::F32x4Splat),
F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_simple_add() {
let wat = r#"
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].index, 0);
assert_eq!(functions[0].export_name, Some("add".to_string()));
assert_eq!(
functions[0].ops,
vec![
WasmOp::LocalGet(0),
WasmOp::LocalGet(1),
WasmOp::I32Add,
WasmOp::End
]
);
}
#[test]
fn test_decode_i64_i32_width_conversions() {
let wat = r#"
(module
(func (export "conv") (param i32 i64) (result i32)
local.get 0
i64.extend_i32_u
local.get 0
i64.extend_i32_s
i64.add
local.get 1
i64.add
i32.wrap_i64
)
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let functions = decode_wasm_functions(&wasm).expect("decode");
let ops = &functions[0].ops;
assert!(
ops.contains(&WasmOp::I64ExtendI32U),
"i64.extend_i32_u must decode (not be dropped): {ops:?}"
);
assert!(
ops.contains(&WasmOp::I64ExtendI32S),
"i64.extend_i32_s must decode (not be dropped): {ops:?}"
);
assert!(
ops.contains(&WasmOp::I32WrapI64),
"i32.wrap_i64 must decode (not be dropped): {ops:?}"
);
}
#[test]
fn test_decode_br_table() {
let wat = r#"
(module
(func (export "bt") (param i32) (result i32)
(block (block (block
local.get 0
br_table 2 0 1 2)
i32.const 30 return)
i32.const 20 return)
i32.const 10))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let functions = decode_wasm_functions(&wasm).expect("decode");
let bt = functions[0]
.ops
.iter()
.find_map(|o| match o {
WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
_ => None,
})
.expect("br_table must decode (not be dropped)");
assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
assert_eq!(bt.1, 2, "br_table default preserved");
}
#[test]
fn test_decode_arithmetic() {
let wat = r#"
(module
(func (export "calc") (result i32)
i32.const 5
i32.const 3
i32.mul
i32.const 2
i32.add
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].export_name, Some("calc".to_string()));
assert_eq!(
functions[0].ops,
vec![
WasmOp::I32Const(5),
WasmOp::I32Const(3),
WasmOp::I32Mul,
WasmOp::I32Const(2),
WasmOp::I32Add,
WasmOp::End,
]
);
}
#[test]
fn test_decode_multi_function_module() {
let wat = r#"
(module
(func $helper)
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
(func (export "sub") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.sub
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 3);
assert_eq!(functions[0].index, 0);
assert_eq!(functions[0].export_name, None);
assert_eq!(functions[1].index, 1);
assert_eq!(functions[1].export_name, Some("add".to_string()));
assert_eq!(functions[2].index, 2);
assert_eq!(functions[2].export_name, Some("sub".to_string()));
}
#[test]
fn test_decode_module_with_imports() {
let wat = r#"
(module
(import "env" "log" (func $log (param i32)))
(import "env" "memory" (memory 1))
(func (export "run") (param i32)
local.get 0
call 0
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let module = decode_wasm_module(&wasm).expect("Failed to decode");
assert_eq!(module.imports.len(), 2);
assert_eq!(module.num_imported_funcs, 1);
assert_eq!(module.imports[0].module, "env");
assert_eq!(module.imports[0].name, "log");
assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
assert_eq!(module.imports[1].module, "env");
assert_eq!(module.imports[1].name, "memory");
assert_eq!(module.imports[1].kind, ImportKind::Memory);
assert_eq!(module.functions.len(), 1);
assert_eq!(module.functions[0].index, 1);
assert_eq!(module.functions[0].export_name, Some("run".to_string()));
}
#[test]
fn test_find_function_by_export_name() {
let wat = r#"
(module
(func $helper)
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
let add_func = functions
.iter()
.find(|f| f.export_name.as_deref() == Some("add"))
.expect("Should find 'add' function");
assert_eq!(add_func.index, 1);
assert!(add_func.ops.contains(&WasmOp::I32Add));
}
#[test]
fn test_decode_subword_loads() {
let wat = r#"
(module
(memory 1)
(func (export "test") (param i32) (result i32)
local.get 0
i32.load8_u
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
offset: 0,
align: 0,
}));
}
#[test]
fn test_decode_subword_stores() {
let wat = r#"
(module
(memory 1)
(func (export "test") (param i32 i32)
local.get 0
local.get 1
i32.store8
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
offset: 0,
align: 0,
}));
}
#[test]
fn test_decode_memory_size_grow() {
let wat = r#"
(module
(memory 1)
(func (export "test") (result i32)
memory.size
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
}
#[test]
fn test_decode_memory_grow() {
let wat = r#"
(module
(memory 1)
(func (export "test") (param i32) (result i32)
local.get 0
memory.grow
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
}
#[test]
fn test_decode_bulk_memory_374() {
let wat = r#"
(module
(memory 1)
(func (export "cpy") (param i32 i32 i32)
local.get 0 local.get 1 local.get 2 memory.copy)
(func (export "fil") (param i32 i32 i32)
local.get 0 local.get 1 local.get 2 memory.fill)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 2);
assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
assert!(functions[0].unsupported.is_none());
assert!(functions[1].unsupported.is_none());
}
#[test]
fn test_decode_i64_subword_loads() {
let wat = r#"
(module
(memory 1)
(func (export "test") (param i32) (result i64)
local.get 0
i64.load8_s
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
offset: 0,
align: 0,
}));
}
#[test]
fn test_decode_all_subword_memory_ops() {
let wat = r#"
(module
(memory 1)
(func (export "test") (param i32)
;; i32 sub-word loads
local.get 0
i32.load8_s
drop
local.get 0
i32.load8_u
drop
local.get 0
i32.load16_s
drop
local.get 0
i32.load16_u
drop
;; i32 sub-word stores
local.get 0
i32.const 42
i32.store8
local.get 0
i32.const 42
i32.store16
;; i64 sub-word loads
local.get 0
i64.load8_s
drop
local.get 0
i64.load8_u
drop
local.get 0
i64.load16_s
drop
local.get 0
i64.load16_u
drop
local.get 0
i64.load32_s
drop
local.get 0
i64.load32_u
drop
;; i64 sub-word stores
local.get 0
i64.const 42
i64.store8
local.get 0
i64.const 42
i64.store16
local.get 0
i64.const 42
i64.store32
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
let ops = &functions[0].ops;
assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
}
#[test]
fn test_decode_simd_i32x4_add() {
let wat = r#"
(module
(func (export "add_v128") (param v128 v128) (result v128)
local.get 0
local.get 1
i32x4.add
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(
functions[0].ops.contains(&WasmOp::I32x4Add),
"Should decode i32x4.add: {:?}",
functions[0].ops
);
}
#[test]
fn test_decode_simd_v128_const() {
let wat = r#"
(module
(func (export "const_v128") (result v128)
v128.const i32x4 1 2 3 4
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(
functions[0]
.ops
.iter()
.any(|o| matches!(o, WasmOp::V128Const(_))),
"Should decode v128.const: {:?}",
functions[0].ops
);
}
#[test]
fn test_decode_simd_v128_load_store() {
let wat = r#"
(module
(memory 1)
(func (export "load_store") (param i32)
local.get 0
v128.load
local.get 0
v128.store
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
let ops = &functions[0].ops;
assert!(
ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
"Should decode v128.load"
);
assert!(
ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
"Should decode v128.store"
);
}
#[test]
fn test_decode_simd_bitwise_ops() {
let wat = r#"
(module
(func (export "bitwise") (param v128 v128) (result v128)
local.get 0
local.get 1
v128.and
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::V128And));
}
#[test]
fn test_decode_simd_splat() {
let wat = r#"
(module
(func (export "splat") (param i32) (result v128)
local.get 0
i32x4.splat
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
}
#[test]
fn test_decode_simd_extract_lane() {
let wat = r#"
(module
(func (export "extract") (param v128) (result i32)
local.get 0
i32x4.extract_lane 2
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(
functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
"Should decode i32x4.extract_lane 2"
);
}
#[test]
fn test_decode_simd_f32x4_arithmetic() {
let wat = r#"
(module
(func (export "f32x4_add") (param v128 v128) (result v128)
local.get 0
local.get 1
f32x4.add
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
}
#[test]
fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
let wat = r#"
(module
(func (export "fadd") (param f32 f32) (result f32)
local.get 0 local.get 1 f32.add)
(func (export "iadd") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let functions = decode_wasm_functions(&wasm).expect("decode");
let fadd = functions
.iter()
.find(|f| f.export_name.as_deref() == Some("fadd"))
.unwrap();
let iadd = functions
.iter()
.find(|f| f.export_name.as_deref() == Some("iadd"))
.unwrap();
assert!(
fadd.unsupported.is_some(),
"f32.add must flag the function unsupported (loud-skip), got {:?}",
fadd.unsupported
);
assert!(
fadd.unsupported.as_deref().unwrap().contains("F32Add"),
"diagnostic should name the op: {:?}",
fadd.unsupported
);
assert!(
iadd.unsupported.is_none(),
"a pure-integer function must NOT be flagged: {:?}",
iadd.unsupported
);
}
#[test]
fn test_369_float_global_access_flags_function_unsupported() {
let wat = r#"
(module
(global $fg f32 (f32.const 2.5))
(global $dg (mut f64) (f64.const 1.5))
(global $ig (mut i32) (i32.const 7))
(func (export "fget") (result f32) global.get $fg)
(func (export "dset") (param f64) local.get 0 global.set $dg)
(func (export "iget") (result i32) global.get $ig))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode module");
for functions in [
&module.functions,
&decode_wasm_functions(&wasm).expect("decode fns"),
] {
let by_name = |n: &str| {
functions
.iter()
.find(|f| f.export_name.as_deref() == Some(n))
.unwrap()
};
let fget = by_name("fget");
assert!(
fget.unsupported.is_some(),
"global.get of an f32 global must flag the function (loud-skip), got {:?}",
fget.unsupported
);
let reason = fget.unsupported.as_deref().unwrap();
assert!(
reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
"diagnostic should name the op and GI-FPU-001: {reason:?}"
);
let dset = by_name("dset");
assert!(
dset.unsupported
.as_deref()
.is_some_and(|r| r.contains("GlobalSet")),
"global.set of an f64 global must flag the function, got {:?}",
dset.unsupported
);
assert!(
by_name("iget").unsupported.is_none(),
"an i32 global access must NOT be flagged: {:?}",
by_name("iget").unsupported
);
}
}
#[test]
fn test_369_imported_float_global_shifts_index_space() {
let wat = r#"
(module
(import "env" "fg" (global f64))
(global $ig i32 (i32.const 3))
(func (export "fget") (result f64) global.get 0)
(func (export "iget") (result i32) global.get 1))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let functions = decode_wasm_functions(&wasm).expect("decode");
let by_name = |n: &str| {
functions
.iter()
.find(|f| f.export_name.as_deref() == Some(n))
.unwrap()
};
assert!(
by_name("fget")
.unsupported
.as_deref()
.is_some_and(|r| r.contains("GI-FPU-001")),
"imported f64 global access must flag: {:?}",
by_name("fget").unsupported
);
assert!(
by_name("iget").unsupported.is_none(),
"defined i32 global at shifted index 1 must NOT flag: {:?}",
by_name("iget").unsupported
);
}
#[test]
fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
let wat = r#"
(module
(memory 1)
(func (export "vadd") (param i32 i32) (result i32)
(i32x4.extract_lane 2
(i32x4.add (i32x4.splat (local.get 0))
(i32x4.splat (local.get 1)))))
(func (export "vstore") (param i32 i32) (result i32)
(v128.store (i32.const 0) (i32x4.splat (local.get 0)))
(i32.load (i32.const 0)))
(func (export "iadd") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode module");
for functions in [
&module.functions,
&decode_wasm_functions(&wasm).expect("decode fns"),
] {
let by_name = |n: &str| {
functions
.iter()
.find(|f| f.export_name.as_deref() == Some(n))
.unwrap()
};
for name in ["vadd", "vstore"] {
let reason = by_name(name).unsupported.as_deref();
assert!(
reason.is_some(),
"{name}: v128 ops must flag the function (loud-skip), got None"
);
let reason = reason.unwrap();
assert!(
reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
"{name}: diagnostic must name the target gap and #680: {reason:?}"
);
}
assert!(
by_name("vadd")
.unsupported
.as_deref()
.unwrap()
.contains("I32x4Splat"),
"diagnostic should name the op: {:?}",
by_name("vadd").unsupported
);
assert!(
by_name("iadd").unsupported.is_none(),
"a scalar function in the same module must NOT be flagged: {:?}",
by_name("iadd").unsupported
);
}
}
#[test]
fn test_680_v128_local_and_signature_flag_function() {
let wat = r#"
(module
(func (export "vlocal") (result i32) (local v128)
i32.const 7)
(func (export "vpass") (param v128) (result v128)
local.get 0)
(func (export "scalar") (param i32) (result i32)
local.get 0))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode module");
for functions in [
&module.functions,
&decode_wasm_functions(&wasm).expect("decode fns"),
] {
let by_name = |n: &str| {
functions
.iter()
.find(|f| f.export_name.as_deref() == Some(n))
.unwrap()
};
assert!(
by_name("vlocal")
.unsupported
.as_deref()
.is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
"a v128-typed local declaration must flag: {:?}",
by_name("vlocal").unsupported
);
assert!(
by_name("vpass")
.unsupported
.as_deref()
.is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
"a v128 param/result signature must flag (op-free body!): {:?}",
by_name("vpass").unsupported
);
assert!(
by_name("scalar").unsupported.is_none(),
"a scalar function must NOT be flagged: {:?}",
by_name("scalar").unsupported
);
}
}
#[test]
fn test_680_v128_global_access_flags_function() {
let wat = r#"
(module
(import "env" "vg" (global v128))
(global $ig (mut i32) (i32.const 7))
(global $dg (mut v128) (v128.const i32x4 1 2 3 4))
(func (export "vget") global.get 0 drop)
(func (export "iget") (result i32) global.get $ig))
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode module");
for functions in [
&module.functions,
&decode_wasm_functions(&wasm).expect("decode fns"),
] {
let by_name = |n: &str| {
functions
.iter()
.find(|f| f.export_name.as_deref() == Some(n))
.unwrap()
};
let reason = by_name("vget").unsupported.as_deref();
assert!(
reason.is_some_and(|r| r.contains("GlobalGet")
&& r.contains("v128-typed global")
&& r.contains("#680")),
"global.get of an imported v128 global must flag: {reason:?}"
);
assert!(
by_name("iget").unsupported.is_none(),
"an i32 global access must NOT be flagged: {:?}",
by_name("iget").unsupported
);
}
}
#[test]
fn test_decode_simd_multiple_ops() {
let wat = r#"
(module
(func (export "simd_ops") (param v128 v128 v128) (result v128)
;; (a + b) * c
local.get 0
local.get 1
i32x4.add
local.get 2
i32x4.mul
)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
assert_eq!(functions.len(), 1);
let ops = &functions[0].ops;
assert!(ops.contains(&WasmOp::I32x4Add));
assert!(ops.contains(&WasmOp::I32x4Mul));
}
#[test]
fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
let wat = r#"
(module
(func (export "f") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
i32.const 7
i32.mul))
"#;
let wasm = wat::parse_str(wat).expect("parse WAT");
let functions = decode_wasm_functions(&wasm).expect("decode");
let f = &functions[0];
assert_eq!(
f.op_offsets.len(),
f.ops.len(),
"op_offsets must be parallel to ops"
);
assert!(!f.op_offsets.is_empty());
assert!(
f.op_offsets.windows(2).all(|w| w[1] > w[0]),
"wasm byte offsets must strictly increase: {:?}",
f.op_offsets
);
assert!(
f.op_offsets[0] >= 8,
"module-relative offset is past the 8-byte wasm header"
);
}
#[test]
fn test_decode_captures_global_initializer() {
let wat = r#"
(module
(memory 2)
(global $__stack_pointer (mut i32) (i32.const 65536))
(global $immutable_const i32 (i32.const 7))
(func (export "f") (result i32) global.get 0)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let module = decode_wasm_module(&wasm).expect("Failed to decode");
assert_eq!(module.globals.len(), 2, "both globals captured");
let sp = &module.globals[0];
assert_eq!(sp.index, 0);
assert_eq!(
sp.init,
Some(GlobalInit::I32(65536)),
"stack-pointer init captured"
);
assert!(sp.mutable, "stack pointer is mutable");
let c = &module.globals[1];
assert_eq!(c.init, Some(GlobalInit::I32(7)));
assert!(!c.mutable, "second global is immutable");
assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
assert_eq!(c.slot_bytes, 4);
}
#[test]
fn test_decode_records_global_slot_widths_643() {
let wat = r#"
(module
(global $c (mut i64) (i64.const 0))
(global $k (mut i32) (i32.const 0))
(global $f (mut f64) (f64.const 0))
(func (export "f") (result i32) global.get 1)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let module = decode_wasm_module(&wasm).expect("Failed to decode");
assert_eq!(module.globals.len(), 3);
assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
}
#[test]
fn test_decode_captures_i64_global_initializer_649() {
let wat = r#"
(module
(global $g (mut i64) (i64.const 0x123456789ABCDEF0))
(global $n (mut i64) (i64.const -1))
(global $f (mut f64) (f64.const 1.5))
(global $h (mut f32) (f32.const 2.5))
(func (export "f") (result i32) i32.const 0)
)
"#;
let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
let module = decode_wasm_module(&wasm).expect("Failed to decode");
assert_eq!(module.globals.len(), 4);
assert_eq!(
module.globals[0].init,
Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
"nonzero i64 init captured with both words"
);
assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
assert_eq!(
module.globals[2].init, None,
"f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
);
assert_eq!(
module.globals[3].init, None,
"f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
);
}
#[test]
fn test_decode_records_block_arity_side_table_509() {
let wat = r#"
(module
(func (export "f") (param i32) (result i32)
(block (result i32)
(block (nop))
(local.get 0)
(if (result i32)
(then (i32.const 1))
(else (i32.const 2)))))
(func (export "g") (result i32)
(block (result i32 i32)
(i32.const 1) (i32.const 2))
i32.add)
(func (export "h") (param i32) (result i32)
(local.get 0)
(loop (param i32) (result i32))))
"#;
let wasm = wat::parse_str(wat).expect("parse WAT");
for functions in [
decode_wasm_functions(&wasm).expect("decode"),
decode_wasm_module(&wasm).expect("decode").functions,
] {
assert_eq!(
functions[0].block_arity,
vec![(0, 1), (0, 0), (0, 1)],
"f: ValType/Empty/ValType blocktypes"
);
assert_eq!(
functions[1].block_arity,
vec![(0, 2)],
"g: functype blocktype result count from the type section"
);
assert_eq!(
functions[2].block_arity,
vec![(1, 1)],
"h: loop params captured"
);
}
}
#[test]
fn test_call_indirect_guards_closed_world_verified_642() {
let wat = r#"
(module
(type $bin (func (param i32 i32) (result i32)))
(table 3 funcref)
(elem (i32.const 0) $add $sub $mul)
(func $add (param i32 i32) (result i32)
(i32.add (local.get 0) (local.get 1)))
(func $sub (param i32 i32) (result i32)
(i32.sub (local.get 0) (local.get 1)))
(func $mul (param i32 i32) (result i32)
(i32.mul (local.get 0) (local.get 1)))
(func (export "f") (param i32 i32) (result i32)
(call_indirect (type $bin)
(local.get 0) (i32.const 10) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
assert_eq!(module.table_size, Some(3), "table section min size");
assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
assert_eq!(
module.elem_segments,
vec![ElemSegmentInfo {
table_index: 0,
offset: Some(0),
funcs: Some(vec![0, 1, 2]),
}]
);
assert_eq!(module.func_type_indices.len(), 4);
let guards = module.call_indirect_guards();
assert_eq!(guards.tables.len(), 1);
assert_eq!(guards.tables[0].table_size, Some(3));
assert_eq!(
guards.tables[0].base_byte_offset,
Some(0),
"#650: a single-table module keeps table 0 at R11 offset 0 by construction"
);
assert_eq!(
guards.tables[0].type_reject.first(),
Some(&None),
"closed-world type check must verify the homogeneous table: {:?}",
guards.tables[0].type_reject
);
assert!(
!guards.tables[0].has_null_slots,
"#664: a fully-initialized table must NOT request the runtime \
null check (dispatch bytes stay identical by construction)"
);
}
#[test]
fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
let wat = r#"
(module
(type $bin (func (param i32 i32) (result i32)))
(type $un (func (param i32) (result i32)))
(table 2 funcref)
(elem (i32.const 0) $add $neg)
(func $add (type $bin)
(i32.add (local.get 0) (local.get 1)))
(func $neg (type $un)
(i32.sub (i32.const 0) (local.get 0)))
(func (export "f") (param i32 i32) (result i32)
(call_indirect (type $bin)
(local.get 0) (i32.const 10) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert_eq!(guards.tables[0].table_size, Some(2));
assert!(
guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
"heterogeneous table must reject every expected type: {:?}",
guards.tables[0].type_reject
);
assert!(
guards.tables[0].runtime_type_check,
"heterogeneous-but-known table must offer the runtime check (#676)"
);
assert_eq!(
guards.type_ids_byte_offset,
Some(8),
"sidecar sits after the 2-slot pointer region"
);
assert_eq!(
guards.type_ids_image,
vec![1, 2],
"slot 0 = $bin (class 1), slot 1 = $un (class 2)"
);
assert_eq!(guards.type_class_ids, vec![1, 2]);
}
#[test]
fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
let wat = r#"
(module
(type $s (func (result i32)))
(table 3 funcref)
(elem (i32.const 0) $f0 $f1)
(func $f0 (result i32) (i32.const 10))
(func $f1 (result i32) (i32.const 11))
(func (export "run") (param i32) (result i32)
(call_indirect (type $s) (local.get 0)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert_eq!(guards.tables[0].table_size, Some(3));
assert_eq!(
guards.tables[0].type_reject.first(),
Some(&None),
"initialized slots are homogeneous in $s — the verdict must \
verify despite the null slot (#664): {:?}",
guards.tables[0].type_reject
);
assert!(
guards.tables[0].has_null_slots,
"slot 2 is uninitialized — the lowering must emit the runtime \
null check (#664)"
);
}
#[test]
fn test_call_indirect_guards_sparse_table_664() {
let wat = r#"
(module
(type $t (func (param i32) (result i32)))
(table 4 4 funcref)
(func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
(func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
(elem (i32.const 1) $f1)
(elem (i32.const 3) $f3)
(func (export "via") (param i32 i32) (result i32)
(call_indirect (type $t) (local.get 0) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert_eq!(guards.tables[0].table_size, Some(4));
assert_eq!(
guards.tables[0].type_reject.first(),
Some(&None),
"slots 1,3 are homogeneous in $t — verified: {:?}",
guards.tables[0].type_reject
);
assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
let wat = r#"
(module
(type $t (func (param i32) (result i32)))
(type $u (func (param i32 i32) (result i32)))
(table 4 4 funcref)
(func $f1 (type $t) (local.get 0))
(func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
(elem (i32.const 1) $f1)
(elem (i32.const 3) $f3)
(func (export "via") (param i32 i32) (result i32)
(call_indirect (type $t) (local.get 0) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert!(
guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
"a heterogeneous sparse table must still reject every type: {:?}",
guards.tables[0].type_reject
);
assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
assert_eq!(
guards.type_ids_image,
vec![0, 1, 0, 2],
"nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
$u slot 3 = class 2"
);
}
#[test]
fn test_call_indirect_guards_heterogeneous_sidecar_676() {
let wat = r#"
(module
(type $bin (func (param i32 i32) (result i32)))
(type $un (func (param i32) (result i32)))
(type $bin2 (func (param i32 i32) (result i32)))
(table 5 5 funcref)
(func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
(func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
(func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
(elem (i32.const 0) func $add $neg $sub)
(func (export "via2") (param i32 i32) (result i32)
(call_indirect (type $bin)
(local.get 0) (i32.const 3) (local.get 1)))
(func (export "via1") (param i32 i32) (result i32)
(call_indirect (type $un) (local.get 0) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert!(guards.tables[0].runtime_type_check);
assert_eq!(
guards.type_class_ids,
vec![1, 2, 1],
"$bin2 is a structural duplicate of $bin — one class id (#676)"
);
assert_eq!(
guards.type_ids_image,
vec![1, 2, 1, 0, 0],
"slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
);
assert_eq!(
guards.type_ids_byte_offset,
Some(20),
"sidecar starts after the 5 pointer words"
);
let wat = r#"
(module
(type $t (func (param i32) (result i32)))
(table 2 2 funcref)
(func $f0 (type $t) (local.get 0))
(func $f1 (type $t) (i32.const 7))
(elem (i32.const 0) func $f0 $f1)
(func (export "via") (param i32 i32) (result i32)
(call_indirect (type $t) (local.get 0) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert!(!guards.tables[0].runtime_type_check);
assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
assert!(guards.type_ids_image.is_empty());
assert!(guards.type_class_ids.is_empty());
}
#[test]
fn test_call_indirect_guards_no_table_642() {
let wat = r#"
(module
(func (export "f") (param i32) (result i32) (local.get 0))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
assert_eq!(module.table_size, None);
assert!(module.table_sizes.is_empty(), "#650: no tables declared");
let guards = module.call_indirect_guards();
assert!(
guards.tables.is_empty(),
"no table → no guard entry → every call_indirect declines"
);
}
#[test]
fn test_call_indirect_guards_duplicate_types_verified_642() {
let wat = r#"
(module
(type $a (func (result i32)))
(type $b (func (result i32)))
(table 1 funcref)
(elem (i32.const 0) $f)
(func $f (type $a) (i32.const 7))
(func (export "run") (param i32) (result i32)
(call_indirect (type $b) (local.get 0)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
let guards = module.call_indirect_guards();
assert_eq!(
&guards.tables[0].type_reject[0..2],
&[None, None],
"structural signature comparison must accept duplicate types: {:?}",
guards.tables[0].type_reject
);
assert!(
guards.tables[0].type_reject[2].is_some(),
"the structurally-different third type must still be rejected"
);
}
#[test]
fn test_call_indirect_guards_multi_table_650() {
let wat = r#"
(module
(type $t (func (param i32) (result i32)))
(type $u (func (param i32 i32) (result i32)))
(table $t0 3 3 funcref)
(table $t1 2 2 funcref)
(func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
(func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
(func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
(func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
(func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
(elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
(elem (table $t1) (i32.const 0) func $b0 $b1)
(func (export "f") (param i32 i32) (result i32)
(call_indirect $t1 (type $u)
(local.get 0) (i32.const 10) (local.get 1)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
assert_eq!(
module.elem_segments[0].table_index, 0,
"segment 0 targets table 0"
);
assert_eq!(
module.elem_segments[1],
ElemSegmentInfo {
table_index: 1,
offset: Some(0),
funcs: Some(vec![3, 4]),
},
"segment 1 is statically attributed to table 1 (#650)"
);
let guards = module.call_indirect_guards();
assert_eq!(guards.tables.len(), 2);
assert_eq!(guards.tables[0].table_size, Some(3));
assert_eq!(guards.tables[0].base_byte_offset, Some(0));
assert_eq!(guards.tables[1].table_size, Some(2));
assert_eq!(
guards.tables[1].base_byte_offset,
Some(12),
"table 1 base = size(table 0) * 4 within the contiguous R11 region"
);
assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
}
#[test]
fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
let wat = r#"
(module
(type $t (func (result i32)))
(import "env" "tbl" (table 4 funcref))
(table $d 1 1 funcref)
(func $f (type $t) (i32.const 7))
(elem (table $d) (i32.const 0) func $f)
(func (export "run") (param i32) (result i32)
(call_indirect $d (type $t) (local.get 0)))
)
"#;
let wasm = wat::parse_str(wat).expect("parse");
let module = decode_wasm_module(&wasm).expect("decode");
assert_eq!(
module.table_sizes,
vec![None, Some(1)],
"growable import (no max) has no sound compile-time size"
);
let guards = module.call_indirect_guards();
assert_eq!(guards.tables[0].base_byte_offset, Some(0));
assert!(
guards.tables[0].type_reject.iter().all(|r| r.is_some()),
"unknown-size table rejects every type"
);
assert_eq!(
guards.tables[1].base_byte_offset, None,
"a later table's base is not a compile-time constant when a \
preceding table's size is unknown (#650)"
);
assert_eq!(guards.tables[1].table_size, Some(1));
}
}