Skip to content

Commit 7d6a247

Browse files
test: #697 — move stdlib_io FFI coverage into behavioral tests (#758)
* test: #697 — move stdlib_io FFI coverage into behavioral tests Moves the 155 entries from `test_ffi_surface_stdlib_io.ts` into `@covers` annotations attached to the existing behavioral fixtures for each surface: - fetch / Response / Request / Headers / Blob → test_gap_fetch_response.ts - node:stream/web → test_parity_stream_web.ts - node:http / node:https → test_parity_http.ts - node:net + TLS upgrade → test_net_socket.ts - node:readline → test_parity_readline.ts - node:worker_threads → test_parity_worker_threads.ts - WebSocket upgrade → test_node_http_ws_upgrade.ts - framework (multipart, request, response, server) → test_fastify_integration.ts Regenerated via `./test-coverage/regen_ts_surface_inventory.py`: `test_ffi_surface_stdlib_io.ts` shrinks from 155 unique FFI names to 0. `./test-coverage/audit.sh --markdown` still reports 100% TypeScript and combined FFI coverage (1790/1790). * chore: version bump v0.5.907 + CHANGELOG for #758 Maintainer-side bump folded in at merge time per CLAUDE.md external-PR workflow. Pure metadata + Cargo.lock workspace-version refresh; no behavioral change. --------- Co-authored-by: Ralph Küpper <ralph.kuepper@skelpo.com>
1 parent 19d485b commit 7d6a247

15 files changed

Lines changed: 273 additions & 252 deletions

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.907 — test(stdlib): #697 — move stdlib_io FFI coverage into behavioral tests (#758). Follow-up to #694: relocates the 155 inventory entries from `test_ffi_surface_stdlib_io.ts` into `@covers` annotations attached to the existing behavioral fixtures for each surface — `fetch.rs` (43 names) → `test_gap_fetch_response.ts`; `streams.rs` (37) → `test_parity_stream_web.ts`; `http.rs` (13) → `test_parity_http.ts`; `net/mod.rs` (10) → `test_net_socket.ts`; `readline.rs` (5) → `test_parity_readline.ts`; `worker_threads.rs` (6) → `test_parity_worker_threads.ts`; `ws.rs` (11) → `test_node_http_ws_upgrade.ts`; `framework/{multipart,request,response,server}.rs` (28) → `test_fastify_integration.ts`. Each receiving fixture already exercises real behavior on its surface (local loopback servers / deterministic in-memory probes / Headers + Blob + Response API roundtrips) — no external services added. **Mechanism.** `regen_ts_surface_inventory.py` excludes the six `test_ffi_surface_*.ts` inventory files when scanning for `@covers` references, so any FFI name that appears in a non-inventory fixture's `@covers` block is dropped from its inventory file on the next regen. After this PR, `test_ffi_surface_stdlib_io.ts` shrinks from **155 → 0** unique FFI names. **Audit.** `./test-coverage/audit.sh --markdown` keeps TypeScript and combined FFI coverage at **1791/1791 (100.0%)**. `test_ffi_surface_runtime_core.ts` re-regenerated against current `main` post-rebase — the inventory drift visible in the diff (added `js_array_flat_depth`, `js_template_raw`, `js_path_matches_glob`, `js_async_first_call`, the 7 new `webassembly.rs` exports; removed `js_mul`, `js_net_socket_destroy/end/write`, the 9 `js_ws_*` stubs from `stdlib_stubs.rs`) reflects upstream Rust refactors, not hand edits. Comment-only changes; no behavioral or runtime impact. Contributor: @TheHypnoo.
6+
57
## 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.
68

79
## 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.

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.906
11+
**Current Version:** 0.5.907
1212

1313

1414
## TypeScript Parity Status

0 commit comments

Comments
 (0)