Skip to content

Commit e58ff60

Browse files
Ralph Kuepperclaude
andcommitted
fix(ui-gtk4): wire 7 missing FFIs blocking Linux doc-tests gate (v0.5.322)
Closes #216, #217, #218. #216 perry_ui_open_folder_dialog (FileChooserAction::SelectFolder). #217 widget_remove_child / _reorder_child / _add_overlay / _set_overlay_frame (Box::remove + reorder_child_after; Overlay:: add_overlay with non-Overlay-parent fallback + warning; halign/valign + margins + size_request approximation for the constraint-vs-absolute-frame mismatch). #218 splitview_create / splitview_add_child via gtk::Paned (2-child cap vs N on macOS NSSplitView; third+ child no-op + warning). cargo build -p perry-ui-gtk4 clean. All 7 FFIs present in libperry_ui_gtk4.a. styling-matrix --check clean (none are styling props). docs/examples/ui/dialogs/snippets.ts and docs/examples/ui/layout/snippets.ts compile + link + run-in-test-mode exit 0 on Linux GTK4 4.20.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bbbf48f commit e58ff60

7 files changed

Lines changed: 283 additions & 32 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.321
11+
**Current Version:** 0.5.322
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.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).
152153
- **v0.5.321** — Closes #214: `string[]` element access miscompile when elements were SSO (short-string-optimization) values from `JSON.parse`. Two reported symptoms shared one root cause: (1) `arr.indexOf(s)` returned -1 for present elements; (2) `arr[i]` "SIGSEGV'd at 0x10000000028" — really the next consumer (string concat, ===, .toUpperCase()) treating the SSO bits as a `*StringHeader`. Pre-fix, the codegen emitted `bits & POINTER_MASK_I64` for any "I have a NaN-boxed string handle, need a `*StringHeader`" call site, which returns the lower 48 bits — correct for STRING_TAG (0x7FFF) heap strings, but for SHORT_STRING_TAG (0x7FF9) values those 48 bits encode the inline len + payload, not a pointer. JSON.parse emits SSO inline for strings ≤5 bytes (v0.5.216), so `JSON.parse('["hello", "world"]')[0]` returned SSO bits, then any string consumer dereferenced the inline bytes as a `*StringHeader` → segfault at the inline-payload-derived address (the user's "$" repro produced 0x10000000028 because SSO("$") → lower-48 = 0x010000000024, +offset 4 = 0x010000000028). For indexOf, `js_array_indexOf_f64` did raw bit compare, so SSO array element bits never matched the heap-string needle bits → -1. Fix in 5 codegen sites: (a) new `unbox_str_handle(blk, val)` helper in `crates/perry-codegen/src/expr.rs` calls `js_get_string_pointer_unified` (already SSO-aware — has been since v0.5.277, materializes SSO to heap on demand). (b) Both `Array.indexOf` arms (`crates/perry-codegen/src/lower_array_method.rs:293` + `crates/perry-codegen/src/expr.rs::Expr::ArrayIndexOf`) flip from `js_array_indexOf_f64` → `js_array_indexOf_jsvalue` (mirrors what `includes` already did at line 276). New `js_array_indexOf_jsvalue` extern in `runtime_decls.rs`. (c) String concat fast paths (`lower_string_concat`, `lower_string_self_append`, `lower_string_coerce_concat`) use `unbox_str_handle` for string operands. (d) Both-strings === / !== / loose eq arm (`expr.rs:1495`) and both-strings < / > / <= / >= arm (`expr.rs:1522`) use `unbox_str_handle`. (e) Every `unbox_to_i64(blk, &recv_box)` / `unbox_to_i64(blk, &<string_arg>)` site in `crates/perry-codegen/src/lower_string_method.rs` (38 occurrences across slice/substring/charAt/repeat/replace/at/charCodeAt/codePointAt/lastIndexOf/padStart/padEnd/normalize/localeCompare/search/match/matchAll/isWellFormed/toWellFormed/concat/substr/startsWith/endsWith/includes) now uses `unbox_str_handle`. The runtime perf cost is one extra function call per string operand (`js_get_string_pointer_unified` returns the heap pointer in ~1 ns when STRING_TAG, materializes when SSO); the call is dwarfed by the runtime work that follows in every case. The slice-no-end-arg length-read at `lower_string_method.rs:81` also routed through `unbox_str_handle` so length reads on SSO receivers materialize first. New regression test `test-files/test_issue_214_string_array_sso.ts` covers indexOf / array-index-into-concat / strict equality / != / .toUpperCase() / .length / .startsWith / .indexOf / push-then-indexOf, all byte-for-byte against `node --experimental-strip-types`. Gap-test side-effect: `/tmp/run_gap_tests.sh` jumps from the documented 18/28 baseline → 26/28 — 8 previously-failing tests (`array_methods`, `async_advanced`, `error_extensions`, `fetch_response`, `global_apis`, `map_set_extended`, `object_methods`, `string_methods`) were all symptoms of this same SSO unboxing bug, fixed transitively. Remaining 2 (`console_methods` ci-env diff, `typed_arrays` categorical gap) unrelated. Version slot: bumped above v0.5.319/v0.5.320 (#181 Linux GTK4 link + strip-dedup, parallel-track on origin) per the merge precedent for collisions.
153154
- **v0.5.320** — #181 part B (Linux strip-dedup over-prune): the `strip_duplicate_objects_from_lib` filter at `crates/perry/src/commands/compile.rs:985-986` was dropping every staticlib member whose name matched `m.contains("perry_runtime-")` or `m.contains("perry_stdlib-")` on the assumption that the standalone `libperry_runtime.a` / `libperry_stdlib.a` would re-supply those objects' symbols. That assumption is false for generic monomorphizations. When `perry-ui-gtk4` is built as a staticlib, Cargo re-monomorphizes generics (e.g. `hashbrown::raw::RawTable<T,A>::reserve_rehash` for GTK4-specific HashMap key/value types) into whichever crate's CGU first triggers the instantiation — sometimes a `perry_runtime-*` CGU bundled inside `libperry_ui_gtk4.a`. Standalone `libperry_runtime.a` (which has only ever seen perry_runtime's *own* type instantiations) doesn't define those GTK4-specific monomorphizations, so dropping the staticlib's bundled `perry_runtime-*` CGU silently removes the only definition. Link bombs with `undefined reference to hashbrown::raw::RawTable<T,A>::reserve_rehash` — exactly the Arch Linux comment on issue #181. The drop was added 2026-03-26 (commit 7a2e27ca) to placate lld-link's Windows-cross-compile duplicate-symbol rejection, but `strip-dedup` is now skipped entirely on Windows (see `is_windows` guard at the call site), making the filter dead-weight that only hurts Linux/macOS where `--allow-multiple-definition` (ELF) / `ld64` first-wins (Mach-O) handle real duplicates safely. Fix: delete the two `m.contains("perry_runtime-")` / `m.contains("perry_stdlib-")` early-returns. Reproduced locally on Fedora 43 by setting `PERRY_LLVM_AR` so strip-dedup actually runs (default search path doesn't pick up `/usr/lib64/rocm/llvm/bin/llvm-ar`); pre-fix produced the issue's exact `reserve_rehash` undefined reference, post-fix produces a 0.9 MB ELF that links cleanly with one extra `perry_runtime-*` CGU retained (152 → 152 final objects, kept count 135 → 136). Verified the missing symbol `reserve_rehash17h1c96b8c6ae9855b6E` lives in `perry_runtime-4d338a77f5f2cc14.perry_runtime.838b7969b3e5446c-cgu.0.rcgu.o` inside libperry_ui_gtk4.a but is *not* present in standalone libperry_runtime.a (`llvm-nm --defined-only` across all 352 members returns 0 hits) — confirming the dead-symbol-via-pattern-drop diagnosis. Combined-mode regression also verified: `PERRY_LLVM_AR=… PKG_CONFIG_PATH=/nonexistent` exercises both Bug A (v0.5.319) and Bug B (this commit) at the same time and produces a clean executable.
154155
- **v0.5.319** — #181 part A (Linux/Ubuntu GTK4 link): the `pkg-config --libs gtk4` invocation in `crates/perry/src/commands/compile.rs:7142` only had a fallback for the spawn-failure case (`if let Ok(out) = ... { if out.status.success() { use it } } else { fallback }`). When pkg-config existed but `gtk4.pc` wasn't on its search path (status != 0), the code silently emitted no GTK link flags and the linker bombed with hundreds of `g_object_unref` / `gtk_widget_set_*` / `g_signal_connect_data` / `gtk_window_*` undefined references — exactly the failure shape from issue #181. Fix: track success via a `got_gtk_libs` flag, fall back when *either* spawn fails *or* status != 0, print a warning so the user knows pkg-config didn't help, and expand the fallback list from the original 4 libs (`-lgtk-4 -lgobject-2.0 -lglib-2.0 -lgio-2.0`) to the 11 libs `pkg-config --libs gtk4` actually returns on a standard libgtk-4-dev install (adds `-lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0` — pre-fix the short fallback would still leave pango/cairo/gdk_pixbuf/graphene refs undefined). Reproduced locally on Fedora 43 by forcing `PKG_CONFIG_PATH=/nonexistent PKG_CONFIG_LIBDIR=/nonexistent` — pre-fix produced the exact issue's error signature, post-fix produces a 0.9 MB ELF executable with one informational warning. Happy path (pkg-config finds `gtk4.pc`) is unchanged.

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

