fix(hir): resolve same-module enum referenced before its declaration (#4510) - #4522
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #4510. An
enumdeclared 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 to0and the compiler emitted aWarning: unknown identifier '<Enum>'. This was a silent miscompile — the binary linked but the enum value was wrong. Found in the wild inzod(ZodFirstPartyTypeKindis declared near the end oftypes.tsand 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 failedlookup_enum, fell through to theunknown identifier → GlobalGet(0) → 0path inlower_expr, and miscompiled. Functions are already hoisted via a pre-pass; enums were not.Fix
pre_register_module_enums(inlower/pre_scan.rs), a pre-scan that registers every module-level enum — bothexport enumand plainenum— before any function body is lowered, mirroring the existing function-hoisting pre-pass. It runs right afterpre_scan_mixin_functionsinlower_module_fn.rs.compute_enum_membersas a pure helper (noctx) so the pre-scan and the real declaration site compute identical names + values.lower_enum_declnow reuses the pre-registered enum id when one exists instead of minting a duplicate, soctx.enumshas 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.tscovers string / numeric / auto-increment forward enums, a top-levelconstinitialized from a later enum, switch-dispatch (the zod pattern), and a backward reference (regression guard):No more
Warning: unknown identifier. The issue's minimal repro now printsfwd: B(wasfwd: 0).cargo test --release -p perry-hir— all pass.test_enum.ts/test_edge_enums_const.tscompile and run clean (no regression).cargo fmt --all -- --checkclean.(Node's
--experimental-strip-typesrejectsenumas non-erasable syntax, so enum behavior is validated directly rather than byte-compared.)