use std::convert::Infallible;
use std::sync::Arc;
use rayon::prelude::*;
use thiserror::Error;
use tracing::{debug, info};
use wasm_encoder::reencode::Reencode;
use wasmparser::{Operator, Parser, Payload};
use tensor_wasm_core::types::TenantId;
use crate::cache::{CacheKey, CachedKernel, CompiledHandle, KernelCache};
use crate::clif_lower::lower_block;
use crate::detector::{classify_ops, BlockIR, DetectorConfig, DetectorVerdict, Op};
use crate::ir::ElemType;
use crate::ptx_emit::emit;
pub const DEFAULT_HOST_MODULE: &str = "tensor-wasm:jit/host";
pub const DEFAULT_HOST_FN: &str = "dispatch";
pub const DEFAULT_HOST_ALLOC_FN: &str = "alloc";
pub const DEFAULT_HOST_FREE_FN: &str = "free";
pub const DEFAULT_SM_VERSION: u32 = 80;
#[derive(Debug, Clone)]
pub struct RewriteOptions {
pub host_module: String,
pub host_fn: String,
pub host_alloc_fn: String,
pub host_free_fn: String,
pub sm_version: u32,
pub tenant_id: TenantId,
pub detector: DetectorConfig,
}
impl Default for RewriteOptions {
fn default() -> Self {
Self {
host_module: DEFAULT_HOST_MODULE.into(),
host_fn: DEFAULT_HOST_FN.into(),
host_alloc_fn: DEFAULT_HOST_ALLOC_FN.into(),
host_free_fn: DEFAULT_HOST_FREE_FN.into(),
sm_version: DEFAULT_SM_VERSION,
tenant_id: TenantId(0),
detector: DetectorConfig::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OffloadedFunction {
pub function_index: u32,
pub fingerprint: u64,
pub original_op_count: usize,
}
#[derive(Debug, Clone)]
pub struct RewriteOutcome {
pub rewritten_wasm: Vec<u8>,
pub offloaded_functions: Vec<OffloadedFunction>,
pub total_defined_functions: u32,
}
#[derive(Debug, Error)]
pub enum RewriteError {
#[error("wasmparser: {0}")]
Parse(String),
#[error("lower: {0}")]
Lower(String),
#[error("reencode: {0}")]
Reencode(String),
#[error("trampoline: {0}")]
Trampoline(String),
}
impl<E: std::fmt::Display> From<wasm_encoder::reencode::Error<E>> for RewriteError {
fn from(e: wasm_encoder::reencode::Error<E>) -> Self {
RewriteError::Reencode(format!("{e}"))
}
}
#[derive(Debug, Clone)]
struct FuncInfo {
type_index: u32,
verdict: DetectorVerdict,
fingerprint: Option<u64>,
op_count: usize,
}
fn op_to_detector_op(op: &Operator<'_>) -> Op {
use wasmparser::Operator::*;
match op {
V128Load { .. } => Op::Load,
V128Store { .. } => Op::Store,
F32Add | I32Add | I64Add | F64Add => Op::ScalarAdd,
F32Mul | I32Mul | I64Mul | F64Mul => Op::ScalarMul,
I32Load { .. } | I64Load { .. } | F32Load { .. } | F64Load { .. } => Op::Load,
I32Store { .. } | I64Store { .. } | F32Store { .. } | F64Store { .. } => Op::Store,
Br { .. }
| BrIf { .. }
| BrTable { .. }
| If { .. }
| Else
| Loop { .. }
| Block { .. } => Op::Branch,
Call { .. } | CallIndirect { .. } | ReturnCall { .. } | ReturnCallIndirect { .. } => {
Op::Call
}
F32x4Add => Op::V128Add {
lane_ty: ElemType::F32,
lanes: 4,
},
I32x4Add => Op::V128Add {
lane_ty: ElemType::I32,
lanes: 4,
},
F32x4Mul => Op::V128Mul {
lane_ty: ElemType::F32,
lanes: 4,
},
I32x4Mul => Op::V128Mul {
lane_ty: ElemType::I32,
lanes: 4,
},
F64x2Add | I64x2Add | I16x8Add | I8x16Add | F64x2Mul | I64x2Mul | I16x8Mul => Op::Other,
_ => Op::Other,
}
}
#[derive(Debug, Clone)]
struct DecodedFuncType {
params: Vec<wasmparser::ValType>,
results: Vec<wasmparser::ValType>,
}
struct PreFuncInfo {
type_index: u32,
verdict: DetectorVerdict,
op_count: usize,
func_index_in_global_space: u32,
trip_guess: Option<u64>,
detector_ops: Vec<Op>,
has_memory: bool,
signature_ok: bool,
}
fn analyse(
wasm: &[u8],
opts: &RewriteOptions,
cache: &KernelCache,
) -> Result<AnalyseOutcome, RewriteError> {
let mut types: Vec<Option<DecodedFuncType>> = Vec::new();
let mut function_type_indices: Vec<u32> = Vec::new();
let mut num_function_imports: u32 = 0;
let mut pre_infos: Vec<PreFuncInfo> = Vec::new();
let mut defined_function_cursor: usize = 0;
let mut has_memory: bool = false;
for payload in Parser::new(0).parse_all(wasm) {
let payload = payload.map_err(|e| RewriteError::Parse(format!("{e}")))?;
match payload {
Payload::TypeSection(reader) => {
for rec_group in reader {
let rec = rec_group.map_err(|e| RewriteError::Parse(format!("{e}")))?;
for sub in rec.into_types() {
let decoded = match sub.composite_type.inner {
wasmparser::CompositeInnerType::Func(f) => Some(DecodedFuncType {
params: f.params().to_vec(),
results: f.results().to_vec(),
}),
_ => None,
};
types.push(decoded);
}
}
}
Payload::ImportSection(reader) => {
for import in reader {
let import = import.map_err(|e| RewriteError::Parse(format!("{e}")))?;
if matches!(import.ty, wasmparser::TypeRef::Func(_)) {
num_function_imports += 1;
}
if matches!(import.ty, wasmparser::TypeRef::Memory(_)) {
has_memory = true;
}
}
}
Payload::MemorySection(reader) if reader.count() > 0 => {
has_memory = true;
}
Payload::FunctionSection(reader) => {
for ty_idx in reader {
let ty_idx = ty_idx.map_err(|e| RewriteError::Parse(format!("{e}")))?;
function_type_indices.push(ty_idx);
}
}
Payload::CodeSectionEntry(body) => {
let type_index = function_type_indices
.get(defined_function_cursor)
.copied()
.ok_or_else(|| {
RewriteError::Parse(format!(
"code body {defined_function_cursor} has no matching function type"
))
})?;
let mut ops_reader = body
.get_operators_reader()
.map_err(|e| RewriteError::Parse(format!("{e}")))?;
let mut detector_ops = Vec::new();
let mut saw_loop = false;
while !ops_reader.eof() {
match ops_reader.read() {
Ok(op) => {
if matches!(op, wasmparser::Operator::Loop { .. }) {
saw_loop = true;
}
detector_ops.push(op_to_detector_op(&op));
}
Err(_) => break,
}
}
let func_index_in_global_space =
num_function_imports + defined_function_cursor as u32;
let trip_guess = if saw_loop { Some(128) } else { None };
let verdict = classify_ops(&detector_ops, trip_guess, &opts.detector);
let op_count = detector_ops.len();
let signature_ok = types
.get(type_index as usize)
.and_then(|t| t.as_ref())
.map(|t| {
t.params.iter().all(is_supported_primitive)
&& t.results.iter().all(is_supported_primitive)
})
.unwrap_or(false);
pre_infos.push(PreFuncInfo {
type_index,
verdict,
op_count,
func_index_in_global_space,
trip_guess,
detector_ops,
has_memory,
signature_ok,
});
defined_function_cursor += 1;
}
_ => {}
}
}
let n_slots = pre_infos.len();
let mut indexed: Vec<(usize, FuncInfo)> = pre_infos
.into_par_iter()
.enumerate()
.map(|(idx, pre)| {
let fingerprint = emit_for_slot(&pre, opts, cache);
(
idx,
FuncInfo {
type_index: pre.type_index,
verdict: pre.verdict,
fingerprint,
op_count: pre.op_count,
},
)
})
.collect();
indexed.sort_by_key(|(idx, _)| *idx);
debug_assert_eq!(
indexed.len(),
n_slots,
"rayon emit pass returned a different number of slots than it consumed"
);
let func_infos: Vec<FuncInfo> = indexed.into_iter().map(|(_, fi)| fi).collect();
Ok(AnalyseOutcome {
types,
num_function_imports,
func_infos,
})
}
fn emit_for_slot(pre: &PreFuncInfo, opts: &RewriteOptions, cache: &KernelCache) -> Option<u64> {
if !matches!(pre.verdict, DetectorVerdict::Offload) {
return None;
}
if !pre.has_memory {
debug!(
target: "tensor_wasm_jit::rewrite",
function = pre.func_index_in_global_space,
"offload candidate rejected: module has no memory for trampoline marshalling"
);
return None;
}
if !pre.signature_ok {
debug!(
target: "tensor_wasm_jit::rewrite",
function = pre.func_index_in_global_space,
"offload candidate rejected: unsupported parameter/result type"
);
return None;
}
let lower_block_input = BlockIR::new(
format!("func{}", pre.func_index_in_global_space),
pre.detector_ops
.iter()
.copied()
.filter(|o| {
matches!(
o,
Op::V128Add { .. }
| Op::V128Mul { .. }
| Op::V128Fma { .. }
| Op::Load
| Op::Store
)
})
.collect(),
pre.trip_guess,
);
match lower_block(&lower_block_input) {
Ok(blueprint) => match emit(&blueprint) {
Ok(ptx) => {
let fp = blueprint.fingerprint();
let key = CacheKey::for_tenant(opts.tenant_id, fp, opts.sm_version);
cache.put(
key,
CachedKernel::new(fp, Arc::new(ptx), CompiledHandle::default()),
);
info!(
target: "tensor_wasm_jit::rewrite",
function = pre.func_index_in_global_space,
op_count = pre.op_count,
fingerprint = fp,
"pre-populated kernel cache for offload candidate"
);
Some(fp)
}
Err(e) => {
debug!(
target: "tensor_wasm_jit::rewrite",
function = pre.func_index_in_global_space,
op_count = pre.op_count,
reason = %e,
"offload candidate rejected by PTX emitter"
);
None
}
},
Err(e) => {
debug!(
target: "tensor_wasm_jit::rewrite",
function = pre.func_index_in_global_space,
op_count = pre.op_count,
reason = %e,
"offload candidate rejected by lowering"
);
None
}
}
}
struct AnalyseOutcome {
types: Vec<Option<DecodedFuncType>>,
num_function_imports: u32,
func_infos: Vec<FuncInfo>,
}
fn is_supported_primitive(v: &wasmparser::ValType) -> bool {
matches!(
v,
wasmparser::ValType::I32
| wasmparser::ValType::I64
| wasmparser::ValType::F32
| wasmparser::ValType::F64
)
}
fn enc_val_type(v: wasmparser::ValType) -> Result<wasm_encoder::ValType, RewriteError> {
match v {
wasmparser::ValType::I32 => Ok(wasm_encoder::ValType::I32),
wasmparser::ValType::I64 => Ok(wasm_encoder::ValType::I64),
wasmparser::ValType::F32 => Ok(wasm_encoder::ValType::F32),
wasmparser::ValType::F64 => Ok(wasm_encoder::ValType::F64),
wasmparser::ValType::V128 => Err(RewriteError::Trampoline(
"v128 result not supported by dispatch trampoline".into(),
)),
wasmparser::ValType::Ref(_) => Err(RewriteError::Trampoline(
"reference result not supported by dispatch trampoline".into(),
)),
}
}
fn val_type_size(v: wasmparser::ValType) -> Result<u32, RewriteError> {
match v {
wasmparser::ValType::I32 | wasmparser::ValType::F32 => Ok(4),
wasmparser::ValType::I64 | wasmparser::ValType::F64 => Ok(8),
_ => Err(RewriteError::Trampoline(format!(
"val_type_size: unsupported type {v:?}"
))),
}
}
fn aligned_advance(off: u32, v: wasmparser::ValType) -> Result<u32, RewriteError> {
let align = val_type_size(v)?;
let mask = align - 1;
let summed = off.checked_add(mask).ok_or_else(|| {
RewriteError::Trampoline(format!(
"aligned_advance: offset overflow (off={off}, mask={mask})"
))
})?;
Ok(summed & !mask)
}
struct PackedLayout {
offsets: Vec<u32>,
total_bytes: u32,
}
fn pack_layout(types: &[wasmparser::ValType]) -> Result<PackedLayout, RewriteError> {
let mut offsets = Vec::with_capacity(types.len());
let mut off = 0u32;
for t in types {
let aligned = aligned_advance(off, *t)?;
offsets.push(aligned);
let size = val_type_size(*t)?;
off = aligned.checked_add(size).ok_or_else(|| {
RewriteError::Trampoline(format!(
"pack_layout: cursor overflow (aligned={aligned}, size={size})"
))
})?;
}
let total_bytes = off.checked_add(7).ok_or_else(|| {
RewriteError::Trampoline(format!(
"pack_layout: total-bytes round-up overflow (off={off})"
))
})? & !7u32;
Ok(PackedLayout {
offsets,
total_bytes,
})
}
struct DispatchImports {
dispatch: u32,
alloc: u32,
free: u32,
}
fn build_trampoline(
fingerprint: u64,
imports: &DispatchImports,
params: &[wasmparser::ValType],
results: &[wasmparser::ValType],
) -> Result<wasm_encoder::Function, RewriteError> {
for t in params.iter().chain(results.iter()) {
if !is_supported_primitive(t) {
return Err(RewriteError::Trampoline(format!(
"unsupported type in signature: {t:?}",
)));
}
}
let args_layout = pack_layout(params)?;
let results_layout = pack_layout(results)?;
let scratch_size = args_layout
.total_bytes
.checked_add(results_layout.total_bytes)
.ok_or_else(|| RewriteError::Trampoline("scratch size overflow".into()))?;
if scratch_size > i32::MAX as u32 {
return Err(RewriteError::Trampoline(format!(
"scratch size {scratch_size} exceeds i32::MAX; trampoline cannot encode it"
)));
}
let scratch_local_idx: u32 = params.len() as u32;
let rc_local_idx: u32 = scratch_local_idx + 1;
let mut func = wasm_encoder::Function::new(std::iter::once((2u32, wasm_encoder::ValType::I32)));
use wasm_encoder::Instruction as I;
func.instruction(&I::I32Const(scratch_size as i32));
func.instruction(&I::Call(imports.alloc));
func.instruction(&I::LocalSet(scratch_local_idx));
for (i, ty) in params.iter().enumerate() {
let off = args_layout.offsets[i];
func.instruction(&I::LocalGet(scratch_local_idx));
func.instruction(&I::LocalGet(i as u32));
let memarg = wasm_encoder::MemArg {
offset: off as u64,
align: log2_align(*ty),
memory_index: 0,
};
match ty {
wasmparser::ValType::I32 => {
func.instruction(&I::I32Store(memarg));
}
wasmparser::ValType::I64 => {
func.instruction(&I::I64Store(memarg));
}
wasmparser::ValType::F32 => {
func.instruction(&I::F32Store(memarg));
}
wasmparser::ValType::F64 => {
func.instruction(&I::F64Store(memarg));
}
_ => unreachable!("validated above"),
}
}
let fp_lo = (fingerprint & 0xFFFF_FFFF) as i64;
let fp_hi = (fingerprint >> 32) as i64;
func.instruction(&I::I64Const(fp_lo));
func.instruction(&I::I64Const(fp_hi));
func.instruction(&I::LocalGet(scratch_local_idx));
func.instruction(&I::I32Const(args_layout.total_bytes as i32));
func.instruction(&I::I32Const(results_layout.total_bytes as i32));
func.instruction(&I::Call(imports.dispatch));
func.instruction(&I::LocalTee(rc_local_idx));
func.instruction(&I::I32Const(0));
func.instruction(&I::I32Ne);
func.instruction(&I::If(wasm_encoder::BlockType::Empty));
func.instruction(&I::LocalGet(scratch_local_idx));
func.instruction(&I::I32Const(scratch_size as i32));
func.instruction(&I::Call(imports.free));
func.instruction(&I::Unreachable);
func.instruction(&I::End);
for (i, ty) in results.iter().enumerate() {
let off = args_layout.total_bytes + results_layout.offsets[i];
func.instruction(&I::LocalGet(scratch_local_idx));
let memarg = wasm_encoder::MemArg {
offset: off as u64,
align: log2_align(*ty),
memory_index: 0,
};
match ty {
wasmparser::ValType::I32 => {
func.instruction(&I::I32Load(memarg));
}
wasmparser::ValType::I64 => {
func.instruction(&I::I64Load(memarg));
}
wasmparser::ValType::F32 => {
func.instruction(&I::F32Load(memarg));
}
wasmparser::ValType::F64 => {
func.instruction(&I::F64Load(memarg));
}
_ => unreachable!("validated above"),
}
}
func.instruction(&I::LocalGet(scratch_local_idx));
func.instruction(&I::I32Const(scratch_size as i32));
func.instruction(&I::Call(imports.free));
func.instruction(&I::End);
Ok(func)
}
fn log2_align(v: wasmparser::ValType) -> u32 {
match v {
wasmparser::ValType::I32 | wasmparser::ValType::F32 => 2,
wasmparser::ValType::I64 | wasmparser::ValType::F64 => 3,
_ => 0,
}
}
struct TensorWasmRewriter<'a> {
opts: &'a RewriteOptions,
num_function_imports: u32,
dispatch_type_index: u32,
alloc_type_index: u32,
free_type_index: u32,
imports: DispatchImports,
imports_added: u32,
func_infos: &'a [FuncInfo],
types: &'a [Option<DecodedFuncType>],
code_cursor: usize,
swapped: Vec<OffloadedFunction>,
type_section_appended: bool,
import_section_appended: bool,
}
impl<'a> TensorWasmRewriter<'a> {
fn shifted_fn_index(&self, orig: u32) -> u32 {
if orig < self.num_function_imports {
orig
} else {
orig + self.imports_added
}
}
fn dispatch_func_type(&self) -> (Vec<wasm_encoder::ValType>, Vec<wasm_encoder::ValType>) {
(
vec![
wasm_encoder::ValType::I64, wasm_encoder::ValType::I64, wasm_encoder::ValType::I32, wasm_encoder::ValType::I32, wasm_encoder::ValType::I32, ],
vec![wasm_encoder::ValType::I32],
)
}
fn alloc_func_type(&self) -> (Vec<wasm_encoder::ValType>, Vec<wasm_encoder::ValType>) {
(
vec![wasm_encoder::ValType::I32],
vec![wasm_encoder::ValType::I32],
)
}
fn free_func_type(&self) -> (Vec<wasm_encoder::ValType>, Vec<wasm_encoder::ValType>) {
(
vec![wasm_encoder::ValType::I32, wasm_encoder::ValType::I32],
vec![],
)
}
fn write_import_types(&self, types: &mut wasm_encoder::TypeSection) {
let (p, r) = self.dispatch_func_type();
types.ty().function(p, r);
let (p, r) = self.alloc_func_type();
types.ty().function(p, r);
let (p, r) = self.free_func_type();
types.ty().function(p, r);
}
fn write_imports(&self, imports: &mut wasm_encoder::ImportSection) {
imports.import(
&self.opts.host_module,
&self.opts.host_fn,
wasm_encoder::EntityType::Function(self.dispatch_type_index),
);
imports.import(
&self.opts.host_module,
&self.opts.host_alloc_fn,
wasm_encoder::EntityType::Function(self.alloc_type_index),
);
imports.import(
&self.opts.host_module,
&self.opts.host_free_fn,
wasm_encoder::EntityType::Function(self.free_type_index),
);
}
}
impl<'a> Reencode for TensorWasmRewriter<'a> {
type Error = Infallible;
fn function_index(&mut self, func: u32) -> u32 {
self.shifted_fn_index(func)
}
fn parse_type_section(
&mut self,
types: &mut wasm_encoder::TypeSection,
section: wasmparser::TypeSectionReader<'_>,
) -> Result<(), wasm_encoder::reencode::Error<Self::Error>> {
wasm_encoder::reencode::utils::parse_type_section(self, types, section)?;
self.write_import_types(types);
self.type_section_appended = true;
Ok(())
}
fn parse_import_section(
&mut self,
imports: &mut wasm_encoder::ImportSection,
section: wasmparser::ImportSectionReader<'_>,
) -> Result<(), wasm_encoder::reencode::Error<Self::Error>> {
wasm_encoder::reencode::utils::parse_import_section(self, imports, section)?;
self.write_imports(imports);
self.import_section_appended = true;
Ok(())
}
fn parse_function_body(
&mut self,
code: &mut wasm_encoder::CodeSection,
func: wasmparser::FunctionBody<'_>,
) -> Result<(), wasm_encoder::reencode::Error<Self::Error>> {
let cursor = self.code_cursor;
self.code_cursor += 1;
let info = match self.func_infos.get(cursor) {
Some(i) => i,
None => {
return wasm_encoder::reencode::utils::parse_function_body(self, code, func);
}
};
let should_swap =
matches!(info.verdict, DetectorVerdict::Offload) && info.fingerprint.is_some();
if !should_swap {
return wasm_encoder::reencode::utils::parse_function_body(self, code, func);
}
let func_ty = self
.types
.get(info.type_index as usize)
.and_then(|t| t.as_ref());
let func_ty = match func_ty {
Some(t) => t,
None => {
return wasm_encoder::reencode::utils::parse_function_body(self, code, func);
}
};
let trampoline = match build_trampoline(
info.fingerprint.expect("fingerprint set"),
&self.imports,
&func_ty.params,
&func_ty.results,
) {
Ok(t) => t,
Err(_) => {
return wasm_encoder::reencode::utils::parse_function_body(self, code, func);
}
};
code.function(&trampoline);
for p in &func_ty.params {
let _ = enc_val_type(*p);
}
self.swapped.push(OffloadedFunction {
function_index: self.num_function_imports + cursor as u32,
fingerprint: info.fingerprint.expect("fingerprint set"),
original_op_count: info.op_count,
});
Ok(())
}
fn intersperse_section_hook(
&mut self,
module: &mut wasm_encoder::Module,
_after: Option<wasm_encoder::SectionId>,
before: Option<wasm_encoder::SectionId>,
) -> Result<(), wasm_encoder::reencode::Error<Self::Error>> {
if matches!(
before,
Some(wasm_encoder::SectionId::Custom)
| Some(wasm_encoder::SectionId::Type)
| Some(wasm_encoder::SectionId::Import)
) {
return Ok(());
}
if !self.type_section_appended {
let mut types = wasm_encoder::TypeSection::new();
self.write_import_types(&mut types);
module.section(&types);
self.type_section_appended = true;
}
if !self.import_section_appended {
let mut imports = wasm_encoder::ImportSection::new();
self.write_imports(&mut imports);
module.section(&imports);
self.import_section_appended = true;
}
Ok(())
}
}
pub fn rewrite_wasm(
wasm: &[u8],
opts: &RewriteOptions,
cache: &KernelCache,
) -> Result<RewriteOutcome, RewriteError> {
let analysis = analyse(wasm, opts, cache)?;
let dispatch_type_index = analysis.types.len() as u32;
let alloc_type_index = dispatch_type_index + 1;
let free_type_index = dispatch_type_index + 2;
let imports = DispatchImports {
dispatch: analysis.num_function_imports,
alloc: analysis.num_function_imports + 1,
free: analysis.num_function_imports + 2,
};
let total_defined_functions = analysis.func_infos.len() as u32;
let mut rewriter = TensorWasmRewriter {
opts,
num_function_imports: analysis.num_function_imports,
dispatch_type_index,
alloc_type_index,
free_type_index,
imports,
imports_added: 3,
func_infos: &analysis.func_infos,
types: &analysis.types,
code_cursor: 0,
swapped: Vec::new(),
type_section_appended: false,
import_section_appended: false,
};
let mut module = wasm_encoder::Module::new();
let parser = wasmparser::Parser::new(0);
Reencode::parse_core_module(&mut rewriter, &mut module, parser, wasm)?;
debug_assert!(
rewriter.type_section_appended,
"rewriter failed to append dispatch type"
);
debug_assert!(
rewriter.import_section_appended,
"rewriter failed to append dispatch import"
);
let swapped = rewriter.swapped;
Ok(RewriteOutcome {
rewritten_wasm: module.finish(),
offloaded_functions: swapped,
total_defined_functions,
})
}
#[cfg(test)]
mod tests {
use super::*;
const V128_HEAVY_WAT: &str = r#"
(module
(memory 1)
(func (export "hot") (result i32)
(local $v v128)
(loop $L
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
)
(i32.const 0)
)
)
"#;
const NOOP_WAT: &str = r#"(module (func (export "noop")))"#;
#[test]
fn op_to_detector_op_threads_element_type_and_fails_closed() {
use wasmparser::Operator;
assert_eq!(
op_to_detector_op(&Operator::F32x4Add),
Op::V128Add {
lane_ty: ElemType::F32,
lanes: 4
}
);
assert_eq!(
op_to_detector_op(&Operator::I32x4Add),
Op::V128Add {
lane_ty: ElemType::I32,
lanes: 4
}
);
assert_eq!(
op_to_detector_op(&Operator::I32x4Mul),
Op::V128Mul {
lane_ty: ElemType::I32,
lanes: 4
}
);
assert_ne!(
op_to_detector_op(&Operator::I32x4Add),
Op::V128Add {
lane_ty: ElemType::F32,
lanes: 4
}
);
for op in [
Operator::F64x2Add,
Operator::I64x2Add,
Operator::I16x8Add,
Operator::I8x16Add,
Operator::F64x2Mul,
Operator::I16x8Mul,
] {
assert_eq!(
op_to_detector_op(&op),
Op::Other,
"non-emittable SIMD width must fail closed to Op::Other, got a SIMD op"
);
}
}
#[test]
fn op_to_detector_op_return_call_indirect_is_a_call() {
use wasmparser::Operator;
assert_eq!(
op_to_detector_op(&Operator::ReturnCallIndirect {
type_index: 0,
table_index: 0,
}),
Op::Call
);
assert_eq!(
op_to_detector_op(&Operator::ReturnCall { function_index: 0 }),
Op::Call
);
}
fn noop_module_swaps_nothing_inner() {
let wasm = wat::parse_str(NOOP_WAT).unwrap();
let cache = KernelCache::new();
let out = rewrite_wasm(&wasm, &RewriteOptions::default(), &cache).expect("rewrite");
assert!(out.offloaded_functions.is_empty());
assert!(cache.is_empty());
let mut saw_import = false;
for p in wasmparser::Parser::new(0).parse_all(&out.rewritten_wasm) {
if let wasmparser::Payload::ImportSection(reader) = p.expect("rewritten payload parses")
{
for imp in reader {
let imp = imp.expect("import parses");
if imp.module == DEFAULT_HOST_MODULE && imp.name == DEFAULT_HOST_FN {
saw_import = true;
}
}
}
}
assert!(
saw_import,
"rewritten module is missing the dispatch import"
);
}
#[test]
fn noop_module_swaps_nothing_and_still_adds_dispatch_import() {
noop_module_swaps_nothing_inner();
}
#[test]
fn v128_heavy_module_swaps_function_and_inserts_call() {
let wasm = wat::parse_str(V128_HEAVY_WAT).unwrap();
let cache = KernelCache::new();
let opts = RewriteOptions {
detector: DetectorConfig {
v128_ratio_threshold: 0.05,
min_trip_count: 64,
},
..RewriteOptions::default()
};
let out = rewrite_wasm(&wasm, &opts, &cache).expect("rewrite");
assert_eq!(
out.offloaded_functions.len(),
1,
"the v128-heavy module should produce exactly one swap"
);
let swapped = &out.offloaded_functions[0];
assert_eq!(swapped.function_index, 0);
let key = CacheKey::for_tenant(TenantId(0), swapped.fingerprint, DEFAULT_SM_VERSION);
assert!(cache.get(&key).is_some(), "kernel was pre-populated");
wasmparser::Validator::new()
.validate_all(&out.rewritten_wasm)
.expect("rewritten wasm must validate");
}
#[test]
fn rewrite_options_default_values_match_constants() {
let opts = RewriteOptions::default();
assert_eq!(opts.host_module, DEFAULT_HOST_MODULE);
assert_eq!(opts.host_fn, DEFAULT_HOST_FN);
assert_eq!(opts.host_alloc_fn, DEFAULT_HOST_ALLOC_FN);
assert_eq!(opts.host_free_fn, DEFAULT_HOST_FREE_FN);
assert_eq!(opts.sm_version, DEFAULT_SM_VERSION);
assert_eq!(opts.tenant_id, TenantId(0));
}
#[test]
fn rewrite_prepop_uses_configured_tenant_id() {
let wasm = wat::parse_str(V128_HEAVY_WAT).unwrap();
let cache = KernelCache::new();
let tenant = TenantId(4242);
let opts = RewriteOptions {
tenant_id: tenant,
detector: DetectorConfig {
v128_ratio_threshold: 0.05,
min_trip_count: 64,
},
..RewriteOptions::default()
};
let out = rewrite_wasm(&wasm, &opts, &cache).expect("rewrite");
assert_eq!(
out.offloaded_functions.len(),
1,
"the v128-heavy module should produce exactly one swap"
);
let fp = out.offloaded_functions[0].fingerprint;
let configured_key = CacheKey::for_tenant(tenant, fp, DEFAULT_SM_VERSION);
assert!(
cache.get(&configured_key).is_some(),
"pre-populated kernel must be keyed under the configured tenant id"
);
let placeholder_key = CacheKey::for_tenant(TenantId(0), fp, DEFAULT_SM_VERSION);
assert!(
cache.get(&placeholder_key).is_none(),
"with a non-zero tenant configured, the placeholder TenantId(0) \
key must be a miss — keys are tenant-scoped"
);
}
#[test]
fn rewrite_outcome_reports_total_defined_functions() {
let wat = r#"
(module
(func (export "a"))
(func (export "b"))
)
"#;
let wasm = wat::parse_str(wat).unwrap();
let cache = KernelCache::new();
let out = rewrite_wasm(&wasm, &RewriteOptions::default(), &cache).expect("rewrite");
assert_eq!(out.total_defined_functions, 2);
}
#[test]
fn invalid_wasm_returns_parse_error() {
let bytes = [0x00, 0x61, 0x73, 0x6d, 0xff, 0xff, 0xff, 0xff];
let cache = KernelCache::new();
let err = rewrite_wasm(&bytes, &RewriteOptions::default(), &cache).unwrap_err();
assert!(matches!(
err,
RewriteError::Parse(_) | RewriteError::Reencode(_)
));
}
#[test]
fn build_trampoline_round_trip_validates() {
let imports = DispatchImports {
dispatch: 0,
alloc: 1,
free: 2,
};
let small = build_trampoline(0xCAFEBABE, &imports, &[], &[]).unwrap();
let mid = build_trampoline(
0xCAFEBABE,
&imports,
&[wasmparser::ValType::I32, wasmparser::ValType::I32],
&[wasmparser::ValType::I32],
)
.unwrap();
let big = build_trampoline(
0xCAFEBABE,
&imports,
&[
wasmparser::ValType::I32,
wasmparser::ValType::I64,
wasmparser::ValType::F32,
wasmparser::ValType::F64,
],
&[wasmparser::ValType::I32, wasmparser::ValType::F64],
)
.unwrap();
assert!(small.byte_len() < mid.byte_len());
assert!(mid.byte_len() < big.byte_len());
}
#[test]
fn build_trampoline_refuses_v128() {
let imports = DispatchImports {
dispatch: 0,
alloc: 1,
free: 2,
};
let err = build_trampoline(0, &imports, &[wasmparser::ValType::V128], &[])
.expect_err("must refuse v128 param");
assert!(matches!(err, RewriteError::Trampoline(_)));
}
#[test]
fn build_trampoline_within_i32_max_builds() {
let imports = DispatchImports {
dispatch: 0,
alloc: 1,
free: 2,
};
let params = vec![wasmparser::ValType::I64; 32];
let results = vec![wasmparser::ValType::F64; 8];
let f = build_trampoline(0xABCD, &imports, ¶ms, &results)
.expect("within-i32::MAX trampoline must build");
assert!(f.byte_len() > 0);
}
#[test]
fn rewritten_module_passes_wasm_validator() {
let modules = [
r#"(module (func (export "noop")))"#,
r#"(module
(memory 1)
(func (export "add") (param i32 i32) (result i32)
(i32.add (local.get 0) (local.get 1))))"#,
r#"(module
(memory 1)
(func (export "f") (param f32) (result f32)
(f32.add (local.get 0) (local.get 0))))"#,
];
for (i, wat_src) in modules.iter().enumerate() {
let wasm = wat::parse_str(wat_src).unwrap_or_else(|e| panic!("wat #{i}: {e}"));
let cache = KernelCache::new();
let out = rewrite_wasm(&wasm, &RewriteOptions::default(), &cache)
.unwrap_or_else(|e| panic!("rewrite #{i}: {e}"));
wasmparser::Validator::new()
.validate_all(&out.rewritten_wasm)
.unwrap_or_else(|e| panic!("validation #{i}: {e}"));
}
}
#[test]
fn pack_layout_natural_alignment() {
let layout = pack_layout(&[
wasmparser::ValType::I32,
wasmparser::ValType::I64,
wasmparser::ValType::F32,
wasmparser::ValType::F64,
])
.expect("supported types pack cleanly");
assert_eq!(layout.offsets, vec![0, 8, 16, 24]);
assert_eq!(layout.total_bytes, 32);
}
#[test]
fn pack_layout_empty_is_zero() {
let layout = pack_layout(&[]).expect("empty layout is trivially Ok");
assert!(layout.offsets.is_empty());
assert_eq!(layout.total_bytes, 0);
}
#[test]
fn aligned_advance_overflows_cleanly() {
let err = aligned_advance(u32::MAX - 3, wasmparser::ValType::I64)
.expect_err("near-MAX offset must trip overflow guard");
assert!(
matches!(err, RewriteError::Trampoline(ref msg) if msg.contains("aligned_advance")),
"expected Trampoline overflow error, got {err:?}"
);
}
#[test]
fn pack_layout_total_bytes_overflow() {
let err = aligned_advance(u32::MAX, wasmparser::ValType::I32)
.expect_err("u32::MAX cannot round up to a 4-byte boundary");
assert!(
matches!(err, RewriteError::Trampoline(_)),
"expected Trampoline overflow error, got {err:?}"
);
}
#[test]
fn val_type_size_is_fallible() {
let err = val_type_size(wasmparser::ValType::V128)
.expect_err("v128 is not a supported primitive");
assert!(
matches!(err, RewriteError::Trampoline(ref msg) if msg.contains("val_type_size")),
"expected Trampoline error, got {err:?}"
);
}
#[test]
fn trampoline_traps_on_nonzero_dispatch() {
let imports = DispatchImports {
dispatch: 0,
alloc: 1,
free: 2,
};
let func = build_trampoline(
0xDEAD_BEEF,
&imports,
&[wasmparser::ValType::I32],
&[wasmparser::ValType::I32],
)
.expect("trampoline builds");
let mut code = wasm_encoder::CodeSection::new();
code.function(&func);
let mut out = Vec::new();
wasm_encoder::Encode::encode(&code, &mut out);
const PREFIX: &[u8] = &[0x22, 0x02, 0x41, 0x00, 0x47, 0x04, 0x40, 0x20, 0x01];
const SUFFIX: &[u8] = &[0x10, 0x02, 0x00, 0x0B];
let prefix_at = out
.windows(PREFIX.len())
.position(|w| w == PREFIX)
.unwrap_or_else(|| {
panic!(
"trampoline missing the trap-branch prefix \
(local.tee rc; i32.const 0; i32.ne; if; local.get scratch); \
body bytes: {out:02x?}"
)
});
let found_free_before_trap = out[prefix_at..].windows(SUFFIX.len()).any(|w| w == SUFFIX);
assert!(
found_free_before_trap,
"trampoline missing `call $free; unreachable; end` in the trap \
branch — scratch leaks on the dispatch-failure trap path; \
body bytes: {out:02x?}"
);
}
}