Skip to content

Locals munged to the same JavaScript name (a-b, a_b) shadow each other instead of overwriting (#18) - #21

Merged
k7d merged 1 commit into
mainfrom
fix-id-overwrite
Oct 30, 2026
Merged

k7d merged 1 commit into
mainfrom
fix-id-overwrite

Conversation

@k7d

@k7d k7d commented Oct 30, 2026

Copy link
Copy Markdown
Member

Fixes #18.

Locals whose names munge to the same JavaScript identifier (a-b and a_b) weren't renamed apart, so one overwrote the other (CLJS-2461, CLJS-3426). The common case is a snake_case param turned into a kebab-case local:

(defn handler [{:keys [board_id]}]
  (let [board-id (parse-uuid board_id)] ...))   ; board_id became the parsed value

This was a prerequisite for #14: with const/let output each collision becomes a parse error.

Fix

The analyzer decided that one local shadows another by comparing symbols. The new cljs.analyzer/shadowed-local also looks for locals in scope that munge to the same name and sets the new local's :shadow to the one with the longest shadow chain. The compiler's existing shadow renaming then emits a_b__$1, a_b__$2, ... Picking the deepest one matters for (let [a_b 1 a-b 2 a_b 3] ...): any other choice gives the third local the same depth as the second, and they collide again.

It replaces handle-symbol-local where locals are bound: fn params, the fn self-name, let/loop (which also covers destructuring and catch, which binds through a let) and letfn.

letfn bindings had no :info {:shadow ...}, which the compiler reads when it renames, so they were never renamed: even (let [x 1] (letfn [(x [] 2)] ...)) emitted var x twice. They now carry it.

Code with no colliding locals compiles to exactly the same output as before, including classic :advanced output: the compiler tests' output assertions and size ratchet pass unchanged. A name with no _ or $ can't collide with another local, so the analyzer skips the scan for it. Compiling the runtime test suite took no measurably longer (about 10.0s with the fix, 10.3s before).

Tests

cljs.core-test/test-locals-munged-to-the-same-name covers lets in return and expression position, fn params, :keys destructuring, loop/recur, nested lets and closures, letfn, catch and reserved words (let/let$). Seven of its assertions fail on main. test-letfn-shadowing-let covers a letfn shadowing a let local.

  • script/test-esm: 0 failures, 0 errors
  • compiler tests (-M:closure:compiler.test:compiler.test.run): 0 failures, 0 errors
  • runtime :advanced, lite and self-host suites under Node: 0 failures, 0 errors

…dow each other instead of one overwriting the other (CLJS-2461, CLJS-3426)

Fixes #18
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.

Locals that munge to the same name (a-b, a_b) overwrite each other (CLJS-2461)

1 participant