wasm: implement all addition operators (wrapping, panicking, saturating, and checked)

This commit is contained in:
Brian Carroll 2022-06-21 23:12:17 +01:00
parent d7ac8cfcbc
commit b1193e4134
No known key found for this signature in database
GPG key ID: 9CF4E3BF9C4722C7
9 changed files with 99 additions and 77 deletions

View file

@ -231,7 +231,22 @@ pub const RocDec = extern struct {
const answer = RocDec.addWithOverflow(self, other);
if (answer.has_overflowed) {
@panic("TODO runtime exception for overflow!");
roc_panic("Dec overflow", 1);
unreachable;
} else {
return answer.value;
}
}
pub fn addSaturated(self: RocDec, other: RocDec) RocDec {
const answer = RocDec.addWithOverflow(self, other);
if (answer.has_overflowed) {
// We can unambiguously tell which way it wrapped, because we have 129 bits including the overflow bit
if (answer.value.num < 0) {
return RocDec.max;
} else {
return RocDec.min;
}
} else {
return answer.value;
}
@ -1096,12 +1111,9 @@ pub fn divC(arg1: RocDec, arg2: RocDec) callconv(.C) i128 {
}
pub fn addOrPanicC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec {
const result = arg1.addWithOverflow(arg2);
if (result.has_overflowed) {
const todo_why_does_this_argument_exist = 1;
roc_panic("Dec overflow", todo_why_does_this_argument_exist);
unreachable;
} else {
return result.value;
return @call(.{ .modifier = always_inline }, RocDec.add, .{ arg1, arg2 });
}
pub fn addSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec {
return @call(.{ .modifier = always_inline }, RocDec.addSaturated, .{ arg1, arg2 });
}

View file

@ -22,6 +22,7 @@ comptime {
exportDecFn(dec.mulC, "mul_with_overflow");
exportDecFn(dec.divC, "div");
exportDecFn(dec.addOrPanicC, "add_or_panic");
exportDecFn(dec.addSaturatedC, "add_saturated");
}
// List Module
@ -89,7 +90,6 @@ comptime {
const num = @import("num.zig");
const INTEGERS = [_]type{ i8, i16, i32, i64, i128, u8, u16, u32, u64, u128 };
const WIDEINTS = [_]type{ i16, i32, i64, i128, i256, u16, u32, u64, u128, u256 };
const FLOATS = [_]type{ f32, f64 };
const NUMBERS = INTEGERS ++ FLOATS;
@ -97,7 +97,7 @@ comptime {
exportNumFn(num.bytesToU16C, "bytes_to_u16");
exportNumFn(num.bytesToU32C, "bytes_to_u32");
inline for (INTEGERS) |T, i| {
inline for (INTEGERS) |T| {
num.exportPow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".pow_int.");
num.exportDivCeil(T, ROC_BUILTINS ++ "." ++ NUM ++ ".div_ceil.");
@ -106,8 +106,7 @@ comptime {
num.exportAddWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_with_overflow.");
num.exportAddOrPanic(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_or_panic.");
const Wider = WIDEINTS[i];
num.exportAddSaturatedInt(T, Wider, ROC_BUILTINS ++ "." ++ NUM ++ ".add_saturated.");
num.exportAddSaturatedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_saturated.");
}
inline for (INTEGERS) |FROM| {

View file

@ -221,22 +221,19 @@ pub fn exportAddWithOverflow(comptime T: type, comptime name: []const u8) void {
@export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong });
}
pub fn exportAddSaturatedInt(comptime T: type, comptime Wider: type, comptime name: []const u8) void {
pub fn exportAddSaturatedInt(comptime T: type, comptime name: []const u8) void {
comptime var f = struct {
fn func(self: T, other: T) callconv(.C) T {
const self_w: Wider = self;
const other_w: Wider = other;
const answer = self_w + other_w;
const max = std.math.maxInt(T);
const min = std.math.minInt(T);
if (answer > max) {
return max;
} else if (answer < min) {
return min;
const result = addWithOverflow(T, self, other);
if (result.has_overflowed) {
// We can unambiguously tell which way it wrapped, because we have N+1 bits including the overflow bit
if (result.value < 0) {
return std.math.maxInt(T);
} else {
return @intCast(T, answer);
return std.math.minInt(T);
}
} else {
return result.value;
}
}
}.func;

View file

@ -285,6 +285,8 @@ pub const NUM_DIV_CEIL: IntrinsicName = int_intrinsic!("roc_builtins.num.div_cei
pub const NUM_ROUND_F32: IntrinsicName = int_intrinsic!("roc_builtins.num.round_f32");
pub const NUM_ROUND_F64: IntrinsicName = int_intrinsic!("roc_builtins.num.round_f64");
pub const NUM_ADD_OR_PANIC_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_or_panic");
pub const NUM_ADD_SATURATED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_saturated");
pub const NUM_ADD_WITH_OVERFLOW_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_with_overflow");
pub const NUM_ADD_WITH_OVERFLOW_FLOAT: IntrinsicName = float_intrinsic!("roc_builtins.num.add_with_overflow");
@ -373,6 +375,8 @@ pub const DEC_ADD_WITH_OVERFLOW: &str = "roc_builtins.dec.add_with_overflow";
pub const DEC_SUB_WITH_OVERFLOW: &str = "roc_builtins.dec.sub_with_overflow";
pub const DEC_MUL_WITH_OVERFLOW: &str = "roc_builtins.dec.mul_with_overflow";
pub const DEC_DIV: &str = "roc_builtins.dec.div";
pub const DEC_ADD_OR_PANIC: &str = "roc_builtins.dec.add_or_panic";
pub const DEC_ADD_SATURATED: &str = "roc_builtins.dec.add_saturated";
pub const UTILS_TEST_PANIC: &str = "roc_builtins.utils.test_panic";
pub const UTILS_ALLOCATE_WITH_REFCOUNT: &str = "roc_builtins.utils.allocate_with_refcount";

View file

@ -6931,29 +6931,7 @@ fn build_float_binop<'a, 'ctx, 'env>(
let bd = env.builder;
match op {
NumAdd => {
let builder = env.builder;
let context = env.context;
let result = bd.build_float_add(lhs, rhs, "add_float");
let is_finite =
call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width])
.into_int_value();
let then_block = context.append_basic_block(parent, "then_block");
let throw_block = context.append_basic_block(parent, "throw_block");
builder.build_conditional_branch(is_finite, then_block, throw_block);
builder.position_at_end(throw_block);
throw_exception(env, "float addition overflowed!");
builder.position_at_end(then_block);
result.into()
}
NumAdd => bd.build_float_add(lhs, rhs, "add_float").into(),
NumAddChecked => {
let context = env.context;

View file

@ -319,20 +319,24 @@ impl<'a> LowLevelCall<'a> {
}
// Num
NumAdd => {
NumAdd => match self.ret_layout {
Layout::Builtin(Builtin::Int(width)) => {
self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_OR_PANIC_INT[width])
}
Layout::Builtin(Builtin::Float(FloatWidth::F32)) => {
self.load_args(backend);
match CodeGenNumType::from(self.ret_layout) {
CodeGenNumType::I32 => backend.code_builder.i32_add(),
CodeGenNumType::I64 => backend.code_builder.i64_add(),
CodeGenNumType::F32 => backend.code_builder.f32_add(),
CodeGenNumType::F64 => backend.code_builder.f64_add(),
CodeGenNumType::I128 => todo!("{:?}", self.lowlevel),
CodeGenNumType::F128 => todo!("{:?}", self.lowlevel),
CodeGenNumType::Decimal => {
self.load_args_and_call_zig(backend, bitcode::DEC_ADD_WITH_OVERFLOW)
backend.code_builder.f32_add()
}
Layout::Builtin(Builtin::Float(FloatWidth::F64)) => {
self.load_args(backend);
backend.code_builder.f64_add()
}
Layout::Builtin(Builtin::Float(FloatWidth::F128)) => todo!("Num.add for f128"),
Layout::Builtin(Builtin::Decimal) => {
self.load_args_and_call_zig(backend, bitcode::DEC_ADD_OR_PANIC)
}
_ => panic_ret_type(),
},
NumAddWrap => {
self.load_args(backend);
@ -344,24 +348,53 @@ impl<'a> LowLevelCall<'a> {
I64 => backend.code_builder.i64_add(),
F32 => backend.code_builder.f32_add(),
F64 => backend.code_builder.f64_add(),
Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_ADD_WITH_OVERFLOW),
x => todo!("{:?} for {:?}", self.lowlevel, x),
I128 => self.load_args_and_call_zig(
backend,
&bitcode::NUM_ADD_OR_PANIC_INT[IntWidth::I128], // TODO: don't panic
),
Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_ADD_OR_PANIC), // TODO: don't panic
F128 => todo!("f128 NumAddWrap"),
}
}
NumToStr => todo!("{:?}", self.lowlevel),
NumAddChecked => {
let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]];
match arg_layout {
Layout::Builtin(Builtin::Int(width)) => {
self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_WITH_OVERFLOW_INT[width])
}
Layout::Builtin(Builtin::Float(width)) => {
self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_WITH_OVERFLOW_FLOAT[width])
Layout::Builtin(Builtin::Int(width)) => self.load_args_and_call_zig(
backend,
&bitcode::NUM_ADD_WITH_OVERFLOW_INT[width],
),
Layout::Builtin(Builtin::Float(width)) => self.load_args_and_call_zig(
backend,
&bitcode::NUM_ADD_WITH_OVERFLOW_FLOAT[width],
),
Layout::Builtin(Builtin::Decimal) => {
self.load_args_and_call_zig(backend, bitcode::DEC_ADD_WITH_OVERFLOW)
}
x => internal_error!("NumAddChecked is not defined for {:?}", x),
}
}
NumAddSaturated => todo!("{:?}", self.lowlevel),
NumAddSaturated => match self.ret_layout {
Layout::Builtin(Builtin::Int(width)) => {
self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_SATURATED_INT[width])
}
Layout::Builtin(Builtin::Float(FloatWidth::F32)) => {
self.load_args(backend);
backend.code_builder.f32_add()
}
Layout::Builtin(Builtin::Float(FloatWidth::F64)) => {
self.load_args(backend);
backend.code_builder.f64_add()
}
Layout::Builtin(Builtin::Decimal) => {
self.load_args_and_call_zig(backend, bitcode::DEC_ADD_SATURATED)
}
_ => panic_ret_type(),
},
NumSub => {
self.load_args(backend);
match CodeGenNumType::from(self.ret_layout) {

View file

@ -1664,7 +1664,7 @@ fn atan() {
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[should_panic(expected = r#"Roc failed with message: "integer addition overflowed!"#)]
fn int_add_overflow() {
assert_evals_to!(
@ -1753,8 +1753,7 @@ fn float_add_checked_fail() {
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
#[should_panic(expected = r#"Roc failed with message: "float addition overflowed!"#)]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn float_add_overflow() {
assert_evals_to!(
indoc!(
@ -1762,7 +1761,7 @@ fn float_add_overflow() {
1.7976931348623157e308 + 1.7976931348623157e308
"#
),
0.0,
f64::INFINITY,
f64
);
}
@ -3095,7 +3094,7 @@ fn u8_mul_greater_than_i8() {
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn add_saturated() {
assert_evals_to!(
indoc!(

View file

@ -1059,7 +1059,7 @@ fn call_with_bad_record_runtime_error() {
#[test]
#[cfg(any(feature = "gen-wasm"))]
#[should_panic(expected = r#"Can't create record with improper layout"#)]
#[should_panic(expected = r#"Roc failed with message: "Can't create record with improper layout"#)]
fn call_with_bad_record_runtime_error() {
expect_runtime_error_panic!(indoc!(
r#"

View file

@ -208,7 +208,7 @@ where
match test_wrapper.call(&[]) {
Err(e) => {
if let Some(msg) = get_roc_panic_msg(&instance, memory) {
Err(msg)
Err(format!("Roc failed with message: \"{}\"", msg))
} else {
Err(e.to_string())
}