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.98%. Comparing base (145df8d) to head (6af9e8f).

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.93%   81.98%   +0.05%     
==========================================
  Files         456      457       +1     
  Lines       65517    65699     +182     
==========================================
+ Hits        53679    53862     +183     
+ Misses      11838    11837       -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 81.40% <95.50%> (+1.40%) ⬆️

... 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: 6af9e8f

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 <jjprest@gmail.com>
@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 6 times, most recently from 3096465 to 5cf3317 Compare November 9, 2026 22:04
@github-actions

github-actions Bot commented Nov 9, 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

6af9e8f · 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.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🎉 Compiler benchmarks

Allocated words

In total: 682,019,210 → 681,511,242 (-0.07%).

Benchmark Base Head Change
packages/@rescript/belt/src/Belt_SetDict.res 1,631,791 1,617,090 -0.90%
packages/@rescript/belt/src/Belt_internalMapInt.res 1,608,163 1,594,103 -0.87%
packages/@rescript/belt/src/Belt_internalMapString.res 1,610,011 1,595,959 -0.87%
packages/@rescript/belt/src/Belt_MapDict.res 1,871,993 1,856,350 -0.84%
packages/@rescript/belt/src/Belt_MutableSet.res 2,116,807 2,099,308 -0.83%
packages/@rescript/belt/src/Belt_SetInt.res 1,410,950 1,399,583 -0.81%
packages/@rescript/belt/src/Belt_SetString.res 1,411,679 1,400,330 -0.80%
packages/@rescript/belt/src/Belt_MutableSetInt.res 1,826,887 1,813,456 -0.74%
packages/@rescript/belt/src/Belt_MapInt.res 1,331,271 1,321,496 -0.73%
packages/@rescript/belt/src/Belt_MapString.res 1,331,803 1,322,044 -0.73%
160 more changes
Benchmark Base Head Change
packages/@rescript/belt/src/Belt_MutableSetString.res 1,828,130 1,814,860 -0.73%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,188,191 1,179,695 -0.72%
packages/@rescript/belt/src/Belt_HashMapString.res 1,188,901 1,180,429 -0.71%
packages/@rescript/belt/src/Belt_HashMap.res 1,359,437 1,350,204 -0.68%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,289,119 1,280,400 -0.68%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,289,400 1,280,701 -0.67%
packages/@rescript/belt/src/Belt_Set.res 930,370 924,162 -0.67%
packages/@rescript/belt/src/Belt_MutableMap.res 1,456,456 1,446,838 -0.66%
packages/@rescript/belt/src/Belt_HashSetInt.res 930,277 924,535 -0.62%
packages/@rescript/belt/src/Belt_HashSetString.res 930,590 924,870 -0.61%
packages/@rescript/belt/src/Belt_HashSet.res 1,090,460 1,083,777 -0.61%
packages/@rescript/belt/src/Belt_internalSetInt.res 787,177 782,451 -0.60%
packages/@rescript/belt/src/Belt_internalSetString.res 788,368 783,660 -0.60%
packages/@rescript/belt/src/Belt_Map.res 1,071,556 1,065,433 -0.57%
stress/RefsInLoop.res 17,105,782 17,025,108 -0.47%
packages/@rescript/belt/src/Belt_internalBuckets.res 1,073,630 1,069,964 -0.34%
packages/@rescript/runtime/Dom.res 1,210,810 1,214,815 +0.33%
packages/@rescript/runtime/Primitive_hash.res 1,019,965 1,016,636 -0.33%
packages/@rescript/runtime/Primitive_list.resi 91,805 92,093 +0.31%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 758,266 755,888 -0.31%
packages/@rescript/runtime/Primitive_module.resi 103,071 103,386 +0.31%
packages/@rescript/runtime/Primitive_string_extern.res 97,963 98,251 +0.29%
packages/@rescript/runtime/Stdlib_Iterable.resi 98,222 98,510 +0.29%
packages/@rescript/runtime/Stdlib_Iterable.res 98,912 99,200 +0.29%
packages/@rescript/runtime/Primitive_dict.res 102,760 103,057 +0.29%
packages/@rescript/runtime/Jsx.res 121,936 122,287 +0.29%
packages/@rescript/runtime/Stdlib_TaggedTemplate.resi 106,541 106,847 +0.29%
packages/@rescript/runtime/Stdlib_AsyncIterable.resi 98,369 98,648 +0.28%
packages/@rescript/runtime/Stdlib_AsyncIterable.res 98,983 99,262 +0.28%
packages/@rescript/runtime/Primitive_promise.res 96,839 97,109 +0.28%
packages/@rescript/runtime/Stdlib_Intl_Segments.res 116,578 116,902 +0.28%
packages/@rescript/runtime/Stdlib_Bool.resi 116,698 117,022 +0.28%
packages/@rescript/runtime/Stdlib_ArrayBuffer.res 107,184 107,481 +0.28%
packages/@rescript/runtime/Stdlib_File.res 95,317 95,576 +0.27%
packages/@rescript/runtime/Primitive_option.resi 122,474 122,805 +0.27%
packages/@rescript/runtime/Stdlib_ArrayBuffer.resi 110,904 111,201 +0.27%
packages/@rescript/runtime/RescriptTools_ExtractCodeBlocks.res 111,992 112,289 +0.27%
packages/@rescript/runtime/Stdlib_JsExn.resi 122,962 123,286 +0.26%
packages/@rescript/runtime/Dom_storage2.res 119,828 120,143 +0.26%
packages/@rescript/runtime/Obj.res 98,894 99,153 +0.26%
packages/@rescript/runtime/RescriptTools.res 104,127 104,397 +0.26%
packages/@rescript/runtime/Stdlib_TaggedTemplate.res 115,737 116,034 +0.26%
packages/@rescript/runtime/Stdlib_Lazy.resi 112,450 112,738 +0.26%
packages/@rescript/runtime/Primitive_object.resi 117,194 117,489 +0.25%
packages/@rescript/runtime/Primitive_array_extern.res 114,436 114,724 +0.25%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat_Grouping.res 160,480 160,085 -0.25%
packages/@rescript/runtime/Stdlib_Intl_Segmenter.res 154,798 155,176 +0.24%
packages/@rescript/runtime/Stdlib_Bool.res 147,102 147,453 +0.24%
packages/@rescript/runtime/Stdlib_WeakSet.res 123,070 123,358 +0.23%
packages/@rescript/runtime/Stdlib_Temporal_Now.res 143,057 143,381 +0.23%
packages/@rescript/runtime/Stdlib_WeakMap.res 135,944 136,250 +0.23%
packages/@rescript/runtime/Stdlib_Intl_Collator.res 180,290 180,695 +0.22%
packages/@rescript/runtime/Primitive_float_extern.res 93,342 93,547 +0.22%
packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res 181,435 181,831 +0.22%
packages/@rescript/runtime/Stdlib_Intl_ListFormat.res 170,604 170,973 +0.22%
packages/@rescript/runtime/Stdlib_RegExp.res 163,888 164,239 +0.21%
packages/@rescript/runtime/Primitive_exceptions.resi 100,176 100,390 +0.21%
packages/@rescript/runtime/Stdlib_Symbol.res 144,546 144,852 +0.21%
packages/@rescript/runtime/Stdlib_Blob.res 93,480 93,676 +0.21%
packages/@rescript/runtime/Primitive_string.res 163,342 163,684 +0.21%
packages/@rescript/runtime/Primitive_hash.resi 102,642 102,856 +0.21%
packages/@rescript/runtime/Stdlib_Ordering.res 153,792 154,107 +0.20%
packages/@rescript/runtime/Primitive_float.res 149,439 149,745 +0.20%
packages/@rescript/runtime/RescriptTools_Docgen.resi 281,494 282,060 +0.20%
packages/@rescript/runtime/Primitive_int_extern.res 97,611 97,807 +0.20%
packages/@rescript/runtime/Stdlib_Type.res 204,082 204,487 +0.20%
packages/@rescript/belt/src/Belt_Int.resi 149,668 149,965 +0.20%
packages/@rescript/runtime/Stdlib_Type.resi 135,857 136,125 +0.20%
packages/@rescript/belt/src/Belt_Float.resi 151,144 151,441 +0.20%
packages/@rescript/belt/src/Belt_Float.res 165,794 166,118 +0.20%
packages/@rescript/runtime/Stdlib_Iterator.resi 161,329 161,644 +0.20%
packages/@rescript/runtime/Primitive_exceptions.res 207,499 207,904 +0.20%
packages/@rescript/belt/src/Belt_Int.res 166,463 166,787 +0.19%
packages/@rescript/runtime/RescriptTools_Docgen.res 291,782 292,348 +0.19%
packages/@rescript/runtime/Stdlib_Intl_PluralRules.res 204,411 204,807 +0.19%
packages/@rescript/runtime/Primitive_bool.res 149,351 149,639 +0.19%
packages/@rescript/runtime/Stdlib_Symbol.resi 160,309 160,615 +0.19%
packages/@rescript/belt/src/Belt_SortArrayString.resi 170,493 170,817 +0.19%
packages/@rescript/belt/src/Belt_HashSetString.resi 176,028 176,361 +0.19%
packages/@rescript/runtime/Stdlib_JsExn.res 176,820 177,153 +0.19%
packages/@rescript/runtime/Primitive_object_extern.res 114,600 114,814 +0.19%
packages/@rescript/belt/src/Belt_Id.res 295,529 296,079 +0.19%
packages/@rescript/runtime/Stdlib_IterableIterator.resi 151,539 151,821 +0.19%
packages/@rescript/runtime/Stdlib_RegExp.resi 189,072 189,423 +0.19%
packages/@rescript/belt/src/Belt_MutableStack.resi 170,230 170,545 +0.19%
packages/@rescript/belt/src/Belt_SortArrayInt.resi 170,611 170,926 +0.18%
packages/@rescript/belt/src/Belt_HashSetInt.resi 176,141 176,465 +0.18%
packages/@rescript/runtime/Primitive_bigint.res 181,503 181,827 +0.18%
packages/@rescript/runtime/Stdlib_Pair.res 168,275 168,572 +0.18%
packages/@rescript/runtime/Stdlib_Lazy.res 230,796 231,201 +0.18%
packages/@rescript/runtime/Stdlib_Int.resi 207,003 207,363 +0.17%
packages/@rescript/runtime/Primitive_int.res 175,973 176,279 +0.17%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi 162,293 162,575 +0.17%
packages/@rescript/runtime/Stdlib_BigInt.resi 187,210 187,534 +0.17%
packages/@rescript/runtime/Stdlib_Float.resi 195,163 195,496 +0.17%
packages/@rescript/runtime/Stdlib_Global.res 141,736 141,977 +0.17%
packages/@rescript/runtime/Stdlib_Set.res 213,932 214,291 +0.17%
packages/@rescript/runtime/Stdlib_Intl_Locale.res 199,607 199,940 +0.17%
packages/@rescript/belt/src/Belt_HashMapString.resi 216,055 216,415 +0.17%
packages/@rescript/runtime/Stdlib_Generator.resi 162,671 162,941 +0.17%
packages/@rescript/runtime/Stdlib_JsError.resi 185,904 186,210 +0.16%
packages/@rescript/runtime/Stdlib_BigInt.res 209,114 209,456 +0.16%
packages/@rescript/belt/src/Belt_HashMapInt.resi 216,170 216,521 +0.16%
packages/@rescript/runtime/Stdlib_Temporal_Common.res 376,579 377,190 +0.16%
packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res 396,383 397,021 +0.16%
packages/@rescript/runtime/Stdlib_Map.res 209,783 210,120 +0.16%
packages/@rescript/runtime/Stdlib_Intl.res 174,194 174,473 +0.16%
packages/@rescript/belt/src/Belt_Array.res 2,339,392 2,335,651 -0.16%
packages/@rescript/belt/src/Belt_Id.resi 261,289 261,705 +0.16%
packages/@rescript/belt/src/Belt_MutableQueue.resi 209,174 209,507 +0.16%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res 412,454 413,110 +0.16%
packages/@rescript/runtime/Stdlib_Float.res 220,930 221,272 +0.15%
packages/@rescript/runtime/Stdlib_Global.resi 156,151 156,392 +0.15%
packages/@rescript/runtime/Stdlib_AsyncGenerator.resi 169,131 169,392 +0.15%
packages/@rescript/runtime/Stdlib_JSON.resi 238,996 239,364 +0.15%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 270,652 271,062 +0.15%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 377,278 377,849 +0.15%
packages/@rescript/runtime/Stdlib_Intl_Common.res 153,932 154,164 +0.15%
packages/@rescript/runtime/Stdlib_Dict.resi 244,605 244,965 +0.15%
packages/@rescript/belt/src/Belt_HashSet.resi 226,597 226,930 +0.15%
packages/@rescript/runtime/Stdlib_Set.resi 232,306 232,647 +0.15%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 240,773 240,426 -0.14%
packages/@rescript/runtime/Stdlib_Map.resi 221,399 221,718 +0.14%
packages/@rescript/belt/src/Belt_SetString.resi 261,447 261,816 +0.14%
packages/@rescript/runtime/Stdlib_DataView.res 231,443 231,767 +0.14%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 311,764 311,328 -0.14%
packages/@rescript/belt/src/Belt_MutableSetString.resi 266,624 266,993 +0.14%
packages/@rescript/runtime/Stdlib_Option.resi 240,826 241,159 +0.14%
packages/@rescript/runtime/Pervasives.res 241,109 241,442 +0.14%
packages/@rescript/runtime/Primitive_js_extern.res 241,375 241,708 +0.14%
packages/@rescript/belt/src/Belt_SetInt.resi 261,574 261,934 +0.14%
packages/@rescript/belt/src/Belt_MutableSetInt.resi 266,741 267,101 +0.13%
packages/@rescript/runtime/Stdlib_JsError.res 234,639 234,954 +0.13%
packages/@rescript/runtime/Stdlib_Iterator.res 242,779 243,103 +0.13%
packages/@rescript/runtime/Stdlib_Null.res 348,889 348,425 -0.13%
packages/@rescript/runtime/Stdlib_Object.res 246,006 246,330 +0.13%
packages/@rescript/runtime/Dom_storage.res 169,684 169,907 +0.13%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 310,050 310,447 +0.13%
packages/@rescript/runtime/Stdlib_Nullable.res 346,834 346,393 -0.13%
packages/@rescript/runtime/Stdlib_Nullable.resi 204,013 203,757 -0.13%
packages/@rescript/runtime/Stdlib_Result.resi 305,010 305,379 +0.12%
packages/@rescript/belt/src/Belt_Range.resi 177,034 177,248 +0.12%
packages/@rescript/belt/src/Belt_Option.resi 245,756 246,053 +0.12%
packages/@rescript/runtime/Stdlib_Temporal_PlainTime.res 322,361 322,749 +0.12%
packages/@rescript/belt/src/Belt_Result.resi 247,666 247,963 +0.12%
packages/@rescript/runtime/Stdlib_DataView.resi 270,830 271,154 +0.12%
packages/@rescript/belt/src/Belt_SortArray.resi 256,163 256,460 +0.12%
packages/@rescript/belt/src/Belt_HashMap.resi 303,052 303,403 +0.12%
packages/@rescript/belt/src/Belt_MutableMapString.resi 319,879 320,248 +0.12%
packages/@rescript/runtime/Stdlib_JSON.res 301,793 302,140 +0.11%
packages/@rescript/runtime/Stdlib_Dict.res 327,220 327,589 +0.11%
packages/@rescript/belt/src/Belt_MutableMapInt.resi 319,997 320,357 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 406,241 406,693 +0.11%
packages/@rescript/runtime/Stdlib_IteratorObject.resi 240,921 241,187 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 579,976 580,615 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 386,420 386,836 +0.11%
packages/@rescript/runtime/Primitive_object.res 1,236,175 1,234,872 -0.11%
packages/@rescript/belt/src/Belt_Range.res 299,486 299,792 +0.10%
packages/@rescript/runtime/JsxEvent.res 1,097,145 1,098,252 +0.10%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 440,560 441,002 +0.10%

