authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-03-09 15:06:25-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-03-09 12:07:47-07:00
log372d56371fb6251ac2c95759461c4b9c12c2f03f
treed3ea5606dc16454e4f76d27f0cbb587d0f06ec8e
parent6d44a8cd0beb7fbbc267667a68e1df53f7cdd496

Merge pull request #21933 from kcbanner/comptime_nan_comparison

Fix float vector comparisons with signed zero and NaN, add test coverage

3 files changed, 138 insertions(+), 2 deletions(-)

src/Sema.zig+5
...@@ -38029,6 +38029,11 @@ fn compareScalar(...@@ -38029,6 +38029,11 @@ fn compareScalar(
38029 const pt = sema.pt;38029 const pt = sema.pt;
38030 const coerced_lhs = try pt.getCoerced(lhs, ty);38030 const coerced_lhs = try pt.getCoerced(lhs, ty);
38031 const coerced_rhs = try pt.getCoerced(rhs, ty);38031 const coerced_rhs = try pt.getCoerced(rhs, ty);
38032
38033 // Equality comparisons of signed zero and NaN need to use floating point semantics
38034 if (coerced_lhs.isFloat(pt.zcu) or coerced_rhs.isFloat(pt.zcu))
38035 return Value.compareHeteroSema(coerced_lhs, op, coerced_rhs, pt);
38036
38032 switch (op) {38037 switch (op) {
38033 .eq => return sema.valuesEqual(coerced_lhs, coerced_rhs, ty),38038 .eq => return sema.valuesEqual(coerced_lhs, coerced_rhs, ty),
38034 .neq => return !(try sema.valuesEqual(coerced_lhs, coerced_rhs, ty)),38039 .neq => return !(try sema.valuesEqual(coerced_lhs, coerced_rhs, ty)),
src/Value.zig+2
...@@ -1132,6 +1132,8 @@ pub fn compareHeteroAdvanced(...@@ -1132,6 +1132,8 @@ pub fn compareHeteroAdvanced(
1132 else => {},1132 else => {},
1133 }1133 }
1134 }1134 }
1135
1136 if (lhs.isNan(zcu) or rhs.isNan(zcu)) return op == .neq;
1135 return (try orderAdvanced(lhs, rhs, strat, zcu, tid)).compare(op);1137 return (try orderAdvanced(lhs, rhs, strat, zcu, tid)).compare(op);
1136}1138}
11371139
test/behavior/floatop.zig+131-2
...@@ -132,13 +132,20 @@ test "cmp f16" {...@@ -132,13 +132,20 @@ test "cmp f16" {
132 try comptime testCmp(f16);132 try comptime testCmp(f16);
133}133}
134134
135test "cmp f32/f64" {135test "cmp f32" {
136 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO136 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
137 if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
138 if (builtin.cpu.arch.isArm() and builtin.target.abi.float() == .soft) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21234137 if (builtin.cpu.arch.isArm() and builtin.target.abi.float() == .soft) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21234
138 if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
139139
140 try testCmp(f32);140 try testCmp(f32);
141 try comptime testCmp(f32);141 try comptime testCmp(f32);
142}
143
144test "cmp f64" {
145 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
146 if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
147 if (builtin.cpu.arch.isArm() and builtin.target.abi.float() == .soft) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21234
148
142 try testCmp(f64);149 try testCmp(f64);
143 try comptime testCmp(f64);150 try comptime testCmp(f64);
144}151}
...@@ -224,6 +231,98 @@ fn testCmp(comptime T: type) !void {...@@ -224,6 +231,98 @@ fn testCmp(comptime T: type) !void {
224 }231 }
225}232}
226233
234test "vector cmp f16" {
235 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
236 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
237 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
238 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
239 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
240 if (builtin.cpu.arch.isArm()) return error.SkipZigTest;
241 if (builtin.cpu.arch.isPowerPC64()) return error.SkipZigTest;
242
243 try testCmpVector(f16);
244 try comptime testCmpVector(f16);
245}
246
247test "vector cmp f32" {
248 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
249 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
250 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
251 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
252 if (builtin.cpu.arch.isArm()) return error.SkipZigTest;
253 if (builtin.cpu.arch.isPowerPC64()) return error.SkipZigTest;
254
255 try testCmpVector(f32);
256 try comptime testCmpVector(f32);
257}
258
259test "vector cmp f64" {
260 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
261 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
262 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
263 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
264 if (builtin.cpu.arch.isArm()) return error.SkipZigTest;
265 if (builtin.cpu.arch.isPowerPC64()) return error.SkipZigTest;
266
267 try testCmpVector(f64);
268 try comptime testCmpVector(f64);
269}
270
271test "vector cmp f128" {
272 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
273 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
274 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
275 if (builtin.zig_backend == .stage2_c and builtin.cpu.arch.isArm()) return error.SkipZigTest;
276 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
277 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
278 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
279 if (builtin.cpu.arch.isArm()) return error.SkipZigTest;
280 if (builtin.cpu.arch.isPowerPC64()) return error.SkipZigTest;
281
282 try testCmpVector(f128);
283 try comptime testCmpVector(f128);
284}
285
286test "vector cmp f80/c_longdouble" {
287 if (true) return error.SkipZigTest;
288
289 try testCmpVector(f80);
290 try comptime testCmpVector(f80);
291 try testCmpVector(c_longdouble);
292 try comptime testCmpVector(c_longdouble);
293}
294fn testCmpVector(comptime T: type) !void {
295 var edges = [_]T{
296 -math.inf(T),
297 -math.floatMax(T),
298 -math.floatMin(T),
299 -math.floatTrueMin(T),
300 -0.0,
301 math.nan(T),
302 0.0,
303 math.floatTrueMin(T),
304 math.floatMin(T),
305 math.floatMax(T),
306 math.inf(T),
307 };
308 _ = &edges;
309 for (edges, 0..) |rhs, rhs_i| {
310 const rhs_v: @Vector(4, T) = .{ rhs, rhs, rhs, rhs };
311 for (edges, 0..) |lhs, lhs_i| {
312 const no_nan = lhs_i != 5 and rhs_i != 5;
313 const lhs_order = if (lhs_i < 5) lhs_i else lhs_i - 2;
314 const rhs_order = if (rhs_i < 5) rhs_i else rhs_i - 2;
315 const lhs_v: @Vector(4, T) = .{ lhs, lhs, lhs, lhs };
316 try expect(@reduce(.And, (lhs_v == rhs_v)) == (no_nan and lhs_order == rhs_order));
317 try expect(@reduce(.And, (lhs_v != rhs_v)) == !(no_nan and lhs_order == rhs_order));
318 try expect(@reduce(.And, (lhs_v < rhs_v)) == (no_nan and lhs_order < rhs_order));
319 try expect(@reduce(.And, (lhs_v > rhs_v)) == (no_nan and lhs_order > rhs_order));
320 try expect(@reduce(.And, (lhs_v <= rhs_v)) == (no_nan and lhs_order <= rhs_order));
321 try expect(@reduce(.And, (lhs_v >= rhs_v)) == (no_nan and lhs_order >= rhs_order));
322 }
323 }
324}
325
227test "different sized float comparisons" {326test "different sized float comparisons" {
228 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO327 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
229 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO328 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
...@@ -1703,3 +1802,33 @@ test "optimized float mode" {...@@ -1703,3 +1802,33 @@ test "optimized float mode" {
1703 try expect(S.optimized(small) == small);1802 try expect(S.optimized(small) == small);
1704 try expect(S.strict(small) == tiny);1803 try expect(S.strict(small) == tiny);
1705}1804}
1805
1806fn MakeType(comptime x: anytype) type {
1807 return struct {
1808 fn get() @TypeOf(x) {
1809 return x;
1810 }
1811 };
1812}
1813
1814const nan_a: f32 = @bitCast(@as(u32, 0xffc00000));
1815const nan_b: f32 = @bitCast(@as(u32, 0xffe00000));
1816
1817fn testMemoization() !void {
1818 try expect(MakeType(nan_a) == MakeType(nan_a));
1819 try expect(MakeType(nan_b) == MakeType(nan_b));
1820 try expect(MakeType(nan_a) != MakeType(nan_b));
1821}
1822
1823fn testVectorMemoization(comptime T: type) !void {
1824 const nan_a_v: T = @splat(nan_a);
1825 const nan_b_v: T = @splat(nan_b);
1826 try expect(MakeType(nan_a_v) == MakeType(nan_a_v));
1827 try expect(MakeType(nan_b_v) == MakeType(nan_b_v));
1828 try expect(MakeType(nan_a_v) != MakeType(nan_b_v));
1829}
1830
1831test "comptime calls are only memoized when float arguments are bit-for-bit equal" {
1832 try comptime testMemoization();
1833 try comptime testVectorMemoization(@Vector(4, f32));
1834}