Skip to content

Expand chains of type aliases without repeated occur checks - #9257

Draft
cknitt wants to merge 1 commit into
masterfrom
type-alias-chain-perf
Draft

cknitt wants to merge 1 commit into
masterfrom
type-alias-chain-perf

Conversation

@cknitt

@cknitt cknitt commented Nov 7, 2026

Copy link
Copy Markdown
Member

Part of #9256 (chains of type aliases).

A chain like type t1<'a> = t0<('a, int)>, type t2<'a> = t1<('a, int)>, … was cubic: each new alias is checked by expanding the whole chain several times, and every expansion step ran occur checks over arguments that grow along the chain.

  • Ctype.subst binds the freshly instantiated parameters of an expanded alias without an occur check, when they are distinct unlinked variables that nothing can contain yet. Constrained or non-generic parameters keep the full check.
  • Typedecl.check_constraints_rec skips type constructors whose parameters are distinct variables, since they accept any arguments. Matching against the constrained instance expanded both sides fully.
  • Ctype.cyclic_abbrev forgets the expansions it memorized, so that generalizing the declaration doesn't walk the whole chain again, and keeps the types it has seen in a set.

Measured with bsc on linux-arm64, allocated words and wall time:

Aliases Before After
50 13.3M, 38 ms 1.8M, 24 ms
100 102M, 207 ms 5.4M, 44 ms
200 851M, 1.62 s 19.3M, 150 ms

What remains: expansions memorize each alias in one list shared along the chain, and lookups scan it, so 800 aliases still take 6.7 s (290M words). Fixing that needs a different memo structure.

New test: constraint_failed_recursive checks that a constraint is still enforced through a recursive group, where only check_constraints_rec sees it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kbf3gK1dcjbe97yY1wRMgs

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

  • Speed up type checking of long chains of parameterized type aliases.

Semantic Diff

1 commit · Full page

Expand chains of type aliases without repeated occur checks

6a1750e · cknitt + Claude · Nov 07, 2026, 10:05 UTC

Why

Each alias in type t1<'a> = t0<('a, int)>, type t2<'a> = t1<('a, int)>, … was checked by expanding the whole chain several times, and every expansion step ran occur checks over arguments that grow along the chain: 200 aliases allocated 850M words and took 1.6 s. They now allocate 19M words and take 0.15 s.

Binding the fresh parameters of an expanded alias skips the occur check, since nothing can contain them yet. The constraint check skips type constructors whose parameters are distinct variables, which accept any arguments, and the cycle check forgets the expansions it memorized.

Review

  • What remains superlinear? Expansions memorize each alias in one list shared along the chain, and looking an alias up scans it, a small cubic term: 800 aliases take 6.7 s, 290M words.

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.29412% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.74%. Comparing base (bf35764) to head (6a1750e).

Files with missing lines Patch % Lines
compiler/ml/ctype.ml 85.00% 3 Missing ⚠️
compiler/ml/typedecl.ml 85.71% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9257   +/-   ##
=======================================
  Coverage   81.74%   81.74%           
=======================================
  Files         457      457           
  Lines       65713    65736   +23     
=======================================
+ Hits        53715    53737   +22     
- Misses      11998    11999    +1     
Files with missing lines Coverage Δ
compiler/ml/typedecl.ml 83.66% <85.71%> (+0.13%) ⬆️
compiler/ml/ctype.ml 70.34% <85.00%> (+0.15%) ⬆️
🚀 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 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 6a1750e

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Why:
    Each alias in `type t1<'a> = t0<('a, int)>`, `type t2<'a> = t1<('a, int)>`,
    … was checked by expanding the whole chain several times, and every
    expansion step ran occur checks over arguments that grow along the
    chain: 200 aliases allocated 850M words and took 1.6 s. They now
    allocate 19M words and take 0.15 s.

Binding the fresh parameters of an expanded alias skips the occur check,
since nothing can contain them yet. The constraint check skips type
constructors whose parameters are distinct variables, which accept any
arguments, and the cycle check forgets the expansions it memorized.

