Skip to content

Split let rec groups in linear time without the ordered hash map stack - #9153

Merged
cknitt merged 3 commits into
masterfrom
remove-scc-hash-maps
Nov 1, 2026
Merged

cknitt merged 3 commits into
masterfrom
remove-scc-hash-maps

Conversation

@cknitt

@cknitt cknitt commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Splitting a let rec group into its recursive components was the only user of Ordered_hash_map{,_gen,_local_ident}, Hash_set_ident_mask and Ext_util.power_2_above. Together with their two ounit test files that's 468 lines. It was also O(n²) in the size of the group. (The @deriving registry removal that was here is now #9157.)

Commit 1: plain arrays. preprocess_deps numbers the bindings with an array and a Hash_ident table, and records which bindings each body refers to, still stopping the walk once a body has referred to every binding. Edges now come out in binding order rather than the mask's hash order.

Commit 2: linear. Each body allocated and scanned a bool array as long as the group. One shared array now records which body last referred to each binding, so finding the edges costs the size of the bodies.

The generated JavaScript is unchanged across the test suites, and also for a hand-written group of 9 bindings that splits into several components.

Tests (third commit):

  • ounit_lambda_scc_tests.ml checks bind_rec's output: an edge to a binding that an earlier body also referred to survives the shared array (a mutation that leaks it between bodies makes this fail); a mixed group keeps cycles, self-recursion and plain lets apart, in dependency order; and a body referring to every binding hits the early stop correctly.
  • let_rec_components_test.res runs such a group end to end.

Performance

Measured against master bsc (user time, min of 5 or 9 runs; JS identical in every case).

Large generated groups, each binding calling the next one and the first:

Bindings master arrays (commit 1) linear (commit 2)
4,000 0.62 s 0.56 s 0.48 s
8,000 1.73 s 1.50 s 1.11 s
16,000 5.31 s 4.32 s 2.74 s

Normal code: 2,000 groups of each size. Time is unchanged within noise, and allocation goes up by about 0.1–0.15%, roughly 30 words for a 2-binding group:

Group size master this PR
1 0.15 s 0.15 s (separate path, untouched)
2 0.32 s 0.31 s
5 0.88 s 0.90 s
10 1.89 s 1.87 s

Peak memory is unchanged. What remains superlinear for huge groups is in the type checker: Typecore.enter_variable checks each pattern variable for a duplicate name by scanning all variables bound so far in the group, worth about another 25% at 16,000 bindings. That's for a separate PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kbf3gKyiYMf54wPRxFdUWe

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

  • Split large let rec groups into their recursive components in time linear in their size; a group of 16,000 bindings compiles about twice as fast.

Semantic Diff

3 commits · Full page

Split let rec groups with plain arrays instead of an ordered hash map

2e7b96b · cknitt + Claude · Oct 31, 2026, 10:16 UTC

Lambda_scc.preprocess_deps was the only user of Ordered_hash_map and Hash_set_ident_mask (468 lines with their test files). It now numbers the bindings with an array and a Hash_ident table, and collects each body's references in a bool array, still stopping the walk once every binding is hit. Edges come out in binding order; the generated JavaScript is unchanged across the test suites.

Find the edges of a let rec group in time linear in its bodies

707e268 · cknitt + Claude · Oct 31, 2026, 10:44 UTC

Each body allocated and scanned a bool array as long as the group, so a group of n bindings cost O(n^2) on top of walking the bodies. One shared array now records which body last referred to each binding, and each body's edges are collected as found and sorted. A generated group of 16,000 bindings compiles in 2.7 s instead of 4.3 s (5.3 s on master); the generated JavaScript is unchanged.

✨ Test how let rec groups are split into components

b23f519 · cknitt + Claude · Nov 01, 2026, 06:55 UTC · new

Codex review: the rewrite of preprocess_deps removed the old modules' tests and added none. ounit_lambda_scc_tests checks bind_rec's output: an edge to a binding an earlier body also referred to survives the shared seen array (a mutation that leaks it between bodies fails the test), a mixed group keeps cycles, self-recursion and plain lets apart in dependency order, and a body referring to every binding stops early correctly. let_rec_components_test runs such a group end to end.

@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.30508% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.73%. Comparing base (8e26a9c) to head (b23f519).

Files with missing lines Patch % Lines
tests/ounit_tests/ounit_lambda_scc_tests.ml 97.22% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9153      +/-   ##
==========================================
- Coverage   81.74%   81.73%   -0.02%     
==========================================
  Files         460      455       -5     
  Lines       64944    64793     -151     
==========================================
- Hits        53091    52957     -134     
+ Misses      11853    11836      -17     
Files with missing lines Coverage Δ
compiler/ext/ext_util.ml 23.07% <ø> (-1.93%) ⬇️
compiler/ml/lambda_scc.ml 85.48% <100.00%> (ø)
tests/ounit_tests/ounit_tests_main.ml 100.00% <ø> (ø)
tests/ounit_tests/ounit_lambda_scc_tests.ml 97.22% <97.22%> (ø)

... and 2 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 Oct 31, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: b23f519

@github-actions

Copy link
Copy Markdown

cknitt and others added 2 commits November 1, 2026 06:32
Lambda_scc.preprocess_deps was the only user of Ordered_hash_map and
Hash_set_ident_mask (468 lines with their test files). It now numbers
the bindings with an array and a Hash_ident table, and collects each
body's references in a bool array, still stopping the walk once every
binding is hit. Edges come out in binding order; the generated
JavaScript is unchanged across the test suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gKyiYMf54wPRxFdUWe
Signed-off-by: Christoph Knittel <ck@cca.io>
Each body allocated and scanned a bool array as long as the group, so a
group of n bindings cost O(n^2) on top of walking the bodies. One shared
array now records which body last referred to each binding, and each
body's edges are collected as found and sorted. A generated group of
16,000 bindings compiles in 2.7 s instead of 4.3 s (5.3 s on master);
the generated JavaScript is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gKyiYMf54wPRxFdUWe
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the remove-scc-hash-maps branch from 0dc558d to 707e268 Compare November 1, 2026 06:37
@cknitt cknitt changed the title Remove single-use machinery: ordered hash maps for let rec, the @deriving registry Split let rec groups in linear time without the ordered hash map stack Nov 1, 2026
@cknitt
cknitt requested a review from cristianoc November 1, 2026 06:43
@cknitt
cknitt marked this pull request as ready for review November 1, 2026 06:43
@cknitt

cknitt commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 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-01T06:57:03.869117Z b23f519 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 chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 707e2680c8

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

Comment thread compiler/ml/lambda_scc.ml Resolved
Codex review: the rewrite of preprocess_deps removed the old modules'
tests and added none. ounit_lambda_scc_tests checks bind_rec's output:
an edge to a binding an earlier body also referred to survives the
shared seen array (a mutation that leaks it between bodies fails the
test), a mixed group keeps cycles, self-recursion and plain lets apart
in dependency order, and a body referring to every binding stops early
correctly. let_rec_components_test runs such a group end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gKyiYMf54wPRxFdUWe
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: b23f519f0a

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

@cknitt
cknitt enabled auto-merge (squash) November 1, 2026 07:05
@cknitt
cknitt merged commit 8d48a8e into master Nov 1, 2026
26 checks passed
@cknitt
cknitt deleted the remove-scc-hash-maps branch November 1, 2026 13:31
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.

2 participants