diff --git a/starstream-to-wasm/src/intrinsics.rs b/starstream-to-wasm/src/intrinsics.rs index 4e3810b1..c9c7fc16 100644 --- a/starstream-to-wasm/src/intrinsics.rs +++ b/starstream-to-wasm/src/intrinsics.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use starstream_types::IntWidth; use wasm_encoder::{BlockType, FuncType, ValType}; @@ -6,196 +8,43 @@ use crate::{Compiler, StFunction}; /// Internal functions generated by the compiler. #[derive(Default)] pub struct Intrinsics { - i64_add_checked: Option, - i64_sub_checked: Option, - i64_mul_checked: Option, + /// Overflow-checked arithmetic, by operation and width. + checked: HashMap<(CheckedOp, IntWidth), u32>, } -impl Compiler { - /// Get or create the `__starstream_i64_add_checked` function for overflow-checked addition. - /// Returns the function index. - pub fn get_or_create_i64_add_checked(&mut self) -> u32 { - if let Some(checked_sum) = self.intrinsics.i64_add_checked { - return checked_sum; - } - - let params = [ValType::I64, ValType::I64]; - let result = [ValType::I64]; - let mut code = wasm_encoder::Function::new_with_locals_types([ - // 0-1 are params, 2+ are here - ValType::I64, - ]); - let sum_local = 2; - - code.instructions().local_get(0); // [x] - code.instructions().local_get(1); // [x, y] - code.instructions().i64_add(); - code.instructions().local_tee(sum_local); // [sum] - - // Check for overflow. - - // (x^y)>=0 - code.instructions().local_get(0); // [sum, x] - code.instructions().local_get(1); // [sum, x, y] - code.instructions().i64_xor(); // [sum, x^y] - code.instructions().i64_const(0); // [sum, x^y, 0] - code.instructions().i64_ge_s(); // [sum, (x^y)>=0] - - // (sum^x)<0 - code.instructions().local_get(sum_local); // [sum, (x^y)>=0, sum] - code.instructions().local_get(0); // [sum, (x^y)>=0, sum, x] - code.instructions().i64_xor(); // [sum, (x^y)>=0, sum^x] - code.instructions().i64_const(0); // [sum, (x^y)>=0, sum^x, 0] - code.instructions().i64_lt_s(); // [sum, (x^y)>=0, (sum^x)<0] - code.instructions().i32_and(); // [sum, overflow_flag] - - // If overflow_flag != 0, trap. - code.instructions().i32_const(0); // [sum, overflow_flag, 0] - code.instructions().i32_ne(); // [sum, overflowed?] - code.instructions().if_(BlockType::Empty); // [sum] - code.instructions().unreachable(); - code.instructions().end(); - - // Sum is already on stack, add function body end - code.instructions().end(); - - let idx = self.add_function(&FuncType::new(params, result), code.into_raw_body()); - self.intrinsics.i64_add_checked = Some(idx); - idx - } - - /// Get or create the `__starstream_i64_sub_checked` function for overflow-checked subtraction. - /// Returns the function index. - pub fn get_or_create_i64_sub_checked(&mut self) -> u32 { - if let Some(checked_sub) = self.intrinsics.i64_sub_checked { - return checked_sub; - } - - let params = [ValType::I64, ValType::I64]; - let result = [ValType::I64]; - let mut code = wasm_encoder::Function::new_with_locals_types([ - // 0-1 are params, 2+ are here - ValType::I64, - ]); - let diff_local = 2; - - code.instructions().local_get(0); // [x] - code.instructions().local_get(1); // [x, y] - code.instructions().i64_sub(); - code.instructions().local_tee(diff_local); // [diff] - - // Check for overflow. - // Overflow occurs when subtracting a negative from a positive yields negative, - // or subtracting a positive from a negative yields positive. - // In other words: (x^y)<0 && (diff^x)<0 - - // (x^y)<0 - code.instructions().local_get(0); // [diff, x] - code.instructions().local_get(1); // [diff, x, y] - code.instructions().i64_xor(); // [diff, x^y] - code.instructions().i64_const(0); // [diff, x^y, 0] - code.instructions().i64_lt_s(); // [diff, (x^y)<0] - - // (diff^x)<0 - code.instructions().local_get(diff_local); // [diff, (x^y)<0, diff] - code.instructions().local_get(0); // [diff, (x^y)<0, diff, x] - code.instructions().i64_xor(); // [diff, (x^y)<0, diff^x] - code.instructions().i64_const(0); // [diff, (x^y)<0, diff^x, 0] - code.instructions().i64_lt_s(); // [diff, (x^y)<0, (diff^x)<0] - code.instructions().i32_and(); // [diff, overflow_flag] - - // If overflow_flag != 0, trap. - code.instructions().i32_const(0); // [diff, overflow_flag, 0] - code.instructions().i32_ne(); // [diff, overflowed?] - code.instructions().if_(BlockType::Empty); // [diff] - code.instructions().unreachable(); - code.instructions().end(); - - // Diff is already on stack, add function body end - code.instructions().end(); - - let idx = self.add_function(&FuncType::new(params, result), code.into_raw_body()); - self.intrinsics.i64_sub_checked = Some(idx); - idx - } +/// An integer operation that traps when its result doesn't fit its type. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CheckedOp { + Add, + Sub, + Mul, + /// Signed 8- and 16-bit division only: `MIN / -1` fits in the `i32` + /// they're computed in. Wider types trap natively. + Div, +} - /// Get or create the `__starstream_i64_mul_checked` function for overflow-checked multiplication. +impl Compiler { + /// Get or create the function that computes `op` on two `width` integers, + /// trapping when the result doesn't fit in `width`. /// Returns the function index. - pub fn get_or_create_i64_mul_checked(&mut self) -> u32 { - if let Some(checked_mul) = self.intrinsics.i64_mul_checked { - return checked_mul; + pub fn get_or_create_checked(&mut self, op: CheckedOp, width: IntWidth) -> u32 { + if let Some(&idx) = self.intrinsics.checked.get(&(op, width)) { + return idx; } - let params = [ValType::I64, ValType::I64]; - let result = [ValType::I64]; - let mut code = wasm_encoder::Function::new_with_locals_types([ - // 0-1 are params, 2+ are here - ValType::I64, - ValType::I64, - ValType::I64, - ]); - let product_local = 2; - let x_local = 3; - let y_local = 4; - - // Store parameters in locals for reuse - code.instructions().local_get(0); // [x] - code.instructions().local_tee(x_local); // [x] - code.instructions().local_get(1); // [x, y] - code.instructions().local_tee(y_local); // [x, y] - code.instructions().i64_mul(); - code.instructions().local_tee(product_local); // [product] - - // Check for overflow. - // For multiplication overflow detection, we check: - // 1. If y == 0, no overflow - // 2. If product / y != x, then overflow occurred - // Special case: MIN * -1 overflows (since -MIN doesn't fit in i64) - - // Special case: check if x == MIN && y == -1 - code.instructions().local_get(x_local); // [product, x] - code.instructions().i64_const(i64::MIN); // [product, x, MIN] - code.instructions().i64_eq(); // [product, x==MIN] - code.instructions().local_get(y_local); // [product, x==MIN, y] - code.instructions().i64_const(-1); // [product, x==MIN, y, -1] - code.instructions().i64_eq(); // [product, x==MIN, y==-1] - code.instructions().i32_and(); // [product, (x==MIN && y==-1)] - - // Also check y == MIN && x == -1 - code.instructions().local_get(y_local); // [product, (x==MIN && y==-1), y] - code.instructions().i64_const(i64::MIN); // [product, (x==MIN && y==-1), y, MIN] - code.instructions().i64_eq(); // [product, (x==MIN && y==-1), y==MIN] - code.instructions().local_get(x_local); // [product, (x==MIN && y==-1), y==MIN, x] - code.instructions().i64_const(-1); // [product, (x==MIN && y==-1), y==MIN, x, -1] - code.instructions().i64_eq(); // [product, (x==MIN && y==-1), y==MIN, x==-1] - code.instructions().i32_and(); // [product, (x==MIN && y==-1), (y==MIN && x==-1)] - code.instructions().i32_or(); // [product, overflow_special] - - code.instructions().if_(BlockType::Empty); // [product] - code.instructions().unreachable(); - code.instructions().end(); - - // Check regular overflow: if y != 0 and product / y != x - code.instructions().local_get(y_local); // [product, y] - code.instructions().i64_const(0); // [product, y, 0] - code.instructions().i64_ne(); // [product, y!=0] - code.instructions().if_(BlockType::Empty); // [product] - // y != 0, so check if product / y == x - code.instructions().local_get(product_local); // [product, product] - code.instructions().local_get(y_local); // [product, product, y] - code.instructions().i64_div_s(); // [product, product/y] - code.instructions().local_get(x_local); // [product, product/y, x] - code.instructions().i64_ne(); // [product, (product/y)!=x] - code.instructions().if_(BlockType::Empty); // [product] - code.instructions().unreachable(); - code.instructions().end(); - code.instructions().end(); - - // Product is already on stack, add function body end - code.instructions().end(); - - let idx = self.add_function(&FuncType::new(params, result), code.into_raw_body()); - self.intrinsics.i64_mul_checked = Some(idx); + let code = match width { + IntWidth::I64 => checked_i64(op), + IntWidth::U64 => checked_u64(op), + IntWidth::I32 | IntWidth::U32 => checked_32(op, width.is_signed()), + IntWidth::I8 | IntWidth::I16 | IntWidth::U8 | IntWidth::U16 => checked_sub32(op, width), + }; + let ty = if width.is_64bit() { + ValType::I64 + } else { + ValType::I32 + }; + let idx = self.add_function(&FuncType::new([ty, ty], [ty]), code.into_raw_body()); + self.intrinsics.checked.insert((op, width), idx); idx } @@ -226,3 +75,286 @@ impl Compiler { } } } + +/// Signed 64-bit arithmetic that traps on overflow. +fn checked_i64(op: CheckedOp) -> wasm_encoder::Function { + match op { + CheckedOp::Add => i64_add_checked(), + CheckedOp::Sub => i64_sub_checked(), + CheckedOp::Mul => i64_mul_checked(), + CheckedOp::Div => unreachable!("64-bit division traps natively on overflow"), + } +} + +/// Body of `__starstream_i64_add_checked`, for overflow-checked addition. +fn i64_add_checked() -> wasm_encoder::Function { + let mut code = wasm_encoder::Function::new_with_locals_types([ + // 0-1 are params, 2+ are here + ValType::I64, + ]); + let sum_local = 2; + + code.instructions().local_get(0); // [x] + code.instructions().local_get(1); // [x, y] + code.instructions().i64_add(); + code.instructions().local_tee(sum_local); // [sum] + + // Check for overflow. + + // (x^y)>=0 + code.instructions().local_get(0); // [sum, x] + code.instructions().local_get(1); // [sum, x, y] + code.instructions().i64_xor(); // [sum, x^y] + code.instructions().i64_const(0); // [sum, x^y, 0] + code.instructions().i64_ge_s(); // [sum, (x^y)>=0] + + // (sum^x)<0 + code.instructions().local_get(sum_local); // [sum, (x^y)>=0, sum] + code.instructions().local_get(0); // [sum, (x^y)>=0, sum, x] + code.instructions().i64_xor(); // [sum, (x^y)>=0, sum^x] + code.instructions().i64_const(0); // [sum, (x^y)>=0, sum^x, 0] + code.instructions().i64_lt_s(); // [sum, (x^y)>=0, (sum^x)<0] + code.instructions().i32_and(); // [sum, overflow_flag] + + // If overflow_flag != 0, trap. + code.instructions().i32_const(0); // [sum, overflow_flag, 0] + code.instructions().i32_ne(); // [sum, overflowed?] + code.instructions().if_(BlockType::Empty); // [sum] + code.instructions().unreachable(); + code.instructions().end(); + + // Sum is already on stack, add function body end + code.instructions().end(); + + code +} + +/// Body of `__starstream_i64_sub_checked`, for overflow-checked subtraction. +fn i64_sub_checked() -> wasm_encoder::Function { + let mut code = wasm_encoder::Function::new_with_locals_types([ + // 0-1 are params, 2+ are here + ValType::I64, + ]); + let diff_local = 2; + + code.instructions().local_get(0); // [x] + code.instructions().local_get(1); // [x, y] + code.instructions().i64_sub(); + code.instructions().local_tee(diff_local); // [diff] + + // Check for overflow. + // Overflow occurs when subtracting a negative from a positive yields negative, + // or subtracting a positive from a negative yields positive. + // In other words: (x^y)<0 && (diff^x)<0 + + // (x^y)<0 + code.instructions().local_get(0); // [diff, x] + code.instructions().local_get(1); // [diff, x, y] + code.instructions().i64_xor(); // [diff, x^y] + code.instructions().i64_const(0); // [diff, x^y, 0] + code.instructions().i64_lt_s(); // [diff, (x^y)<0] + + // (diff^x)<0 + code.instructions().local_get(diff_local); // [diff, (x^y)<0, diff] + code.instructions().local_get(0); // [diff, (x^y)<0, diff, x] + code.instructions().i64_xor(); // [diff, (x^y)<0, diff^x] + code.instructions().i64_const(0); // [diff, (x^y)<0, diff^x, 0] + code.instructions().i64_lt_s(); // [diff, (x^y)<0, (diff^x)<0] + code.instructions().i32_and(); // [diff, overflow_flag] + + // If overflow_flag != 0, trap. + code.instructions().i32_const(0); // [diff, overflow_flag, 0] + code.instructions().i32_ne(); // [diff, overflowed?] + code.instructions().if_(BlockType::Empty); // [diff] + code.instructions().unreachable(); + code.instructions().end(); + + // Diff is already on stack, add function body end + code.instructions().end(); + + code +} + +/// Body of `__starstream_i64_mul_checked`, for overflow-checked multiplication. +fn i64_mul_checked() -> wasm_encoder::Function { + let mut code = wasm_encoder::Function::new_with_locals_types([ + // 0-1 are params, 2+ are here + ValType::I64, + ValType::I64, + ValType::I64, + ]); + let product_local = 2; + let x_local = 3; + let y_local = 4; + + // Store parameters in locals for reuse + code.instructions().local_get(0); // [x] + code.instructions().local_tee(x_local); // [x] + code.instructions().local_get(1); // [x, y] + code.instructions().local_tee(y_local); // [x, y] + code.instructions().i64_mul(); + code.instructions().local_tee(product_local); // [product] + + // Check for overflow. + // For multiplication overflow detection, we check: + // 1. If y == 0, no overflow + // 2. If product / y != x, then overflow occurred + // Special case: MIN * -1 overflows (since -MIN doesn't fit in i64) + + // Special case: check if x == MIN && y == -1 + code.instructions().local_get(x_local); // [product, x] + code.instructions().i64_const(i64::MIN); // [product, x, MIN] + code.instructions().i64_eq(); // [product, x==MIN] + code.instructions().local_get(y_local); // [product, x==MIN, y] + code.instructions().i64_const(-1); // [product, x==MIN, y, -1] + code.instructions().i64_eq(); // [product, x==MIN, y==-1] + code.instructions().i32_and(); // [product, (x==MIN && y==-1)] + + // Also check y == MIN && x == -1 + code.instructions().local_get(y_local); // [product, (x==MIN && y==-1), y] + code.instructions().i64_const(i64::MIN); // [product, (x==MIN && y==-1), y, MIN] + code.instructions().i64_eq(); // [product, (x==MIN && y==-1), y==MIN] + code.instructions().local_get(x_local); // [product, (x==MIN && y==-1), y==MIN, x] + code.instructions().i64_const(-1); // [product, (x==MIN && y==-1), y==MIN, x, -1] + code.instructions().i64_eq(); // [product, (x==MIN && y==-1), y==MIN, x==-1] + code.instructions().i32_and(); // [product, (x==MIN && y==-1), (y==MIN && x==-1)] + code.instructions().i32_or(); // [product, overflow_special] + + code.instructions().if_(BlockType::Empty); // [product] + code.instructions().unreachable(); + code.instructions().end(); + + // Check regular overflow: if y != 0 and product / y != x + code.instructions().local_get(y_local); // [product, y] + code.instructions().i64_const(0); // [product, y, 0] + code.instructions().i64_ne(); // [product, y!=0] + code.instructions().if_(BlockType::Empty); // [product] + // y != 0, so check if product / y == x + code.instructions().local_get(product_local); // [product, product] + code.instructions().local_get(y_local); // [product, product, y] + code.instructions().i64_div_s(); // [product, product/y] + code.instructions().local_get(x_local); // [product, product/y, x] + code.instructions().i64_ne(); // [product, (product/y)!=x] + code.instructions().if_(BlockType::Empty); // [product] + code.instructions().unreachable(); + code.instructions().end(); + code.instructions().end(); + + // Product is already on stack, add function body end + code.instructions().end(); + + code +} + +/// Unsigned 64-bit arithmetic that traps on overflow or underflow. +fn checked_u64(op: CheckedOp) -> wasm_encoder::Function { + // 0-1 are params, 2 is the result. + let mut code = wasm_encoder::Function::new_with_locals_types([ValType::I64]); + let result = 2; + let mut sink = code.instructions(); + match op { + CheckedOp::Add => { + // Overflow iff the sum wrapped around below x. + sink.local_get(0).local_get(1).i64_add().local_tee(result); // [sum] + sink.local_get(result).local_get(0).i64_lt_u(); // [sum, sum { + // Underflow iff y > x. + sink.local_get(0).local_get(1).i64_sub().local_tee(result); // [diff] + sink.local_get(0).local_get(1).i64_lt_u(); // [diff, x { + // Overflow iff y != 0 and product / y != x. + sink.local_get(0).local_get(1).i64_mul().local_tee(result); // [product] + sink.local_get(1).i64_eqz(); // [product, y==0] + sink.if_(BlockType::Result(ValType::I32)); + sink.i32_const(0); + sink.else_(); + sink.local_get(result) + .local_get(1) + .i64_div_u() + .local_get(0) + .i64_ne(); + sink.end(); // [product, (product/y)!=x] + } + CheckedOp::Div => unreachable!("unsigned division can't overflow"), + } + sink.if_(BlockType::Empty).unreachable().end(); // [result] + sink.end(); + code +} + +/// 32-bit arithmetic computed exactly in i64, trapping when the result doesn't +/// fit back in 32 bits. +fn checked_32(op: CheckedOp, signed: bool) -> wasm_encoder::Function { + // 0-1 are params, 2 is the 64-bit result. + let mut code = wasm_encoder::Function::new_with_locals_types([ValType::I64]); + let wide = 2; + let mut sink = code.instructions(); + for param in [0, 1] { + sink.local_get(param); + if signed { + sink.i64_extend_i32_s(); + } else { + sink.i64_extend_i32_u(); + } + } + match op { + CheckedOp::Add => sink.i64_add(), + CheckedOp::Sub => sink.i64_sub(), + CheckedOp::Mul => sink.i64_mul(), + CheckedOp::Div => unreachable!("32-bit division traps natively on overflow"), + }; + sink.local_tee(wide); // [wide] + if signed { + // Doesn't fit iff sign-extending its low half changes it. + sink.local_get(wide) + .i32_wrap_i64() + .i64_extend_i32_s() + .local_get(wide) + .i64_ne(); + } else { + // Doesn't fit iff its high half is nonzero. + sink.local_get(wide) + .i64_const(32) + .i64_shr_u() + .i64_const(0) + .i64_ne(); + } + sink.if_(BlockType::Empty).unreachable().end(); // [wide] + sink.i32_wrap_i64(); + sink.end(); + code +} + +/// 8- and 16-bit arithmetic computed exactly in i32, trapping when normalizing +/// the result to `width` would change it. +fn checked_sub32(op: CheckedOp, width: IntWidth) -> wasm_encoder::Function { + // 0-1 are params, 2 is the result. + let mut code = wasm_encoder::Function::new_with_locals_types([ValType::I32]); + let result = 2; + let mut sink = code.instructions(); + sink.local_get(0).local_get(1); + match op { + CheckedOp::Add => sink.i32_add(), + CheckedOp::Sub => sink.i32_sub(), + CheckedOp::Mul => sink.i32_mul(), + CheckedOp::Div => { + assert!(width.is_signed(), "unsigned division can't overflow"); + sink.i32_div_s() + } + }; + sink.local_tee(result); // [result] + sink.local_get(result); + match width { + IntWidth::I8 => sink.i32_extend8_s(), + IntWidth::I16 => sink.i32_extend16_s(), + IntWidth::U8 => sink.i32_const(0xFF).i32_and(), + IntWidth::U16 => sink.i32_const(0xFFFF).i32_and(), + _ => unreachable!("{width:?} isn't narrower than 32 bits"), + }; + sink.local_get(result).i32_ne(); // [result, normalized!=result] + sink.if_(BlockType::Empty).unreachable().end(); // [result] + sink.end(); + code +} diff --git a/starstream-to-wasm/src/lib.rs b/starstream-to-wasm/src/lib.rs index 4150eb43..d518aaab 100644 --- a/starstream-to-wasm/src/lib.rs +++ b/starstream-to-wasm/src/lib.rs @@ -33,6 +33,7 @@ use crate::component_abi::{ }; use crate::component_encoder::TypeBuilder; use crate::decision_tree::{Ctor, DecisionTree, Matrix, Pat, Row}; +use crate::intrinsics::CheckedOp; use crate::ir::{ControlFlowGraph, Out}; use crate::stackifier::stackify; @@ -2848,13 +2849,11 @@ impl Compiler { rhs?; match (&left.node.ty, &right.node.ty) { (Type::Int(w), Type::Int(_)) => { - if w.is_64bit() { - if self.options.check_overflows { - let checked_sum = self.get_or_create_i64_add_checked(); - func.instructions(bb).call(checked_sum); - } else { - func.instructions(bb).i64_add(); - } + if self.options.check_overflows { + let checked_sum = self.get_or_create_checked(CheckedOp::Add, *w); + func.instructions(bb).call(checked_sum); + } else if w.is_64bit() { + func.instructions(bb).i64_add(); } else { func.instructions(bb).i32_add(); self.emit_truncate(func, bb, *w); @@ -2877,13 +2876,11 @@ impl Compiler { rhs?; match (&left.node.ty, &right.node.ty) { (Type::Int(w), Type::Int(_)) => { - if w.is_64bit() { - if self.options.check_overflows { - let checked_sub = self.get_or_create_i64_sub_checked(); - func.instructions(bb).call(checked_sub); - } else { - func.instructions(bb).i64_sub(); - } + if self.options.check_overflows { + let checked_sub = self.get_or_create_checked(CheckedOp::Sub, *w); + func.instructions(bb).call(checked_sub); + } else if w.is_64bit() { + func.instructions(bb).i64_sub(); } else { func.instructions(bb).i32_sub(); self.emit_truncate(func, bb, *w); @@ -2906,13 +2903,11 @@ impl Compiler { rhs?; match (&left.node.ty, &right.node.ty) { (Type::Int(w), Type::Int(_)) => { - if w.is_64bit() { - if self.options.check_overflows { - let checked_mul = self.get_or_create_i64_mul_checked(); - func.instructions(bb).call(checked_mul); - } else { - func.instructions(bb).i64_mul(); - } + if self.options.check_overflows { + let checked_mul = self.get_or_create_checked(CheckedOp::Mul, *w); + func.instructions(bb).call(checked_mul); + } else if w.is_64bit() { + func.instructions(bb).i64_mul(); } else { func.instructions(bb).i32_mul(); self.emit_truncate(func, bb, *w); @@ -2941,6 +2936,15 @@ impl Compiler { } else { func.instructions(bb).i64_div_u(); } + } else if w.is_signed() && w.is_sub32() { + // `MIN / -1` fits in i32, so it doesn't trap natively. + if self.options.check_overflows { + let checked_div = self.get_or_create_checked(CheckedOp::Div, *w); + func.instructions(bb).call(checked_div); + } else { + func.instructions(bb).i32_div_s(); + self.emit_truncate(func, bb, *w); + } } else if w.is_signed() { func.instructions(bb).i32_div_s(); } else { @@ -2986,19 +2990,24 @@ impl Compiler { op: UnaryOp::Negate, expr: inner, } => { - // `-x` compiles to `0 - x`. + // `-x` compiles to `0 - x`, so `-MIN` overflows like `0 - MIN`. match &inner.node.ty { - Type::Int(w) if w.is_64bit() => { - func.instructions(bb).i64_const(0); - self.visit_expr_stack(func, bb, locals, inner.span, &inner.node)?; - func.instructions(bb).i64_sub(); - Ok(()) - } Type::Int(w) => { - func.instructions(bb).i32_const(0); + if w.is_64bit() { + func.instructions(bb).i64_const(0); + } else { + func.instructions(bb).i32_const(0); + } self.visit_expr_stack(func, bb, locals, inner.span, &inner.node)?; - func.instructions(bb).i32_sub(); - self.emit_truncate(func, bb, *w); + if self.options.check_overflows { + let checked_sub = self.get_or_create_checked(CheckedOp::Sub, *w); + func.instructions(bb).call(checked_sub); + } else if w.is_64bit() { + func.instructions(bb).i64_sub(); + } else { + func.instructions(bb).i32_sub(); + self.emit_truncate(func, bb, *w); + } Ok(()) } lhs => Err(self.push_error(span, format!("TODO: Negate({lhs:?})"))), diff --git a/starstream-to-wasm/tests/overflow.rs b/starstream-to-wasm/tests/overflow.rs index 83295aa7..caed3e4a 100644 --- a/starstream-to-wasm/tests/overflow.rs +++ b/starstream-to-wasm/tests/overflow.rs @@ -1,8 +1,14 @@ use starstream_compiler::TypecheckOptions; -use wasmtime::{Config, Engine, Linker, Module, Store}; +use starstream_to_wasm::CompileOptions; +use wasmtime::{Config, Engine, Linker, Module, Store, Val}; /// Helper to compile a Starstream program fn compile_program(source: &str) -> Vec { + compile_program_with(source, CompileOptions::default()) +} + +/// Helper to compile a Starstream program with non-default options +fn compile_program_with(source: &str, options: CompileOptions) -> Vec { let parse_output = starstream_compiler::parse_program(source); let (program, errors) = parse_output.into_output_errors(); assert!(errors.is_empty(), "Parse errors: {errors:?}"); @@ -16,9 +22,9 @@ fn compile_program(source: &str) -> Vec { ) .expect("Typecheck failed"); - let compile_result = starstream_to_wasm::compile(&success.program); + let compile_result = options.compile(&success.program); assert!( - errors.is_empty(), + compile_result.errors.is_empty(), "Compile errors: {:?}", compile_result.errors ); @@ -26,8 +32,8 @@ fn compile_program(source: &str) -> Vec { compile_result.wasm.expect("No WASM generated") } -/// Helper to execute WASM and check if it traps -fn execute_wasm(wasm: &[u8], func_name: &str, params: &[i64]) -> Result, String> { +/// Helper to call an exported function, returning the trap message if it traps +fn call(wasm: &[u8], func_name: &str, params: &[Val], results: &mut [Val]) -> Result<(), String> { let mut config = Config::new(); config.wasm_component_model(false); let engine = Engine::new(&config).unwrap(); @@ -42,16 +48,16 @@ fn execute_wasm(wasm: &[u8], func_name: &str, params: &[i64]) -> Result .get_func(&mut store, func_name) .unwrap_or_else(|| panic!("Function {func_name} not found")); - let mut results = vec![wasmtime::Val::I64(0)]; - let params: Vec = params.iter().map(|&p| wasmtime::Val::I64(p)).collect(); + func.call(&mut store, params, results) + .map_err(|e| format!("{e}")) +} - match func.call(&mut store, ¶ms, &mut results) { - Ok(()) => { - let result: Vec = results.iter().map(|v| v.i64().unwrap()).collect(); - Ok(result) - } - Err(e) => Err(format!("{e}")), - } +/// Helper to execute WASM and check if it traps +fn execute_wasm(wasm: &[u8], func_name: &str, params: &[i64]) -> Result, String> { + let mut results = vec![Val::I64(0)]; + let params: Vec = params.iter().map(|&p| Val::I64(p)).collect(); + call(wasm, func_name, ¶ms, &mut results)?; + Ok(results.iter().map(|v| v.i64().unwrap()).collect()) } // ============================================================================ @@ -1155,3 +1161,124 @@ script fn test_mul(x: i64, y: i64) -> i64 { assert!(result.is_ok(), "Should not overflow with zero"); assert_eq!(result.unwrap()[0], 0); } + +// ============================================================================ +// Every integer width +// ============================================================================ + +/// `(type, MIN, MAX)` for every integer type. +const INT_TYPES: [(&str, i128, i128); 8] = [ + ("i8", i8::MIN as i128, i8::MAX as i128), + ("i16", i16::MIN as i128, i16::MAX as i128), + ("i32", i32::MIN as i128, i32::MAX as i128), + ("i64", i64::MIN as i128, i64::MAX as i128), + ("u8", 0, u8::MAX as i128), + ("u16", 0, u16::MAX as i128), + ("u32", 0, u32::MAX as i128), + ("u64", 0, u64::MAX as i128), +]; + +/// The core Wasm value of a `ty` integer: an `i64` for 64-bit types, otherwise +/// an `i32`, sign-extended for signed types. +fn int_val(ty: &str, value: i128) -> Val { + if ty.ends_with("64") { + Val::I64(value as i64) + } else { + Val::I32(value as i32) + } +} + +/// Compile `add`, `sub`, `mul` and `div` on `ty`, plus `neg` for signed types. +fn compile_arithmetic(ty: &str, options: CompileOptions) -> Vec { + let mut source = String::new(); + for (name, op) in [("add", "+"), ("sub", "-"), ("mul", "*"), ("div", "/")] { + source.push_str(&format!( + "script fn {name}(x: {ty}, y: {ty}) -> {ty} {{\n x {op} y\n}}\n" + )); + } + if ty.starts_with('i') { + source.push_str(&format!("script fn neg(x: {ty}) -> {ty} {{\n -x\n}}\n")); + } + compile_program_with(&source, options) +} + +/// Call `func_name` on `ty` arguments: `Some(result)` must come back, `None` +/// means it must trap. +fn assert_int_call(wasm: &[u8], ty: &str, func_name: &str, args: &[i128], expected: Option) { + let params: Vec = args.iter().map(|&arg| int_val(ty, arg)).collect(); + let mut results = [int_val(ty, 0)]; + let result = call(wasm, func_name, ¶ms, &mut results); + let call = format!("{func_name}{args:?} on {ty}"); + match expected { + Some(value) => { + assert!(result.is_ok(), "{call} trapped: {result:?}"); + let (actual, expected) = match (&results[0], int_val(ty, value)) { + (Val::I32(actual), Val::I32(expected)) => (i64::from(*actual), i64::from(expected)), + (Val::I64(actual), Val::I64(expected)) => (*actual, expected), + other => panic!("{call}: mismatched result types {other:?}"), + }; + assert_eq!(actual, expected, "{call}"); + } + None => assert!(result.is_err(), "{call} should trap"), + } +} + +#[test] +fn test_overflow_traps_for_every_width() { + for (ty, min, max) in INT_TYPES { + let wasm = compile_arithmetic(ty, CompileOptions::default()); + let check = |func_name, args: &[i128], expected| { + assert_int_call(&wasm, ty, func_name, args, expected) + }; + + check("add", &[max, 1], None); + check("add", &[max - 1, 1], Some(max)); + check("mul", &[max, 2], None); + check("mul", &[max, 1], Some(max)); + check("div", &[max, 1], Some(max)); + + if ty.starts_with('i') { + check("add", &[min, -1], None); + check("add", &[min + 1, -1], Some(min)); + check("sub", &[min, 1], None); + check("sub", &[max, -1], None); + check("sub", &[min + 1, 1], Some(min)); + check("mul", &[min, -1], None); + check("mul", &[-1, max], Some(min + 1)); + check("div", &[min, -1], None); + check("div", &[min, 1], Some(min)); + check("neg", &[min], None); + check("neg", &[max], Some(-max)); + } else { + let half = 1i128 << (ty[1..].parse::().unwrap() / 2); + check("sub", &[0, 1], None); + check("sub", &[max, max], Some(0)); + check("mul", &[half, half], None); + check("mul", &[half - 1, half + 1], Some(max)); + } + } +} + +#[test] +fn test_unchecked_arithmetic_wraps_for_every_width() { + let options = CompileOptions { + check_overflows: false, + ..CompileOptions::default() + }; + for (ty, min, max) in INT_TYPES { + let wasm = compile_arithmetic(ty, options.clone()); + let check = |func_name, args: &[i128], expected| { + assert_int_call(&wasm, ty, func_name, args, Some(expected)) + }; + + check("add", &[max, 1], min); + check("sub", &[min, 1], max); + if ty.starts_with('i') { + check("neg", &[min], min); + // 32- and 64-bit `MIN / -1` traps natively. + if matches!(ty, "i8" | "i16") { + check("div", &[min, -1], min); + } + } + } +} diff --git a/starstream-to-wasm/tests/snapshots/inputs@score.star.snap b/starstream-to-wasm/tests/snapshots/inputs@score.star.snap index 475b139c..c6fd0c93 100644 --- a/starstream-to-wasm/tests/snapshots/inputs@score.star.snap +++ b/starstream-to-wasm/tests/snapshots/inputs@score.star.snap @@ -2955,44 +2955,25 @@ TypedProgram { (unreachable) ) (func (;20;) (type 8) (param i64 i64) (result i64) - (local i64 i64 i64) + (local i64) (local.tee 2 (i64.mul - (local.tee 3 - (local.get 0)) - (local.tee 4 - (local.get 1)))) - (if ;; label = @1 - (i32.or - (i32.and - (i64.eq - (local.get 3) - (i64.const -9223372036854775808)) - (i64.eq - (local.get 4) - (i64.const -1))) - (i32.and - (i64.eq - (local.get 4) - (i64.const -9223372036854775808)) - (i64.eq - (local.get 3) - (i64.const -1)))) - (then - (unreachable))) + (local.get 0) + (local.get 1))) (if ;; label = @1 - (i64.ne - (local.get 4) - (i64.const 0)) - (then - (if ;; label = @2 + (if (result i32) ;; label = @1 + (i64.eqz + (local.get 1)) + (then + (i32.const 0)) + (else (i64.ne - (i64.div_s + (i64.div_u (local.get 2) - (local.get 4)) - (local.get 3)) - (then - (unreachable))))) + (local.get 1)) + (local.get 0)))) + (then + (unreachable))) ) (func (;21;) (type 5) (param i32) (result i32) (local i32) @@ -3057,19 +3038,9 @@ TypedProgram { (local.get 0) (local.get 1))) (if ;; label = @1 - (i32.ne - (i32.and - (i64.ge_s - (i64.xor - (local.get 0) - (local.get 1)) - (i64.const 0)) - (i64.lt_s - (i64.xor - (local.get 2) - (local.get 0)) - (i64.const 0))) - (i32.const 0)) + (i64.lt_u + (local.get 2) + (local.get 0)) (then (unreachable))) )