Skip to content

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

Merged
JonoPrest merged 4 commits into
masterfrom
jono/occur-check-fast-path
Nov 10, 2026
Merged

JonoPrest merged 4 commits into
masterfrom
jono/occur-check-fast-path

Conversation

@JonoPrest

@JonoPrest JonoPrest commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem. 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, although almost every call sees only a few nodes: on a 552 module React app they make 943k and 494k calls, about 0.5 s of 14 s single domain CPU. Separately, specialize_primitive copied the primitive's whole function type on every use, though only unified operators and comparisons read it (95% of uses don't).

Change.

  • Both occur checks first try a fast bounded walk: no visited set and no marks, giving up after 256 nodes (which also stops cycles and heavy sharing). If it finishes, its answer is final. If it gives up, the existing walk runs unchanged, so the unlucky case costs at most 256 extra node visits.
  • The fast walks allocate nothing: the budget is passed as an integer and the edges of iter_type_expr are written out with an exhaustive match, so a new type constructor is a compile error rather than a missed edge.
  • specialize_primitive calls find_builtin directly for a function type whose primitive is neither a unified operator nor a comparison.

Why it is safe.

  • occur, expression mode only: a fast walk that finishes without meeting the variable proves occur_rec would neither raise nor expand anything. Pattern mode looks types up, so it always takes the full walk.
  • deep_occur: the fast walk only answers "absent". It gives up on meeting the variable and on nodes an enclosing walk already marked, so the marks left behind are the same as the full walk's.
  • Scraping a function type looks nothing up, so skipping it only skips a copy, made during translation after .cmi and .cmt are written.

Results.

measurement this PR / master
compiler benchmarks, allocated words -6.2%
compiler benchmarks, instructions -4.7%
stress/TypeAliasChain.res, allocated words -74%
React app, 552 modules, clean build, 1 domain (wall / CPU) 0.96 / 0.96
backend service, 391 modules, clean build, 1 domain 0.94 / 0.94
library, clean build, 1 domain 0.99 / 0.98

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. New super_errors fixtures cover occur check errors through a function type, through a type constructor argument, and past the 256 node budget; their output matches master. make test and make test-rewatch pass.

Checklist

  • Added a changelog fragment (see changes/README.md), or no release notes are needed and a maintainer should apply changelog:skip.
  • Added tests for the fix or feature.
  • Gave every commit a description below its subject, saying why it is made (see "Commit messages" in CONTRIBUTING.md).
  • Signed off every commit for the DCO (git commit -s).

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

Signed-off-by: Jono Prest <jjprest@gmail.com>
@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

4 commits · Full page

Try a cheap bounded walk before the full occur checks

0dd61d0 · 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

184ee39 · 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

6c4bc78 · JonoPrest · Nov 09, 2026, 13:10 UTC

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.

Walk the occur fast paths without allocating

679fbb9 · JonoPrest · Nov 09, 2026, 14:33 UTC

Why

The bounded walks that occur and deep_occur try first allocated a ref for their budget and a closure for the walk on every call, and iter_type_expr allocated another closure at each arrow type they visited. Unification runs occur for every variable it links, mostly on small types, so this allocation was a large share of what the fast paths cost.

occur_reaches and deep_occur_fast_absent now walk the type with mutually recursive functions that take the budget left and return what remains, following the edges of iter_type_expr written out by constructor. The match on type_desc lists every constructor, so a new one is a compile error here rather than a missed edge. Only deep_occur_fast_absent at a Tvariant still goes through iter_type_expr with a closure, for the row's fields. The nodes visited, the order, the budget and the exceptions raised are unchanged.

Measured with scripts/compilerBenchmarks.js against the previous commit: allocated words -1.03% in total (-1.14% without the project builds), stress/TypeAliasChain.res -8.3%, Belt map and set modules -2.7% to -3.4%. On a 552 module React app (clean build, one domain) the build allocates 0.99% fewer words. Keeping the closures and only skipping the walk for a leaf type (a variable or a constructor without arguments) gave -0.57% in total and -0.54% on the app.

Review

  • Can a result change? No: a walk that finishes visited exactly the nodes the closure version visited, within the same budget; one that gives up still falls back to the full check.

@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 91.37931% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.97%. Comparing base (dcedd5d) to head (86cf87a).

Files with missing lines Patch % Lines
compiler/ml/ctype.ml 90.38% 5 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9193   +/-   ##
=======================================
  Coverage   81.97%   81.97%           
=======================================
  Files         456      456           
  Lines       65613    65671   +58     
=======================================
+ Hits        53783    53836   +53     
- Misses      11830    11835    +5     
Files with missing lines Coverage Δ
compiler/ml/translcore.ml 88.38% <100.00%> (+0.09%) ⬆️
compiler/ml/ctype.ml 70.58% <90.38%> (+0.32%) ⬆️

... 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: 86cf87a

@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: 683,552,659 → 640,795,881 (-6.26%).

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

