Skip to content

Remember type path lookups within a compilation - #9190

Open
JonoPrest wants to merge 1 commit into
masterfrom
jono/type-path-memo
Open

JonoPrest wants to merge 1 commit into
masterfrom
jono/type-path-memo

Conversation

@JonoPrest

Copy link
Copy Markdown
Contributor

Typechecking a 552 module React app calls Env.find_type_full 1.76M times. Most of those resolve the same few paths against the same environment: option alone is 528k calls, then string, array, Jsx.element. Each one walks the open layers of env.types. On top of that, expanding a type that isn't an abbreviation (a variant, record or abstract type) normalizes its path, which raises and catches Not_found to return the path unchanged, 666k times.

This adds a small direct-mapped memo for both, per request:

  • An entry is reused only while the environment tables the lookup read are physically the same: env.types for a Pident, env.components for a Pdot, and env.modules plus env.components for a normalization. These tables are immutable, so the same table gives the same answer.
  • Only successful lookups and unchanged normalizations are stored. A changed normalization is computed again, so paths in types are allocated as before.
  • Paths with a functor application aren't cached, the memo is skipped while loading .cmi files is disabled, and it is emptied when a request ends or a loaded module is replaced.

On the corpus below, 94% of lookups and 96% of normalizations hit.

Clean builds, 8 vCPU Linux VM, median of interleaved runs:

project 1 domain CPU 7 domains CPU
React app, 552 modules 0.94 to 0.97 0.94 to 0.98
backend service, 391 modules ~1.00 ~1.00

The gain is on code that leans on many small type lookups. Backend code without JSX doesn't change.

Testing:

  • New ounit_type_path_memo_tests.ml binds the same path differently in two environments within one request (recursive type placeholders, a recursive module approximation, an alias). Each test fails if its check is removed from the memo.
  • type_path_lookup_test.res covers shadowing, local opens, aliases, functors, first class modules, recursive types and recursive modules. Its output matches master.
  • A compiler request test checks the memo is empty after each request and that colliding identifiers across requests compile like cold builds.
  • JS, .cmi, .cmj, .ast and .cmt are byte identical to master across three production codebases and their dependencies. make test and make test-rewatch pass.

@JonoPrest

Copy link
Copy Markdown
Contributor Author

@codex review

JonoPrest added a commit that referenced this pull request Nov 3, 2026
The entry names #9190, so the changelog points at this PR.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

  • Build: remember type path lookups within a compilation, which avoids looking up the same types such as option and string hundreds of thousands of times per build.

Semantic Diff

1 commit · Full page

Remember type path lookups within a compilation

3096465 · JonoPrest · Nov 03, 2026, 07:59 UTC

Why

Typechecking a 552 module React app calls Env.find_type_full 1.76M times, and normalize_path 666k times from expand_abbrev. Most calls look up the same few paths against the same environment: option alone is 528k of them, followed by string, array, Jsx.element. Each lookup walks the open layers of env.types, and expanding a type that isn't an abbreviation also normalizes its path, which raises and catches Not_found to return the path unchanged.

A direct-mapped table now remembers successful lookups and unchanged normalizations. An entry is only reused while the environment tables the lookup read are physically the same (env.types for a Pident, env.components for a Pdot, env.modules and env.components for a normalization), and only within the request that stored it. Changed normalizations are computed again, so paths in types are allocated as before. Paths with a functor application aren't cached, the memo is skipped while loading .cmi files is disabled, and it is emptied when a request ends and when a loaded module is replaced.

The two tables start empty and grow from 64 to 4096 slots as entries are stored, and a replaced entry is updated in place, so a small compilation allocates little for them: fixed 4096 slot tables made each runtime file allocate about 3% more words than master. Now the file that grows most, Dom.res, allocates 0.24% more, and the runtime and Belt in total 0.06% less.

On the corpus 94% of lookups and 96% of normalizations hit, and the output is byte identical to master.

Review

  • Can an entry be stale? The tables an entry depends on are immutable, so the same physical table gives the same answer. New unit tests bind the same path differently in two environments within one request (recursive type placeholders, recursive module approximations, an alias) and fail if any of these checks is removed.
  • Can growing or reusing an entry return a wrong result? Growth moves each entry to the slot of its path in the larger table, and a lookup still checks the path, the epoch and the tables of each entry it finds. An entry is only referenced from its slot, so updating it in place changes no result a caller holds. A unit test looks up 3000 types across the growth and fails if a replaced entry keeps its old result.

