Summary
A dynamic import() whose specifier is a runtime variable or computed value — import(path) where path is a function parameter or non-const local — is a hard compile error today. Because Perry is AOT and closed-world (the full module universe is known at collect_modules time), we can resolve a finite candidate set in many cases instead of erroring.
Real-world drivers are tooling-side: config-file loading (await import(resolvedConfigPath) — Vite, ESLint flat config, Jest, Drizzle Kit, tsx) and plugin/adapter-by-name from user config (await import(userConfig.adapter), import(`@scope/${name}`) — Vite/Rollup/PostCSS/unplugin). Lower priority than the literal bare-specifier / node:-builtin tier, but needed to run modern tooling natively.
Background
crates/perry-hir/src/dynamic_import.rs:560 (resolve_import_path_with_consts) currently resolves: string literals, ternaries, template literals over const interpolations, and module-level const locals. Anything else returns Resolution::Unresolved, which the driver turns into a compile error. The downstream multi-target dispatch (dyn_extern_i18n.rs) already consumes a Vec<String> and emits an N-way js_string_equals chain, so widening the resolver requires no new runtime or codegen for the type-narrowing path.
Proposed approach (two sub-parts, can ship independently)
A. Type/flow narrowing. Resolve a finite candidate set from static info already available:
- String-literal union types:
path: './a.ts' | './b.ts' → enumerate the union.
- Finite reaching-definitions: a non-
const local assigned only literals across the function → union those. (scan_mutations_expr, dynamic_import.rs:517, already tracks which locals are mutated; invert it to collect assigned values.)
B. Glob / context-module scoping (Vite import.meta.glob style). When a template literal has a non-const interpolation but fixed prefix/suffix, treat ${var} as a wildcard and glob <prefix>*<suffix> against the discovered module universe at collect_modules time. The existing DYNAMIC_IMPORT_PATH_CAP (64) bounds the candidate set.
Cross-cutting concerns
- Path canonicalization. Once candidates come from globbing or runtime variables, the runtime arg string must match a compile-time key exactly. Apply one normalization function to both the keys (build time) and the runtime arg (dispatch): extension resolution (
./foo → ./foo.ts), index resolution, ./ vs absolute. This is the most likely source of "compiled fine but silently rejects" bugs.
- Miss semantics. No-match → rejected promise (current fallthrough), which matches Node. Optionally offer a compile-time exhaustiveness mode for provably-covered union-typed args that skips the reject branch.
- Retention. Every candidate module must survive dead-stripping (cf.
#[used] / auto-optimize interaction). The is_dynamic edge handles graph inclusion; verify the namespace global isn't stripped when its only reference is a runtime string compare.
Acceptance criteria
let p = cond ? './a.ts' : './b.ts'; await import(p) (non-const local) resolves both targets — type/flow narrowing.
await import(`./plugins/${name}.ts`) with non-const name resolves to all matching files under ./plugins/ — glob.
- Unresolvable-and-unbounded specifiers still produce a clear compile error (no silent whole-program retention).
- Canonicalization covered by tests for
./foo vs ./foo.ts vs absolute.
Files
crates/perry-hir/src/dynamic_import.rs:560 (resolver)
crates/perry/src/commands/compile/collect_modules.rs:465-548 (glob cross-ref against module universe; cap enforcement)
Part of the dynamic-import Node.js-compatibility series (variable/computed-specifier tier — lower priority than the literal tier).
Summary
A dynamic
import()whose specifier is a runtime variable or computed value —import(path)wherepathis a function parameter or non-constlocal — is a hard compile error today. Because Perry is AOT and closed-world (the full module universe is known atcollect_modulestime), we can resolve a finite candidate set in many cases instead of erroring.Real-world drivers are tooling-side: config-file loading (
await import(resolvedConfigPath)— Vite, ESLint flat config, Jest, Drizzle Kit, tsx) and plugin/adapter-by-name from user config (await import(userConfig.adapter),import(`@scope/${name}`)— Vite/Rollup/PostCSS/unplugin). Lower priority than the literal bare-specifier /node:-builtin tier, but needed to run modern tooling natively.Background
crates/perry-hir/src/dynamic_import.rs:560(resolve_import_path_with_consts) currently resolves: string literals, ternaries, template literals over const interpolations, and module-levelconstlocals. Anything else returnsResolution::Unresolved, which the driver turns into a compile error. The downstream multi-target dispatch (dyn_extern_i18n.rs) already consumes aVec<String>and emits an N-wayjs_string_equalschain, so widening the resolver requires no new runtime or codegen for the type-narrowing path.Proposed approach (two sub-parts, can ship independently)
A. Type/flow narrowing. Resolve a finite candidate set from static info already available:
path: './a.ts' | './b.ts'→ enumerate the union.constlocal assigned only literals across the function → union those. (scan_mutations_expr, dynamic_import.rs:517, already tracks which locals are mutated; invert it to collect assigned values.)B. Glob / context-module scoping (Vite
import.meta.globstyle). When a template literal has a non-const interpolation but fixedprefix/suffix, treat${var}as a wildcard and glob<prefix>*<suffix>against the discovered module universe atcollect_modulestime. The existingDYNAMIC_IMPORT_PATH_CAP(64) bounds the candidate set.Cross-cutting concerns
./foo→./foo.ts), index resolution,./vs absolute. This is the most likely source of "compiled fine but silently rejects" bugs.#[used]/ auto-optimize interaction). Theis_dynamicedge handles graph inclusion; verify the namespace global isn't stripped when its only reference is a runtime string compare.Acceptance criteria
let p = cond ? './a.ts' : './b.ts'; await import(p)(non-const local) resolves both targets — type/flow narrowing.await import(`./plugins/${name}.ts`)with non-constnameresolves to all matching files under./plugins/— glob../foovs./foo.tsvs absolute.Files
crates/perry-hir/src/dynamic_import.rs:560(resolver)crates/perry/src/commands/compile/collect_modules.rs:465-548(glob cross-ref against module universe; cap enforcement)Part of the dynamic-import Node.js-compatibility series (variable/computed-specifier tier — lower priority than the literal tier).