Skip to content

Commit 85f7497

Browse files
author
Ralph Kuepper
committed
fix(ui): align Win64 perry-ui ABI with dispatch table (v0.5.345)
Two doc-tests crashed on Windows CI (`ui/layout/snippets.ts` and `ui/menus/snippets.ts`) with ACCESS_VIOLATION. Root cause: three perry-ui runtime extern signatures didn't match the arg shapes declared in `perry-dispatch::PERRY_UI_TABLE`. On Win64 ABI, integer and float positional args share the same positional slot indices (RDX/XMM1, R8/XMM2, ...) so a mismatched signature reads garbage from uninitialized registers. SysV (macOS/Linux) uses separate int/float register pools and happened to land valid bits in the right physical registers, so the bugs were Windows-only. - `perry_ui_navstack_create` now takes 0 args (was `(title_ptr, body_handle)`); dispatch row is `args: &[]`. The previous signature read RCX as `*const u8` and dereferenced it in `str_from_header`. - `perry_ui_menu_add_item_with_shortcut` reorders to `(menu, title, shortcut: i64, callback: f64)` matching dispatch `[Widget, Str, Str, Closure]`. The previous order put the closure pointer into the runtime's `shortcut_ptr: i64` slot. - `perry_ui_app_set_timer` now takes the leading App handle the dispatch row passes — was missing, causing the f64 args to land in the wrong XMM slots. - `perry_ui_stack_set_distribution` on Windows changed from `i64` to `f64` (already `f64` on every other platform) so the dispatched float arg lands in XMM1 instead of being read as garbage from RDX. - `perry-ui-windows::toolbar::create()` now parents under the parking HWND. The old code passed `None` for a `WS_CHILD` parent, which Windows refuses with HRESULT 0x8007057E "Cannot create a top-level child window." All 8 perry-ui-* platform crates updated for the first three so the dispatch ABI is consistent everywhere, not just Windows. Verified locally on Windows: `./scripts/run_doc_tests.ps1 --skip-xcompile --filter-exclude ui/gallery.ts` reports 76/81 passed, 0 failed, 5 skipped (matches CI shape; previously the same sweep was 74/81 with `ui/layout/snippets.ts` and `ui/menus/snippets.ts` reporting RUN_FAIL).
1 parent 0cfcc64 commit 85f7497

12 files changed

