feat(frontend): bound entry-point integers by the field and read ABI inputs under a run-time field - #20
Open
BornPsych wants to merge 9 commits into
Open
feat(frontend): bound entry-point integers by the field and read ABI inputs under a run-time field#20BornPsych wants to merge 9 commits into
BornPsych wants to merge 9 commits into
Conversation
BornPsych
added this pull request to stack #21
September 22, 2026 23:45
|
Thank you for your contribution to the Noir language. Please do not force push to this branch after the Noir team have started review of this PR. Doing so will only delay us merging your PR as we will need to start the review process from scratch. Thanks for your understanding. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 6 of the Goldilocks port, stacked on #19. The ABI carries every scalar in one field element, so an integer type can cross an entry point only if each of its values is below the modulus. A parameter or return of
mainor of a contract function, including one nested in an array, tuple, struct or alias, is now refused at compile time when its width is the bit length of the modulus or more (FieldConfig::fits_unsigned), signed and unsigned alike, because a signed value crosses as its unsigned bit pattern. Under Goldilocks the widest entry-point integer is 63 bits, sou64andi64are refused there; under bn254 it is 253 bits, which no test program approaches.Fieldandboolare unaffected, and so is every width inside a program: the rule is on the signature, never on a value. On the tooling side,Format::parsereads TOML and JSON inputs under aFieldConfigits caller passes, and a newscalarmodule converts between a scalar and its field element under any field, so a consumer other than the circuit backend chooses the field at run time.nargopasses the field it is linked against and behaves as before.Three things I'd point a reviewer at.
The rule is decided on the type, before any value exists. Accepting
u64under Goldilocks and refusing only the values whose pattern reaches p would let a program compile under a field and then fail per value when it runs; refusing the type can later be loosened without breaking a program that compiles today. Test and fuzz functions,#[fold]functions and#[abi(tag)]globals stay outside the rule: no field element carries their values across a proving boundary, and a tag carries an integer's magnitude. An#[export]function is outside it too, as it is outsideprogram_validityaltogether. There is no monomorphizer re-check: an entry point cannot be generic, so a width that a struct's generic argument, an alias, a global or an arithmetic expression spells out is already a number when the signature is elaborated, and it is refused there. The diagnostic keeps the entry-point primary, adds a secondary that names the field and the widest width ("Integers wider than 63 bits are not valid entry point types under goldilocks. Found: u64") and replaces the vectors-and-references note with the rule.Parsed values still live in
InputValue::Field, whose element belongs to the linked field. That element is an exact container for every field whose modulus is at most the linked one, so one bn254 build parses bn254 and Goldilocks inputs, while a field with a larger modulus is refused before any value is read (FieldNotCarried), since reading it would reduce values silently; bls12_381 inputs need a bls12_381 build. Every public entry of the parser makes that check,Format::parse,parse_jsonandInputValue::try_from_jsonalike; the per-value entry once skipped it, and a bls12_381 value equal to bn254's p parsed to 0. Nothing is reduced at the boundary: aFieldwritten as a negative number is the negation of its magnitude in the selected field, so-1under Goldilocks is2^64 - 2^32, not bn254'sp - 1.Abi::encodeandAbi::decodekeep their signatures. They build and read the backend'sWitnessMap<FieldElement>and are linked-field by that type, so a field mismatch cannot be expressed there and a check would have nothing to compare.noirc_abi::conformance::boundary_vectors(FieldConfig)publishes the accept and reject cases of the single-element domain,p - 1,pandp + 1, the 64-bit values on either side of the Goldilocks modulus, the widest entry-point width and one bit wider, a native spelling of a wide type and the spellings each scalar type refuses, as a function of the field rather than a data file, because every vector is a function of p.the_parser_accepts_and_refuses_exactly_the_boundary_vectorspins both formats to them under every field the build carries and round-trips each accepted value throughserialize; the ast-interpreter'sProver.tomlbridge iterates the same vectors, so the two readers cannot drift apart. The per-rule parser tests that restated these cases are gone, and a sabotaged parser that accepts p is caught by the vectors (u254 = pwould have been carried as 0).Elsewhere: the signed hex serializer reads the element's bit pattern at the declared width and pads to
FieldConfig::num_bytes, so it no longer goes throughu128(a signed 200-bit value used to panic on serialize with "attempt to shift left with overflow"); it is byte-identical to the old one at every width up to 128 under both builds. The arbitrary ABI generator draws integers at every width up to 253 bits.InputExceedsFieldModulusnames the selected field and its modulus. The Goldilocks lane runs the wholenoirc_abipackage, every one of whose tests passes under the Goldilocks-linked element, and dropspackage(noirc_frontend): with the frontend linked to Goldilocks, 24 upstream frontend tests whosemaintakes a 64-bit integer are refused, correctly, and nothing in the frontend is Goldilocks-gated, so the field-contract tests select every field in the default build instead. Six fork-owned frontend tests and the driver's width tests keep their intent by moving the wide type behind a narrowmain.design/field-genericity.mdstates the rule, the three layers of the codec, the container invariant and the vectors. The benchmark gathering script concatenatesnargo's daily-rotated logs instead of moving one file, so an external-repo report whose runs straddle midnight UTC no longer fails atmv(the first run of this PR did, onrollup-tx-base-public, between 23:57 and 00:01 UTC).Tests:
field_contract.rsgainsentry_point_integers_must_fit_below_the_modulus(the widest width and one wider, both signednesses, as a private parameter, a public parameter and a return, under every field),the_entry_point_rule_looks_through_every_aggregate,the_entry_point_rule_leaves_other_functions_alone,contract_functions_follow_the_entry_point_ruleandthe_entry_point_diagnostic_names_the_field_and_the_widest_width, red on behaviour once the variant existed without the rule.scalar.rspins the codec: the largest element accepted and the modulus and one above refused, every pattern of an integer's width, a container at or above a narrower field's modulus refused rather than reduced, aggregates, an uncarried field, and a decode-then-encode property. The parser's own tests run under every carried field:field_values_are_accepted_iff_below_the_selected_moduluspins the refusal's wording,nested_fields_use_the_selected_modulusandthe_widest_integers_round_trip_through_every_formatthe aggregates and the widest widths, anda_field_the_linked_element_cannot_hold_is_refusedthe container invariant in both formats and attry_from_json. The Goldilocks vectors are pinned to refuse exactly the four 64-bit signed patterns at or above p.Gates on the tip: frontend and driver 2341 tests,
noirc_abi42 in the default build, the Goldilocks lane's test command 98 tests with a-DwarningsGoldilocks build, clippy at-Dwarningsacross the workspace on every target and rustfmt clean, the design-link check, and the fork residual with nine files entering the manifest; each ABI commit builds warning-free and passes the crate's tests on its own. The workspace suite passed 15987 on the tree before the final review pass, which touched only the ABI crate, the lane and the docs. Theexecutesnapshot suite was not rerun; no snapshot carries the rule text, and no test program names an integer of 254 bits or more.Known follow-ups, not here: the ast-interpreter companion reads the program's field through the new layers and pins at this PR's merge commit; a versioned multi-element encoding for integers wider than the field; whether
#[export]functions should fall under the rule, since their ABIs cross the same boundary; the wasm crate's JS tests were not run (its Rust side is covered by clippy, and the one message they assert on is unchanged).