Benchmark Base Head Change
stress/TypeAliasChain.res 22,208,211 2,360,682 -89.37%
packages/@rescript/runtime/Dom.res 1,210,844 1,068,603 -11.75%
packages/@rescript/belt/src/Belt_MapDict.res 1,875,071 1,656,908 -11.63%
packages/@rescript/belt/src/Belt_internalMapInt.res 1,610,677 1,433,498 -11.00%
packages/@rescript/belt/src/Belt_internalMapString.res 1,612,527 1,435,351 -10.99%
packages/@rescript/belt/src/Belt_HashMap.res 1,362,983 1,219,648 -10.52%
packages/@rescript/belt/src/Belt_MapInt.res 1,333,449 1,198,354 -10.13%
packages/@rescript/belt/src/Belt_MapString.res 1,333,987 1,198,890 -10.13%
packages/@rescript/belt/src/Belt_SetDict.res 1,634,169 1,469,668 -10.07%
packages/@rescript/belt/src/Belt_internalAVLtree.res 3,941,284 3,555,284 -9.79%
233 more changes
Benchmark Base Head Change
packages/@rescript/runtime/Stdlib_Result.res 1,418,710 1,280,486 -9.74%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,190,629 1,078,033 -9.46%
packages/@rescript/belt/src/Belt_HashMapString.res 1,191,337 1,078,743 -9.45%
packages/@rescript/belt/src/Belt_MutableMap.res 1,459,148 1,321,866 -9.41%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,291,463 1,171,538 -9.29%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,291,746 1,171,815 -9.28%
packages/@rescript/belt/src/Belt_HashSet.res 1,092,814 993,108 -9.12%
packages/@rescript/belt/src/Belt_internalBuckets.res 1,076,130 978,938 -9.03%
stress/NestedClosures.res 11,543,225 10,519,868 -8.87%
packages/@rescript/belt/src/Belt_MutableSet.res 2,121,669 1,937,135 -8.70%
packages/@rescript/belt/src/Belt_internalAVLset.res 3,174,101 2,917,053 -8.10%
packages/@rescript/belt/src/Belt_MutableSetInt.res 1,830,853 1,694,015 -7.47%
packages/@rescript/belt/src/Belt_MutableSetString.res 1,832,096 1,695,258 -7.47%
packages/@rescript/belt/src/Belt_Map.res 1,073,930 994,793 -7.37%
packages/@rescript/belt/src/Belt_HashSetInt.res 932,283 863,749 -7.35%
packages/@rescript/belt/src/Belt_HashSetString.res 932,602 864,066 -7.35%
packages/@rescript/belt/src/Belt_SetInt.res 1,413,088 1,310,302 -7.27%
packages/@rescript/belt/src/Belt_SetString.res 1,413,817 1,311,027 -7.27%
stress/LetRec.res 34,771,631 32,331,677 -7.02%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 759,864 706,850 -6.98%
packages/@rescript/belt/src/Belt_Array.res 2,349,207 2,192,976 -6.65%
packages/@rescript/belt/src/Belt_Set.res 932,088 870,613 -6.60%
packages/@rescript/belt/src/Belt_List.res 3,451,343 3,225,060 -6.56%
packages/@rescript/belt/src/Belt_Result.res 439,339 412,290 -6.16%
packages/@rescript/belt/src/Belt_internalSetInt.res 788,501 741,386 -5.98%
packages/@rescript/belt/src/Belt_internalSetString.res 789,694 742,577 -5.97%
packages/@rescript/runtime/Stdlib_List.res 2,903,131 2,730,832 -5.93%
packages/@rescript/belt/src/Belt_SortArray.res 1,563,066 1,471,640 -5.85%
packages/@rescript/belt/src/Belt_SortArrayInt.res 1,448,282 1,363,993 -5.82%
packages/@rescript/belt/src/Belt_SortArrayString.res 1,450,080 1,365,791 -5.81%
packages/@rescript/belt/src/Belt_internalAVLtree.resi 480,017 452,708 -5.69%
packages/@rescript/belt/src/Belt_MutableStack.res 413,965 390,695 -5.62%
packages/@rescript/runtime/JsxDOM.res 2,832,022 2,673,396 -5.60%
packages/@rescript/belt/src/Belt_MutableQueue.res 847,634 800,514 -5.56%
packages/@rescript/runtime/Stdlib_AsyncGenerator.res 224,175 211,739 -5.55%
packages/@rescript/runtime/Stdlib_Option.res 806,317 762,733 -5.41%
packages/@rescript/runtime/Primitive_object.res 1,240,833 1,174,425 -5.35%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 312,594 296,292 -5.22%
packages/@rescript/runtime/Stdlib_Promise.res 371,448 352,269 -5.16%
packages/@rescript/runtime/Stdlib_Promise.resi 376,507 357,602 -5.02%
packages/@rescript/runtime/JsxDOMStyle.res 2,036,625 1,935,126 -4.98%
packages/@rescript/runtime/Stdlib_IteratorObject.res 264,450 251,327 -4.96%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 241,203 229,257 -4.95%
packages/@rescript/belt/src/Belt_internalBuckets.resi 234,499 223,014 -4.90%
packages/@rescript/runtime/Stdlib_Array.res 1,361,912 1,296,515 -4.80%
packages/@rescript/runtime/Primitive_option.res 315,661 300,637 -4.76%
build/shared-edit 75,191,251 71,648,339 -4.71%
build/no-op 9,848,483 9,391,719 -4.64%
packages/@rescript/runtime/Primitive_module.res 356,839 340,830 -4.49%
packages/@rescript/runtime/Stdlib_Nullable.res 347,588 332,357 -4.38%
packages/@rescript/runtime/Primitive_hash.res 1,022,797 978,081 -4.37%
packages/@rescript/belt/src/Belt_internalAVLset.resi 371,093 355,037 -4.33%
packages/@rescript/runtime/Stdlib_Generator.res 209,917 200,845 -4.32%
packages/@rescript/runtime/Stdlib_Null.res 349,603 334,776 -4.24%
stress/ListLiteral.res 3,544,449 3,400,400 -4.06%
build/leaf-edit 13,851,969 13,294,766 -4.02%
packages/@rescript/belt/src/Belt_List.resi 791,279 759,884 -3.97%
packages/@rescript/runtime/Stdlib_IterableIterator.res 186,264 179,055 -3.87%
packages/@rescript/runtime/Stdlib_Lazy.res 231,686 222,767 -3.85%
packages/@rescript/runtime/Primitive_array.res 201,292 193,776 -3.73%
packages/@rescript/runtime/Primitive_list.res 213,196 205,375 -3.67%
packages/@rescript/belt/src/Belt_MapDict.resi 533,107 513,911 -3.60%
packages/@rescript/belt/src/Belt_internalSetBuckets.resi 201,245 194,153 -3.52%
stress/InlineChain.res 3,727,128 3,599,402 -3.43%
packages/@rescript/runtime/Stdlib_Iterator.res 243,369 235,554 -3.21%
stress/RefsInLoop.res 17,141,783 16,594,154 -3.19%
packages/@rescript/belt/src/Belt_Result.resi 247,960 240,104 -3.17%
packages/@rescript/belt/src/Belt_Array.resi 672,434 651,387 -3.13%
build/clean 246,759,308 239,118,590 -3.10%
packages/@rescript/belt/src/Belt_Range.res 300,432 291,395 -3.01%
packages/@rescript/runtime/Stdlib_Result.resi 305,424 296,377 -2.96%
packages/@rescript/runtime/RescriptTools_Docgen.resi 281,500 273,197 -2.95%
packages/@rescript/belt/src/Belt_Map.resi 597,409 579,876 -2.93%
packages/@rescript/belt/src/Belt_MutableMap.resi 407,490 395,658 -2.90%
packages/@rescript/runtime/RescriptTools_Docgen.res 291,788 283,378 -2.88%
packages/@rescript/belt/src/Belt_Option.res 403,611 392,120 -2.85%
packages/@rescript/runtime/Stdlib_Temporal_Common.res 376,573 365,971 -2.82%
packages/@rescript/runtime/Primitive_exceptions.res 207,901 202,122 -2.78%
packages/@rescript/belt/src/Belt_SetDict.resi 414,166 403,001 -2.70%
packages/@rescript/runtime/Stdlib_Int.res 470,160 457,929 -2.60%
packages/@rescript/runtime/Stdlib_AsyncIterator.resi 182,203 177,696 -2.47%
packages/@rescript/runtime/Stdlib_IteratorObject.resi 241,227 235,362 -2.43%
packages/@rescript/runtime/Stdlib_Dict.res 328,030 320,203 -2.39%
packages/@rescript/belt/src/Belt_Set.resi 447,971 437,571 -2.32%
packages/@rescript/belt/src/Belt_MutableSet.resi 407,088 397,681 -2.31%
packages/@rescript/belt/src/Belt_HashMap.resi 303,382 296,841 -2.16%
packages/@rescript/runtime/Stdlib_Nullable.resi 204,247 199,872 -2.14%
packages/@rescript/runtime/Stdlib_AsyncGenerator.resi 169,209 165,608 -2.13%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res 412,808 404,073 -2.12%
packages/@rescript/belt/src/Belt_internalBucketsType.res 299,013 292,694 -2.11%
packages/@rescript/runtime/Stdlib_Map.res 210,049 205,629 -2.10%
packages/@rescript/runtime/Stdlib_JsExn.res 177,282 173,566 -2.10%
packages/@rescript/runtime/Stdlib_JsError.res 235,345 230,562 -2.03%
packages/@rescript/runtime/Primitive_int.res 176,287 172,711 -2.03%
packages/@rescript/runtime/Primitive_string.res 163,632 160,326 -2.02%
packages/@rescript/runtime/Primitive_bigint.res 181,841 178,187 -2.01%
packages/@rescript/runtime/Stdlib_Null.resi 208,735 204,556 -2.00%
packages/@rescript/runtime/Stdlib_Option.resi 241,144 236,393 -1.97%
packages/@rescript/runtime/Stdlib_List.resi 418,354 410,239 -1.94%
packages/@rescript/belt/src/Belt_Id.res 296,203 290,467 -1.94%
packages/@rescript/runtime/Stdlib_Console.res 377,726 370,421 -1.93%
packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res 396,537 388,880 -1.93%
packages/@rescript/belt/src/Belt_MapString.resi 399,342 391,664 -1.92%
packages/@rescript/belt/src/Belt_MapInt.resi 399,385 391,707 -1.92%
packages/@rescript/belt/src/Belt_HashSet.resi 226,843 222,650 -1.85%
packages/@rescript/runtime/Stdlib_Array.resi 746,634 733,322 -1.78%
packages/@rescript/runtime/Stdlib_Console.resi 422,754 415,449 -1.73%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi 162,395 159,633 -1.70%
packages/@rescript/runtime/Stdlib_Map.resi 221,621 217,861 -1.70%
packages/@rescript/runtime/Stdlib_String.res 514,012 505,388 -1.68%
packages/@rescript/belt/src/Belt_MutableMapString.resi 320,569 315,200 -1.67%
packages/@rescript/runtime/Dom_storage.res 169,942 167,096 -1.67%
packages/@rescript/belt/src/Belt_MutableMapInt.resi 320,687 315,318 -1.67%
packages/@rescript/runtime/Stdlib_Pair.res 168,525 165,831 -1.60%
packages/@rescript/runtime/Stdlib_IterableIterator.resi 151,641 149,231 -1.59%
packages/@rescript/runtime/Stdlib_Intl_PluralRules.res 204,565 201,334 -1.58%
packages/@rescript/runtime/Stdlib_Set.res 214,234 210,896 -1.56%
packages/@rescript/runtime/Stdlib_Generator.resi 162,749 160,256 -1.53%
packages/@rescript/runtime/Stdlib_Dict.resi 244,911 241,324 -1.46%
packages/@rescript/runtime/Stdlib_Iterator.resi 161,455 159,134 -1.44%
packages/@rescript/runtime/Primitive_float.res 149,645 147,516 -1.42%
packages/@rescript/runtime/Stdlib_JSON.res 302,239 297,965 -1.41%
packages/@rescript/runtime/Primitive_bool.res 149,417 147,391 -1.36%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 581,114 573,314 -1.34%
packages/@rescript/runtime/Stdlib_Math.res 332,110 327,665 -1.34%
packages/@rescript/runtime/Stdlib_Type.res 204,444 201,731 -1.33%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 387,018 382,079 -1.28%
packages/@rescript/belt/src/Belt_HashMapString.resi 216,385 213,646 -1.27%
packages/@rescript/belt/src/Belt_HashMapInt.resi 216,500 213,761 -1.27%
packages/@rescript/runtime/Stdlib_Intl_Locale.res 199,881 197,371 -1.26%
packages/@rescript/runtime/Stdlib_Intl_Collator.res 180,384 178,148 -1.24%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 377,976 373,341 -1.23%
packages/@rescript/runtime/Stdlib_Set.resi 232,564 229,853 -1.17%
packages/@rescript/runtime/Stdlib_Date.res 558,990 552,492 -1.16%
packages/@rescript/belt/src/Belt_internalBucketsType.resi 166,434 164,547 -1.13%
packages/@rescript/belt/src/Belt_Option.resi 246,170 243,394 -1.13%
packages/@rescript/runtime/Stdlib_Ordering.res 154,046 152,309 -1.13%
packages/@rescript/runtime/Stdlib_BigInt.res 209,476 207,140 -1.12%
packages/@rescript/runtime/Stdlib_TypedArray.res 795,026 786,253 -1.10%
packages/@rescript/runtime/Stdlib_Float.res 221,240 218,812 -1.10%
packages/@rescript/belt/src/Belt_MutableQueue.resi 209,492 207,262 -1.06%
packages/@rescript/belt/src/Belt_SortArray.resi 256,541 253,973 -1.00%
packages/@rescript/runtime/Stdlib_JSON.resi 239,282 236,911 -0.99%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 270,970 268,323 -0.98%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 310,488 307,619 -0.92%
packages/@rescript/runtime/Pervasives.res 242,003 239,833 -0.90%
packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res 181,549 179,930 -0.89%
packages/@rescript/runtime/Stdlib_Intl_ListFormat.res 170,718 169,289 -0.84%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 407,039 403,636 -0.84%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 441,498 437,853 -0.83%
packages/@rescript/runtime/Primitive_js_extern.res 241,489 239,512 -0.82%
packages/@rescript/runtime/JsxEvent.res 1,104,000 1,094,963 -0.82%
packages/@rescript/runtime/Stdlib_Temporal_PlainTime.res 322,879 320,291 -0.80%
packages/@rescript/belt/src/Belt_MutableSetString.resi 267,242 265,131 -0.79%
packages/@rescript/belt/src/Belt_MutableSetInt.resi 267,359 265,248 -0.79%
packages/@rescript/runtime/Stdlib_Date.resi 624,268 619,498 -0.76%
packages/@rescript/runtime/Stdlib_String.resi 504,688 500,847 -0.76%
packages/@rescript/belt/src/Belt_SetString.resi 262,017 260,023 -0.76%
packages/@rescript/belt/src/Belt_SetInt.resi 262,144 260,150 -0.76%
packages/@rescript/belt/src/Belt_MutableStack.resi 170,428 169,134 -0.76%
packages/@rescript/runtime/Stdlib_WeakMap.res 136,058 135,050 -0.74%
packages/@rescript/runtime/Stdlib_Bool.res 147,200 146,119 -0.73%
packages/@rescript/belt/src/Belt_SortArrayString.resi 170,583 169,347 -0.72%
packages/@rescript/belt/src/Belt_SortArrayInt.resi 170,701 169,465 -0.72%
packages/@rescript/runtime/Stdlib_TaggedTemplate.resi 106,559 105,855 -0.66%
packages/@rescript/runtime/Stdlib_RegExp.res 164,130 163,059 -0.65%
packages/@rescript/runtime/Stdlib_TaggedTemplate.res 115,787 115,035 -0.65%
packages/@rescript/belt/src/Belt_Float.res 165,952 164,890 -0.64%
packages/@rescript/runtime/Stdlib_Int.resi 207,249 205,923 -0.64%
packages/@rescript/belt/src/Belt_Int.res 166,621 165,559 -0.64%
packages/@rescript/runtime/Stdlib_DataView.res 231,761 230,376 -0.60%
packages/@rescript/belt/src/Belt_Id.resi 261,547 260,016 -0.59%
packages/@rescript/runtime/Jsx.res 122,098 121,406 -0.57%
packages/@rescript/runtime/Stdlib_Intl_Segmenter.res 154,892 154,015 -0.57%
packages/@rescript/runtime/Stdlib_RegExp.resi 189,282 188,211 -0.57%
packages/@rescript/runtime/Stdlib_Math.resi 332,942 331,231 -0.51%
packages/@rescript/runtime/Stdlib_DataView.resi 271,148 269,763 -0.51%
packages/@rescript/runtime/Primitive_array_extern.res 114,530 113,951 -0.51%
packages/@rescript/belt/src/Belt_HashSetString.resi 176,274 175,425 -0.48%
packages/@rescript/belt/src/Belt_HashSetInt.resi 176,387 175,538 -0.48%
packages/@rescript/runtime/Stdlib_JsError.resi 186,162 185,268 -0.48%
packages/@rescript/runtime/Stdlib_BigInt.resi 187,480 186,582 -0.48%
stress/ObjectFields.res 20,792,126 20,697,729 -0.45%
packages/@rescript/runtime/Stdlib_JsExn.resi 123,052 122,494 -0.45%
packages/@rescript/runtime/Primitive_option.resi 122,540 122,008 -0.43%
packages/@rescript/runtime/Stdlib_Object.res 246,460 245,405 -0.43%
packages/@rescript/runtime/Primitive_dict.res 102,794 102,359 -0.42%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat_Grouping.res 160,578 159,923 -0.41%
packages/@rescript/belt/src/Belt_Range.resi 177,268 176,572 -0.39%
packages/@rescript/runtime/Stdlib_Intl_Segments.res 116,632 116,175 -0.39%
packages/@rescript/runtime/Stdlib_Float.resi 195,361 194,601 -0.39%
packages/@rescript/runtime/Stdlib_WeakSet.res 123,164 122,689 -0.39%
packages/@rescript/runtime/Stdlib_Iterable.resi 98,228 97,864 -0.37%
packages/@rescript/runtime/Stdlib_AsyncIterable.resi 98,375 98,011 -0.37%
packages/@rescript/runtime/Stdlib_Iterable.res 98,918 98,554 -0.37%
packages/@rescript/runtime/Stdlib_AsyncIterable.res 98,989 98,625 -0.37%
packages/@rescript/runtime/Primitive_object.resi 117,272 116,852 -0.36%
stress/Record.res 26,259,755 26,166,724 -0.35%
packages/@rescript/runtime/Stdlib_Symbol.res 144,612 144,152 -0.32%
packages/@rescript/runtime/Primitive_object_extern.res 114,734 114,376 -0.31%
packages/@rescript/runtime/Stdlib_Lazy.resi 112,504 112,159 -0.31%
packages/@rescript/runtime/Stdlib_Global.res 141,938 141,528 -0.29%
packages/@rescript/runtime/Stdlib_Symbol.resi 160,375 159,915 -0.29%
packages/@rescript/runtime/Dom_storage2.res 119,942 119,607 -0.28%
packages/@rescript/runtime/Stdlib_Global.resi 156,337 155,927 -0.26%
packages/@rescript/runtime/Stdlib_Bool.resi 116,764 116,498 -0.23%
packages/@rescript/runtime/Stdlib_Type.resi 135,887 135,581 -0.23%
packages/@rescript/runtime/RescriptTools_ExtractCodeBlocks.res 112,006 111,768 -0.21%
packages/@rescript/runtime/Primitive_list.resi 91,811 91,617 -0.21%
packages/@rescript/belt/src/Belt_Int.resi 149,758 149,457 -0.20%
packages/@rescript/runtime/Stdlib_Int8Array.res 344,396 343,706 -0.20%
packages/@rescript/runtime/Stdlib_Uint8Array.res 344,411 343,721 -0.20%
packages/@rescript/runtime/Stdlib_Float32Array.res 344,514 343,824 -0.20%
packages/@rescript/runtime/Stdlib_Float64Array.res 344,514 343,824 -0.20%
packages/@rescript/runtime/Stdlib_Int16Array.res 344,543 343,853 -0.20%
packages/@rescript/runtime/Stdlib_Int32Array.res 344,545 343,855 -0.20%
packages/@rescript/runtime/Stdlib_Uint16Array.res 344,550 343,860 -0.20%
packages/@rescript/runtime/Stdlib_Uint32Array.res 344,550 343,860 -0.20%
packages/@rescript/runtime/Stdlib_BigInt64Array.res 344,576 343,886 -0.20%
packages/@rescript/runtime/Stdlib_BigUint64Array.res 344,650 343,960 -0.20%
packages/@rescript/runtime/Stdlib_Uint8ClampedArray.res 344,926 344,236 -0.20%
packages/@rescript/belt/src/Belt_Float.resi 151,234 150,933 -0.20%
packages/@rescript/runtime/Primitive_hash.resi 102,684 102,495 -0.18%
packages/@rescript/runtime/Primitive_module.resi 103,101 102,912 -0.18%
packages/@rescript/runtime/Stdlib.res 984,362 982,576 -0.18%
packages/@rescript/runtime/Primitive_exceptions.resi 100,194 100,034 -0.16%
packages/@rescript/runtime/Primitive_promise.res 96,873 96,723 -0.15%
packages/@rescript/runtime/Stdlib_Intl.res 174,248 173,988 -0.15%
packages/@rescript/runtime/Obj.res 98,908 98,793 -0.12%
packages/@rescript/runtime/Stdlib_ArrayBuffer.res 107,226 107,102 -0.12%
packages/@rescript/runtime/Stdlib_ArrayBuffer.resi 110,946 110,822 -0.11%
packages/@rescript/runtime/Primitive_string_extern.res 97,997 97,891 -0.11%
packages/@rescript/runtime/Stdlib_Temporal_Now.res 143,171 143,021 -0.10%

