diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index d1bc00f1c1..cc6cc64850 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -132,7 +132,15 @@ pub(super) fn compile_module_entry( let main = if is_dylib { llmod.define_function("perry_module_init", VOID, vec![]) } else { - llmod.define_function("main", I32, vec![]) + // Allow the host build to override the C entry symbol. On arm64_32 + // watchOS we can't rename `_main → __perry_user_main` after the + // fact (rust-objcopy's MachOWriter crashes on arm64_32 objects), so + // we emit the final symbol directly. Pass e.g. `_perry_user_main` + // (the leading underscore yields Mach-O `__perry_user_main`, which + // the Swift `@main` shell references via @_silgen_name). + let entry_name = + std::env::var("PERRY_ENTRY_SYMBOL").unwrap_or_else(|_| "main".to_string()); + llmod.define_function(&entry_name, I32, vec![]) }; main.add_pre_return_void_call("js_typed_feedback_maybe_dump_trace"); let _ = main.create_block("entry"); diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index 680a018858..eed8183e15 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -397,6 +397,12 @@ pub fn resolve_target_triple(name: &str) -> Option { "ios-simulator" => Some("arm64-apple-ios17.0-simulator".to_string()), "visionos" => Some("arm64-apple-xros1.0".to_string()), "visionos-simulator" => Some("arm64-apple-xros1.0-simulator".to_string()), + // arm64_32 (Series 4-8 / SE) when opted in via PERRY_WATCHOS_ARM64_32; + // otherwise arm64 (S9+). Sets the arch of the emitted TS object files, + // which must match the runtime/native-lib/link triples. + "watchos" if std::env::var("PERRY_WATCHOS_ARM64_32").is_ok() => { + Some("arm64_32-apple-watchos".to_string()) + } "watchos" => Some("aarch64-apple-watchos".to_string()), "watchos-simulator" => Some("arm64-apple-watchos10.0-simulator".to_string()), "tvos" => Some("aarch64-apple-tvos".to_string()), diff --git a/crates/perry-runtime/src/array/from_concat.rs b/crates/perry-runtime/src/array/from_concat.rs index 84eb49d8c6..59471d18c2 100644 --- a/crates/perry-runtime/src/array/from_concat.rs +++ b/crates/perry-runtime/src/array/from_concat.rs @@ -413,7 +413,9 @@ fn array_like_length(items: f64) -> usize { let n = n.floor(); let max = (1u64 << 53) as f64 - 1.0; if n > max { - return (1usize << 53) - 1; + // 2^53 - 1 (JS max safe integer). usize can't represent this on 32-bit + // targets (arm64_32 watchOS, wasm32), so saturate to usize::MAX there. + return usize::try_from((1u64 << 53) - 1).unwrap_or(usize::MAX); } n as usize } diff --git a/crates/perry-runtime/src/box.rs b/crates/perry-runtime/src/box.rs index 97c5dbd269..8304c9b36e 100644 --- a/crates/perry-runtime/src/box.rs +++ b/crates/perry-runtime/src/box.rs @@ -87,7 +87,7 @@ pub fn scan_box_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { // address (alloc gives 8-aligned pointers in user space) // matches `is_plausible_box_ptr` to keep this a no-op for // any pathological entry. - if addr >= 0x1000 && addr < 0x0001_0000_0000_0000 && addr % 8 == 0 { + if addr >= 0x1000 && (addr as u64) < 0x0001_0000_0000_0000 && addr % 8 == 0 { unsafe { visitor.visit_nanbox_f64_raw_slot(&raw mut (*ptr).value); } @@ -200,7 +200,7 @@ fn is_plausible_box_ptr(ptr: *mut Box) -> bool { if addr < 0x1000 { return false; } - if addr >= 0x0001_0000_0000_0000 { + if (addr as u64) >= 0x0001_0000_0000_0000 { return false; } if addr % std::mem::align_of::() != 0 { diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index 88dbb0b5c3..c769ba7074 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -1694,7 +1694,10 @@ fn looks_like_raw_heap_pointer(value: f64) -> bool { return false; } let addr = bits as usize; - (0x1000..0x8000_0000_0000usize).contains(&addr) && addr >= crate::gc::GC_HEADER_SIZE + 0x1000 + // Compare in u64 so the 2^47 upper bound stays in range on 32-bit targets + // (arm64_32 watchOS, wasm32), where it's a no-op — no addresses that high. + (0x1000..0x8000_0000_0000u64).contains(&(addr as u64)) + && addr >= crate::gc::GC_HEADER_SIZE + 0x1000 } fn formatted_deep_equal(left: f64, right: f64, skip_prototype: bool) -> bool { diff --git a/crates/perry-runtime/src/value/dynamic_object.rs b/crates/perry-runtime/src/value/dynamic_object.rs index b3caa128d7..d8df1018a2 100644 --- a/crates/perry-runtime/src/value/dynamic_object.rs +++ b/crates/perry-runtime/src/value/dynamic_object.rs @@ -87,7 +87,7 @@ pub extern "C" fn js_value_length_f64(value: f64) -> f64 { target_os = "visionos", )))] let heap_min: usize = 0x200_0000_0000; - if handle < heap_min || handle >= 0x8000_0000_0000 { + if handle < heap_min || (handle as u64) >= 0x8000_0000_0000 { return 0.0; } if let Some(value) = unsafe { diff --git a/crates/perry/src/commands/compile/app_metadata.rs b/crates/perry/src/commands/compile/app_metadata.rs index d4cc9634dd..3a7adcdebe 100644 --- a/crates/perry/src/commands/compile/app_metadata.rs +++ b/crates/perry/src/commands/compile/app_metadata.rs @@ -158,6 +158,13 @@ pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { Some("visionos-simulator") => Some("aarch64-apple-visionos-sim"), Some("visionos") => Some("aarch64-apple-visionos"), Some("watchos-simulator") => Some("aarch64-apple-watchos-sim"), + // arm64_32 watchOS (Series 4-8 / SE) when opted in; otherwise arm64 + // (S9+). Governs the rust target used for the auto-optimize runtime + // rebuild and for building/resolving native libraries, so all three + // must agree with the link triple in platform_cmd.rs / link/mod.rs. + Some("watchos") if std::env::var("PERRY_WATCHOS_ARM64_32").is_ok() => { + Some("arm64_32-apple-watchos") + } Some("watchos") => Some("aarch64-apple-watchos"), Some("tvos-simulator") => Some("aarch64-apple-tvos-sim"), Some("tvos") => Some("aarch64-apple-tvos"), diff --git a/crates/perry/src/commands/compile/bundle_apple.rs b/crates/perry/src/commands/compile/bundle_apple.rs index a2f215c146..dd6564ce72 100644 --- a/crates/perry/src/commands/compile/bundle_apple.rs +++ b/crates/perry/src/commands/compile/bundle_apple.rs @@ -173,10 +173,17 @@ pub(super) fn bundle_for_watchos( // #4849: read version/build_number from perry.toml (was hardcoded "1.0"). let (app_version, app_build_number) = read_apple_app_version(input); - // Device builds are arm64-only, which requires watchOS 26 (S9+); the - // simulator target keeps the lower floor. + // Device builds default to arm64-only, which requires watchOS 26 (S9+). + // arm64_32 device builds (PERRY_WATCHOS_ARM64_32) reach pre-S9 watches and + // use a low floor (overridable via PERRY_WATCHOS_MIN); the simulator target + // keeps the lower floor too. + let arm64_32 = target == Some("watchos") && std::env::var("PERRY_WATCHOS_ARM64_32").is_ok(); + let min_os_owned; let min_os = if target == Some("watchos-simulator") { "10.0" + } else if arm64_32 { + min_os_owned = std::env::var("PERRY_WATCHOS_MIN").unwrap_or_else(|_| "11.0".to_string()); + min_os_owned.as_str() } else { "26.0" }; diff --git a/crates/perry/src/commands/compile/link/mod.rs b/crates/perry/src/commands/compile/link/mod.rs index ce5ad94cb4..b0416a2a71 100644 --- a/crates/perry/src/commands/compile/link/mod.rs +++ b/crates/perry/src/commands/compile/link/mod.rs @@ -1762,11 +1762,20 @@ pub(super) fn build_and_run_link( } else { "watchos" }; + // arm64_32 watchOS (Series 4-8 / SE): opt-in, matches the app + // binary's triple in platform_cmd.rs so the native @main lib + // links against the same arch. + let swift_arm64_32 = + target == Some("watchos") && std::env::var("PERRY_WATCHOS_ARM64_32").is_ok(); + let swift_watchos_min = + std::env::var("PERRY_WATCHOS_MIN").unwrap_or_else(|_| "11.0".to_string()); + let swift_triple_owned; let swift_triple = if target == Some("watchos-simulator") { "arm64-apple-watchos10.0-simulator" + } else if swift_arm64_32 { + swift_triple_owned = format!("arm64_32-apple-watchos{}", swift_watchos_min); + swift_triple_owned.as_str() } else { - // Device builds are arm64-only (S9+ / watchOS 26): Perry's - // NaN-boxed values need 64-bit pointers, which arm64_32 lacks. "arm64-apple-watchos26.0" }; let swift_sysroot = String::from_utf8( diff --git a/crates/perry/src/commands/compile/link/platform_cmd.rs b/crates/perry/src/commands/compile/link/platform_cmd.rs index 545dbdee40..b1db7096c2 100644 --- a/crates/perry/src/commands/compile/link/platform_cmd.rs +++ b/crates/perry/src/commands/compile/link/platform_cmd.rs @@ -53,11 +53,19 @@ pub fn select_linker_command( )? .trim() .to_string(); + // arm64_32 watchOS (Series 4-8 / SE): opt-in via PERRY_WATCHOS_ARM64_32. + // Lets the device target reach pre-S9 watches; deployment min defaults + // low (these watches run watchOS 9-11) but is overridable. + let arm64_32 = target == Some("watchos") && std::env::var("PERRY_WATCHOS_ARM64_32").is_ok(); + let watchos_min = std::env::var("PERRY_WATCHOS_MIN").unwrap_or_else(|_| "11.0".to_string()); + let triple_owned; let triple = if target == Some("watchos-simulator") { "arm64-apple-watchos10.0-simulator" + } else if arm64_32 { + triple_owned = format!("arm64_32-apple-watchos{}", watchos_min); + triple_owned.as_str() } else { - // Device builds are arm64-only (S9+ / watchOS 26): Perry's - // NaN-boxed values need 64-bit pointers, which arm64_32 lacks. + // Device builds default to arm64-only (S9+ / watchOS 26). "arm64-apple-watchos26.0" }; @@ -108,7 +116,10 @@ pub fn select_linker_command( .unwrap_or(false) }) }); - if let Some(entry_obj) = entry_obj { + // arm64_32: rust-objcopy crashes on these Mach-O objects, so the entry + // symbol was emitted directly by codegen (PERRY_ENTRY_SYMBOL) instead of + // renamed here. Skip the objcopy pass entirely. + if let Some(entry_obj) = entry_obj.filter(|_| !arm64_32) { let objcopy = std::env::var("HOME").ok() .map(|h| PathBuf::from(h).join(".rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/rust-objcopy")) .filter(|p| p.exists())