Skip to content

codegen: a method's typed body covers its index clones when the index parameters are int32 of any sign (typed set) - #14450

Merged
proggeramlug merged 11 commits into
mainfrom
perf/typed-set
Nov 11, 2026
Merged

proggeramlug merged 11 commits into
mainfrom
perf/typed-set

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

A method whose parameters feed element keys gets the non-negative index clones on main ($idx_u31_…, with their
$i32 raw bodies, and the same again under the proven-this clone). P2's selector (signature_entry::select_methods)
types such a method only when the signature is at least as precise as every tier it would replace, and for the index
clone that was read as "each index parameter is an I32 whose interval starts at 0". The chess board helper

private set(square: number, piece: number): void {
  const old = this.at(square);
  if (old !== 0) { this.hashA ^= this.zobristA[(old + 6) * 128 + square] ?? 0; … }
  this.board[square] = piece;
  if (piece !== 0) { this.hashA ^= this.zobristA[(piece + 6) * 128 + square] ?? 0; … }
}

has the signature (i32[-128,255], i32[-8,7]) on main (both parameters are int32 since the board's element interval,
#14429), and the index clone covers both parameters (piece is index-used through (piece + 6) * 128 + square).
Neither interval can ever start at 0, so set stayed untyped (why=index-clone): make inlined three copies of the
$i32 index clone, kept guarded splits for the sites whose tuple is not proven non-negative, and called the
double-ABI set$pshape$generic 0.92 times per make (198 542 calls in perft 4, 163 instructions each).

What changes

  • The selector rule (signature_entry::index_param_covered, signed_index_params). An index clone's parameter
    is covered by an I32 signature slot of any sign. The clone proves its parameters non-negative int32s so that an
    element key built from them needs no sign test. Since the unsigned bound (N10, codegen: an unsigned bound absorbs the element key's sign test (N10) #14426) an int32 key is bounded by
    one unsigned compare that declines every negative key onto the property path, so the sign buys the clone nothing a
    typed body lacks; and the typed body takes the parameter as a word at every site, where the clone needs each
    site to prove the whole tuple non-negative, or its guarded entry. A parameter the signature gives F64 or Boxed
    still keeps the clones (unchanged).
  • Semantics are P2's (DESIGN-p2 §3.2.1, "nonnegative-index clones: subsumed"): M$sig is the generic body with
    the parameter in an i32 slot; only a parameter whose interval has lo >= 0 seeds nonnegative_integer_locals
    (nonnegative_params, unchanged). A negative key reaches the runtime-key arm with its original value (the
    unsigned bound's miss arm) and names a property.
  • No wpfacts representation changes: the signature and the typed call sites are main's. wpfacts gains a probe only.

Switch, probes, witness, sabotage

  • PERRY_SIG_SIGNED_INDEX=0 (off, false): main's rule (lo >= 0). The rule is also off with
    PERRY_UNSIGNED_BOUND=0, which it rests on. Compile-cache input (compile_env.rs, test
    signed_index_switch_is_a_cache_input).
  • PERRY_FACTS_DUMP: # typed_call module=<m> call=<offset> callee=<name> typed=1, or typed=0 why=<reason>, for
    each call that resolves to a body with a signature and each method call that names candidates but not one body
    (why: argument-<i>-outside-the-interval, argument-<i>-not-a-number, argument-<i>-omitted,
    method-not-one-target, callee-not-one-body, distinct-calls-share-the-key, spread, …). wpfacts
    signatures::collect; the typed call sites are unchanged (call_targets is resolve filtered; refusal is
    the old admits with its reason).
  • PERRY_SIG_DIAG=1 (codegen, stderr): [perry sig] Class.method typed=1 params=(…) or typed=0 why=<tier> for
    each method a typed call reaches (index-clone, typed-abi-clone, proven-this, proven-argument, tower,
    excluded-tier, excluded-shape, default, all-boxed).
  • Check mode: no new claim. The signature witness (Fact::Signature, rep and interval at every typed site and at
    the boxed entry) and the unsigned bound's witness (Fact::UnsignedBound) cover the newly typed bodies.
  • S0 arm signed-index-negative (sabotage signature-range): a method typed by this rule whose two index
    parameters receive negative values; the sabotage claims [0, 127] for both. Correct rc 0, sabotaged rc 86,
    predicate signature.

Results

Chess (qb10, instructions:u M, one build, on vs PERRY_SIG_SIGNED_INDEX=0, twice each, identical runs,
checksums and PV equal in every arm). Build r2 = 1d11007 (this branch's code merged with main e4f004e, which has
the prologue PR #14442; the PR head adds test and comment changes, a dead-code removal and the merge of main
80c09e3, not re-measured):

workload on off (= main) change
search 250000 21 360.0 21 974.1 −2.79%
perft 4 490.7 511.4 −4.05%
evaluate 100000 1 487.8 1 487.8 0
legal 10000 700.5 715.8 −2.14%

Before merging main (build r1 = 90d8424 on main fd6e707): 21 420.9 / 491.3 / 1 500.0 / 709.4 vs 21 968.7 /
509.9 / 1 500.0 / 723.1 (−2.49 / −3.65 / 0 / −1.89%).

Probes (chess): [perry sig] Chess.set typed=1 params=(I32[-128,255],I32[-8,7]), off typed=0 why=index-clone;
Chess.at typed=0 why=index-clone params=(F64) on both arms (a double key reaches at; every at call is a field
read call, so it has no cost). # typed_call: all six set sites typed=1, the three addPawn sites typed=1
(they are typed on main too: the FOLLOWUPS entry "[N1: addPawn typed sites]" is stale), one refusal in the engine
(generateLegal, distinct-calls-share-the-key).

Where it lands (r1). set$sig is inlined at its five sites in make and its site in readFen; the eight bodies of
set on main (set, $generic, $idx_u31_245_246, …$i32, and the four under $pshape) are one set$sig and
the boxed entry.

measure on off
callgrind (x86-64-v3) perft 4, total Ir 500 035 464 519 623 763
… make$sig self 136 589 657 123 861 353
… set$pshape$generic self (198 542 calls) – 32 319 559
… per make (216 386 calls), make + set 631 722
callgrind search 20000, total Ir 2 301 503 536 2 364 538 518
perf record search 250000 (M): make$sig 4 410.2 4 190.9
… set$pshape$generic – 670.6

The +219 M of make$sig reported in #14439 (on vs PERRY_REST_CAPACITY=0, perf record at e0f2ca1, before
PR C #14436 was merged in) does not exist on main: with the board at rest make$sig is 5 149.5 M vs 5 250.1 M
(perf record, search), 123.86 M vs 130.23 M (callgrind, perft 4). That commit was not rebuilt; the likely cause
[E] is the i32 → double crossing of each board value make puts into the Undo literal, which PR C's I32 lanes
removed. The cost that is really in make is the one this PR starts on: set's index clones inlined into it plus
the generic body.

Peak RSS (/usr/bin/time %M, KiB, 5 runs each, r2): search on 25 856–26 624, off 26 368–26 880; perft 18 944–
19 200 both; evaluate on 18 688–19 200, off 18 944–19 456; legal 18 688–19 200 both. Equal inside the 256 KiB
allocator step.

Where else it engages (parity IR census, 2 603 programs): five programs change IR beside main's
declaration-order case, each with the same output: test_array_reps_fields (Board.masked),
test_array_reps_fields_growth (Queue.at), test_unsigned_bound (Board.at), test_rest_capacity (Board.at)
and the new fixture. In each, a method's $generic / $idx_u31 / $idx_u31…$i32 bodies (and the same under
$pshape) become one $sig. tssrc and the three.js skin have no such method (IR and sizes equal).

Gates

qb10, on vs PERRY_SIG_SIGNED_INDEX=0. Builds: r1 = 90d8424 (the change, on main fd6e707) and r2 =
1d11007 (r1 + tests + main e4f004e). The PR head 9752c1e is r2's compiler plus: the fixture's last
sections, rustfmt of two hunks, the PERRY_SIG_DIAG exclusion entry, the removal of the dead admits, and the
merge of main 80c09e3 (clean). The rows say which build they ran on.

gate result
chess, instructions:u tables above (r2 and r1), two identical runs per arm
chess check mode (PERRY_VERIFY_FACTS=1) r1 and r2: compile rc 0; perft 3, legal 1000, search 20000, evaluate 1000: rc 0, 0 misses
chess IR / size IR differs in 1 of 2 modules (the engine). r1: every exec section on 7 592 056 B, off 7 599 484 B: −7 428 B (.text −2 816, perry_js_text −4 612)
S0 r1 and r2: rc 0, 123 executable controls pass (signed-index-negative: correct rc 0, sabotaged rc 86, predicate signature)
parity fixtures × {on, off, PERRY_REST_ELEMS=0, verify, GC schedule stress on, stress off} r2, PR head's fixtures: 12 fixtures × 6 arms, 72/72 MATCH node v26.8.1: function/test_p2_signed_index_params (new), test_p2_signature_methods, test_p2_signature_functions, test_rest_capacity, test_rest_loops, test_rest_elems, test_rest_fields, test_rest_zero_sign, test_n1_int_temps, test_facts_int32_bindings, test_array_reps_fields, test_array_reps_loop_base_gc
GC schedule stress (instrumented runtime, PERRY_GC_SCHEDULE_SEED=7, RATE=1) the new fixture: 25 forced copying minors, 4 394 moved objects, on and off; test_rest_capacity 4 253 / 95 666; test_array_reps_loop_base_gc 25 000 / 460 099; test_p2_signature_functions 245 / 27 866. Output equals node in every arm
parity IR census, on vs off r1: 2 603 programs, 6 IRDIFF (above), each with the same output; 0 compile-rc differences; 11 OUTDIFF, all with identical IR (main's set: non-deterministic or compiling in neither arm)
vf, PERRY_VERIFY_FACTS=1 r1: n = 1 538, rc86 = 0, crc ≠ 0 = 15: the same 15 as #14439 (the known pair, 12 list entries no longer in the tree, and test_gap_13049_chunked_literal_births, whose compile passes the runner's 300 s limit under load)
tssrc lost sites r1: 64 169 site lines, 24 552 proven in both arms: lost 0, gained 0
hello / hello2 r1 and r2: on vs off byte-identical (r2 md5 f446a6b50365, 07847e85a106); callgrind Ir 133 150 / 642 287 in both arms
sizes chess −7 428 B (above); three.js skin 25 349 411 B on = off, every section equal (r1); Supabase (via /root/supa-one.sh, r2): every exec section on 216 508 924 B = off 216 508 924 B, each section equal (no method of Supabase changes tier)
unit tests (new) codegen signature_entry::tests: a_signed_i32_slot_covers_an_index_clone_parameter, without_the_rule_a_negative_interval_keeps_the_index_clone; wpfacts signatures: a_signed_key_and_value_are_i32_parameters_with_their_intervals, the_probe_names_each_typed_call_and_each_refusal, without_diagnostics_there_is_no_probe_line; perry compile_env: signed_index_switch_is_a_cache_input
touched test modules (de203f4) wpfacts signatures 25 passed; codegen signature_entry 2 passed; perry compile_env 10 passed, 1 failed (codegen_env_vars_are_build_cache_inputs, "shared list must be sorted and unique", main's known failure)
full suites, ONCE (de203f4 = this branch's code on main e4f004e; the head adds the fixture's at-rest board, the PERRY_SIG_DIAG exclusion entry, the removal of a dead function and the merge of main 80c09e3) perry-codegen 2 909 passed, 0 failed; perry-wpfacts 2 366 passed, 0 failed; num_seeds_audit 2 passed; perry-hir 887 passed; perry-runtime 6 682 passed, 0 failed
-D warnings de203f4: perry-hir and perry-codegen 0 errors. perry-wpfacts 1 error, this PR's (admits unused after refusal took its place): removed in the head, and the head's perry-wpfacts is clean (cargo check -p perry-wpfacts with -D warnings, on the Mac). perry-runtime 1 error, main's unused import at intl.rs:72 (this PR does not touch the file). perry failed only through perry-wpfacts
scripts/check_file_size.sh rc 1 on main and here: four allowlist counts are stale on main e4f004e (codegen/mod.rs 3 666 vs 3 662, expr/mod.rs 2 839 vs 2 838, tests/native_proof_regressions.rs 15 688 vs 15 687, run_pipeline.rs 6 254 vs 6 204). This PR changes no line count of an allowlisted file

Not done

See FOLLOWUPS "[typed set follow-ups]". In short:

  • the typed body's at-rest board read and store take the double-key form (PERRY_REST_DIAG: form=f64), where the
    $i32 index clone took the int32 form for its non-negative key: codegen has no interval for a signed I32
    parameter. The typed body also computes its Zobrist keys in doubles and normalizes them by a call, as the
    generic body did. Both are the next PR of this lane (perf/int32-local-ranges: the facts' interval of an int32
    local reaches codegen's range proofs, and a key they bound to int32 is lowered as its i32; measured on top of
    this PR: search −4.2%, perft −4.7%, evaluate −3.9%, legal −3.1%);
  • at is untyped (params=(f64)), which costs nothing while every at call is a field read call;
  • a method whose value is extracted (const f = o.set) has Boxed parameters by the facts and keeps its clones
    (the fixture's Kept.set).

@proggeramlug
proggeramlug merged commit 7523aa7 into main Nov 11, 2026
94 of 101 checks passed
@proggeramlug
proggeramlug deleted the perf/typed-set branch November 11, 2026 13:01
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Explain Change →

Pre-merge checks: Inconclusive 2

Failed Failed checks: 2 inconclusive

Check name Status Explanation Resolution
Title check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
Description check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

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