Instructions

In total: 9,664,672,172 → 9,211,217,327 (-4.69%).

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

Benchmark Base Head Change
stress/TypeAliasChain.res 525,256,483 171,814,968 -67.29%
packages/@rescript/belt/src/Belt_internalAVLtree.res 142,735,364 135,814,563 -4.85%
packages/@rescript/belt/src/Belt_List.res 121,661,773 117,626,305 -3.32%
stress/NestedClosures.res 546,868,023 529,348,832 -3.20%
stress/LetRec.res 1,412,349,753 1,372,951,767 -2.79%
stress/ListLiteral.res 134,742,076 131,005,969 -2.77%
packages/@rescript/runtime/JsxDOM.res 120,169,601 116,984,784 -2.65%
stress/RefsInLoop.res 399,652,127 389,433,141 -2.56%
packages/@rescript/runtime/JsxDOMStyle.res 97,177,924 95,077,071 -2.16%
packages/@rescript/belt/src/Belt_List.resi 39,658,986 38,957,631 -1.77%
13 more changes
Benchmark Base Head Change
stress/InlineChain.res 139,353,280 137,127,404 -1.60%
packages/@rescript/belt/src/Belt_Array.resi 34,160,653 33,667,597 -1.44%
packages/@rescript/runtime/Stdlib_Array.resi 40,252,025 39,934,581 -0.79%
packages/@rescript/runtime/Stdlib_List.resi 24,702,435 24,512,749 -0.77%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 31,210,384 31,029,751 -0.58%
stress/ObjectFields.res 950,039,987 944,692,772 -0.56%
packages/@rescript/runtime/Stdlib_TypedArray.res 41,282,880 41,090,423 -0.47%
packages/@rescript/runtime/Stdlib_Date.resi 34,227,146 34,100,359 -0.37%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 24,041,157 23,967,079 -0.31%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 25,478,003 25,403,219 -0.29%
packages/@rescript/runtime/Stdlib_String.resi 29,427,061 29,344,392 -0.28%
packages/@rescript/runtime/Stdlib_Math.resi 21,220,107 21,178,522 -0.20%
stress/Record.res 1,073,226,829 1,071,462,741 -0.16%

