Hey, all! I’ve just released saline, a shard for using saturating arithmetic in Crystal. This is something I’ve been meaning to do, but some recent discussion motivated me to make a shard available.
I’d love to get some feedback if anyone has any ideas (especially if you’re actually going to be using the shard).
n = Saturating(Int32).new(Int32::MAX - 2)
n += 20 # => 2147483647 (Int32::MAX)
m = Saturating(Int32).new(Int32::MIN + 3)
m -= 30 # => -2147483648 (Int32::MIN)
At first I was confused that you’d pass the (arbitrary) maximum value you want to allow to .new :D
API wise for a shard like this I wouldn’t mind adding itself to Int, so providing something like 0.to_si32 etc.
I’m not sure why you’d want to keep multiple implementation around, the intrinsics version should beat them all :) Meanwhile an implementation using wrapping operators (&+ etc) and checking whether the value wrapped around (by keeping value from before the operation around and comparing) should be slightly more performant as it avoids the stack unwinding :)
lib LibIntrinsics
fun sat_uadd8 = "llvm.uadd.sat.i8"(a : UInt8, b : UInt8) : UInt8
end
module Intrinsics
def self.sadd(a : UInt8, b : UInt8) : UInt8
LibIntrinsics.sat_uadd8(a, b)
end
end
pp! a = 100_u8 # => 100
pp! a = Intrinsics.sadd(a, 100_u8) # => 200
pp! a = Intrinsics.sadd(a, 100_u8) # => 255
Another idea that I would like to throw is that I think it should be possible to use macros to rewrite calls to + to some custom function like Intrinsics.sadd.
So Saline.eval(1 + 2 + 3) would compile to Intrinsics.sadd(Intrinsics.sadd(1, 2), 3) or something similar.
This is a good point. I’ll update the README. In particular, my number choice seems to have caused confusion already…
I don’t like throwing stuff away… However, if I’m able to keep an API that’s easy to use (like, I’d argue, the current one), I’m fine only using the LLVM-based implementation.
Yeah, that’s on my to-do list. I saw your code snippet, and I was planning on using something like that. However, I think wrapping it in a type will probably still be a friendlier API.
OT
I had worked on saturating arithmetic a little before, but a large part of the reason I made this shard was so that you could use it. I’m not asking for overflowing gratitude, but it would be nice if you weren’t rude about this thing that was made for you.
This is a product of my bad example in the README (see @jhass’s post above). In a call Saturating(T).new(x), the T is obviously the type, and the x is the value you want the variable to have (not the max). I’ll be changing the README to make that more clear.
I don’t think this is possible, but I’ll look into it.
People have discussed this with you, and I’m not going to rehash this argument. This thread is not a place to argue that this shard shouldn’t exist because you think the language should already work like this.
Before introducing the &+ and others operators for wrapping operations there were some proposal and discussion about how wrapping types can be used. If interested, check https://github.com/crystal-lang/crystal/pull/6223 .
@RespiteSage That was so nice of you to help out You did nothing wrong and it is one person acting in a hostile manner. Your contribution to the conversation by creating a solution is a great idea!
The documentation is still pretty lacking, but the big thing is that Saturating is much faster in almost every case due to a new LLVM-backed implementation. It’s pretty complicated under the hood, and it’s currently only properly implemented for Int* and UInt* types, but my benchmarks show most operations taking less than 1.2x as long as those operations normally take for primitives. The worst slowdowns, for multiplication (there’s no LLVM backing for saturating multiplication), still take less than 10x as long as the non-saturating operations on primitives.
If anyone is using Saline or uses it in the future, please let me know. I’m probably not going to put much more work into it unless/until I know that people need feature or documentation improvements for it.