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

Proper Int64 #171

Merged
merged 23 commits into from
May 26, 2022
Merged

Proper Int64 #171

merged 23 commits into from
May 26, 2022

Conversation

mitschabaude
Copy link
Collaborator

@mitschabaude mitschabaude commented May 23, 2022

  • Replace unfinished, unconstrained Int64 with a proper implementation
  • Closes Finish Int64 implementation #66
  • Make Int64 API less boilerplate-y by allowing number, string, etc as method arguments
  • Remove some hacks in toJson conversion, possible bc the new Int64 has the same layout as Currency.Amount.Signed.t

This is stacked on top of #164, so that it can:

  • Make previously failing Int64 unit tests work
  • Add more Int64 unit tests

Since this will use updated bindings from MinaProtocol/mina#11042, which fix an aspect of the Field constructor, this also adds a few new tests for the Field constructor.

@mitschabaude mitschabaude marked this pull request as ready for review May 23, 2022 09:03
@mitschabaude mitschabaude changed the base branch from main to feature/more-tests May 23, 2022 10:48
@mitschabaude mitschabaude changed the base branch from feature/more-tests to main May 23, 2022 10:53
@mitschabaude mitschabaude changed the base branch from main to feature/more-tests May 23, 2022 10:53
src/lib/int.ts Outdated
return Int64.sizeInFields();
function argToField(
name: string,
x: { value: Field } | number | string
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: add range checks (not constraints, just checks) on constant UInt64 / UInt32 / Int64 as you create them

src/lib/int.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@mimoo mimoo left a comment

Choose a reason for hiding this comment

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

This is awesome! Code is clear, and this looks good to me, left a few minor comments

src/lib/int.ts Outdated Show resolved Hide resolved
src/lib/int.ts Outdated Show resolved Hide resolved
src/lib/int.ts Show resolved Hide resolved
src/lib/int.ts Outdated Show resolved Hide resolved
src/lib/int.ts Outdated Show resolved Hide resolved
return new Int64(this.value.neg());
add(y: Int64 | number | string | bigint | UInt64 | UInt32) {
let y_ = argToInt64(y);
return Int64.fromField(this.toField().add(y_.toField()));
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 thinking that with lookups, it'd be easier to just represent the Int64 as a Field and do range checks after every operation to make sure that 2^64 * res is in a [0, 2^128] range. To contrast:

  • that would be 2 range checks (because our range is smaller than 2^128) and 1 constraint to shift the value being looked up. Including the addition that's 4 constraints)
  • This PR's implementation is:
    • 2 direct toField (2 constraints)
    • fromField is 1 toField, 1 assertEquals, 1 Witness
    • the Witness will also call check which will call 1 square and 1 assertEquals,
    • check also calls Uint64.Check, which is actually quite heavy due to the lack of lookups as well. But we have to imagine that this would be just two lookup constraints at most eventually (with range lookups)
    • so that's 8-9 constraints

Not sure if I'm reading this correctly, or if I'm missing something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I think you may have a point.. I might reconsider this when range checks become cheap.

Do you mean shifting with 2^64 + res and range-checking that? I don't follow how it works with multiplication & negative numbers.

Currently, it's convenient to have Int64 be {magnitude, sign} because that mirrors the Ocaml data structure. If I make it a single Field constrained to 64 bits, I allow a slightly smaller range than the Ocaml type (64 vs 65 bits)

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah + not * sorry

src/lib/int.ts Outdated
if (typeof x === 'bigint') return Int64.fromBigInt(x);
if (x instanceof UInt64 || x instanceof UInt32) return Int64.fromUnsigned(x);
return x;
}
Copy link
Contributor

@mimoo mimoo May 23, 2022

Choose a reason for hiding this comment

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

why is this not implemented on Int64? Something like Int64.from(x: number | string | bigint | UInt64 | UInt32 | Int64).

and same issue here, the fromNumber and other similar functions don't error if the user tries to input values that are too large (or too small)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

great suggestion!

src/lib/int.ts Outdated
@@ -53,7 +42,7 @@ export class UInt64 extends CircuitValue {

static NUM_BITS = 64;

divMod(y: UInt64 | number): [UInt64, UInt64] {
divMod(y: UInt64 | number | string): [UInt64, UInt64] {
Copy link
Contributor

Choose a reason for hiding this comment

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

this return type is a bit error prone, why not return something like { quotient, rest } instead?

Especially when we do things like let [q] = or let [, r] =

throw 'repr';
equals(y: Int64 | number | string | bigint | UInt64 | UInt32) {
let y_ = argToInt64(y);
return this.toField().equals(y_.toField());
Copy link
Contributor

@mimoo mimoo May 23, 2022

Choose a reason for hiding this comment

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

would this.magnitude.equals(y_.magnitude).and(this.sign.equals(y_.sign) be better? The two equalities could be optimized by the compiler (using the permutation), and thus you'd only have one constraint (the and)

actually never mind, you can't optimize these with the permutation as you don't assert. But maybe the assertEquals could use something like this instead

this.magnitude.assertEquals(y_.magnitude);
this.sign.assertEquals(y_.sign);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't do that because I was worried about special-casing for 0, where -0 should equal 0.. The special-casing would also require multiplication constraints I think

Copy link
Contributor

Choose a reason for hiding this comment

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

good point :o

@mitschabaude
Copy link
Collaborator Author

@mimoo addressed all feedback,
also added Field.to/fromBigint() to make the code cleaner in some places

this should be good to go, will merge once the parent PR is merged

@mitschabaude mitschabaude changed the base branch from feature/more-tests to main May 26, 2022 09:46
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.

Finish Int64 implementation Field constructor vs static methods
2 participants