neural/trident/audit/nox-entry-abi-review.md

Independent Nox typed-entry ABI review

Date: 2026-09-12. Scope: src/ir/tree/lower/nox/entry.rs, its call from NoxCompiler::compile_fn, module/type qualification and aggregate navigation, reference/nox.md, compiler API 3 in reference/warrior-api.md, tests/nox_surface/entry.rs, and Joy rs/tests/typed_entry.rs.

Status: both identified entry findings are corrected and independently retested. The reviewer implemented the owner-authorized lexical-size correction after the read-only review; the owner implemented chunked paths. Reviewed files are stable. Frozen FINAL3 was not changed.

Contract and sound portions

The formula consumes exactly the declared flat public-word sequence, in source parameter/field/index order. It then reconstructs the internal reverse parameter stack, with declaration-order cons lists for structs/tuples/arrays and a balanced four-limb Digest tree. Ordinary calls do not invoke this adapter. The public arity check is sequenced before reconstruction and the body; it checks the exact zero terminator rather than merely checking that enough inputs exist.

Each Field leaf is used by addition with zero, requiring an atom even when the source body ignores that parameter. Bool and U32 use native less-than guards against 2 and 2^32. The Bool convention remains Nox's 0=true/1=false. Only the selected guard branch executes the inv(0) crash; its scalar result does not force the body result to have scalar shape. Rebuilding the complete subject executes every narrow-type check before any source effect.

Stateful entries select external words below the BBG root and restore the original root above reconstructed parameters. The root is not counted as a public word or interpreted as an aggregate parameter. Existing state surface coverage uses the actual look provider; this review did not generate a new state proof. Arity currently allows 58 words without state, 57 with a root.

Input navigation is safe under these limits: generated axes remain below the field modulus and word arithmetic cannot overflow. Layout recursion checks 16 levels and 4096 nodes; each list is capped at 58 elements. Checked addition and multiplication protect symbolic array-size arithmetic. These constraints do not, however, bound every internal aggregate path (finding 2).

The guards live inside the exported formula, not solely in the host CLI. Thus public and private execution relations bind them with the complete formula, input, output and native cost. Inspected Joy tests independently mutate each public aggregate leaf, output, unused narrow inputs and arity. The private test also exercises divine input. This review does not claim to have executed those proof tests; the owner runs their receipts separately. Native adapter work contributes real reductions, including checks for unused inputs; it cannot be priced as the old body-only entry.

Finding 1: imported symbolic array lengths lose lexical ownership

A accepted imported type fails to compile through the adapter:

// lib.tri
module lib
const N: U32 = 2
pub struct Pair { words: [Field; N] }
// import.tri
program entry
use lib
fn main(x: lib.Pair) -> Field { x.words[0]*100+x.words[1] }

The fresh release compiler rejects it with nox: unresolved entry array length. modules.rs::qualified_type qualifies Named types but clones ArraySize; later entry_size resolves N through the entry module instead of lib. A same-named entry constant could select a different shape. The unused-import warning is a separate diagnostic and is not the fatal cause.

Required correction: resolve or qualify all array-size expression leaves in the lexical defining module, recursively through nested imported struct fields and function signatures. Preserve checked arithmetic. Test a foreign N, a conflicting entry N, and a nested imported array; assert actual source-order execution.

Finding 2: bounded flat entry can overflow a fused internal axis

Construct Inner with 40 zero-width [Field;0] fields followed by value: Field; construct Outer with 40 such fields followed by inner: Inner. Then:

fn main(x: Outer) -> Field { x.inner.value }

This is one public word, two shallow structures and bounded lists. Compilation succeeds, but actual native execution on input 7 rejects axis out of range, instead of returning 7. Concatenating both field paths exceeds the machine axis width. dotted_axis, place_axis, and elem_access all use unchecked axis_compose; bounding public-word count alone does not make those safe.

