Skip to content

JS: clean up reused undeclared variables when scopes close - #133

Merged
tdewolff merged 1 commit into
tdewolff:masterfrom
joshuarubin:fix/js-undeclared-scope-cleanup
Oct 18, 2026
Merged

tdewolff merged 1 commit into
tdewolff:masterfrom
joshuarubin:fix/js-undeclared-scope-cleanup

Conversation

@joshuarubin

Copy link
Copy Markdown
Contributor

Fixes #132.

Keep the indexed Declare path cheap by reusing the variable and setting its declaration kind without removing its Undeclared entry or shifting indices. When the scope closes, HoistUndeclared removes entries that also belong to that scope's Declared list in one pass, then rebuilds the index once if needed. It also adjusts the NumArgUses offset when removing entries before it.

The cleanup checks variable identity against the scope's declarations. Testing only Decl != NoDecl would also remove references to variables declared in enclosing scopes, such as the b in {var b}. Those references must remain in Undeclared.

Module and inline parsing now close the global scope too. HoistUndeclared cleans that scope without trying to hoist into a parent.

Validation:

  • Added scope-disjoint checks for read-before-declare, nested functions, default arguments, arrows, blocks, classes, switches and loop shadowing, with 1, 15, 16 and 32 names in both module and inline mode. The checks also validate indices and the argument-use offset. Added an indexed case that preserves references to enclosing declarations. The overlap checks fail on v2.8.17 and pass with this change.
  • Strengthened the existing read-then-declare timing guard to 32,000 names with a one-second limit. It fails on v2.8.18 at 12.16 seconds and passes with this change at 18.4 milliseconds on the same machine.
  • Compared all 348,150 distinct js.Var records across 2,605 real JavaScript programs, totaling 16,310,585 bytes, against v2.8.16. Every file parsed successfully. Names, declaration kinds, use counts and links all match, with zero differences.
  • Reproduced JS: v2.24.20 hangs on an esbuild bundle (v2.24.19 finishes in ~20ms) minify#1049 with its 185,602-byte input and minify v2.24.20. With parser v2.8.17, both file and stdin input time out after 15 seconds with no output. With this change, both finish in about 6 milliseconds and produce 130,822 bytes, identical to the output with v2.8.16 and v2.8.18. This fixes the reported reproducer while retaining the faster declaration path.
  • go test ./... passes. go vet ./... reports the same two findings as unchanged v2.8.18: binary.go:512, the WriteByte signature, and js/ast.go:76, unreachable code in AST.JSON.

Defer indexed declaration cleanup until scope close, including global scopes. Preserve outer references and argument-use offsets. Add scope regression checks and strengthen the timing guard.
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.

js: Parse is quadratic in the number of distinct names in a scope

2 participants