Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

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.

**Current Version:** 0.5.654
**Current Version:** 0.5.655


## TypeScript Parity Status
Expand Down Expand Up @@ -153,6 +153,7 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re

One-liners only — full detail in CHANGELOG.md.

- **v0.5.655** — **Closes #562** (user classes can extend `WritableStream` / `ReadableStream` / `TransformStream`): canonical Web-Stream-subclassing pattern (`class ObjectUploader extends WritableStream<Uint8Array> { constructor(...) { super({ write, close, abort }); this.getResult = () => result; } }` from `@bradenmacdonald/s3-lite-client`) now compiles + runs end-to-end. Pre-fix, `super({...})` to a stream base class fell through to the SuperCall arm's "parent not in ctx.classes, not error_like" branch which lowered the args for side effects then returned undefined — the user's `write` / `close` / `abort` (or `start` / `pull` / `cancel` / `transform` / `flush`) callbacks were dropped, and a subclass instance passed to `pipeTo` / `pipeThrough` was a NaN-boxed object pointer that the existing stream FFIs (`js_writable_stream_*` / `js_readable_stream_*` / `js_transform_stream_*`) interpreted as a registry id, found nothing, and silently no-op'd. **Three coordinated fixes**: (1) `crates/perry-hir/src/lower_decl.rs` — add `ReadableStream` / `WritableStream` / `TransformStream` to both `native_parent` matches alongside the existing `EventEmitter` / `AsyncLocalStorage` / `WebSocketServer` arms, AND populate `extends_name` (parent name) alongside `native_extends` so the codegen SuperCall arm reaches the parent-name switch instead of the no-extends fallthrough. New `class_native_extends` registry on `LoweringContext` so `let x = new SubclassOfStream(...)` in `destructuring.rs` can route the local through the parent stream module's dispatch table. New `current_class_super_ident` field for the `super({...})` pre-scan in `expr_call.rs` to register `start`/`pull`/`transform`/`flush` callback's controller param as a `readable_stream` native instance — same shape the `new TransformStream({...})` pre-scan in `expr_new.rs` does. (2) `crates/perry-codegen/src/expr.rs::Expr::SuperCall` — when `parent_name` is one of the three stream classes (and not in `ctx.classes`), invoke a new `lower_stream_super_init(ctx, kind, super_args)` helper that walks `args[0]` via the existing `extract_options_fields` (handles both `Expr::Object` and `__AnonShape_*` forms), lowers each canonical callback (`start`/`pull`/`cancel` for readable, `write`/`close`/`abort` for writable, `transform`/`flush` for transform; missing → `TAG_UNDEFINED`), reads `highWaterMark` from `args[1]` if present, then dispatches to a runtime `js_X_stream_subclass_init(this, ...callbacks, hwm)` shim. Per JS spec, leaf-class field initializers run AFTER super() returns — the helper applies `FieldInitMode::SelfOnly` after the runtime call so subclass fields like `seenLengths = []` are initialized before any inherited callback fires. (3) `crates/perry-stdlib/src/streams.rs` — three new `*_subclass_init` shims reuse the existing `alloc_readable` / `alloc_writable` / `js_transform_stream_new` paths so the registry entries / GC roots / `TRANSFORM_PAIRS` row are identical to a plain `new ReadableStream(...)`; the resulting registry-id is then stashed on the subclass instance via `js_object_set_field_by_name(this, "__perry_stream_handle__", handle)`. Plus a fourth piece: `js_stream_unwrap_handle(value: f64) -> f64` — checks for a NaN-boxed object pointer (top16=0x7FFD), reads `__perry_stream_handle__`, returns its f64 numeric value; falls through to the input unchanged for already-numeric handles. Wrapped around every stream-FFI receiver and stream-handle argument in `crates/perry-codegen/src/lower_call.rs` (`module == "readable_stream"` recv + `pipeTo` dest + `pipeThrough` transform; `module == "writable_stream"` recv; `module == "transform_stream"` recv) so subclass instances and bare numeric handles are interchangeable at the FFI boundary. Plus two gates in `expr_member.rs` + `expr_call.rs` (matching helper `is_stream_api_member` / `is_stream_api_method` keyed on the same dispatch tables in `lower_call.rs`) so subclass-declared user fields (`uploader.uploadedSize`) and methods (`uploader.getResult()`) fall through to regular object property / class-method dispatch instead of hitting the streams `NativeMethodCall` arm and returning the receiver-less zero-sentinel. Stream-API methods/properties themselves still route through the streams arms so inherited `pipeTo` / `getWriter` / `cancel` etc. work on subclass instances. End-to-end smoke (`test-files/test_issue_562_stream_subclass.ts`) exercises all three subclass shapes (Readable / Writable / Transform); a scaled-up s3-lite-client style test (TransformChunkSizes feeding ObjectUploader, 3 chunks totaling 224 bytes) reports the right `uploadedSize` and `completed` via the user's `getResult()` method. The chained `readable.pipeThrough(t).pipeTo(w)` form requires the inner `pipeTo` to dispatch through `module == "readable_stream"` based on `pipeThrough`'s return type — a separate (pre-existing) limitation tracked outside this issue; the split form (`const piped = readable.pipeThrough(t); await piped.pipeTo(w)`) where the intermediate variable picks up the existing `destructuring.rs:1709` ReadableStream-from-pipeThrough recognition works fully. Sanity tests `test_inheritance`, `test_gap_class_advanced`, `test_gap_closures`, `test_edge_promises`, `test_gap_async_advanced`, `test_gap_fetch_response` continue to match Node byte-for-byte; gap suite holds at 26/28 with the same 2 pre-existing fails (`test_gap_console_methods`, `test_gap_object_methods`).
- **v0.5.654** — **Closes #561** (Web Crypto: `crypto.subtle.{digest,importKey,sign,verify}` for AWS SigV4 / JWT / web-push signing chains): pre-fix, every pure-JS S3 / signed-URL / JWT library (s3-lite-client, aws4fetch, jose, oidc-client-ts, web-push) hit the issue-#463 strict-API gate at `crypto.subtle` and refused to compile — `compilePackages` couldn't pull any of them in. New `crates/perry-stdlib/src/webcrypto.rs` ships the symmetric WebCrypto subset: `digest("SHA-1"|"SHA-256"|"SHA-384"|"SHA-512", data)` → `Promise<Uint8Array>`, `importKey("raw", keyBytes, {name:"HMAC", hash:{name:"SHA-256"}}, extractable, usages)` → `Promise<CryptoKey>`, `sign("HMAC", key, data)` → `Promise<Uint8Array>`, `verify("HMAC", key, sig, data)` → `Promise<boolean>`. `CryptoKey` is a Buffer marked Uint8Array with an entry in a process-global `CRYPTO_KEY_REGISTRY: Mutex<HashMap<usize, (algo, hash)>>` recording the `(KeyAlgo::Hmac, HashAlgo)` pair so `sign` / `verify` route to the right primitive. The async wrapping is decorative — SHA / HMAC are CPU-bound and resolve synchronously inside the returned Promise (issue's implementation note explicitly says this). `verify` uses constant-time byte comparison so it doesn't leak the position of the first mismatching byte through timing. `bytes_from_jsvalue` accepts strings (StringHeader / SHORT_STRING_TAG inline SSO), Buffer / Uint8Array (BufferHeader registered in BUFFER_REGISTRY), TypedArrayHeader (Uint8Array via the typed-array path), AND TextEncoder-allocated ArrayHeaders — the last one is the subtle case: `enc.encode("abc")` returns an ArrayHeader registered in BUFFER_REGISTRY but with bytes stored as f64 elements at offset 8 (so `instanceof Uint8Array` works while the decoder can recover UTF-8); without the side-table check (`text::is_text_encoder_result`), reading `enc.encode("abc")` as packed u8 yielded the first 3 bytes of each f64's IEEE-754 LE representation (all-zero for small ints) instead of the source bytes — first iteration of the impl produced `709e80c8...` for SHA-256("abc") instead of the spec value `ba7816bf...`. Made `text::is_text_encoder_result` `pub` to expose the registry to webcrypto.rs. New HIR variants `WebCryptoDigest / WebCryptoImportKey / WebCryptoSign / WebCryptoVerify` in `crates/perry-hir/src/ir.rs`; HIR lowering arm in `crates/perry-hir/src/lower/expr_call.rs` recognizes `crypto.subtle.<method>(args)` (3-deep Member chain `Member(Member(Ident("crypto"), "subtle"), "digest")`) BEFORE the generic `mod.X.Y()` arm, dispatches to the right HIR variant by method+arity, and emits a clear "out of scope" error for unsupported subtle methods (encrypt/decrypt/generateKey/wrapKey/unwrapKey/deriveKey — explicitly listed as out of scope in the issue). Codegen at `crates/perry-codegen/src/expr.rs` lowers each variant to the corresponding `js_webcrypto_*` extern that returns `*mut Promise` and NaN-boxes with POINTER_TAG. New runtime decls in `runtime_decls.rs`. New manifest entry `property("crypto", "subtle")` in `crates/perry-api-manifest/src/entries.rs`. Detection arm in `collect_modules.rs::compute_required_features` flips on the `crypto` Cargo feature so auto-optimize rebuilds perry-stdlib with the SHA / HMAC deps available — without it, link fails with `_js_webcrypto_digest` undefined. End-to-end smoke `test-files/test_issue_561_webcrypto.ts` exercises NIST SHA-256("abc") + AWS SigV4 first-step kDate + RFC 4231 HMAC-SHA-256 Test Case 2 + verify round-trip + tampered-sig rejection — all 6 lines match `node --experimental-strip-types` byte-for-byte. Acceptance smoke `test-files/test_issue_561_sigv4_chain.ts` runs the full `kSecret → kDate → kRegion → kService → kSigning` chain shape from `@bradenmacdonald/s3-lite-client::signing.ts:317-330` (the issue's named acceptance criterion); final hex matches Node byte-for-byte. Updated `crates/perry-hir/tests/unimplemented_api_check.rs` — replaced the pre-#561 "crypto.subtle is rejected" test with three new tests: `crypto_subtle_namespace_compiles`, `crypto_subtle_digest_compiles`, `crypto_subtle_encrypt_is_rejected` (covers the out-of-scope path). All 10 tests in the strict-API check suite pass; the 5 webcrypto unit tests (parse_hash_alg, AWS SigV4 vector, SHA-256 empty + abc vectors, constant_time_eq) pass. Out of scope per the issue: asymmetric (`RSA-PSS`, `ECDSA`, `RSA-OAEP`), `generateKey`, `wrapKey` / `unwrapKey`, `deriveKey`, `encrypt` / `decrypt`.
- **v0.5.653** — **Refs #486** (try/catch with unused catch binding around recursive async): `async function dispatch(i) { ... try { await dispatch(1); } catch (e) {} ... }` returned `undefined` and never even entered state 0. The async-to-generator pre-pass correctly recorded the catch param `e` (id=K) in its `compute_max_local_id` walk — its `Stmt::Try` arm scans `c.param`. The downstream generator state-machine pass (`crates/perry-transform/src/generator.rs`) ALSO has its own `compute_max_local_id` (it re-walks because the async pre-pass may have allocated new ids), but its `scan_stmt_for_max_local`'s `Stmt::Try` arm only walked `body` / `catch.body` / `finally` — it never read `catch.param`. When the catch body was empty (`catch (e) {}`), no `LocalGet`/`LocalSet` carried the catch param's id, so the body-only scan missed it entirely; the generator transform then allocated `__gen_state` over that same id. Every `LocalSet(__gen_state, N)` clobbered `e`, every read of `e` returned the state counter, and the state machine never advanced — the function appeared to return `undefined` immediately because `__async_step(undefined, false)` saw `done=true` on the very first `next()` call (state was already past every arm). Fix: scan `c.param` in `scan_stmt_for_max_local`'s `Stmt::Try` arm, mirroring the equivalent scan in `crates/perry-transform/src/async_to_generator.rs::scan_stmt`. Repro `try { await dispatch(1); } catch (e) {}` now prints `[d] 0 / [d] 1 / result: 0` matching Node. Hono's `compose()` wraps every middleware in this exact shape (`try { res = await handler(c, () => dispatch(i+1)); } catch (err) { ... }` — the `err` binding is unused on the success path), so this fix unblocks a class of hono failures distinct from the v0.5.649 RegExp / v0.5.643 string+string / v0.5.641 alias work.
- **v0.5.652** — **Refs #536** (`@perryts/mysql` connection-pool acquire returned the wrong branch on empty pool + event loop exited before `await sock.on('connect')` resolved when sockets were routed through perry-ext-net): three coordinated fixes. (1) `crates/perry-runtime/src/array.rs::js_array_pop_f64` and `js_array_shift_f64` returned `f64::NAN` (bare NaN bits) for empty arrays — but per ECMAScript §23.1.3.21 / §23.1.3.27, `Array.prototype.pop` / `shift` on an empty array return `undefined`, NOT NaN. Bare NaN compares `!== undefined` (different bit patterns), so connection-pool drivers like `@perryts/mysql` doing `const entry = this.idle.shift(); if (entry !== undefined) { ... } else { /* allocate new */ }` took the truthy branch on an empty pool, attempted to use a NaN-typed entry as a connection, and crashed. Fix returns `TAG_UNDEFINED_F64` (NaN-boxed undefined: `0x7FFC_0000_0000_0001`) which compares correctly against `undefined`. (2) `crates/perry-ext-net/src/lib.rs` — new `js_ext_net_has_active_handles()` exposing perry-ext-net's `pending_events` + `sockets` registries through the same FFI shape as `js_net_has_active_handles` from perry-stdlib's bundled-net. "Live" intentionally counts sockets still establishing TCP — counting only fully-open ones caused early exit before async `connect` ever completed (the open-flag flips inside the spawned task, AFTER `await TcpStream::connect`). (3) `crates/perry-stdlib/src/common/async_bridge.rs::js_stdlib_has_active_handles` — split the existing `feature = "net"` arm into two: `bundled-net` (perry-stdlib's own net impl is compiled in) keeps the direct `crate::net::js_net_has_active_handles()` call; new `external-net-pump` arm (the well-known flip routes `import 'net'` to perry-ext-net) declares `extern fn js_ext_net_has_active_handles()` and consults it. Pre-fix only the bundled gate fired, so programs using TS-source drivers like `@perryts/mysql` (which routes through perry-ext-net) saw `await new Promise(r => sock.on('connect', r))` exit early because perry-stdlib's empty `NET_SOCKETS` map reported no active handles. Plus a docs change: `docs/src/native-libraries/authoring-guide.md` swaps inline ts code blocks for `{{#include ../../examples/_fixtures/native-libraries/...}}` references so the snippets compile-test against real fixture files; matching `crates/perry-doc-tests/src/main.rs` skip-list update adds `_fixtures` (illustrative snippets that may legitimately import packages absent from this tree, drift-protected via PR diff rather than compile-testing).
Expand Down
Loading
Loading