Skip to content

fix(hir): resolve same-module enum referenced before its declaration (#4510) - #4522

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4510-enum-forward-ref
Jun 5, 2026
Merged

fix(hir): resolve same-module enum referenced before its declaration (#4510)#4522
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4510-enum-forward-ref

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #4510. An enum declared in a module but referenced before its textual declaration (valid TypeScript — enum bindings are module-scoped) was treated as an unknown global: member reads lowered to 0 and the compiler emitted a Warning: unknown identifier '<Enum>'. This was a silent miscompile — the binary linked but the enum value was wrong. Found in the wild in zod (ZodFirstPartyTypeKind is declared near the end of types.ts and referenced earlier throughout), corrupting its internal type-kind dispatch.

Root cause

Perry registered each enum in the lowering context (define_enum) only when its declaration statement was lowered. A function body lowered earlier in the same pass therefore failed lookup_enum, fell through to the unknown identifier → GlobalGet(0) → 0 path in lower_expr, and miscompiled. Functions are already hoisted via a pre-pass; enums were not.

Fix

  • Add pre_register_module_enums (in lower/pre_scan.rs), a pre-scan that registers every module-level enum — both export enum and plain enumbefore any function body is lowered, mirroring the existing function-hoisting pre-pass. It runs right after pre_scan_mixin_functions in lower_module_fn.rs.
  • Extract compute_enum_members as a pure helper (no ctx) so the pre-scan and the real declaration site compute identical names + values.
  • lower_enum_decl now reuses the pre-registered enum id when one exists instead of minting a duplicate, so ctx.enums has exactly one entry per enum and ids stay in textual declaration order. Already-working (enum-before-use) code is byte-identical.

Testing

New gap test test-files/test_gap_4510_enum_forward_ref.ts covers string / numeric / auto-increment forward enums, a top-level const initialized from a later enum, switch-dispatch (the zod pattern), and a backward reference (regression guard):

fwd: B        num: 2      auto: 2
before: green dispatch: isB back: off

No more Warning: unknown identifier. The issue's minimal repro now prints fwd: B (was fwd: 0).

  • cargo test --release -p perry-hir — all pass.
  • Existing test_enum.ts / test_edge_enums_const.ts compile and run clean (no regression).
  • cargo fmt --all -- --check clean.

(Node's --experimental-strip-types rejects enum as non-erasable syntax, so enum behavior is validated directly rather than byte-compared.)

…4510)

Enum bindings are module-scoped in TypeScript, so a function body (or any
earlier statement) may legally reference an enum declared later in the file.
Perry registered enums only when their declaration statement was lowered, so
a forward reference failed enum lookup, fell through to the unknown-identifier
path, printed a warning, and silently lowered member reads to 0 — corrupting
e.g. zod's ZodFirstPartyTypeKind dispatch.

Add pre_register_module_enums, a pre-scan that registers every module-level
enum (member names + values are computed purely) before any function body is
lowered, mirroring the existing function-hoisting pre-pass. lower_enum_decl
now reuses a pre-registered id instead of minting a duplicate. Enum ids stay
in textual order, so already-working code is byte-identical.
@proggeramlug
proggeramlug merged commit c6d6d52 into main Jun 5, 2026
13 of 14 checks passed
@proggeramlug
proggeramlug deleted the worktree-fix-4510-enum-forward-ref branch June 5, 2026 06:09
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.

frontend: same-module enum referenced before its declaration resolves to 0 (silent miscompile)

1 participant