Skip to content

feat(experimental!): let the default integer type be i32 instead of Field#8965

Closed
asterite wants to merge 5 commits intomasterfrom
ab/default-integer-is-i32
Closed

feat(experimental!): let the default integer type be i32 instead of Field#8965
asterite wants to merge 5 commits intomasterfrom
ab/default-integer-is-i32

Conversation

@asterite
Copy link
Collaborator

Description

Problem

No reported issue, but having Field be the default integer type might lead to confusion.

Summary

Note: this is just an experiment to see how much code breaks with this change. There's also this bug that, when fixed, should lead to even less breakage (I'll try to fix that next).

The main issue with having integer literals default to Field is that negative literals are allowed, but Field is mainly an unsigned type. It's okay to allow negative literals for it, but that should only probably be allowed with an explicit type annotation, or if the literal destination is a Field. For example right now (-56) as i8 gives -55, because the field -56 is actually a very big positive number that, when casted to i8, gives -55. With the change in this PR, -56 is an i32, and casting that to i8 preserves the value -56. But regardless of this issue, it might make more sense for the default type to be a signed type because literals can be positive or negative.

The reasoning here is that because Noir, like Rust, requires type annotations in function signatures, struct types, etc., changing the default from Field to i32 shouldn't have a big impact, because if existing code expects Field, it will still use Field, and code would still compile.

For example:

fn foo(x: Field) { ... }

fn main() {
    foo(1)
}

Here it doesn't matter what that 1 defaults to, because the type will be bound to the call argument. It's the same case if that's assigned to a struct field, etc. The only issue right now is #8963 where this isn't true, but that's a bug that we can fix.

Then in isolated code the type can change. For example here:

let x = 1;
println(x);

That used to print 0x01, but now it prints 1 because x is i32 instead of Field. But that code is mostly used in tests. For example, only one change was required to be done to noir_stdlib to make it compile again, and it was in some test code related to macros.

In any case, this is just experimental! I understand that Field is more efficient than any other integer type, but my hope is that this change doesn't actually impact performance.

Additional Context

Documentation*

Check one:

  • No documentation needed.
  • Documentation included in this PR.
  • [For Experimental Features] Documentation to be submitted in a separate PR.

PR Checklist*

  • I have tested the changes locally.
  • I have formatted the changes with Prettier and/or cargo fmt on default settings.

@github-actions
Copy link
Contributor

github-actions bot commented Jun 18, 2025

Changes to number of Brillig opcodes executed

Generated at commit: 790fa295bec379ef5fb38add7b8ceb107c17ee14, compared to commit: 46abffdff8e42af7ae1e2477ff7493031739cc31

🧾 Summary (10% most significant diffs)

Program Brillig opcodes (+/-) %
loop_break_regression_8319_inliner_max +433 ❌ +95.16%
loop_break_regression_8319_inliner_zero +433 ❌ +95.16%
loop_break_regression_8319_inliner_min +433 ❌ +59.97%
comptime_variable_at_runtime_inliner_max +45 ❌ +56.96%
comptime_variable_at_runtime_inliner_min +45 ❌ +56.96%
comptime_variable_at_runtime_inliner_zero +45 ❌ +56.96%

