Skip to content

fix(lower): linear-time inference for nested literals (#5258)#5262

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5258-nested-objlit-lowering
Jun 16, 2026
Merged

fix(lower): linear-time inference for nested literals (#5258)#5262
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5258-nested-objlit-lowering

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5258 — lowering a nested object literal was O(n²) in nesting depth, so perry check/perry compile stalled in check-lower on large/minified bundles (a 13 MB bundle never cleared the stage).

Root cause

infer_type_from_expr is re-run on the current value at every nesting level during lowering (e.g. lower_object calls it per field at expr_object.rs:619). Its Object/Array/Arrow arms recurse into the entire remaining subtree, so per-level work scaled with the remaining depth → O(n²) overall. (This is the classic recursive-descent O(n²) the issue's profile pointed at — three mutually-recursive functions lower_object → lower_prop_value_named → lower_expr → lower_object, with the per-level subtree re-walk inside the inference call.)

Fix

Cap the inference recursion at depth 48 (crates/perry-hir/src/lower_types.rs). Past the cap it returns Type::Any — the universal sound fallback the codebase already relies on (codegen routes Any through the tag-aware paths; see the Array/Bin arm comments). This only drops type precision far past any realistic source nesting, never correctness, and the top-level type tag is unchanged so anon-shape class dedup is unaffected.

Per-level cost is now bounded → lowering is linear in depth.

Measured (issue's repro)

depth before after
1000 0.30 s 0.05 s
2000 1.12 s 0.07 s
4000 4.41 s 0.11 s
8000 23.8 s 0.20 s
20000 0.49 s

Correctness

  • Full perry-hir test suite passes (incl. the existing shape-inference tests).
  • A new regression test (nested_object_literal_lowers_in_linear_time) lowers a 6000-deep literal and asserts linear wall-time + that the outer binding still infers Type::Object.
  • Spot-checked a compiled program with 100-deep nested objects (past the cap), nested member access, and number-field arithmetic — byte-for-byte matches node --experimental-strip-types.

Scope note

The issue also flagged nested arrow chains (()=>()=>…) as a milder super-linear case. That turns out to be a separate root cause — closure capture analysis (compute_closure_captures) re-walks nested closure bodies at every level — and lives in correctness-sensitive capture code (dayjs/Effect bug history). It is not addressed here and is left for a focused follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed performance degradation and potential crashes when type inferencing deeply nested object literals.
  • Tests

    • Added regression test ensuring deeply nested structures are processed efficiently without performance degradation.

… depth (#5258)

Lowering a deeply-nested object literal was O(n²). `infer_type_from_expr`
is re-run on the *current* value at every nesting level during lowering,
and its Object/Array/Arrow arms recursed into the entire remaining subtree,
so per-level work scaled with the remaining depth. An 8000-deep literal
stalled `check-lower` for ~24s; a 13 MB minified bundle (wall-to-wall
deeply-nested object literals) never finished.

Cap the inference recursion at depth 48. Past the cap it returns Type::Any —
the universal sound fallback the codebase already relies on (codegen routes
Any through the tag-aware paths), so this only drops type *precision* far
past any realistic source nesting, never correctness. The top-level type tag
(hence anon-shape dedup) is unchanged. Per-level cost is now bounded, so
lowering is linear in depth: 8000-deep drops from 23.8s to 0.20s; 20000-deep
finishes in 0.49s.

Regression test lowers a 6000-deep literal and asserts linear wall-time.

Note: nested arrow chains (`()=>()=>…`) show a milder super-linear shape from
a *separate* root cause — closure capture analysis (compute_closure_captures)
re-walks nested closure bodies at every level — left for a follow-up since it
touches correctness-sensitive (dayjs/Effect) capture code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa2e7515-5b2e-4a36-9708-26a395822171

📥 Commits

Reviewing files that changed from the base of the PR and between ffc9ba8 and 146422a.

📒 Files selected for processing (2)
  • crates/perry-hir/src/lower_types.rs
  • crates/perry-hir/tests/shape_inference.rs

📝 Walkthrough

Walkthrough

infer_type_from_expr gains a thread-local recursion depth counter capped at INFER_TYPE_RECURSION_CAP; exceeding it returns Type::Any immediately. A DepthGuard with Drop keeps the counter balanced. A new regression test builds a 6000-deep object literal, lowers it on a high-stack thread, and asserts wall-clock time stays under 5 seconds.

Changes

Recursion depth cap for infer_type_from_expr

Layer / File(s) Summary
Depth cap + regression test
crates/perry-hir/src/lower_types.rs, crates/perry-hir/tests/shape_inference.rs
INFER_TYPE_RECURSION_CAP and a thread-local INFER_DEPTH counter are added to lower_types.rs; a DepthGuard decrements the counter on drop and the function short-circuits to Type::Any at the cap. The regression test nested_object_literal_lowers_in_linear_time builds a 6000-deep object literal, runs lowering on a high-stack thread, and asserts both a sub-5-second wall time and Type::Object for the outermost binding.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Down the rabbit hole I'd go,
Each nested object, level by level below.
But now a guard stands at the stair —
"Too deep!" it cries, returning Any with care.
The compiler hops along in linear time,
No more quadratic waits — oh so sublime! 🎉

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-5258-nested-objlit-lowering

Comment @coderabbitai help to get the list of available commands and usage tips.

@proggeramlug proggeramlug merged commit 0dafb62 into main Jun 16, 2026
12 of 15 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-5258-nested-objlit-lowering branch June 16, 2026 11:16
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.

lower: nested object literals lower in O(n²) time — large/minified bundles stall in check-lower

1 participant