Skip to content

Commit 9592cc6

Browse files
committed
chore: version bump v0.5.906 + CHANGELOG for #757
Folds in version bump and CHANGELOG entry per CLAUDE.md "maintainer amends at merge time" convention. Bumps Cargo.toml workspace package version 0.5.905 → 0.5.906, refreshes Cargo.lock, updates CLAUDE.md **Current Version**, and prepends the v0.5.906 block to CHANGELOG.md. Test-only PR; no runtime/codegen changes.
1 parent 0b08c9d commit 9592cc6

4 files changed

Lines changed: 72 additions & 70 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Detailed changelog for Perry. See CLAUDE.md for concise summaries.
44

5+
## v0.5.906 — test(stdlib): #696 — behavioral parity fixtures for stdlib core utilities (#757). Converts the stdlib-core FFI inventory entries called out in #696 into focused parity fixtures: 13 new `test_parity_*.ts` files (`lodash`, `dotenv`, `validator`, `uuid`, `nanoid`, `decimal`, `dayjs`, `moment`, `date_fns`, `lru_cache`, `commander`, `cron`, `async_local_storage`) plus `@covers` blocks appended to the existing `test_parity_events.ts` and `test_parity_zlib.ts`. Non-deterministic surfaces (uuid v4/v7, nanoid, dayjs.now / fromNow) use shape checks (length, alphabet, `validate()` round-trip) so output stays byte-stable; everything else uses fixed inputs and anchored UTC timestamps (e.g. dayjs anchored to `1709209096789` / 2024-02-29). `test_parity_async_local_storage.ts` is the one fixture that runs end-to-end in Node today (built-in `node:async_hooks`); the other 12 import npm packages not installed in the parity worker and will record as `NODE_FAIL`/`SKIP` until those land, acting as `@covers`-driven coverage anchors in the meantime. After `./test-coverage/regen_ts_surface_inventory.py`, `test_ffi_surface_stdlib_core.ts` drops from **313 → 84** unique FFI names; `./test-coverage/audit.sh --markdown` keeps TS-side coverage at **1791/1791 (100.0%)**. CI workflow tweak: `decimal`, `dotenv`, `lodash`, `moment`, `validator` added to the compile-smoke `SKIP_TESTS` list (no native stdlib path); the other 7 npm-package fixtures compile under perry-stdlib's native dispatch so they're left to the standard runner. `test_ffi_surface_runtime_core.ts` re-regenerated against current `main` (post-#756) — pure regen artifact, not a behavioral change. Contributor: @TheHypnoo.
6+
57
## 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.
68

