Skip to content

Commit

Permalink
feat: implement codegen for i32 ops
Browse files Browse the repository at this point in the history
  • Loading branch information
bitwalker committed Oct 18, 2023
1 parent 3b12bb9 commit a8180bb
Show file tree
Hide file tree
Showing 7 changed files with 618 additions and 6 deletions.
321 changes: 321 additions & 0 deletions codegen/masm/intrinsics/i32.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
const.SIGN_BIT=2147483648 # 1 << 31
const.MIN=SIGN_BIT
const.NEG1=4294967295 # u32::MAX

# Returns `1` if `a` has its sign bit set, else `0`
#
# This function consumes `a`.
export.is_signed # [a]
push.SIGN_BIT u32checked_and push.SIGN_BIT eq
end

# Adds `a` to `b`, asserting that both inputs are valid i32.
#
# Returns the result modulo 2^32, plus a boolean indicating whether or not the subtraction underflowed.
export.overflowing_add # [b, a]
u32assert2

# is `b` signed?
dup.0 exec.is_signed # [is_b_signed, b, a]

# is `a` signed?
dup.2 exec.is_signed # [is_a_signed, is_b_signed, b, a]

# compute result
movup.3 movup.3 u32wrapping_add

# is `result` signed?
dup.0 exec.is_signed # [is_result_signed, result, is_a_signed, is_b_signed]

# if `b` and `result` have the same sign, which is different than the sign of `a`,
# then overflow has occurred. In all other cases, the result is valid
movup.3 # [is_b_signed, is_result_signed, result, is_a_signed]
dup.1 eq # [do_b_and_result_have_same_sign, is_result_signed, result, is_a_signed]
swap.1 movup.3 # [is_a_signed, is_result_signed, do_b_and_result_have_same_sign, result]
neq # [do_a_and_result_have_different_sign, do_b_and_result_have_same_sign, result]
and # [overflowed, result]
end

# Adds `b` to `a`, asserting that both inputs are valid i32,
# and asserting that the addition does not overflow.
export.checked_add # [b, a]
exec.overflowing_add # [overflowed, result]
assertz # [result]
end

# Get the negation of `a`
#
# This operation is checked, so if the input is not a valid i32,
# or if the negation is not a valid i32, execution traps
export.checked_neg # [a]
# assert that the negation is representable
dup.0 push.MIN eq assertz
u32checked_not u32wrapping_add.1
end

# Get the negation of `a`
#
# This operation is unchecked, so if the input is not a valid i32 the behavior is undefined
export.unchecked_neg # [a]
u32checked_not u32wrapping_add.1
end

# Subtracts `b` from `a`
#
# This operation will fail if `b` is not a valid i32, or if `result` is not a valid i32
export.unchecked_sub # [b, a]
u32checked_not
add
add.1 # [!b + a + 1]
end

# Subtracts `b` from `a`, asserting that both inputs are valid i32.
#
# Returns the result modulo 2^32, plus a boolean indicating whether or not the subtraction underflowed.
export.overflowing_sub # [b, a]
u32assert2

# is `b` signed?
dup.0 exec.is_signed # [is_b_signed, b, a]

# is `a` signed?
dup.2 exec.is_signed # [is_a_signed, is_b_signed, b, a]

# compute result
movup.3 movup.3 u32wrapping_sub

# is `result` signed?
dup.0 exec.is_signed # [is_result_signed, result, is_a_signed, is_b_signed]

# if `b` and `result` have the same sign, which is different than the sign of `a`,
# then overflow has occurred. In all other cases, the result is valid
movup.3 # [is_b_signed, is_result_signed, result, is_a_signed]
dup.1 eq # [do_b_and_result_have_same_sign, is_result_signed, result, is_a_signed]
swap.1 movup.3 # [is_a_signed, is_result_signed, do_b_and_result_have_same_sign, result]
neq # [do_a_and_result_have_different_sign, do_b_and_result_have_same_sign, result]
and # [overflowed, result]
end

# Subtracts `b` from `a`, asserting that both inputs are valid i32,
# and asserting that the subtraction does not underflow.
export.checked_sub # [b, a]
exec.overflowing_sub # [overflowed, result]
assertz # [result]
end

# Multiplies `a` by `b`, asserting that both inputs are valid i32.
#
# Returns the result modulo 2^32, plus a boolean indicating whether or not the multiplication overflowed.
export.overflowing_mul # [b, a]
u32assert2

# is `b` signed?
dup.0 exec.is_signed # [is_b_signed, b, a]

