From 617293c5f053baafdfa9859f918d108e0c278e4d Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 20:08:27 -0700 Subject: [PATCH 1/4] quat/cmplx nan cmp --- src/exact_value.cpp | 27 ++++ .../internal/test_quaternion_comparison.odin | 130 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 tests/internal/test_quaternion_comparison.odin diff --git a/src/exact_value.cpp b/src/exact_value.cpp index 2f82a52cf..5c436fee6 100644 --- a/src/exact_value.cpp +++ b/src/exact_value.cpp @@ -1019,6 +1019,10 @@ gb_internal bool compare_exact_values(TokenKind op, ExactValue x, ExactValue y) f64 b = x.value_complex->imag; f64 c = y.value_complex->real; f64 d = y.value_complex->imag; + if (isnan(a) || isnan(b) || isnan(c) || isnan(d)) { + return op == Token_NotEq; + } + switch (op) { case Token_CmpEq: return cmp_f64(a, c) == 0 && cmp_f64(b, d) == 0; case Token_NotEq: return cmp_f64(a, c) != 0 || cmp_f64(b, d) != 0; @@ -1026,6 +1030,29 @@ gb_internal bool compare_exact_values(TokenKind op, ExactValue x, ExactValue y) break; } + case ExactValue_Quaternion: { + Quaternion256 a = *x.value_quaternion; + Quaternion256 b = *y.value_quaternion; + if (isnan(a.real) || isnan(a.imag) || isnan(a.jmag) || isnan(a.kmag) || + isnan(b.real) || isnan(b.imag) || isnan(b.jmag) || isnan(b.kmag)) { + return op == Token_NotEq; + } + + switch (op) { + case Token_CmpEq: + return cmp_f64(a.real, b.real) == 0 && + cmp_f64(a.imag, b.imag) == 0 && + cmp_f64(a.jmag, b.jmag) == 0 && + cmp_f64(a.kmag, b.kmag) == 0; + case Token_NotEq: + return cmp_f64(a.real, b.real) != 0 || + cmp_f64(a.imag, b.imag) != 0 || + cmp_f64(a.jmag, b.jmag) != 0 || + cmp_f64(a.kmag, b.kmag) != 0; + } + break; + } + case ExactValue_String: { String a = x.value_string; String b = y.value_string; diff --git a/tests/internal/test_quaternion_comparison.odin b/tests/internal/test_quaternion_comparison.odin new file mode 100644 index 000000000..93de83b8e --- /dev/null +++ b/tests/internal/test_quaternion_comparison.odin @@ -0,0 +1,130 @@ +package test_internal + +import "core:testing" + +// Each constant case is paired with the same comparison on variables: the folded answer +// has to be the answer the backend produces. + +@(test) +compare_constant_quaternions :: proc(t: ^testing.T) { + I :: quaternion128(0+1i+0j+0k) + J :: quaternion128(0+0i+1j+0k) + K :: quaternion128(0+0i+0j+1k) + R :: quaternion128(1+0i+0j+0k) + + testing.expect_value(t, R == 1, true) + testing.expect_value(t, 1 == R, true) + testing.expect_value(t, R != 1, false) + testing.expect_value(t, I == J, false) + testing.expect_value(t, J == K, false) + testing.expect_value(t, K == R, false) + testing.expect_value(t, I != J, true) + testing.expect_value(t, I == I, true) + + // every lane must take part, not just the real one + A :: quaternion128(2+3i+4j+5k) + testing.expect_value(t, A == quaternion128(2+3i+4j+5k), true) + testing.expect_value(t, A == quaternion128(9+3i+4j+5k), false) + testing.expect_value(t, A == quaternion128(2+9i+4j+5k), false) + testing.expect_value(t, A == quaternion128(2+3i+9j+5k), false) + testing.expect_value(t, A == quaternion128(2+3i+4j+9k), false) + + // promotion of the other operand, from integer, float and complex + testing.expect_value(t, R == 1.0, true) + testing.expect_value(t, quaternion128(2+3i+0j+0k) == 2+3i, true) + testing.expect_value(t, quaternion128(2+3i+0j+0k) == 2+4i, false) + + testing.expect_value(t, quaternion64(0) == 0, true) + testing.expect_value(t, quaternion256(0) == 0, true) + testing.expect_value(t, quaternion128(0) == quaternion128(-0.0), true) +} + +@(test) +compare_variable_quaternions :: proc(t: ^testing.T) { + I := quaternion128(0+1i+0j+0k) + J := quaternion128(0+0i+1j+0k) + K := quaternion128(0+0i+0j+1k) + R := quaternion128(1+0i+0j+0k) + + testing.expect_value(t, R == 1, true) + testing.expect_value(t, 1 == R, true) + testing.expect_value(t, R != 1, false) + testing.expect_value(t, I == J, false) + testing.expect_value(t, J == K, false) + testing.expect_value(t, K == R, false) + testing.expect_value(t, I != J, true) + testing.expect_value(t, I == I, true) + + A := quaternion128(2+3i+4j+5k) + testing.expect_value(t, A == quaternion128(2+3i+4j+5k), true) + testing.expect_value(t, A == quaternion128(9+3i+4j+5k), false) + testing.expect_value(t, A == quaternion128(2+9i+4j+5k), false) + testing.expect_value(t, A == quaternion128(2+3i+9j+5k), false) + testing.expect_value(t, A == quaternion128(2+3i+4j+9k), false) + + testing.expect_value(t, R == 1.0, true) + testing.expect_value(t, quaternion128(2+3i+0j+0k) == 2+3i, true) + testing.expect_value(t, quaternion128(2+3i+0j+0k) == 2+4i, false) + + q64 := quaternion64(0) + q256 := quaternion256(0) + testing.expect_value(t, q64 == 0, true) + testing.expect_value(t, q256 == 0, true) + testing.expect_value(t, quaternion128(0) == quaternion128(-0.0), true) +} + +@(test) +compare_constant_quaternion_nans :: proc(t: ^testing.T) { + NaN :: f64(0h7ff8_0000_0000_0000) + Q :: quaternion(w=NaN, x=0, y=0, z=0) + L :: quaternion(w=0, x=0, y=0, z=NaN) + + testing.expect_value(t, Q == Q, false) + testing.expect_value(t, Q != Q, true) + testing.expect_value(t, L == L, false) + testing.expect_value(t, L != L, true) + testing.expect_value(t, Q == 0, false) + testing.expect_value(t, Q != 0, true) +} + +@(test) +compare_variable_quaternion_nans :: proc(t: ^testing.T) { + NaN := f64(0h7ff8_0000_0000_0000) + Q := quaternion(w=NaN, x=0, y=0, z=0) + L := quaternion(w=0, x=0, y=0, z=NaN) + + testing.expect_value(t, Q == Q, false) + testing.expect_value(t, Q != Q, true) + testing.expect_value(t, L == L, false) + testing.expect_value(t, L != L, true) + testing.expect_value(t, Q == 0, false) + testing.expect_value(t, Q != 0, true) +} + +@(test) +compare_constant_complex_nans :: proc(t: ^testing.T) { + NaN :: f64(0h7ff8_0000_0000_0000) + C :: complex(NaN, 0) + D :: complex(0, NaN) + + testing.expect_value(t, C == C, false) + testing.expect_value(t, C != C, true) + testing.expect_value(t, D == D, false) + testing.expect_value(t, D != D, true) + testing.expect_value(t, C == 0, false) + testing.expect_value(t, C != 0, true) +} + +@(test) +compare_variable_complex_nans :: proc(t: ^testing.T) { + NaN := f64(0h7ff8_0000_0000_0000) + C := complex(NaN, 0) + D := complex(0, NaN) + + testing.expect_value(t, C == C, false) + testing.expect_value(t, C != C, true) + testing.expect_value(t, D == D, false) + testing.expect_value(t, D != D, true) + testing.expect_value(t, C == 0, false) + testing.expect_value(t, C != 0, true) +} From 1658e2d841a528b18d07c3ac570c9edc7617cba6 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 20:13:25 -0700 Subject: [PATCH 2/4] fix typo on cmplx -> quat --- src/check_builtin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 4a59a59b7..7498a4004 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -3866,7 +3866,7 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As if (is_type_untyped(x->type)) { if (x->mode == Addressing_Constant) { if (is_type_numeric(x->type)) { - x->type = t_untyped_complex; + x->type = t_untyped_quaternion; } } else{ convert_to_typed(c, x, t_quaternion256); From b378f21f4c0259fdee6514ad168f4f5360b8fde9 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 20:24:41 -0700 Subject: [PATCH 3/4] update tests --- tests/internal/test_nan_comparison.odin | 97 +++++++ tests/internal/test_quat_cmplx.odin | 272 ++++++++++++++++++ .../internal/test_quaternion_comparison.odin | 130 --------- 3 files changed, 369 insertions(+), 130 deletions(-) create mode 100644 tests/internal/test_quat_cmplx.odin delete mode 100644 tests/internal/test_quaternion_comparison.odin diff --git a/tests/internal/test_nan_comparison.odin b/tests/internal/test_nan_comparison.odin index 5740be477..b47cc9dfc 100644 --- a/tests/internal/test_nan_comparison.odin +++ b/tests/internal/test_nan_comparison.odin @@ -89,3 +89,100 @@ compare_variable_nans_f64 :: proc(t: ^testing.T) { testing.expect_value(t, NaN > NaN, false) testing.expect_value(t, NaN >= NaN, false) } + +// A complex or quaternion compares componentwise, so a NaN in any one lane makes the whole +// comparison fail. The folded form used to disagree: `cmp_f64` is `(a>b)-(a Date: Sat, 15 Aug 2026 20:41:22 -0700 Subject: [PATCH 4/4] fix #5964 --- src/check_builtin.cpp | 8 ---- tests/internal/test_quat_cmplx.odin | 57 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 7498a4004..521570800 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -3846,10 +3846,6 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As default: GB_PANIC("Invalid type"); break; } - if (type_hint != nullptr && check_is_castable_to(c, operand, type_hint)) { - operand->type = type_hint; - } - break; } @@ -3902,10 +3898,6 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As default: GB_PANIC("Invalid type"); break; } - if (type_hint != nullptr && check_is_castable_to(c, operand, type_hint)) { - operand->type = type_hint; - } - break; } diff --git a/tests/internal/test_quat_cmplx.odin b/tests/internal/test_quat_cmplx.odin index 9edef4da5..bf8df6f0d 100644 --- a/tests/internal/test_quat_cmplx.odin +++ b/tests/internal/test_quat_cmplx.odin @@ -270,3 +270,60 @@ accessor_results_are_untyped :: proc(t: ^testing.T) { testing.expect_value(t, R32, 2) testing.expect_value(t, I32, 3) } + +// An enclosing conversion passes its destination down as a type hint, and the accessors +// used to adopt it as their own result type. That decides which type the arithmetic +// inside the conversion happens in, so the wrong answer is observable: the division +// below was checked in i8, where `3.2` does not exist. + +@(test) +accessors_ignore_the_enclosing_conversions_type :: proc(t: ^testing.T) { + c: complex128 = 10 + testing.expect_value(t, i8(real(c) / 3.2), 3) + testing.expect_value(t, i8(imag(c) / 3.2), 0) + + d: complex128 = 0 + 10i + testing.expect_value(t, i8(imag(d) / 3.2), 3) + + q: quaternion256 = quaternion(w=10, x=10, y=10, z=10) + testing.expect_value(t, i8(real(q) / 3.2), 3) + testing.expect_value(t, i8(imag(q) / 3.2), 3) + testing.expect_value(t, i8(jmag(q) / 3.2), 3) + testing.expect_value(t, i8(kmag(q) / 3.2), 3) + + // the same on constants + C :: complex128(10) + Q :: quaternion256(1+2i+3j+4k) + testing.expect_value(t, int(real(C) / 2.5), 4) + testing.expect_value(t, int(kmag(Q) / 0.5), 8) +} + +// `transmute` supplies a hint too, and it is the spelling that failed silently: the +// accessor was retyped to the destination, so what got reinterpreted was already an +// integer and the float's bits were gone. Naming the intermediate was the workaround, +// so a named one is the control here. + +@(test) +accessors_keep_their_bits_through_transmute :: proc(t: ^testing.T) { + c32 : complex32 = complex(f16(1.5), f16(2.5)) + c64 : complex64 = complex(f32(1), f32(2)) + c128 : complex128 = complex(f64(1), f64(2)) + + n := real(c32) + r := real(c64) + i := imag(c128) + + testing.expect_value(t, transmute(u16)real(c32), transmute(u16)n) + testing.expect_value(t, transmute(u32)real(c64), transmute(u32)r) + testing.expect_value(t, transmute(u64)imag(c128), transmute(u64)i) + + testing.expect_value(t, transmute(u16)real(c32), 15872) + testing.expect_value(t, transmute(u32)real(c64), 1065353216) + testing.expect_value(t, transmute(u64)imag(c128), 4611686018427387904) + + q : quaternion256 = quaternion(w=1, x=2, y=3, z=4) + j := jmag(q) + k := kmag(q) + testing.expect_value(t, transmute(u64)jmag(q), transmute(u64)j) + testing.expect_value(t, transmute(u64)kmag(q), transmute(u64)k) +}