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

feat: decompose Instruction::Constrain into multiple more basic constraints #3892

Merged
merged 26 commits into from
Jan 10, 2024

Conversation

TomAFrench
Copy link
Member

@TomAFrench TomAFrench commented Dec 20, 2023

Description

Problem*

Some experimentation related to #3782.

Summary*

If we switch to an unary constrain instruction then we will want to be able to decompose an assertion on a composite type to perform assertions directly on its fields. Otherwise we have to create a predicate for each field and then collect these all together before constraining that.

For example consider this program which simulates an unary constrain

fn main(x: [Field; 2], y: [Field; 2]) {
    let equal = x == y;
    assert(equal);
}

Currently this produces:

acir fn main f0 {
  b0(v0: [Field; 2], v1: [Field; 2]):
    v66 = array_get v0, index Field 0
    v67 = array_get v1, index Field 0
    v68 = eq v66, v67
    v69 = array_get v0, index Field 1
    v70 = array_get v1, index Field 1
    v71 = eq v69, v70
    v72 = mul v68, v71
    constrain v72 == u1 1
    return 
}

Compiled ACIR for main (unoptimized):
current witness index : 10
public parameters indices : []
return value indices : []
EXPR [ (-1, _1) (1, _3) (-1, _5) 0 ]
BRILLIG: inputs: [Single(Expression { mul_terms: [], linear_combinations: [(1, Witness(5))], q_c: 0 })]
outputs: [Simple(Witness(6))]
[JumpIfNot { condition: RegisterIndex(0), location: 3 }, Const { destination: RegisterIndex(1), value: Value { inner: 1 } }, BinaryFieldOp { destination: RegisterIndex(0), op: Div, lhs: RegisterIndex(1), rhs: RegisterIndex(0) }, Stop]

EXPR [ (1, _5, _6) (1, _7) -1 ]
EXPR [ (1, _5, _7) 0 ]
EXPR [ (-1, _2) (1, _4) (-1, _8) 0 ]
BRILLIG: inputs: [Single(Expression { mul_terms: [], linear_combinations: [(1, Witness(8))], q_c: 0 })]
outputs: [Simple(Witness(9))]
[JumpIfNot { condition: RegisterIndex(0), location: 3 }, Const { destination: RegisterIndex(1), value: Value { inner: 1 } }, BinaryFieldOp { destination: RegisterIndex(0), op: Div, lhs: RegisterIndex(1), rhs: RegisterIndex(0) }, Stop]

EXPR [ (1, _8, _9) (1, _10) -1 ]
EXPR [ (1, _8, _10) 0 ]
EXPR [ (1, _7, _10) -1 ]

After this PR we can recover the current optimisations for assert(x == y)

acir fn main f0 {
  b0(v0: [Field; 2], v1: [Field; 2]):
    v66 = array_get v0, index Field 0
    v67 = array_get v1, index Field 0
    v68 = array_get v0, index Field 1
    v69 = array_get v1, index Field 1
    constrain v66 == v67
    constrain v68 == v69
    return 
}

Compiled ACIR for main (unoptimized):
current witness index : 4
public parameters indices : []
return value indices : []
EXPR [ (1, _1) (-1, _3) 0 ]
EXPR [ (1, _2) (-1, _4) 0 ]

Additional Context

Documentation*

Check one:

  • No documentation needed.
  • Documentation included in this PR.
  • [Exceptional Case] 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.

Copy link
Contributor

github-actions bot commented Dec 20, 2023

Changes to circuit sizes

Generated at commit: 01c01fc456913fe863967ed5df30d057ce604e85, compared to commit: cc3c2c22644f0b5d8369bad2362ea6e9112a0713

🧾 Summary (10% most significant diffs)

Program ACIR opcodes (+/-) % Circuit size (+/-) %
simple_2d_array -7 ✅ -63.64% -5 ✅ -35.71%
brillig_blake2s -127 ✅ -64.47% -95 ✅ -42.60%
brillig_sha256 -127 ✅ -66.15% -95 ✅ -43.78%
array_eq -127 ✅ -79.87% -95 ✅ -71.97%