# is `a` signed?
dup.2 exec.is_signed # [is_a_signed, is_b_signed, b, a]

# compute result
movup.3 movup.3 u32wrapping_mul

# if `b` and `result` have the same sign, which is different than the sign of `a`,
# then overflow has occurred. In all other cases, the result is valid
movup.3 # [is_b_signed, is_result_signed, result, is_a_signed]
dup.1 eq # [do_b_and_result_have_same_sign, is_result_signed, result, is_a_signed]
swap.1 movup.3 # [is_a_signed, is_result_signed, do_b_and_result_have_same_sign, result]
neq # [do_a_and_result_have_different_sign, do_b_and_result_have_same_sign, result]
and # [overflowed, result]
end

# Multiplies `a` by `b`, asserting that both inputs are valid i32,
# and asserting that the multiplication does not underflow.
export.checked_mul # [b, a]
exec.overflowing_mul # [overflowed, result]
assertz # [result]
end

# Divides `a` by `b`, asserting that both inputs are valid i32
export.checked_div # [b, a]
u32assert2

# get positive dividend
dup.1 exec.unchecked_neg # [-a, b, a]
dup.2 swap.1 # [-a, a, b, a]
movup.3 exec.is_signed # [is_a_signed, -a, a, b]
dup.0 movdn.4 cdrop # [|a|, b, is_a_signed]

# get positive divisor
dup.1 exec.unchecked_neg # [-b, |a|, b, is_a_signed]
dup.2 swap.1 # [-b, b, |a|, b, is_a_signed]
movup.3 exec.is_signed # [is_b_signed, -b, b, |a|, is_a_signed]
dup.0 movdn.4 cdrop # [|b|, |a|, is_a_signed, is_b_signed]

# divide
u32unchecked_div # [|a / b|, is_a_signed, is_b_signed]

# if the signs differ, negate the result
movdn.2 neq # [signs_differ, |a / b|]
dup.1 exec.unchecked_neg # [-|a / b|, signs_differ, |a / b|]
swap.1 cdrop # [result]
end

# Given two i32 values in two's complement representation, compare them,
# returning -1 if `a` < `b`, 0 if equal, and 1 if `a` > `b`.
#
export.icmp # [b, a]
dup.1 # [a, b, a]
dup.1 # [b, a, b, a]

# get the most-significant bit of `b`
push.SIGN_BIT # [1<<31, b, a, b, a]
u32checked_and # [b_msb, a, b, a]

# get the most-significant bit of `a`
swap.1 # [a, b_msb, b, a]
push.SIGN_BIT # [1<<31, a, b_msb, b, a]
u32checked_and # [a_msb, b_msb, b, a]

eq.0 # [a_msb == 0, b_msb, b, a]
swap.1 eq.0 # [b_msb == 0, a_msb == 0, b, a]
swap.1 dup.1 neq # [a_msb != b_msb, b_msb == 0, b, a]

# if a_msb != b_msb, then a > b (if a_msb == 0), or a < b (if a_msb == 1)
if.true # [b_msb == 0, b, a]
movdn.2 drop drop # [b_msb == 0]
push.1 push.NEG1 # [-1, 1, b_msb == 0]
swap.2 # [b_msb == 0, 1, -1]
cdrop # [1 or -1]
else # [b_msb == 0, b, a]
# a_msb == b_msb, so we can compare the remaining bits lexicographically,
# which we get for free via the lt/gt ops
drop # [b, a]
dup.1 dup.1 # [b, a, b, a]
u32unchecked_gt movdn.2 # [b, a, a > b]
u32unchecked_lt # [a < b, a > b]
push.0 push.NEG1 push.1
swap.3 # [a < b, -1, 0, 1, a > b]
cdrop # [-1 or 0, 1, a > b]
swap.2 # [a > b, 1, -1 or 0]
cdrop # [1 or -1 or 0]
end
end

# Given two i32 values in two's complement representation, return 1 if `a < b`, else 0
export.is_lt # [b, a]
exec.icmp push.NEG1 eq
end

# Given two i32 values in two's complement representation, return 1 if `a <= b`, else 0
export.is_lte # [b, a]
exec.icmp neq.1
end

# Given two i32 values in two's complement representation, return 1 if `a > b`, else 0
export.is_gt # [b, a]
exec.icmp eq.1
end

# Given two i32 values in two's complement representation, return 1 if `a >= b`, else 0
export.is_gte # [b, a]
exec.icmp push.NEG1 neq
end

