From 837da0541620333d779511eef140224d3d1c2774 Mon Sep 17 00:00:00 2001 From: Sigilante Date: Sat, 30 May 2026 11:37:54 -0500 Subject: [PATCH] Add coverage: rounding modes, N==0 no-ops, float128 layout Phase 4 quick wins from the audit's coverage gaps: - test_saxpy_rounding_modes (C6): exercises all five rounding modes on a positive and a negative half-ULP tie (1.0 +/- 2^-24). Distinguishes every mode -- round-up vs round-down by sign, round-toward-zero vs round-down on the negative operand, 'a' rounding outward on both ties, 'n' staying (1.0 has an even mantissa). First test to prove all five documented modes are honored, not just 'u' vs 'n'. - test_{sasum,sdot,snrm2,saxpy,scopy,sscal}_zero (C3): N==0 is a no-op -- reductions return 0, axpy/copy/scal leave their output untouched. Generalizes the existing sswap_zero across the Level-1 families. - test_qasum_layout (C7): pins the float128_t storage invariant every q* test depends on -- sign+exponent in the high word v[1], low mantissa in v[0]. 151/151 tests pass. Co-Authored-By: Claude Opus 4.8 --- tests/blas/include/test.h | 16 +++++++++++ tests/blas/level1/test_qasum.c | 16 +++++++++++ tests/blas/level1/test_sasum.c | 10 +++++++ tests/blas/level1/test_saxpy.c | 49 ++++++++++++++++++++++++++++++++++ tests/blas/level1/test_scopy.c | 12 +++++++++ tests/blas/level1/test_sdot.c | 11 ++++++++ tests/blas/level1/test_snrm2.c | 10 +++++++ tests/blas/level1/test_sscal.c | 12 +++++++++ tests/test_all.c | 8 ++++++ 9 files changed, 144 insertions(+) diff --git a/tests/blas/include/test.h b/tests/blas/include/test.h index ca2999b..deacce2 100644 --- a/tests/blas/include/test.h +++ b/tests/blas/include/test.h @@ -24,6 +24,22 @@ MunitResult test_saxpy_neg_stride(const MunitParameter params[], void* user_data_or_fixture); MunitResult test_saxpy_rounding(const MunitParameter params[], void* user_data_or_fixture); +MunitResult test_sasum_zero(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_sdot_zero(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_snrm2_zero(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_saxpy_zero(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_scopy_zero(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_sscal_zero(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_saxpy_rounding_modes(const MunitParameter params[], + void* user_data_or_fixture); +MunitResult test_qasum_layout(const MunitParameter params[], + void* user_data_or_fixture); MunitResult test_scopy_all(const MunitParameter params[], void* user_data_or_fixture); MunitResult test_scopy_stride(const MunitParameter params[], diff --git a/tests/blas/level1/test_qasum.c b/tests/blas/level1/test_qasum.c index fe2cd13..ea90b3e 100644 --- a/tests/blas/level1/test_qasum.c +++ b/tests/blas/level1/test_qasum.c @@ -84,3 +84,19 @@ MunitResult test_qasum_negpi(const MunitParameter params[], free(QX); return MUNIT_OK; } + +// Pins the float128_t storage layout every q* test depends on: the sign and +// exponent live in the high word v[1]; v[0] is the low mantissa word. +MunitResult test_qasum_layout(const MunitParameter params[], + void* user_data_or_fixture) { + // 1.0 in IEEE binary128, written {lo, hi}. + float128_t one = {{ 0x0000000000000000, 0x3fff000000000000 }}; + assert_ullong(one.v[0], ==, 0x0000000000000000); // low mantissa word + assert_ullong(one.v[1], ==, 0x3fff000000000000); // sign+exponent word + + // A routine that reads the pair agrees: |1.0| == 1.0, bit-for-bit. + float128_t r = qasum(1, &one, 1, 'n'); + assert_ullong(r.v[1], ==, 0x3fff000000000000); + assert_ullong(r.v[0], ==, 0x0000000000000000); + return MUNIT_OK; +} diff --git a/tests/blas/level1/test_sasum.c b/tests/blas/level1/test_sasum.c index 7f35701..1e388cb 100644 --- a/tests/blas/level1/test_sasum.c +++ b/tests/blas/level1/test_sasum.c @@ -43,3 +43,13 @@ MunitResult test_sasum_stride(const MunitParameter params[], return MUNIT_OK; } + +// N==0 is a no-op: returns 0, reads nothing. +MunitResult test_sasum_zero(const MunitParameter params[], + void* user_data_or_fixture) { + float32_t* SX = svec((float[]){42.0f}, 1); + float32_t r = sasum(0, SX, 1, 'n'); + assert_ulong(r.v, ==, (uint32_t)SB_REAL32_ZERO); + free(SX); + return MUNIT_OK; +} diff --git a/tests/blas/level1/test_saxpy.c b/tests/blas/level1/test_saxpy.c index 7024844..a5ad0d1 100644 --- a/tests/blas/level1/test_saxpy.c +++ b/tests/blas/level1/test_saxpy.c @@ -116,3 +116,52 @@ MunitResult test_saxpy_rounding(const MunitParameter params[], free(SXu); free(SYu); free(SXn); free(SYn); return MUNIT_OK; } + +// Exercises all five rounding modes on a positive and a negative half-ULP +// tie (1.0 +/- 2^-24 via saxpy). Distinguishes every mode: round-up vs +// round-down differ by sign, round-toward-zero vs round-down differ on the +// negative operand, and 'a' (ties-away) rounds outward on both ties while +// 'n' (ties-even) stays (1.0 has an even mantissa). +MunitResult test_saxpy_rounding_modes(const MunitParameter params[], + void* user_data_or_fixture) { + const float32_t SA = { SB_REAL32_ONE }; + const float eps = 5.9604644775390625e-8f; // 2^-24 + const uint32_t ONE = 0x3f800000, UP = 0x3f800001; // 1.0, next-above + const uint32_t NEG_ONE = 0xbf800000, NEG_UP = 0xbf800001; // -1.0, next-below + + // positive tie: 1.0 + 2^-24 + struct { char m; uint32_t want; } pos[] = { + {'n', ONE}, {'z', ONE}, {'d', ONE}, {'u', UP}, {'a', UP} }; + for (uint64_t k = 0; k < 5; k++) { + float32_t* X = svec((float[]){eps}, 1); + float32_t* Y = svec((float[]){1.0f}, 1); + saxpy(1, SA, X, 1, Y, 1, pos[k].m); + assert_ulong(Y[0].v, ==, pos[k].want); + free(X); free(Y); + } + + // negative tie: -1.0 - 2^-24 + struct { char m; uint32_t want; } neg[] = { + {'n', NEG_ONE}, {'z', NEG_ONE}, {'u', NEG_ONE}, {'d', NEG_UP}, {'a', NEG_UP} }; + for (uint64_t k = 0; k < 5; k++) { + float32_t* X = svec((float[]){-eps}, 1); + float32_t* Y = svec((float[]){-1.0f}, 1); + saxpy(1, SA, X, 1, Y, 1, neg[k].m); + assert_ulong(Y[0].v, ==, neg[k].want); + free(X); free(Y); + } + return MUNIT_OK; +} + +// N==0 is a no-op: Y is left untouched. +MunitResult test_saxpy_zero(const MunitParameter params[], + void* user_data_or_fixture) { + const float32_t SA = { SB_REAL32_ONE }; + float32_t* SX = svec((float[]){42.0f}, 1); + float32_t* SY = svec((float[]){7.0f}, 1); + saxpy(0, SA, SX, 1, SY, 1, 'n'); + float32_t* RY = svec((float[]){7.0f}, 1); + assert_ulong(SY[0].v, ==, RY[0].v); + free(SX); free(SY); free(RY); + return MUNIT_OK; +} diff --git a/tests/blas/level1/test_scopy.c b/tests/blas/level1/test_scopy.c index 99fd3f1..0d8c86d 100644 --- a/tests/blas/level1/test_scopy.c +++ b/tests/blas/level1/test_scopy.c @@ -49,3 +49,15 @@ MunitResult test_scopy_stride(const MunitParameter params[], return MUNIT_OK; } + +// N==0 is a no-op: Y is left untouched. +MunitResult test_scopy_zero(const MunitParameter params[], + void* user_data_or_fixture) { + float32_t* SX = svec((float[]){42.0f}, 1); + float32_t* SY = svec((float[]){7.0f}, 1); + scopy(0, SX, 1, SY, 1, 'n'); + float32_t* RY = svec((float[]){7.0f}, 1); + assert_ulong(SY[0].v, ==, RY[0].v); + free(SX); free(SY); free(RY); + return MUNIT_OK; +} diff --git a/tests/blas/level1/test_sdot.c b/tests/blas/level1/test_sdot.c index 8e9086b..6966896 100644 --- a/tests/blas/level1/test_sdot.c +++ b/tests/blas/level1/test_sdot.c @@ -63,3 +63,14 @@ MunitResult test_sdot_neg_stride(const MunitParameter params[], return MUNIT_OK; } + +// N==0 is a no-op: returns 0. +MunitResult test_sdot_zero(const MunitParameter params[], + void* user_data_or_fixture) { + float32_t* SX = svec((float[]){42.0f}, 1); + float32_t* SY = svec((float[]){7.0f}, 1); + float32_t r = sdot(0, SX, 1, SY, 1, 'n'); + assert_ulong(r.v, ==, (uint32_t)SB_REAL32_ZERO); + free(SX); free(SY); + return MUNIT_OK; +} diff --git a/tests/blas/level1/test_snrm2.c b/tests/blas/level1/test_snrm2.c index 4857db7..7ee4bc1 100644 --- a/tests/blas/level1/test_snrm2.c +++ b/tests/blas/level1/test_snrm2.c @@ -41,3 +41,13 @@ MunitResult test_snrm2_stride(const MunitParameter params[], return MUNIT_OK; } + +// N==0 is a no-op: returns 0. +MunitResult test_snrm2_zero(const MunitParameter params[], + void* user_data_or_fixture) { + float32_t* SX = svec((float[]){42.0f}, 1); + float32_t r = snrm2(0, SX, 1, 'n'); + assert_ulong(r.v, ==, (uint32_t)SB_REAL32_ZERO); + free(SX); + return MUNIT_OK; +} diff --git a/tests/blas/level1/test_sscal.c b/tests/blas/level1/test_sscal.c index a4d6c51..7adb08c 100644 --- a/tests/blas/level1/test_sscal.c +++ b/tests/blas/level1/test_sscal.c @@ -55,3 +55,15 @@ MunitResult test_sscal_stride(const MunitParameter params[], return MUNIT_OK; } + +// N==0 is a no-op: X is left untouched. +MunitResult test_sscal_zero(const MunitParameter params[], + void* user_data_or_fixture) { + const float32_t SA = { SB_REAL32_ZERO }; + float32_t* SX = svec((float[]){42.0f}, 1); + sscal(0, SA, SX, 1, 'n'); + float32_t* RX = svec((float[]){42.0f}, 1); + assert_ulong(SX[0].v, ==, RX[0].v); + free(SX); free(RX); + return MUNIT_OK; +} diff --git a/tests/test_all.c b/tests/test_all.c index 6bd385f..57804ca 100644 --- a/tests/test_all.c +++ b/tests/test_all.c @@ -12,6 +12,14 @@ int main(int argc, char* argv[MUNIT_ARRAY_PARAM(argc + 1)]) { {"/test_saxpy_stride", test_saxpy_stride, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/test_saxpy_neg_stride", test_saxpy_neg_stride, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/test_saxpy_rounding", test_saxpy_rounding, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_sasum_zero", test_sasum_zero, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_sdot_zero", test_sdot_zero, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_snrm2_zero", test_snrm2_zero, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_saxpy_zero", test_saxpy_zero, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_scopy_zero", test_scopy_zero, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_sscal_zero", test_sscal_zero, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_saxpy_rounding_modes", test_saxpy_rounding_modes, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {"/test_qasum_layout", test_qasum_layout, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/test_scopy_all", test_scopy_all, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/test_scopy_stride", test_scopy_stride, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/test_sdot_0", test_sdot_0, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},