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

chore!: remove U128 struct from stdlib #7529

Merged
merged 4 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 0 additions & 48 deletions docs/docs/noir/concepts/data_types/integers.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,54 +58,6 @@ fn main(x: i16, y: i16) {

Modulo operation is defined for negative integers thanks to integer division, so that the equality `x = (x/y)*y + (x%y)` holds.

## 128 bits Unsigned Integers

The built-in structure `U128` allows you to use 128-bit unsigned integers almost like a native integer type. However, there are some differences to keep in mind:
- You cannot cast between a native integer and `U128`
- There is a higher performance cost when using `U128`, compared to a native type.

Conversion between unsigned integer types and U128 are done through the use of `from_integer` and `to_integer` functions. `from_integer` also accepts the `Field` type as input.

```rust
fn main() {
let x = U128::from_integer(23);
let y = U128::from_hex("0x7");
let z = x + y;
assert(z.to_integer() == 30);
}
```

`U128` is implemented with two 64 bits limbs, representing the low and high bits, which explains the performance cost. You should expect `U128` to be twice more costly for addition and four times more costly for multiplication.
You can construct a U128 from its limbs:
```rust
fn main(x: u64, y: u64) {
let z = U128::from_u64s_be(x,y);
assert(z.hi == x as Field);
assert(z.lo == y as Field);
}
```

Note that the limbs are stored as Field elements in order to avoid unnecessary conversions.
Apart from this, most operations will work as usual:

```rust
fn main(x: U128, y: U128) {
// multiplication
let c = x * y;
// addition and subtraction
let c = c - x + y;
// division
let c = x / y;
// bit operation;
let c = x & y | y;
// bit shift
let c = x << y;
// comparisons;
let c = x < y;
let c = x == y;
}
```

## Overflows

Computations that exceed the type boundaries will result in overflow errors. This happens with both signed and unsigned integers. For example, attempting to prove:
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/noir/standard_library/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ As a general rule of thumb, `From` may be implemented in the [situations where i
- The conversion is *infallible*: Noir does not provide an equivalent to Rust's `TryFrom`, if the conversion can fail then provide a named method instead.
- The conversion is *lossless*: semantically, it should not lose or discard information. For example, `u32: From<u16>` can losslessly convert any `u16` into a valid `u32` such that the original `u16` can be recovered. On the other hand, `u16: From<u32>` should not be implemented as `2**16` is a `u32` which cannot be losslessly converted into a `u16`.
- The conversion is *value-preserving*: the conceptual kind and meaning of the resulting value is the same, even though the Noir type and technical representation might be different. While it's possible to infallibly and losslessly convert a `u8` into a `str<2>` hex representation, `4u8` and `"04"` are too different for `str<2>: From<u8>` to be implemented.
- The conversion is *obvious*: it's the only reasonable conversion between the two types. If there's ambiguity on how to convert between them such that the same input could potentially map to two different values then a named method should be used. For instance rather than implementing `U128: From<[u8; 16]>`, the methods `U128::from_le_bytes` and `U128::from_be_bytes` are used as otherwise the endianness of the array would be ambiguous, resulting in two potential values of `U128` from the same byte array.
- The conversion is *obvious*: it's the only reasonable conversion between the two types. If there's ambiguity on how to convert between them such that the same input could potentially map to two different values then a named method should be used. For instance rather than implementing `u128: From<[u8; 16]>`, the methods `u128::from_le_bytes` and `u128::from_be_bytes` are used as otherwise the endianness of the array would be ambiguous, resulting in two potential values of `u128` from the same byte array.

One additional recommendation specific to Noir is:
- The conversion is *efficient*: it's relatively cheap to convert between the two types. Due to being a ZK DSL, it's more important to avoid unnecessary computation compared to Rust. If the implementation of `From` would encourage users to perform unnecessary conversion, resulting in additional proving time, then it may be preferable to expose functionality such that this conversion may be avoided.
Expand Down
11 changes: 0 additions & 11 deletions noir_stdlib/src/hash/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use crate::embedded_curve_ops::{
EmbeddedCurvePoint, EmbeddedCurveScalar, multi_scalar_mul, multi_scalar_mul_array_return,
};
use crate::meta::derive_via;
use crate::uint128::U128;

// Kept for backwards compatibility
pub use sha256::{digest, sha256, sha256_compression, sha256_var};
Expand Down Expand Up @@ -302,16 +301,6 @@ impl Hash for () {
{}
}

impl Hash for U128 {
fn hash<H>(self, state: &mut H)
where
H: Hasher,
{
H::write(state, self.lo as Field);
H::write(state, self.hi as Field);
}
}

impl<T, let N: u32> Hash for [T; N]
where
T: Hash,
Expand Down
39 changes: 38 additions & 1 deletion noir_stdlib/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pub mod cmp;
pub mod ops;
pub mod default;
pub mod prelude;
pub mod uint128;
pub mod runtime;
pub mod meta;
pub mod append;
Expand Down Expand Up @@ -121,8 +120,46 @@ where
pub fn as_witness(x: Field) {}

mod tests {
use super::wrapping_mul;

#[test(should_fail_with = "custom message")]
fn test_static_assert_custom_message() {
super::static_assert(1 == 2, "custom message");
}

#[test(should_fail)]
fn test_wrapping_mul() {
// This currently fails.
// See: https://github.com/noir-lang/noir/issues/7528
let zero: u128 = 0;
let one: u128 = 1;
let two_pow_64: u128 = 0x10000000000000000;
let u128_max: u128 = 0xffffffffffffffffffffffffffffffff;

// 1*0==0
assert_eq(zero, wrapping_mul(zero, one));

// 0*1==0
assert_eq(zero, wrapping_mul(one, zero));

// 1*1==1
assert_eq(one, wrapping_mul(one, one));

// 0 * ( 1 << 64 ) == 0
assert_eq(zero, wrapping_mul(zero, two_pow_64));

// ( 1 << 64 ) * 0 == 0
assert_eq(zero, wrapping_mul(two_pow_64, zero));

// 1 * ( 1 << 64 ) == 1 << 64
assert_eq(two_pow_64, wrapping_mul(two_pow_64, one));

// ( 1 << 64 ) * 1 == 1 << 64
assert_eq(two_pow_64, wrapping_mul(one, two_pow_64));

// ( 1 << 64 ) * ( 1 << 64 ) == 1 << 64
assert_eq(zero, wrapping_mul(two_pow_64, two_pow_64));
// -1 * -1 == 1
assert_eq(one, wrapping_mul(u128_max, u128_max));
}
}
1 change: 0 additions & 1 deletion noir_stdlib/src/prelude.nr
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ pub use crate::default::Default;
pub use crate::meta::{derive, derive_via};
pub use crate::option::Option;
pub use crate::panic::panic;
pub use crate::uint128::U128;
Loading
Loading