From a0f6135e856472774c01ef2ce6a40aa8d0a7d9c9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 28 Jul 2026 07:52:12 -0700 Subject: [PATCH 1/2] testFFI: compare NaN returns semantically in the invoke-thunk differential The invoke-thunk differential calls each fixture two ways -- through the generated thunk and through a templated reference call -- with identical argument slots, and compared the raw return slots bitwise. Fixtures like ffi_mix_1 return a sum over their arguments, so when two argument NaNs are in play (the f64 sNaN slot and the f32 sNaN slot) the result's NaN payload is the payload of whichever operand is 'first' in an addsd. Addition is commutative, so the compiler may legally flip operand order, and it does so differently for the inlined reference call under -O2 than for the outlined body reached through the thunk. That surfaced as 0x7ff8000020000000 != 0x7ff8000000000001 on x86-64 release only: same delivered arguments, same body, different-but-legal instruction operand order. The identical binary passes under Rosetta 2, which does not emulate x86 NaN-payload propagation -- consistent with a payload artifact rather than a marshalling error. NaN payloads are implementation-defined, so treat two NaN return slots of the same type as equal. Real value mismatches, NaN-vs-number, and infinities still compare bitwise. --- Source/JavaScriptCore/ffi/tests/testFFI.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/ffi/tests/testFFI.cpp b/Source/JavaScriptCore/ffi/tests/testFFI.cpp index 5724198e1470c..e89f0fc79163c 100644 --- a/Source/JavaScriptCore/ffi/tests/testFFI.cpp +++ b/Source/JavaScriptCore/ffi/tests/testFFI.cpp @@ -1527,7 +1527,12 @@ static void differentialCase(ASCIILiteral name, R (*function)(Arguments...), Vec R nativeResult = callNativeFromSlots(function, nativeSlots.span().data()); uint64_t expected = canonicalizeSlot(returnType, nativeReturnRawBits(nativeResult)); uint64_t actual = slots[argumentCount]; - ok &= actual == expected; + bool returnsEqual = actual == expected; + if (!returnsEqual && returnType == FFI::Type::Double) + returnsEqual = std::isnan(std::bit_cast(actual)) && std::isnan(std::bit_cast(expected)); + else if (!returnsEqual && returnType == FFI::Type::Float) + returnsEqual = std::isnan(std::bit_cast(static_cast(actual))) && std::isnan(std::bit_cast(static_cast(expected))); + ok &= returnsEqual; if (!ok) { s_failureCount++; dataLogLn(" FAIL: invoke-thunk differential for ", name.characters(), " (", signature->toString(), ") iteration ", iteration, From f8f2eb7f0f7b14042791b2e745e1276a159833b6 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 28 Jul 2026 08:28:45 -0700 Subject: [PATCH 2/2] testFFI: gate the NaN-equal fallback on multi-argument fixtures The operand-order nondeterminism that justifies treating two NaN return slots as equal only exists when two or more argument NaNs are in play. For zero/one-argument fixtures the bitwise check is sound and would catch a future thunk that quiets an SNaN or canonicalizes a payload, so apply the fallback only when sizeof...(Arguments) >= 2. --- Source/JavaScriptCore/ffi/tests/testFFI.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/JavaScriptCore/ffi/tests/testFFI.cpp b/Source/JavaScriptCore/ffi/tests/testFFI.cpp index e89f0fc79163c..803dccd939b2a 100644 --- a/Source/JavaScriptCore/ffi/tests/testFFI.cpp +++ b/Source/JavaScriptCore/ffi/tests/testFFI.cpp @@ -1528,10 +1528,13 @@ static void differentialCase(ASCIILiteral name, R (*function)(Arguments...), Vec uint64_t expected = canonicalizeSlot(returnType, nativeReturnRawBits(nativeResult)); uint64_t actual = slots[argumentCount]; bool returnsEqual = actual == expected; - if (!returnsEqual && returnType == FFI::Type::Double) - returnsEqual = std::isnan(std::bit_cast(actual)) && std::isnan(std::bit_cast(expected)); - else if (!returnsEqual && returnType == FFI::Type::Float) - returnsEqual = std::isnan(std::bit_cast(static_cast(actual))) && std::isnan(std::bit_cast(static_cast(expected))); + constexpr bool operandOrderCanFlipNaNPayload = sizeof...(Arguments) >= 2; + if constexpr (operandOrderCanFlipNaNPayload) { + if (!returnsEqual && returnType == FFI::Type::Double) + returnsEqual = std::isnan(std::bit_cast(actual)) && std::isnan(std::bit_cast(expected)); + else if (!returnsEqual && returnType == FFI::Type::Float) + returnsEqual = std::isnan(std::bit_cast(static_cast(actual))) && std::isnan(std::bit_cast(static_cast(expected))); + } ok &= returnsEqual; if (!ok) { s_failureCount++;