Full diff report 👇
Program ACIR opcodes (+/-) % Circuit size (+/-) %
6 70 (-127) -64.47% 38,799 (0) 0.00%
7 70 (-127) -64.47% 19,350 (0) 0.00%
blake3 70 (-127) -64.47% 18,774 (0) 0.00%
conditional_regression_short_circuit 96 (-127) -56.95% 38,799 (0) 0.00%
ecdsa_secp256k1 233 (-127) -35.28% 41,049 (0) 0.00%
keccak256 76 (-127) -62.56% 54,830 (0) 0.00%
regression 207 (-19) -8.41% 4,130 (0) 0.00%
sha256 75 (-127) -62.87% 38,799 (0) 0.00%
strings 69 (-49) -41.53% 14,373 (0) 0.00%
sha2_byte 40,133 (-382) -0.94% 154,258 (-286) -0.19%
slice_struct_field 33,241 (-583) -1.72% 104,584 (-424) -0.40%
to_be_bytes 83 (0) 0.00% 167 (-1) -0.60%
tuple_inputs 48 (-33) -40.74% 3,678 (-24) -0.65%
u128 857 (-43) -4.78% 4,689 (-31) -0.66%
nested_slice_dynamic 1,700 (-113) -6.23% 12,548 (-83) -0.66%
nested_array_dynamic 4,450 (-202) -4.34% 13,845 (-151) -1.08%
struct_inputs 27 (-48) -64.00% 2,840 (-41) -1.42%
brillig_keccak 132 (-127) -49.03% 3,062 (-95) -3.01%
bit_and 24 (-28) -53.85% 4,106 (-374) -8.35%
simple_2d_array 4 (-7) -63.64% 9 (-5) -35.71%
brillig_blake2s 70 (-127) -64.47% 128 (-95) -42.60%
brillig_sha256 65 (-127) -66.15% 122 (-95) -43.78%
array_eq 32 (-127) -79.87% 37 (-95) -71.97%

@TomAFrench
Copy link
Member Author

TomAFrench commented Dec 21, 2023

One trouble I'm running into is that we can only propagate the simplification back one level per constant folding pass. In general we need to be able to propagate back simplifications as many levels as there are fields in the type being constrained.

We could just rerun the constant folding pass until it stabilises but that's slow and can result in memory issues. One solution that seems plausible is recursively inspect all of the precursor instructions to the Constrain to build up a full list of requirements, generate all of the primitive Constrains which made up the original and then rely on DIE to clean up everything. We may need some special sauce to apply the constrained values within a "side-effect-enabled block" to do this satisfactorily however.

@TomAFrench
Copy link
Member Author

TomAFrench commented Dec 21, 2023

The increase in constraints for to_be_bytes comes from the fact that we now take the snippet

if (bytes[30] != 60) | (bytes[29] != 33) | (bytes[28] != 31) {
    assert(false);
}

which has the SSA

v679 = array_get v621, index u64 28
v680 = lt u64 29, v620
constrain v680 == u1 1 'Index out of bounds'
v681 = array_get v621, index u64 29
v682 = lt u64 30, v620
constrain v682 == u1 1 'Index out of bounds'
v683 = array_get v621, index u64 30
v684 = eq v683, u8 60
v685 = not v684
v686 = eq v681, u8 33
v687 = not v686
v688 = or v685, v687
v689 = eq v679, u8 31
v690 = not v689
v691 = or v688, v690
enable_side_effects v691
constrain u1 0 == v691
enable_side_effects u1 1

And seeing that this is equivalent to constraining the values used to calculate v691 such that we never enter that if-branch.

