diff --git a/CLAUDE.md b/CLAUDE.md index 38f8caed45..d53967e24a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 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. -**Current Version:** 0.5.1616 +**Current Version:** 0.5.1617 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index e43fec73e1..1899e6c64f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5565,7 +5565,7 @@ checksum = "1473d470930ed48574515a25df34900f3af89c6fa422d903e019121312a9f13e" [[package]] name = "perry" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "base64 0.22.1", @@ -5629,7 +5629,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-dispatch", "serde", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "cc", "libc", @@ -5646,7 +5646,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "aho-corasick", "anyhow", @@ -5663,7 +5663,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-hir", @@ -5671,7 +5671,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-hir", @@ -5679,7 +5679,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-dispatch", @@ -5688,7 +5688,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-hir", @@ -5696,7 +5696,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "base64 0.22.1", @@ -5708,7 +5708,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-hir", @@ -5716,7 +5716,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "async-trait", "clap", @@ -5740,14 +5740,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "serde", "serde_json", @@ -5755,7 +5755,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1616" +version = "0.5.1617" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "clap", @@ -5781,7 +5781,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "block2", "objc2", @@ -5791,7 +5791,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "argon2", "perry-ffi", @@ -5800,7 +5800,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "bcrypt", "perry-ffi", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "rusqlite", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "scraper", @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "chrono", "cron", @@ -5834,7 +5834,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "rust_decimal", @@ -5842,7 +5842,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5850,7 +5850,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "perry-runtime", @@ -5858,14 +5858,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fetch" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "bytes", "lazy_static", @@ -5878,7 +5878,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "bytes", @@ -5910,7 +5910,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "lazy_static", "perry-ffi", @@ -5920,7 +5920,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "chrono", "perry-ffi", @@ -5928,7 +5928,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "bson", "futures-util", @@ -5940,7 +5940,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "chrono", "perry-ffi", @@ -5952,7 +5952,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "bytes", "perry-ffi", @@ -5967,7 +5967,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "lettre", "perry-ffi", @@ -5996,7 +5996,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "notify", "perry-ffi", @@ -6008,7 +6008,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "printpdf", @@ -6016,7 +6016,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "sqlx", @@ -6025,7 +6025,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "fast_image_resize", "image", @@ -6036,7 +6036,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "lazy_static", "perry-ffi", @@ -6045,7 +6045,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-ffi", @@ -6065,7 +6065,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-ffi", "perry-runtime", @@ -6074,7 +6074,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "futures-util", "lazy_static", @@ -6087,7 +6087,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "brotli", "flate2", @@ -6097,7 +6097,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6107,7 +6107,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-api-manifest", @@ -6127,11 +6127,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1616" +version = "0.5.1617" [[package]] name = "perry-parser" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "perry-diagnostics", @@ -6144,7 +6144,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perex", "regex", @@ -6152,7 +6152,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "ahash", "base64 0.22.1", @@ -6210,14 +6210,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6301,21 +6301,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "dirs", "perry-ffi", @@ -6325,7 +6325,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "jni", @@ -6340,7 +6340,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "rand 0.10.2", "serde", @@ -6350,7 +6350,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6373,7 +6373,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "block2", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "block2", @@ -6407,7 +6407,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1616" +version = "0.5.1617" [[package]] name = "perry-ui-test" @@ -6418,11 +6418,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1616" +version = "0.5.1617" [[package]] name = "perry-ui-tvos" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "block2", @@ -6439,7 +6439,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "block2", @@ -6456,7 +6456,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "block2", "libc", @@ -6470,7 +6470,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "libc", @@ -6489,7 +6489,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "base64 0.22.1", "libc", @@ -6502,7 +6502,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "anyhow", "base64 0.22.1", @@ -6517,7 +6517,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1616" +version = "0.5.1617" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index e95e318f8a..d1d68ef65f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -321,7 +321,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1616" +version = "0.5.1617" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/10779-fcmp-operand-type.md b/changelog.d/10779-fcmp-operand-type.md new file mode 100644 index 0000000000..3a6b2ac05b --- /dev/null +++ b/changelog.d/10779-fcmp-operand-type.md @@ -0,0 +1,7 @@ +**`Buffer.readFloatLE` and `readFloatBE` compile again.** + +The NaN canonicaliser added for #10779 emits an `fcmp` on an `f32` lane, but `LlBlock::fcmp` rendered its operand type as `double` unconditionally and the in-process LLVM builder hardcoded the same. Any module calling `Buffer.readFloatLE` or `readFloatBE` therefore failed to compile with `'%r1' defined with type 'float' but expected 'double'`. + +`LlInst::FCmp` now carries its operand type. `fcmp()` keeps its `double` signature and delegates to a new `fcmp_ty()`, so no existing call site changes. + +Also closes the remaining raw float source: NaNs arriving from native code. A C `float` return previously segfaulted — the forged `StringHeader*` was dereferenced — and a C `double` return printed its payload integer. The `double` arm is gated strictly on the manifest declaring `F64`, because it also serves perry's own double ABI where the value already *is* a NaN box. diff --git a/crates/perry-codegen/src/block.rs b/crates/perry-codegen/src/block.rs index 65ace3da66..7de1bf8b1f 100644 --- a/crates/perry-codegen/src/block.rs +++ b/crates/perry-codegen/src/block.rs @@ -561,10 +561,18 @@ impl LlBlock { /// Float comparison. `cond` is an LLVM predicate string: `olt`, `ole`, /// `ogt`, `oge`, `oeq`, `one`, `ord`, `uno`, … pub fn fcmp(&mut self, cond: &str, a: &str, b: &str) -> String { + self.fcmp_ty(crate::types::DOUBLE, cond, a, b) + } + + /// `fcmp` on an operand type other than `double` — a `float` in the + /// native lattice, above all. The untyped [`Self::fcmp`] above assumes + /// `double`; calling it on a `float` emits IR LLVM rejects. + pub fn fcmp_ty(&mut self, ty: LlvmType, cond: &str, a: &str, b: &str) -> String { let r = self.reg(); self.push_inst(crate::inst::LlInst::FCmp { dst: r.clone(), pred: cond.to_string(), + ty, a: a.to_string(), b: b.to_string(), }); diff --git a/crates/perry-codegen/src/dialect/mod.rs b/crates/perry-codegen/src/dialect/mod.rs index 542afdea61..d34c83d39c 100644 --- a/crates/perry-codegen/src/dialect/mod.rs +++ b/crates/perry-codegen/src/dialect/mod.rs @@ -1352,8 +1352,11 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> { } self.def(dst, out) } - I::FCmp { dst, pred, a, b } => { - let t = basic_type(self.ctx, "double")?; + // #10779: `ty` was hardcoded `"double"` here and in `inst.rs`. + // Kept on one line: this file sits against the 2000-line gate. + #[rustfmt::skip] + I::FCmp { dst, pred, ty, a, b } => { + let t = basic_type(self.ctx, ty)?; let av = self.val(t, a)?; let bv = self.val(t, b)?; let out: BasicValueEnum = self diff --git a/crates/perry-codegen/src/expr/nanbox_inline.rs b/crates/perry-codegen/src/expr/nanbox_inline.rs index 713e0da74b..16aaabcb31 100644 --- a/crates/perry-codegen/src/expr/nanbox_inline.rs +++ b/crates/perry-codegen/src/expr/nanbox_inline.rs @@ -26,8 +26,9 @@ pub(crate) fn nanbox_canon_enabled() -> bool { }) } -/// #10779: collapse any NaN in a float lane just loaded from ArrayBuffer-backed -/// memory to the canonical quiet NaN, so it cannot alias a NaN-box tag. +/// #10779: collapse any NaN in a raw native `f64` — an ArrayBuffer float lane, +/// a POD record field, or a C function's `double` return — to the canonical +/// quiet NaN, so it cannot alias a NaN-box tag. /// /// The runtime twin is `perry_runtime::array::canonical_raw_f64`, whose doc /// comment carries the full argument for why EVERY NaN must be collapsed and @@ -64,7 +65,10 @@ pub(crate) fn canonicalize_lane_f32(blk: &mut LlBlock, value: &str) -> String { if !nanbox_canon_enabled() { return value.to_string(); } - let is_nan = blk.fcmp("uno", value, value); + // MUST be `fcmp uno float`, not the `double` default: the operand is an + // f32 in the native lattice. Emitting `double` here made every program + // calling `Buffer.readFloatLE` fail codegen. + let is_nan = blk.fcmp_ty(F32, "uno", value, value); blk.select(I1, &is_nan, F32, CANONICAL_QNAN_DOUBLE, value) } @@ -118,3 +122,93 @@ pub(crate) fn i32_to_nanbox(blk: &mut LlBlock, i32_val: &str) -> String { let tagged = blk.or(I64, &payload, INT32_TAG_I64); blk.bitcast_i64_to_double(&tagged) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::block::{LlBlock, RegCounter}; + use crate::inst::LlInst; + use crate::types::DOUBLE; + use std::rc::Rc; + + fn blk() -> LlBlock { + LlBlock::new("t", Rc::new(RegCounter::new())) + } + + /// #10779 follow-up. `LlBlock::fcmp` renders its operand type as `double` + /// unconditionally, so canonicalising an f32 lane through it emitted + /// `fcmp uno double %f32` — IR LLVM rejects with + /// "'%r' defined with type 'float' but expected 'double'". Every program + /// calling `Buffer.readFloatLE` failed codegen, and no fixture in the + /// suite called it, so nothing caught it. + /// + /// The sabotage is one word: change `F32` back to `DOUBLE` in + /// `canonicalize_lane_f32` and this test fails, naming the emitted type. + #[test] + fn f32_canonicalisation_compares_as_float_not_double() { + let mut b = blk(); + let out = canonicalize_lane_f32(&mut b, "%x"); + assert_ne!(out, "%x", "the f32 lane must actually be canonicalised"); + let fcmp = b + .insts() + .iter() + .find_map(|i| match i { + LlInst::FCmp { pred, ty, .. } => Some((pred.clone(), *ty)), + _ => None, + }) + .expect("canonicalize_lane_f32 must emit an fcmp"); + assert_eq!(fcmp.0, "uno", "the NaN test must be an unordered compare"); + assert_eq!( + fcmp.1, F32, + "an f32 lane must be compared AS float; `double` here is IR LLVM \ + rejects and it broke every `Buffer.readFloatLE` call site" + ); + } + + /// The f64 twin, so a future edit cannot fix the f32 case by widening + /// both to `float`. + #[test] + fn f64_canonicalisation_compares_as_double() { + let mut b = blk(); + let out = canonicalize_lane_f64(&mut b, "%x"); + assert_ne!(out, "%x"); + let ty = b + .insts() + .iter() + .find_map(|i| match i { + LlInst::FCmp { ty, .. } => Some(*ty), + _ => None, + }) + .expect("canonicalize_lane_f64 must emit an fcmp"); + assert_eq!(ty, DOUBLE); + } + + /// Both canonicalisers must select the SAME canonical quiet NaN, spelled + /// in LLVM's hex double form so the payload cannot be rounded away, and + /// must select it on the TRUE (is-NaN) arm. + #[test] + fn both_select_the_canonical_quiet_nan_on_the_nan_arm() { + for (name, want_ty) in [("f64", DOUBLE), ("f32", F32)] { + let mut b = blk(); + if name == "f64" { + let _ = canonicalize_lane_f64(&mut b, "%x"); + } else { + let _ = canonicalize_lane_f32(&mut b, "%x"); + } + let sel = b + .insts() + .iter() + .find_map(|i| match i { + LlInst::Select { ty, a, b: fb, .. } => Some((*ty, a.clone(), fb.clone())), + _ => None, + }) + .unwrap_or_else(|| panic!("{name} must emit a select")); + assert_eq!(sel.0, want_ty, "{name} select operand type"); + assert_eq!( + sel.1, CANONICAL_QNAN_DOUBLE, + "{name} must pick the canonical quiet NaN when the value IS a NaN" + ); + assert_eq!(sel.2, "%x", "{name} must pass a non-NaN through unchanged"); + } + } +} diff --git a/crates/perry-codegen/src/expr/pod_record.rs b/crates/perry-codegen/src/expr/pod_record.rs index 8070f9878d..62476825a4 100644 --- a/crates/perry-codegen/src/expr/pod_record.rs +++ b/crates/perry-codegen/src/expr/pod_record.rs @@ -480,6 +480,15 @@ pub(crate) fn load_pod_field_native( let llvm_ty = llvm_type_for_native_rep(&field.native_rep).expect("pod field reps have scalar LLVM types"); let value = ctx.block().load_aligned(llvm_ty, &ptr, field.alignment); + // #10779: a POD record's backing memory is a native struct — written by C, + // by Rust, or by a previous native store — so its float fields can hold any + // NaN, including one whose bits alias a NaN-box tag. The integer field reps + // cannot be NaN and pay nothing. + let value = match field.native_rep { + NativeRep::F64 => crate::expr::nanbox_inline::canonicalize_lane_f64(ctx.block(), &value), + NativeRep::F32 => crate::expr::nanbox_inline::canonicalize_lane_f32(ctx.block(), &value), + _ => value, + }; let lowered = LoweredValue { semantic: SemanticKind::JsNumber, rep: field.native_rep.clone(), diff --git a/crates/perry-codegen/src/inst.rs b/crates/perry-codegen/src/inst.rs index c4859f17c8..566a331405 100644 --- a/crates/perry-codegen/src/inst.rs +++ b/crates/perry-codegen/src/inst.rs @@ -69,6 +69,11 @@ pub enum LlInst { FCmp { dst: String, pred: String, + /// Operand type. This USED to be hardcoded `double` at the render + /// site, which silently produced invalid IR for a `float` operand + /// (#10779 follow-up: `Buffer.readFloatLE` failed codegen with + /// "'%r' defined with type 'float' but expected 'double'"). + ty: LlvmType, a: String, b: String, }, @@ -193,8 +198,14 @@ impl LlInst { LlInst::FNeg { dst, pre, a } => { let _ = write!(out, " {dst} = fneg {pre}double {a}"); } - LlInst::FCmp { dst, pred, a, b } => { - let _ = write!(out, " {dst} = fcmp {pred} double {a}, {b}"); + LlInst::FCmp { + dst, + pred, + ty, + a, + b, + } => { + let _ = write!(out, " {dst} = fcmp {pred} {ty} {a}, {b}"); } LlInst::ICmp { dst, diff --git a/crates/perry-codegen/src/lower_call/extern_func.rs b/crates/perry-codegen/src/lower_call/extern_func.rs index bbbee2bdb7..fee3c71858 100644 --- a/crates/perry-codegen/src/lower_call/extern_func.rs +++ b/crates/perry-codegen/src/lower_call/extern_func.rs @@ -1769,6 +1769,10 @@ pub fn try_lower_extern_func_call( } else if returns_f32 { ctx.pending_declares.push((name.clone(), F32, arg_types)); let raw = ctx.block().call(F32, name, &arg_slices); + // #10779: a C `float` return is raw native bits; an f32 NaN + // widens KEEPING its payload (0x7FFFFFFF -> a forged + // StringHeader*). Canonicalise before `materialize_js_value`. + let raw = crate::expr::nanbox_inline::canonicalize_lane_f32(ctx.block(), &raw); let lowered = LoweredValue::f32(raw.clone()); if let Some(descriptor) = manifest_ret { record_native_abi_return(ctx, descriptor, &lowered, name); @@ -1812,6 +1816,16 @@ pub fn try_lower_extern_func_call( // return value directly (no sitofp needed). ctx.pending_declares.push((name.clone(), DOUBLE, arg_types)); let raw = ctx.block().call(DOUBLE, name, &arg_slices); + // #10779: this arm serves BOTH a C `double` return (raw native + // bits) and Perry's own double ABI (already a NaN box, whose tags + // canonicalising would destroy). Gate strictly on the manifest + // saying `F64`; `JsValue` or no descriptor is left untouched. + let is_native_f64 = matches!(manifest_ret, Some(NativeAbiType::F64)); + let raw = if is_native_f64 { + crate::expr::nanbox_inline::canonicalize_lane_f64(ctx.block(), &raw) + } else { + raw + }; if let Some(descriptor) = manifest_ret { let lowered = if matches!(descriptor, NativeAbiType::JsValue) { LoweredValue::js_value(raw.clone())