Full diff report 👇
Program Brillig opcodes (+/-) %
loop_break_regression_8319_inliner_max 888 (+433) +95.16%
loop_break_regression_8319_inliner_zero 888 (+433) +95.16%
loop_break_regression_8319_inliner_min 1,155 (+433) +59.97%
comptime_variable_at_runtime_inliner_max 124 (+45) +56.96%
comptime_variable_at_runtime_inliner_min 124 (+45) +56.96%
comptime_variable_at_runtime_inliner_zero 124 (+45) +56.96%
break_and_continue_inliner_max 143 (+37) +34.91%
break_and_continue_inliner_min 143 (+37) +34.91%
break_and_continue_inliner_zero 143 (+37) +34.91%
slices_inliner_max 2,941 (+451) +18.11%
debug_logs_inliner_max 5,919 (+791) +15.43%
array_dynamic_inliner_max 467 (+61) +15.02%
array_dynamic_inliner_zero 467 (+61) +15.02%
array_dynamic_inliner_min 481 (+61) +14.52%
debug_logs_inliner_min 6,028 (+687) +12.86%
debug_logs_inliner_zero 6,028 (+687) +12.86%
higher_order_functions_inliner_min 1,968 (+156) +8.61%
slices_inliner_zero 2,963 (+227) +8.30%
strings_inliner_max 1,638 (+114) +7.48%
strings_inliner_zero 1,682 (+114) +7.27%
strings_inliner_min 1,696 (+114) +7.21%
array_dedup_regression_inliner_max 716 (+41) +6.07%
array_dedup_regression_inliner_min 716 (+41) +6.07%
array_dedup_regression_inliner_zero 716 (+41) +6.07%
references_inliner_zero 261 (+13) +5.24%
slices_inliner_min 3,981 (+179) +4.71%
references_inliner_min 403 (+17) +4.40%
higher_order_functions_inliner_zero 1,218 (+32) +2.70%
references_inliner_max 152 (+1) +0.66%
while_loop_break_regression_8521_inliner_max 180 (+1) +0.56%
while_loop_break_regression_8521_inliner_min 180 (+1) +0.56%
while_loop_break_regression_8521_inliner_zero 180 (+1) +0.56%

@github-actions
Copy link
Contributor

github-actions bot commented Jun 18, 2025

Changes to Brillig bytecode sizes

Generated at commit: 790fa295bec379ef5fb38add7b8ceb107c17ee14, compared to commit: 46abffdff8e42af7ae1e2477ff7493031739cc31

🧾 Summary (10% most significant diffs)

Program Brillig opcodes (+/-) %
comptime_variable_at_runtime_inliner_max +45 ❌ +56.96%
comptime_variable_at_runtime_inliner_min +45 ❌ +56.96%
comptime_variable_at_runtime_inliner_zero +45 ❌ +56.96%
loop_break_regression_8319_inliner_max +24 ❌ +50.00%
loop_break_regression_8319_inliner_zero +24 ❌ +50.00%

Full diff report 👇
Program Brillig opcodes (+/-) %
comptime_variable_at_runtime_inliner_max 124 (+45) +56.96%
comptime_variable_at_runtime_inliner_min 124 (+45) +56.96%
comptime_variable_at_runtime_inliner_zero 124 (+45) +56.96%
loop_break_regression_8319_inliner_max 72 (+24) +50.00%
loop_break_regression_8319_inliner_zero 72 (+24) +50.00%
break_and_continue_inliner_max 70 (+14) +25.00%
break_and_continue_inliner_min 70 (+14) +25.00%
break_and_continue_inliner_zero 70 (+14) +25.00%
loop_break_regression_8319_inliner_min 243 (+34) +16.27%
array_dedup_regression_inliner_max 300 (+41) +15.83%
array_dedup_regression_inliner_min 300 (+41) +15.83%
array_dedup_regression_inliner_zero 300 (+41) +15.83%
debug_logs_inliner_max 5,907 (+791) +15.46%
slices_inliner_max 1,938 (+254) +15.08%
higher_order_functions_inliner_min 1,272 (+150) +13.37%
debug_logs_inliner_min 6,004 (+687) +12.92%
debug_logs_inliner_zero 6,004 (+687) +12.92%
strings_inliner_zero 1,007 (+114) +12.77%
strings_inliner_min 1,017 (+114) +12.62%
strings_inliner_max 1,031 (+114) +12.43%
conditional_regression_short_circuit_inliner_max 229 (+25) +12.25%
conditional_regression_short_circuit_inliner_zero 229 (+25) +12.25%
conditional_regression_short_circuit_inliner_min 256 (+25) +10.82%
slices_inliner_zero 1,757 (+135) +8.32%
while_loop_break_regression_8521_inliner_max 190 (+14) +7.95%
while_loop_break_regression_8521_inliner_min 190 (+14) +7.95%
while_loop_break_regression_8521_inliner_zero 190 (+14) +7.95%
slices_inliner_min 2,299 (+139) +6.44%
references_inliner_zero 157 (+9) +6.08%
references_inliner_min 241 (+11) +4.78%
higher_order_functions_inliner_zero 683 (+31) +4.75%
array_dynamic_inliner_max 313 (+14) +4.68%
array_dynamic_inliner_zero 313 (+14) +4.68%
array_dynamic_inliner_min 323 (+14) +4.53%
references_inliner_max 84 (+1) +1.20%