v681 = array_get v621, index u64 29
v682 = lt u64 30, v620
constrain v682 == u1 1 'Index out of bounds'
v683 = array_get v621, index u64 30
v684 = eq v683, u8 60
v685 = not v684
v686 = eq v681, u8 33
v687 = not v686
v688 = or v685, v687
v689 = eq v679, u8 31
v690 = not v689
v691 = or v688, v690
enable_side_effects v691
constrain v683 == u8 60
constrain v681 == u8 33
constrain v679 == u8 31
enable_side_effects u1 1

We should be able to remove v684-v691 with DIE but the enable_side_effects instruction prevents this, we could get around this with a pass which removes these instructions if they're not followed by another instructions which uses the condition value before the next enable_side_effects instruction We can't remove this cleanly but this is also a fairly non-idiomatic piece of noir code.

@TomAFrench TomAFrench changed the title feat: decompose constraints on boolean multiplications feat: decompose constraints into set Dec 21, 2023
@TomAFrench TomAFrench changed the title feat: decompose constraints into set feat: decompose constraints into se Dec 21, 2023
@TomAFrench TomAFrench changed the title feat: decompose constraints into se feat: decompose Instruction::Constrain into multiple more basic constraints Dec 21, 2023
Copy link
Member Author

@TomAFrench TomAFrench left a comment

Choose a reason for hiding this comment

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

