Skip to content

Simplify codegen of mixed-type checked integer addition and subtraction #15878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

HertzDevil
Copy link
Contributor

Code generation for mixed-type uses of overflow-checked primitive integer addition or subtraction is currently somewhat convoluted, in particular when the receiver is signed and the argument unsigned, or vice versa. The compiler would create an intermediate integer type that has one more bit than the operands, e.g. i9 or i33; most LLVM targets do not like these types, and this leads to some rather unsightly LLVM IR that is hard to optimize.

This PR is a complete overhaul of the way these additions and subtractions are emitted. There are now multiple specialized code paths depending on whether the two operand types have the same signedness or width. The following Crystal snippet illustrates how each code path could be expressed equivalently in terms of other primitive calls in native Crystal:

fun i8_add_u8(p1 : Int8, p2 : UInt8) : Int8
  p1_biased = (p1 ^ Int8::MIN).to_u8!
  result = p1_biased + p2 # same-type, checked
  result.to_i8! ^ Int8::MIN
end

fun u16_add_i8(p1 : UInt16, p2 : Int8) : UInt16
  p1_biased = p1.to_i16! ^ Int16::MIN
  result = i16_add_i8(p1_biased, p2) # checked, see below
  (result ^ Int16::MIN).to_u16!
end

fun i8_add_u16(p1 : Int8, p2 : UInt16) : Int8
  p1_biased = (p1 ^ Int8::MIN).to_u8!
  result = u8_add_u16(p1_biased, p2) # checked, see below
  result.to_i8! ^ Int8::MIN
end

fun i8_add_i16(p1 : Int8, p2 : Int16) : Int8
  p1_ext = p1.to_i16!
  result = p1_ext &+ p2
  result.to_i8 # checked
end

# the actual optimal call sequence is slightly different,
# probably due to some short-circuit evaluation issue 
fun u8_add_u16(p1 : UInt8, p2 : UInt16) : UInt8
  p2_trunc = p2.to_u8 # checked
  p1 + p2_trunc       # same-type, checked
end

fun i16_add_i8(p1 : Int16, p2 : Int8) : Int16
  p2_ext = p2.to_i16!
  p1 + p2_ext # same-type, checked
end

(Before and after on Compiler Explorer)

The gist here is that mixed-signedness operations are transformed into same-signedness ones by applying a bias to the first operand and switching its signedness, using a bitwise XOR. For example, -0x80_i8..0x7F_i8 maps linearly to 0x00_u8..0xFF_u8, and vice-versa. The same-signedness arithmetic operation that follows will overflow if and only if the original operation does. The result is XOR'ed again afterwards, as the bias action is the inverse of itself. This is the trick that allows mixed-type addition or subtraction without resorting to arcane integer bit widths.

@straight-shoota
Copy link
Member

It might be nice to put the snippets with the native Crystal equivalents directly into code comments.
And perhaps also a bit of the explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants