Skip to content

Skip unneeded work in occur checks and primitive specialization - #9193

Open
JonoPrest wants to merge 3 commits into
masterfrom
jono/occur-check-fast-path
Open

JonoPrest wants to merge 3 commits into
masterfrom
jono/occur-check-fast-path

Conversation

@JonoPrest

Copy link
Copy Markdown
Contributor

Two small type checker hot spots that do work nobody uses.

Occur checks. Every variable link in unification runs occur, and the Tvar/Tconstr cases also run deep_occur. occur keeps a set of visited nodes along every path and deep_occur marks every node and then walks the type again to unmark it, but almost all of these walks see only a few nodes. On a 552 module React app that is 943k and 494k calls, about 0.5 s of 14 s single domain CPU.

Both now first walk the type without a visited set or marks, giving up after 256 nodes (which also stops cycles and heavy sharing). When it gives up, the existing walk runs unchanged.

  • occur, expression mode only: if the cheap walk finishes without meeting the variable, occur_rec would not have raised or expanded anything either. Pattern mode looks types up, so it always takes the full walk.
  • deep_occur: the cheap walk only answers "absent". It gives up on meeting the variable and on nodes an enclosing walk already marked, so marks end up the same.

Primitive specialization. specialize_primitive scraped the primitive's function type (a copy of the whole type) on every use, but only unified operators and comparisons read it. For the other 95% of uses it now calls find_builtin directly. Scraping a function type looks nothing up, so the only work skipped is that copy, made during translation after .cmi and .cmt are written.

Clean builds, 8 vCPU Linux VM, 1 domain, this PR / master, 5 runs:

project wall CPU
React app, 552 modules 0.96 0.96
backend service, 391 modules 0.94 0.94
library 0.99 0.98

Testing:

  • New super_errors fixtures for occur check errors: through a function type, through a type constructor argument, and past the 256 node budget. Their output matches master.
  • JS, .cmi, .cmj, .ast, .cmt and .cmti are byte identical to master across three production codebases and their dependencies, at 1 and the default number of domains. make test and make test-rewatch pass.

JonoPrest added a commit that referenced this pull request Nov 3, 2026
The entry names #9193.

Signed-off-by: Jono Prest <[email protected]>
@JonoPrest

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

  • Build: skip unneeded work in the type checker's occur checks and when specializing primitives.

Semantic Diff

3 commits · Full page

Try a cheap bounded walk before the full occur checks

393d6cb · JonoPrest · Nov 03, 2026, 09:27 UTC

Why

Every variable link in unification runs occur, and the Tvar/Tconstr cases also run deep_occur. occur builds an AVL set of visited nodes along every path and deep_occur marks every node and walks the type again to unmark it, although almost all of these walks see a handful of nodes. On a 552 module React app they make 943k and 494k calls, about 0.5 s of 14 s single domain CPU.

Both checks now first walk the type with no visited set and no marks, giving up after 256 nodes (which also stops cycles and heavy sharing); when the walk gives up, the unchanged full walk runs. - occur in expression mode: a walk over the edges occur_rec follows that finishes without meeting the variable proves occur_rec would neither raise nor expand anything. Pattern mode, which looks types up, always runs the full walk. - deep_occur: the walk only answers "absent". It gives up on meeting the variable and on a node an enclosing walk already marked, so the marks left behind are the same as the full walk's.

Review

  • Can an answer, a diagnostic or an artifact change? No: each fast walk either proves the full walk would do nothing more than it did, or gives up and the full walk runs from scratch. The new fixtures pin the errors, including one past the budget, and the corpus (5,398 artifacts and 1,147 .cmt/.cmti) is byte identical to master.
Skip scraping a primitive's type when no specialization reads it

58959d9 · JonoPrest · Nov 03, 2026, 09:27 UTC

Why

specialize_primitive scraped the primitive's whole function type (a deep copy plus head expansion) on every use, but only unified operators and comparisons look at it. For the other 95% of uses (on a 552 module React app, 21,582 of 22,770) the result was thrown away and find_builtin answered.

For a function type whose primitive is in neither table, it now calls find_builtin directly.

Review

  • Could the skipped scrape have had an effect? Scraping a function type looks nothing up: only a Tconstr head is expanded. What is left is a copy with fresh type ids, made during Lambda translation after the .cmi and .cmt are written, whose result nothing read. The corpus artifacts and .cmt files are byte identical to master.
✏️ Build the occur walk's closure once per call

dfc6baa · JonoPrest · Nov 09, 2026, 13:10 UTC · edited

Why

occur_reaches passed occur_reaches budget ty0 to iter_type_expr at every node, so the fast walk allocated a partial application per node it visited.

occur_reaches now defines its walk once per call, capturing the budget and the variable, as deep_occur_fast_absent already does. The edges it follows and the exceptions it raises are unchanged.

Measured with scripts/compilerBenchmarks.js against the previous commit: allocated words -1.58% in total, stress/TypeAliasChain.res -53%, Belt and runtime files -2% to -2.7%. On a 552 module React app (clean build, one domain) the build allocates 0.05% fewer words. A version that returns a bool instead of raising allocated more than this one (-1.27% in total), because List.for_all takes a closure per list.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-03T09:29:13.297443Z 939eff6 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 939eff643c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.93%. Comparing base (e941d27) to head (dfc6baa).

Files with missing lines Patch % Lines
compiler/ml/ctype.ml 92.30% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9193   +/-   ##
=======================================
  Coverage   81.92%   81.93%           
=======================================
  Files         456      456           
  Lines       65469    65501   +32     
=======================================
+ Hits        53635    53665   +30     
- Misses      11834    11836    +2     
Files with missing lines Coverage Δ
compiler/ml/translcore.ml 88.38% <100.00%> (+0.09%) ⬆️
compiler/ml/ctype.ml 70.37% <92.30%> (+0.12%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9193

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9193

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9193

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9193

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9193

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9193

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9193

commit: dfc6baa

@JonoPrest
JonoPrest marked this pull request as ready for review November 3, 2026 09:46
@JonoPrest
JonoPrest requested a review from cknitt November 3, 2026 09:46
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch 4 times, most recently from 4eb8cf5 to ece2955 Compare November 9, 2026 07:18
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🎉 Compiler benchmarks

Allocated words

In total: 685,970,852 → 650,036,516 (-5.24%).

🎉 Improvement: 112 benchmarks allocate more than 1% less.

Benchmark Base Head Change
stress/TypeAliasChain.res 22,211,288 2,578,624 -88.39%
packages/@rescript/belt/src/Belt_MapDict.res 1,881,855 1,719,204 -8.64%
packages/@rescript/belt/src/Belt_HashMap.res 1,370,729 1,259,667 -8.10%
packages/@rescript/runtime/Dom.res 1,219,256 1,122,873 -7.91%
packages/@rescript/belt/src/Belt_internalMapInt.res 1,613,500 1,486,640 -7.86%
packages/@rescript/belt/src/Belt_internalMapString.res 1,615,341 1,488,478 -7.85%
packages/@rescript/belt/src/Belt_internalAVLtree.res 3,948,378 3,650,637 -7.54%
packages/@rescript/runtime/Stdlib_Result.res 1,426,213 1,320,007 -7.45%
packages/@rescript/belt/src/Belt_MapInt.res 1,339,147 1,243,885 -7.11%
packages/@rescript/belt/src/Belt_MapString.res 1,339,690 1,244,430 -7.11%
208 more changes
Benchmark Base Head Change
packages/@rescript/belt/src/Belt_internalBuckets.res 1,079,095 1,002,889 -7.06%
packages/@rescript/belt/src/Belt_MutableMap.res 1,464,523 1,362,012 -7.00%
packages/@rescript/belt/src/Belt_SetDict.res 1,644,089 1,529,028 -7.00%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,194,013 1,111,348 -6.92%
packages/@rescript/belt/src/Belt_HashMapString.res 1,194,719 1,112,056 -6.92%
packages/@rescript/belt/src/Belt_HashSet.res 1,096,235 1,020,535 -6.91%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,296,550 1,211,936 -6.53%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,296,231 1,211,662 -6.52%
packages/@rescript/belt/src/Belt_MutableSet.res 2,132,033 2,001,434 -6.13%
stress/NestedClosures.res 11,547,196 10,849,489 -6.04%
packages/@rescript/belt/src/Belt_internalAVLset.res 3,179,986 2,992,702 -5.89%
packages/@rescript/belt/src/Belt_Map.res 1,083,641 1,023,060 -5.59%
packages/@rescript/belt/src/Belt_HashSetInt.res 935,203 885,795 -5.28%
packages/@rescript/belt/src/Belt_HashSetString.res 935,514 886,104 -5.28%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 762,359 722,718 -5.20%
packages/@rescript/belt/src/Belt_List.res 3,471,754 3,300,368 -4.94%
packages/@rescript/belt/src/Belt_Array.res 2,364,907 2,248,847 -4.91%
packages/@rescript/belt/src/Belt_MutableSetInt.res 1,835,596 1,746,998 -4.83%
packages/@rescript/belt/src/Belt_MutableSetString.res 1,836,834 1,748,236 -4.82%
stress/LetRec.res 34,866,262 33,210,280 -4.75%
packages/@rescript/belt/src/Belt_Set.res 942,014 897,327 -4.74%
packages/@rescript/belt/src/Belt_SetInt.res 1,417,672 1,352,661 -4.59%
packages/@rescript/belt/src/Belt_SetString.res 1,418,396 1,353,381 -4.58%
packages/@rescript/runtime/Stdlib_List.res 2,918,447 2,785,353 -4.56%
packages/@rescript/belt/src/Belt_Result.res 443,933 424,670 -4.34%
packages/@rescript/belt/src/Belt_SortArray.res 1,567,872 1,499,992 -4.33%
packages/@rescript/belt/src/Belt_SortArrayInt.res 1,451,993 1,391,857 -4.14%
packages/@rescript/belt/src/Belt_SortArrayString.res 1,453,781 1,393,641 -4.14%
packages/@rescript/runtime/Stdlib_AsyncGenerator.res 226,125 216,797 -4.13%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 315,763 303,023 -4.03%
packages/@rescript/runtime/Primitive_object.res 1,243,266 1,195,172 -3.87%
packages/@rescript/belt/src/Belt_MutableQueue.res 851,277 818,421 -3.86%
packages/@rescript/runtime/Stdlib_Option.res 812,085 780,914 -3.84%
packages/@rescript/belt/src/Belt_internalSetInt.res 790,634 760,301 -3.84%
packages/@rescript/belt/src/Belt_internalSetString.res 791,819 761,486 -3.83%
packages/@rescript/belt/src/Belt_MutableStack.res 416,421 400,574 -3.81%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 243,379 234,547 -3.63%
packages/@rescript/runtime/JsxDOM.res 2,833,559 2,731,874 -3.59%
packages/@rescript/belt/src/Belt_internalAVLtree.resi 482,192 465,320 -3.50%
packages/@rescript/runtime/Primitive_option.res 317,329 306,275 -3.48%
packages/@rescript/runtime/Stdlib_Array.res 1,387,413 1,339,418 -3.46%
build/shared-edit 75,488,127 72,905,057 -3.42%
build/no-op 9,860,189 9,523,861 -3.41%
packages/@rescript/runtime/Primitive_module.res 358,493 346,333 -3.39%
packages/@rescript/runtime/Stdlib_Promise.res 378,343 365,977 -3.27%
packages/@rescript/runtime/Stdlib_Promise.resi 377,967 365,846 -3.21%
packages/@rescript/belt/src/Belt_internalBuckets.resi 236,220 228,655 -3.20%
packages/@rescript/runtime/Primitive_hash.res 1,024,750 992,145 -3.18%
packages/@rescript/runtime/Stdlib_Nullable.res 352,095 341,203 -3.09%
packages/@rescript/runtime/Stdlib_Generator.res 211,811 205,297 -3.08%
packages/@rescript/runtime/Stdlib_Null.res 354,055 343,544 -2.97%
build/leaf-edit 13,869,148 13,467,163 -2.90%
packages/@rescript/runtime/Primitive_array.res 202,527 196,816 -2.82%
packages/@rescript/runtime/Primitive_list.res 214,508 208,466 -2.82%
packages/@rescript/runtime/Stdlib_IteratorObject.res 267,667 260,177 -2.80%
packages/@rescript/runtime/Stdlib_Lazy.res 234,088 227,677 -2.74%
packages/@rescript/runtime/JsxDOMStyle.res 2,037,751 1,982,207 -2.73%
packages/@rescript/runtime/Stdlib_IterableIterator.res 188,340 183,296 -2.68%
packages/@rescript/belt/src/Belt_internalAVLset.resi 373,161 363,587 -2.57%
packages/@rescript/runtime/Stdlib_Iterator.res 246,033 239,910 -2.49%
stress/RefsInLoop.res 17,145,475 16,746,607 -2.33%
stress/ListLiteral.res 3,545,575 3,464,554 -2.29%
build/clean 247,984,659 242,369,728 -2.26%
packages/@rescript/belt/src/Belt_Range.res 303,342 296,496 -2.26%
stress/InlineChain.res 3,736,337 3,652,472 -2.24%
packages/@rescript/belt/src/Belt_internalSetBuckets.resi 202,920 198,418 -2.22%
packages/@rescript/belt/src/Belt_List.resi 793,860 776,605 -2.17%
packages/@rescript/runtime/Primitive_exceptions.res 209,263 205,040 -2.02%
packages/@rescript/belt/src/Belt_Option.res 408,989 400,892 -1.98%
packages/@rescript/belt/src/Belt_MapDict.resi 535,243 525,488 -1.82%
packages/@rescript/runtime/Stdlib_Int.res 476,227 467,583 -1.82%
packages/@rescript/belt/src/Belt_Result.resi 249,576 245,178 -1.76%
packages/@rescript/runtime/RescriptTools_Docgen.resi 282,886 278,174 -1.67%
packages/@rescript/runtime/Stdlib_Dict.res 335,109 329,566 -1.65%
packages/@rescript/runtime/RescriptTools_Docgen.res 293,265 288,446 -1.64%
packages/@rescript/runtime/Stdlib_Result.resi 306,815 301,877 -1.61%
packages/@rescript/belt/src/Belt_Array.resi 674,939 664,196 -1.59%
packages/@rescript/runtime/Stdlib_JsExn.res 179,969 177,247 -1.51%
packages/@rescript/runtime/Stdlib_Temporal_Common.res 378,582 372,873 -1.51%
packages/@rescript/runtime/Stdlib_JsError.res 240,527 236,982 -1.47%
packages/@rescript/belt/src/Belt_Map.resi 599,715 590,905 -1.47%
packages/@rescript/belt/src/Belt_internalBucketsType.res 300,880 296,468 -1.47%
packages/@rescript/belt/src/Belt_MutableMap.resi 409,587 403,635 -1.45%
packages/@rescript/runtime/Primitive_string.res 164,858 162,607 -1.37%
packages/@rescript/belt/src/Belt_SetDict.resi 416,142 410,493 -1.36%
packages/@rescript/runtime/Stdlib_AsyncIterator.resi 183,530 181,109 -1.32%
packages/@rescript/runtime/Primitive_int.res 177,556 175,312 -1.26%
packages/@rescript/runtime/Primitive_bigint.res 183,136 180,856 -1.24%
packages/@rescript/belt/src/Belt_Id.res 299,260 295,685 -1.19%
packages/@rescript/runtime/Stdlib_Map.res 214,996 212,472 -1.17%
packages/@rescript/belt/src/Belt_Set.resi 450,104 444,842 -1.17%
packages/@rescript/belt/src/Belt_MutableSet.resi 409,178 404,510 -1.14%
packages/@rescript/runtime/Dom_storage.res 171,345 169,394 -1.14%
packages/@rescript/runtime/Stdlib_AsyncGenerator.resi 170,447 168,512 -1.14%
packages/@rescript/runtime/Stdlib_IteratorObject.resi 242,599 239,905 -1.11%
packages/@rescript/belt/src/Belt_HashMap.resi 305,234 301,850 -1.11%
packages/@rescript/runtime/Stdlib_Nullable.resi 205,592 203,324 -1.10%
packages/@rescript/runtime/Stdlib_Pair.res 169,853 167,991 -1.10%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res 414,949 410,414 -1.09%
packages/@rescript/runtime/Stdlib_Option.resi 242,510 239,894 -1.08%
packages/@rescript/runtime/Stdlib_Null.resi 210,068 207,905 -1.03%
packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res 398,566 394,535 -1.01%
packages/@rescript/runtime/Stdlib_String.res 533,227 527,937 -0.99%
packages/@rescript/belt/src/Belt_HashSet.resi 228,633 226,449 -0.96%
packages/@rescript/belt/src/Belt_MapString.resi 401,415 397,608 -0.95%
packages/@rescript/belt/src/Belt_MapInt.resi 401,463 397,656 -0.95%
packages/@rescript/runtime/Stdlib_List.resi 420,127 416,156 -0.95%
packages/@rescript/runtime/Stdlib_Console.res 387,785 384,323 -0.89%
packages/@rescript/runtime/Stdlib_Type.res 206,368 204,535 -0.89%
packages/@rescript/runtime/Stdlib_Map.resi 222,960 221,034 -0.86%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi 163,630 162,247 -0.85%
packages/@rescript/runtime/Stdlib_Array.resi 748,953 742,662 -0.84%
packages/@rescript/runtime/Primitive_float.res 150,854 149,591 -0.84%
packages/@rescript/runtime/Stdlib_Intl_PluralRules.res 206,049 204,358 -0.82%
packages/@rescript/runtime/Stdlib_Math.res 345,035 342,213 -0.82%
packages/@rescript/runtime/Stdlib_Console.resi 424,585 421,123 -0.82%
packages/@rescript/belt/src/Belt_MutableMapString.resi 322,527 319,902 -0.81%
packages/@rescript/belt/src/Belt_MutableMapInt.resi 322,652 320,027 -0.81%
packages/@rescript/runtime/Stdlib_Set.res 220,377 218,592 -0.81%
packages/@rescript/runtime/Stdlib_Ordering.res 155,378 154,192 -0.76%
packages/@rescript/runtime/Stdlib_Dict.resi 246,311 244,460 -0.75%
packages/@rescript/runtime/Stdlib_IterableIterator.resi 152,861 151,718 -0.75%
packages/@rescript/runtime/Stdlib_Generator.resi 163,979 162,788 -0.73%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 583,642 579,524 -0.71%
packages/@rescript/runtime/Primitive_bool.res 150,624 149,562 -0.71%
packages/@rescript/runtime/Stdlib_JSON.res 308,080 305,930 -0.70%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 388,977 386,285 -0.69%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 379,969 377,406 -0.67%
packages/@rescript/runtime/Stdlib_Float.res 227,352 225,825 -0.67%
packages/@rescript/runtime/Stdlib_BigInt.res 214,235 212,818 -0.66%
packages/@rescript/runtime/Stdlib_Iterator.resi 162,753 161,685 -0.66%
packages/@rescript/runtime/Stdlib_Intl_Collator.res 181,823 180,665 -0.64%
packages/@rescript/belt/src/Belt_HashMapString.resi 218,114 216,761 -0.62%
packages/@rescript/belt/src/Belt_HashMapInt.resi 218,235 216,882 -0.62%
packages/@rescript/runtime/Stdlib_Intl_Locale.res 201,316 200,094 -0.61%
packages/@rescript/belt/src/Belt_internalBucketsType.resi 167,986 166,967 -0.61%
packages/@rescript/runtime/Stdlib_Date.res 580,343 576,897 -0.59%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 272,622 271,088 -0.56%
packages/@rescript/runtime/Stdlib_Set.resi 233,956 232,708 -0.53%
packages/@rescript/belt/src/Belt_Option.resi 247,834 246,556 -0.52%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 312,242 310,647 -0.51%
packages/@rescript/runtime/Stdlib_TypedArray.res 798,092 794,114 -0.50%
packages/@rescript/belt/src/Belt_MutableQueue.resi 211,184 210,158 -0.49%
packages/@rescript/runtime/Primitive_js_extern.res 243,719 242,582 -0.47%
packages/@rescript/runtime/Stdlib_TaggedTemplate.res 117,365 116,818 -0.47%
packages/@rescript/belt/src/Belt_SortArray.resi 258,170 256,988 -0.46%
packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res 183,066 182,252 -0.44%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 409,120 407,320 -0.44%
packages/@rescript/runtime/Stdlib_Temporal_PlainTime.res 324,682 323,256 -0.44%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 443,705 441,824 -0.42%
packages/@rescript/runtime/Stdlib_JSON.resi 240,755 239,756 -0.41%
packages/@rescript/runtime/Pervasives.res 244,019 243,011 -0.41%
packages/@rescript/belt/src/Belt_Float.res 168,477 167,799 -0.40%
packages/@rescript/belt/src/Belt_Int.res 168,912 168,234 -0.40%
packages/@rescript/runtime/Stdlib_Intl_ListFormat.res 172,198 171,511 -0.40%
packages/@rescript/runtime/Stdlib_Bool.res 149,249 148,678 -0.38%
packages/@rescript/runtime/JsxEvent.res 1,116,164 1,112,030 -0.37%
packages/@rescript/runtime/Stdlib_TaggedTemplate.resi 107,679 107,304 -0.35%
packages/@rescript/runtime/Stdlib_Date.resi 626,642 624,476 -0.35%
packages/@rescript/belt/src/Belt_MutableStack.resi 172,020 171,426 -0.35%
packages/@rescript/runtime/Stdlib_WeakMap.res 137,302 136,840 -0.34%
packages/@rescript/belt/src/Belt_MutableSetString.resi 269,193 268,314 -0.33%
packages/@rescript/belt/src/Belt_MutableSetInt.resi 269,315 268,436 -0.33%
packages/@rescript/runtime/Stdlib_String.resi 506,684 505,062 -0.32%
packages/@rescript/belt/src/Belt_SetString.resi 263,929 263,104 -0.31%
packages/@rescript/belt/src/Belt_SetInt.resi 264,063 263,238 -0.31%
packages/@rescript/runtime/Stdlib_RegExp.res 170,368 169,850 -0.30%
packages/@rescript/runtime/Stdlib_Int.resi 208,695 208,076 -0.30%
packages/@rescript/runtime/Stdlib_RegExp.resi 190,659 190,141 -0.27%
packages/@rescript/runtime/Stdlib_Intl_Segmenter.res 156,301 155,879 -0.27%
packages/@rescript/runtime/Jsx.res 123,422 123,092 -0.27%
packages/@rescript/runtime/Stdlib_DataView.res 241,218 240,582 -0.26%
stress/ObjectFields.res 20,800,458 20,745,668 -0.26%
packages/@rescript/belt/src/Belt_Id.resi 263,973 263,289 -0.26%
packages/@rescript/belt/src/Belt_SortArrayString.resi 172,104 171,687 -0.24%
packages/@rescript/belt/src/Belt_SortArrayInt.resi 172,228 171,811 -0.24%
packages/@rescript/runtime/Primitive_dict.res 103,930 103,684 -0.24%
packages/@rescript/runtime/Stdlib_DataView.resi 272,602 271,966 -0.23%
packages/@rescript/runtime/Primitive_array_extern.res 115,724 115,460 -0.23%
packages/@rescript/runtime/Stdlib_Math.resi 334,774 334,036 -0.22%
packages/@rescript/runtime/Stdlib_Intl_Segments.res 117,843 117,589 -0.22%
stress/Record.res 26,266,119 26,211,216 -0.21%
packages/@rescript/runtime/Stdlib_JsError.resi 187,651 187,261 -0.21%
packages/@rescript/runtime/Stdlib_BigInt.resi 188,873 188,486 -0.20%
packages/@rescript/runtime/Primitive_option.resi 123,775 123,523 -0.20%
packages/@rescript/belt/src/Belt_HashSetString.resi 177,936 177,591 -0.19%
packages/@rescript/belt/src/Belt_HashSetInt.resi 178,054 177,709 -0.19%
packages/@rescript/runtime/Stdlib_Iterable.resi 99,346 99,157 -0.19%
packages/@rescript/runtime/Stdlib_AsyncIterable.resi 99,493 99,304 -0.19%
packages/@rescript/runtime/Stdlib_Iterable.res 100,122 99,933 -0.19%
packages/@rescript/runtime/Stdlib_AsyncIterable.res 100,199 100,010 -0.19%
packages/@rescript/runtime/Primitive_object.resi 118,534 118,321 -0.18%
packages/@rescript/belt/src/Belt_Range.resi 178,791 178,473 -0.18%
packages/@rescript/runtime/Stdlib_WeakSet.res 124,392 124,176 -0.17%
packages/@rescript/runtime/Stdlib_Float.resi 196,779 196,446 -0.17%
packages/@rescript/runtime/Stdlib_JsExn.resi 124,255 124,054 -0.16%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat_Grouping.res 161,849 161,589 -0.16%
packages/@rescript/runtime/Stdlib_Object.res 247,971 247,595 -0.15%
packages/@rescript/runtime/Primitive_object_extern.res 115,996 115,834 -0.14%
packages/@rescript/runtime/Stdlib_Lazy.resi 113,681 113,525 -0.14%
packages/@rescript/runtime/Stdlib_Global.res 146,264 146,078 -0.13%
packages/@rescript/runtime/Stdlib_Symbol.res 148,408 148,228 -0.12%
packages/@rescript/runtime/Stdlib_Global.resi 157,646 157,460 -0.12%
packages/@rescript/runtime/RescriptTools_ExtractCodeBlocks.res 113,178 113,052 -0.11%
packages/@rescript/runtime/Stdlib_Symbol.resi 161,723 161,543 -0.11%
packages/@rescript/runtime/Dom_storage2.res 121,212 121,080 -0.11%
packages/@rescript/runtime/Primitive_list.resi 92,899 92,803 -0.10%
packages/@rescript/runtime/Stdlib.res 987,224 986,231 -0.10%

Instructions

In total: 9,675,830,989 → 9,259,635,021 (-4.30%).

🎉 Improvement: 12 benchmarks execute more than 1% fewer instructions.

Benchmark Base Head Change
stress/TypeAliasChain.res 525,391,569 187,073,069 -64.39%
packages/@rescript/belt/src/Belt_internalAVLtree.res 143,296,963 137,272,114 -4.20%
packages/@rescript/belt/src/Belt_List.res 122,764,819 119,263,757 -2.85%
stress/NestedClosures.res 547,145,591 532,421,150 -2.69%
stress/ListLiteral.res 134,848,952 131,543,782 -2.45%
stress/LetRec.res 1,416,825,991 1,383,335,679 -2.36%
packages/@rescript/runtime/JsxDOM.res 120,293,789 117,596,562 -2.24%
stress/RefsInLoop.res 399,990,108 391,097,300 -2.22%
packages/@rescript/runtime/JsxDOMStyle.res 97,375,252 95,556,714 -1.87%
packages/@rescript/belt/src/Belt_List.resi 39,833,071 39,260,070 -1.44%
13 more changes
Benchmark Base Head Change
stress/InlineChain.res 139,874,266 137,994,184 -1.34%
packages/@rescript/belt/src/Belt_Array.resi 34,858,966 34,423,610 -1.25%
packages/@rescript/runtime/Stdlib_Array.resi 40,435,232 40,165,608 -0.67%
packages/@rescript/runtime/Stdlib_List.resi 24,861,230 24,697,270 -0.66%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 31,393,068 31,230,112 -0.52%
packages/@rescript/runtime/Stdlib_TypedArray.res 41,478,161 41,306,488 -0.41%
packages/@rescript/runtime/Stdlib_Date.resi 34,423,798 34,307,665 -0.34%
stress/ObjectFields.res 949,819,511 952,844,605 +0.32%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 24,193,346 24,123,459 -0.29%
packages/@rescript/runtime/Stdlib_String.resi 29,603,925 29,523,200 -0.27%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 25,635,377 25,565,864 -0.27%
packages/@rescript/runtime/Stdlib_Math.resi 21,381,758 21,345,493 -0.17%
stress/Record.res 1,073,756,902 1,072,273,512 -0.14%

Output size

In total: 28,859,904 → 28,860,480 (±0.00%).

None of the 13 benchmarks changed by 0.1% or more.

Pull request dfc6baa compared with base e941d27 in this run.

@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch 2 times, most recently from 699552f to 879a21d Compare November 9, 2026 12:42
Why:
    Every variable link in unification runs `occur`, and the Tvar/Tconstr
    cases also run `deep_occur`. `occur` builds an AVL set of visited
    nodes along every path and `deep_occur` marks every node and walks the
    type again to unmark it, although almost all of these walks see a
    handful of nodes. On a 552 module React app they make 943k and 494k
    calls, about 0.5 s of 14 s single domain CPU.

Both checks now first walk the type with no visited set and no marks,
giving up after 256 nodes (which also stops cycles and heavy sharing);
when the walk gives up, the unchanged full walk runs.
- `occur` in expression mode: a walk over the edges `occur_rec` follows
  that finishes without meeting the variable proves `occur_rec` would
  neither raise nor expand anything. Pattern mode, which looks types up,
  always runs the full walk.
- `deep_occur`: the walk only answers "absent". It gives up on meeting
  the variable and on a node an enclosing walk already marked, so the
  marks left behind are the same as the full walk's.

Review: Can an answer, a diagnostic or an artifact change? No: each fast
    walk either proves the full walk would do nothing more than it did, or
    gives up and the full walk runs from scratch. The new fixtures pin the
    errors, including one past the budget, and the corpus (5,398 artifacts
    and 1,147 .cmt/.cmti) is byte identical to master.

Signed-off-by: Jono Prest <[email protected]>
Why:
    `specialize_primitive` scraped the primitive's whole function type (a
    deep copy plus head expansion) on every use, but only unified
    operators and comparisons look at it. For the other 95% of uses (on a
    552 module React app, 21,582 of 22,770) the result was thrown away
    and `find_builtin` answered.