Review: What remains superlinear? Expansions memorize each alias in one
list shared along the chain, and looking an alias up scans it, a small
cubic term: 800 aliases take 6.7 s, 290M words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gK1dcjbe97yY1wRMgs
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the type-alias-chain-perf branch from 5546f81 to 6a1750e Compare November 7, 2026 18:06
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

🎉 Compiler benchmarks

Allocated words

In total: 456,989,425 → 435,343,562 (-4.74%).

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

Benchmark Base Head Change
stress/TypeAliasChain.res 22,449,889 2,299,449 -89.76%
packages/@rescript/runtime/Dom.res 1,695,433 1,479,248 -12.75%
packages/@rescript/runtime/JsxDOM.res 3,314,709 2,997,372 -9.57%
packages/@rescript/runtime/JsxDOMStyle.res 2,517,375 2,295,680 -8.81%
packages/@rescript/runtime/Stdlib_Temporal_Common.res 855,124 826,025 -3.40%
packages/@rescript/runtime/RescriptTools_Docgen.resi 759,438 741,809 -2.32%
packages/@rescript/runtime/RescriptTools_Docgen.res 769,841 752,212 -2.29%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res 891,600 875,649 -1.79%
packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res 875,253 860,052 -1.74%
packages/@rescript/belt/src/Belt_HashMap.res 1,848,233 1,820,407 -1.51%
99 more changes
Benchmark Base Head Change
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 1,060,605 1,046,001 -1.38%
packages/@rescript/belt/src/Belt_HashSet.res 1,573,656 1,554,187 -1.24%
packages/@rescript/belt/src/Belt_MapDict.res 2,359,280 2,331,758 -1.17%
packages/@rescript/belt/src/Belt_MapInt.res 1,816,494 1,797,521 -1.04%
packages/@rescript/belt/src/Belt_MapString.res 1,817,038 1,798,065 -1.04%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,671,321 1,654,047 -1.03%
packages/@rescript/belt/src/Belt_HashMapString.res 1,672,033 1,654,755 -1.03%
packages/@rescript/belt/src/Belt_internalBuckets.resi 712,893 706,054 -0.96%
packages/@rescript/belt/src/Belt_internalAVLtree.resi 958,863 949,669 -0.96%
packages/@rescript/belt/src/Belt_internalMapInt.res 2,090,736 2,071,022 -0.94%
packages/@rescript/belt/src/Belt_internalMapString.res 2,092,574 2,072,860 -0.94%
packages/@rescript/runtime/Stdlib_Intl_PluralRules.res 682,314 676,400 -0.87%
packages/@rescript/belt/src/Belt_MutableMap.res 1,941,992 1,925,767 -0.84%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,773,700 1,759,005 -0.83%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,773,969 1,759,282 -0.83%
packages/@rescript/runtime/Stdlib_Promise.resi 854,160 847,362 -0.80%
packages/@rescript/belt/src/Belt_internalBuckets.res 1,556,445 1,544,553 -0.76%
packages/@rescript/runtime/Stdlib_Result.res 1,903,060 1,889,380 -0.72%
packages/@rescript/belt/src/Belt_internalSetBuckets.resi 679,586 674,881 -0.69%
packages/@rescript/belt/src/Belt_SetDict.res 2,121,382 2,106,700 -0.69%
packages/@rescript/runtime/Stdlib_Promise.res 854,604 848,712 -0.69%
packages/@rescript/belt/src/Belt_Map.res 1,561,134 1,551,040 -0.65%
packages/@rescript/runtime/Stdlib_Intl_Locale.res 677,566 673,397 -0.62%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 1,239,575 1,232,037 -0.61%
packages/@rescript/runtime/Stdlib_Intl_Collator.res 658,124 654,133 -0.61%
packages/@rescript/belt/src/Belt_Set.res 1,419,186 1,410,603 -0.60%
packages/@rescript/belt/src/Belt_internalAVLset.resi 849,821 844,819 -0.59%
packages/@rescript/belt/src/Belt_HashSetInt.res 1,412,456 1,404,318 -0.58%
packages/@rescript/belt/src/Belt_HashSetString.res 1,412,763 1,404,627 -0.58%
packages/@rescript/belt/src/Belt_internalAVLtree.res 4,426,588 4,402,308 -0.55%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 865,691 861,091 -0.53%
packages/@rescript/runtime/Stdlib_IteratorObject.res 743,930 740,087 -0.52%
packages/@rescript/belt/src/Belt_MutableSet.res 2,609,727 2,596,681 -0.50%
packages/@rescript/runtime/JsxEvent.res 1,593,265 1,585,629 -0.48%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 856,482 852,447 -0.47%
packages/@rescript/belt/src/Belt_MapDict.resi 1,011,869 1,007,393 -0.44%
packages/@rescript/belt/src/Belt_Map.resi 1,076,538 1,071,895 -0.43%
packages/@rescript/belt/src/Belt_List.resi 1,270,413 1,265,371 -0.40%
packages/@rescript/belt/src/Belt_MutableStack.res 893,146 889,704 -0.39%
packages/@rescript/belt/src/Belt_MutableMap.resi 886,344 883,025 -0.37%
packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res 659,287 656,835 -0.37%
packages/@rescript/belt/src/Belt_MutableQueue.res 1,328,287 1,323,404 -0.37%
packages/@rescript/belt/src/Belt_Result.res 920,631 917,353 -0.36%
packages/@rescript/belt/src/Belt_Set.resi 926,931 923,932 -0.32%
packages/@rescript/runtime/Stdlib_Result.resi 782,936 780,444 -0.32%
packages/@rescript/belt/src/Belt_HashMap.resi 781,990 779,592 -0.31%
packages/@rescript/belt/src/Belt_MutableSet.resi 885,936 883,233 -0.31%
packages/@rescript/belt/src/Belt_MutableSetString.res 2,314,432 2,307,414 -0.30%
packages/@rescript/belt/src/Belt_MutableSetInt.res 2,313,192 2,306,186 -0.30%
packages/@rescript/runtime/Stdlib_Intl_ListFormat.res 648,404 646,463 -0.30%
packages/@rescript/runtime/Stdlib_IteratorObject.resi 718,829 716,720 -0.29%
packages/@rescript/belt/src/Belt_SetDict.resi 892,770 890,153 -0.29%
packages/@rescript/belt/src/Belt_internalAVLset.res 3,657,958 3,647,442 -0.29%
packages/@rescript/belt/src/Belt_internalSetInt.res 1,267,668 1,264,086 -0.28%
packages/@rescript/belt/src/Belt_internalSetString.res 1,268,854 1,265,274 -0.28%
packages/@rescript/belt/src/Belt_internalBucketsType.resi 644,590 642,774 -0.28%
packages/@rescript/runtime/Stdlib_AsyncGenerator.res 702,496 700,521 -0.28%
packages/@rescript/runtime/Stdlib_JSON.resi 717,065 715,084 -0.28%
packages/@rescript/belt/src/Belt_Result.resi 726,130 724,132 -0.28%
packages/@rescript/belt/src/Belt_SetString.res 1,895,759 1,890,688 -0.27%
packages/@rescript/belt/src/Belt_SetInt.res 1,895,032 1,889,967 -0.27%
packages/@rescript/belt/src/Belt_HashSet.resi 705,390 703,522 -0.26%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 719,827 718,025 -0.25%
packages/@rescript/runtime/Stdlib_Date.res 1,057,120 1,054,506 -0.25%
packages/@rescript/runtime/Stdlib_Generator.res 688,088 686,407 -0.24%
packages/@rescript/runtime/Stdlib_JSON.res 784,466 782,553 -0.24%
packages/@rescript/runtime/Stdlib_Date.resi 1,103,247 1,100,633 -0.24%
packages/@rescript/runtime/Stdlib_IterableIterator.res 664,689 663,134 -0.23%
packages/@rescript/runtime/Stdlib_Option.res 1,288,904 1,285,890 -0.23%
packages/@rescript/runtime/Stdlib_AsyncGenerator.resi 646,704 645,209 -0.23%
packages/@rescript/runtime/Stdlib_AsyncIterator.resi 659,649 658,140 -0.23%
packages/@rescript/belt/src/Belt_internalBucketsType.res 777,633 775,870 -0.23%
packages/@rescript/runtime/Stdlib_Console.res 864,284 862,530 -0.20%
packages/@rescript/runtime/Stdlib_Console.resi 901,027 899,273 -0.19%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi 639,847 638,621 -0.19%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 792,369 790,852 -0.19%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 788,857 787,353 -0.19%
packages/@rescript/belt/src/Belt_Array.resi 1,151,534 1,149,376 -0.19%
packages/@rescript/runtime/Stdlib_Intl_Segmenter.res 632,537 631,380 -0.18%
packages/@rescript/runtime/Stdlib_Generator.resi 640,209 639,062 -0.18%
packages/@rescript/belt/src/Belt_Id.res 775,865 774,515 -0.17%
packages/@rescript/runtime/Stdlib_IterableIterator.resi 629,052 627,959 -0.17%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 749,204 748,004 -0.16%
packages/@rescript/belt/src/Belt_List.res 3,950,091 3,943,864 -0.16%
packages/@rescript/runtime/Primitive_hash.res 1,501,651 1,499,346 -0.15%
packages/@rescript/runtime/Stdlib_Option.resi 718,636 717,582 -0.15%
stress/Record.res 26,508,094 26,474,488 -0.13%
packages/@rescript/runtime/Stdlib_Nullable.resi 681,799 680,951 -0.12%
packages/@rescript/runtime/Stdlib_List.resi 896,248 895,138 -0.12%
packages/@rescript/runtime/Stdlib_Intl_Segments.res 593,977 593,244 -0.12%
packages/@rescript/runtime/Stdlib_Type.resi 613,261 612,507 -0.12%
packages/@rescript/runtime/Stdlib_Null.resi 686,311 685,478 -0.12%
packages/@rescript/belt/src/Belt_Array.res 2,842,879 2,839,600 -0.12%
packages/@rescript/runtime/Stdlib_Dict.resi 722,506 721,715 -0.11%
packages/@rescript/runtime/Stdlib_Null.res 830,463 829,592 -0.10%
packages/@rescript/runtime/Stdlib_TaggedTemplate.resi 583,770 583,160 -0.10%
packages/@rescript/runtime/Stdlib_Type.res 682,516 681,804 -0.10%
packages/@rescript/runtime/Stdlib_Nullable.res 828,570 827,726 -0.10%
packages/@rescript/runtime/Stdlib_RegExp.res 646,540 645,887 -0.10%

