use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum WasmOp {
I32Add,
I32Sub,
I32Mul,
I32DivS,
I32DivU,
I32RemS,
I32RemU,
I32And,
I32Or,
I32Xor,
I32Shl,
I32ShrS,
I32ShrU,
I32Rotl, I32Rotr, I32Clz, I32Ctz, I32Popcnt,
I32Extend8S, I32Extend16S,
I32Eqz, I32Eq,
I32Ne,
I32LtS,
I32LtU,
I32LeS,
I32LeU,
I32GtS,
I32GtU,
I32GeS,
I32GeU,
I32Const(i32),
I32Load {
offset: u32,
align: u32,
},
I32Store {
offset: u32,
align: u32,
},
I32Load8S {
offset: u32,
align: u32,
}, I32Load8U {
offset: u32,
align: u32,
}, I32Load16S {
offset: u32,
align: u32,
}, I32Load16U {
offset: u32,
align: u32,
},
I32Store8 {
offset: u32,
align: u32,
}, I32Store16 {
offset: u32,
align: u32,
},
Block,
Loop,
Br(u32), BrIf(u32), BrTable {
targets: Vec<u32>,
default: u32,
},
Return,
Call(u32),
CallIndirect {
type_index: u32,
table_index: u32,
},
LocalGet(u32),
LocalSet(u32),
LocalTee(u32),
GlobalGet(u32),
GlobalSet(u32),
MemorySize(u32), MemoryGrow(u32),
MemoryCopy, MemoryFill,
MultiMemory {
memory: u32,
op: Box<WasmOp>,
},
Drop,
Select,
If,
Else,
End,
Unreachable,
Nop,
I64Add,
I64Sub,
I64Mul,
I64DivS,
I64DivU,
I64RemS,
I64RemU,
I64And,
I64Or,
I64Xor,
I64Shl,
I64ShrS,
I64ShrU,
I64Rotl,
I64Rotr,
I64Clz,
I64Ctz,
I64Popcnt,
I64Eqz,
I64Eq,
I64Ne,
I64LtS,
I64LtU,
I64LeS,
I64LeU,
I64GtS,
I64GtU,
I64GeS,
I64GeU,
I64Const(i64),
I64Load {
offset: u32,
align: u32,
},
I64Store {
offset: u32,
align: u32,
},
I64Load8S {
offset: u32,
align: u32,
},
I64Load8U {
offset: u32,
align: u32,
},
I64Load16S {
offset: u32,
align: u32,
},
I64Load16U {
offset: u32,
align: u32,
},
I64Load32S {
offset: u32,
align: u32,
},
I64Load32U {
offset: u32,
align: u32,
},
I64Store8 {
offset: u32,
align: u32,
},
I64Store16 {
offset: u32,
align: u32,
},
I64Store32 {
offset: u32,
align: u32,
},
I64ExtendI32S, I64ExtendI32U, I32WrapI64,
I64Extend8S, I64Extend16S, I64Extend32S,
F32Add,
F32Sub,
F32Mul,
F32Div,
F32Eq,
F32Ne,
F32Lt,
F32Le,
F32Gt,
F32Ge,
F32Abs,
F32Neg,
F32Ceil,
F32Floor,
F32Trunc,
F32Nearest,
F32Sqrt,
F32Min,
F32Max,
F32Copysign,
F32Const(f32),
F32Load {
offset: u32,
align: u32,
},
F32Store {
offset: u32,
align: u32,
},
F32ConvertI32S, F32ConvertI32U, F32ConvertI64S, F32ConvertI64U, F32DemoteF64, F32ReinterpretI32, I32ReinterpretF32, I32TruncF32S, I32TruncF32U,
I32TruncSatF32S, I32TruncSatF32U, I64TruncSatF32S, I64TruncSatF32U,
F64Add,
F64Sub,
F64Mul,
F64Div,
F64Eq,
F64Ne,
F64Lt,
F64Le,
F64Gt,
F64Ge,
F64Abs,
F64Neg,
F64Ceil,
F64Floor,
F64Trunc,
F64Nearest,
F64Sqrt,
F64Min,
F64Max,
F64Copysign,
F64Const(f64),
F64Load {
offset: u32,
align: u32,
},
F64Store {
offset: u32,
align: u32,
},
F64ConvertI32S, F64ConvertI32U, F64ConvertI64S, F64ConvertI64U, F64PromoteF32, F64ReinterpretI64, I64ReinterpretF64, I64TruncF64S, I64TruncF64U, I32TruncF64S, I32TruncF64U, I64TruncF32S, I64TruncF32U,
I32TruncSatF64S, I32TruncSatF64U, I64TruncSatF64S, I64TruncSatF64U,
V128Const([u8; 16]), V128Load {
offset: u32,
align: u32,
}, V128Store {
offset: u32,
align: u32,
},
V128And, V128Or, V128Xor, V128Not, V128AndNot,
I8x16Add, I8x16Sub, I8x16Neg, I8x16Eq, I8x16Ne, I8x16LtS, I8x16LtU, I8x16GtS, I8x16GtU, I8x16LeS, I8x16LeU, I8x16GeS, I8x16GeU, I8x16Splat, I8x16ExtractLaneS(u8), I8x16ExtractLaneU(u8), I8x16ReplaceLane(u8), I8x16Shuffle([u8; 16]), I8x16Swizzle,
I16x8Add, I16x8Sub, I16x8Mul, I16x8Neg, I16x8Eq, I16x8Ne, I16x8LtS, I16x8LtU, I16x8GtS, I16x8GtU, I16x8LeS, I16x8LeU, I16x8GeS, I16x8GeU, I16x8Splat, I16x8ExtractLaneS(u8), I16x8ExtractLaneU(u8), I16x8ReplaceLane(u8),
I32x4Add, I32x4Sub, I32x4Mul, I32x4Neg, I32x4Eq, I32x4Ne, I32x4LtS, I32x4LtU, I32x4GtS, I32x4GtU, I32x4LeS, I32x4LeU, I32x4GeS, I32x4GeU, I32x4Splat, I32x4ExtractLane(u8), I32x4ReplaceLane(u8),
I64x2Add, I64x2Sub, I64x2Mul, I64x2Neg, I64x2Eq, I64x2Ne, I64x2LtS, I64x2GtS, I64x2LeS, I64x2GeS, I64x2Splat, I64x2ExtractLane(u8), I64x2ReplaceLane(u8),
F32x4Add, F32x4Sub, F32x4Mul, F32x4Div, F32x4Abs, F32x4Neg, F32x4Sqrt, F32x4Eq, F32x4Ne, F32x4Lt, F32x4Le, F32x4Gt, F32x4Ge, F32x4Splat, F32x4ExtractLane(u8), F32x4ReplaceLane(u8), }
pub fn referenced_locals(wasm_ops: &[WasmOp]) -> u32 {
wasm_ops
.iter()
.filter_map(|op| match op {
WasmOp::LocalGet(i) | WasmOp::LocalSet(i) | WasmOp::LocalTee(i) => Some(*i + 1),
_ => None,
})
.max()
.unwrap_or(0)
}
pub fn count_params_heuristic(wasm_ops: &[WasmOp]) -> u32 {
let mut first_access: std::collections::HashMap<u32, bool> = std::collections::HashMap::new();
for op in wasm_ops {
match op {
WasmOp::LocalGet(idx) => {
first_access.entry(*idx).or_insert(true);
}
WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) => {
first_access.entry(*idx).or_insert(false);
}
_ => {}
}
}
first_access
.iter()
.filter_map(|(&idx, &is_read_first)| if is_read_first { Some(idx + 1) } else { None })
.max()
.unwrap_or(0)
}
pub fn rewrite_memory_grow_zero(wasm_ops: &[WasmOp]) -> Vec<WasmOp> {
let mut out = Vec::with_capacity(wasm_ops.len());
let mut i = 0;
while i < wasm_ops.len() {
if matches!(wasm_ops[i], WasmOp::I32Const(0))
&& let Some(WasmOp::MemoryGrow(m)) = wasm_ops.get(i + 1)
{
out.push(WasmOp::MemorySize(*m));
i += 2;
} else {
out.push(wasm_ops[i].clone());
i += 1;
}
}
out
}
pub fn find_param_block_type(
wasm_ops: &[WasmOp],
block_arity: &[(u8, u8)],
) -> Option<(&'static str, usize, (u8, u8))> {
find_unlowered_param_block_type(wasm_ops, block_arity, ParamBlockLowering::DECLINED)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ParamBlockLowering {
pub block: bool,
pub r#if: bool,
pub r#loop: bool,
}
impl ParamBlockLowering {
pub const DECLINED: Self = Self {
block: false,
r#if: false,
r#loop: false,
};
fn lowers(self, what: &str) -> bool {
match what {
"block" => self.block,
"if" => self.r#if,
"loop" => self.r#loop,
_ => false,
}
}
}
pub fn arm_param_block_lowering(relocatable: bool) -> ParamBlockLowering {
if relocatable {
ParamBlockLowering {
block: true,
r#if: true,
r#loop: true,
}
} else {
ParamBlockLowering::DECLINED
}
}
pub fn find_unlowered_param_block_type(
wasm_ops: &[WasmOp],
block_arity: &[(u8, u8)],
lowered: ParamBlockLowering,
) -> Option<(&'static str, usize, (u8, u8))> {
if block_arity.iter().all(|&(p, _)| p == 0) {
return None; }
let mut ord = 0usize;
for op in wasm_ops {
let what = match op {
WasmOp::Block => "block",
WasmOp::Loop => "loop",
WasmOp::If => "if",
_ => continue,
};
let arity = block_arity.get(ord).copied().unwrap_or((0, 0));
if arity.0 != 0 && !lowered.lowers(what) {
return Some((what, ord, arity));
}
ord += 1;
}
None
}
pub fn param_block_decline_msg(backend: &str, what: &str, ord: usize, arity: (u8, u8)) -> String {
let why = match what {
"if" => {
"the RV32 checkpoint at frame entry cannot represent params \
consumed BELOW it (with an `else` the reconciliation split \
panics; without one the false path returns an uninitialized \
register — measured, #1097) and the ARM direct lowering is proven \
by the #1097 oracle only on --relocatable (RQ-64-MVLOWER); no \
other path has an execution-oracle leg"
}
"loop" => {
"the RV32 back-edge mis-reconciles the header value (measured \
lpb(3) = 2, want 10, #1097) and the ARM direct lowering is proven \
by the #1097 oracle only on --relocatable (RQ-64-MVLOWER); no \
other path has an execution-oracle leg"
}
_ => {
"the RV32 checkpoint drops the carried parameter on a branch edge \
(measured, #1097) and the ARM direct lowering is proven by the \
#1097 oracle only on --relocatable (RQ-64-MVLOWER); no other path \
has an execution-oracle leg"
}
};
format!(
"{what} #{ord} has type {arity:?} — a PARAMETER-taking block type \
(multi-value) is not lowered on {backend}: {why}; loud-declining \
(#1093, the aarch64 VCR-A64-CF-001 refusal ported)"
)
}
#[cfg(test)]
mod grow_zero_tests {
use super::*;
#[test]
fn folds_const_zero_grow_to_size() {
assert_eq!(
rewrite_memory_grow_zero(&[WasmOp::I32Const(0), WasmOp::MemoryGrow(0)]),
vec![WasmOp::MemorySize(0)]
);
}
#[test]
fn leaves_nonzero_grow_alone() {
assert_eq!(
rewrite_memory_grow_zero(&[WasmOp::I32Const(2), WasmOp::MemoryGrow(0)]),
vec![WasmOp::I32Const(2), WasmOp::MemoryGrow(0)]
);
}
#[test]
fn leaves_variable_grow_alone() {
assert_eq!(
rewrite_memory_grow_zero(&[WasmOp::LocalGet(0), WasmOp::MemoryGrow(0)]),
vec![WasmOp::LocalGet(0), WasmOp::MemoryGrow(0)]
);
}
#[test]
fn preserves_memory_index() {
assert_eq!(
rewrite_memory_grow_zero(&[WasmOp::I32Const(0), WasmOp::MemoryGrow(3)]),
vec![WasmOp::MemorySize(3)]
);
}
#[test]
fn param_block_empty_table_is_void() {
let ops = [WasmOp::Block, WasmOp::If, WasmOp::End, WasmOp::End];
assert_eq!(find_param_block_type(&ops, &[]), None);
}
#[test]
fn param_block_all_void_is_none() {
let ops = [WasmOp::Block, WasmOp::Loop, WasmOp::End, WasmOp::End];
assert_eq!(find_param_block_type(&ops, &[(0, 1), (0, 0)]), None);
}
#[test]
fn param_block_reports_construct_and_ordinal() {
let ops = [
WasmOp::I32Const(1),
WasmOp::I32Const(2),
WasmOp::LocalGet(0),
WasmOp::If,
WasmOp::I32Add,
WasmOp::Else,
WasmOp::I32Sub,
WasmOp::End,
];
assert_eq!(
find_param_block_type(&ops, &[(2, 1)]),
Some(("if", 0, (2, 1)))
);
}
#[test]
fn param_block_ordinal_keying_matches_decoder_order() {
let ops = [
WasmOp::Block, WasmOp::Loop, WasmOp::End,
WasmOp::End,
];
assert_eq!(
find_param_block_type(&ops, &[(0, 0), (1, 1)]),
Some(("loop", 1, (1, 1)))
);
}
#[test]
fn param_block_msg_carries_the_parity_needle() {
let msg = param_block_decline_msg("the ARM direct selector", "if", 0, (2, 1));
assert!(msg.contains("PARAMETER-taking block type"));
assert!(msg.contains("if #0 has type (2, 1)"));
assert!(msg.contains("#1093"));
}
#[test]
fn unlowered_predicate_skips_only_the_lowered_construct() {
use WasmOp::*;
let ops = vec![
I32Const(7),
Block,
LocalGet(0),
BrIf(0),
End,
I32Const(7),
LocalGet(0),
If,
I32Const(1),
I32Add,
End,
End,
];
let arity = vec![(1, 1), (1, 1)];
assert_eq!(
find_unlowered_param_block_type(&ops, &arity, ParamBlockLowering::DECLINED),
Some(("block", 0, (1, 1)))
);
assert_eq!(
find_param_block_type(&ops, &arity),
Some(("block", 0, (1, 1))),
"the legacy entry point is the all-declined reading"
);
let block_only = ParamBlockLowering {
block: true,
..ParamBlockLowering::DECLINED
};
assert_eq!(
find_unlowered_param_block_type(&ops, &arity, block_only),
Some(("if", 1, (1, 1)))
);
let all = ParamBlockLowering {
block: true,
r#if: true,
r#loop: true,
};
assert_eq!(find_unlowered_param_block_type(&ops, &arity, all), None);
assert_eq!(
find_unlowered_param_block_type(&ops, &[(0, 1), (0, 1)], ParamBlockLowering::DECLINED),
None
);
}
#[test]
fn arm_policy_is_keyed_on_the_oracle_covered_configuration() {
assert_eq!(
arm_param_block_lowering(true),
ParamBlockLowering {
block: true,
r#if: true,
r#loop: true
}
);
assert_eq!(
arm_param_block_lowering(false),
ParamBlockLowering::DECLINED
);
}
}