Skip to content

perf: preserve structural sharing for nested ledger ADT types#256

Open
RomarQ wants to merge 2 commits intoLFDT-Minokawa:mainfrom
RomarQ:fix/adt-nesting-preserve-sharing
Open

perf: preserve structural sharing for nested ledger ADT types#256
RomarQ wants to merge 2 commits intoLFDT-Minokawa:mainfrom
RomarQ:fix/adt-nesting-preserve-sharing

Conversation

@RomarQ
Copy link
Copy Markdown
Contributor

@RomarQ RomarQ commented Mar 25, 2026

Related to #255, instead of just adding a guard, it adds an optimization to preserve structurally shared tadt nodes across nanopass passes.

Problem

The Compact compiler crashes with OOM when compiling contracts with deeply nested Map types (e.g. Map<Field, Map<Field, Map<...>>>). Depth 4 compiles fine, but depth 7 causes OOM.

Each nanopass pass auto-transforms through tadt nodes, rebuilding the entire subtree even when nothing changes. The expand-modules-and-types pass creates structurally shared tadt nodes (multiple references point to the same Scheme object), but every subsequent pass breaks this sharing by creating independent copies. With ~14 passes in the analysis pipeline each independently expanding the tree, memory grows exponentially.

Measured before this fix:

  • Depth 6: 3.4 GB, 10s
  • Depth 8: OOM

Fix

Add explicit tadt clauses to the analysis passes so they preserve structural sharing instead of blindly rebuilding:

  • Cross-language passes use an eq?-keyed hash table so shared input nodes produce shared output nodes.
  • Same-language passes return the original node unchanged, since they don't modify types.

Measured after this fix:

  • Depth 6: 80 MB, 0.05s
  • Depth 8: 80 MB, 0.05s
  • Depth 16: 80 MB, 0.06s

@RomarQ RomarQ requested review from a team as code owners March 25, 2026 09:15
Nanopass passes auto-transform through tadt nodes, rebuilding the
entire subtree at each pass. For nested Map types, this breaks
structural sharing created by expand-modules-and-types, causing
exponential memory growth across the analysis pass pipeline.

Fix by adding:
- eq?-memoized tadt clauses to cross-language passes (infer-types,
  remove-tundeclared, combine-ledger-declarations, recognize-let,
  determine-ledger-paths, remove-disclose) so shared input nodes
  produce shared output nodes
- Pass-through tadt clauses to same-language passes
  (generate-contract-ht, discard-unused-functions, etc.) that
  return the original node unchanged

Result: compilation uses constant ~80MB regardless of nesting depth,
down from 3.4GB at depth 6 / OOM at depth 8.

Signed-off-by: Rodrigo Quelhas <rodrigo_quelhas@outlook.pt>
@RomarQ RomarQ force-pushed the fix/adt-nesting-preserve-sharing branch from 5f4b993 to d3a27b0 Compare March 25, 2026 09:18
@github-actions
Copy link
Copy Markdown

Compactc E2E Tests Results

  1 files  ±     0   48 suites  +47   2m 32s ⏱️ - 27m 27s
467 tests  - 14 533  461 ✅  - 14 539  6 💤 +6  0 ❌ ±0 
478 runs   - 14 522  472 ✅  - 14 528  6 💤 +6  0 ❌ ±0 

Results for commit d3a27b0. ± Comparison against base commit fd5310c.

This pull request removes 15000 and adds 467 tests. Note that renamed tests count towards both.
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_0.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_1.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_10.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_100.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_101.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_102.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_103.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_104.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_105.compact'
src/tests/fuzzer.e2e.test.ts ‑ [E2E] Fuzzer tests for compiler > should be able to compile synthetic contract: 'assert_contract_106.compact'
…
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[MFG-413] should compile and not thro…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-12371] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-15405] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-15733] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-15826] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16040] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16059] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16447] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16853] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16999] should return an error if …'
…

@github-actions
Copy link
Copy Markdown

Plugin Test Summary

 1 files   3 suites   1s ⏱️
21 tests 21 ✅ 0 💤 0 ❌
23 runs  23 ✅ 0 💤 0 ❌

Results for commit fcf36c8.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

Compactc E2E Test Summary

    1 files  ±    0      1 suites   - 47   5m 57s ⏱️ + 3m 22s
2 825 tests +2 358  2 825 ✅ +2 364  0 💤  - 6  0 ❌ ±0 
2 825 runs  +2 347  2 825 ✅ +2 353  0 💤  - 6  0 ❌ ±0 

Results for commit fcf36c8. ± Comparison against base commit b2a2f08.

This pull request removes 467 and adds 2825 tests. Note that renamed tests count towards both.
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[MFG-413] should compile and not thro…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-12371] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-15405] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-15733] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-15826] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16040] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16059] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16447] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16853] should compile and not thr…'
src/tests/bugs/compiler.bugs.e2e.test.ts ‑ [Bugs] Compiler > '[PM-16999] should return an error if …'
…
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_0.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_10.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_100.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1000.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1001.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1002.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1003.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1004.compact'
src/tests/extracted.e2e.test.ts ‑ [E2E] Extracted unit tests for compiler > should be able to compile extracted contract: '/home/runner/work/_temp/nix-shell.XE0ZIn/temp-test-Ektv0m/contract_1005.compact'
…

♻️ This comment has been updated with latest results.

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