You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 120208a
Browse filesBrowse the repository at this point in the historyBrowse files
Three coordinated changes that complete the Redis path:
(1) crates/perry-hir/src/ir.rs:46 — added "ioredis" to NATIVE_MODULES.
The old workaround (class-name-only tracking in lower.rs:910 keeping
ioredis out of NATIVE_MODULES with a "// Note: ioredis NOT in
NATIVE_MODULES" comment) was needed when `import { Redis } from
'ioredis'` was expected to fall through to a JS interpreter. Perry's
native Rust ioredis impl is the canonical path now, and the JS
fallback no longer runs anything. Keeping ioredis out forced
requires_stdlib() to return false, which made
`Linking (runtime-only)` skip the stdlib archive — every direct
js_ioredis_* reference link-failed with `Undefined symbols`.
Listing it triggers the database-redis feature
(stdlib_features.rs:47) so the linker pulls in the right subset.
(2) crates/perry-codegen/src/lower_call.rs::lower_builtin_new — new
"Redis" branch calling js_ioredis_new(0). The runtime fn ignores
its config arg and reads connection settings from REDIS_HOST /
REDIS_PORT / REDIS_PASSWORD / REDIS_TLS env vars; connect is lazy
on first method call. Pre-fix new Redis() fell into the empty
js_object_alloc(0,0) placeholder and every chained method
dispatched against junk (same #187 shape as commander/EE/LRU/etc).
(3) NATIVE_MODULE_TABLE ioredis row symbol fix — pre-existing latent
bug. All 9 ioredis rows referenced runtime symbols js_redis_*
(without the `io` prefix) but the actual stdlib functions are
js_ioredis_*. Fixed createClient/set/get/del/exists/incr/decr/
expire/quit. The bug was dormant because pre-#187 no codepath
could reach a real Redis handle — `new Redis()` produced a junk
receiver, and method calls against it never made it to the link
stage's reachability analysis. With the v0.5.270 ctor making the
receiver real, the dispatch rows now actually link.
The createClient row aliases to js_ioredis_new (npm `redis`'s
createClient(opts) and ioredis's `new Redis(opts)` are shape-compatible:
both produce a client; opts is host/port/etc., which we read from env
vars regardless).
Verified end-to-end against a live Redis (redis-cli ping = PONG):
await r.set('k', 'hello-perry-' + Date.now());
await r.get('k'); // returns the value
await r.del('k');
await r.get('k'); // returns null
await r.quit();
Tests: stdlib 33/33, runtime 168/168, codegen 18/18, HIR 20/20.
Regressions: commander v0.5.250, EE/LRU/WSS v0.5.252, Decimal v0.5.259,
AsyncLocalStorage v0.5.261 all still green.
Out of scope (still): Big/BigNumber (no Rust stdlib), Pool/Client (pg),
MongoClient (mongodb), WebSocket (ws client) — these use async factory
functions returning Promise<T> (js_pg_connect, js_pg_create_pool,
js_mongodb_connect, js_ws_connect), not synchronous constructors;
new T() for those needs Promise-wrapped allocation that's a different
lowering pattern entirely.
Copy file name to clipboardExpand all lines: CLAUDE.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
8
8
9
9
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.
10
10
11
-
**Current Version:** 0.5.263
11
+
**Current Version:** 0.5.271
12
12
13
13
## TypeScript Parity Status
14
14
@@ -149,6 +149,8 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re
149
149
150
150
Keep entries to 1-2 lines max. Full details in CHANGELOG.md.
151
151
152
+
- **v0.5.271** — Bench refactor: add the two benchmarks that address the most-likely skeptic objections to this README within 30 seconds of reading it. (1) **`loop_data_dependent`** — sibling to `loop_overhead` with a multiplicative carry through `sum` and runtime-loaded array reads (kernel `sum = sum * x[i%64] + x[(i*7)%64]`, x in `[0.5, 1.0)` so the chain contracts to a bounded fixed point). LLVM cannot apply reassoc, IV-simplify, or vectorization — verified at the asm level (rustc 1.85 stable, `cargo rustc --release -- --emit=asm`, loop body LBB5_41 is a 4-instruction scalar fmul/fadd chain with two array loads, comment in `polyglot/bench.rs:122` records the exact dump). Result: Perry / Rust / C++ / Swift / Java / Node / Bun all land within 6 ms of each other (228-233 ms median); Go is the outlier at 132 ms. **The "Perry is 7× faster than C++" headline from `loop_overhead` does NOT replicate when the compiler can't fold** — that's the honest finding this bench was designed to surface. Added to `benchmarks/suite/17_loop_data_dependent.ts` plus `bench_loop_data_dependent` function in all 6 polyglot bench.X files. (2) **`bench_field_access` (JSON parse-and-iterate)** — companion to the existing roundtrip bench that FORCES Perry's lazy tape to materialize. Per iteration: parse → sum every record's nested.x (touches every element) → stringify. Added 6 implementations (`bench_field_access.{ts,go,rs,swift,cpp,kt}`). On this workload **Perry doesn't lead**: median 482 ms (gen-gc + lazy tape) — slower than its own mark-sweep escape-hatch mode at 384 ms (the lazy tape pays overhead it can't amortize when every element gets touched). Rust serde_json wins at 201 ms; Bun is competitive at 260 ms. **README restructure**: `loop_overhead` and the other flag-aggressiveness probes (`math_intensive`, `accumulate`, `array_read`, `array_write`) demoted to a new "Optimization probes (compiler flag-aggressiveness, not runtime perf)" subsection with explicit framing — they measure whether the compiler applied reassoc + IV-simplify + autovectorize, NOT runtime perf. The headline compute table now shows 4 cells: `fibonacci`, `loop_data_dependent`, `object_create`, `nested_loops`. JSON section renamed `JSON parse + stringify` → `JSON validate-and-roundtrip` (TL;DR §A) with new sibling `JSON parse-and-iterate` (TL;DR §B); both stay headline. Both run scripts updated; runner accepts the new benches with no special-case invocation. RUNS=11 fresh data across all benches.
153
+
- **v0.5.270** — #187 follow-up: `Redis` (ioredis) end-to-end + fixes a pre-existing dispatch-table-symbol-mismatch bug. Three coordinated changes: (1) **`crates/perry-hir/src/ir.rs:46`** — added `"ioredis"` to `NATIVE_MODULES` (replacing the prior `// Note: ioredis NOT in NATIVE_MODULES` comment with one explaining the change). The old workaround (class-name-only tracking in `lower.rs:910` keeping ioredis out of `NATIVE_MODULES`) was needed when `import { Redis } from 'ioredis'` was expected to fall through to a JS interpreter — but Perry's native Rust ioredis impl is the canonical path and the JS fallback path no longer runs anything. Keeping it out of NATIVE_MODULES forced `requires_stdlib("ioredis") → false`, which made `Linking (runtime-only)` skip the stdlib archive — every direct `js_ioredis_*` reference link-failed with `Undefined symbols`. Listing it triggers `database-redis` feature (`stdlib_features.rs:47`) so the linker pulls in the right stdlib subset. (2) **`crates/perry-codegen/src/lower_call.rs::lower_builtin_new`** — new `"Redis"` branch calling `js_ioredis_new(0)` (the runtime fn ignores its config arg and reads connection settings from `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD`/`REDIS_TLS` env vars; connect is lazy on first method call). Pre-fix `new Redis()` fell into the empty-placeholder branch and every chained method dispatched against junk. (3) **`NATIVE_MODULE_TABLE` ioredis row symbol fix** — pre-existing latent bug: all 9 ioredis dispatch rows referenced runtime symbols `js_redis_*` (without the `io` prefix) but the actual stdlib functions are `js_ioredis_*`. Fixed `createClient` / `set` / `get` / `del` / `exists` / `incr` / `decr` / `expire` / `quit` to point at the real symbols. The bug was dormant because pre-#187 no codepath could reach a real Redis handle — `new Redis()` produced a junk receiver, methods dispatched against it never actually linked the symbol because the link-time reachability analysis stripped them. With the v0.5.264 ctor making the receiver real, the dispatch rows now actually link. The `createClient` row aliases to `js_ioredis_new` (npm `redis`'s `createClient(opts)` and ioredis's `new Redis(opts)` are shape-compatible: both produce a client; opts is host/port/etc., which we read from env vars regardless). **Verified end-to-end against a live Redis (redis-cli ping = PONG):** `await r.set('k', 'hello-perry-' + Date.now())`, `await r.get('k')` returns the value, `await r.del('k')` then `await r.get('k')` returns `null`, `await r.quit()` clean. Tests: stdlib 33/33, runtime 168/168, codegen 18/18, HIR 20/20. Regressions: commander v0.5.250, EE/LRU/WSS v0.5.252, Decimal v0.5.259, AsyncLocalStorage v0.5.261 all still green. **Out of scope (still)**: `Big`/`BigNumber` (no Rust stdlib), `Pool`/`Client` (pg), `MongoClient` (mongodb), `WebSocket` (ws client) — these use async factory functions returning `Promise<T>` (`js_pg_connect`/`js_pg_create_pool`/`js_mongodb_connect`/`js_ws_connect`); `new T()` for those needs Promise-wrapped allocation that's a different lowering pattern entirely.
152
154
- **v0.5.263** — Stub-audit follow-up: (1) latent type-discrimination bug in `js_promise_all` / `js_promise_race` / `js_promise_any` — same shape as the Promise.allSettled fix landed at `32c997ca` (v0.5.260 era). All three were calling `js_nanbox_get_pointer(promise_f64).is_null()` to detect non-Promise inputs, but STRING_TAG and BIGINT_TAG NaN-boxed values produce non-null lower-48-bit pointers, so they were silently passed to `js_promise_then` as if they were Promises (UB; plain numbers happened to escape because their bit patterns made `js_nanbox_get_pointer` return 0 via the fallback). Fixed all three in `crates/perry-runtime/src/promise.rs:758/884/1156` by reordering the discrimination: check `js_value_is_promise(promise_f64) == 0` (which walks back to the GcHeader's `obj_type`) BEFORE extracting the pointer, mirroring the agent's allSettled fix exactly. Smoke-test with mixed Promise/string/number/bigint inputs: `Promise.all([Promise.resolve(1), "two", 3, Promise.resolve("four")])` returns `[1,"two",3,"four"]`; `Promise.race(["wins", Promise.resolve("loses")])` returns `"wins"`; `Promise.any(["fast", ...])` returns `"fast"`; bigint sibling `Promise.all([Promise.resolve(1n), 2n])` returns `[1n,2n]` — all match Node byte-for-byte. (2) **Re-validated every entry in `test-parity/known_failures.json`** against current behavior. 16 of 33 entries (48%) actually pass today on macOS host. After excluding `ci-env` (still fail on macOS-14 CI runner) and `flake` (intermittent) entries, **11 deterministic-pass entries removed**: `test_edge_class_advanced` / `test_edge_classes` / `test_edge_complex_patterns` / `test_edge_generics` / `test_edge_interfaces` / `test_gap_map_set_extended` / `test_gap_string_methods` / `test_getters_setters` / `test_method_inherit` / `test_perf_paths` / `test_super_calls`. Each verified passing twice in isolation (no flakiness). The `test_gap_map_set_extended` removal is consistent with v0.5.176's -0/+0 fix; the class/inheritance ones likely benefited from the v0.5.249 monomorph property-type-check + the v0.5.245-era cross-module getter/setter fix. Net: known_failures shrank from 33 → **22 entries**, all of which were independently verified to still fail (or be CI-env-only / flake). perry-runtime tests 168/168 unchanged.
153
155
- **v0.5.262** — Fix #95 (Android half): `notificationRegisterRemote` + `notificationOnReceive` actually wire to FCM on Android. Pre-fix the FFI symbols resolved at link time but the Rust shims were no-op stubs. Six pieces: (1) **gradle**: `id("com.google.gms.google-services") version "4.4.2"` plugin in root + app `build.gradle.kts`; `firebase-bom:33.5.1` + `firebase-messaging-ktx` deps. (2) **placeholder `app/google-services.json`**: dummy api_key + project_number so the gradle plugin's parse-and-validate at apply time succeeds — package_name matches the actual `applicationId = "com.skelpo.pickcolor"` (the plugin matches against applicationId not namespace; first attempt with `com.perry.app` failed `:app:processDebugGoogleServices`). User overlays this file with their real Firebase project's `google-services.json` to make FCM actually authenticate; the placeholder lets the build succeed but the SDK rejects it at runtime. (3) **`PerryFirebaseMessagingService.kt`** extends `FirebaseMessagingService`. `onNewToken` forwards the token string to native via `PerryBridge.nativeNotificationToken`; `onMessageReceived` serializes the `RemoteMessage` to JSON via `org.json.JSONObject` (data map + notification block fields: title/body/tag/color/icon/sound/clickAction/channelId, plus from/messageId/messageType/sentTime/ttl) and forwards via `PerryBridge.nativeNotificationReceive`. `UnsatisfiedLinkError` caught for the cold-start broadcast case (#98 territory). (4) **`AndroidManifest`**: `<service android:name=".PerryFirebaseMessagingService" android:exported="false">` with intent-filter for `com.google.firebase.MESSAGING_EVENT`. (5) **`PerryBridge.registerForRemoteNotifications(activity)`**: calls `FirebaseMessaging.getInstance().token.addOnSuccessListener { nativeNotificationToken(it) }`. The outer `try { … } catch (Throwable)` wraps the FCM init to handle the placeholder-google-services.json case — logs + continues without crashing. Two new externs: `nativeNotificationToken(token: String)` + `nativeNotificationReceive(payloadJson: String)`. (6) **Rust** (`crates/perry-ui-android/src/system.rs`): two new statics `NOTIFICATION_REMOTE_TOKEN_KEY` + `NOTIFICATION_RECEIVE_KEY`. `notification_register_remote(callback)` registers via the existing `crate::callback::register` global table, saves the key, and calls `PerryBridge.registerForRemoteNotifications` via JNI to kick off the initial token fetch. `notification_on_receive(callback)` just stores the callback. New JNI exports `Java_com_perry_app_PerryBridge_nativeNotificationToken` (token → NaN-boxed string → `invoke1`) and `_nativeNotificationReceive` (JSON string → `js_json_parse` to a Perry object → `invoke1`). The two `lib.rs` FFI shims that were `_unused` no-op stubs now route through. **Caveats** (in addition to the placeholder-json one): no `WAKE_LOCK` permission added — FCM SDK requests it transitively but heavy-message-volume apps may want the explicit declaration; no notification-icon metadata (`com.google.firebase.messaging.default_notification_icon` etc.) set so server-side notification-shape pushes use the system default; cold-start broadcast (process killed but FCM payload arrives) still drops messages — that's #98 territory and would need to refactor the JNI dispatcher to lazily init the Perry runtime before invoking the callback. Background delivery beyond what `FirebaseMessagingService.onMessageReceived` natively gives us isn't wired. Verified: `cargo check -p perry-ui-android` clean; `./gradlew :app:processDebugGoogleServices` → BUILD SUCCESSFUL (placeholder validates); `./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL with the firebase-messaging-ktx dep linked. End-to-end runtime validation needs the user's real Firebase project + `google-services.json` overlay + a real Android device + an APNs-equivalent push from the Firebase console.
154
156
- **v0.5.261** — #187 follow-up: `AsyncLocalStorage` (async_hooks) end-to-end. Same `new T()` placeholder bug as #187 / Decimal / EE/LRU/WSS — `Expr::New { class_name: "AsyncLocalStorage" }` fell into `lower_call.rs:1855`'s empty `js_object_alloc(0,0)` branch, and **on top of that** the 5 instance methods (`run`/`getStore`/`enterWith`/`exit`/`disable`) had **zero** rows in `NATIVE_MODULE_TABLE` so even with a real handle, every call dispatched against the unknown-method sentinel returning `0.0` / `TAG_UNDEFINED`. Pre-fix `als.run('x', () => console.log(als.getStore()))` printed nothing — the callback never fired and the store was never recorded. Two coordinated changes: (1) **`crates/perry-codegen/src/lower_call.rs`** — new `"AsyncLocalStorage"` branch in `lower_builtin_new` calling `js_async_local_storage_new()` and NaN-boxing as POINTER. 5 new `NATIVE_MODULE_TABLE` rows for `module: "async_hooks"`: `run` (NA_F64 store + NA_PTR closure → `_run`), `getStore` (no args → `_get_store`), `enterWith` (NA_F64 store → `_enter_with`, NR_VOID), `exit` (NA_PTR closure → `_exit`), `disable` (no args → `_disable`, NR_VOID). The runtime functions in `crates/perry-stdlib/src/async_local_storage.rs` were already complete + already declared in `runtime_decls.rs:1120-1125` from prior work — just had no callers. The `_run` and `_exit` runtime fns take the closure pointer as raw `i64` (NA_PTR coerces from NaN-boxed value) and call `js_closure_call0` internally with `nullptr` checks. (2) Verified end-to-end against Node 25.8 (`--experimental-strip-types`): all 10 lines of a comprehensive test (basic run + outside-run undefined + nested A→B + enterWith persistence + disable clears + exit-temporarily-clears semantic) match byte-for-byte. Tests: regression sweep clean — commander v0.5.250, EE/LRU v0.5.252, Decimal v0.5.259 all still green. **GC caveat**: stored `f64` JS values held in the per-handle `stores: Vec<f64>` are not yet anchored by a registered root scanner — during a `run(...)` invocation the store is also on the Rust call stack so conservative scanning catches it, but a long-lived `enterWith(obj)` followed by a malloc-triggered GC could in principle sweep the object. No reproducer hit yet; matching `events.rs`'s `for_each_handle_of` scanner pattern would close this — deferred as polish. **Out of scope (still)**: `Big`/`BigNumber` (no Rust stdlib), `Redis` (excluded from `NATIVE_MODULES`), `Pool`/`Client`/`MongoClient`/`WebSocket(client)` (need Promise-wrapped allocation pattern).
0 commit comments