-
Notifications
You must be signed in to change notification settings - Fork 38
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
Bounded unsigned integer spells #1230
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few questions and small suggestions from me :)
Generally looks great.
examples/spells/BoundedUInt.qnt
Outdated
/// If the `error` field is nonempty, the record represents an exception of some | ||
/// sort happening during computation, such as an overflow. | ||
/// Otherwise, the record represents the integer `v`, such that `MIN <= v <= MAX`. | ||
type T = { v: int, error: str } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like UInt
would be a more descriptive type name. Otherwise it is impractical to import the module unqualified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if actual use case needs an error
message string. Would it be possible that having error: bool
would be more ergonomic and just knowing that there is an error one way or the other would suffice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can rename it UintT
(UInt
clashes with the constructor). As for the error field, I prefer keeping it. You can always call i.isValid
, and get a boolean evaluation, whereas the error string is more descriptive (and already implemented)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor could be intToUInt
. I think this is how we are making converters of, e.g., maps to sets: https://github.com/informalsystems/quint/blob/main/quint/src/builtin.qnt#L310
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if that article is supposed to be an example of nomenclature, or if you mis-pasted it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Miss pasted! Sorry. That should be https://github.com/informalsystems/quint/blob/main/quint/src/builtin.qnt#L310
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if actual use case needs an
error
message string. Would it be possible that havingerror: bool
would be more ergonomic and just knowing that there is an error one way or the other would suffice?
The error
field is similar to what was done with the modelling of Decimals in https://github.com/informalsystems/quint-sandbox/blob/main/decimal/decimal.qnt. While a Boolean would suffice, having a string is almost as cheap (from the analysis p.o.v.) as having a Boolean, and it it produces better counterexamples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with leaving it as an error message if that has proven useful.
I think the remaining item left for this comment is then the type name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, it looks nicer. If we go with an interface that requires wrapping values, makes sense to keep it short.
examples/spells/BoundedUInt.qnt
Outdated
/////////////////////////// | ||
// SATURATING OPERATIONS // | ||
/////////////////////////// | ||
|
||
/// Unsafe saturating integer addition. | ||
/// Computes `lhs + rhs`, saturating at the numeric bounds instead of overflowing. | ||
/// Assumes lhs and rhs are valid (w.r.t. `isValid`). | ||
pure def saturatingAddUnsafe(lhs: T, rhs: T): T = { | ||
val res = lhs.v + rhs.v | ||
{ | ||
v: if (res < MAX) res else MAX, | ||
error: "" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these don't error out, would it not make more sense to just have it defined over int
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the same thing. Seems to me that we'd want all saturating
and wrapping
operators to be defined over int
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an interface question. You want these operations to all be (UIntT, UIntT) => UIntT
so you can chain them.
If this returned an int, you couldn't do e.g. saturatingAdd(saturatingAdd(a,b), c)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are thinking the signature should be (int, int) => int
, so that would chain fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For saturating
alone, yes, but you couldn't combine e.g. saturatingAdd(checkedAdd(a,b), c)
(which was supposed to be the example above, but I borked it in copypaste)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure I understand the above, you also prefer
saturatingAdd: (int, int) => int
checkedAdd: (int, int) => T
?
We can do it like that, but I'll just reiterate #1230 (comment), doing this allows for "nonsensical" (in the unsigned-int universe) constructions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both ways are "chainable"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our goal here should be brief, self-explanatory specs
I agree with that goal! But its worth noting that requiring the saturatinAdd
arguments and return value to be wrapped introduces its own complexity, and leaving them as type int
makes it clearer when interacting with other int operators, like say the predicates or indexing operations.
Wrapping everything in the UInt
type, whether or not it is needed, will make it easier to chain these operators and that is clearer, but at the expense of every other point where the returned or supplied values have to be cast to/from an int.
An additional benefit of expecting this to use Option
combinators for chaining (once sum types are in, but using a makeshift maybe in the meantime) is that it makes it self-explanatory to readers that one is chaining partial computations.
Some example use cases using the two different approaches may be illustrative to track the tradeoffs. Do we have any code in mind that would be a good test case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least from the code where this would be useful that I've seen (Rust in Cosmos, bounded integers in smart contracts), you would firmly stay within the bounded fragment. I've never seen an unwrap to integers.
For this purpose, I believe that a single, initial wrapping of Quint's native int
into a UInt
, and then simply remaining in UInt
, is the most practical.
But I'm happy to consider other examples, or if we have none atm, revise this library when necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
<!-- !test check BoundedUint8 - Syntax/Types & Effects/Unit tests --> | ||
quint test --main=BoundedUInt8Test ../examples/spells/BoundedUInt.qnt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests should get run in the examples dashboard. Whats the added value of adding an additional test here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left a few comments
examples/spells/BoundedUInt.qnt
Outdated
/////////////////////////// | ||
// SATURATING OPERATIONS // | ||
/////////////////////////// | ||
|
||
/// Unsafe saturating integer addition. | ||
/// Computes `lhs + rhs`, saturating at the numeric bounds instead of overflowing. | ||
/// Assumes lhs and rhs are valid (w.r.t. `isValid`). | ||
pure def saturatingAddUnsafe(lhs: T, rhs: T): T = { | ||
val res = lhs.v + rhs.v | ||
{ | ||
v: if (res < MAX) res else MAX, | ||
error: "" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, chaining would be good IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love the new tests! Thank you for all the changes 💅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making the tests clearer :)
CHANGELOG.md
for any new functionalityREADME.md
updated for any listed functionalitycoauthored w/ @thpani