79
## v0.5.904 — fix(perry-ext-fastify): #747 + #746 — `event_loop` and `wait_for_promise` now pump `js_run_stdlib_pump`, so perry-ext-{ws,net,http,fetch} events accumulated on tokio workers finally dispatch on the JS main thread when fastify is the dominant event loop. **Symptom.** Any program that imports `fastify` together with `ws` (or `net`, or node `http`, or outbound `fetch`) had its perry-ext-* listener callbacks stranded the moment `app.listen()` entered its loop — `wss.on('connection', cb)` would bind the port, accept handshakes, push `PendingWsEvent::Connection(...)` to the queue, and never fire the callback. The user's hub (`hub.perryts.com`, `PerryTS/hub`) reproduced this end-to-end against v0.5.700 through v0.5.892 and filed #746 against the WS surface; the underlying pump-bridge omission lives in `perry-ext-fastify` and was filed separately as #747 once the WS-only minimal repro was extracted. The same defect is what made #746's "WebSocketServer callbacks never fire" symptom durable in practice — when fastify is in the same program as ws, fastify's event_loop runs forever on the JS main thread and never calls back into the codegen-emitted main event loop where `js_run_stdlib_pump` is wired. **Root cause.** `crates/perry-ext-fastify/src/server.rs`'s `event_loop` (added in v0.5.572 as part of the perry-stdlib→perry-ext port: commit 97e80d3b) only drains microtasks via `js_promise_run_microtasks()`. The original perry-stdlib version it replaced (`crates/perry-stdlib/src/fastify/server.rs:270-289`, untouched) called `crate::common::js_stdlib_process_pending()` ahead of the microtask drain on every loop tick. That single missing call routes through perry-stdlib's `async_bridge::js_stdlib_process_pending`, which fans out to `js_ws_process_pending` / `js_net_process_pending` / `js_http_process_pending` / `js_node_http_server_process_pending` — the per-wrapper queues every perry-ext-* binding pushes into from its tokio workers. With the call missing, those queues grow unboundedly while the fastify event_loop spins on microtasks and the 10 ms `try_recv_with_timeout` request poll, and listener callbacks never run. The same omission also exists in `wait_for_promise` (the per-request promise-await helper) — minor by comparison, because the codegen-emitted `await` loop already pumps stdlib at `crates/perry-codegen/src/expr.rs:9500`, but it matters for the rare case where a route handler returns a Promise that's directly resolved by an external mechanism rather than via an inner `await` chain (and matches the parity baseline of perry-stdlib's wait_for_promise at `crates/perry-stdlib/src/fastify/server.rs:432`, which has had the pump call all along). **Fix.** Three targeted edits in `crates/perry-ext-fastify/src/server.rs`: (1) extend the local `extern "C"` block with `fn js_run_stdlib_pump();` — the runtime symbol is in `crates/perry-runtime/src/lib.rs:177` and dispatches through the `STDLIB_PUMP_FN` AtomicPtr that perry-stdlib registers at startup via `js_register_stdlib_pump`, so the call stays decoupled from perry-stdlib's internal feature set; (2) prepend the call to the fastify `event_loop` body, ahead of `js_promise_run_microtasks()` — same ordering perry-stdlib's `event_loop` uses; (3) prepend the call to `wait_for_promise`'s per-iteration body for parity with perry-stdlib's version. **Validation.** Combined fastify + ws test (`/tmp/test_746_with_fastify.ts`: `Fastify().get("/", ...).listen({port: 18905})` alongside `new WebSocketServer({port: 18906})`) before the fix: `[wss listening]` never printed, `[wss connection fired]` never printed — a `websocat -1 ws://127.0.0.1:18906/` exited without triggering either callback. After the fix: both fire on the next event-loop tick. Pure-WS minimal repro (no fastify, `/tmp/test_746_exact.ts` exactly matching the issue body) already passed on macOS at HEAD prior to this commit — `connection` fires under the codegen-emitted main event loop's existing pump call; the user's hub-context Linux symptom is fully covered by the fastify path fix, since their build imports both wrappers. Sanity: rate-limiter-flexible (v0.5.898's #665 fix) still passes the chain-on-fastify probe. Multi-await fastify handler (`/tmp/test_748_fastify_async.ts`: 3× `await Promise.resolve(N)` inside a `POST /multi` handler) returns the expected `{a:11,b:22,c:33,sum:66,stage:4}` payload with HTTP 201 in 4 ms. Multi-await on a perry-ext-bcrypt chain (`/tmp/test_748_bcrypt.ts`: 2× `bcrypt.hash` + `bcrypt.compare` inside a `POST /hash` handler) returns `{h1_len:60,h2_len:60,ok:1,hashes_differ:true}` with HTTP 201 in 4 ms. `cargo build --release -p perry-ext-fastify -p perry` clean. **Scope.** Closes #747 (the precise omission filed) and resolves the realistic-use-case half of #746 (programs that combine fastify with ws). The pure-WS-only Linux scenario from #746's minimal repro doesn't reproduce on macOS at HEAD — left as a Linux-only follow-up if it persists; the diagnostic path forward is to check whether the user's hub binary actually pulls in a fastify-like helper through any transitive import that engages perry-ext-fastify's event_loop. **Out of scope.** #748 — investigated in tandem and *not* closed by this commit. Initial hypothesis was that `wait_for_promise`'s missing pump caused @perryts/mysql's `await pool.exec(...)` chain to stall after the first INSERT; testing with a perry-ext-bcrypt chain proved otherwise (the codegen-emitted `await` loop pumps stdlib on every iteration at `crates/perry-codegen/src/expr.rs:9500`, so inner awaits never strand on stdlib resolution). The wait_for_promise pump call ships here as defense-in-depth + parity with perry-stdlib::fastify, not as a #748 fix. The actual #748 root cause needs reproduction with a real `@perryts/mysql` setup against MySQL and is left open for separate investigation. **Version-bump note.** Renumbered v0.5.899 → v0.5.904 to follow #743/#749/#750 in the merge queue (v0.5.900/v0.5.901+v0.5.902/v0.5.903 respectively).

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation.
1010

11-
**Current Version:** 0.5.905
11+
**Current Version:** 0.5.906
1212

1313

1414
## TypeScript Parity Status

0 commit comments

Comments
 (0)