# Compute 2^n, where `n` must be less than 31, or the result will overflow i32::MAX
export.pow2 # [n]
dup.0
push.31
u32checked_lt # [n < 31, pow]
assert # [n]
push.1 swap.1 # [n, 1]
u32checked_shl # [1 << n]
end

# Compute a^b, where `b` must be a positive i32 value < 31
export.ipow # [b, a]
dup.0 eq.0 # [b == 0, b, a]
dup.2 eq.0 # [a == 0, b == 0, b, a]
or # [a == 0 || b == 0, b, a]
# if a == 0, the result is always 0; otherwise if b == 0, then the result is always 1
if.true
eq.0 # [b == 0, a]
push.1 push.0 # [0, 1, b == 0, a]
swap.2 # [b == 0, 1, 0, a]
cdrop # [1 or 0, a]
swap.1 drop # [1 or 0]
else # [b, a]
# for all other values, we do exponentiation by squaring
push.1 # [acc, b, a]
dup.1 # [b, acc, b, a]
push.1 # [1, b, acc, b, a]
u32checked_gt # [b > 1, acc, b, a]
while.true # [acc, b, a => base]
dup.2 dup.1 # [acc, base, acc, b, base]
mul # [base * acc, acc, b, base]
dup.2 # [b, base * acc, acc, b, base]
push.1 # [1, b, base * acc, acc, b, base]
u32checked_and # [b & 1, base * acc, acc, b, base]
eq.1 # [b & 1 == 1, base * acc, acc, b, base]
cdrop # [acc, b, base]
u32assert
swap.1 # [b, acc, base]
u32checked_div.2 # [b /= 2, acc, base]
movup.2 dup.0 # [base, base, b, acc]
u32checked_mul # [base * base, b, acc]
swap.1 # [b, base, acc]
movup.2 # [acc, b, base]
dup.1 push.1 # [1, b, acc, b, base]
u32checked_gt # [b > 1, acc, b, base]
end
swap.1 drop # [acc, base]
u32checked_mul # [acc * base]
end
end

# Arithmetic shift-right, i.e. `a >> b` preserves the signedness of the value
#
# This implementation is checked, so it will assert if the inputs are invalid
export.checked_shr # [b, a]
# validate the shift is valid
dup.0 push.32
u32checked_lt # [b < 32, b, a]
assert

# if the input is zero, the output is always zero,
# and if the shift is zero, the input is returned unchanged
dup.0 eq.0 # [b == 0, b, a]
dup.2 eq.0 # [a == 0, b == 0, b, a]
or # [a == 0 || b == 0, b, a]
if.true
# return `a` if `b == 0`, otherwise `a == 0` so return 0
eq.0 # [b == 0, a]
swap.1 push.0 # [0, a, b == 0]
swap.2 # [b == 0, a, 0]
cdrop # [a or 0]
else # [b, a]
# get the signedness of the value
dup.1 # [a, b, a]
push.SIGN_BIT # [1<<31, a, b, a]
u32checked_and push.SIGN_BIT eq # [is_signed, b, a]

# if the value is signed, we must sign-extend the result,
# otherwise we can treat it as an unsigned shift
if.true # [b, a]
swap.1 # [a, b]
dup.1 # [b, a, b]
u32checked_shr # [shifted, b]

# compute the extension mask
push.1 dup.2 # [b, 1, shifted, b]
u32unchecked_shl
sub.1 # [(1 << b) - 1, shifted, b]

# shift the mask into place
push.32 movup.3 # [b, 32, mask, shifted]
sub # [32 - b, mask, shifted]
u32unchecked_shl # [mask << (32 - b), shifted]
u32checked_or # [shifted | mask]
else
u32checked_shr
end
end
end
5 changes: 5 additions & 0 deletions codegen/masm/src/masm/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use rustc_hash::FxHashMap;

use super::{Function, FunctionListAdapter, Import, ModuleImportInfo};

const I32_INTRINSICS: &'static str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/intrinsics/i32.masm"));
const MEM_INTRINSICS: &'static str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/intrinsics/mem.masm"));

Expand Down Expand Up @@ -228,5 +230,8 @@ impl Module {
Self::parse_str(MEM_INTRINSICS, "intrinsics::mem").expect("invalid module")
}

/// This is a helper that parses and returns the predefined `intrinsics::i32` module
pub fn i32_intrinsics() -> Self {
Self::parse_str(I32_INTRINSICS, "intrinsics::i32").expect("invalid module")
}
}
Loading

0 comments on commit a8180bb

Please sign in to comment.