fix(lower): linear-time inference for nested literals (#5258)#5262
Conversation
… 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>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesRecursion depth cap for infer_type_from_expr
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Summary
Fixes #5258 — lowering a nested object literal was O(n²) in nesting depth, so
perry check/perry compilestalled incheck-loweron large/minified bundles (a 13 MB bundle never cleared the stage).Root cause
infer_type_from_expris re-run on the current value at every nesting level during lowering (e.g.lower_objectcalls it per field atexpr_object.rs:619). ItsObject/Array/Arrowarms 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 functionslower_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 returnsType::Any— the universal sound fallback the codebase already relies on (codegen routesAnythrough the tag-aware paths; see theArray/Binarm 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)
Correctness
perry-hirtest suite passes (incl. the existing shape-inference tests).nested_object_literal_lowers_in_linear_time) lowers a 6000-deep literal and asserts linear wall-time + that the outer binding still infersType::Object.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
Tests