Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ jobs:
test_issue_610_foreach \
test_issue_610_smoke \
test_issue_640_navstack_textfield \
test_issue_763_reactive_textfield \
test_issue_764_state_at_module_init \
test_parity_assert \
test_parity_async_hooks \
test_parity_buffer \
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Detailed changelog for Perry. See CLAUDE.md for concise summaries.

## v0.5.910 — fix(transform): #764 — `State()` at module-init level no longer breaks `stateOnChange` / `stateBindTextfield` / `stateBindToggle`. **Symptom (reported as #763).** `const cell = State(""); stateOnChange(cell, cb); cell.set("v")` at module top level (NOT inside `function main()`) updated `cell.value` correctly but never fired `cb`. The same shape inside a function worked. The HIR-level `try_desugar_reactive_text` pass (which lowers `Text(\`...${state.value}...\`)` into an IIFE that registers `stateOnChange`) inherits the same failure when its host module declares the state at module-init level, because the desugar emits exactly the kind of `stateOnChange(cell, ...)` call that gets stranded. **Root cause.** `crates/perry-transform/src/state_desugar.rs::collect_state_bindings` (issue #535, extended in #612 to also catch capital-`S` `State`) was rewriting *every* module-init-level `State(initial)` / `state(initial)` declaration into the synthetic keyed runtime: `let cell = undefined` + `__state_init("__state_N", initial)`, with downstream `cell.set(v)` → `__state_set("__state_N", v)` and `cell.value` → `__state_get("__state_N")`. The two-state-systems-in-one-namespace problem: the synthetic `__state_*` runtime is registry/key-keyed (one HashMap keyed by string id), the public handle-based API (`perry_ui_state_create` → integer 1-based handle → `perry_ui_state_set` → `perry_ui_state_on_change`) is *separate*. After the rewrite, `stateOnChange(cell, cb)` saw `cell = undefined` (the `Let { init: Undefined }` placeholder the desugar leaves behind to keep the LocalId addressable), `perry_ui_state_on_change(handle: i64, callback: f64)` extracted garbage as its handle, and the subsequent `__state_set("__state_N", v)` wrote to a completely different store — so the handle-based subscriber registry stayed empty and the callback never fired. The reactive-Text desugar's `textSetString(__h, fresh_concat)` thus only ran when something else triggered it (`stateBindTextfield` two-way binding wakes the textfield up at type-time, but nothing wakes up the standalone `Text`). **Fix.** Gate `state_desugar` so it only rewrites a binding when *no* handle-based state API consumes it. After `collect_state_bindings`, a new `collect_handle_based_state_uses(module)` walks all of `module.init`, every function body, and every class method/ctor body looking for `NativeMethodCall { module: "perry/ui", method: "stateOnChange" | "stateBindTextfield" | "stateBindToggle" | "stateBindSlider" | "stateBindVisibility" | "stateBindTextNumeric", args[0]: LocalGet(id) }`. Any `id` it finds is removed from the rewrite map — the binding stays as the original `State(...)` constructor call, the handle-based runtime owns it end-to-end, and `stateOnChange` / friends register against the real integer handle. The `set`/`value`/`get`/`text` / `NavStack(state, routes)` / `ForEach(state, render)` rewrites are unaffected (they already had complete keyed-side implementations) and lowercase `state(...)` still works exactly as #535 designed when the user goes purely keyed-API. **Why not just remove the uppercase-`State` branch from `collect_state_bindings`?** #612's NavStack(state, routes) flow specifically needs the synth_id machinery for the `__navstack_register_route` call, and `state` and `State` are both documented as the constructor for that flow. Pulling uppercase out would re-break #612. The gate restores the v0.5.112+ handle-based contract for `State` without disturbing #535 / #612. **Validation.** (1) Four new unit tests in `crates/perry-transform/src/state_desugar.rs::tests` lock in the gate: module-init uppercase + `stateOnChange` is skipped; module-init lowercase + `stateBindTextfield` is skipped; module-init State with *only* `.set` is still rewritten (NavStack / ForEach path unaffected); a handle use inside a function body still gates the module-init binding (matches the #763 shape where `State()` is at module-init and `stateOnChange` is inside `main()`). (2) New integration fixture `test-files/test_issue_763_reactive_textfield.ts` exercises the original repro under AppleScript automation on macOS — typing into the TextField and clicking the "set hello world" button both reactively update the standalone `Text(\`current state for text: ${text.value}\`)` widget (verified via System Events' static-text enumeration). (3) NavStack regression: `test-files/test_issue_640_navstack_textfield.ts` (the original #612 repro) still routes from A→B correctly. `cargo test --release -p perry-transform` green (23/0/0); workspace test suite (excluding cross-host UI crates per CLAUDE.md) green. **Issue #764 context.** Originally filed to track auto-binding template literals over state cells to `perry_ui_state_bind_text_template` — but that auto-bind has actually shipped since v0.5.112 (HIR `try_desugar_reactive_text`); it emits an IIFE + `stateOnChange` + `textSetString` rather than calling the template-binding FFI directly, which works correctly when state is in function scope. The user-reported repro in #763 was actually exposing this separate module-init regression; this commit fixes that path. The "use `perry_ui_state_bind_text_template` directly" optimization is left as a future architectural improvement (it would centralize re-render in the runtime instead of one closure-per-state on the JS side).

## v0.5.909 — feat: add `libDirs` to native library manifest (#762). New optional `targets.<target>.libDirs` field in `package.json`'s `perry.nativeLibrary` block — array of linker search paths emitted ahead of the `libs` list. **Surface.** Documented in `docs/src/native-libraries/manifest-v1.md` alongside the existing `libs` / `frameworks` / `pkgConfig` entries. Use it when a wrapper ships a vendored `.a`/`.dylib` outside the cargo crate (the standard `lib_name` lookup probes `target/<triple>/release/`); previously the only escape hatch was an absolute path glued into `libs` via a workaround. **Plumbing.** Three edits — (1) `TargetNativeConfig.lib_dirs: Vec<PathBuf>` field added in `crates/perry/src/commands/compile.rs`; (2) `parse_native_library_manifest` parses `libDirs` and anchors relative entries to `package_dir` via `package_dir.join(p)` — mirrors the existing `swift_sources` / `metal_sources` pattern so a `"./vendor/lib"` in the manifest resolves against the wrapper's directory, not the user's cwd (absolute entries pass through unchanged since `PathBuf::join` ignores the base on an absolute right-hand side); (3) `build_and_run_link` in `crates/perry/src/commands/compile/link.rs` emits each entry between the `frameworks` loop and the `libs` loop — `-L<dir>` on every non-Windows target, `/LIBPATH:<dir>` when `is_windows` (mirrors the `libs` loop's existing `{lib}.lib` vs `-l{lib}` MSVC/Unix split, so a `targets.windows.libDirs` entry actually resolves the `{lib}.lib` lookups instead of being a silent no-op against link.exe). **Validation.** Two new unit tests in `crates/perry/src/commands/compile/resolve.rs::manifest_parse_tests` — `lib_dirs_relative_paths_anchored_to_package_dir` round-trips a manifest with both `"vendor/lib"` and `"/abs/path"` and asserts the package_dir-anchored / pass-through behavior; `lib_dirs_defaults_to_empty_when_absent` confirms the field is optional. `cargo build --release` clean; `cargo test -p perry --lib` green. **Version-bump note.** Renumbered v0.5.908 → v0.5.909 because #760 (test: argon2 + ethers parity) landed on main mid-review and took the v0.5.908 slot. **Contributor.** Implementation by @Lebei2046; MSVC `/LIBPATH:` branch + `package_dir` anchoring + tests + manifest doc entry + this changelog folded in at merge time.

## v0.5.908 — test: #698 — behavioral parity tests for argon2 + ethers integrations (#760). Follow-up to #694, addressing #698. Adds two new parity fixtures and a `@covers` block on `test_parity_crypto.ts`, moving 18 FFI entries out of `test_ffi_surface_stdlib_integrations.ts` (156 → 138 unique names) into behavioral coverage. **`test_parity_argon2.ts`** — round-trips `argon2.hash` / `argon2.verify` against the `perry-ext-argon2` wrapper; non-deterministic salt is handled by shape-checking the `$argon2id$` prefix and asserting verify round-trips against a freshly produced hash. Only the async path is in `NativeModSig`, so this covers the two FFI entries (`js_argon2_hash`, `js_argon2_verify`) reachable end-to-end. **`test_parity_ethers.ts`** — pure deterministic helpers from `perry-ext-ethers`: `getAddress` (EIP-55 checksum on lower- and upper-case input), `parseEther`/`formatEther` round-trip on 1.5 ETH, `parseUnits`/`formatUnits` at 6- (USDC) and 9-decimal (gwei) positions. Covers the five `js_ethers_*` helpers. Both new fixtures use the `test-parity/expected/` mechanism — Perry routes the npm-style imports to the bundled `perry-ext-*` wrappers, but Node can't resolve the same names without `node_modules`, so they fall through to stored expected-output comparison. **`test_parity_crypto.ts`** — adds a `@covers` block declaring the crypto/webcrypto/crypto_e2e FFI surface it already exercises via `node:crypto` and `crypto.subtle` (digest, hash, pbkdf2/hkdf, subtle.sign/verify); no behavioral change. **Out of scope (each gets its own follow-up).** `jsonwebtoken.sign` — `NativeModSig` passes `payload`/`secret` as `NA_F64` (NaN-boxed) but `js_jwt_sign` reads them as `*const StringHeader`, producing garbage tokens (calling-convention mismatch, same shape as the #591 argon2 fix). `bcrypt.hash` — return value reports `typeof === "object"` in user code; `compare` round-trips correctly but `.startsWith(...)` on the hash fails. `cheerio` — `$.select(...)` returns a bare-number handle, so `.length()` / `.first()` etc. fail with `(number).method is not a function` at the prototype check. Service-backed integrations (MongoDB, MySQL, PG, ioredis, Nodemailer, Sharp) also remain in the inventory pending the local-container / opt-in test setup described in #698. **Audit.** `./test-coverage/audit.sh --markdown` keeps TS-side and combined FFI coverage at **1791/1791 (100.0%)**. `test_ffi_surface_stdlib_integrations.ts` re-regenerated via `regen_ts_surface_inventory.py` to fold in the new `@covers` blocks. Comment-only changes; no behavioral or runtime impact. Contributor: @TheHypnoo.
Expand Down
2 changes: 1 addition & 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.909
**Current Version:** 0.5.910


## TypeScript Parity Status
Expand Down
Loading
Loading