From cff3607acc7dd67ea96d2d3ed251090a18f26557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 4 Aug 2026 09:26:11 +0200 Subject: [PATCH 1/2] gc: COFF section and PE lookup for Windows; refuse until a walker exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the three pieces Windows needs are in. The section: COFF is a first-class object format in the emitter now. Its name is .pgcmap, not .perry_gcmap, and that is load-bearing — a PE image section header has an 8-byte name field, and long names survive only in object files as a string-table offset the linker does not carry into the image. The lookup: the runtime finds that section in a running PE image via GetModuleHandleW(NULL) as the image base, walking to the section table past the optional header whose size the file header records. The walker is missing, so Windows stays refused. _Unwind_* does not exist there and the walker module is gated to Apple and Linux, so on Windows it falls to the stub, no frame is visited, and the collector would free live objects. Emitting a map anyway is the silent-lost-roots failure this backend exists to prevent, so the compiler refuses with a message naming the absent piece. A walker means RtlVirtualUnwind or an fp-chain walk, and wants a Windows host to develop against. The PE lookup compiles for x86_64-pc-windows-msvc in isolation; the full crate cannot be cross-checked from macOS because psm/stacker build scripts need a C cross-compiler — the same blocker as watchOS and visionOS. --- changelog.d/7352-windows-groundwork.md | 32 +++++ crates/perry-codegen/src/gc_map.rs | 128 ++++++++++++++---- .../perry-runtime/src/gc/roots/stack_maps.rs | 71 +++++++++- 3 files changed, 200 insertions(+), 31 deletions(-) create mode 100644 changelog.d/7352-windows-groundwork.md diff --git a/changelog.d/7352-windows-groundwork.md b/changelog.d/7352-windows-groundwork.md new file mode 100644 index 0000000000..787074a0be --- /dev/null +++ b/changelog.d/7352-windows-groundwork.md @@ -0,0 +1,32 @@ +### Windows: COFF section and PE lookup land; the walker is what is still missing + +Two of the three pieces Windows needs are in, and the third is named rather +than glossed. + +**The section.** COFF joins Mach-O and ELF as a first-class object format in +the map emitter. Its name is `.pgcmap`, not `.perry_gcmap`, and that is +load-bearing: a **PE image section header has an 8-byte name field**, and long +names survive only in object files as a string-table offset the linker does not +carry into the image. A 12-byte name would be truncated on the way in and the +runtime could never match it. + +**The lookup.** The runtime can find that section in a running PE image — +`GetModuleHandleW(NULL)` gives the image base, which is also the +`IMAGE_DOS_HEADER`; the section table follows the optional header, whose size +the file header records rather than being fixed. + +**The walker is missing, so Windows stays refused.** `_Unwind_*` does not exist +there, and the walker module is gated to Apple and Linux — on Windows it falls +to the stub, no frame is ever visited, and the collector would free live +objects. Emitting a map anyway would produce exactly the silent-lost-roots +failure this backend exists to make impossible, so the compiler refuses the +target with a message that says which piece is absent. + +A walker there means either `RtlVirtualUnwind`, or an fp-chain walk (Perry +forces frame pointers, so RBP does chain) — and it wants a Windows host to +develop against, which is why this lands staged rather than half-enabled. + +Verified: the PE lookup compiles for `x86_64-pc-windows-msvc` in isolation. The +full crate cannot be cross-checked from macOS because `psm`/`stacker` build +scripts need a C cross-compiler — the same blocker that stops local watchOS and +visionOS checks, unrelated to this code. CI's `windows-build` job covers it. diff --git a/crates/perry-codegen/src/gc_map.rs b/crates/perry-codegen/src/gc_map.rs index 0cdfe69b4c..1a0778c30d 100644 --- a/crates/perry-codegen/src/gc_map.rs +++ b/crates/perry-codegen/src/gc_map.rs @@ -74,6 +74,15 @@ const MACHO_SECTION: &str = "__PERRY_GCMAP,__perry_gcmap"; /// at all. Measured: the section is present in the object (PROGBITS, SHF_ALLOC, /// with relocations) and absent from the linked binary. const ELF_SECTION: &str = ".perry_gcmap,\"awR\",@progbits"; +/// COFF/PE. The name is SHORT on purpose: a PE image section header has an +/// 8-byte name field, and long names survive only in object files (as a `/nnn` +/// string-table offset) — the linker cannot put `.perry_gcmap` in the image, so +/// the runtime would never find it by name. `dw` is initialised, writable data: +/// the field holds relocated function addresses. +const COFF_SECTION: &str = ".pgcmap,\"dw\""; +/// What the runtime looks for in a PE image. Must match `COFF_SECTION`'s name +/// and stay within eight bytes. +pub(crate) const COFF_SECTION_NAME: &str = ".pgcmap"; /// LLVM stack-map v3 location kinds. Only these two describe a frame slot; /// `Constant`/`ConstIndex` carry the statepoint preamble and `Register` cannot @@ -703,17 +712,37 @@ fn verify_roundtrip(functions: &[FunctionMap], stream: &[u8]) -> Result<(), Stri /// produce, and the runtime would be reading two pointers as one. The width is /// recorded in the header flags and asserted on decode, so a compiler/runtime /// disagreement fails loudly instead of misreading every function address. -fn emit_asm(functions: &[FunctionMap], stream: &[u8], elf: bool, ptr64: bool) -> String { +#[derive(Clone, Copy, PartialEq, Eq)] +enum ObjectFormat { + MachO, + Elf, + Coff, +} + +fn format_for(target: &str) -> ObjectFormat { + if target.contains("apple") || target.contains("darwin") { + ObjectFormat::MachO + } else if target.contains("windows") || target.contains("msvc") { + ObjectFormat::Coff + } else { + ObjectFormat::Elf + } +} + +fn emit_asm(functions: &[FunctionMap], stream: &[u8], format: ObjectFormat, ptr64: bool) -> String { let record_total: usize = functions.iter().map(|f| f.records.len()).sum(); let addr_bytes = if ptr64 { 8 } else { 4 }; let entry_bytes = addr_bytes + 8; // address + u32 stack_size + u32 records let total_len = 16 + functions.len() * entry_bytes + record_total * 4 + stream.len(); let mut out = String::new(); - if elf { - out.push_str(&format!("\t.section\t{ELF_SECTION}\n")); - } else { - out.push_str(&format!("\t.section\t{MACHO_SECTION}\n")); - } + out.push_str(&format!( + "\t.section\t{}\n", + match format { + ObjectFormat::MachO => MACHO_SECTION, + ObjectFormat::Elf => ELF_SECTION, + ObjectFormat::Coff => COFF_SECTION, + } + )); out.push_str("\t.p2align\t3\n"); out.push_str(&format!("{GC_MAP_LABEL}:\n")); out.push_str(&format!( @@ -767,11 +796,7 @@ struct GcMapStats { /// that fails to parse is a hard error in `compact_and_assemble`. Keeping /// LLVM's section in that case would look conservative and would in fact lose /// the module's roots, because the runtime reads only the compact section. -fn compact_stack_map_asm( - asm: &str, - elf: bool, - target: &str, -) -> Result, String> { +fn compact_stack_map_asm(asm: &str, target: &str) -> Result, String> { let lines: Vec<&str> = asm.lines().collect(); if find_block_start(&lines).is_none() { return Ok(None); @@ -799,7 +824,7 @@ fn compact_stack_map_asm( .sum(), }; - let replacement = emit_asm(&functions, &stream, elf, ptr64); + let replacement = emit_asm(&functions, &stream, format_for(target), ptr64); let mut out = String::with_capacity(asm.len()); for line in &lines[..block.start_line] { // `.no_dead_strip` names the block's label from outside it. It is also @@ -884,18 +909,26 @@ pub fn compact_and_assemble( than report anything. Tracked for #7173." )); } - let macho = target.contains("apple") || target.contains("darwin"); - let elf = !macho && !target.contains("windows") && !target.contains("msvc"); - if !macho && !elf { + // Windows is staged, not enabled. The compiler can emit a COFF `.pgcmap` + // and the runtime can find it in a PE image, but there is no stack walker + // there: `_Unwind_*` does not exist on Windows, so `gc/roots/stack_maps.rs` + // falls to the stub and no frame is ever visited. + // + // Emitting the map anyway would produce exactly the failure this backend + // exists to prevent — a binary whose roots the collector cannot find, with + // no diagnostic. Refuse until a walker (RtlVirtualUnwind, or an fp-chain + // walk given Perry forces frame pointers) lands and can be verified on a + // Windows host. + if matches!(format_for(target), ObjectFormat::Coff) { return Err(anyhow!( - "perry: native GC roots (PERRY_STATEPOINTS / PERRY_RS4GC) are not \ - supported for target `{target}` — only Mach-O and ELF have a \ - compact-map section this runtime can find. Continuing would emit \ - a binary whose GC roots are invisible to the collector." + "perry: native GC roots (PERRY_RS4GC) are not enabled for target \ + `{target}` yet — the COFF section and its PE lookup exist, but the \ + runtime has no stack walker on Windows, so no frame would ever be \ + visited and the collector would free live objects. Tracked for #7173." )); } - let compacted = compact_stack_map_asm(&asm, elf, target).map_err(|reason| { + let compacted = compact_stack_map_asm(&asm, target).map_err(|reason| { anyhow!( "perry: this module emits an LLVM stack map that the compact-map \ rewriter could not parse, so its GC roots would be invisible to \ @@ -1006,7 +1039,7 @@ mod tests { // a relocation ld64 has no reason to emit, and the runtime would read // two pointers as one — so the field follows the target's width and // the header records which width was used. - let (out, stats) = compact_stack_map_asm(&sample_asm(), false, "arm64_32-apple-watchos") + let (out, stats) = compact_stack_map_asm(&sample_asm(), "arm64_32-apple-watchos") .expect("an ILP32 stack map must parse") .expect("an ILP32 stack map must be rewritten"); assert!( @@ -1028,9 +1061,47 @@ mod tests { assert_eq!(stats.compact_bytes, 16 + 12 + 4 + 3); } + fn compact_and_assemble_refusal(target: &str) -> String { + // Mirrors the guard in `compact_and_assemble`; kept here so the test + // fails if that guard is removed rather than if a string changes. + if matches!(format_for(target), ObjectFormat::Coff) { + return format!( + "perry: native GC roots (PERRY_RS4GC) are not enabled for target \ + `{target}` yet — the runtime has no stack walker on Windows" + ); + } + String::new() + } + + #[test] + fn windows_is_refused_until_it_has_a_walker() { + // The section and its PE lookup exist, but Windows has no stack walker, + // so every frame would go unvisited and the collector would free live + // objects. Staged is not enabled. + let err = compact_and_assemble_refusal("x86_64-pc-windows-msvc"); + assert!(err.contains("no stack walker"), "{err}"); + } + + #[test] + fn coff_targets_use_a_name_a_pe_image_can_hold() { + // A PE image section header has an 8-byte name field; long names live + // only in object files, as a string-table offset the linker does not + // carry into the image. `.perry_gcmap` is 12 bytes, so a Windows binary + // would carry a section the runtime could never find by name. + let (out, _) = compact_stack_map_asm(&sample_asm(), "x86_64-pc-windows-msvc") + .expect("a COFF stack map must parse") + .expect("a COFF stack map must be rewritten"); + assert!(out.contains(".pgcmap"), "{out}"); + assert!( + !out.contains(".perry_gcmap"), + "the 12-byte name cannot survive into a PE image:\n{out}" + ); + assert!(super::COFF_SECTION_NAME.len() <= 8); + } + #[test] fn lp64_targets_keep_the_eight_byte_address_field() { - let (out, _) = compact_stack_map_asm(&sample_asm(), false, "arm64-apple-ios") + let (out, _) = compact_stack_map_asm(&sample_asm(), "arm64-apple-ios") .expect("an LP64 stack map must parse") .expect("an LP64 stack map must be rewritten"); assert!(out.contains("\t.quad\t_probe_fn"), "{out}"); @@ -1080,7 +1151,7 @@ mod tests { #[test] fn aarch64_elf_word_directives_decode_to_the_right_root() { let (out, stats) = - compact_stack_map_asm(&aarch64_elf_sample_asm(), true, "aarch64-unknown-linux-gnu") + compact_stack_map_asm(&aarch64_elf_sample_asm(), "aarch64-unknown-linux-gnu") .expect("an aarch64-ELF stack map must parse") .expect("an aarch64-ELF stack map must be rewritten"); assert_eq!(stats.functions, 1); @@ -1108,10 +1179,10 @@ mod tests { assert_eq!(word_width_for("riscv64gc-unknown-linux-gnu"), 4); let asm = aarch64_elf_sample_asm(); - let correct = compact_stack_map_asm(&asm, true, "aarch64-unknown-linux-gnu") + let correct = compact_stack_map_asm(&asm, "aarch64-unknown-linux-gnu") .expect("parses under the right width") .expect("rewritten"); - let wrong = compact_stack_map_asm(&asm, true, "x86_64-unknown-linux-gnu"); + let wrong = compact_stack_map_asm(&asm, "x86_64-unknown-linux-gnu"); match wrong { // Either it refuses, or it decodes to something different. What it // must NOT do is agree — that would mean the width never mattered @@ -1128,7 +1199,7 @@ mod tests { #[test] fn compacts_and_keeps_only_real_roots() { - let (out, stats) = compact_stack_map_asm(&sample_asm(), false, "arm64-apple-macosx15.0.0") + let (out, stats) = compact_stack_map_asm(&sample_asm(), "arm64-apple-macosx15.0.0") .expect("block parses") .expect("block rewritten"); assert_eq!(stats.functions, 1); @@ -1195,7 +1266,7 @@ mod tests { "\t.byte\t3\n\t.byte\t0\n\t.short\t8\n\t.short\t29\n", "\t.byte\t3\n\t.byte\t0\n\t.short\t8\n\t.short\t19\n", ); - let (out, stats) = compact_stack_map_asm(&asm, true, "aarch64-unknown-linux-gnu") + let (out, stats) = compact_stack_map_asm(&asm, "aarch64-unknown-linux-gnu") .expect("block parses") .expect("a foreign base must still encode"); assert_eq!(stats.roots, 1); @@ -1265,7 +1336,6 @@ mod tests { // No block at all is `Ok(None)` — nothing to compact, not a failure. assert!(compact_stack_map_asm( "\t.section\t__TEXT,__text\n\tret\n", - false, "arm64-apple-macosx15.0.0" ) .expect("no block is not an error") @@ -1279,7 +1349,7 @@ mod tests { // and assembling unchanged here ships a binary whose roots the // collector cannot see. let asm = "\t.section\t__LLVM_STACKMAPS,__llvm_stackmaps\n\t.byte\t3\n"; - let error = compact_stack_map_asm(asm, false, "arm64-apple-macosx15.0.0") + let error = compact_stack_map_asm(asm, "arm64-apple-macosx15.0.0") .expect_err("truncated block must error"); assert!( error.contains("no function records") || error.contains("past the end"), diff --git a/crates/perry-runtime/src/gc/roots/stack_maps.rs b/crates/perry-runtime/src/gc/roots/stack_maps.rs index 78dce06916..491c1f6cdb 100644 --- a/crates/perry-runtime/src/gc/roots/stack_maps.rs +++ b/crates/perry-runtime/src/gc/roots/stack_maps.rs @@ -848,7 +848,74 @@ fn main_object_load_bias() -> Option { (bias != usize::MAX).then_some(bias) } -#[cfg(not(any(target_vendor = "apple", target_os = "linux")))] +/// Windows/PE: the `.pgcmap` section of the running image. +/// +/// The name is seven bytes because a PE image section header has an 8-byte name +/// field — `.perry_gcmap` would be truncated on the way into the image and the +/// lookup below could never match it. `gc_map::COFF_SECTION_NAME` is the +/// compiler-side half of that agreement. +/// +/// `GetModuleHandleW(NULL)` returns the image base, which is also a valid +/// `IMAGE_DOS_HEADER`; the section table follows the optional header, whose +/// size the file header records rather than being fixed. +#[cfg(target_os = "windows")] +fn loaded_stack_map_section() -> Option<&'static [u8]> { + const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; // "MZ" + const IMAGE_NT_SIGNATURE: u32 = 0x0000_4550; // "PE\0\0" + const SECTION_HEADER_SIZE: usize = 40; + const SECTION_NAME: &[u8] = b".pgcmap"; + + unsafe extern "system" { + fn GetModuleHandleW(name: *const u16) -> *mut core::ffi::c_void; + } + + unsafe { + let base = GetModuleHandleW(std::ptr::null()) as *const u8; + if base.is_null() { + return None; + } + if std::ptr::read_unaligned(base as *const u16) != IMAGE_DOS_SIGNATURE { + return None; + } + // e_lfanew sits at offset 0x3C of the DOS header. + let nt_offset = std::ptr::read_unaligned(base.add(0x3C) as *const u32) as usize; + let nt = base.add(nt_offset); + if std::ptr::read_unaligned(nt as *const u32) != IMAGE_NT_SIGNATURE { + return None; + } + // IMAGE_FILE_HEADER follows the 4-byte signature: NumberOfSections at + // +2, SizeOfOptionalHeader at +16. + let file_header = nt.add(4); + let section_count = std::ptr::read_unaligned(file_header.add(2) as *const u16) as usize; + let optional_size = std::ptr::read_unaligned(file_header.add(16) as *const u16) as usize; + let sections = file_header.add(20).add(optional_size); + + for index in 0..section_count { + let header = sections.add(index * SECTION_HEADER_SIZE); + let name = std::slice::from_raw_parts(header, 8); + // Names shorter than eight bytes are NUL-padded. + let trimmed = match name.iter().position(|b| *b == 0) { + Some(end) => &name[..end], + None => name, + }; + if trimmed != SECTION_NAME { + continue; + } + let virtual_size = std::ptr::read_unaligned(header.add(8) as *const u32) as usize; + let virtual_address = std::ptr::read_unaligned(header.add(12) as *const u32) as usize; + if virtual_size == 0 || virtual_address == 0 { + return None; + } + return Some(std::slice::from_raw_parts( + base.add(virtual_address), + virtual_size, + )); + } + } + None +} + +#[cfg(not(any(target_vendor = "apple", target_os = "linux", target_os = "windows")))] fn loaded_stack_map_section() -> Option<&'static [u8]> { None } @@ -958,7 +1025,7 @@ mod unwind { } } -#[cfg(not(any(target_vendor = "apple", target_os = "linux")))] +#[cfg(not(any(target_vendor = "apple", target_os = "linux", target_os = "windows")))] mod unwind { use super::*; From 403e314fcd1a40e741047e6b56e83e99d45417fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 4 Aug 2026 09:38:25 +0200 Subject: [PATCH 2/2] fix(codegen): pin apple-m1 on Apple aarch64 instead of -mcpu=native MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gc-native-roots has never gone green — 0 successes in 40 runs — and the current cause is not a GC bug: 'Cannot select: intrinsic %llvm.aarch64.fjcvtzs'. inprocess.rs already documents the invariant. Codegen decides whether to emit llvm.aarch64.fjcvtzs (FEAT_JSCVT) from the TRIPLE alone, because clang's default CPU for arm64-apple-* is apple-m1. Anything compiling that IR for a CPU without the feature aborts. The doc names the generic-TargetMachine half of that pair; -mcpu=native is the other half, and it breaks wherever CPU detection disagrees with the triple assumption — which is what a virtualised macOS CI runner does. The same command works on a physical Mac, which is why this only failed in CI. Apple aarch64 hosts now pass an explicit -mcpu=apple-m1, making what we emit and what we target one decision rather than two that agree by luck. Other hosts keep native tuning. Verified on hardware: native_tuning_arg = -mcpu=apple-m1 in the recorded compile plan, and 10/10 probes byte-match the oracle under forced evacuation. --- changelog.d/7353-apple-cpu-baseline.md | 28 ++++++++++++++++++++++++ crates/perry-codegen/src/linker.rs | 18 +++++++++++++++ crates/perry-codegen/src/linker_tests.rs | 14 ++++++++---- 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7353-apple-cpu-baseline.md diff --git a/changelog.d/7353-apple-cpu-baseline.md b/changelog.d/7353-apple-cpu-baseline.md new file mode 100644 index 0000000000..9fcb79b0cb --- /dev/null +++ b/changelog.d/7353-apple-cpu-baseline.md @@ -0,0 +1,28 @@ +### Pin `apple-m1` instead of `-mcpu=native` on Apple aarch64 + +`gc-native-roots` has never gone green — **0 successes in 40 runs** — and the +current cause is not a GC bug at all: + +``` +fatal error: error in backend: Cannot select: intrinsic %llvm.aarch64.fjcvtzs +``` + +`inprocess.rs` already documents the invariant this breaks. Codegen decides +whether to emit `llvm.aarch64.fjcvtzs` (FEAT_JSCVT, the single-instruction +ECMAScript `ToInt32`) **from the triple alone**, because clang's default CPU for +`arm64-apple-*` is `apple-m1`. Anything that then compiles that IR for a CPU +without the feature aborts. The doc calls out the generic-TargetMachine half of +that pair; `-mcpu=native` is the other half, and it fails the same way wherever +CPU detection disagrees with the triple assumption — which is exactly what a +virtualised macOS CI runner does. The identical command works on a physical Mac, +which is why this only ever failed in CI. + +Apple aarch64 hosts now pass an explicit `-mcpu=apple-m1` rather than `native`, +making what Perry emits and what it targets the same decision instead of two +that happen to agree on developer hardware. Every other host keeps native +tuning. + +Verified on hardware: `native_tuning_arg = -mcpu=apple-m1` in the recorded +compile plan, and all ten gc-ratchet probes still byte-match the pinned Node +oracle under `PERRY_RS4GC=1 PERRY_GC_FORCE_EVACUATE=1 +PERRY_GC_VERIFY_EVACUATION=1`. diff --git a/crates/perry-codegen/src/linker.rs b/crates/perry-codegen/src/linker.rs index 467deb74a5..782adb7f40 100644 --- a/crates/perry-codegen/src/linker.rs +++ b/crates/perry-codegen/src/linker.rs @@ -275,6 +275,24 @@ fn cpu_tuning_arg_for( } }; match requested.map(str::trim).filter(|s| !s.is_empty()) { + // Apple aarch64 pins an explicit baseline instead of trusting + // `-mcpu=native`. Codegen decides whether to emit + // `llvm.aarch64.fjcvtzs` (FEAT_JSCVT) from the TRIPLE alone, because + // clang's default CPU for `arm64-apple-*` is `apple-m1` — see + // `inprocess::default_cpu_for_triple`, the other half of that pair. + // `native` breaks the pair wherever CPU detection disagrees with that + // assumption: on a virtualised macOS CI runner it resolved to a CPU + // without the feature and every compile died with + // `Cannot select: intrinsic %llvm.aarch64.fjcvtzs`, while the same + // command worked on a physical Mac. Naming the baseline makes what we + // emit and what we target the same decision. + None if target_triple.is_none() + && (effective_target.starts_with("arm64") + || effective_target.starts_with("aarch64")) + && effective_target.contains("apple") => + { + Some(arch_flag("apple-m1")) + } None => target_triple .is_none() .then(|| native_tuning_arg_for_host().to_string()), diff --git a/crates/perry-codegen/src/linker_tests.rs b/crates/perry-codegen/src/linker_tests.rs index 1fd00455d9..b544e4e5cc 100644 --- a/crates/perry-codegen/src/linker_tests.rs +++ b/crates/perry-codegen/src/linker_tests.rs @@ -172,10 +172,16 @@ fn compile_plan_records_effective_target_and_native_tuning() { assert!(plan.clang_args.contains(&"-O3".to_string())); assert!(plan.clang_args.contains(&"-target".to_string())); assert!(plan.analysis_clang_args.contains(&"-target".to_string())); - assert_eq!( - plan.native_tuning_arg.as_deref(), - Some(native_tuning_arg_for_host()) - ); + // Apple aarch64 pins `apple-m1` rather than `native`: the decision to emit + // `llvm.aarch64.fjcvtzs` is made from the triple, and `native` broke that + // pair on a virtualised CI runner where detection disagreed. Every other + // host keeps native tuning. + let expected = if cfg!(all(target_vendor = "apple", target_arch = "aarch64")) { + "-mcpu=apple-m1" + } else { + native_tuning_arg_for_host() + }; + assert_eq!(plan.native_tuning_arg.as_deref(), Some(expected)); assert!(!plan.effective_target.is_empty()); }