Instructions

In total: 9,666,377,165 → 9,633,507,777 (-0.34%).

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

Benchmark Base Head Change
stress/RefsInLoop.res 399,581,435 379,612,564 -5.00%
stress/ListLiteral.res 134,735,655 131,998,915 -2.03%
packages/@rescript/runtime/JsxDOM.res 120,203,495 118,683,145 -1.26%
packages/@rescript/belt/src/Belt_List.res 121,662,988 120,238,991 -1.17%
stress/NestedClosures.res 546,570,565 542,451,306 -0.75%
packages/@rescript/runtime/Stdlib_Math.resi 21,201,960 21,291,126 +0.42%
stress/LetRec.res 1,412,529,618 1,407,710,479 -0.34%
packages/@rescript/belt/src/Belt_List.resi 39,663,680 39,791,832 +0.32%
stress/InlineChain.res 139,324,076 138,900,496 -0.30%
packages/@rescript/runtime/JsxDOMStyle.res 97,156,934 97,428,913 +0.28%
12 more changes
Benchmark Base Head Change
packages/@rescript/runtime/Stdlib_TypedArray.res 41,276,404 41,388,768 +0.27%
packages/@rescript/belt/src/Belt_Array.resi 34,150,978 34,239,377 +0.26%
packages/@rescript/belt/src/Belt_internalAVLtree.res 142,708,562 142,419,187 -0.20%
parse/RedBlackTree.res 14,932,933 14,961,189 +0.19%
parse/RedBlackTreeNoComments.res 14,524,105 14,551,153 +0.19%
stress/TypeAliasChain.res 525,270,295 526,080,213 +0.15%
packages/@rescript/runtime/Stdlib_Array.resi 40,240,950 40,182,074 -0.15%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 31,202,307 31,157,078 -0.14%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 24,036,613 24,062,523 +0.11%
format/RedBlackTreeNoComments.res 24,058,822 24,084,366 +0.11%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 25,476,390 25,503,302 +0.11%
format/RedBlackTree.res 24,403,259 24,428,803 +0.10%

Output size

In total: 25,571,598 → 25,573,568 (+0.01%).

Benchmark Base Head Change
output/playground/compiler.js 1,891,749 1,893,719 +0.10%

Pull request 6af9e8f compared with base 145df8d in this run.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

@JonoPrest
JonoPrest force-pushed the jono/type-path-memo branch 2 times, most recently from 069bcf9 to f79567f Compare November 10, 2026 07:24
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 <jjprest@gmail.com>

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