In sloppy JavaScript, an Annex B block-function declaration can assign its function object to an existing same-name var binding when the block executes. The compressor does not see that runtime alias write when semantic keeps the block function and the existing var as separate symbols, so it can inline a stale value and change program behavior.
Reproduction
function outer(c) {
var f = 1;
if (c) {
function f() {}
}
return () => f;
}
console.log(typeof outer(true)(), typeof outer(false)());
The original prints:
Compress-only minification on current main (cf628ac433) produces:
function outer(c){if(c){function f(){}}return()=>1}
The minified program prints:
The same divergence remains after #24650 and #24658. On that stack, running compression twice also changes the output again by removing the now-unused block, although both compressed outputs are already behaviorally wrong.
Cause
When the enclosing var scope already contains f, the Annex B binder path does not move the block function into that scope. The outer var f and block function f therefore have separate symbols, and symbol_redeclarations for the outer symbol is empty. At runtime, Annex B still copies the function object into the outer binding, but the compressor's symbol facts see no write and treat f = 1 as constant.
This is related to #24331, which demonstrates the same var-first Annex B alias blind spot in the mangler, but the failure here happens during compression without mangling. #20728 fixed the general hoisting model; this existing-binding edge remains.
The fix likely needs semantic metadata that records the runtime alias even when the symbols stay separate, or an equivalent conservative gate in consumers of symbol value facts.
Reported with AI assistance (Claude Code and OpenAI Codex). The behavior and Node output were independently reproduced on current main and the #24658 stack before filing.
In sloppy JavaScript, an Annex B block-function declaration can assign its function object to an existing same-name
varbinding when the block executes. The compressor does not see that runtime alias write when semantic keeps the block function and the existingvaras separate symbols, so it can inline a stale value and change program behavior.Reproduction
The original prints:
Compress-only minification on current main (
cf628ac433) produces:The minified program prints:
The same divergence remains after #24650 and #24658. On that stack, running compression twice also changes the output again by removing the now-unused block, although both compressed outputs are already behaviorally wrong.
Cause
When the enclosing var scope already contains
f, the Annex B binder path does not move the block function into that scope. The outervar fand blockfunction ftherefore have separate symbols, andsymbol_redeclarationsfor the outer symbol is empty. At runtime, Annex B still copies the function object into the outer binding, but the compressor's symbol facts see no write and treatf = 1as constant.This is related to #24331, which demonstrates the same var-first Annex B alias blind spot in the mangler, but the failure here happens during compression without mangling. #20728 fixed the general hoisting model; this existing-binding edge remains.
The fix likely needs semantic metadata that records the runtime alias even when the symbols stay separate, or an equivalent conservative gate in consumers of symbol value facts.
Reported with AI assistance (Claude Code and OpenAI Codex). The behavior and Node output were independently reproduced on current main and the #24658 stack before filing.