@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-03T07:44:49.243439Z 06d84a7 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. Delightful!

Reviewed commit: 06d84a7295

ℹ️ 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".

@JonoPrest
JonoPrest marked this pull request as ready for review November 3, 2026 07:45
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.89119% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.97%. Comparing base (b82d944) to head (3096465).

Files with missing lines Patch % Lines
compiler/ml/env.ml 95.50% 4 Missing ⚠️
compiler/ml/typecore.ml 66.66% 1 Missing ⚠️
tests/ounit_tests/ounit_type_path_memo_tests.ml 98.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9190      +/-   ##
==========================================
+ Coverage   81.92%   81.97%   +0.04%     
==========================================
  Files         456      457       +1     
  Lines       65464    65646     +182     
==========================================
+ Hits        53631    53812     +181     
- Misses      11833    11834       +1     
Files with missing lines Coverage Δ
compiler/ml/ctype.ml 70.25% <100.00%> (ø)
tests/ounit_tests/ounit_compiler_request_tests.ml 97.18% <100.00%> (+0.10%) ⬆️
tests/ounit_tests/ounit_tests_main.ml 100.00% <ø> (ø)
compiler/ml/typecore.ml 89.15% <66.66%> (+<0.01%) ⬆️
tests/ounit_tests/ounit_type_path_memo_tests.ml 98.33% <98.33%> (ø)
compiler/ml/env.ml 80.60% <95.50%> (+1.32%) ⬆️

... and 3 files 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@9190

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 3096465

JonoPrest added a commit that referenced this pull request Nov 3, 2026
The entry names #9190, so the changelog points at this PR.

Signed-off-by: Jono Prest <[email protected]>
@JonoPrest
JonoPrest force-pushed the jono/type-path-memo branch from 06d84a7 to d5690b3 Compare November 3, 2026 07:59
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

@JonoPrest
JonoPrest requested a review from cknitt November 3, 2026 08:21
@JonoPrest
JonoPrest force-pushed the jono/type-path-memo branch 4 times, most recently from 27cb0bf to 4bb2b5d 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,688 → 683,045,157 (-0.07%).