Instructions

In total: 10,226,961,817 → 9,754,934,599 (-4.62%).

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

Benchmark Base Head Change
stress/TypeAliasChain.res 536,835,458 95,336,545 -82.24%
packages/@rescript/runtime/JsxDOM.res 143,848,113 128,092,469 -10.95%
packages/@rescript/runtime/JsxDOMStyle.res 119,996,614 109,179,625 -9.01%
packages/@rescript/runtime/Stdlib_Temporal_ZonedDateTime.res 54,183,618 53,393,643 -1.46%
packages/@rescript/belt/src/Belt_internalAVLtree.res 166,149,501 165,325,949 -0.50%
packages/@rescript/runtime/Stdlib_Date.resi 57,209,151 57,025,484 -0.32%
packages/@rescript/belt/src/Belt_List.resi 62,577,399 62,391,473 -0.30%
packages/@rescript/belt/src/Belt_Array.resi 57,640,007 57,480,895 -0.28%
packages/@rescript/runtime/Stdlib_Array.resi 63,213,231 63,119,483 -0.15%
packages/@rescript/belt/src/Belt_List.res 145,581,008 145,370,895 -0.14%
2 more changes
Benchmark Base Head Change
packages/@rescript/runtime/Stdlib_List.resi 47,643,213 47,575,596 -0.14%
stress/Record.res 1,084,006,651 1,082,558,749 -0.13%

Pull request 6a1750e compared with base bf35764 in this run.

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