Parent: #493 Related: #926 (same class, different scan), #496
Status: code inspection, verified by reading. No measurement exists yet and no performance claim is made here.
Found while tracing #926's blast radius. This is a second instance of the same "re-scan the whole subtree per nested scope" shape in src/parser.zig, on a different axis from the private-name one.
Mechanism
checkLexicalDupes enforces the Block 14.2.1 / Script 16.1.1 / FunctionBody 15.2.1 early error that a scope's LexicallyDeclaredNames must not intersect its VarDeclaredNames. Because var hoists out of nested blocks, it collects var names across the whole subtree (src/parser.zig:1059-1061):
if (seen.count() > 0) {
var var_names = self.secureStringMap(void);
for (stmts) |s| try self.collectVarNames(s, &var_names);collectVarNames (src/parser.zig:1143) descends through .block, .if_stmt, loops and try without crossing a function boundary, and recurseScope re-enters checkLexicalDupes for each nested block. So a block nested D deep has its var subtree walked once per enclosing block: O(N x D) time for a source of size O(N), and the maps are self.arena-backed (out.put(self.arena, ...)) so each rebuild is retained for the rest of the parse.
Reachability
The if (seen.count() > 0) guard means the scan only runs for a block that actually has lexical declarations — but that is trivially satisfiable from attacker-controlled source, e.g. { let a; { let b; { let c; ... { var x; } ... } } }, where every level pays for the whole remaining subtree.
Why this is not #926
#926 was the private-name membership set, keyed per class. This is the var-name set, keyed per block, in a different function with no shared state — which is also why the #926 fix neither helps nor hurts it. Both are instances of #493's class.
Suggested order, mirroring #926
- Freeze growth workloads and controls in
bench/frontend_parse.zigbefore touching parser code — width and depth varied independently, plus a control with lexical declarations but no nested blocks, so "depth is expensive" is separated from "re-scanning is expensive". - Measure allocation requests, retained bytes and wall time per width under the bounded guard. Confirm the curve before proposing a fix — I have deliberately not claimed one here. Note from #926 that retained bytes are the cleaner algorithmic signal; wall time picks up a memory-system tax once the arena passes a few hundred MB.
- Only then consider computing the var set once per function-level scope and consulting it from nested blocks, preserving the exact Annex B.3.3 rigid/non-rigid distinction at src/parser.zig:1048-1049 and the function-boundary stop.
Non-goals
No depth limit, no weakening of the early error, and no published benchmark claim until dated evidence lands under docs/.data/.