Required correction: preserve path segments and use sequential navigation when fusion is not representable. Apply the same principle to recursive assignment reconstruction, not only reads. A fused address must be a canonical field atom, not merely fit u64. Rejecting every such valid bounded aggregate would narrow the accepted language rather than complete its implementation. Regressions must include reading and writing a deep last field, an unaffected sibling, and the boundary around the largest canonical fused axis.

Independently executed evidence

cargo build --release --locked --bin trident completed successfully on the live workspace (16.28 s). Both counterexamples were then compiled with that fresh release compiler. The second emitted formula was executed as a raw .nox file by the actual Joy native runner, avoiding any stale source compiler in Joy.

Exact temporary sources and machine-readable results: /tmp/nox-entry-extra-review/{lib.tri,import.tri,deep.tri,receipt.json}. Finding 1: compile exit 1. Finding 2: compile exit 0, native run exit 1. The older FINAL3 observations at /tmp/nox-entry-abi-review-20260912/receipt.json document the original entry bug; they are not evidence that the new scalar checks fail.

Authorized correction checkpoint

Following the independent findings, the owner assigned the reviewer the narrow lexical-size correction in modules.rs and the new tests/nox_surface/imported_entry.rs; the owner retains path lowering. The correction resolves literal module constants before qualifying declarations, recursively qualifies unresolved size leaves, folds size addition/multiplication only with checked arithmetic, and preserves explicit function generic names. The unit test lexical_sizes_preserve_generics_and_never_wrap_arithmetic passed (/tmp/nox-lexical-size-unit.log). The integration regression also covers nested arrays of imported structures with a conflicting entry-module N and an ordinary local generic call. It exposed missing expression-type recovery through dotted variables and indexing; that adjacent path correction is with the owner.

A first exploratory imported generic-function call was rejected by the earlier typechecker (not generic), before Nox lowering. It is not claimed fixed by the lexical-size patch; the preserved-generic regression instead uses the existing local generic call path. This is a separate imported generics surface limitation.

Final independent retest and scope closure

Both original sources were recompiled with the freshly rebuilt release compiler, then executed by the actual native runner:

  • Imported Pair, input [7,19]: compile/run exit 0, output 719, 32 reductions.
  • Deep zero-width-prefix structures, input [7]: compile/run exit 0, output 7, 187 reductions.

Exact post-fix receipt: /tmp/nox-entry-extra-review/fixed-receipt.json; the original failing receipt is retained separately. No failing source was weakened.

The persistent integration regression imported_entry_sizes_keep_lexical_constants_and_generic_calls passed in both source profiles (/tmp/nox-imported-entry-fix.log). It uses a conflicting local N, a foreign N+1 nested array, checks all six imported scalar leaves, and verifies a local generic call. The owner-fixed dotted/index expression type recovery is therefore exercised as part of real execution.

The persistent deep_aggregate_reads_and_writes_keep_full_paths_and_siblings regression independently passed (/tmp/nox-deep-path-review.log), exercising reads, two writes and unaffected siblings in both source profiles. I inspected path.rs: paths are represented without truncation, navigation is split into canonical 62-edge chunks, reconstruction evaluates the RHS exactly once against the original subject, and path/emitted-node bounds are explicit. The adapter's original direct external axes remain safe under its flat-word bound.

No further blocker was identified in this reviewed adapter scope. This is not a claim that the separately documented general variable-shape Nox proof relation or imported generic-function typechecking has been implemented. Broad workspace and genuine proof receipts remain the owner's separately executed release gates.

Subsequent imported-generics correction

The imported generic-function limitation recorded above was a real finding at this review's checkpoint. It is now superseded by the separate concrete AST specialization and body-checking work, independently reviewed in Trisha's imported-generics audit. That gate has five passing real VM/type-rejection tests (both source profiles) and a separately executed genuine default-security proof binding program, public input and public output. The original failure description remains as historical evidence; it is no longer the current imported-generics status.

Graph