cc @jfecher, do you have any issues with this PR in principle (thinking about how we're returning multiple instructions to insert)? I'll clean up the actual implementation and mark ready for review if not

compiler/noirc_evaluator/src/ssa/ir/dfg.rs Outdated Show resolved Hide resolved
@jfecher
Copy link
Contributor

jfecher commented Jan 2, 2024

cc @jfecher, do you have any issues with this PR in principle (thinking about how we're returning multiple instructions to insert)? I'll clean up the actual implementation and mark ready for review if not

This PR is interesting and a good optimization if we can get it to work, but I worry that it will be obsolete once we have operator overloading for == and start disallowing primitive == on arrays which has always been broken (it does not check if the element type implements Eq). When array == becomes a normal function call this optimization becomes more difficult I think. That being said, if we could get it to work for all == method calls somehow, that'd be great.

@TomAFrench
Copy link
Member Author

TomAFrench commented Jan 2, 2024

I worry that it will be obsolete once we have operator overloading for == and start disallowing primitive == on arrays which has always been broken (it does not check if the element type implements Eq). When array == becomes a normal function call this optimization becomes more difficult I think.

This should be fine as long as we are still in-lining the eq function in SSA. There's nothing tying this optimization to array equalities as they're currently performed, it's generic over any constraint so will work for arbitrary types including structs.

I've added an integration test to show this which compiles down to

Compiled ACIR for main (unoptimized):
current witness index : 5
public parameters indices : []
return value indices : []
EXPR [ (1, _2, _2) (-1, _2) 0 ]
BLACKBOX::RANGE [(_3, num_bits: 8)] [ ]
BLACKBOX::RANGE [(_4, num_bits: 8)] [ ]
BLACKBOX::RANGE [(_5, num_bits: 8)] [ ]
EXPR [ (1, _1) -42 ]
EXPR [ (1, _2) -1 ]
EXPR [ (1, _3) -1 ]
EXPR [ (1, _4) -2 ]
EXPR [ (1, _5) -3 ]

@TomAFrench
Copy link
Member Author

@jfecher @kevaundray This should be good for a final review now.

@TomAFrench
Copy link
Member Author

Bumping this.

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.

👍

@TomAFrench TomAFrench added this pull request to the merge queue Jan 10, 2024
Merged via the queue into master with commit 51cf9d3 Jan 10, 2024
30 checks passed
@TomAFrench TomAFrench deleted the tf/decompose-asserted-boolean-multiplications branch January 10, 2024 20:32
TomAFrench added a commit that referenced this pull request Jan 11, 2024
* master:
  chore: quiet down aztec-bot! (#4006)
  chore: force public functions for trait implementation (#3986)
  feat: docker testing flow (#3895)
  feat: decompose `Instruction::Constrain` into multiple more basic constraints (#3892)
  feat: remove truncation from brillig casts (#3997)
  chore: add test case showing reexports (#4001)
TomAFrench added a commit that referenced this pull request Jan 11, 2024
* master:
  chore(ci): disable commit comments on docs publishing (#3998)
  feat: simplify chains of casts to be all in terms of the original `ValueId` (#3984)
  chore: quiet down aztec-bot! (#4006)
  chore: force public functions for trait implementation (#3986)
  feat: docker testing flow (#3895)
  feat: decompose `Instruction::Constrain` into multiple more basic constraints (#3892)
  feat: remove truncation from brillig casts (#3997)
  chore: add test case showing reexports (#4001)
  feat: remove truncations which can be seen to be noops using type information (#3953)
  chore: remove homemade keccakf1600 implementation (#3996)
TomAFrench added a commit that referenced this pull request Jan 12, 2024
* master: (285 commits)
  feat: Add `assert_max_bit_size` method to `Field` (#4016)
  fix: fixup exports from `noir_wasm` (#4022)
  chore(ci): fix docker CI (#4014)
  feat(lsp): goto trait method declaration (#3991)
  chore: add action to check for dead links (#3992)
  chore: improve cspell setup (#3985)
  feat: Add dependency resolver for `noir_wasm` and implement `FileManager` for consistency with native interface (#3891)
  chore(docs): devcontainer docs (#4007)
  chore(ci): disable commit comments on docs publishing (#3998)
  feat: simplify chains of casts to be all in terms of the original `ValueId` (#3984)
  chore: quiet down aztec-bot! (#4006)
  chore: force public functions for trait implementation (#3986)
  feat: docker testing flow (#3895)
  feat: decompose `Instruction::Constrain` into multiple more basic constraints (#3892)
  feat: remove truncation from brillig casts (#3997)
  chore: add test case showing reexports (#4001)
  feat: remove truncations which can be seen to be noops using type information (#3953)
  chore: remove homemade keccakf1600 implementation (#3996)
  feat!: Breaking changes from aztec-packages (#3955)
  chore(docs): small grammar fix on the landing page (#3989)
  ...
github-merge-queue bot pushed a commit that referenced this pull request Jan 22, 2024
🤖 I have created a release *beep* *boop*
---


<details><summary>0.23.0</summary>

## [0.23.0](v0.22.0...v0.23.0)
(2024-01-22)


### ⚠ BREAKING CHANGES

* Ban nested slices
([#4018](#4018))
* Breaking changes from aztec-packages
([#3955](#3955))
* Rename Arithmetic opcode to AssertZero
([#3840](#3840))
* remove circuit methods from noir_wasm
([#3869](#3869))

### Features

* Add `assert_max_bit_size` method to `Field`
([#4016](#4016))
([bc9a44f](bc9a44f))
* Add `noir-compiler` checks to `aztec_macros`
([#4031](#4031))
([420a5c7](420a5c7))
* Add a `--force` flag to force a full recompile
([#4054](#4054))
([27a8e68](27a8e68))
* Add dependency resolver for `noir_wasm` and implement `FileManager`
for consistency with native interface
([#3891](#3891))
([c29c7d7](c29c7d7))
* Add foreign call support to `noir_codegen` functions
([#3933](#3933))
([e5e52a8](e5e52a8))
* Add MVP `nargo export` command
([#3870](#3870))
([fbb51ed](fbb51ed))
* Add support for codegenning multiple functions which use the same
structs in their interface
([#3868](#3868))
([1dcfcc5](1dcfcc5))
* Added efficient field comparisons for bn254
([#4042](#4042))
([1f9cad0](1f9cad0))
* Assert maximum bit size when creating a U128 from an integer
([#4024](#4024))
([8f9c7e4](8f9c7e4))
* Avoid unnecessary range checks by inspecting instructions for casts
([#4039](#4039))
([378c18e](378c18e))
* Breaking changes from aztec-packages
([#3955](#3955))
([5be049e](5be049e))
* Bubble up `Instruction::Constrain`s to be applied as early as
possible. ([#4065](#4065))
([66f5cdd](66f5cdd))
* Cached LSP parsing
([#4083](#4083))
([b4f724e](b4f724e))
* Comparison for signed integers
([#3873](#3873))
([bcbd49b](bcbd49b))
* Decompose `Instruction::Cast` to have an explicit truncation
instruction ([#3946](#3946))
([35f18ef](35f18ef))
* Decompose `Instruction::Constrain` into multiple more basic
constraints ([#3892](#3892))
([51cf9d3](51cf9d3))
* Docker testing flow
([#3895](#3895))
([179c90d](179c90d))
* Extract parsing to its own pass and do it in parallel
([#4063](#4063))
([569cbbc](569cbbc))
* Implement `Eq` trait on curve points
([#3944](#3944))
([abf751a](abf751a))
* Implement DAP protocol in Nargo
([#3627](#3627))
([13834d4](13834d4))
* Implement generic traits
([#4000](#4000))
([916fd15](916fd15))
* Implement Operator Overloading
([#3931](#3931))
([4b16090](4b16090))
* **lsp:** Cache definitions for goto requests
([#3930](#3930))
([4a2140f](4a2140f))
* **lsp:** Goto global
([#4043](#4043))
([15237b3](15237b3))
* **lsp:** Goto struct member inside Impl method
([#3918](#3918))
([99c2c5a](99c2c5a))
* **lsp:** Goto trait from trait impl
([#3956](#3956))
([eb566e2](eb566e2))
* **lsp:** Goto trait method declaration
([#3991](#3991))
([eb79166](eb79166))
* **lsp:** Goto type alias
([#4061](#4061))
([dc83385](dc83385))
* **lsp:** Goto type definition
([#4029](#4029))
([8bb4ddf](8bb4ddf))
* **lsp:** Re-add code lens feature with improved performance
([#3829](#3829))
([8f5cd6c](8f5cd6c))
* Optimize array ops for arrays of structs
([#4027](#4027))
([c9ec0d8](c9ec0d8))
* Optimize logic gate ACIR-gen
([#3897](#3897))
([926460a](926460a))
* Prefer `AcirContext`-native methods for performing logic operations
([#3898](#3898))
([0ec39b8](0ec39b8))
* Remove range constraints from witnesses which are constrained to be
constants ([#3928](#3928))
([afe9c7a](afe9c7a))
* Remove truncation from brillig casts
([#3997](#3997))
([857ff97](857ff97))
* Remove truncations which can be seen to be noops using type
information ([#3953](#3953))
([cc3c2c2](cc3c2c2))
* Remove unnecessary predicate from `Lt` instruction
([#3922](#3922))
([a63433f](a63433f))
* Simplify chains of casts to be all in terms of the original `ValueId`
([#3984](#3984))
([2384d3e](2384d3e))
* Simplify multiplications by `0` or `1` in ACIR gen
([#3924](#3924))
([e58844d](e58844d))
* Support for u128
([#3913](#3913))
([b4911dc](b4911dc))
* Support printing more types
([#4071](#4071))
([f5c4632](f5c4632))
* Sync `aztec-packages`
([#4011](#4011))
([fee2452](fee2452))
* Sync commits from `aztec-packages`
([#4068](#4068))
([7a8f3a3](7a8f3a3))
* Use singleton `WasmBlackBoxFunctionSolver` in `noir_js`
([#3966](#3966))
([10b28de](10b28de))


### Bug Fixes

* Acir gen doesn't panic on unsupported BB function
([#3866](#3866))
([34fd978](34fd978))
* Allow abi encoding arrays of structs from JS
([#3867](#3867))
([9b713f8](9b713f8))
* Allow abi encoding tuples from JS
([#3894](#3894))
([f7fa181](f7fa181))
* Allow ast when macro errors
([#4005](#4005))
([efccec3](efccec3))
* Allow lsp to run inside of a docker container
([#3876](#3876))
([2529977](2529977))
* Bit-shifts for signed integers
([#3890](#3890))
([6ddd98a](6ddd98a))
* Checks for cyclic dependencies
([#3699](#3699))
([642011a](642011a))
* **debugger:** Crash when stepping through locations spanning multiple
lines ([#3920](#3920))
([223e860](223e860))
* Don't fail if no tests and the user didn't provide a pattern
([#3864](#3864))
([decbd0f](decbd0f))
* Fix advisory issue in cargo-deny
([#4077](#4077))
([19baea0](19baea0))
* Fixing dark mode background on the CTA button
([#3882](#3882))
([57eae42](57eae42))
* Fixup exports from `noir_wasm`
([#4022](#4022))
([358cdd2](358cdd2))
* Handle multiple imports in the same file
([#3903](#3903))
([219423e](219423e))
* Hoist constraints on inputs to top of program
([#4076](#4076))
([447aa34](447aa34))
* Implement missing codegen for `BlackBoxFunc::EcdsaSecp256r1` in
brillig ([#3943](#3943))
([2c5eceb](2c5eceb))
* Improve `nargo test` output
([#3973](#3973))
([3ab5ff4](3ab5ff4))
* Make `constant_to_radix` emit a slice instead of an array
([#4049](#4049))
([5cdb1d0](5cdb1d0))
* Operator overloading & static trait method references resolving to
generic impls ([#3967](#3967))
([f1de8fa](f1de8fa))
* Preserve brillig entrypoint functions without arguments
([#3951](#3951))
([1111465](1111465))
* Prevent `Instruction::Constrain`s for non-primitive types
([#3916](#3916))
([467948f](467948f))
* Remove panic for adding an invalid crate name in wasm compiler
([#3977](#3977))
([7a1baa5](7a1baa5))
* Return error rather instead of panicking on invalid circuit
([#3976](#3976))
([67201bf](67201bf))
* Search all levels of struct nesting before codegenning primitive types
([#3970](#3970))
([13ae014](13ae014))
* Update generics docs to mention we have traits now
([#3980](#3980))
([c2acdf1](c2acdf1))


### Miscellaneous Chores

* Ban nested slices
([#4018](#4018))
([f8a1fb7](f8a1fb7))
* Remove circuit methods from noir_wasm
([#3869](#3869))
([12d884e](12d884e))
* Rename Arithmetic opcode to AssertZero
([#3840](#3840))
([836f171](836f171))
</details>

<details><summary>0.39.0</summary>

## [0.39.0](v0.38.0...v0.39.0)
(2024-01-22)


### ⚠ BREAKING CHANGES

* Breaking changes from aztec-packages
([#3955](#3955))
* Rename Arithmetic opcode to AssertZero
([#3840](#3840))
* Remove unused methods on ACIR opcodes
([#3841](#3841))
* Remove partial backend feature
([#3805](#3805))

### Features

* Aztec-packages
([#3754](#3754))
([c043265](c043265))
* Breaking changes from aztec-packages
([#3955](#3955))
([5be049e](5be049e))
* Remove range constraints from witnesses which are constrained to be
constants ([#3928](#3928))
([afe9c7a](afe9c7a))
* Speed up transformation of debug messages
([#3815](#3815))
([2a8af1e](2a8af1e))
* Sync `aztec-packages`
([#4011](#4011))
([fee2452](fee2452))
* Sync commits from `aztec-packages`
([#4068](#4068))
([7a8f3a3](7a8f3a3))


### Bug Fixes

* Deserialize odd length hex literals
([#3747](#3747))
([4000fb2](4000fb2))
* Return error rather instead of panicking on invalid circuit
([#3976](#3976))
([67201bf](67201bf))


### Miscellaneous Chores

* Remove partial backend feature
([#3805](#3805))
([0383100](0383100))
* Remove unused methods on ACIR opcodes
([#3841](#3841))
([9e5d0e8](9e5d0e8))
* Rename Arithmetic opcode to AssertZero
([#3840](#3840))
([836f171](836f171))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: TomAFrench <tom@tomfren.ch>
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