fix(dynamic-import): resolve function/closure-local const string specifiers (#1725) - #1727
Merged
Merged
Conversation
…ifiers (#1725) `import { logger } from 'hono/logger'` failed to compile because `hono/dist/utils/color.js` does: async function getColorEnabledAsync() { const cfWorkers = "cloudflare:workers"; ... try { return "NO_COLOR" in ((await import(cfWorkers)).env ?? {}); } catch { return false; } } `cfWorkers` is a function-local const string literal (captured into a nested IIFE), used as a dynamic `import()` specifier. The compile-time const-folder (`collect_module_const_locals`) only collected MODULE-level consts, on the now- wrong assumption that a dynamic import always evaluates in module-init scope — so the local const was "not a module-level const initialized to a literal" and the compile hard-errored. Fix: collect non-mutated `const x = <init>` bindings from the full scope set that `for_each_dynamic_import_mut` already walks (module init, every function / method / getter / setter / static-method body, field + global initializers), descending into closure bodies. At this HIR stage closures are still inline and capture by the original `LocalId`, and the import walker descends into closure bodies, so a const declared in any enclosing scope now resolves at the import site. `LocalId`s are module-unique, so a single flat id->init map is unambiguous. The mutation-invalidation scan was extended symmetrically (it now also descends into closures) so a reassigned binding is still dropped. `cfWorkers` resolves to `"cloudflare:workers"`, which has no module under Perry and so reject-at-runtime (a warning, not a hard error) — caught by the package's own `try/catch`, exactly matching Node's optional-dep idiom. Verified end-to-end: the #1725 acceptance program (`app.use('*', logger())`) now compiles and runs. This is a targeted slice of the dynamic-import series (#1674's variable-specifier axis); fully-dynamic / computed specifiers remain out of scope. Tests: resolve_closure_local_const_specifier, collect_consts_invalidates_closure_mutation (perry-hir). Existing resolve_unresolved_param_local / resolve_unresolvable_local still reject as before.
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.
Fixes #1725.
Problem
import { logger } from 'hono/logger'fails to compile under Perry-native becausehono/dist/utils/color.jscontains:cfWorkersis a function-localconststring literal (captured into a nested IIFE), used as a dynamicimport()specifier inside the classic optional-deptry/catch. Perry's compile-time const-folder (collect_module_const_locals) only collected module-level consts — on the now-incorrect assumption that a dynamic import always runs in module-init scope — so it rejected the local const with a hard error:Relationship to the dynamic-import series
This is a targeted slice of #1674 (the variable/non-literal specifier axis of the #1672/#1673/#1674 series). It is not the runtime-dispatch gap (#1672/#1673) — the specifier here is statically a string literal, just function-scoped. Fully-dynamic / computed specifiers remain #1674's scope.
Fix
Collect non-mutated
const x = <init>bindings from the full scope set thatfor_each_dynamic_import_mutalready walks — module init, every function / method / getter / setter / static-method body, and field + global initializers — descending into closure bodies. At this HIR stage closures are still inline and capture by the originalLocalId, and the import walker descends into closure bodies, so a const declared in any enclosing scope now resolves at the import site.LocalIds are module-unique, so a single flatid → initmap is unambiguous. The mutation-invalidation scan was extended symmetrically (it now also descends into closures) so a reassigned binding is still dropped.cfWorkersthen resolves to"cloudflare:workers", which has no module under Perry, so it rejects at runtime (a warning, not a compile error) — caught by the package's owntry/catch, exactly matching Node's optional-dep behavior.Validation
The #1725 acceptance program compiles and runs:
→ compiles (graceful
Could not resolve import 'cloudflare:workers'warning), runs,app.use('*', logger())works, routes register.Tests
resolve_closure_local_const_specifier— a const inside a closure body resolves as a specifier.collect_consts_invalidates_closure_mutation— a binding reassigned inside a closure is dropped (soundness).resolve_unresolved_param_local/resolve_unresolvable_localstill reject (no regression); fullperry-hirlib suite green.