For a function type whose primitive is in neither table, it now calls
`find_builtin` directly.

Review: Could the skipped scrape have had an effect? Scraping a function
    type looks nothing up: only a Tconstr head is expanded. What is left
    is a copy with fresh type ids, made during Lambda translation after
    the .cmi and .cmt are written, whose result nothing read. The corpus
    artifacts and .cmt files are byte identical to master.

Signed-off-by: Jono Prest <[email protected]>
@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch from 879a21d to 58959d9 Compare November 9, 2026 13:05
Why:
    `occur_reaches` passed `occur_reaches budget ty0` to `iter_type_expr`
    at every node, so the fast walk allocated a partial application per
    node it visited.

`occur_reaches` now defines its walk once per call, capturing the budget
and the variable, as `deep_occur_fast_absent` already does. The edges it
follows and the exceptions it raises are unchanged.

Measured with scripts/compilerBenchmarks.js against the previous commit:
allocated words -1.58% in total, stress/TypeAliasChain.res -53%, Belt
and runtime files -2% to -2.7%. On a 552 module React app (clean build,
one domain) the build allocates 0.05% fewer words. A version that
returns a bool instead of raising allocated more than this one (-1.27%
in total), because `List.for_all` takes a closure per list.

Signed-off-by: Jono Prest <[email protected]>
@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch from 75351f6 to dfc6baa Compare November 9, 2026 13:11

This branch has not been deployed

No deployments
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