Output size

In total: 25,569,598 → 25,570,980 (+0.01%).

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

Pull request 679fbb9 compared with base b82d944 in this run.

@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch 6 times, most recently from 679fbb9 to 2c6629c Compare November 9, 2026 22:04
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

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

Semantic Diff

4 commits · Full page

Try a cheap bounded walk before the full occur checks

aa25c9e · 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

f987a7d · 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

2962050 · JonoPrest · Nov 09, 2026, 13:10 UTC

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.

Walk the occur fast paths without allocating

86cf87a · JonoPrest · Nov 09, 2026, 14:33 UTC

Why

The bounded walks that occur and deep_occur try first allocated a ref for their budget and a closure for the walk on every call, and iter_type_expr allocated another closure at each arrow type they visited. Unification runs occur for every variable it links, mostly on small types, so this allocation was a large share of what the fast paths cost.

occur_reaches and deep_occur_fast_absent now walk the type with mutually recursive functions that take the budget left and return what remains, following the edges of iter_type_expr written out by constructor. The match on type_desc lists every constructor, so a new one is a compile error here rather than a missed edge. Only deep_occur_fast_absent at a Tvariant still goes through iter_type_expr with a closure, for the row's fields. The nodes visited, the order, the budget and the exceptions raised are unchanged.

