Skip to content

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
ys/goldilocks-stage-5dfrom
ys/goldilocks-stage-6
Open

BornPsych wants to merge 9 commits into
ys/goldilocks-stage-5dfrom
ys/goldilocks-stage-6

Conversation

@BornPsych

@BornPsych BornPsych commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

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 main or 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, so u64 and i64 are refused there; under bn254 it is 253 bits, which no test program approaches. Field and bool are unaffected, and so is every width inside a program: the rule is on the signature, never on a value. On the tooling side, Format::parse reads TOML and JSON inputs under a FieldConfig its caller passes, and a new scalar module 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. nargo passes 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 u64 under 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 outside program_validity altogether. 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_json and InputValue::try_from_json alike; 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: a Field written as a negative number is the negation of its magnitude in the selected field, so -1 under Goldilocks is 2^64 - 2^32, not bn254's p - 1. Abi::encode and Abi::decode keep their signatures. They build and read the backend's WitnessMap<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, p and p + 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_vectors pins both formats to them under every field the build carries and round-trips each accepted value through serialize; the ast-interpreter's Prover.toml bridge 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 = p would 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 through u128 (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. InputExceedsFieldModulus names the selected field and its modulus. The Goldilocks lane runs the whole noirc_abi package, every one of whose tests passes under the Goldilocks-linked element, and drops package(noirc_frontend): with the frontend linked to Goldilocks, 24 upstream frontend tests whose main takes 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 narrow main. design/field-genericity.md states the rule, the three layers of the codec, the container invariant and the vectors. The benchmark gathering script concatenates nargo's daily-rotated logs instead of moving one file, so an external-repo report whose runs straddle midnight UTC no longer fails at mv (the first run of this PR did, on rollup-tx-base-public, between 23:57 and 00:01 UTC).

Tests: field_contract.rs gains entry_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_rule and the_entry_point_diagnostic_names_the_field_and_the_widest_width, red on behaviour once the variant existed without the rule. scalar.rs pins 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_modulus pins the refusal's wording, nested_fields_use_the_selected_modulus and the_widest_integers_round_trip_through_every_format the aggregates and the widest widths, and a_field_the_linked_element_cannot_hold_is_refused the container invariant in both formats and at try_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_abi 42 in the default build, the Goldilocks lane's test command 98 tests with a -Dwarnings Goldilocks build, clippy at -Dwarnings across 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. The execute snapshot 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).

@BornPsych
BornPsych added this pull request to stack #21 September 22, 2026 23:45
@github-actions

Copy link
Copy Markdown

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.

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.

1 participant