@github-actions
Copy link
Contributor

github-actions bot commented Jun 18, 2025

Changes to circuit sizes

Generated at commit: 790fa295bec379ef5fb38add7b8ceb107c17ee14, compared to commit: 46abffdff8e42af7ae1e2477ff7493031739cc31

🧾 Summary (10% most significant diffs)

Program ACIR opcodes (+/-) % Circuit size (+/-) %
array_dynamic +97 ❌ +75.19% +82 ❌ +2.19%

Full diff report 👇
Program ACIR opcodes (+/-) % Circuit size (+/-) %
array_dynamic 226 (+97) +75.19% 3,824 (+82) +2.19%

Copy link
Contributor

@jfecher jfecher left a comment

Choose a reason for hiding this comment

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

I agree that (-56) as i8 = -55 is confusing but I think defaulting to Field is still sensible from an efficiency standpoint as well as a "what type is most commonly used?" standpoint.

I also think we should keep allowing negative literals for field since negatives are still consistent within field operations. E.g. -5 + 7 = 2. (If anything I think field division is more confusing).

If we don't want (-56) as i8 = -55 as the expected semantics then I'd lean toward altering the field to signed int cast instead. We could add a branch in the cast which would make it slower but perhaps more expected. This may be fine for the signed integer types which are rarely used today:

// This would be built-in, but just to show the logic:
fn field_to_i8(x: Field) -> Option<i8> {
    // Absolute value of i8::MIN
    let abs_min = pow(2, 7);
    // I may be off by 1 here, don't quote me
    if x >= -abs_min {
        truncate(x + abs_min)
    } else {
        truncate(x)
    }
}

@asterite
Copy link
Collaborator Author

If we don't want (-56) as i8 = -55 as the expected semantics then I'd lean toward altering the field to signed int cast instead.

I agree, we could do that. However, I'd find it a bit odd that (-56) as i8 would give -56 but 21888242871839275222246405745257275088548364400416034343698204186575808495561 as i8 would also give -56... when both are the "same" number, except that the latter would be considered the negative version.

Maybe the odd thing is that printing the Field -1 shows 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffffc9? That is, we allow negative fields literals, we even have some branching logic in our codebase that field values above a certain value are considered negative, but then -1 isn't actually shown like that. If we changed Field to be like a signed type, maybe that would solve these issues? For example casting the i8 -1 to Field should give you -1, not 0 (as I think you proposed a while ago).

@asterite
Copy link
Collaborator Author

The above would also mean that this could shouldn't compile:

fn main() {
    let x = 21888242871839275222246405745257275088548364400416034343698204186575808495616;
    println(x);
}

as 21888242871839275222246405745257275088548364400416034343698204186575808495616 is currently the max field value, but if we changed it to be treated like a signed integer then the maximum value would be about half of that.

@jfecher
Copy link
Contributor

jfecher commented Jun 18, 2025

@asterite I don't think we should dedicate bit patterns in field to be exclusively negative unless we wanted to add it as a separate signed field type.

For the printing discrepancy, I just think we're going to have to draw the line somewhere.

  • It could be "negative field values are actually positive, so when printing they appear as large positive numbers,"
  • It could be "large/negative field values cannot be cast to signed integers (they give a range constraint error)"
  • or somewhere else

@asterite
Copy link
Collaborator Author

Thanks!

I'll close this in the meantime. It doesn't seem this change breaks a lot of things, which is what I wanted to know when creating this PR. The compilation errors are:

  1. Large integer literals that were previously inferred to Field now don't fit and would need an explicit cast or type annotation
  2. Issues because of the bug in Integer type not correctly inferred when unrelated trait impl exists #8963

@asterite asterite closed this Jun 18, 2025
@asterite asterite deleted the ab/default-integer-is-i32 branch June 18, 2025 16:05
@asterite
Copy link
Collaborator Author

I agree that (-56) as i8 = -55 is confusing but I think defaulting to Field is still sensible from an efficiency standpoint as well as a "what type is most commonly used?" standpoint.

@jfecher I kept thinking about this and wondering if this is still true today:

#10096

I understand that Field is the most efficient type, but according to the above PR we want to discourage users from using Field.

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.

2 participants