Measured with scripts/compilerBenchmarks.js against the previous commit: allocated words -1.03% in total (-1.14% without the project builds), stress/TypeAliasChain.res -8.3%, Belt map and set modules -2.7% to -3.4%. On a 552 module React app (clean build, one domain) the build allocates 0.99% fewer words. Keeping the closures and only skipping the walk for a leaf type (a variable or a constructor without arguments) gave -0.57% in total and -0.54% on the app.

Review

  • Can a result change? No: a walk that finishes visited exactly the nodes the closure version visited, within the same budget; one that gives up still falls back to the full check.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🎉 Compiler benchmarks

Allocated words

In total: 683,220,137 → 640,767,501 (-6.21%).

Generated projects: 923,206,562 → 920,854,584 (-0.25%).

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

Benchmark Base Head Change
stress/TypeAliasChain.res 22,208,214 2,360,685 -89.37%
packages/@rescript/runtime/Dom.res 1,210,810 1,068,573 -11.75%
packages/@rescript/belt/src/Belt_MapDict.res 1,871,993 1,654,954 -11.59%
packages/@rescript/belt/src/Belt_internalMapInt.res 1,608,163 1,431,975 -10.96%
packages/@rescript/belt/src/Belt_internalMapString.res 1,610,011 1,433,825 -10.94%
packages/@rescript/belt/src/Belt_HashMap.res 1,359,437 1,217,470 -10.44%
packages/@rescript/belt/src/Belt_MapInt.res 1,331,271 1,196,896 -10.09%
packages/@rescript/belt/src/Belt_MapString.res 1,331,803 1,197,432 -10.09%
packages/@rescript/belt/src/Belt_SetDict.res 1,631,791 1,468,164 -10.03%
packages/@rescript/belt/src/Belt_internalAVLtree.res 3,933,418 3,550,634 -9.73%
236 more changes
Benchmark Base Head Change
packages/@rescript/runtime/Stdlib_Result.res 1,417,652 1,279,780 -9.73%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,188,191 1,076,499 -9.40%
packages/@rescript/belt/src/Belt_HashMapString.res 1,188,901 1,077,211 -9.39%
packages/@rescript/belt/src/Belt_MutableMap.res 1,456,456 1,320,136 -9.36%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,289,119 1,170,024 -9.24%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,289,400 1,170,305 -9.24%
packages/@rescript/belt/src/Belt_HashSet.res 1,090,460 991,640 -9.06%
packages/@rescript/belt/src/Belt_internalBuckets.res 1,073,636 977,478 -8.96%
packages/@rescript/belt/src/Belt_MutableSet.res 2,116,807 1,934,129 -8.63%
stress/NestedClosures.res 11,440,640 10,458,075 -8.59%
packages/@rescript/belt/src/Belt_internalAVLset.res 3,166,947 2,912,820 -8.02%
packages/@rescript/belt/src/Belt_MutableSetInt.res 1,826,887 1,691,533 -7.41%
packages/@rescript/belt/src/Belt_MutableSetString.res 1,828,130 1,692,776 -7.40%
packages/@rescript/belt/src/Belt_Map.res 1,071,556 993,237 -7.31%
packages/@rescript/belt/src/Belt_HashSetInt.res 930,277 862,485 -7.29%
packages/@rescript/belt/src/Belt_HashSetString.res 930,590 862,804 -7.28%
packages/@rescript/belt/src/Belt_SetInt.res 1,410,950 1,308,940 -7.23%
packages/@rescript/belt/src/Belt_SetString.res 1,411,679 1,309,669 -7.23%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 758,272 705,903 -6.91%
stress/LetRec.res 34,491,626 32,163,672 -6.75%
packages/@rescript/belt/src/Belt_Set.res 930,370 869,443 -6.55%
packages/@rescript/belt/src/Belt_Array.res 2,339,392 2,186,790 -6.52%
packages/@rescript/belt/src/Belt_List.res 3,442,085 3,219,410 -6.47%
packages/@rescript/belt/src/Belt_Result.res 438,885 411,964 -6.13%
packages/@rescript/belt/src/Belt_internalSetInt.res 787,177 740,576 -5.92%
packages/@rescript/belt/src/Belt_internalSetString.res 788,368 741,767 -5.91%
packages/@rescript/runtime/Stdlib_List.res 2,895,341 2,726,170 -5.84%
packages/@rescript/belt/src/Belt_SortArray.res 1,557,297 1,468,180 -5.72%
packages/@rescript/belt/src/Belt_SortArrayInt.res 1,443,518 1,361,145 -5.71%
packages/@rescript/belt/src/Belt_SortArrayString.res 1,445,318 1,362,941 -5.70%
packages/@rescript/belt/src/Belt_internalAVLtree.resi 479,327 452,018 -5.70%
packages/@rescript/belt/src/Belt_MutableStack.res 413,391 390,329 -5.58%
packages/@rescript/runtime/JsxDOM.res 2,819,068 2,663,322 -5.52%
packages/@rescript/belt/src/Belt_MutableQueue.res 846,284 799,664 -5.51%
packages/@rescript/runtime/Stdlib_AsyncGenerator.res 223,817 211,493 -5.51%
packages/@rescript/runtime/Stdlib_Option.res 805,621 762,255 -5.38%
packages/@rescript/runtime/Primitive_object.res 1,236,175 1,171,547 -5.23%
packages/@rescript/runtime/Stdlib_Promise.res 370,826 351,719 -5.15%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 311,764 295,706 -5.15%
packages/@rescript/runtime/Stdlib_Promise.resi 376,073 357,176 -5.02%
packages/@rescript/runtime/JsxDOMStyle.res 2,036,631 1,935,132 -4.98%
packages/@rescript/runtime/Stdlib_IteratorObject.res 264,056 250,973 -4.95%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 240,773 228,959 -4.91%
packages/@rescript/belt/src/Belt_internalBuckets.resi 234,349 222,864 -4.90%
packages/@rescript/runtime/Stdlib_Array.res 1,357,082 1,293,029 -4.72%
build/shared-edit 76,478,943 72,914,007 -4.66%
packages/@rescript/runtime/Primitive_option.res 313,590 299,092 -4.62%
build/no-op 9,753,471 9,304,415 -4.60%
packages/@rescript/runtime/Primitive_module.res 356,021 340,340 -4.40%
packages/@rescript/belt/src/Belt_internalAVLset.resi 370,547 354,491 -4.33%
packages/@rescript/runtime/Stdlib_Nullable.res 346,812 331,825 -4.32%
packages/@rescript/runtime/Stdlib_Generator.res 209,639 200,647 -4.29%
packages/@rescript/runtime/Primitive_hash.res 1,019,965 976,349 -4.28%
packages/@rescript/runtime/Stdlib_Null.res 348,889 334,290 -4.18%
stress/ListLiteral.res 3,544,452 3,400,403 -4.06%
packages/@rescript/belt/src/Belt_List.resi 789,425 758,030 -3.98%
build/leaf-edit 13,741,070 13,196,139 -3.97%
packages/@rescript/runtime/Stdlib_IterableIterator.res 185,970 178,833 -3.84%
packages/@rescript/runtime/Stdlib_Lazy.res 230,796 222,145 -3.75%
packages/@rescript/runtime/Primitive_array.res 200,766 193,438 -3.65%
packages/@rescript/runtime/Primitive_list.res 212,870 205,169 -3.62%
packages/@rescript/belt/src/Belt_MapDict.resi 532,227 513,029 -3.61%
packages/@rescript/belt/src/Belt_internalSetBuckets.resi 201,143 194,051 -3.53%
stress/InlineChain.res 3,707,887 3,586,173 -3.28%
packages/@rescript/belt/src/Belt_Result.resi 247,666 239,810 -3.17%
packages/@rescript/runtime/Stdlib_Iterator.res 242,779 235,116 -3.16%
packages/@rescript/belt/src/Belt_Array.resi 670,820 649,773 -3.14%
stress/RefsInLoop.res 17,105,782 16,572,549 -3.12%
build/clean 246,031,780 238,457,313 -3.08%
packages/@rescript/runtime/Stdlib_Result.resi 305,010 295,963 -2.97%
packages/@rescript/runtime/RescriptTools_Docgen.resi 281,494 273,191 -2.95%
packages/@rescript/belt/src/Belt_Map.resi 596,506 578,948 -2.94%
packages/@rescript/belt/src/Belt_MutableMap.resi 406,788 394,956 -2.91%
packages/@rescript/belt/src/Belt_Range.res 299,486 290,813 -2.90%
packages/@rescript/runtime/RescriptTools_Docgen.res 291,782 283,372 -2.88%
packages/@rescript/runtime/Stdlib_Temporal_Common.res 376,579 365,977 -2.82%
packages/@rescript/belt/src/Belt_Option.res 402,945 391,642 -2.81%
packages/@rescript/runtime/Primitive_exceptions.res 207,507 201,856 -2.72%
packages/@rescript/belt/src/Belt_SetDict.resi 413,596 402,431 -2.70%
packages/@rescript/runtime/Stdlib_Int.res 469,154 457,227 -2.54%
packages/@rescript/runtime/Stdlib_AsyncIterator.resi 182,053 177,546 -2.48%
packages/@rescript/runtime/Stdlib_IteratorObject.resi 240,921 235,056 -2.43%
packages/@rescript/runtime/Stdlib_Dict.res 327,220 319,549 -2.34%
packages/@rescript/belt/src/Belt_Set.resi 447,353 436,953 -2.32%
packages/@rescript/belt/src/Belt_MutableSet.resi 406,470 397,063 -2.31%
packages/@rescript/belt/src/Belt_HashMap.resi 303,052 296,511 -2.16%
packages/@rescript/runtime/Stdlib_Nullable.resi 204,013 199,638 -2.14%
packages/@rescript/runtime/Stdlib_AsyncGenerator.resi 169,131 165,530 -2.13%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res 412,454 403,719 -2.12%
packages/@rescript/runtime/Stdlib_Map.res 209,783 205,383 -2.10%
packages/@rescript/belt/src/Belt_internalBucketsType.res 298,711 292,500 -2.08%
packages/@rescript/runtime/Stdlib_JsExn.res 176,820 173,244 -2.02%
packages/@rescript/runtime/Stdlib_Null.resi 208,501 204,322 -2.00%
packages/@rescript/runtime/Primitive_int.res 175,973 172,497 -1.98%
packages/@rescript/runtime/Stdlib_Option.resi 240,826 236,075 -1.97%
packages/@rescript/runtime/Stdlib_JsError.res 234,639 230,016 -1.97%
packages/@rescript/runtime/Primitive_string.res 163,342 160,132 -1.97%
packages/@rescript/runtime/Primitive_bigint.res 181,503 177,949 -1.96%
packages/@rescript/runtime/Stdlib_List.resi 417,424 409,309 -1.94%
packages/@rescript/runtime/Stdlib_Console.res 377,072 369,767 -1.94%
packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res 396,383 388,726 -1.93%
packages/@rescript/belt/src/Belt_MapString.resi 398,460 390,782 -1.93%
packages/@rescript/belt/src/Belt_MapInt.resi 398,503 390,825 -1.93%
packages/@rescript/belt/src/Belt_Id.res 295,529 289,973 -1.88%
packages/@rescript/belt/src/Belt_HashSet.resi 226,597 222,404 -1.85%
packages/@rescript/runtime/Stdlib_Array.resi 745,104 731,792 -1.79%
packages/@rescript/runtime/Stdlib_Console.resi 422,100 414,795 -1.73%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi 162,293 159,531 -1.70%
packages/@rescript/runtime/Stdlib_Map.resi 221,399 217,639 -1.70%
packages/@rescript/belt/src/Belt_MutableMapString.resi 319,879 314,510 -1.68%
packages/@rescript/belt/src/Belt_MutableMapInt.resi 319,997 314,628 -1.68%
packages/@rescript/runtime/Stdlib_String.res 512,906 504,410 -1.66%
packages/@rescript/runtime/Dom_storage.res 169,684 166,886 -1.65%
packages/@rescript/runtime/Stdlib_IterableIterator.resi 151,539 149,129 -1.59%
packages/@rescript/runtime/Stdlib_Intl_PluralRules.res 204,411 201,180 -1.58%
packages/@rescript/runtime/Stdlib_Pair.res 168,275 165,629 -1.57%
packages/@rescript/runtime/Stdlib_Set.res 213,932 210,614 -1.55%
packages/@rescript/runtime/Stdlib_Generator.resi 162,671 160,178 -1.53%
packages/@rescript/runtime/Stdlib_Dict.resi 244,605 241,018 -1.47%
packages/@rescript/runtime/Stdlib_Iterator.resi 161,329 159,008 -1.44%
packages/@rescript/runtime/Stdlib_JSON.res 301,793 297,543 -1.41%
packages/@rescript/runtime/Primitive_float.res 149,439 147,378 -1.38%
packages/@rescript/runtime/Primitive_bool.res 149,351 147,337 -1.35%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 579,976 572,204 -1.34%
packages/@rescript/runtime/Stdlib_Math.res 331,172 326,831 -1.31%
packages/@rescript/runtime/Stdlib_Type.res 204,082 201,437 -1.30%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 386,420 381,509 -1.27%
packages/@rescript/belt/src/Belt_HashMapString.resi 216,055 213,316 -1.27%
packages/@rescript/belt/src/Belt_HashMapInt.resi 216,170 213,431 -1.27%
packages/@rescript/runtime/Stdlib_Intl_Locale.res 199,607 197,097 -1.26%
packages/@rescript/runtime/Stdlib_Intl_Collator.res 180,290 178,054 -1.24%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 377,278 372,671 -1.22%
packages/@rescript/runtime/Stdlib_Set.resi 232,306 229,595 -1.17%
packages/@rescript/runtime/Stdlib_Date.res 557,824 551,382 -1.15%
packages/@rescript/belt/src/Belt_internalBucketsType.resi 166,380 164,493 -1.13%
packages/@rescript/belt/src/Belt_Option.resi 245,756 242,980 -1.13%
packages/@rescript/runtime/Stdlib_TypedArray.res 791,432 782,659 -1.11%
packages/@rescript/runtime/Stdlib_BigInt.res 209,114 206,822 -1.10%
packages/@rescript/runtime/Stdlib_Ordering.res 153,792 152,123 -1.09%
packages/@rescript/runtime/Stdlib_Float.res 220,930 218,542 -1.08%
packages/@rescript/belt/src/Belt_MutableQueue.resi 209,174 206,944 -1.07%
packages/@rescript/belt/src/Belt_SortArray.resi 256,163 253,595 -1.00%
packages/@rescript/runtime/Stdlib_JSON.resi 238,996 236,625 -0.99%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 270,652 268,033 -0.97%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 310,050 307,209 -0.92%
packages/@rescript/runtime/Pervasives.res 241,109 238,939 -0.90%
packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res 181,435 179,816 -0.89%
packages/@rescript/runtime/Stdlib_Intl_ListFormat.res 170,604 169,175 -0.84%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 406,241 402,866 -0.83%
packages/@rescript/runtime/JsxEvent.res 1,097,145 1,088,117 -0.82%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 440,560 436,943 -0.82%
packages/@rescript/runtime/Primitive_js_extern.res 241,375 239,398 -0.82%
packages/@rescript/runtime/Stdlib_Temporal_PlainTime.res 322,361 319,801 -0.79%
packages/@rescript/belt/src/Belt_MutableSetString.resi 266,624 264,513 -0.79%
packages/@rescript/belt/src/Belt_MutableSetInt.resi 266,741 264,630 -0.79%
packages/@rescript/runtime/Stdlib_Date.resi 623,278 618,508 -0.77%
packages/@rescript/belt/src/Belt_SetString.resi 261,447 259,453 -0.76%
packages/@rescript/belt/src/Belt_SetInt.resi 261,574 259,580 -0.76%
packages/@rescript/runtime/Stdlib_String.resi 503,890 500,049 -0.76%
packages/@rescript/belt/src/Belt_MutableStack.resi 170,230 168,936 -0.76%
packages/@rescript/runtime/Stdlib_WeakMap.res 135,944 134,936 -0.74%
packages/@rescript/belt/src/Belt_SortArrayString.resi 170,493 169,257 -0.72%
packages/@rescript/belt/src/Belt_SortArrayInt.resi 170,611 169,375 -0.72%
packages/@rescript/runtime/Stdlib_Bool.res 147,102 146,041 -0.72%
packages/@rescript/runtime/Stdlib_TaggedTemplate.resi 106,541 105,837 -0.66%
packages/@rescript/runtime/Stdlib_RegExp.res 163,888 162,817 -0.65%
packages/@rescript/runtime/Stdlib_Int.resi 207,003 205,677 -0.64%
packages/@rescript/runtime/Stdlib_TaggedTemplate.res 115,737 115,001 -0.64%
packages/@rescript/belt/src/Belt_Float.res 165,794 164,752 -0.63%
packages/@rescript/belt/src/Belt_Int.res 166,463 165,421 -0.63%
packages/@rescript/runtime/Stdlib_DataView.res 231,443 230,058 -0.60%
packages/@rescript/belt/src/Belt_Id.resi 261,289 259,758 -0.59%
packages/@rescript/runtime/Stdlib_Intl_Segmenter.res 154,798 153,921 -0.57%
packages/@rescript/runtime/Stdlib_RegExp.resi 189,072 188,001 -0.57%
packages/@rescript/runtime/Jsx.res 121,936 121,256 -0.56%
packages/@rescript/runtime/Stdlib_Math.resi 332,372 330,661 -0.51%
packages/@rescript/runtime/Stdlib_DataView.resi 270,830 269,445 -0.51%
packages/@rescript/runtime/Primitive_array_extern.res 114,436 113,857 -0.51%
packages/@rescript/belt/src/Belt_HashSetString.resi 176,028 175,179 -0.48%
packages/@rescript/belt/src/Belt_HashSetInt.resi 176,141 175,292 -0.48%
packages/@rescript/runtime/Stdlib_JsError.resi 185,904 185,010 -0.48%
packages/@rescript/runtime/Stdlib_BigInt.resi 187,210 186,312 -0.48%
packages/@rescript/runtime/Stdlib_JsExn.resi 122,962 122,404 -0.45%
stress/ObjectFields.res 20,784,135 20,692,938 -0.44%
packages/@rescript/runtime/Primitive_option.resi 122,474 121,942 -0.43%
packages/@rescript/runtime/Stdlib_Object.res 246,006 244,951 -0.43%
packages/@rescript/runtime/Primitive_dict.res 102,760 102,325 -0.42%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat_Grouping.res 160,480 159,837 -0.40%
packages/@rescript/belt/src/Belt_Range.resi 177,034 176,338 -0.39%
packages/@rescript/runtime/Stdlib_Intl_Segments.res 116,578 116,121 -0.39%
packages/@rescript/runtime/Stdlib_Float.resi 195,163 194,403 -0.39%
packages/@rescript/runtime/Stdlib_WeakSet.res 123,070 122,595 -0.39%
packages/@rescript/runtime/Stdlib_Iterable.resi 98,222 97,858 -0.37%
packages/@rescript/runtime/Stdlib_AsyncIterable.resi 98,369 98,005 -0.37%
packages/@rescript/runtime/Stdlib_Iterable.res 98,912 98,548 -0.37%
packages/@rescript/runtime/Stdlib_AsyncIterable.res 98,983 98,619 -0.37%
packages/@rescript/runtime/Primitive_object.resi 117,194 116,778 -0.35%
stress/Record.res 26,253,758 26,163,127 -0.35%
build/WideDeps/clean 497,646,737 495,953,001 -0.34%
packages/@rescript/runtime/Stdlib_Symbol.res 144,546 144,086 -0.32%
packages/@rescript/runtime/Primitive_object_extern.res 114,600 114,242 -0.31%
packages/@rescript/runtime/Stdlib_Lazy.resi 112,450 112,105 -0.31%
packages/@rescript/runtime/Stdlib_Global.res 141,736 141,326 -0.29%
packages/@rescript/runtime/Stdlib_Symbol.resi 160,309 159,849 -0.29%
packages/@rescript/runtime/Dom_storage2.res 119,828 119,493 -0.28%
packages/@rescript/runtime/Stdlib_Global.resi 156,151 155,741 -0.26%
packages/@rescript/runtime/Stdlib_Bool.resi 116,698 116,432 -0.23%
packages/@rescript/runtime/Stdlib_Type.resi 135,857 135,551 -0.23%
packages/@rescript/runtime/RescriptTools_ExtractCodeBlocks.res 111,992 111,754 -0.21%
packages/@rescript/runtime/Primitive_list.resi 91,805 91,611 -0.21%
packages/@rescript/belt/src/Belt_Int.resi 149,668 149,367 -0.20%
packages/@rescript/runtime/Stdlib_Int8Array.res 343,690 343,000 -0.20%
packages/@rescript/runtime/Stdlib_Uint8Array.res 343,705 343,015 -0.20%
packages/@rescript/runtime/Stdlib_Float32Array.res 343,808 343,118 -0.20%
packages/@rescript/runtime/Stdlib_Float64Array.res 343,808 343,118 -0.20%
packages/@rescript/runtime/Stdlib_Int16Array.res 343,837 343,147 -0.20%
packages/@rescript/runtime/Stdlib_Int32Array.res 343,839 343,149 -0.20%
packages/@rescript/runtime/Stdlib_Uint16Array.res 343,844 343,154 -0.20%
packages/@rescript/runtime/Stdlib_Uint32Array.res 343,844 343,154 -0.20%
packages/@rescript/runtime/Stdlib_BigInt64Array.res 343,870 343,180 -0.20%
packages/@rescript/runtime/Stdlib_BigUint64Array.res 343,944 343,254 -0.20%
packages/@rescript/runtime/Stdlib_Uint8ClampedArray.res 344,220 343,530 -0.20%
packages/@rescript/belt/src/Belt_Float.resi 151,144 150,843 -0.20%
build/WideDeps/edit 228,708,048 228,260,949 -0.20%
packages/@rescript/runtime/Primitive_hash.resi 102,642 102,453 -0.18%
packages/@rescript/runtime/Primitive_module.resi 103,071 102,882 -0.18%
packages/@rescript/runtime/Stdlib.res 984,148 982,378 -0.18%
packages/@rescript/runtime/Primitive_exceptions.resi 100,176 100,016 -0.16%
packages/@rescript/runtime/Primitive_promise.res 96,839 96,689 -0.15%
packages/@rescript/runtime/Stdlib_Intl.res 174,194 173,934 -0.15%
packages/@rescript/runtime/Obj.res 98,894 98,779 -0.12%
packages/@rescript/runtime/Stdlib_ArrayBuffer.res 107,184 107,060 -0.12%
packages/@rescript/runtime/Stdlib_ArrayBuffer.resi 110,904 110,780 -0.11%
build/Namespace/clean 188,908,061 188,697,166 -0.11%
packages/@rescript/runtime/Primitive_string_extern.res 97,963 97,857 -0.11%
packages/@rescript/runtime/Stdlib_Temporal_Now.res 143,057 142,907 -0.10%

