ReviewOS

zig-utils/zig-js

Parser: nested vars ignore module top-level lexical declarations

#949
Closed chris opened this 10 days ago ·
10 days ago

Parent: #904 Follow-up to: #930

Invalid modules accepted

Node v24.18.0 rejects each module with SyntaxError; current zig-js (f2bafeb3) parses the first one successfully in the standalone parser test root, proving the module early-error path accepts it.

moduleNodezig-js
let x; { var x; }SyntaxErroraccepts
export let x; { var x; }SyntaxErroraccepts
function f(){} { var f; }SyntaxErroraccepts
import {x} from "m"; { var x; }SyntaxErroraccepts
export default function f(){} { var f; }SyntaxErroraccepts

Node accepts the function-boundary controls let x; function f(){ var x; }, export let x; export function f(){ var x; }, and import {x} from "m"; const f = function(){ var x; };.

Cause

checkModuleEarlyErrors collects the ModuleBody's top-level lexical names, but creates an empty LexicalScope before calling recurseScope over nested statements. A var inside a nested block therefore cannot see an import, lexical declaration, module-scoped function, or named default export that remains open in the module's var scope.

Script parsing does not have this gap: checkLexicalDupes opens the ScriptBody's lexical names before the same walk.

This was listed as an unexecuted reading in #930 because test262 --eval has no module mode. It is now executed against Node's vm.SourceTextModule and zig-js's parser directly.

Definition of done

  • Open the already-collected module lexical names once in the shared scope before walking ModuleItems; do not re-collect subtrees.
  • Preserve the fresh var-scope boundary for functions.
  • Pin the invalid and valid cases above.
  • Compare affected test262 module and declaration subtrees before/after with nonzero denominators and zero regressions.
10 days ago

Fixed on main by 1c0bb44.

The module early-error pass now lends its existing top-level lexical-name inventory to the shared scope walk. Nested var declarations therefore see module let/const, import, function, and named default-export bindings, while function boundaries remain isolated. Reusing the existing map avoids a second hash-table population for import-heavy modules.

Evidence:

  • Node 24.18.0 rejects all five pinned conflicts and accepts the three function-boundary controls.
  • Standalone parser root: 80/80 tests pass.
  • Exact-parent test262 comparison across nine affected subtrees: 1,805 → 1,805 pass, 0 fail; every diagnostic transcript is byte-identical (0 flips).
  • Frozen representative_frontend_modules_{1024,2048,4096} workloads, 18 samples per binary and width: allocation counts and allocated bytes are identical to the parent at every width. Median elapsed time ranged from -0.3% to -2.7%, within noise.

Sign in to comment on this issue.