Benchmark Base Head Change
packages/@rescript/belt/src/Belt_SetDict.res 1,634,169 1,619,468 -0.90%
packages/@rescript/belt/src/Belt_internalMapInt.res 1,610,677 1,596,613 -0.87%
packages/@rescript/belt/src/Belt_internalMapString.res 1,612,527 1,598,477 -0.87%
packages/@rescript/belt/src/Belt_MapDict.res 1,875,071 1,859,428 -0.83%
packages/@rescript/belt/src/Belt_MutableSet.res 2,121,669 2,104,232 -0.82%
packages/@rescript/belt/src/Belt_SetInt.res 1,413,088 1,401,721 -0.80%
packages/@rescript/belt/src/Belt_SetString.res 1,413,817 1,402,468 -0.80%
packages/@rescript/belt/src/Belt_MutableSetInt.res 1,830,853 1,817,422 -0.73%
packages/@rescript/belt/src/Belt_MapInt.res 1,333,449 1,323,674 -0.73%
packages/@rescript/belt/src/Belt_MapString.res 1,333,987 1,324,226 -0.73%
160 more changes
Benchmark Base Head Change
packages/@rescript/belt/src/Belt_MutableSetString.res 1,832,096 1,818,826 -0.72%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,190,629 1,182,126 -0.71%
packages/@rescript/belt/src/Belt_HashMapString.res 1,191,337 1,182,865 -0.71%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,291,463 1,282,680 -0.68%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,291,746 1,282,979 -0.68%
packages/@rescript/belt/src/Belt_HashMap.res 1,362,983 1,353,845 -0.67%
packages/@rescript/belt/src/Belt_Set.res 932,088 925,866 -0.67%
packages/@rescript/belt/src/Belt_MutableMap.res 1,459,148 1,449,530 -0.66%
packages/@rescript/belt/src/Belt_HashSetInt.res 932,283 926,536 -0.62%
packages/@rescript/belt/src/Belt_HashSetString.res 932,602 926,878 -0.61%
packages/@rescript/belt/src/Belt_HashSet.res 1,092,814 1,086,133 -0.61%
packages/@rescript/belt/src/Belt_internalSetInt.res 788,501 783,775 -0.60%
packages/@rescript/belt/src/Belt_internalSetString.res 789,694 784,986 -0.60%
packages/@rescript/belt/src/Belt_Map.res 1,073,930 1,067,813 -0.57%
stress/RefsInLoop.res 17,141,783 17,061,109 -0.47%
packages/@rescript/belt/src/Belt_internalBuckets.res 1,076,130 1,072,452 -0.34%
packages/@rescript/runtime/Dom.res 1,210,844 1,214,849 +0.33%
packages/@rescript/runtime/Primitive_hash.res 1,022,797 1,019,470 -0.33%
packages/@rescript/runtime/Primitive_list.resi 91,811 92,099 +0.31%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 759,864 757,486 -0.31%
packages/@rescript/runtime/Primitive_module.resi 103,101 103,416 +0.31%
packages/@rescript/runtime/Primitive_string_extern.res 97,997 98,285 +0.29%
packages/@rescript/runtime/Stdlib_Iterable.resi 98,228 98,516 +0.29%
packages/@rescript/runtime/Stdlib_Iterable.res 98,918 99,206 +0.29%
packages/@rescript/runtime/Primitive_dict.res 102,794 103,091 +0.29%
packages/@rescript/runtime/Jsx.res 122,098 122,449 +0.29%
packages/@rescript/runtime/Stdlib_TaggedTemplate.resi 106,559 106,865 +0.29%
packages/@rescript/runtime/Stdlib_AsyncIterable.resi 98,375 98,654 +0.28%
packages/@rescript/runtime/Stdlib_AsyncIterable.res 98,989 99,268 +0.28%
packages/@rescript/runtime/Primitive_promise.res 96,873 97,143 +0.28%
packages/@rescript/runtime/Stdlib_Intl_Segments.res 116,632 116,956 +0.28%
packages/@rescript/runtime/Stdlib_Bool.resi 116,764 117,088 +0.28%
packages/@rescript/runtime/Stdlib_ArrayBuffer.res 107,226 107,523 +0.28%
packages/@rescript/runtime/Stdlib_File.res 95,311 95,570 +0.27%
packages/@rescript/runtime/Primitive_option.resi 122,540 122,871 +0.27%
packages/@rescript/runtime/Stdlib_ArrayBuffer.resi 110,946 111,243 +0.27%
packages/@rescript/runtime/RescriptTools_ExtractCodeBlocks.res 112,006 112,303 +0.27%
packages/@rescript/runtime/Stdlib_JsExn.resi 123,052 123,376 +0.26%
packages/@rescript/runtime/Dom_storage2.res 119,942 120,257 +0.26%
packages/@rescript/runtime/Obj.res 98,908 99,167 +0.26%
packages/@rescript/runtime/RescriptTools.res 104,121 104,391 +0.26%
packages/@rescript/runtime/Stdlib_TaggedTemplate.res 115,787 116,084 +0.26%
packages/@rescript/runtime/Stdlib_Lazy.resi 112,504 112,792 +0.26%
packages/@rescript/runtime/Primitive_object.resi 117,272 117,567 +0.25%
packages/@rescript/runtime/Primitive_array_extern.res 114,530 114,818 +0.25%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat_Grouping.res 160,578 160,183 -0.25%
packages/@rescript/runtime/Stdlib_Intl_Segmenter.res 154,892 155,270 +0.24%
packages/@rescript/runtime/Stdlib_Bool.res 147,200 147,551 +0.24%
packages/@rescript/runtime/Stdlib_WeakSet.res 123,164 123,452 +0.23%
packages/@rescript/runtime/Stdlib_Temporal_Now.res 143,171 143,495 +0.23%
packages/@rescript/runtime/Stdlib_WeakMap.res 136,058 136,364 +0.22%
packages/@rescript/runtime/Stdlib_Intl_Collator.res 180,384 180,789 +0.22%
packages/@rescript/runtime/Primitive_float_extern.res 93,356 93,561 +0.22%
packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res 181,549 181,945 +0.22%
packages/@rescript/runtime/Stdlib_Intl_ListFormat.res 170,718 171,087 +0.22%
packages/@rescript/runtime/Stdlib_RegExp.res 164,130 164,481 +0.21%
packages/@rescript/runtime/Primitive_exceptions.resi 100,194 100,408 +0.21%
packages/@rescript/runtime/Stdlib_Symbol.res 144,612 144,918 +0.21%
packages/@rescript/runtime/Stdlib_Blob.res 93,474 93,670 +0.21%
packages/@rescript/runtime/Primitive_string.res 163,632 163,974 +0.21%
packages/@rescript/runtime/Primitive_hash.resi 102,684 102,898 +0.21%
packages/@rescript/runtime/Stdlib_Ordering.res 154,046 154,361 +0.20%
packages/@rescript/runtime/Primitive_float.res 149,645 149,951 +0.20%
packages/@rescript/runtime/RescriptTools_Docgen.resi 281,500 282,066 +0.20%
packages/@rescript/runtime/Primitive_int_extern.res 97,645 97,841 +0.20%
packages/@rescript/belt/src/Belt_Int.resi 149,758 150,055 +0.20%
packages/@rescript/runtime/Stdlib_Type.res 204,444 204,849 +0.20%
packages/@rescript/runtime/Stdlib_Type.resi 135,887 136,155 +0.20%
packages/@rescript/belt/src/Belt_Float.resi 151,234 151,531 +0.20%
packages/@rescript/belt/src/Belt_Float.res 165,952 166,276 +0.20%
packages/@rescript/runtime/Stdlib_Iterator.resi 161,455 161,770 +0.20%
packages/@rescript/runtime/Primitive_exceptions.res 207,901 208,306 +0.19%
packages/@rescript/belt/src/Belt_Int.res 166,621 166,945 +0.19%
packages/@rescript/runtime/RescriptTools_Docgen.res 291,788 292,354 +0.19%
packages/@rescript/runtime/Stdlib_Intl_PluralRules.res 204,565 204,961 +0.19%
packages/@rescript/runtime/Primitive_bool.res 149,417 149,705 +0.19%
packages/@rescript/runtime/Stdlib_Symbol.resi 160,375 160,681 +0.19%
packages/@rescript/belt/src/Belt_SortArrayString.resi 170,583 170,907 +0.19%
packages/@rescript/belt/src/Belt_HashSetString.resi 176,274 176,607 +0.19%
packages/@rescript/runtime/Stdlib_JsExn.res 177,282 177,615 +0.19%
packages/@rescript/runtime/Primitive_object_extern.res 114,734 114,948 +0.19%
packages/@rescript/runtime/Stdlib_IterableIterator.resi 151,641 151,923 +0.19%
packages/@rescript/belt/src/Belt_Id.res 296,203 296,753 +0.19%
packages/@rescript/runtime/Stdlib_RegExp.resi 189,282 189,633 +0.19%
packages/@rescript/belt/src/Belt_MutableStack.resi 170,428 170,743 +0.18%
packages/@rescript/belt/src/Belt_SortArrayInt.resi 170,701 171,016 +0.18%
packages/@rescript/belt/src/Belt_HashSetInt.resi 176,387 176,711 +0.18%
packages/@rescript/runtime/Primitive_bigint.res 181,841 182,165 +0.18%
packages/@rescript/runtime/Stdlib_Pair.res 168,525 168,822 +0.18%
packages/@rescript/runtime/Stdlib_Lazy.res 231,686 232,091 +0.17%
packages/@rescript/runtime/Stdlib_Int.resi 207,249 207,609 +0.17%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi 162,395 162,677 +0.17%
packages/@rescript/runtime/Primitive_int.res 176,287 176,593 +0.17%
packages/@rescript/runtime/Stdlib_BigInt.resi 187,480 187,804 +0.17%
packages/@rescript/runtime/Stdlib_Float.resi 195,361 195,694 +0.17%
packages/@rescript/runtime/Stdlib_Global.res 141,938 142,179 +0.17%
packages/@rescript/runtime/Stdlib_Set.res 214,234 214,593 +0.17%
packages/@rescript/runtime/Stdlib_Intl_Locale.res 199,881 200,214 +0.17%
packages/@rescript/belt/src/Belt_HashMapString.resi 216,385 216,745 +0.17%
packages/@rescript/runtime/Stdlib_Generator.resi 162,749 163,019 +0.17%
packages/@rescript/runtime/Stdlib_JsError.resi 186,162 186,468 +0.16%
packages/@rescript/runtime/Stdlib_BigInt.res 209,476 209,818 +0.16%
packages/@rescript/runtime/Stdlib_Temporal_Common.res 376,573 377,184 +0.16%
packages/@rescript/belt/src/Belt_HashMapInt.resi 216,500 216,851 +0.16%
packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res 396,537 397,175 +0.16%
packages/@rescript/runtime/Stdlib_Map.res 210,049 210,386 +0.16%
packages/@rescript/runtime/Stdlib_Intl.res 174,248 174,527 +0.16%
packages/@rescript/belt/src/Belt_Id.resi 261,547 261,963 +0.16%
packages/@rescript/belt/src/Belt_MutableQueue.resi 209,492 209,825 +0.16%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res 412,808 413,464 +0.16%
packages/@rescript/belt/src/Belt_Array.res 2,349,207 2,345,499 -0.16%
packages/@rescript/runtime/Stdlib_Float.res 221,240 221,582 +0.15%
packages/@rescript/runtime/Stdlib_AsyncGenerator.resi 169,209 169,470 +0.15%
packages/@rescript/runtime/Stdlib_Global.resi 156,337 156,578 +0.15%
packages/@rescript/runtime/Stdlib_JSON.resi 239,282 239,650 +0.15%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 270,970 271,380 +0.15%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 377,976 378,547 +0.15%
packages/@rescript/runtime/Stdlib_Intl_Common.res 153,926 154,158 +0.15%
packages/@rescript/runtime/Stdlib_Dict.resi 244,911 245,271 +0.15%
packages/@rescript/belt/src/Belt_HashSet.resi 226,843 227,176 +0.15%
packages/@rescript/runtime/Stdlib_Set.resi 232,564 232,905 +0.15%
packages/@rescript/runtime/Stdlib_Map.resi 221,621 221,940 +0.14%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 241,203 240,856 -0.14%
packages/@rescript/belt/src/Belt_SetString.resi 262,017 262,386 +0.14%
packages/@rescript/runtime/Stdlib_DataView.res 231,761 232,085 +0.14%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 312,594 312,158 -0.14%
packages/@rescript/runtime/Stdlib_Option.resi 241,144 241,477 +0.14%
packages/@rescript/belt/src/Belt_MutableSetString.resi 267,242 267,611 +0.14%
packages/@rescript/runtime/Primitive_js_extern.res 241,489 241,822 +0.14%
packages/@rescript/runtime/Pervasives.res 242,003 242,336 +0.14%
packages/@rescript/belt/src/Belt_SetInt.resi 262,144 262,504 +0.14%
packages/@rescript/belt/src/Belt_MutableSetInt.resi 267,359 267,719 +0.13%
packages/@rescript/runtime/Stdlib_JsError.res 235,345 235,660 +0.13%
packages/@rescript/runtime/Stdlib_Iterator.res 243,369 243,693 +0.13%
packages/@rescript/runtime/Stdlib_Null.res 349,603 349,139 -0.13%
packages/@rescript/runtime/Stdlib_Object.res 246,460 246,784 +0.13%
packages/@rescript/runtime/Dom_storage.res 169,942 170,165 +0.13%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 310,488 310,885 +0.13%
packages/@rescript/runtime/Stdlib_Nullable.res 347,588 347,147 -0.13%
packages/@rescript/runtime/Stdlib_Nullable.resi 204,247 203,991 -0.13%
packages/@rescript/runtime/Stdlib_Result.resi 305,424 305,793 +0.12%
packages/@rescript/belt/src/Belt_Range.resi 177,268 177,482 +0.12%
packages/@rescript/belt/src/Belt_Option.resi 246,170 246,467 +0.12%
packages/@rescript/runtime/Stdlib_Temporal_PlainTime.res 322,879 323,267 +0.12%
packages/@rescript/belt/src/Belt_Result.resi 247,960 248,257 +0.12%
packages/@rescript/runtime/Stdlib_DataView.resi 271,148 271,472 +0.12%
packages/@rescript/belt/src/Belt_SortArray.resi 256,541 256,838 +0.12%
packages/@rescript/belt/src/Belt_HashMap.resi 303,382 303,733 +0.12%
packages/@rescript/belt/src/Belt_MutableMapString.resi 320,569 320,938 +0.12%
packages/@rescript/runtime/Stdlib_JSON.res 302,239 302,586 +0.11%
packages/@rescript/runtime/Stdlib_Dict.res 328,030 328,399 +0.11%
packages/@rescript/belt/src/Belt_MutableMapInt.resi 320,687 321,047 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 407,039 407,491 +0.11%
packages/@rescript/runtime/Stdlib_IteratorObject.resi 241,227 241,493 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 581,114 581,753 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 387,018 387,434 +0.11%
packages/@rescript/runtime/Primitive_object.res 1,240,833 1,239,537 -0.10%
packages/@rescript/belt/src/Belt_Range.res 300,432 300,738 +0.10%
packages/@rescript/runtime/JsxEvent.res 1,104,000 1,105,107 +0.10%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 441,498 441,940 +0.10%