Instructions

In total: 9,666,123,646 → 9,211,172,800 (-4.71%).

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

Benchmark Base Head Change
stress/TypeAliasChain.res 525,263,939 171,832,980 -67.29%
packages/@rescript/belt/src/Belt_internalAVLtree.res 142,696,765 135,826,277 -4.81%
packages/@rescript/belt/src/Belt_List.res 121,652,135 117,641,689 -3.30%
stress/NestedClosures.res 546,563,790 529,250,336 -3.17%
stress/LetRec.res 1,412,523,231 1,372,787,736 -2.81%
stress/ListLiteral.res 134,727,751 131,010,481 -2.76%
packages/@rescript/runtime/JsxDOM.res 120,190,510 117,078,963 -2.59%
stress/RefsInLoop.res 399,618,158 389,512,944 -2.53%
packages/@rescript/runtime/JsxDOMStyle.res 97,145,089 95,048,027 -2.16%
packages/@rescript/belt/src/Belt_List.resi 39,650,625 38,947,650 -1.77%
13 more changes
Benchmark Base Head Change
stress/InlineChain.res 139,319,223 137,158,519 -1.55%
packages/@rescript/belt/src/Belt_Array.resi 34,139,379 33,675,836 -1.36%
stress/ObjectFields.res 952,102,208 944,743,971 -0.77%
packages/@rescript/runtime/Stdlib_Array.resi 40,229,112 39,941,655 -0.71%
packages/@rescript/runtime/Stdlib_List.resi 24,670,039 24,502,787 -0.68%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 31,191,044 31,030,446 -0.51%
packages/@rescript/runtime/Stdlib_TypedArray.res 41,262,102 41,079,388 -0.44%
packages/@rescript/runtime/Stdlib_String.resi 29,416,137 29,334,052 -0.28%
packages/@rescript/runtime/Stdlib_Date.resi 34,197,046 34,102,855 -0.28%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 25,463,140 25,401,662 -0.24%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 24,023,981 23,966,342 -0.24%
stress/Record.res 1,073,272,289 1,071,536,226 -0.16%
packages/@rescript/runtime/Stdlib_Math.resi 21,189,353 21,158,495 -0.15%

