## v0.5.905 — feat: closes #100 — compile-time-resolved dynamic `import()`. **Resolved subset (D1 from the spec).** String literals (`await import('./foo.ts')`), ternaries over resolvable args (`await import(flag ? './a.ts' : './b.ts')`), template literals with finite interpolation sets (`` await import(`./locale_${lang}.ts`) ``), const-propagated module-level locals, and `ExportAll` / `ReExport` / `NamespaceReExport` flattening through the import graph. Unresolvable paths surface a clear compile error from the HIR resolver (covered by unit tests in `crates/perry-hir/src/dynamic_import.rs`) rather than silently falling through — addresses the v0.5.103/v0.5.104 walker catch-all lesson called out in #100. **Implementation.** Four coordinated layers. (1) **HIR resolver** (`crates/perry-hir/src/dynamic_import.rs`): a `resolve_import_path` walker that const-folds `Expr::Lit(Str)`, `Expr::Binary(Add, ...)` template chains (Cartesian product over interpolation sets, capped at `DYNAMIC_IMPORT_PATH_CAP = 64` resolutions to keep blowup contained), `Expr::Ternary`, and `Expr::LocalGet` of module-level non-mutated consts. Every wrapper expression that can contain the path argument is covered by explicit pattern matching; the `_ => Resolution::Unresolved { reason }` arm carries a diagnostic message back to the compile-error path. New IR variants: `Expr::DynamicImport { paths: Vec<String>, arg: Box<Expr> }` (paths is the resolved set; arg is retained for multi-path dispatch) and `Module::init_kind: ModuleInitKind::{Eager, Deferred}`. Walker covers the new variant (`for_each_dynamic_import_mut`) so transform passes see it. Stable hash bumped to include `DynamicImport` and `ModuleInitKind` so incremental builds invalidate correctly. (2) **`flatten_exports`** (in `crates/perry-hir/src/dynamic_import.rs`, alongside the resolver): resolves the transitive closure of `export *` / `export { X } from "..."` / `export * as ns from "..."` through the import graph; cycle-safe via depth-first visited tracking with silent back-edge break; last-writer-wins on name collisions (Node semantics). Returns a `Vec<FlatExport>` with `NamespaceEntryKind::{LocalVar, LocalFunction, LocalClass, ForeignVar, NestedNamespace}` per entry — every cross-module value flavor the codegen needs to materialize. (3) **Codegen** (`crates/perry-codegen/src/codegen.rs` + `expr.rs`): per-module `@__perry_ns_<prefix>` global with external linkage emitted for every dynamic-import target module, populated at the tail of `__perry_init_<prefix>` (or `main` for the entry) via a new `emit_namespace_populator` that walks the module's `namespace_entries` and dispatches by kind — `LocalVar` reads the module-level slot; `LocalFunction` calls the closure-singleton getter; `LocalClass` INT32-tags the class id (0x7FFE-tagged so `typeof` returns `"function"`); `ForeignVar` calls the cross-module getter `perry_fn_<source_prefix>__<local>`; `NestedNamespace` calls `js_create_namespace` recursively. `Expr::DynamicImport` dispatch: single-path emits a direct load of the namespace global wrapped in `js_promise_resolved`; multi-path emits a `js_string_equals` chain over each compile-time path with a `js_promise_rejected("TypeError: unknown dynamic import path")` fallthrough. (4) **Runtime** (`crates/perry-runtime/src/object.rs`): `js_create_namespace(keys_ptr, values_ptr, count) -> f64` allocates a `JSObject`, populates fields via `js_object_set_field_by_name`, NaN-boxes with `POINTER_TAG` (`0x7FFD`) and returns. GC-root-registered via `js_gc_register_global_root` on the namespace global so the snapshot can't be collected while pending; handles empty modules (count=0) cleanly. **Namespace is a snapshot.** ESM spec says namespace properties are live bindings on the source module's `export let` slots; this PR materializes a snapshot at `__perry_init_<prefix>` end. For Perry workloads (mostly `export const`/`function`/`class`) this is indistinguishable. Live bindings can be added later via indirection in `js_create_namespace`'s setter slots without an ABI break. **TLA + cycles.** Top-level `await` in the target module: Perry's `__perry_init_<prefix>` blocks synchronously on the TLA expression (verified by `test_gap_dynamic_import_tla.ts`), so the populator at the tail of init reads post-TLA values — the snapshot captures the resolved state. Cycles: A statically imports B, B dynamically imports A — `collect_modules` registers the dynamic edge as a regular `Import` with `is_dynamic: true`, topo orders B before A (A depends on B), and `topo_visit` silently breaks the dynamic back-edge; re-entry into A's init does not occur (verified by `test_gap_dynamic_import_cycle.ts`). Init-time dynamic imports (top-level `await import(...)` at module init time): the dynamic edge is registered the same way, putting the target ahead of the consumer in the topological order (verified by `test_gap_dynamic_import_init_time.ts`). **Cache invalidation.** The cache key is fine without explicit `dynamic_import_path_to_prefix` / `namespace_entries` fields: M's HIR contains the resolved path strings via `Expr::DynamicImport`, hashed via `stable_hash::hash_module` (#686 fingerprint), and the target prefix flows through the existing `non_entry_module_prefixes` hash. T's exports flow through T's own HIR (which the populator reads); consumers never need to know T's namespace contents at compile time because property access on the namespace is dynamic via `js_object_get_field_by_name`. **Acceptance.** 7 new gap tests (`test_gap_dynamic_import_literal.ts`, `_ternary`, `_template`, `_reexport`, `_tla`, `_cycle`, `_init_time`) all byte-equal `node --experimental-strip-types`. Full workspace `cargo test` green; full CI (`lint`, `cargo-test`, `parity`, `compile-smoke`, `api-docs-drift`, `security-audit`, `harmonyos-smoke`, `doc-tests`) green. **Follow-up.** Tracked in #753 — startup-latency optimization: skip eager init for modules reached only via dynamic `import()`. `Module.init_kind` is already populated in this PR; #753 wires it through codegen (skip eager call for `Deferred` modules + dispatch-side `__init` call + idempotent guard). Pure-perf; behavior here is functionally correct (all modules eager-init, namespace population deferred). **Contributor.** Original implementation by @TheHypnoo; version bump + this changelog entry folded in at merge time.
0 commit comments