Skip to content

Commit f6e5fda

Browse files
committed
chore(version): v0.5.325 — close #219 doc-tests banner fix
Records the v0.5.325 slot for the 6d1b678 fix that already landed on main (empty `// platforms:` opts out of host run phase, fixing wasm_snippets.ts's link failure on macOS/GTK4/Windows doc-tests gates). That commit didn't bump the workspace version or add a CLAUDE.md Recent Changes entry — this one folds in the metadata so the slot is attributable and the changelog stays continuous.
1 parent 2ef8151 commit f6e5fda

3 files changed

Lines changed: 30 additions & 29 deletions

File tree

CLAUDE.md

Lines changed: 2 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.324
11+
**Current Version:** 0.5.325
1212

1313
## TypeScript Parity Status
1414

@@ -149,6 +149,7 @@ 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.325** — Closes #219: `docs/examples/platforms/wasm_snippets.ts` was failing every host doc-tests gate (macOS, GTK4, Windows) because its `// platforms: macos, linux, windows` banner enabled the host run phase, but the file's `declare function bloom_init_window/bloom_draw_rect` FFIs only resolve as WASM imports under `--target wasm` / `--target web` — on a native compile the host linker can't find them and bombs with `undefined reference to bloom_init_window`. Two-part fix: (1) `crates/perry-doc-tests/src/main.rs::read_banner` now tracks a `platforms_seen` flag — an explicit empty `// platforms:` directive (no values) suppresses the default-fill of `[macos, linux, windows]`, letting an example opt out of the host run phase entirely. Pre-fix the parser couldn't distinguish "directive absent" from "directive present but empty". (2) `docs/examples/platforms/wasm_snippets.ts` banner switched from `// platforms: macos, linux, windows` to `// platforms:` + `// targets: wasm, web` — the cross-compile phase still drives both targets (catching API drift in `declare function`, `fetch()`'s options shape, and `parallelMap`), but the host phase skips. Verified end-to-end: `./target/release/doc-tests --filter wasm_snippets --verbose` now reports `SKIP platform 'macos' not listed in banner` + `XCOMPILE_PASS target='wasm'` (504 ms) + `XCOMPILE_PASS target='web'` (101 ms); `strings target/perry-doc-tests/platforms_wasm_snippets__wasm.wasm` confirms `bloom_init_window` and `bloom_draw_rect` are present as WASM imports (per the issue's spot-check criterion). Full sweep `./target/release/doc-tests --skip-xcompile` now 78/80 pass with the lone fail being the pre-existing `ui/gallery.ts` retina screenshot baseline mismatch — unchanged from prior baselines, no regression from the parser change.
152153
- **v0.5.324** — Issue #185 follow-up: `box-shadow` on macOS (sections 4 of `docs/examples/ui/styling/visual_test.ts`) was invisible despite the FFI args being correct in the IR dump. Root cause: `crates/perry-ui-macos/src/widgets/mod.rs::set_shadow` set `shadowColor` / `shadowOpacity` / `shadowRadius` / `shadowOffset` on the view's CALayer but never set `setMasksToBounds: false` — and CALayer shadows are clipped by `masksToBounds`. The iOS / tvOS / visionOS mirror impls all set this; the macOS one was missing it. One-line fix: `let _: () = objc2::msg_send![layer, setMasksToBounds: false];` after the four shadow setters. Also adds (1) `docs/examples/ui/styling/visual_test.ts` — single-window comprehensive 13-section visual styling test exercising every styling prop family (colors hex/named/object/alpha/runtime, borders, padding, **shadow** (the case that surfaced the bug), gradient, opacity, typography, buttons, inputs, controls, image symbols, hidden/opacity-0 states, runtime-resolved colors via Phase C step 7); (2) `docs/examples/ui/styling/visual_test.spec.md` — LLM-debuggable per-cell expected-values manifest with 13 numbered sections matching the .ts and a "Visible signature" string per cell so a screenshot can be compared cell-by-cell against the spec; (3) `scripts/run_visual_test_check.sh` — drift check that asserts the .ts's `labeled("N. ...", ...)` calls and the spec's `### N.` headers stay 1:1 in lockstep, fails CI when either file adds/removes a row without updating the other; (4) wired into `.github/workflows/test.yml` after the existing UI styling matrix step. Verified end-to-end via 3 rounds of `shadow_repro.ts` (high-contrast yellow shadow against gray cells) — round 1 was inconclusive low-contrast, round 2 confirmed shadows missing, round 3 (post-fix) confirmed shadows render correctly. Cross-platform compile sanity check: web/wasm + iOS-simulator both produce valid binaries from the same `visual_test.ts` source; tvOS/watchOS sim builds need nightly Rust + `-Zbuild-std` (tier-3 targets), deferred. iOS simulator launch crash observed by user — investigation deferred so Windows/Linux validation can run; the macOS fix here is independently shippable and only touches the macOS UI crate.
153154
- **v0.5.323** — Closes #212: class declared inside a function whose method body references an enclosing-fn local now compiles + runs correctly (pre-fix: `Error compiling module ... lowering body of method 'C::log': ArrayPush(0): local not in scope`). The codegen emitted each class method as a stand-alone Function with no capture wiring, so `class C { log(s) { captured.push(s); } }` inside `function test() { const captured = []; ... }` failed at the LocalGet site for `captured`. Fix is a HIR-level rewrite in `lower_class_decl` (`crates/perry-hir/src/lower_decl.rs`): (1) walk every instance member (methods, getters, setters, constructor) for outer-scope LocalIds via the new `collect_method_captures` helper — same `collect_local_refs_stmt` walk the existing closure capture analysis uses, filtered against a snapshot of `ctx.locals` taken at class-decl-end so inner-closure params (which `collect_local_refs_*` descends through indiscriminately) don't get falsely captured. (2) Add a hidden `__perry_cap_<outer_id>` instance field per captured outer id; field name is keyed off the OUTER id so every method/ctor agrees on which field reads which capture, independent of per-method fresh ids. (3) For each method/getter/setter, allocate a FRESH method-local `LocalId` per captured outer id, run a global LocalId remap on the body (the new `analysis::remap_local_ids_in_stmts_with_field_propagation`, which mirrors `perry_transform::inline::substitute_locals`'s variant coverage so specialized HIR shapes like `Expr::ArrayJoin { array }`, `Expr::ArrayMap`, etc. don't silently fall through and skip the rewrite — the soft-fallback `double_literal(0.0)` for unrecognized LocalIds was producing array handle 0 at runtime), then prepend `Stmt::Let { id: fresh_id, init: PropertyGet(This, "__perry_cap_<outer_id>") }`. The body's existing `LocalGet(outer_id)` now resolves to a method-local slot at codegen. PER-METHOD fresh ids are essential — a `Stmt::Let { id: outer_id }` in a method that has a closure mutating the captured value would mark `outer_id` as boxed *globally* via the module-wide `module_boxed_vars` union, which then makes the outer fn's plain (non-boxed) read of `outer_id` segfault on a `js_box_get` of a non-box pointer. The rebind let preserves the outer's `Type` (looked up in `ctx.locals`) so typed-array fast paths like `out.length` / `out[i]` keep firing instead of falling off into generic by-name dispatch. (4) Constructor: append a fresh-id param per captured outer id, prepend `this.__perry_cap_<outer_id> = LocalGet(fresh_param_id)` after any `super(...)` call (so derived classes initialize `this` first), and apply the same body-remap. (5) Register the class in a new `LoweringContext::class_captures` registry; the `Expr::New { class_name }` lowering site (`crates/perry-hir/src/lower.rs`) now appends `LocalGet(outer_id)` per captured outer id at every construction site (the outer scope's actual id, since we're lowering inside it). Static methods aren't included because they have no `this` to read captures from — if a static method body references an outer local, the original codegen error fires (out of scope for #212). Mutation note: every top-level `LocalSet(outer_id, v)` and `Expr::Update { id: outer_id, .. }` in a method body is wrapped in a `Sequence` that also writes through to `this.__perry_cap_<id>` via the new `analysis::remap_local_ids_in_stmts_with_field_propagation`, so subsequent method calls re-reading the field see the latest value (the canonical `set value(v) { stored = v; } get value() { return stored; }` pattern works — getter after setter sees the just-written value). Mutations still don't propagate back to the OUTER scope's slot — `stored` in the enclosing function still reads its own original snapshot — that's the residual JS divergence. The propagation only fires at top-level expression positions (statement, return value, condition); deeply-nested captured writes like `(stored = v).toString()` only update the local copy. Reference-type mutation (array.push, obj.x = ...) works regardless because the method-local copy and the outer binding hold the same reference. Side effect: removed the v0.5.317 silent-drop guard for `[Symbol.dispose]` / `[Symbol.asyncDispose]` methods at `lower_decl.rs:744` — the dispose family was being dropped when nested in a function and capturing outer locals (the `disposed: string[]` pattern in `test_gap_async_advanced.ts`), which is now redundant: the same hidden-field rewrite that lets `log() { captured.push(...) }` work also lets `[Symbol.dispose]() { disposed.push(this.label); }` work. Inheritance: `class Derived extends Base` where Base has captures works too — at child class lowering, the parent's `__perry_cap_<id>` field declarations are detected via `lookup_class_field_names` and skipped in the child's `fields` (otherwise the keys array would carry two same-named entries at different offsets, the parent's method body would read its index while the child's ctor wrote to the child's index — the inherited-class-shared-capture bug). The child's synthesized ctor still takes the inherited capture as a param + emits `this.__perry_cap_<id> = LocalGet(param)`; the runtime's by-name PropertySet writes to the (single) parent-declared field. For disjoint-capture inheritance (Base captures one local, Derived captures a different one), the child's lowering unions the parent's `class_captures` registry into its own captures_vec — without this, the child's synthesized ctor wouldn't take the parent's capture as a param and the parent's field would never initialize. New regression test `test-files/test_issue_212_class_method_capture.ts` (10 cases — issue's exact repro, multi-capture, user-written ctor with capture-using body, multiple instances per outer-fn invocation, nested closure inside capturing method, dispose-hook pattern from #154, shared-capture inheritance, disjoint-capture inheritance, captured-primitive reassignment via setter/getter, async method with capture — all byte-for-byte against `node --experimental-strip-types`). Existing `test_issue_154_using_dispose.ts` also still passes byte-for-byte; gap tests still 26/28 (no regressions); doc-tests 79/80 (the lone fail is the pre-existing retina screenshot baseline mismatch in `ui/gallery.ts`). `analysis::remap_local_ids_in_stmts` is a new public HIR helper documented to require a new arm whenever a HIR variant carrying a LocalId-bearing sub-expr is added — same class of bug as the one fixed here. Version slot: bumped above v0.5.322 (GTK4 FFIs, #216/#217/#218) per the merge precedent for collisions — origin's pull-and-cherry-pick on top of my v0.5.322 commit ate it before push, so this re-applies as v0.5.323.
154155
- **v0.5.322** — Closes #216, #217, #218: 7 GTK4 FFIs that the doc-tests gate on Linux CI was tripping over. Each issue's symptom was an `undefined reference to perry_ui_<name>` link error against `libperry_ui_gtk4.a` for an FFI the macOS twin already wired. (1) **#216** `perry_ui_open_folder_dialog` (`docs/examples/ui/dialogs/snippets.ts`): factored `crates/perry-ui-gtk4/src/file_dialog.rs::open_dialog` into a private `open_dialog_with_action(callback, title, action, accept_label)` helper, then added `open_folder_dialog` that calls it with `FileChooserAction::SelectFolder` + "Choose Folder" / "Choose" labels. Same callback contract as `open_dialog`: NaN-boxed string on success, `TAG_UNDEFINED` (0x7FFC_0000_0000_0001) on cancel — matches the macOS `NSOpenPanel`-with-`canChooseDirectories: YES, canChooseFiles: NO` shape. (2) **#217** 4 widget-tree FFIs (`docs/examples/ui/layout/snippets.ts`): all in `crates/perry-ui-gtk4/src/widgets/mod.rs`. `widget_remove_child` dispatches by parent kind (`Box::remove`, `ScrolledWindow::set_child(None)`, `Overlay::set_child(None)` for main child or `remove_overlay` for overlays, `Frame` inner-box `remove`); the WIDGETS vec doesn't shrink because handles are positional indices, only the GTK4 parent link is severed (matches `removeFromSuperview`). `widget_reorder_child(parent, from_index, to_index)` snapshots `Box` siblings via `first_child()` walk, locates the child at `from_index`, then calls `Box::reorder_child_after(&child, anchor)` where the anchor index is `to` when moving forward or `to-1` when moving back (the macOS `arrangedSubviews + insertArrangedSubview:atIndex:` shape doesn't translate directly because `reorder_child_after` is positional-relative-to-sibling, not absolute-by-index). Out-of-range indices are clamped: `from > N-1` → no-op, `to >= N` → moves to end. `widget_add_overlay`: if parent is `gtk4::Overlay` → `add_overlay`; otherwise log a warning + fall through to `add_child` (GTK4 widgets have a single immutable parent slot — can't retroactively wrap a non-Overlay parent). `widget_set_overlay_frame(handle, x, y, w, h)`: GTK4's layout model is constraint-based not absolute-frame, so we approximate with `halign/valign = Start` + `set_margin_start(x)` + `set_margin_top(y)` + `set_size_request(w, h)` — works correctly when the parent is a ZStack (which is backed by `gtk4::Overlay` and honors child halign/valign), an OK approximation elsewhere. (3) **#218** SplitView (`docs/examples/ui/layout/snippets.ts`): new `crates/perry-ui-gtk4/src/widgets/splitview.rs` backed by `gtk4::Paned::new(Orientation::Horizontal)` (matches macOS `NSSplitView`'s vertical-divider/side-by-side default). `splitview_create(_left_width)` accepts the macOS arg for signature parity but ignores it — `Paned::set_position` is a separate setter. `splitview_add_child`: first call → `set_start_child`, second → `set_end_child`, third+ → no-op + warning. **2-vs-N caveat**: GTK4 `Paned` supports exactly 2 children where `NSSplitView` supports N; users wanting >2 panes on Linux nest SplitViews recursively. Verified: `cargo build --release -p perry-ui-gtk4` clean (only pre-existing `js_string_from_bytes` signature redeclaration warnings between `audio.rs` and `lib.rs`); all 7 new symbols present in `nm --defined-only target/release/libperry_ui_gtk4.a` (`perry_ui_open_folder_dialog`, `perry_ui_widget_remove_child`, `_reorder_child`, `_add_overlay`, `_set_overlay_frame`, `perry_ui_splitview_create`, `_add_child`); `cargo run --release -p perry-ui --bin styling-matrix -- --check` clean (none of these FFIs are styling props — folder dialog is "system", layout/splitview are container management — so no matrix rows are added); `PERRY_UI_TEST_MODE=1 ./perry compile docs/examples/ui/dialogs/snippets.ts -o /tmp/dialogs_snippets` produces a 0.9 MB ELF that exits 0 in test mode; same for `docs/examples/ui/layout/snippets.ts` → 1.0 MB ELF, exit 0. The layout snippet emits the documented `widget_add_overlay on non-Overlay parent — falling back to add_child` warnings at runtime because the example uses `widgetAddOverlay` on an HStack/VStack rather than a ZStack — the warning surface I added makes this discoverable so example authors can wrap with ZStack to get true float-above semantics on GTK4 (separate sample-update follow-up; not blocking the link gate the issues filed for).

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.324"
112+
version = "0.5.325"
113113
edition = "2021"
114114
license = "MIT"
115115
repository = "https://github.com/PerryTS/perry"

0 commit comments

Comments
 (0)