Output size

In total: 25,841,785 → 25,843,198 (+0.01%).

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

Pull request 86cf87a compared with base dcedd5d in this run.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch 5 times, most recently from 130e13c to 6b84b88 Compare November 10, 2026 14:44
@cknitt
cknitt requested a review from cristianoc November 10, 2026 16:05
@cknitt

cknitt commented Nov 10, 2026

Copy link
Copy Markdown
Member

Thanks! Looks good to me and improves performance noticeably. But as it affects the type checker, maybe @cristianoc could also have a look.

@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch 3 times, most recently from 95ab303 to 98c65e7 Compare November 10, 2026 17:24

@cristianoc cristianoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely there's semantic diff to provide clarity as the PR description conflates the problem, the solution, and the justification in the same text.

So there's a fast approx path for occurs check. If it says OK, it means the occur check would agree.
If it says don't know then the normal occurs check runs.
So typical lucky path optimisation with backtracking.

The rest is speedup refactoring to avoid allocations and similar things.

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 <jjprest@gmail.com>
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 <jjprest@gmail.com>
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 <jjprest@gmail.com>
Why:
    The bounded walks that `occur` and `deep_occur` try first allocated
    a ref for their budget and a closure for the walk on every call, and
    `iter_type_expr` allocated another closure at each arrow type they
    visited. Unification runs `occur` for every variable it links, mostly
    on small types, so this allocation was a large share of what the
    fast paths cost.