crates/perry-ui-gtk4/src/file_dialog.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,43 @@ extern "C" {
1111
/// Open a file dialog. Calls callback with the selected file path (NaN-boxed string).
1212
/// If user cancels, callback is called with TAG_UNDEFINED.
1313
pub fn open_dialog(callback: f64) {
14+
open_dialog_with_action(
15+
callback,
16+
"Open File",
17+
FileChooserAction::Open,
18+
"Open",
19+
);
20+
}
21+
22+
/// Open a folder picker. Calls callback with the selected directory path
23+
/// (NaN-boxed string). If user cancels, callback is called with TAG_UNDEFINED.
24+
/// Mirrors macOS `perry_ui_open_folder_dialog` (NSOpenPanel with
25+
/// `canChooseDirectories: YES, canChooseFiles: NO`).
26+
pub fn open_folder_dialog(callback: f64) {
27+
open_dialog_with_action(
28+
callback,
29+
"Choose Folder",
30+
FileChooserAction::SelectFolder,
31+
"Choose",
32+
);
33+
}
34+
35+
fn open_dialog_with_action(
36+
callback: f64,
37+
title: &str,
38+
action: FileChooserAction,
39+
accept_label: &str,
40+
) {
1441
// Get the active window
1542
let window: Option<Window> = None; // No parent window reference in GTK4 static context
1643

1744
let dialog = FileChooserDialog::new(
18-
Some("Open File"),
45+
Some(title),
1946
window.as_ref(),
20-
FileChooserAction::Open,
47+
action,
2148
&[
2249
("Cancel", ResponseType::Cancel),
23-
("Open", ResponseType::Accept),
50+
(accept_label, ResponseType::Accept),
2451
],
2552
);
2653

0 commit comments

Comments
 (0)