perf(codegen): type erased numeric predicates - #9921
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The entry was created under PerryTS#9919, which is the GC pointer-validation PR. This change is PR PerryTS#9921.
jdalton
left a comment
There was a problem hiding this comment.
Review of f01946abd549110dab4a6c00836140c18c979ca5 (2026-09-07).
The diff consistently threads the inferred parameter representation through clone emission, inventory, and the public trampoline. The remaining acceptance gap is behavioral guard-failure coverage: the new test inspects LLVM but never executes the wrapper with a numeric string, an object with side-effecting valueOf, BigInt, NaN, or undefined from an out-of-range codePointAt. Please add a linked parity fixture for those cases, asserting the coercion count as well as the Boolean result. That would distinguish a retained-but-unreachable generic body from a functioning fallback. No correctness defect established in the inspected lowering.
Validation scope: source/diff inspection; I have not run this PR's build or test suite locally.
Infer long &&/|| chains with a bounded iterative walk so generated Boolean predicates retain their Boolean return type past the general recursion cap. Cover the exact YF6 source and its guarded codePointAt caller through HIR and LLVM lowering.
Record the failing inference condition, the bounded iterative fix, validation status, and the requested perrymaster bundle and perf checks.
…tion clones The fixture caller takes a `string`, so the lowering splits it into `caller$spec_b` / `caller$generic` behind `js_typed_string_arg_guard`; the guarded YF6 wrapper call lives in those clones, not in the public entry the assertion read. Assert over the union of the caller and its clones.
Compiler-only, on main
504e180d0. Written by codex from the campaign's YF6 map (cc-perf-campaign/codex/REPORT_map_yf6.md); not yet compiled into a cc bundle — the perrymaster rows and the lowering proof on the real bundle will be appended here.Why
On the current best cc bundle the wide-character predicate
YF6(136||terms ofcp >= a && cp <= brange tests, 152 generic relational calls per invocation) is the largest single mutator symbol: 9.46 % self on the 3300-char reply, called once per grapheme from the wrapText loop. Its parameter is erased (any) in the bundle, so the existing typed-i1 clone declined it (ParamNotI1).What changes
typed_i1_function_param_reps: a Boolean-returning, non-async, non-generator, capture-free top-level function with erased (Any/Unknown) parameters is admitted to the typed-i1 clone with those parameters asF64when the complete straight-line body lowers under the typed-i1 body rules with that assignment and contains at least four numeric comparisons (ERASED_NUMERIC_PREDICATE_MIN_COMPARISONS). Declared scalar parameters keep their declared reps; nothing changes for functions that already admitted.F64parameter once and keeps the unchanged generic body for guard failure, so a non-number argument runs the spec path.typed_i1_function_rejection_reasonreportsNonefor such a function (instead ofParamNotI1), so the module-level clone inventory and the trampoline agree.Evidence to land
YF6's generic name gains a typed-i1 clone and the trampoline's one-time guard (objdump/nmon the compiled bundle), plus the native-proof regression test innative_proof_regressions.rs.YF6's self share (from 9.46 %).Gates
Authored: the native-proof regression test. Not yet run here (disk floor):
cargo build --release -p perry, the codegen test target, the bundle compile. Draft until the rows are on this PR.Evidence so far (from the authoring run)
cc-perf-campaign/codex/yf6_probe.js(23 ranges + 22 equalities, called withcodePointAt):param q: Any,param_not_i1=1, no$typed_i1clone, 23js_rel_ge+ 23js_rel_lecalls in the predicate.…is_wide$typed_i1(double %arg1)withfcmp oge/ole/oeqand zero@js_rel_references; the generic fallback survives with itsjs_rel_ge/lecalls; the public entry holds one numeric tag guard, calls the clone on success and the generic body on failure; thecodePointAtcaller calls the guarded public entry, never the clone directly.$typed_i1symbol and fails it.Gates run / not run
cargo test -p perry-codegenafter the change: 1,939 passed, 0 failed, 5 ignored (31 suites); the new regression passed.cargo build --release -p perrywas run only on clean main (for the before trace).cargo build --release -p perrydefault-feature gate, the probe recompiled with the post-fix CLI, any linked timing, the cc bundle. All of these run on perrymaster; the bundle marker isperry_fn_cli_2_1_112_js__YF6self share from 9.46 %.Gate history
f01946abd(this head, picked onto the campaign's best tree asac0d8c6f6) on perrymaster:cargo build --release -p perryrc=0;cargo test --release -p perry-codegenrc=0 withtyped_i1_erased_numeric_predicate_guards_once_and_uses_f64_comparisonsand every othertyped_i1_*clone test green by name.yf6_probe.js,--no-auto-optimize --no-link --trace hir,llvm --focus isWide --explain-lowering): control compiler rejects withparam_not_i1=1, defines noisWide$typed_i1, and callsjs_rel_ge23 /js_rel_le23. The branch compiler definesisWide$typed_i1once, adds exactly 23fcmp oge+ 23fcmp ole+ 22fcmp oeq(46/46/44 total vs 23/23/22), and has zero@js_rel_calls inside the clone; the generic body keeps its 23 + 23 relational calls as the wrapper's fallback.perry_fn_cli_2_1_112_js__YF6$typed_i11 vs 0), then paired rows and a 999 Hz perf draw. Stays draft until those are here.nmcount ofperry_fn_cli_2_1_112_js__YF6$typed_i1= 0 in the branch bundle (and 0 in the control); the branch bundle holds 4$typed_i1clones in total. The admission fires on the probe but not on the realYF6, so no rows were taken for this arm. Next: the HIR dump and typed-clone rejection lines forYF6on the bundle (PERRY_NATIVE_REPS_ALL_TYPED_CLONE_REJECTIONS=1 … --no-link --trace hir --focus YF6 --explain-lowering), then a follow-up commit that makes the admission match the real function, then the identity check again before any rows. Until then this PR has no measured effect on cc.--trace hir --focus YF6):YF6 (params: 1, async: false, exported: false),param q: Any, body = oneReturnof a left-deep 136-term||chain; source is only>=,<=,===,&&,||with decimal literals (76 / 76 / 60 comparisons) — the probe's shape at three times the size. The bundle compiled with this branch holds 4 typed-i1 clones (Tu_,vu_,zF6, one more) and none forYF6, so the admission fails on size or depth or on the return-type inference for a deep tree, not on shape. The whole-bundle explain run died at 85 % of codegen under 23 GB RSS before printing its rejection summary. Follow-up in progress: find the failing condition statically, fix it, and add a regression that lowers the real 136-term function verbatim.ae8e15aa2): not the admission — HIR return-type inference caps recursive expression inference at 48 levels (perry-hir/src/lower_types.rs), and YF6's 135-deep left-associated||spine exceeded it, so an inner subtree inferredAny, the logical unification propagatedAnyto the root,Function.return_typestayedAny, and codegen rejected withReturnTypeNotI1before the erased-parameter admission was consulted (it only runs onParamNotI1). The 22-join probe stays below the cap, which is why it passed. Fix:LogicalAnd/LogicalOrreturn inference flattens the chain onto a worklist with the same sound rule (every leaf infers to the same non-Anytype), the depth-48 guard intact, plus a 512-node work cap covering YF6's 423 nodes. Newtests/yf6_admission.rslowers the exact 2,274-byte function verbatim and asserts the Boolean return type,YF6$typed_i1with 76oge/ 76ole/ 60oeq, zero@js_rel_in the clone, the generic fallback and the guarded wrapper. Not yet compiled; perrymaster rebuilds the compiler and the bundle and checks the identity count before any rows.ae8e15aa2on perrymaster (picked onto the best tree, and separately on this branch's own base):cargo build --release -p perrygreen;cargo test --release -p perry-codegengreen except the new regression's last assertion — everything before it passed on both trees: the real 136-termYF6infersBoolean, getsYF6$typed_i1with 76oge/ 76ole/ 60oeq, no@js_rel_in the clone, fallback intact. The failing assertion read the fixture caller's public entry, but a caller with astringparameter is lowered ascaller$spec_b/caller$genericbehindjs_typed_string_arg_guard, and the guarded wrapper call lives in those clones.4d95f5423(head, test-only): the assertion reads the union of the caller and its specialisation clones. Next on perrymaster: the compiler and full bundle from this head, the identity count on the artefact (YF6$typed_i1= 1, the whole point), then rows and the perf draw.Measured — YF3 (perrymaster: compiler and full bundle from this head on the best tree; identity on the artefact
perry_fn_cli_2_1_112_js__YF6$typed_i1= 1 (control 0), 5 typed-i1 clones in total; quiet box, paired 5-round rotation vs the best bundle)The 999 Hz draw of this PR's bundle shows the clone running:
YF6$typed_i11.04 % self (+zF6$typed_i10.05 %, the publicYF60.05 %) againstYF6at 9.46 % self on the best bundle's draw. So the predicate's self share is gone from the profile and the turn did not move: either the 9.46 % was sampling attribution into a 26 KB function whose cost is really paid by its callers, or the wrapper's guard falls through to the generic body for the valuescodePointAtreturns. Call counts of the clone vs the generic body during one reply, and the symbol-level diff of the two draws, are being taken to decide which. Not a landing candidate on these rows.Verdict from the draw split (same tool as the reference draw; 2,263 main-thread samples): collector 34.0 % vs 34.3 %, wrapText subtree 62.0 % vs 59.3 %; the clone is called from the grapheme loop (0.85 % inclusive) and no generic body appears, so the guard is not falling through. The loop did not get cheaper: its mutator leaves are now
memcmp5.8 % andjs_object_get_field4.4 % (before:memcmp7.1 %,YF66.5 %,js_regexp_new3.0 %). The 136 comparisons were cheap instructions overlapping with the string and field traffic the loop is bound by, so removing them does not shorten the turn. The typed clone is correct and free, but not a cc win; the branch stays draft as a correctness-neutral compiler improvement with its own regression, and the loop's remaining cost is the receiver-probe and string-payload work targeted elsewhere.Call-level confirmation (uprobes on the compiled bundle, one 3300-char reply): the public wrapper's fast path fires 1,486,021 times, the generic entry 0 times, the clone's straight path 1,486,021 times,
AF6entries 1,481,897 (one predicate call per width call); every argument at the guard is a plain double (the reply's code points), never a tagged value. Paired same-session draws:YF6self 9.6 / 10.5 % →YF6$typed_i10.94 %,AF6inclusive 10.0 / 11.9 % → 2.35 %,js_segments_view_regexp_testinclusive 7.9 / 7.0 % → 2.0 / 3.1 %; the collector's share rose 34.2 / 31.5 % → 37.1 / 37.3 % (budgeted majors and dead-owner work) andjs_object_get_field,js_native_call_method,dispatch_primitiveand the buffer probes rose by the same total inside the same loop. About 14 points left the predicate's subtrees and the same amount reappeared as memory-side work; the five paired rows (+0…+4 %) are the truth.