Skip to content
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

Merged
merged 9 commits into from
Nov 8, 2023
Merged

Bounded unsigned integer spells #1230

merged 9 commits into from
Nov 8, 2023

Conversation

Kukovec
Copy link
Contributor

@Kukovec Kukovec commented Oct 25, 2023

  • Tests added for any new code
  • Documentation added for any new functionality
  • Entries added to the respective CHANGELOG.md for any new functionality
  • Feature table on README.md updated for any listed functionality

coauthored w/ @thpani

@Kukovec Kukovec requested review from thpani, konnov and bugarela October 25, 2023 20:59
Copy link
Contributor

@shonfeder shonfeder left a 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/README.md Outdated Show resolved Hide resolved
/// 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 }
Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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)

Copy link
Contributor

@shonfeder shonfeder Oct 31, 2023

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

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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?

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think

UInt: int => UIntT

looks much nicer than

intToUInt: int => UInt

mainly because, if I have to type intToUInt for every integer literal in my spec, that's a lot of line-width bloat.
@konnov @thpani @bugarela

Copy link
Contributor

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 Show resolved Hide resolved
examples/spells/BoundedUInt.qnt Outdated Show resolved Hide resolved
examples/spells/BoundedUInt.qnt Outdated Show resolved Hide resolved
examples/spells/BoundedUInt.qnt Outdated Show resolved Hide resolved
examples/spells/BoundedUInt.qnt Outdated Show resolved Hide resolved
Comment on lines 146 to 164
///////////////////////////
// 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: ""
}
}
Copy link
Contributor

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?

Copy link
Collaborator

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.

Copy link
Contributor Author

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)

Copy link
Contributor

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.

Copy link
Contributor Author

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)

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ways are "chainable"

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

examples/spells/BoundedUInt.qnt Outdated Show resolved Hide resolved
Comment on lines +265 to +266
<!-- !test check BoundedUint8 - Syntax/Types & Effects/Unit tests -->
quint test --main=BoundedUInt8Test ../examples/spells/BoundedUInt.qnt
Copy link
Contributor

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?

Copy link
Contributor

@konnov konnov left a 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 Show resolved Hide resolved
Comment on lines 146 to 164
///////////////////////////
// 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: ""
}
}
Copy link
Contributor

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

Copy link
Collaborator

@bugarela bugarela left a 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 💅

Copy link
Contributor

@shonfeder shonfeder left a 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 :)

@Kukovec Kukovec merged commit 3b0209c into main Nov 8, 2023
15 checks passed
@Kukovec Kukovec deleted the jk/ciWeek branch November 8, 2023 14:26
@Kukovec Kukovec restored the jk/ciWeek branch November 8, 2023 14:26
@Kukovec Kukovec deleted the jk/ciWeek branch November 8, 2023 14:26
@Kukovec Kukovec restored the jk/ciWeek branch November 8, 2023 14:26
@Kukovec Kukovec deleted the jk/ciWeek branch November 8, 2023 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants