Skip to content

Commit 120208a

Browse files
committed
fix: #187 follow-up — Redis (ioredis) end-to-end + table symbol fix (v0.5.270)
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.
1 parent 1ec4856 commit 120208a

5 files changed

Lines changed: 81 additions & 40 deletions

File tree

‎CLAUDE.md‎

Lines changed: 3 additions & 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.263
11+
**Current Version:** 0.5.271
1212

1313
## TypeScript Parity Status
1414

@@ -149,6 +149,8 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re
149149

150150
Keep entries to 1-2 lines max. Full details in CHANGELOG.md.
151151

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.
152154
- **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.
153155
- **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.
154156
- **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).

‎Cargo.lock‎

Lines changed: 27 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ opt-level = "s" # Optimize for size in stdlib
109109
opt-level = 3
110110

111111
[workspace.package]
112-
version = "0.5.263"
112+
version = "0.5.271"
113113
edition = "2021"
114114
license = "MIT"
115115
repository = "https://github.com/PerryTS/perry"

0 commit comments

Comments
 (0)