`occur_reaches` and `deep_occur_fast_absent` now walk the type with
mutually recursive functions that take the budget left and return what
remains, following the edges of `iter_type_expr` written out by
constructor. The match on `type_desc` lists every constructor, so a new
one is a compile error here rather than a missed edge. Only
`deep_occur_fast_absent` at a `Tvariant` still goes through
`iter_type_expr` with a closure, for the row's fields. The nodes visited,
the order, the budget and the exceptions raised are unchanged.

Measured with scripts/compilerBenchmarks.js against the previous commit:
allocated words -1.03% in total (-1.14% without the project builds),
stress/TypeAliasChain.res -8.3%, Belt map and set modules -2.7% to
-3.4%. On a 552 module React app (clean build, one domain) the build
allocates 0.99% fewer words. Keeping the closures and only skipping the
walk for a leaf type (a variable or a constructor without arguments)
gave -0.57% in total and -0.54% on the app.

Review: Can a result change? No: a walk that finishes visited exactly
    the nodes the closure version visited, within the same budget; one
    that gives up still falls back to the full check.

Signed-off-by: Jono Prest <jjprest@gmail.com>
@JonoPrest
JonoPrest force-pushed the jono/occur-check-fast-path branch from 98c65e7 to 86cf87a Compare November 10, 2026 18:14
@JonoPrest
JonoPrest merged commit 00e3f40 into master Nov 10, 2026
22 checks passed
@JonoPrest
JonoPrest deleted the jono/occur-check-fast-path branch November 10, 2026 18:46
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.

3 participants