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
feat(notifications): #98 background-receive on iOS + Android (warm-path) (v0.5.318)
iOS uses application:didReceiveRemoteNotification:fetchCompletionHandler: with
the OS completion block gated on the user's returned Promise — block is
Block_copy'd via RcBlock and stashed by handle until a Promise.then trampoline
(itself a real Perry closure with two i64 captures) fires the matching
UIBackgroundFetchResult.
Android routes through PerryFirebaseMessagingService.onMessageReceived to a
new nativeNotificationBackgroundReceive JNI; both foreground and background
callbacks fire because FCM doesn't split the two at the service layer. Cold
start (process not yet loaded) and an Android equivalent of
UIBackgroundFetchResult are #98 follow-ups.
No-op stubs added on macOS / tvOS / visionOS / watchOS / GTK4 / Windows so
cross-platform user code links cleanly.
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-1Lines changed: 2 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.317
11
+
**Current Version:** 0.5.318
12
12
13
13
## TypeScript Parity Status
14
14
@@ -149,6 +149,7 @@ 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.318** — Closes #98 (warm-path): `notificationOnBackgroundReceive(cb: (payload: object) => Promise<void>): void` for remote notifications delivered while the app is backgrounded (or terminated, on iOS). New TS export in `types/perry/system/index.d.ts` + dispatch row in `crates/perry-codegen/src/lower_call.rs::PERRY_SYSTEM_TABLE` routing to `perry_system_notification_on_background_receive`. **iOS** (`crates/perry-ui-ios/src/`): new `application:didReceiveRemoteNotification:fetchCompletionHandler:` delegate method (`app.rs`); `notifications.rs` adds `dispatch_remote_payload_with_completion` that JSON-decodes the userInfo NSDictionary, invokes the user callback, and gates the iOS completion block on the returned Promise. The block is `Block_copy`'d via `RcBlock::copy` so it outlives the delegate stack frame; a per-handle id stored in `PENDING_COMPLETIONS: HashMap<i64, RcBlock<dyn Fn(u64)>>` lets a Promise.then trampoline (`perry_ios_notification_completion_trampoline`) look the block back up when the user's Promise settles. Trampoline is itself a real Perry closure constructed via `perry_runtime::closure::js_closure_alloc` with two i64 captures (handle, result-code), so it slots into `perry_runtime::promise::js_promise_then`'s `ClosurePtr` API directly — no second runtime path needed. Three terminal cases: no callback registered → `.NoData` immediately; callback returned a Promise → `.NewData` on resolve, `.Failed` on reject; callback returned synchronously → `.NewData` immediately. **Android** (`crates/perry-ui-android/src/`): new `notification_on_background_receive` registers via `crate::callback::register` keyed off `NOTIFICATION_BACKGROUND_RECEIVE_KEY`; new `Java_com_perry_app_PerryBridge_nativeNotificationBackgroundReceive` JNI dispatches through the same JSON-payload shape as the foreground handler and pumps microtasks up to 8 times after the call so synchronously-attached `.then` chains fire before the FCM service returns. `PerryFirebaseMessagingService.kt::onMessageReceived` now calls *both* `nativeNotificationReceive` and `nativeNotificationBackgroundReceive` so users who register one or both handlers see the right shape; FCM doesn't natively split foreground vs background at the service layer (unlike iOS's two delegate methods), so dual-fire is the closest match. **Stub platforms** — macOS, tvOS, visionOS, watchOS, GTK4, Windows: no-op `perry_system_notification_on_background_receive` exports added so cross-platform user code compiles + links. **Out of scope (#98 follow-ups)**: (1) cold-start on Android (FCM waking a terminated app — `UnsatisfiedLinkError` branch in the FCM service still drops the message; needs `Application.onCreate` to load the native lib at process spawn). (2) iOS cold-start boot benchmark. (3) Doze-mode cooperation analysis on Android. (4) An equivalent of UIBackgroundFetchResult on Android (no native equivalent — FCM service runtime budget governs how long async chains have to complete after the JNI call returns). Compile-link verified across 6 non-Windows perry-ui-* crates; iOS builds cleanly for `aarch64-apple-ios-sim`; perry-ui-windows pre-existing cross-compile-from-macOS issue (text.rs:332 `windows` crate resolution) is unrelated. Snippet added to `docs/examples/system/snippets.ts` (`// ANCHOR: notification-background`) demonstrating the canonical pattern: persist payload via `preferencesSet` then optionally hit a server with `await fetch(...)`.
152
153
- **v0.5.317** — Closes #154: `using` / `await using` (ES2024 explicit resource management). Pre-fix, Perry parsed the binding form but lowered it as a plain `const` — `[Symbol.dispose]()` / `[Symbol.asyncDispose]()` hooks never ran, and the `Symbol.dispose` / `Symbol.asyncDispose` accessors weren't recognized as well-known symbols. Three pieces: (1) `symbol_well_known_key` (`crates/perry-hir/src/lower_decl.rs`) and the `Symbol.<name>` member-expression lowerer (`crates/perry-hir/src/lower.rs:9264`) extended to recognize `dispose` / `asyncDispose` — same `@@__perry_wk_<name>` SymbolFor pattern as the existing `iterator` / `toPrimitive` family. (2) `lower_class_method` renames computed-key methods `[Symbol.dispose]` → `__perry_dispose__` and `[Symbol.asyncDispose]` → `__perry_async_dispose__` — stable string-keyed names so the using-block desugarer can dispatch via plain method-call. (3) New `lower_stmts_using_aware` helper in `lower_decl.rs` rewires `lower_block_stmt` / `lower_block_stmt_scoped`: when a using-decl is found, lowers its bindings via the existing `lower_var_decl_with_destructuring` (so `Type::Named("Resource")` flows through and static class-method dispatch fires), then recursively lowers the remaining statements as a try-body wrapped in N nested `Stmt::Try { catch: None, finally: ... }` — one per binding, innermost-first — so disposal runs in reverse declaration order. Each finally checks `id !== null && id !== undefined` before calling the dispose method (per spec). For `await using`, the call is wrapped in `Expr::Await`. Class-method-captures-enclosing-fn-local has its own pre-existing codegen gap (filed as #212): the dispose method is silently dropped when `ctx.scope_depth > 0` AND the body references locals not in `own_locals` — preserves the pre-fix "test compiles, produces no disposed output" baseline for `test_gap_async_advanced.ts`'s class-in-async-fn pattern instead of newly breaking compile. Module-level classes (the canonical pattern) work end-to-end. New regression test `test-files/test_issue_154_using_dispose.ts` covers sync `using`, `await using`, multi-binding (`using a = e1, b = e2, c = e3` — rightmost disposes first), null-skip, byte-for-byte against `node --experimental-strip-types`. SuppressedError chaining (when body throws and a disposer also throws) is out of scope for v1 — Perry's existing `try { ... } finally { ... }` (no catch) doesn't currently re-propagate exceptions to outer catches anyway, so even spec-compliant disposer-error wrapping would behave the same.
153
154
- **v0.5.316** — Closes #167: silent SIGSEGV in tight `buf.readInt32BE(i*4)` / `buf.writeInt32BE(...)` loops past ~250k–300k iterations on macOS arm64 (8 MB stack). Two `alloca [N x double]` sites in `crates/perry-codegen/src/lower_call.rs` (the `js_native_call_method` dispatch at line 1755 + the class-dispatch fallback at line 1203) emitted into whatever basic block `ctx.block()` / `blk` currently represented. When the call site lived inside a loop, that's the loop body — and LLVM lowers a non-entry-block alloca as a runtime `sub %rsp, N` with no matching `add %rsp, N`, so every iteration permanently shrank the stack by 16 bytes (the args-array size, AArch64 16-byte-aligned). At ~250k–500k iterations the stack was exhausted → SIGSEGV with no error output (the crash happens after a flushed `console.log`, exit code propagates as 0 through shell pipes — easy to miss). Math from the issue's investigation: write loop (2-arg call) + read loop (1-arg call) × 16 bytes/iter × 250k each ≈ 8 MB → survives barely; 300k → SIGSEGV. Fix: new `LlFunction::alloca_entry_array(elem_ty, count)` helper in `crates/perry-codegen/src/function.rs` (4 LOC, mirrors the existing scalar `alloca_entry`); both call sites now hoist the args-array alloca to the function entry block where it's executed exactly once at function prologue. Verified end-to-end on the issue's repro: pre-fix N=300k crashed silently after "fill ok"; post-fix N=100k / 300k / 500k / 1M / 2M all complete cleanly with correct sums. New regression test `test-files/test_issue_167_loop_alloca_stack_eat.ts` runs N=500k (decisive pre-fix failure, <100 ms post-fix). No runtime change — pure codegen IR-emission fix that affects every dynamic method-call site routing through `js_native_call_method` (Buffer / Uint8Array numeric ops, Map / Set methods on plain object fields, any user-class method call where the receiver class id misses the static dispatch tower).
154
155
- **v0.5.315** — Closes #106 properly: `--target watchos-simulator --features watchos-game-loop` now links cleanly even when no native lib is configured. Pre-fix the runtime-only path failed with `Undefined symbols: _perry_register_native_classes / _perry_scene_will_connect` because `crates/perry-runtime/src/watchos_game_loop.rs` declared both as `extern "C"` imports and called them unconditionally from `main()` and the fallback `applicationDidFinishLaunching` — but no object in the runtime's own .a archive exported them, so the linker had nothing to resolve against. The original v0.5.114 commit (4b297092) shipped the contract assuming Bloom-style native libs would always be linked alongside; users who tried the issue's literal acceptance test with no native lib hit the wall and the issue stayed open. Fix: add weak no-op fallbacks via `core::arch::global_asm!` at the top of `watchos_game_loop.rs` — `.weak_definition _perry_register_native_classes` + `.weak_definition _perry_scene_will_connect`, both single-`ret` arm64 stubs (no params read since they take c_void). Mach-O resolution rule: weak symbol + strong symbol → strong wins, so any native lib's strong impls override these defaults at link time. With no native lib, the .app bundle now produces correctly (`/tmp/WatchOSGameLoopTest.app/WatchOSGameLoopTest` — 795 KB Mach-O arm64, exports `__perry_user_main` + the two weak stubs); add Bloom and the FFI hooks become live without touching anything else. Also, one-line UX cleanup at `crates/perry/src/commands/compile.rs:8602` — added `!is_watchos` to the strip-skip condition (the watchOS bundle code at line 8330 moves `exe_path` into the .app and removes the original, so the post-link `strip exe_path` was always emitting a noisy `can't open file` error after the success-path "Wrote watchOS app bundle" message). iOS has the same extern-without-fallback shape but isn't fixed here — separate scope; iOS users hitting the acceptance test always plumb in a native lib (Bloom/etc.), and a follow-up can mirror this pattern at `ios_game_loop.rs` if anyone reports the same UX gap there.
0 commit comments