Lines changed: 100 additions & 67 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.344
11+
**Current Version:** 0.5.345
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.345** — Fix Windows CI doc-test failures (`ui/layout/snippets.ts` + `ui/menus/snippets.ts` ACCESS_VIOLATION) by aligning runtime extern signatures with the `perry-dispatch` table. Three coordinated fixes covering all 8 perry-ui-* platform crates: (1) `perry_ui_navstack_create()` now takes 0 args (matching dispatch row `args: &[]`) — was `(title_ptr: i64, body_handle: i64)` on every platform. The TS surface is `NavStack(): Widget`; dispatch emitted a 0-arg LLVM call but Rust read RCX/RDX. On Win64 ABI those were uninitialized, so `str_from_header(garbage_ptr)` dereferenced wild memory at offset 0x5 and crashed. macOS/Linux SysV ABI happened to land 0 in RDI/RSI in this call shape, masking the bug. (2) `perry_ui_menu_add_item_with_shortcut(menu, title, shortcut: i64, callback: f64)` reorders the last two args to match dispatch `[Widget, Str, Str, Closure]` — runtime previously had `(menu, title, callback: f64, shortcut: i64)`. On Win64 ABI int and float positional args share register slot indices (RDX/XMM1, R8/XMM2 etc.), so the swap put the closure value into the runtime's `shortcut_ptr: i64` slot — `str_from_header(closure_ptr)` then crashed. SysV's separate int/float register pools coincidentally landed each value in the right register for the wrong slot name, hiding it on macOS/Linux. (3) `perry_ui_app_set_timer(app: i64, interval_ms: f64, callback: f64)` adds the missing leading App handle to match dispatch `[Widget, F64, Closure]` (TS API `appSetTimer(app, intervalMs, callback)`) — same XMM-misalignment family bug, manifesting as wrong timer intervals + lost callbacks on Win64 rather than a crash. (4) `perry_ui_stack_set_distribution(handle, distribution: f64)` aligns Windows with the other 7 platforms (was `i64` only on Windows) so the f64 dispatch arg lands in XMM1 instead of being read as garbage from RDX. (5) `perry-ui-windows::toolbar::create()` parents the new toolbar HWND under the parking window — was passing `None` for the `WS_CHILD` parent which Windows refuses with HRESULT 0x8007057E "Cannot create a top-level child window". The fix is the same parking-HWND pattern every other widget already uses; `attach()` reparents to the real app window when the layout resolves. Verified: full doc-tests sweep on Windows now reports **76/81 passed, 0 failed, 5 skipped** (matches CI shape — `--filter-exclude ui/gallery.ts`). The 5 skips are `platforms/wasm_snippets.ts` / `runtime/thread_primitives.ts` / `runtime/thread_snippets.ts` / `ui/threading/snippets.ts` / `ui/widgets/image_symbol.ts` — all platform-banner skips, not regressions. Latent mismatches surfaced during the audit but NOT fixed in this commit (they don't crash today by luck and are tracked separately): `Picker` / `tabbarAddTab` / `frameSplitCreate` / `TextArea` constructor / `sheetCreate` arg shapes — each will need a similar dispatch-vs-runtime alignment, but only when those code paths actually exercise the Win64 ABI path.
152153
- **v0.5.344** — Release-unblocker: triage 2 parity failures + re-bless Linux gallery baseline. (1) `test-parity/known_failures.json` adds `test_gap_array_methods` (status:known_limitation — TypedArray.at() returns undefined; categorical typed-array gap, was passing at v0.5.319, regressed in the v0.5.319→v0.5.343 series) and `test_json_lazy_predicates` (status:bug — `Array.isArray()` returns false on `[...].map(fn)` result; missing predicate-side recognition of the map-returned shape). Both surfaced when the v0.5.343 release-packages `await-tests` gate failed: parity job flagged them as new vs the snapshot. (2) `docs/examples/_baselines/linux/gallery.png` re-blessed from the v0.5.343 CI artifact (was 900x1400, now 900x1024 matching the GTK4 runner's actual render). The previous baseline was stale — likely from a pre-#202/#206 GTK4 styling state where the gallery rendered taller. Macos baseline unchanged (passes CI cleanly at 900x970), Windows baseline unchanged (separate ACCESS_VIOLATION crashes in `ui/layout/snippets.ts` + `ui/menus/snippets.ts` are tracked separately — Windows worker investigating). v0.5.343 tag stays as-is per release-skill rule (do not retag); this is the first publishable release after the long extraction series.
153154
- **v0.5.343** — Post-extraction cleanup: removes 66 unused-import warnings introduced across the v0.5.329–v0.5.342 split work via `cargo fix --bin perry --lib -p perry-hir -p perry-codegen --allow-dirty --release`. 25 files touched (15 in the new sub-modules, 10 in unrelated files where cargo fix happened to find pre-existing dead imports). Each fix is exclusively `use`-statement narrowing — no behavior changes. Examples: `expr_object.rs` had `anyhow!`, `FuncId`, `ArrayElement`, `BinaryOp`, `is_destructuring_pattern`, `infer_type_from_expr` from the bulk-import phase that turned out unused once the body was inlined; `lower_call/native.rs` had `Function`, `is_abort_controller_expr`, `lower_abort_controller_call`, `nanbox_string_inline` ditto; `compile/resolve.rs` had `find_file_dep_in_package_json`, `find_node_modules`, `has_perry_native_module`, `is_ts_file`, `resolve_exports`, `resolve_package_entry`, `resolve_package_source_entry`, `resolve_with_extensions` (these are still callable via the `super::` re-exports — they're just not used inside the resolve module itself, only by its callers). Workspace warning count: **321 → 255** (−66, −21%). The remaining 255 are pre-existing categorical issues (63 `unnecessary unsafe block` in perry-runtime, 25 `unreachable pattern` in perry-hir/perry-codegen lowerers, 23 `unnecessary transmute`, etc. — separate bugs, not session-introduced). **Verified**: cargo build --release clean; cargo test --workspace 434/0/5 = baseline; gap tests 25/28 = baseline; doc-tests --skip-xcompile 80/82 = baseline. Final session tally: 15 commits (v0.5.329→v0.5.343), all baselines green throughout, ~14,000 LOC reorganized into 19 focused sub-modules, the most-cited monolith (`lower::lower_expr`) shrunk by 91%, compile.rs by 60%, lower.rs by 44%, lower_call.rs by 33%.
154155
- **v0.5.342** — Tier 2.1 final extraction: moves the per-platform link command construction out of `crates/perry/src/commands/compile.rs::run_with_parse_cache` into a new `compile/link.rs` sub-module. Pre-extraction this was a single ~1240-LOC inline block fanning out across macOS / iOS / tvOS / visionOS / watchOS / Android / Linux / Windows + 5 cross-compile permutations — every platform-specific link flag change churned the orchestrator file. The new `pub(super) fn build_and_run_link(args_input, ctx, target, obj_paths, compiled_features, runtime_lib, stdlib_lib, jsruntime_lib, exe_path, format) -> Result<()>` takes 10 params (no struct needed once `is_*` flags get re-derived from `target` internally) and owns: (1) per-platform `Command::new(...)` selection — clang on macOS/iOS, swiftc on watchOS/visionOS, Android NDK clang, lld-link / link.exe on Windows, ld64.lld on Linux→Apple cross paths, plus the matching SDK / sysroot / triple discovery via `xcrun --sdk ... --show-sdk-path` / `--find clang` / `--find swiftc`. (2) entry-object `_main` rename via `rust-objcopy --redefine-sym` for watchOS / visionOS Swift-app shells (`_main → _perry_main_init`) and iOS / tvOS / watchOS game-loop variants (`_main → __perry_user_main`). (3) accumulating object files + dead-strip flags (`-Wl,--gc-sections` / `-dead_strip` / `-Xlinker -dead_strip` / `/OPT:REF /OPT:ICF`). (4) library link order — jsruntime → runtime fallback → stdlib precedence rules (the comment explains the Mach-O first-wins vs ELF `--allow-multiple-definition` vs MSVC `/FORCE:MULTIPLE` differences). (5) `-o exe_path` / `/OUT:...`. (6) plugin-host `-Wl,-u,_<sym>` force-keep + `-rdynamic` on Linux. (7) per-platform framework / system-lib enumeration (UIKit / SwiftUI / AVFoundation on Apple, GTK4 via `pkg-config --libs gtk4` with hardcoded fallback on Linux, Win32 system libs on Windows, Android stub for `JNI_GetCreatedJavaVMs`). (8) UI lib + strip-dedup invocation + GTK4 `--whole-archive` for `js_stdlib_process_pending`. (9) geisterhand lib link + Windows `/INCLUDE:` symbol force-references. (10) external `perry.nativeLibrary` manifest crates: `cargo build --release [+nightly] [-Zbuild-std] --manifest-path` per crate, find the resulting staticlib, framework / lib / pkg-config additions, swiftc compilation of manifest-declared `swift_sources` for `--features watchos-swift-app` (with deduplication via `seen_swift_sources: HashSet<PathBuf>`), and metal-source target validation. (11) `cmd.status()?` invocation + bail on non-zero. **Visibility changes**: 0 — every helper called by the new module is already `pub(super)` from the prior 9 sub-module extractions in this Tier 2.1 stack. The new module accesses `find_geisterhand_*`, `find_ui_library`, `find_lld_link`, `find_msvc_link_exe`, `find_perry_windows_sdk`, `windows_pe_subsystem_flag`, `find_msvc_lib_paths`, `find_stdlib_library`, `find_llvm_tool`, `find_visionos_swift_runtime`, `find_watchos_swift_runtime`, `apple_sdk_version`, `strip_duplicate_objects_from_lib`, `build_geisterhand_libs`, and `rust_target_triple` (compile.rs's own private fn — accessible to children) via `super::*`. **Argument-passing approach**: the call site is in `run_with_parse_cache` AFTER `args.output` has already been consumed by `unwrap_or_else` (line 2429 in old numbering), so the function takes `args_input: &Path` (i.e. `&args.input`) rather than `&CompileArgs`. The other ~10 params are by reference — `&CompilationContext`, `Option<&str>`, `&[PathBuf]`, `&[String]`, `&Path` (×2), `&Option<PathBuf>` (×2), and `OutputFormat` by value (Copy). The dylib link path stays inline in `run_with_parse_cache` because it returns early with a `CompileResult`; per-platform `.app` bundling (iOS / visionOS / watchOS / tvOS) and Android `.so` companion-lib copying also stay inline since they happen after the link returns and depend on many post-link variables (`result_bundle_id`, `result_app_dir`, etc.). **`is_cross_*` cleanup**: `is_cross_windows`, `is_cross_ios`, `is_cross_visionos`, `is_cross_macos`, and `is_cross_tvos` were all only used inside the extracted block. Their declarations have been removed from `compile.rs`; the new `link.rs` re-derives them internally from `target`. **compile.rs delta**: 5019 → 3783 LOC (-1236, ~25% reduction). **Cumulative across this session** (v0.5.329-v0.5.342, fourteen commits): **compile.rs 9391 → 3783 LOC (~60% total reduction)**, lower_call.rs 7000+ → 4681 LOC (~33%), lower.rs 13591 → 7554 LOC (~44%), `lower::lower_expr` 6687 → 624 LOC (~91%). compile.rs is now ~40% of its starting size; what remains is the orchestrator entry points (`run`, `run_with_parse_cache`), context construction (`CompilationContext::new`), and the parse / typecheck / lower / codegen / cache pipeline — i.e. exactly what an orchestrator file should hold. The full `compile/` sub-module structure has 10 focused files: parse_cache (294 LOC), strip_dedup (518), library_search (704), targets (824), object_cache (747), resolve (849), optimized_libs (413), collect_modules (412), and now link (1245). **What still works**: cargo build --release clean; cargo test --workspace 434/0/5 = baseline; gap tests 25/28 = baseline (the 3 failing — `array_methods`, `console_methods`, `typed_arrays` — pre-existing categorical/CI gaps, not regression-induced); doc-tests --skip-xcompile 80/82 = baseline (the lone fail is the pre-existing `ui/gallery.ts` retina screenshot drift); UI smoke compile (`App({title, body: VStack([Text("OK"), Button("Click", () => {})]) })`) produces a 856K macOS binary that exits 0 in `PERRY_UI_TEST_MODE=1`, and the strip-dedup output (419 → 35 trimmed objects via 178 by-symbol-subset + 222 by-name-pattern + 16 rlib-extracted + 19 kept) matches v0.5.341 byte-for-byte — proves the live path through library_search + targets + strip_dedup + ObjectCache + resolve + link all works end-to-end. **Cumulative across this session** (v0.5.329-v0.5.342, fourteen commits): every plan item delivered, plus four follow-up rounds extracting roughly 13,200 LOC across 18 new sub-modules into the largest cognitive-load reduction in the project's history. Tier 2.1 is now complete.

Cargo.lock

Lines changed: 28 additions & 28 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
@@ -111,7 +111,7 @@ opt-level = "s" # Optimize for size in stdlib
111111
opt-level = 3
112112

113113
[workspace.package]
114-
version = "0.5.344"
114+
version = "0.5.345"
115115
edition = "2021"
116116
license = "MIT"
117117
repository = "https://github.com/PerryTS/perry"

crates/perry-ui-android/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ pub extern "C" fn perry_ui_widget_set_context_menu(widget_handle: i64, menu_hand
492492
}
493493

494494
#[no_mangle]
495-
pub extern "C" fn perry_ui_menu_add_item_with_shortcut(_menu_handle: i64, _title_ptr: i64, _callback: f64, _shortcut_ptr: i64) {
495+
pub extern "C" fn perry_ui_menu_add_item_with_shortcut(_menu_handle: i64, _title_ptr: i64, _shortcut_ptr: i64, _callback: f64) {
496496
// No-op on Android — no menu bar on mobile
497497
}
498498

@@ -573,7 +573,7 @@ pub extern "C" fn perry_ui_app_on_terminate(callback: f64) {
573573
}
574574

575575
#[no_mangle]
576-
pub extern "C" fn perry_ui_app_set_timer(interval_ms: f64, callback: f64) {
576+
pub extern "C" fn perry_ui_app_set_timer(_app_handle: i64, interval_ms: f64, callback: f64) {
577577
app::set_timer(interval_ms, callback);
578578
}
579579

@@ -763,8 +763,9 @@ pub extern "C" fn perry_ui_image_set_tint(handle: i64, r: f64, g: f64, b: f64, a
763763
// =============================================================================
764764

765765
#[no_mangle]
766-
pub extern "C" fn perry_ui_navstack_create(title_ptr: i64, body_handle: i64) -> i64 {
767-
widgets::navstack::create(title_ptr as *const u8, body_handle)
766+
pub extern "C" fn perry_ui_navstack_create() -> i64 {
767+
// Matches the 0-arg dispatch in perry-dispatch::PERRY_UI_TABLE.
768+
widgets::navstack::create(std::ptr::null(), 0)
768769
}
769770

770771
#[no_mangle]

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub extern "C" fn perry_ui_app_on_terminate(callback: f64) {
101101

102102
/// Set a repeating timer.
103103
#[no_mangle]
104-
pub extern "C" fn perry_ui_app_set_timer(interval_ms: f64, callback: f64) {
104+
pub extern "C" fn perry_ui_app_set_timer(_app_handle: i64, interval_ms: f64, callback: f64) {
105105
app::set_timer(interval_ms, callback);
106106
}
107107

@@ -752,7 +752,8 @@ pub extern "C" fn perry_ui_menu_add_item(menu_handle: i64, title_ptr: i64, callb
752752

753753
/// Add a menu item with a keyboard shortcut.
754754
#[no_mangle]
755-
pub extern "C" fn perry_ui_menu_add_item_with_shortcut(menu_handle: i64, title_ptr: i64, callback: f64, shortcut_ptr: i64) {
755+
pub extern "C" fn perry_ui_menu_add_item_with_shortcut(menu_handle: i64, title_ptr: i64, shortcut_ptr: i64, callback: f64) {
756+
// Arg order matches the TS-side API: `menuAddItemWithShortcut(menu, title, shortcut, callback)`.
756757
menu::add_item_with_shortcut(menu_handle, title_ptr as *const u8, callback, shortcut_ptr as *const u8);
757758
}
758759

@@ -1062,8 +1063,9 @@ pub extern "C" fn perry_ui_hstack_create_with_insets(spacing: f64, top: f64, lef
10621063

10631064
/// Create a NavigationStack with initial page.
10641065
#[no_mangle]
1065-
pub extern "C" fn perry_ui_navstack_create(title_ptr: i64, body_handle: i64) -> i64 {
1066-
widgets::navstack::create(title_ptr as *const u8, body_handle)
1066+
pub extern "C" fn perry_ui_navstack_create() -> i64 {
1067+
// Matches the 0-arg dispatch in perry-dispatch::PERRY_UI_TABLE.
1068+
widgets::navstack::create(std::ptr::null(), 0)
10671069
}
10681070

10691071
/// Push a page onto the navigation stack.

0 commit comments

Comments
 (0)