Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions changelog.d/7364-apple-target-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
### Tests

**Every Apple target Perry builds for is now pinned in the GC-map emitter, with the address width its ABI actually uses.**

`gc_map.rs` had one ILP32 test (`arm64_32-apple-watchos`) and one LP64 test
(`arm64-apple-ios`). Those pin two triples; nothing pinned the *set*, so adding
a target without deciding its address width failed at someone's link step
rather than in CI. `every_apple_target_is_accepted_with_its_own_address_width`
covers macOS, iOS, iOS-sim, tvOS, visionOS, watchOS (both `arm64` and the ILP32
`arm64_32`) and x86-64 macOS, asserting each is not refused and emits the right
width.

Written while establishing what actually blocks watchOS and visionOS, which is
worth recording because it is **not Perry**:

`cargo check -p perry-runtime` succeeds on stable for macOS, iOS, iOS-sim and
tvOS, and for watchOS and visionOS with any feature set that excludes
`dyn-eval`. With `dyn-eval` — which is in `default` — both fail in `psm`, three
crates away (`dyn-eval` → perry-parser → swc_ecma_parser → stacker → psm). psm
selects its assembly with

```c
#if defined(CFG_TARGET_OS_darwin) || defined(CFG_TARGET_OS_macos) || defined(CFG_TARGET_OS_ios) || defined(CFG_TARGET_OS_tvos)
```

which omits `watchos` and `visionos`, so both fall through to the ELF branch and
emit `.type`/`.size` — directives the Mach-O assembler rejects. Verified by
patching that one line locally: with it, both targets compile with full default
features, including `dyn-eval`.

Two consequences worth stating plainly:

- **watchOS and visionOS worked before and regressed on 2026-07-18**, when
`dyn-eval` was added to `default` (#6584). Nothing about those platforms
changed; a transitive dependency arrived. The release workflow's watchOS note
blames a different cause (a `ring` pointer-size assertion, which is real but
specific to the ILP32 `arm64_32` triple).
- **No release has caught it.** `release-packages.yml` builds these targets with
default features, and its last successful run was 2026-07-04 — before the
regression. The two attempts since were cancelled.

Programs that never call `new Function` with a runtime-built body are
unaffected: the auto-optimize path enables `dyn-eval` per program.
63 changes: 63 additions & 0 deletions crates/perry-codegen/src/gc_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,69 @@ mod tests {
assert!(super::COFF_SECTION_NAME.len() <= 8);
}

/// Every Apple target Perry can build for must be accepted here, with the
/// address width its ABI actually uses.
///
/// This is cheap and it is not redundant with the two width tests above.
/// Those pin one ILP32 target and one LP64 target; this pins the *set*, so
/// adding a triple to the compiler without deciding its width fails here
/// rather than at someone's link step.
///
/// Measured 2026-08-04, `cargo check -p perry-runtime --target <t>`:
/// macOS, iOS, iOS-sim, tvOS, watchOS and visionOS all compile. watchOS and
/// visionOS needed `--no-default-features` (or any feature set without
/// `dyn-eval`) until a third-party fix landed: `psm`, reached only via
/// `dyn-eval` -> perry-parser -> swc_ecma_parser -> stacker, selects its
/// assembly with
///
/// #if defined(CFG_TARGET_OS_darwin) || ..._macos) || ..._ios) || ..._tvos)
///
/// which omits `watchos` and `visionos`, so both fall to the ELF branch and
/// emit `.type`/`.size` that the Mach-O assembler rejects. Nothing in Perry
/// is involved, and the auto-optimize path enables `dyn-eval` only for
/// programs that construct a function body at runtime — so watch and vision
/// apps that never call `new Function` were always buildable.
#[test]
fn every_apple_target_is_accepted_with_its_own_address_width() {
// (triple, expects 64-bit addresses)
let targets = [
("arm64-apple-macosx15.0.0", true),
("arm64-apple-ios", true),
("arm64-apple-ios-sim", true),
("arm64-apple-tvos", true),
("arm64-apple-visionos", true),
("arm64-apple-watchos", true),
// The one ILP32 Apple target. `arm64_32` must be tested before any
// `arm64` prefix match, which is why the emitter checks it first.
("arm64_32-apple-watchos", false),
("x86_64-apple-macosx15.0.0", true),
];
for (target, lp64) in targets {
assert_eq!(
compact_and_assemble_refusal(target),
"",
"{target} must not be refused"
);
let (out, _) = compact_stack_map_asm(&sample_asm(), target)
.unwrap_or_else(|e| panic!("{target} must parse: {e}"))
.unwrap_or_else(|| panic!("{target} must be rewritten"));
let (want, reject) = if lp64 {
("\t.quad\t_probe_fn", "\t.long\t_probe_fn")
} else {
("\t.long\t_probe_fn", "\t.quad\t_probe_fn")
};
assert!(
out.contains(want),
"{target} must emit a {}-bit address field:\n{out}",
if lp64 { 64 } else { 32 }
);
assert!(
!out.contains(reject),
"{target} emitted the wrong address width:\n{out}"
);
}
}

#[test]
fn lp64_targets_keep_the_eight_byte_address_field() {
let (out, _) = compact_stack_map_asm(&sample_asm(), "arm64-apple-ios")
Expand Down
23 changes: 12 additions & 11 deletions crates/perry-codegen/src/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,19 +511,23 @@ fn build_clang_compile_plan(
/// Matched on the ` within ` instruction syntax rather than the bare opcode
/// names so a user string literal containing "catchpad" cannot trip it.
pub(crate) fn rs4gc_funclet_refusal(ll_text: &str) -> Option<String> {
["catchswitch within ", "catchpad within ", "cleanuppad within "]
.iter()
.any(|needle| ll_text.contains(needle))
.then(|| {
"PERRY_RS4GC: this module contains a `try`/`catch` that lowered to \
[
"catchswitch within ",
"catchpad within ",
"cleanuppad within ",
]
.iter()
.any(|needle| ll_text.contains(needle))
.then(|| {
"PERRY_RS4GC: this module contains a `try`/`catch` that lowered to \
WinEH funclet pads (catchswitch/catchpad — the windows-msvc EH \
shape), and LLVM's rewrite-statepoints-for-gc pass does not \
support funclet EH: it crashes with an access violation rather \
than reporting anything. Refusing before the pass runs. \
Compile without PERRY_RS4GC, or keep `try` out of RS4GC-compiled \
modules on Windows. Tracked in #7354."
.to_string()
})
.to_string()
})
}

fn maybe_rs4gc_preprocess(ll_text: &str) -> Result<Option<String>> {
Expand Down Expand Up @@ -584,10 +588,7 @@ fn maybe_rs4gc_preprocess(ll_text: &str) -> Result<Option<String>> {
// left only a symbol-less stack dump. Write the exact input next to
// the other failure artifacts and name it, so the crash is
// reproducible with one command.
let ir_path = env::temp_dir().join(format!(
"perry_rs4gc_failed_{}.ll",
std::process::id()
));
let ir_path = env::temp_dir().join(format!("perry_rs4gc_failed_{}.ll", std::process::id()));
let ir_note = match fs::write(&ir_path, ll_text) {
Ok(()) => format!("input IR left at: {}", ir_path.display()),
Err(error) => format!("(could not write input IR: {error})"),
Expand Down
Loading