Instructions

In total: 9,664,672,297 → 9,630,314,898 (-0.36%).

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

Benchmark Base Head Change
stress/RefsInLoop.res 399,652,123 379,827,249 -4.96%
stress/ListLiteral.res 134,742,036 131,971,953 -2.06%
packages/@rescript/runtime/JsxDOM.res 120,169,630 118,508,323 -1.38%
packages/@rescript/belt/src/Belt_List.res 121,661,729 120,166,074 -1.23%
stress/NestedClosures.res 546,868,019 542,720,297 -0.76%
stress/LetRec.res 1,412,349,749 1,407,539,891 -0.34%
stress/InlineChain.res 139,353,240 138,883,738 -0.34%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 31,210,376 31,106,864 -0.33%
packages/@rescript/runtime/Stdlib_Array.resi 40,252,017 40,145,405 -0.26%
packages/@rescript/belt/src/Belt_internalAVLtree.res 142,735,490 142,375,716 -0.25%
11 more changes
Benchmark Base Head Change
packages/@rescript/runtime/JsxDOMStyle.res 97,177,916 97,391,011 +0.22%
packages/@rescript/runtime/Stdlib_List.resi 24,702,427 24,654,601 -0.19%
packages/@rescript/belt/src/Belt_List.resi 39,659,022 39,730,855 +0.18%
packages/@rescript/runtime/Stdlib_Date.resi 34,227,138 34,169,664 -0.17%
packages/@rescript/runtime/Stdlib_Math.resi 21,220,099 21,254,990 +0.16%
packages/@rescript/runtime/Stdlib_TypedArray.res 41,282,872 41,346,259 +0.15%
stress/TypeAliasChain.res 525,256,479 526,036,200 +0.15%
packages/@rescript/runtime/Stdlib_String.resi 29,427,053 29,391,494 -0.12%
packages/@rescript/belt/src/Belt_Array.resi 34,160,609 34,197,384 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 25,477,995 25,450,800 -0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 24,041,149 24,016,908 -0.10%

Output size

In total: 25,569,598 → 25,571,454 (+0.01%).

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

Pull request 3096465 compared with base b82d944 in this run.

@JonoPrest
JonoPrest force-pushed the jono/type-path-memo branch 4 times, most recently from e23d28f to dfe4362 Compare November 9, 2026 13:05
Why:
    Typechecking a 552 module React app calls Env.find_type_full 1.76M
    times, and normalize_path 666k times from expand_abbrev. Most calls
    look up the same few paths against the same environment: `option`
    alone is 528k of them, followed by `string`, `array`, `Jsx.element`.
    Each lookup walks the open layers of env.types, and expanding a type
    that isn't an abbreviation also normalizes its path, which raises and
    catches Not_found to return the path unchanged.

A direct-mapped table now remembers successful lookups and unchanged
normalizations. An entry is only reused while the environment tables the
lookup read are physically the same (env.types for a Pident, env.components
for a Pdot, env.modules and env.components for a normalization), and only
within the request that stored it. Changed normalizations are computed
again, so paths in types are allocated as before. Paths with a functor
application aren't cached, the memo is skipped while loading .cmi files is
disabled, and it is emptied when a request ends and when a loaded module is
replaced.

The two tables start empty and grow from 64 to 4096 slots as entries are
stored, and a replaced entry is updated in place, so a small compilation
allocates little for them: fixed 4096 slot tables made each runtime file
allocate about 3% more words than master. Now the file that grows most,
Dom.res, allocates 0.24% more, and the runtime and Belt in total 0.06% less.

On the corpus 94% of lookups and 96% of normalizations hit, and the output
is byte identical to master.

Review: Can an entry be stale? The tables an entry depends on are
immutable, so the same physical table gives the same answer. New unit
tests bind the same path differently in two environments within one
request (recursive type placeholders, recursive module approximations,
an alias) and fail if any of these checks is removed.

Review: Can growing or reusing an entry return a wrong result? Growth moves
each entry to the slot of its path in the larger table, and a lookup still
checks the path, the epoch and the tables of each entry it finds. An entry
is only referenced from its slot, so updating it in place changes no result
a caller holds. A unit test looks up 3000 types across the growth and fails
if a replaced entry keeps its old result.

Signed-off-by: Jono Prest <[email protected]>
@JonoPrest
JonoPrest force-pushed the jono/type-path-memo branch from dfe4362 to 3096465 Compare November 9, 2026 16:43

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