llm
Download
1649 lines 76.5 KiBCModified 2026-08-31 raw
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649
// Tests for the number library (src/stdlib/number.c).
// Build & run: make test-number (compiles with ASan/UBSan).

#include "number.h"

#include <float.h>
#include <gc.h>
#include <gmp.h>
#include <math.h>
#include <stdalign.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int checks = 0;

#define CHECK(cond)                                                                                                    \
    do {                                                                                                               \
        if (!(cond)) {                                                                                                 \
            fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond);                                            \
            exit(1);                                                                                                   \
        }                                                                                                              \
        checks++;                                                                                                      \
    } while (0)

// want_exact: 1/0 to check the exactness flag, -1 to ignore it.
static void expect_str(Num_t x, uint32_t digits, const char *want, int want_exact) {
    bool exact;
    char *s = number_to_string(x, digits, &exact);
    if (strcmp(s, want) != 0) {
        fprintf(stderr, "FAIL: to_string gave \"%s\", want \"%s\"\n", s, want);
        exit(1);
    }
    if (want_exact >= 0 && exact != (want_exact != 0)) {
        fprintf(stderr, "FAIL: to_string(\"%s\") exact=%d, want %d\n", s, exact, want_exact);
        exit(1);
    }
    checks++;
}

static void expect_sym(Num_t x, const char *want) {
    char *s = number_to_symbolic(x);
    if (strcmp(s, want) != 0) {
        fprintf(stderr, "FAIL: to_symbolic gave \"%s\", want \"%s\"\n", s, want);
        exit(1);
    }
    checks++;
}

static void expect_tex(Num_t x, const char *want) {
    char *s = number_to_tex(x);
    if (strcmp(s, want) != 0) {
        fprintf(stderr, "FAIL: to_tex gave \"%s\", want \"%s\"\n", s, want);
        exit(1);
    }
    checks++;
}

static void expect_eq(Num_t a, Num_t b) {
    CHECK(number_equal(a, b));
    CHECK(number_compare(a, b) == 0);
}

static void expect_error(Num_t x) {
    CHECK(number_is_error(x));
}

// Like expect_eq, but treats "both sides are the error
// value" as a pass too: number_equal (which expect_eq uses) documents the
// error value as never equal to anything, including another error --
// correct in general (e.g. 1/0 != 1/0's caller shouldn't get "equal" back
// by accident), but exactly wrong for cross-checking two independently
// -computed error results (e.g. a fast-path dispatcher and its general
// fallback both correctly rejecting the same division by zero) that
// legitimately agree.
static void expect_dispatch_eq(Num_t a, Num_t b) {
    if (number_is_error(a) && number_is_error(b)) {
        checks++;
        return;
    }
    expect_eq(a, b);
}

static void expect_msg(Num_t x, const char *want) {
    CHECK(number_is_error(x));
    const char *msg = number_error_message(x);
    if (!msg || strcmp(msg, want) != 0) {
        fprintf(stderr, "FAIL: error_message gave \"%s\", want \"%s\"\n", msg, want);
        exit(1);
    }
    checks++;
}

static void expect_roundtrip(double d) {
    Num_t x = number_from_double(d);
    double back = number_to_double(x);
    if (back != d) {
        fprintf(stderr, "FAIL: roundtrip %.17g -> %.17g\n", d, back);
        exit(1);
    }
    checks++;
}

static Num_t factorial(int n) {
    Num_t acc = number_from_int(1);
    for (int i = 2; i <= n; i++) {
        Num_t f = number_from_int(i);
        Num_t next = number_mul(acc, f);
        acc = next;
    }
    return acc;
}

// x is borrowed.
static uint32_t tag_of(Num_t x) {
    return (uint32_t)(x.bits & 3);
}

// What tomo_init() does for a real Tomo program: hand GMP the collector, so
// its limb buffers are GC-owned like everything else here.
static void *gc_gmp_alloc(size_t size) {
    return GC_MALLOC_ATOMIC(size);
}
static void *gc_gmp_realloc(void *ptr, size_t old_size, size_t new_size) {
    (void)old_size;
    return GC_REALLOC(ptr, new_size);
}
static void gc_gmp_free(void *ptr, size_t size) {
    (void)ptr, (void)size;
}

int main(void) {
    GC_INIT();
    mp_set_memory_functions(gc_gmp_alloc, gc_gmp_realloc, gc_gmp_free);

    // --- Basic integer arithmetic (integer sub-path) ---
    {
        Num_t two = number_from_int(2);
        Num_t four = number_add(two, two);
        expect_str((four), 5, "4", 1);
        CHECK(tag_of(four) == 1); // still a small immediate
        expect_str(number_from_int(0), 5, "0", 1);
        expect_str(number_from_int(-17), 0, "-17", 1);
    }

    // --- NUMBER_SMALL: the compile-time-literal packing macro (number.h)
    // must produce bit-identical output to the runtime small-rational
    // packing (number_from_ratio, for an already-reduced in-bounds pair) --
    // this is the contract a code generator emitting NUMBER_SMALL(...)
    // literals is trusting; a future change to the internal representation
    // must not silently break it. ---
    {
        int64_t nums[] = {0,
                          1,
                          -1,
                          42,
                          -42,
                          1,
                          -1,
                          1,
                          22,
                          -22,
                          NUMBER_SMALL_NUM_MAX,
                          NUMBER_SMALL_NUM_MIN,
                          NUMBER_SMALL_NUM_MAX,
                          NUMBER_SMALL_NUM_MIN,
                          1};
        uint32_t dens[] = {
            1, 1, 1, 1, 1, 2, 2, 3, 7, 7, 1, 1, NUMBER_SMALL_DEN_MAX, NUMBER_SMALL_DEN_MAX, NUMBER_SMALL_DEN_MAX};
        for (size_t i = 0; i < sizeof(nums) / sizeof(*nums); i++) {
            Num_t viaMacro = NUMBER_SMALL(nums[i], dens[i]);
            Num_t viaRuntime = number_from_ratio(nums[i], (int64_t)dens[i]);
            CHECK(viaMacro.bits == viaRuntime.bits);
            CHECK(tag_of(viaMacro) == 1); // genuinely the small-immediate tier, not a promoted bigrat
        }
    }

    // --- Fast-path dispatcher vs. general implementation: number_add et
    // al. (number.h) are a forced-inline fast path (both operands already
    // tier-1, no overflow) falling through to number_*_general (number.c)
    // for everything else. This cross-checks the two against each other --
    // via expect_eq (number_equal), not raw .bits: a result that correctly
    // overflows out of tier 1 is a bigrat, and two independently-allocated
    // bigrats representing the same value are equal but never bit-identical
    // (different heap addresses). The spread is deliberately aimed at
    // every edge the fast path itself branches on: the integer sub-path
    // (both denominators 1) vs. general small fractions (falls through),
    // overflow right at the NUMBER_SMALL_NUM_MAX/MIN boundary (falls
    // through), values already promoted to bigrat (falls through
    // immediately on the tag check), and, for division specifically,
    // both the evenly-divides fast path and inexact division and division
    // by zero (both fall through). A future change to either
    // implementation that desyncs them fails here, not silently in a
    // caller. ---
    {
        int64_t small_ints[] = {0,
                                1,
                                -1,
                                42,
                                -42,
                                1000,
                                -1000,
                                NUMBER_SMALL_NUM_MAX,
                                NUMBER_SMALL_NUM_MIN,
                                NUMBER_SMALL_NUM_MAX - 1,
                                NUMBER_SMALL_NUM_MIN + 1};
        size_t n_ints = sizeof(small_ints) / sizeof(*small_ints);
        for (size_t i = 0; i < n_ints; i++) {
            for (size_t j = 0; j < n_ints; j++) {
                Num_t a1 = number_from_int(small_ints[i]), a2 = number_from_int(small_ints[i]);
                Num_t b1 = number_from_int(small_ints[j]), b2 = number_from_int(small_ints[j]);
                expect_dispatch_eq(number_add(a1, b1), number_add_general(a2, b2));
                a1 = number_from_int(small_ints[i]);
                a2 = number_from_int(small_ints[i]);
                b1 = number_from_int(small_ints[j]);
                b2 = number_from_int(small_ints[j]);
                expect_dispatch_eq(number_sub(a1, b1), number_sub_general(a2, b2));
                a1 = number_from_int(small_ints[i]);
                a2 = number_from_int(small_ints[i]);
                b1 = number_from_int(small_ints[j]);
                b2 = number_from_int(small_ints[j]);
                expect_dispatch_eq(number_mul(a1, b1), number_mul_general(a2, b2));
                a1 = number_from_int(small_ints[i]);
                a2 = number_from_int(small_ints[i]);
                b1 = number_from_int(small_ints[j]);
                b2 = number_from_int(small_ints[j]);
                expect_dispatch_eq(number_div(a1, b1), number_div_general(a2, b2)); // incl. b==0
            }
            expect_dispatch_eq(number_neg(number_from_int(small_ints[i])),
                               number_neg_general(number_from_int(small_ints[i])));
        }

        // Small fractions: both TAG_SMALL, but not the integer sub-path --
        // exercises the fast dispatcher's "falls through because
        // denominator != 1" branch specifically.
        int64_t frac_nums[] = {1, -1, 22, -22, 355, -355};
        int64_t frac_dens[] = {2, 3, 7, 113, NUMBER_SMALL_DEN_MAX};
        size_t n_frac_nums = sizeof(frac_nums) / sizeof(*frac_nums);
        for (size_t i = 0; i < n_frac_nums; i++) {
            for (size_t j = 0; j < sizeof(frac_dens) / sizeof(*frac_dens); j++) {
                int64_t an = frac_nums[i], ad = frac_dens[j];
                int64_t bn = frac_nums[(i + 1) % n_frac_nums], bd = frac_dens[j];
                // Operands are borrowed (see number.h), so each pair is
                // constructed fresh, borrowed by both the fast and general
                // call, then dropped explicitly. Unlike expect_eq's
                // arguments (its *results*), a borrowed operand is never
                // implicitly freed by the call it's passed to.
                Num_t a1 = number_from_ratio(an, ad), a2 = number_from_ratio(an, ad);
                Num_t b1 = number_from_ratio(bn, bd), b2 = number_from_ratio(bn, bd);
                expect_dispatch_eq(number_add(a1, b1), number_add_general(a2, b2));

                a1 = number_from_ratio(an, ad), a2 = number_from_ratio(an, ad);
                b1 = number_from_ratio(bn, bd), b2 = number_from_ratio(bn, bd);
                expect_dispatch_eq(number_sub(a1, b1), number_sub_general(a2, b2));

                a1 = number_from_ratio(an, ad), a2 = number_from_ratio(an, ad);
                b1 = number_from_ratio(bn, bd), b2 = number_from_ratio(bn, bd);
                expect_dispatch_eq(number_mul(a1, b1), number_mul_general(a2, b2));

                a1 = number_from_ratio(an, ad), a2 = number_from_ratio(an, ad);
                b1 = number_from_ratio(bn, bd), b2 = number_from_ratio(bn, bd);
                expect_dispatch_eq(number_div(a1, b1), number_div_general(a2, b2));
            }
        }

        // Already-promoted bigrat operands: the fast dispatcher's very
        // first check (both TAG_SMALL) must correctly and immediately fall
        // through, not misfire on a heap-pointer's low bits.
        {
            Num_t big1 = number_from_string("123456789012345678901234567890");
            Num_t big2 = number_from_string("123456789012345678901234567890");
            Num_t small1 = number_from_int(7), small2 = number_from_int(7);
            expect_dispatch_eq(number_add(big1, small1), number_add_general(big2, small2));

            big1 = number_from_string("123456789012345678901234567890");
            big2 = number_from_string("123456789012345678901234567890");
            expect_dispatch_eq(number_neg(big1), number_neg_general(big2));
        }
    }

    // --- Fast-path dispatcher vs. general implementation: number_compare
    // and number_equal (number.h) get the same forced-inline fast-path
    // treatment as the arithmetic operators above. Unlike those, these
    // return a plain int/bool, not a `number`, so this compares the
    // returned values directly with == (no bits-vs-pointer or
    // error-is-never-equal-to-another-error subtlety to work around here,
    // since neither is a `number` result). ---
    {
        int64_t cmp_ints[] = {0,
                              1,
                              -1,
                              42,
                              -42,
                              1000,
                              -1000,
                              NUMBER_SMALL_NUM_MAX,
                              NUMBER_SMALL_NUM_MIN,
                              NUMBER_SMALL_NUM_MAX - 1,
                              NUMBER_SMALL_NUM_MIN + 1};
        size_t n_cmp_ints = sizeof(cmp_ints) / sizeof(*cmp_ints);
        for (size_t i = 0; i < n_cmp_ints; i++) {
            for (size_t j = 0; j < n_cmp_ints; j++) {
                Num_t a1 = number_from_int(cmp_ints[i]), a2 = number_from_int(cmp_ints[i]);
                Num_t b1 = number_from_int(cmp_ints[j]), b2 = number_from_int(cmp_ints[j]);
                CHECK(number_compare(a1, b1) == number_compare_general(a2, b2));
                CHECK(number_equal(a1, b1) == number_equal_general(a2, b2));
            }
        }

        // Small fractions: both TAG_SMALL, exercises the cross-multiply
        // path (number_compare) and the bit-equality-suffices path
        // (number_equal).
        int64_t cmp_frac_nums[] = {1, -1, 22, -22, 355, -355};
        int64_t cmp_frac_dens[] = {2, 3, 7, 113, NUMBER_SMALL_DEN_MAX};
        size_t n_cmp_frac_nums = sizeof(cmp_frac_nums) / sizeof(*cmp_frac_nums);
        for (size_t i = 0; i < n_cmp_frac_nums; i++) {
            for (size_t j = 0; j < sizeof(cmp_frac_dens) / sizeof(*cmp_frac_dens); j++) {
                int64_t an = cmp_frac_nums[i], ad = cmp_frac_dens[j];
                int64_t bn = cmp_frac_nums[(i + 1) % n_cmp_frac_nums], bd = cmp_frac_dens[j];
                Num_t a1 = number_from_ratio(an, ad), a2 = number_from_ratio(an, ad);
                Num_t b1 = number_from_ratio(bn, bd), b2 = number_from_ratio(bn, bd);
                CHECK(number_compare(a1, b1) == number_compare_general(a2, b2));
                CHECK(number_equal(a1, b1) == number_equal_general(a2, b2));
                // Also against itself: an equal-but-distinct small rational
                // (same i, both operands from frac_nums[i]) exercises the
                // fast path's own a.bits == b.bits shortcut specifically.
                Num_t c1 = number_from_ratio(an, ad), c2 = number_from_ratio(an, ad);
                Num_t d1 = number_from_ratio(an, ad), d2 = number_from_ratio(an, ad);
                CHECK(number_compare(c1, d1) == number_compare_general(c2, d2));
                CHECK(number_equal(c1, d1) == number_equal_general(c2, d2));
            }
        }

        // Already-promoted bigrat and mixed-tier operands, plus an actual
        // shared heap reference (retain, not a fresh equal construction):
        // the fast dispatcher's outer TAG_SMALL check must correctly and
        // immediately fall through in every case, leaving
        // number_*_general's own same-pointer/same-tier logic untouched.
        {
            Num_t big1 = number_from_string("123456789012345678901234567890");
            Num_t big2 = number_from_string("123456789012345678901234567890");
            Num_t small1 = number_from_int(7), small2 = number_from_int(7);
            CHECK(number_compare(big1, small1) == number_compare_general(big2, small2));
            CHECK(number_equal(big1, small1) == number_equal_general(big2, small2));

            Num_t shared = number_from_string("123456789012345678901234567890");
            Num_t shared_ref = (shared);
            CHECK(number_compare(shared, shared_ref) == 0);
            CHECK(number_equal(shared, shared_ref) == true);
        }

        // Error values: TAG_ERROR never matches the fast path's TAG_SMALL
        // check, so both sides always fall through to _general. This
        // confirms that fallthrough itself stays correct, not new fast-path
        // logic.
        {
            Num_t e1 = number_from_ratio(1, 0), e2 = number_from_ratio(1, 0); // div by zero -> error
            Num_t seven1 = number_from_int(7), seven2 = number_from_int(7);
            CHECK(number_compare(e1, seven1) == number_compare_general(e2, seven2));
            CHECK(number_equal(e1, seven1) == number_equal_general(e2, seven2));
        }
    }

    // --- Fractions, reduction, canonical equality ---
    {
        Num_t third = number_from_ratio(1, 3);
        Num_t sixth = number_from_ratio(1, 6);
        Num_t sum = number_add(third, sixth);
        expect_eq((sum), number_from_ratio(1, 2));
        expect_str(sum, 10, "0.5", 1);

        expect_eq(number_from_ratio(2, 4), number_from_ratio(1, 2));
        expect_eq(number_from_ratio(100, -300), number_from_ratio(-1, 3));
        expect_str(number_from_ratio(-1, 3), 3, "-0.333", 0);
        expect_str(number_from_ratio(22, 7), 5, "3.14286", 0);
    }

    // --- Promotion to big rationals and demotion back ---
    {
        Num_t m = number_from_int(1 << 20);
        Num_t big = number_mul(m, m); // 2^40: overflows the small form
        CHECK(tag_of(big) == 0);
        expect_str((big), 0, "1099511627776", 1);
        Num_t back = number_div(big, m); // demotes to small again
        CHECK(tag_of(back) == 1);
        expect_eq(back, (m));

        Num_t f30 = factorial(30);
        CHECK(tag_of(f30) == 0);
        expect_str((f30), 0, "265252859812191058636308480000000", 1);
        Num_t f29 = factorial(29);
        Num_t ratio = number_div(f30, f29);
        CHECK(tag_of(ratio) == 1);
        expect_eq(ratio, number_from_int(30));
    }

    // --- Exact conversion from double ---
    {
        expect_eq(number_from_double(0.5), number_from_ratio(1, 2));
        expect_eq(number_from_double(-0.75), number_from_ratio(-3, 4));

        // 0.1 is not exactly 1/10 as a double; check its exact expansion.
        Num_t tenth = number_from_double(0.1);
        bool exact;
        char *s = number_to_string(tenth, 60, &exact);
        CHECK(exact);
        CHECK(strlen(s) == 57); // "0." + 55 exact fractional digits
        CHECK(strncmp(s, "0.1000000000000000055511", 24) == 0);
        Num_t reparsed = number_from_string(s);
        expect_eq(reparsed, (tenth));

        expect_error(number_from_double(NAN));
        expect_error(number_from_double(INFINITY));
    }

    // --- Correctly rounded conversion to double ---
    {
        expect_roundtrip(0.0);
        expect_roundtrip(0.1);
        expect_roundtrip(1.0 / 3.0);
        expect_roundtrip(3.141592653589793);
        expect_roundtrip(-2.5);
        expect_roundtrip(123456789.987654321);
        expect_roundtrip(1e300);
        expect_roundtrip(1e-300);
        expect_roundtrip(DBL_MAX);
        expect_roundtrip(DBL_MIN);
        expect_roundtrip(5e-324); // smallest subnormal

        Num_t third = number_from_ratio(1, 3);
        CHECK(number_to_double(third) == 1.0 / 3.0);

        // Big-rational paths (values that can't be small rationals):
        Num_t x = number_from_string("1e300");
        CHECK(number_to_double(x) == 1e300);
        x = number_from_string("1/100000000000000000000");
        CHECK(number_to_double(x) == 1e-20);
        x = number_from_string("123456789012345678901234567890.5");
        CHECK(number_to_double(x) == 123456789012345678901234567890.5);
        x = number_from_string("5e-324"); // decimal -> nearest subnormal
        CHECK(number_to_double(x) == 5e-324);
        x = number_from_string("1e400"); // overflows to infinity
        CHECK(isinf(number_to_double(x)) && number_to_double(x) > 0);
        x = number_from_string("-1e400");
        CHECK(isinf(number_to_double(x)) && number_to_double(x) < 0);
        x = number_from_string("1e-400"); // underflows to zero
        CHECK(number_to_double(x) == 0.0);
    }

    // --- Round-half-even in decimal output ---
    {
        expect_str(number_from_ratio(1, 8), 2, "0.12", 0); // 0.125: tie, 2 even
        expect_str(number_from_ratio(3, 8), 2, "0.38", 0); // 0.375: tie, 7 odd
        expect_str(number_from_ratio(7, 2), 0, "4", 0); // 3.5 -> 4
        expect_str(number_from_ratio(5, 2), 0, "2", 0); // 2.5 -> 2
        expect_str(number_from_ratio(1, 2), 0, "0", 0); // 0.5 -> 0
        expect_str(number_from_string("99.95"), 1, "100.0", 0); // carry cascade
    }

    // --- Parsing ---
    {
        expect_eq(number_from_string("22/7"), number_from_ratio(22, 7));
        expect_eq(number_from_string("0.5"), number_from_ratio(1, 2));
        expect_eq(number_from_string("-3.5"), number_from_ratio(-7, 2));
        expect_eq(number_from_string("  -3.5  "), number_from_ratio(-7, 2));
        expect_eq(number_from_string("3."), number_from_int(3));
        expect_eq(number_from_string("2.5e1"), number_from_int(25));
        expect_eq(number_from_string("25e-1"), number_from_ratio(5, 2));
        expect_str(number_from_string("6.02e23"), 0, "602000000000000000000000", 1);

        expect_error(number_from_string(""));
        expect_error(number_from_string("abc"));
        expect_error(number_from_string("1e"));
        expect_error(number_from_string("1/"));
        expect_error(number_from_string("1/0"));
        expect_error(number_from_string("."));
        expect_error(number_from_string("1.5x"));
        expect_error(number_from_string(NULL));
    }

    // --- number_from_decimal: the exact-literal constructor ---
    {
        // The headline guarantee: "3.15" is 63/20 exactly, NOT the nearest
        // double number_from_double(3.15) would hand back.
        expect_sym(number_from_decimal("3.15"), "63/20");
        {
            Num_t exact = number_from_decimal("3.15");
            Num_t viadbl = number_from_double(3.15);
            CHECK(!number_equal(exact, viadbl)); // the whole point: they differ
        }
        // Same decimal/scientific grammar as number_from_string, all exact.
        expect_eq(number_from_decimal("0.5"), number_from_ratio(1, 2));
        expect_eq(number_from_decimal("  -3.5  "), number_from_ratio(-7, 2));
        expect_eq(number_from_decimal("25e-1"), number_from_ratio(5, 2));
        expect_str(number_from_decimal("6.02e23"), 0, "602000000000000000000000", 1);
        // The ratio form is rejected here (not decimal notation).
        expect_error(number_from_decimal("22/7"));
        expect_error(number_from_decimal("1/0"));
        expect_error(number_from_decimal("abc"));
        expect_error(number_from_decimal(NULL));
    }

    // --- Comparison ---
    {
        Num_t third = number_from_ratio(1, 3);
        Num_t approx = number_from_string("0.333");
        CHECK(number_compare(third, approx) > 0);
        CHECK(number_compare(approx, third) < 0);

        Num_t a = number_from_int(-5), b = number_from_int(3);
        CHECK(number_compare(a, b) < 0);

        Num_t f25 = factorial(25), f26 = factorial(26);
        CHECK(number_compare(f25, f26) < 0);
        CHECK(number_compare(f26, f25) > 0);

        // Irrational vs. rational: decided exactly by interval refinement
        // (never the "unordered" 2), and antisymmetric. This is the case
        // calc's `pi > 3.1` comparisons lean on.
        Num_t pi = number_pi();
        Num_t r31 = number_from_string("3.1");
        CHECK(number_compare(pi, r31) > 0);
        CHECK(number_compare(r31, pi) < 0);
        CHECK(!number_equal(pi, r31));

        // Identical radicals are recognized as equal symbolically, so
        // number_compare returns 0 and number_equal is true even across the
        // two different representations; a rational still orders strictly against them.
        Num_t s2 = number_sqrt(number_from_int(2));
        Num_t twos2 = number_mul(number_from_int(2), s2);
        Num_t s8 = number_sqrt(number_from_int(8)); // == 2*sqrt(2)
        CHECK(number_compare(s8, twos2) == 0);
        CHECK(number_equal(s8, twos2));
        CHECK(number_compare(s8, number_from_int(2)) > 0); // sqrt(8) ~ 2.83

        // The one genuinely undecidable case: a general (non-symbolic-form)
        // irrational equal to a rational with no identity the library
        // recognizes to prove it. Rather than loop forever, both give up past
        // the precision cap: number_compare reports 2 ("unordered") and
        // number_equal reports false. calc's inexact fallback relies on
        // exactly this contract (see calc.c).
        Num_t one = number_from_int(1);
        Num_t sin1 = number_sin(one), cos1 = number_cos(one);
        Num_t sin1sq = number_mul(sin1, sin1), cos1sq = number_mul(cos1, cos1);
        Num_t pyth = number_add(sin1sq, cos1sq); // sin(1)^2 + cos(1)^2, == 1
        CHECK(number_compare(pyth, one) == 2);
        CHECK(!number_equal(pyth, one));
    }

    // --- Unary operations ---
    {
        expect_error(number_neg(NUMBER_ERROR));
        expect_error(number_abs(NUMBER_ERROR));
        Num_t x = number_from_ratio(-2, 3);
        expect_eq(number_neg(x), number_from_ratio(2, 3));
        expect_eq(number_abs(x), number_from_ratio(2, 3));
        expect_eq(number_inverse(x), number_from_ratio(-3, 2));
        CHECK(number_is_negative(x));
        CHECK(number_sign(x) == -1);

        Num_t big = factorial(30);
        Num_t negbig = number_neg(big);
        CHECK(number_sign(negbig) == -1);
        expect_eq(number_abs(negbig), (big));
        Num_t invbig = number_inverse(big);
        Num_t roundtrip = number_inverse(invbig);
        expect_eq(roundtrip, (big));

        expect_error(number_inverse(number_from_int(0)));
    }

    // --- Errors and propagation ---
    {
        expect_error(number_from_ratio(1, 0));
        Num_t one = number_from_int(1), zero = number_from_int(0);
        expect_error(number_div(one, zero));
        expect_error(number_add(NUMBER_ERROR, one));
        expect_error(number_mul(one, NUMBER_ERROR));

        CHECK(isnan(number_to_double(NUMBER_ERROR)));
        CHECK(number_compare(NUMBER_ERROR, NUMBER_ERROR) == 2);
        CHECK(!number_equal(NUMBER_ERROR, NUMBER_ERROR));
        expect_str(NUMBER_ERROR, 5, "(error)", -1);
    }

    // --- Error messages ---
    {
        CHECK(number_error_message(number_from_int(5)) == NULL);
        CHECK(strcmp(number_error_message(NUMBER_ERROR), "undefined result") == 0);

        expect_msg(number_from_ratio(1, 0), "division by zero");
        expect_msg(number_div(number_from_int(1), number_from_int(0)), "division by zero");
        expect_msg(number_inverse(number_from_int(0)), "division by zero");
        expect_msg(number_sqrt(number_from_int(-1)), "square root of a negative number");
        {
            Num_t pi = number_pi();
            Num_t neg_pi = number_neg(pi);
            expect_msg(number_sqrt(neg_pi), "square root of a negative number");
        }
        expect_msg(number_ln(number_from_int(0)), "logarithm of a non-positive number");
        expect_msg(number_ln(number_from_int(-5)), "logarithm of a non-positive number");
        expect_msg(number_log10(number_from_int(-1)), "logarithm of a non-positive number");
        expect_msg(number_asin(number_from_int(2)), "arcsine or arccosine of a value outside [-1, 1]");
        expect_msg(number_acos(number_from_ratio(-3, 2)), "arcsine or arccosine of a value outside [-1, 1]");
        expect_msg(number_pow(number_from_int(0), number_from_int(-1)), "zero raised to a negative power");
        expect_msg(number_pow(number_from_int(-2), number_from_ratio(1, 2)), "square root of a negative number");
        expect_msg(number_pow(number_from_int(-2), number_from_ratio(1, 3)),
                   "a negative number raised to a non-integer power (not a real result)");
        expect_msg(number_from_string("not a number"), "invalid number syntax");
        expect_msg(number_from_string("1/"), "invalid number syntax");
        expect_msg(number_from_double(NAN), "not a finite number (NaN or infinity)");
        expect_msg(number_from_double(INFINITY), "not a finite number (NaN or infinity)");
        {
            Num_t pi2 = number_pi(), two = number_from_int(2);
            Num_t half_pi = number_div(pi2, two);
            expect_msg(number_tan(half_pi), "division by zero"); // cos(pi/2) == 0 exactly
        }

        // Propagation carries the original reason forward through further
        // arithmetic, rather than resetting to a generic message.
        {
            Num_t bad = number_div(number_from_int(1), number_from_int(0));
            Num_t five = number_from_int(5);
            expect_msg(number_mul(bad, five), "division by zero");
        }

        // Two colliding errors: either message is an acceptable answer, but
        // it must be one of the two specific reasons, not a generic fallback.
        {
            Num_t a = number_div(number_from_int(2), number_from_int(0));
            Num_t b = number_sqrt(number_from_int(-1));
            Num_t sum = number_add(a, b);
            const char *msg = number_error_message(sum);
            CHECK(strcmp(msg, "division by zero") == 0 || strcmp(msg, "square root of a negative number") == 0);
        }
    }

    // --- Tier 3: sqrt ---
    {
        expect_eq(number_sqrt(number_from_int(4)), number_from_int(2));
        expect_eq(number_sqrt(number_from_ratio(9, 4)), number_from_ratio(3, 2));
        expect_eq(number_sqrt(number_from_int(0)), number_from_int(0));

        Num_t two = number_from_int(2);
        Num_t s2 = number_sqrt(two);
        CHECK(!number_is_rational(s2));
        expect_str((s2), 10, "1.4142135624", 0);
        CHECK(number_to_double(s2) == 1.4142135623730951);

        // The design's marquee identity: sqrt(2)*sqrt(2) == 2, exactly.
        Num_t prod = number_mul(s2, s2);
        CHECK(number_is_rational(prod));
        expect_eq(prod, (two));

        // sqrt(8) == 2*sqrt(2): commensurable radicands are recognized.
        Num_t s8 = number_sqrt(number_from_int(8));
        Num_t twice_s2 = number_mul(two, s2);
        CHECK(number_equal(s8, twice_s2));
        Num_t q = number_div(s8, s2);
        expect_eq(q, (two));
        Num_t three = number_from_int(3);
        Num_t sum = number_add(s8, s2); // sqrt(8) + sqrt(2) == 3*sqrt(2)
        Num_t three_s2 = number_mul(three, s2);
        CHECK(number_equal(sum, three_s2));

        // Field arithmetic in Q(sqrt 2): (1+sqrt2)^2 = 3+2sqrt2,
        // and 1/(1+sqrt2) = sqrt2 - 1.
        Num_t one = number_from_int(1);
        Num_t onePlus = number_add(one, s2);
        Num_t sq = number_mul(onePlus, onePlus);
        Num_t three2 = number_from_int(3);
        Num_t two_s2 = number_mul(two, s2);
        Num_t expected = number_add(three2, two_s2);
        CHECK(number_equal(sq, expected));
        Num_t inv = number_inverse(onePlus);
        Num_t s2m1 = number_sub(s2, one);
        CHECK(number_equal(inv, s2m1));

        // sqrt(3)/sqrt(27) == 1/3 and sqrt(2)*sqrt(3) == sqrt(6)
        Num_t s3 = number_sqrt(number_from_int(3));
        Num_t s27 = number_sqrt(number_from_int(27));
        Num_t third = number_div(s3, s27);
        expect_eq(third, number_from_ratio(1, 3));
        Num_t s6a = number_mul(s2, s3);
        Num_t s6b = number_sqrt(number_from_int(6));
        CHECK(number_equal(s6a, s6b));

        // Incommensurable sums fall back to the general IRRATIONAL engine.
        Num_t sum23 = number_add(s2, s3);
        CHECK(!number_is_rational(sum23));
        expect_str(sum23, 10, "3.1462643699", 0);
        // Genuine domain errors are unaffected by the general fallback.
        expect_error(number_sqrt(number_from_int(-1)));

        // Nested sqrt now evaluates via the general fallback instead of
        // erroring: sqrt(sqrt(2)) == 2^(1/4).
        Num_t nested = number_sqrt(s2); // borrows s2
        CHECK(!number_is_rational(nested));
        expect_str(nested, 20, "1.18920711500272106672", 0);
        // A negative real (still no closed sqrt form, still a domain error).
        Num_t neg_s2 = number_neg(s2);
        expect_error(number_sqrt(neg_s2));

        CHECK(number_compare(s2, two) < 0);
        Num_t one_and_half = number_from_ratio(3, 2);
        CHECK(number_compare(s2, one_and_half) < 0);
        CHECK(number_compare(one_and_half, s2) > 0);
    }

    // --- Tier 3: pi ---
    {
        Num_t pi = number_pi();
        CHECK(!number_is_rational(pi));
        expect_str((pi), 10, "3.1415926536", 0);
        expect_str((pi), 4, "3.1416", 0);
        CHECK(number_to_double(pi) == 3.141592653589793);

        Num_t diff = number_sub(pi, pi); // pi - pi == 0, exactly rational
        CHECK(number_is_rational(diff));
        expect_eq(diff, number_from_int(0));

        Num_t two = number_from_int(2);
        Num_t twopi = number_mul(two, pi);
        Num_t sum = number_add(pi, pi);
        CHECK(number_equal(sum, twopi));
        expect_str((twopi), 6, "6.283185", 0);
        Num_t half = number_div(twopi, two);
        CHECK(number_equal(half, pi));

        Num_t negpi = number_neg(pi);
        CHECK(number_sign(negpi) == -1);
        expect_str((negpi), 5, "-3.14159", 0);
        Num_t abspi = number_abs(negpi);
        CHECK(number_equal(abspi, pi));

        // pi vs. rationals and algebraic numbers: exact comparisons.
        Num_t approx = number_from_ratio(22, 7);
        CHECK(number_compare(pi, approx) < 0); // pi < 22/7
        CHECK(number_compare(approx, pi) > 0);
        Num_t s2 = number_sqrt(two);
        CHECK(number_compare(s2, pi) < 0); // via interval refinement

        // Outside the closed symbolic forms: the general IRRATIONAL engine.
        Num_t pi_plus_s2 = number_add(pi, s2);
        CHECK(!number_is_rational(pi_plus_s2));
        expect_str(pi_plus_s2, 10, "4.5558062160", 0);
        Num_t pi_sq = number_mul(pi, pi);
        CHECK(!number_is_rational(pi_sq));
        expect_str(pi_sq, 10, "9.8696044011", 0);
        Num_t inv_pi = number_inverse(pi);
        CHECK(!number_is_rational(inv_pi));
        expect_str(inv_pi, 10, "0.3183098862", 0);
    }

    // --- Named constants: NUMBER_ZERO/ONE/NEG_ONE, number_tau, number_sqrt2 ---
    {
        Num_t z = number_from_int(0), o = number_from_int(1), negone = number_from_int(-1);
        CHECK(NUMBER_ZERO.bits == z.bits);
        CHECK(NUMBER_ONE.bits == o.bits);
        CHECK(NUMBER_NEG_ONE.bits == negone.bits);

        // number_pi/number_tau/number_sqrt2 are cached singletons (number.c):
        // repeated calls, retains, and drops of the *same* underlying heap
        // object must never leave a dangling reference or corrupt the cache
        // for the next call. Exercise that cycle hard, since ASan/UBSan (this
        // build) catch any use-after-free or leak here.
        for (int i = 0; i < 50; i++) {
            Num_t a = number_pi();
            Num_t b = number_pi();
            CHECK(number_equal(a, b)); // same cached value every time
        }

        Num_t pi = number_pi();
        Num_t tau = number_tau();
        CHECK(!number_is_rational(tau));
        Num_t two_pi = number_mul(number_from_int(2), pi);
        CHECK(number_equal(tau, two_pi)); // tau == 2*pi, exactly
        expect_str((tau), 10, "6.2831853072", 0);

        Num_t sq2a = number_sqrt2();
        Num_t sq2b = number_sqrt(number_from_int(2));
        CHECK(number_equal(sq2a, sq2b));
        Num_t prod = number_mul(sq2a, sq2a);
        CHECK(number_is_rational(prod)); // sqrt(2)*sqrt(2) == 2, exactly
        expect_eq(prod, number_from_int(2));

        // Precision refined by one call must still be correct when read
        // back from an entirely separate later call to the same constant.
        Num_t pi_hi = number_pi();
        char *s = number_to_string(pi_hi, 30, NULL);
        // Correctly rounded (not truncated) at digit 30: pi's 31st digit is
        // 5 followed by more nonzero digits, so ...383279|50288... rounds
        // ...79 up to ...80.
        CHECK(strncmp(s, "3.141592653589793238462643383280", 32) == 0);
    }

    // --- Tier 3: general engine (sin/cos/tan/asin/acos/atan/exp/ln/log10/
    // sinh/cosh/tanh/pow) ---
    {
        // Rational escapes: exact, no IRRATIONAL node built at all.
        expect_eq(number_sin(number_from_int(0)), number_from_int(0));
        expect_eq(number_cos(number_from_int(0)), number_from_int(1));
        expect_eq(number_tan(number_from_int(0)), number_from_int(0));
        expect_eq(number_atan(number_from_int(0)), number_from_int(0));
        expect_eq(number_asin(number_from_int(0)), number_from_int(0));
        expect_eq(number_exp(number_from_int(0)), number_from_int(1));
        expect_eq(number_ln(number_from_int(1)), number_from_int(0));
        expect_eq(number_log10(number_from_int(1)), number_from_int(0));
        // log10 is exact for any integer power of ten, positive or negative
        // exponent, not just x == 1. log10(1000) used to print as
        // "3.0000000000" (numerically correct but not recognized as exact);
        // it's exactly 3 now.
        expect_eq(number_log10(number_from_int(1000)), number_from_int(3));
        expect_eq(number_log10(number_from_int(100)), number_from_int(2));
        expect_eq(number_log10(number_from_ratio(1, 100)), number_from_int(-2));
        {
            Num_t n2 = number_log10(number_from_int(2));
            CHECK(!number_is_rational(n2)); // not a power of ten: general engine
        }

        // --- LN(r)/EXP(r) closed symbolic forms ---
        {
            // exp(ln(r)) == r and ln(exp(r)) == r exactly, for a rational r,
            // with no interval refinement needed, and the results collapse
            // straight back to the original rational.
            Num_t three = number_from_int(3);
            Num_t ln3 = number_ln(three);
            CHECK(!number_is_rational(ln3)); // ln(3) is genuinely irrational
            Num_t back = number_exp(ln3);
            expect_eq(back, (three));
            Num_t half = number_from_ratio(1, 2);
            Num_t exp_half = number_exp(half);
            CHECK(!number_is_rational(exp_half));
            Num_t back2 = number_ln(exp_half);
            expect_eq(back2, (half));

            // exp(a)*exp(b) == exp(a+b): a closed EXP(5), not an
            // unrecognized product of two EXP nodes.
            Num_t two = number_from_int(2);
            Num_t e2 = number_exp(two);
            Num_t e3 = number_exp(three);
            Num_t prod = number_mul(e2, e3);
            expect_sym((prod), "exp(5)");
            Num_t e5 = number_exp(number_from_int(5));
            expect_eq((prod), e5);

            // exp(a)/exp(b) == exp(a-b).
            Num_t e2b = number_exp(two);
            Num_t e3b = number_exp(three);
            Num_t quot = number_div(e2b, e3b);
            Num_t e_neg1 = number_exp(number_from_int(-1));
            expect_eq(quot, e_neg1);

            // ln(a) + ln(b) == ln(a*b): 2*ln(2) + ln(3) collapses to ln(12).
            Num_t ln2 = number_ln(two);
            Num_t two_ln2 = number_mul(two, ln2);
            Num_t ln3b = number_ln(three);
            Num_t sum = number_add(two_ln2, ln3b);
            expect_sym((sum), "ln(12)");
            Num_t ln12 = number_ln(number_from_int(12));
            expect_eq(sum, ln12);

            // 2*ln(2) - ln(4) == 0 exactly: integer-coefficient LN terms
            // with different (but related) arguments still combine.
            Num_t ln2b = number_ln(two);
            Num_t two_ln2b = number_mul(two, ln2b);
            Num_t ln4 = number_ln(number_from_int(4));
            expect_eq(number_sub(two_ln2b, ln4), number_from_int(0));

            // ln(1/2) == -ln(2): the < 1 canonicalization.
            Num_t ln_half = number_ln(number_from_ratio(1, 2));
            Num_t ln2c = number_ln(two);
            expect_eq(ln_half, number_neg(ln2c));

            // Non-integer-coefficient LN terms and mismatched-exponent EXP
            // terms don't unify: general fallback, still numerically
            // correct, not a rational.
            Num_t ln3c = number_ln(three);
            Num_t half_ln3 = number_mul(number_from_ratio(1, 2), ln3c);
            Num_t ln2d = number_ln(two);
            Num_t mixed = number_add(ln2d, half_ln3);
            CHECK(!number_is_rational(mixed));
            Num_t e2c = number_exp(two);
            Num_t e3c = number_exp(three);
            Num_t esum = number_add(e2c, e3c);
            CHECK(!number_is_rational(esum));
        }

        expect_eq(number_sinh(number_from_int(0)), number_from_int(0));
        expect_eq(number_cosh(number_from_int(0)), number_from_int(1));
        expect_eq(number_tanh(number_from_int(0)), number_from_int(0));
        expect_eq(number_pow(number_from_int(5), number_from_int(0)), number_from_int(1));
        expect_eq(number_pow(number_from_int(0), number_from_int(0)), number_from_int(1));
        expect_eq(number_pow(number_from_int(0), number_from_int(3)), number_from_int(0));
        expect_eq(number_pow(number_from_int(7), number_from_int(1)), number_from_int(7));

        // sin/cos of a rational multiple of pi with denominator 1 or 2 are
        // exact (the general engine alone could only ever approximate these
        // arbitrarily close to their true value, never prove them exactly
        // zero, e.g. sin(pi) would otherwise print as "0.000...0"). The
        // next block below covers denominators 3, 4, 6 (the sqrt-valued
        // special angles) plus confirming denominators outside {1,2,3,4,6}
        // correctly fall through to the general engine.
        {
            // None of sin/cos/tan consume their argument, so each of these
            // can be reused across multiple calls with no retain needed --
            // just one drop per value at the end.
            Num_t pi = number_pi(), two = number_from_int(2), three = number_from_int(3);
            Num_t half_pi = number_div(pi, two);
            Num_t neg_half_pi = number_neg(half_pi);
            Num_t two_pi = number_mul(two, pi);
            Num_t three_half_pi = number_mul(three, half_pi);

            expect_eq(number_sin(pi), number_from_int(0));
            expect_eq(number_cos(pi), number_from_int(-1));
            expect_eq(number_tan(pi), number_from_int(0));
            expect_eq(number_sin(half_pi), number_from_int(1));
            expect_eq(number_cos(half_pi), number_from_int(0));
            expect_eq(number_sin(neg_half_pi), number_from_int(-1));
            expect_eq(number_sin(two_pi), number_from_int(0));
            expect_eq(number_cos(two_pi), number_from_int(1));
            expect_eq(number_cos(three_half_pi), number_from_int(0));
            expect_error(number_tan(half_pi));
        }

        // Denominators 3, 4, 6: the sqrt(2)/2, sqrt(3)/2 special angles,
        // plus their reflections/rotations around the circle. number_div/
        // mul/neg all BORROW their arguments (see the Arithmetic section of
        // number.h), so every intermediate below is computed once, reused by
        // borrowing (sin/cos/tan/mul/neg never consume), and dropped
        // exactly once at the end; number_retain is only needed where the
        // SAME value is handed to expect_eq (which does consume) more than
        // once as the "expected" side.
        {
            Num_t half = number_from_ratio(1, 2);
            Num_t neg_half = number_neg(half);
            Num_t two = number_from_int(2);
            Num_t three = number_from_int(3);
            Num_t four = number_from_int(4);
            Num_t six = number_from_int(6);
            Num_t sq2 = number_sqrt(two);
            Num_t sqrt2_2 = number_div(sq2, two);
            Num_t sq3 = number_sqrt(three);
            Num_t sqrt3_2 = number_div(sq3, two);
            Num_t neg_sqrt2_2 = number_neg(sqrt2_2);
            Num_t neg_sqrt3_2 = number_neg(sqrt3_2);

            Num_t pi = number_pi();
            Num_t pi6 = number_div(pi, six);
            Num_t pi4 = number_div(pi, four);
            Num_t pi3 = number_div(pi, three);

            expect_eq(number_sin(pi6), (half));
            expect_eq(number_cos(pi6), (sqrt3_2));

            expect_eq(number_sin(pi4), (sqrt2_2));
            expect_eq(number_cos(pi4), (sqrt2_2));
            expect_eq(number_tan(pi4), number_from_int(1));

            expect_eq(number_sin(pi3), (sqrt3_2));
            expect_eq(number_cos(pi3), (half));
            expect_sym(number_tan(pi3), "sqrt(3)");

            // Reflections: 2pi/3, 3pi/4, 5pi/6, 7pi/6, 4pi/3, and a negative angle.
            Num_t two_pi3 = number_mul(two, pi3);
            expect_eq(number_sin(two_pi3), (sqrt3_2));
            expect_eq(number_cos(two_pi3), (neg_half));

            Num_t three_pi4 = number_mul(three, pi4);
            expect_eq(number_sin(three_pi4), (sqrt2_2));
            expect_eq(number_cos(three_pi4), (neg_sqrt2_2));

            Num_t five = number_from_int(5);
            Num_t five_pi6 = number_mul(five, pi6);
            expect_eq(number_sin(five_pi6), (half));
            expect_eq(number_cos(five_pi6), (neg_sqrt3_2));

            Num_t seven = number_from_int(7);
            Num_t seven_pi6 = number_mul(seven, pi6);
            expect_eq(number_sin(seven_pi6), (neg_half));

            Num_t four_pi3 = number_mul(four, pi3);
            expect_eq(number_cos(four_pi3), (neg_half));

            Num_t neg_pi6 = number_neg(pi6);
            expect_eq(number_sin(neg_pi6), (neg_half));
            Num_t neg_pi3 = number_neg(pi3);
            expect_eq(number_cos(neg_pi3), (half));
        }

        // Denominators outside {1,2,3,4,6} (needing pi/12's sqrt6+sqrt2 form,
        // or not a "nice" angle at all) fall through to the general engine:
        // not exact rationals/symbolic forms, but still numerically correct.
        {
            Num_t pi = number_pi();
            Num_t pi12 = number_div(pi, number_from_int(12));
            Num_t s1 = number_sin(pi12);
            CHECK(!number_is_rational(s1));
            expect_str(s1, 30, "0.258819045102520762348898837624", 0);
            Num_t pi5 = number_div(pi, number_from_int(5));
            Num_t s2 = number_sin(pi5);
            CHECK(!number_is_rational(s2));
            expect_str(s2, 30, "0.587785252292473129168705954639", 0);
        }

        // asin/acos(+-1) are exact via the closed pi/2 symbolic form, not
        // the general engine. number_is_rational is still false (they're
        // irrational), but the value carries no approximation error.
        Num_t asin1 = number_asin(number_from_int(1));
        CHECK(!number_is_rational(asin1));
        expect_sym(asin1, "pi/2");
        expect_sym(number_asin(number_from_int(-1)), "-pi/2");
        expect_str(number_acos(number_from_int(1)), 5, "0", 1);
        expect_sym(number_acos(number_from_int(-1)), "pi");
        expect_sym(number_acos(number_from_int(0)), "pi/2");

        // asin/atan/acos of the standard special ratios (mirroring the
        // sin/cos pi-multiple table above): 1/2, sqrt(2)/2, sqrt(3)/2 for
        // asin/acos; sqrt(3)/3, 1, sqrt(3) for atan.
        {
            Num_t half = number_from_ratio(1, 2);
            Num_t sq2 = number_sqrt(number_from_int(2));
            Num_t sqrt2_2 = number_div(sq2, number_from_int(2));
            Num_t sqrt3 = number_sqrt(number_from_int(3));
            Num_t sqrt3_2 = number_div(sqrt3, number_from_int(2));
            Num_t sqrt3_3 = number_div(sqrt3, number_from_int(3));

            expect_sym(number_asin(half), "pi/6");
            Num_t neg_half = number_neg(half);
            expect_sym(number_asin(neg_half), "-pi/6");
            expect_sym(number_asin(sqrt2_2), "pi/4");
            expect_sym(number_asin(sqrt3_2), "pi/3");

            expect_sym(number_atan(number_from_int(1)), "pi/4");
            expect_sym(number_atan(sqrt3), "pi/3");
            expect_sym(number_atan(sqrt3_3), "pi/6");
            Num_t neg_sqrt3 = number_neg(sqrt3);
            expect_sym(number_atan(neg_sqrt3), "-pi/3");

            expect_sym(number_acos(sqrt2_2), "pi/4");
            expect_sym(number_acos(half), "pi/3");

            // Not a special ratio: falls through to the general engine.
            Num_t a13 = number_asin(number_from_ratio(1, 3));
            CHECK(!number_is_rational(a13));
            expect_str(a13, 15, "0.339836909454122", 0);
        }

        // pow's exact ladder: integer exponents (any sign, any base) and
        // y=1/2 stay exact rationals/symbolic forms, never touching exp/ln.
        Num_t p1 = number_pow(number_from_int(2), number_from_int(10));
        CHECK(number_is_rational(p1));
        expect_str(p1, 5, "1024", 1);
        Num_t p2 = number_pow(number_from_int(2), number_from_int(-3));
        CHECK(number_is_rational(p2));
        expect_str(p2, 5, "0.125", 1);
        Num_t p3 = number_pow(number_from_int(-2), number_from_int(3));
        CHECK(number_is_rational(p3));
        expect_str(p3, 5, "-8", 1);
        expect_sym(number_pow(number_from_int(2), number_from_ratio(1, 2)), "sqrt(2)");

        // pow's exact ladder, continued: rational exponents p/q with q > 2
        // stay exact when x is itself a rational q-th power (numerator and
        // denominator each a perfect q-th power), e.g. 8^(1/3) == 2, not
        // an unsimplified exp(ln(8)/3). Odd q accepts a negative base (the
        // real q-th root exists); even q rejects it, same as sqrt.
        Num_t p4 = number_pow(number_from_int(8), number_from_ratio(1, 3));
        CHECK(number_is_rational(p4));
        expect_str(p4, 5, "2", 1);
        Num_t p5 = number_pow(number_from_int(27), number_from_ratio(2, 3));
        CHECK(number_is_rational(p5));
        expect_str(p5, 5, "9", 1);
        Num_t p6 = number_pow(number_from_int(4), number_from_ratio(3, 2));
        CHECK(number_is_rational(p6));
        expect_str(p6, 5, "8", 1);
        Num_t p7 = number_pow(number_from_int(-8), number_from_ratio(1, 3));
        CHECK(number_is_rational(p7));
        expect_str(p7, 5, "-2", 1);
        Num_t p8 = number_pow(number_from_int(-8), number_from_ratio(2, 3));
        CHECK(number_is_rational(p8));
        expect_str(p8, 5, "4", 1);
        Num_t p9 = number_pow(number_from_ratio(1, 8), number_from_ratio(1, 3));
        CHECK(number_is_rational(p9));
        expect_str(p9, 5, "0.5", 1);
        expect_error(number_pow(number_from_int(-4), number_from_ratio(1, 4)));
        // Not a perfect cube: falls through to the general engine, still
        // numerically correct, just not exact.
        Num_t p10 = number_pow(number_from_int(2), number_from_ratio(1, 3));
        CHECK(!number_is_rational(p10));
        expect_str(p10, 15, "1.259921049894873", 0);

        // Decimal digits against independently computed reference values
        // (mpmath, at 30+ digits): exercises the general engine's actual
        // precision budgets, not just "looks about right".
        expect_str(number_exp(number_from_int(1)), 15, "2.718281828459045", 0);
        expect_str(number_exp(number_from_int(-1)), 15, "0.367879441171442", 0);
        expect_str(number_ln(number_from_int(2)), 15, "0.693147180559945", 0);
        expect_str(number_ln(number_from_ratio(1, 2)), 15, "-0.693147180559945", 0);
        expect_str(number_log10(number_from_int(2)), 15, "0.301029995663981", 0);
        expect_str(number_sin(number_from_int(1)), 30, "0.841470984807896506652502321630", 0);
        expect_str(number_cos(number_from_int(1)), 30, "0.540302305868139717400936607443", 0);
        // Large-argument range reduction: pi needs boosted precision that
        // scales with the argument's own magnitude, not just the output
        // precision (see reduce_mod_2pi). This is the case that would
        // silently give the wrong low digits if that budget were wrong.
        expect_str(number_sin(number_from_int(1000000)), 30, "-0.349993502171292952117652486781", 0);
        expect_str(number_cos(number_from_int(1000000)), 30, "0.936752127533144786938532535075", 0);
        expect_str(number_tan(number_from_int(1)), 25, "1.5574077246549022305069748", 0);
        expect_str(number_atan(number_from_int(2)), 30, "1.107148717794090503017065460179", 0);
        expect_str(number_atan(number_from_int(10)), 30, // |x| > 1: reciprocal reduction
                   "1.471127674303734591852875571762", 0);
        expect_str(number_asin(number_from_ratio(1, 2)), 30, "0.523598775598298873077107230547", 0);
        expect_str(number_acos(number_from_ratio(1, 2)), 30, "1.047197551196597746154214461093", 0);
        expect_str(number_sinh(number_from_int(1)), 30, "1.175201193643801456882381850596", 0);
        expect_str(number_cosh(number_from_ratio(1, 2)), 30, "1.127625965206380785226225161403", 0);
        expect_str(number_tanh(number_from_int(1)), 30, "0.761594155955764888119458282605", 0);
        expect_str(number_pow(number_from_int(3), number_from_ratio(1, 3)), 30, "1.442249570307408382321638310780", 0);

        // pi + sqrt(2) etc. now evaluate instead of erroring (the general
        // fallback's whole point). Decimal digits and is_rational both
        // confirm this isn't a symbolic form.
        Num_t pi = number_pi();
        Num_t s2 = number_sqrt(number_from_int(2));
        Num_t sum = number_add(pi, s2);
        CHECK(!number_is_rational(sum));
        expect_str(sum, 10, "4.5558062160", 0);

        // sqrt of an already-irrational value (pi itself): also the
        // general fallback, not a closed form.
        Num_t sqrt_pi = number_sqrt(pi); // borrows pi
        CHECK(!number_is_rational(sqrt_pi));
        expect_str(sqrt_pi, 30, "1.772453850905516027298167483341", 0);

        // Domain errors: ln/log10 of a non-positive value, asin/acos
        // outside [-1,1], pow's 0^negative and negative-base/non-integer-
        // exponent cases, and a pole (tan(pi/2), via cos(pi/2) refining to
        // "indistinguishable from zero" at the precision cap).
        expect_error(number_ln(number_from_int(0)));
        expect_error(number_ln(number_from_int(-5)));
        expect_error(number_log10(number_from_int(-1)));
        expect_error(number_asin(number_from_int(2)));
        expect_error(number_acos(number_from_ratio(-3, 2)));
        expect_error(number_pow(number_from_int(0), number_from_int(-1)));
        expect_error(number_pow(number_from_int(-2), number_from_ratio(1, 2)));
        {
            Num_t pi2 = number_pi(), two = number_from_int(2);
            Num_t half_pi = number_div(pi2, two);
            expect_error(number_tan(half_pi)); // borrows, doesn't consume
        }
        // asin/acos are built directly on number_sqrt with no separate
        // rational-only guard, so now that number_sqrt handles irrational
        // input, an already-irrational argument works too instead of
        // erroring: asin(sin(1/2)) == 1/2 (not recognized symbolically, so
        // is_rational is false, but numerically exact to any precision).
        {
            Num_t s05 = number_sin(number_from_ratio(1, 2));
            Num_t back = number_asin(s05); // borrows, doesn't consume
            CHECK(!number_is_rational(back));
            expect_str(back, 20, "0.50000000000000000000", 0);
        }

        // Known, documented gap: identities the general engine doesn't
        // recognize symbolically refine to the precision cap and report
        // "can't decide" rather than proving true. sin(x)^2+cos(x)^2 == 1
        // is mathematically exact but not caught by any identity here, so
        // it should NOT compare equal to the rational 1. If this ever
        // starts passing, it means a real simplification was added and
        // this test (and its comment) should be revisited, not silently
        // deleted.
        {
            Num_t x = number_from_int(2);
            Num_t sinx = number_sin((x));
            Num_t cosx = number_cos(x);
            Num_t sin2 = number_mul(sinx, sinx);
            Num_t cos2 = number_mul(cosx, cosx);
            Num_t sum2 = number_add(sin2, cos2);
            CHECK(!number_equal(sum2, number_from_int(1)));
        }
    }

    // --- Symbolic form ---
    {
        expect_sym(number_from_int(42), "42");
        expect_sym(number_from_int(-17), "-17");
        expect_sym(number_from_ratio(1, 3), "1/3");
        expect_sym(number_from_ratio(-7, 2), "-7/2");
        expect_sym(NUMBER_ERROR, "(error)");
        expect_sym(factorial(25), "15511210043330985984000000");

        Num_t pi = number_pi();
        expect_sym((pi), "pi");
        expect_sym(number_neg(pi), "-pi");
        Num_t two = number_from_int(2);
        expect_sym(number_mul(two, pi), "2*pi");
        Num_t one = number_from_int(1);
        expect_sym(number_add(one, pi), "1 + pi");
        expect_sym(number_div(pi, two), "pi/2");
        Num_t twothirds = number_from_ratio(2, 3);
        expect_sym(number_mul(twothirds, pi), "2*pi/3");

        Num_t six = number_from_int(6);
        expect_sym(number_sqrt(six), "sqrt(6)"); // cf. sin(5+1) -> sin(6)
        Num_t s2 = number_sqrt(two);
        expect_sym((s2), "sqrt(2)");
        Num_t three = number_from_int(3);
        expect_sym(number_sub(three, s2), "3 - sqrt(2)");
        expect_sym(number_from_string("0.5"), "1/2"); // fraction, not decimal

        // The golden ratio: (1 + sqrt(5)) / 2
        Num_t five = number_from_int(5);
        Num_t s5 = number_sqrt(five);
        Num_t top = number_add(one, s5);
        expect_sym(number_div(top, two), "1/2 + sqrt(5)/2");
    }

    // --- TeX form ---
    {
        expect_tex(number_from_int(42), "42");
        expect_tex(number_from_int(-17), "-17");
        expect_tex(number_from_ratio(1, 3), "\\frac{1}{3}");
        expect_tex(number_from_ratio(-7, 2), "-\\frac{7}{2}");
        expect_tex(NUMBER_ERROR, "\\text{error}");
        expect_tex(factorial(25), "15511210043330985984000000");

        Num_t pi = number_pi();
        expect_tex((pi), "\\pi");
        expect_tex(number_neg(pi), "-\\pi");
        Num_t two = number_from_int(2);
        expect_tex(number_mul(two, pi), "2\\pi");
        Num_t one = number_from_int(1);
        expect_tex(number_add(one, pi), "1 + \\pi");
        expect_tex(number_div(pi, two), "\\frac{\\pi}{2}");
        Num_t twothirds = number_from_ratio(2, 3);
        expect_tex(number_mul(twothirds, pi), "\\frac{2\\pi}{3}");

        Num_t six = number_from_int(6);
        expect_tex(number_sqrt(six), "\\sqrt{6}");
        Num_t s2 = number_sqrt(two);
        Num_t three = number_from_int(3);
        expect_tex(number_sub(three, s2), "3 - \\sqrt{2}");

        // The golden ratio again: (1 + sqrt(5)) / 2
        Num_t five = number_from_int(5);
        Num_t s5 = number_sqrt(five);
        Num_t top = number_add(one, s5);
        expect_tex(number_div(top, two), "\\frac{1}{2} + \\frac{\\sqrt{5}}{2}");

        // General IRRATIONAL nodes: expression trees in TeX form.
        expect_tex(number_sin(two), "\\sin(2)");
        expect_tex(number_exp(two), "e^{2}");
        expect_tex(number_ln(two), "\\ln(2)");
        expect_tex(number_log10(two), "\\frac{\\ln(2)}{\\ln(10)}");
        Num_t s2b = number_sqrt(two);
        expect_tex(number_add(pi, s2b), "\\pi + \\sqrt{2}");
    }

    // --- Power collapsing: x*x*x displays as x^3 ---
    {
        Num_t pi = number_pi();
        Num_t two = number_from_int(2);

        Num_t pi2 = number_mul(pi, pi); // bitwise-identical operands (pi is a singleton)
        expect_sym((pi2), "pi^2");
        expect_tex((pi2), "\\pi^{2}");
        expect_sym(number_mul(pi2, pi), "pi^3");
        // int_pow's square-and-multiply tree ((pi * (pi*pi)^2)) flattens too
        Num_t five = number_from_int(5);
        expect_sym(number_pow(pi, five), "pi^5");
        expect_tex(number_pow(pi, five), "\\pi^{5}");
        // 100 = 1100100 in binary: int_pow's first accumulator step is
        // 1 * pi^4, which must not leave a "1 *" factor in the tree
        Num_t hundred = number_from_int(100);
        expect_sym(number_pow(pi, hundred), "pi^100");

        // Separately-built nodes: distinct pointers, same shape
        Num_t sin_a = number_sin(two);
        Num_t sin_b = number_sin(two);
        expect_sym(number_mul(sin_a, sin_b), "sin(2)^2");
        expect_tex(number_mul(sin_a, sin_b), "\\sin(2)^{2}");

        // Distinct factors keep the infix chain around the collapsed power
        expect_sym(number_mul(pi2, sin_a), "pi^2*sin(2)");
        expect_tex(number_mul(pi2, sin_a), "\\pi^{2} \\cdot \\sin(2)");

        // Bases that bind looser than '^' get parenthesized: a real-kind
        // form in both representations, and e^{2} only in TeX (where a bare
        // double superscript would be invalid)
        Num_t one = number_from_int(1);
        Num_t onepi = number_add(one, pi);
        expect_sym(number_mul(onepi, onepi), "(1 + pi)^2");
        expect_tex(number_mul(onepi, onepi), "(1 + \\pi)^{2}");
        // exp(pi), not exp(2): a rational argument now unifies exp(x)*exp(x)
        // into the closed EXP(2x) form (see real_mul), so it wouldn't reach
        // this IRR_MUL/IRR_EXP parenthesization path at all, so pi keeps this
        // exercising the general-fallback case.
        Num_t exp_a = number_exp(pi);
        Num_t exp_b = number_exp(pi);
        expect_sym(number_mul(exp_a, exp_b), "exp(pi)^2");
        expect_tex(number_mul(exp_a, exp_b), "(e^{\\pi})^{2}");

        // tan(2) is a DIV node: already parenthesized symbolically, but
        // \frac needs wrapping before an exponent
        Num_t tan_a = number_tan(two);
        Num_t tan_b = number_tan(two);
        expect_sym(number_mul(tan_a, tan_b), "(sin(2)/cos(2))^2");
        expect_tex(number_mul(tan_a, tan_b), "(\\frac{\\sin(2)}{\\cos(2)})^{2}");

        // Factors that aren't provably identical don't collapse
        Num_t three = number_from_int(3);
        Num_t sin2 = number_sin(two);
        Num_t sin3 = number_sin(three);
        expect_sym(number_mul(sin2, sin3), "sin(2)*sin(3)");
    }

    // --- Infix operands that render looser than their context get parens ---
    {
        Num_t pi = number_pi();
        Num_t one = number_from_int(1);
        Num_t two = number_from_int(2);
        Num_t onepi = number_add(one, pi); // "1 + pi"
        Num_t s2 = number_sqrt2();
        Num_t onesqrt2 = number_add(one, s2); // "1 + sqrt(2)"
        Num_t twopi = number_mul(two, pi); // "2*pi"
        Num_t sin2 = number_sin(two);

        // The reported bug: (1+pi)*(1+sqrt(2)) printed as "(1 + pi * 1 + sqrt(2))"
        expect_sym(number_mul(onepi, onesqrt2), "(1 + pi)*(1 + sqrt(2))");
        expect_tex(number_mul(onepi, onesqrt2), "(1 + \\pi) \\cdot (1 + \\sqrt{2})");
        expect_sym(number_mul(onepi, sin2), "(1 + pi)*sin(2)");

        // Division: a sum on either side, and even a product on the right
        expect_sym(number_div(sin2, onepi), "sin(2)/(1 + pi)");
        expect_sym(number_div(onepi, sin2), "(1 + pi)/sin(2)");
        expect_sym(number_div(sin2, twopi), "sin(2)/(2*pi)");
        expect_sym(number_div(twopi, sin2), "2*pi/sin(2)"); // left product binds fine

        // Subtraction/addition: sin(2) - (1+pi) becomes ADD of the negated
        // form, whose "-1 - pi" rendering leads with a sign
        expect_sym(number_sub(sin2, onepi), "sin(2) + (-1 - pi)");
        expect_sym(number_add(sin2, onepi), "sin(2) + (1 + pi)");
        expect_tex(number_add(sin2, onepi), "\\sin(2) + (1 + \\pi)");

        // IRRATIONAL infix nodes no longer self-parenthesize, so contexts
        // must wrap them like any other loose-binding operand
        Num_t pisum = number_add(pi, s2); // "pi + sqrt(2)": a SUM node
        expect_sym((pisum), "pi + sqrt(2)");
        expect_sym(number_mul(pisum, sin2), "(pi + sqrt(2))*sin(2)");
        expect_sym(number_div(sin2, pisum), "sin(2)/(pi + sqrt(2))");
        Num_t pi2 = number_mul(pi, pi);
        expect_sym(number_div(sin2, pi2), "sin(2)/pi^2"); // a lone power binds like an atom
        Num_t pisin = number_mul(pi, sin2);
        expect_sym(number_div(sin2, pisin), "sin(2)/(pi*sin(2))");
    }

    // --- Reference counting ---
    {
        Num_t x = factorial(25);
        Num_t y = (x);
        expect_str(y, 0, "15511210043330985984000000", 1); // still alive via y
    }

    // --- Rounding: floor/ceil/trunc/round across all tiers ---
    {
        // Small rationals, both signs, against the mathematical definitions.
        struct {
            int64_t n, d, floor, ceil, trunc, round;
        } cases[] = {
            {0, 1, 0, 0, 0, 0},          {7, 1, 7, 7, 7, 7},    {-7, 1, -7, -7, -7, -7},
            {7, 2, 3, 4, 3, 4}, // 3.5: tie -> 4 (even)
            {-7, 2, -4, -3, -3, -4}, // -3.5: tie -> -4 (even)
            {5, 2, 2, 3, 2, 2}, // 2.5: tie -> 2 (even)
            {-5, 2, -3, -2, -2, -2},     {1, 3, 0, 1, 0, 0},    {-1, 3, -1, 0, 0, 0},
            {2, 3, 0, 1, 0, 1},          {-2, 3, -1, 0, 0, -1}, {99, 10, 9, 10, 9, 10},
            {-99, 10, -10, -9, -9, -10},
        };
        for (size_t i = 0; i < sizeof(cases) / sizeof(*cases); i++) {
            Num_t x = number_from_ratio(cases[i].n, cases[i].d);
            expect_eq(number_floor(x), number_from_int(cases[i].floor));
            expect_eq(number_ceil(x), number_from_int(cases[i].ceil));
            expect_eq(number_trunc(x), number_from_int(cases[i].trunc));
            expect_eq(number_round(x), number_from_int(cases[i].round));
        }

        // Big rationals: 25!/29 has a huge inexact quotient (29 is prime
        // and > 25, so it divides nothing in 25!); floor/ceil bracket it
        // and differ by exactly 1. 25! itself (an integer bigrat) rounds
        // to itself under every mode.
        Num_t f = factorial(25);
        Num_t twentynine = number_from_int(29);
        Num_t x = number_div(f, twentynine);
        Num_t fl = number_floor(x), ce = number_ceil(x);
        expect_eq(number_sub(ce, fl), number_from_int(1)); // x isn't an integer
        expect_str((fl), 0, "534869311838999516689655", 1);
        expect_eq(number_floor(f), (f));
        expect_eq(number_round(f), (f));
        Num_t nf = number_neg(f);
        expect_eq(number_trunc(nf), (nf));

        // Symbolic irrationals: decided by refinement, exactly.
        Num_t pi = number_pi();
        expect_eq(number_floor(pi), number_from_int(3));
        expect_eq(number_ceil(pi), number_from_int(4));
        expect_eq(number_round(pi), number_from_int(3));
        Num_t npi = number_neg(pi);
        expect_eq(number_floor(npi), number_from_int(-4));
        expect_eq(number_ceil(npi), number_from_int(-3));
        expect_eq(number_trunc(npi), number_from_int(-3));
        expect_eq(number_round(npi), number_from_int(-3));
        Num_t s2 = number_sqrt2();
        expect_eq(number_floor(s2), number_from_int(1));
        expect_eq(number_round(s2), number_from_int(1));

        // General IRRATIONAL nodes, including trunc straddling zero: sin(1)
        // is in (0, 1) and -sin(1) in (-1, 0), so all four roundings are
        // decided by refinement (and trunc of both is 0).
        Num_t one = number_from_int(1);
        Num_t s = number_sin(one);
        expect_eq(number_floor(s), number_from_int(0));
        expect_eq(number_ceil(s), number_from_int(1));
        expect_eq(number_trunc(s), number_from_int(0));
        expect_eq(number_round(s), number_from_int(1)); // sin(1) = 0.841...
        Num_t ns = number_neg(s);
        expect_eq(number_trunc(ns), number_from_int(0));
        expect_eq(number_round(ns), number_from_int(-1));
        Num_t e = number_exp(one);
        expect_eq(number_round(e), number_from_int(3)); // e = 2.718...

        // Errors propagate.
        expect_error(number_floor(NUMBER_ERROR));
        expect_error(number_round(NUMBER_ERROR));
    }

    // --- number_mod ---
    {
        // Floored modulus: result takes b's sign, and a == b*floor(a/b) +
        // mod(a, b) exactly.
        struct {
            int64_t an, ad, bn, bd, rn, rd;
        } cases[] = {
            {7, 1, 3, 1, 1, 1},    {-7, 1, 3, 1, 2, 1}, {7, 1, -3, 1, -2, 1},
            {-7, 1, -3, 1, -1, 1}, {6, 1, 3, 1, 0, 1},  {7, 2, 2, 1, 3, 2}, // 3.5 mod 2 = 1.5
            {1, 2, 1, 3, 1, 6}, // 1/2 mod 1/3 = 1/6
            {-1, 2, 1, 3, 1, 6}, // -1/2 mod 1/3 = 1/6 (floored, not truncated)
        };
        for (size_t i = 0; i < sizeof(cases) / sizeof(*cases); i++) {
            Num_t a = number_from_ratio(cases[i].an, cases[i].ad);
            Num_t b = number_from_ratio(cases[i].bn, cases[i].bd);
            Num_t m = number_mod(a, b);
            expect_eq((m), number_from_ratio(cases[i].rn, cases[i].rd));
            // The defining identity, reconstructed exactly.
            Num_t q = number_div(a, b);
            Num_t qi = number_floor(q);
            Num_t bq = number_mul(b, qi);
            expect_eq(number_add(bq, m), (a));
        }

        // Range reduction against an irrational modulus stays symbolic and
        // exact: 10 mod pi = 10 - 3*pi.
        Num_t ten = number_from_int(10);
        Num_t pi = number_pi();
        Num_t m = number_mod(ten, pi);
        Num_t three = number_from_int(3);
        Num_t threepi = number_mul(three, pi);
        expect_eq((m), number_sub(ten, threepi));

        // b == 0 and error operands.
        Num_t zero = number_from_int(0);
        expect_error(number_mod(ten, zero));
        expect_error(number_mod(NUMBER_ERROR, ten));
        expect_error(number_mod(ten, NUMBER_ERROR));
    }

    // --- Correctly rounded number_to_double for general (DAG-backed)
    // irrationals, against independently-computed nearest doubles. These
    // conversions are served by the hardware double-interval fast path when
    // it's enabled (the default) and by exact bigint bracketing when it
    // isn't (-DNUMBER_NO_DOUBLE_FILTER). The expected values are the
    // same either way, so this cross-checks the two implementations
    // against each other in whichever build is running. References were
    // computed by rounding 75-digit exact decimal expansions to the
    // nearest double, independently of libm (whose sin/exp/ln are not
    // guaranteed correctly rounded and must NOT be used as references
    // here). ---
    {
        Num_t one = number_from_int(1);
        Num_t e1 = number_exp(one);
        CHECK(number_to_double(e1) == 0x1.5bf0a8b145769p+1); // e
        Num_t two = number_from_int(2);
        Num_t l2 = number_ln(two);
        CHECK(number_to_double(l2) == 0x1.62e42fefa39efp-1); // ln 2
        Num_t s1 = number_sin(one);
        CHECK(number_to_double(s1) == 0x1.aed548f090ceep-1); // sin 1
        Num_t a2 = number_atan(two);
        CHECK(number_to_double(a2) == 0x1.1b6e192ebbe44p+0); // atan 2
        Num_t pi = number_pi();
        Num_t epi = number_exp(pi);
        CHECK(number_to_double(epi) == 0x1.724046eb0933ap+4); // e^pi

        // Comparisons decided through the sign filter agree with the exact
        // ordering: e < pi, and e against tight rational brackets either
        // side (e = 2.71828182845904523536...).
        Num_t e2 = number_exp(one);
        CHECK(number_compare(e2, pi) < 0);
        CHECK(number_compare(pi, e2) > 0);
        Num_t lo_ref = number_from_ratio(2718281828, 1000000000);
        Num_t hi_ref = number_from_ratio(2718281829, 1000000000);
        CHECK(number_compare(e2, lo_ref) > 0);
        CHECK(number_compare(e2, hi_ref) < 0);
    }

    // --- number_is_integer / number_to_int64 ---
    {
        bool ok;
        Num_t five = number_from_int(5);
        CHECK(number_is_integer(five));
        CHECK(number_to_int64(five, &ok) == 5 && ok);

        Num_t half = number_from_ratio(1, 2);
        CHECK(!number_is_integer(half));
        CHECK(number_to_int64(half, &ok) == 0 && !ok);

        Num_t zero = number_from_int(0);
        CHECK(number_is_integer(zero));
        CHECK(number_to_int64(zero, &ok) == 0 && ok); // 0 with ok true really is zero

        // int64 boundaries: INT64_MAX and INT64_MIN are bigrats (out of
        // tier 1) that fit exactly; one past either end doesn't.
        Num_t imax = number_from_int(INT64_MAX);
        CHECK(number_is_integer(imax));
        CHECK(number_to_int64(imax, &ok) == INT64_MAX && ok);
        Num_t imin = number_from_int(INT64_MIN);
        CHECK(number_to_int64(imin, &ok) == INT64_MIN && ok);
        Num_t one = number_from_int(1);
        Num_t over = number_add(imax, one);
        CHECK(number_is_integer(over)); // still an integer, just not an int64
        CHECK(number_to_int64(over, &ok) == 0 && !ok);
        Num_t under = number_sub(imin, one);
        CHECK(number_to_int64(under, &ok) == 0 && !ok);

        Num_t big = factorial(25);
        CHECK(number_is_integer(big));
        CHECK(number_to_int64(big, &ok) == 0 && !ok);

        // Irrationals: never provably integer, never convertible.
        Num_t pi = number_pi();
        CHECK(!number_is_integer(pi));
        CHECK(number_to_int64(pi, &ok) == 0 && !ok);
        Num_t s2 = number_sqrt2();
        CHECK(!number_is_integer(s2));

        CHECK(!number_is_integer(NUMBER_ERROR));
        CHECK(number_to_int64(NUMBER_ERROR, &ok) == 0 && !ok);
    }

    // --- log2: exact on powers of two, correctly rounded otherwise ---
    {
        expect_str(number_log2(number_from_int(8)), 5, "3", 1);
        expect_str(number_log2(number_from_int(1024)), 5, "10", 1);
        expect_str(number_log2(number_from_int(2)), 5, "1", 1);
        expect_str(number_log2(number_from_int(1)), 5, "0", 1);
        expect_str(number_log2(number_from_ratio(1, 4)), 5, "-2", 1);
        expect_str(number_log2(number_from_int(3)), 6, "1.584963", 0);
        CHECK(number_is_error(number_log2(number_from_int(0))));
        CHECK(number_is_error(number_log2(number_from_int(-1))));
    }

    // --- atan2: quadrants, axes, and the undefined origin ---
    {
        Num_t pi = number_pi();
        Num_t halfpi = number_mul(pi, NUMBER_SMALL(1, 2));

        Num_t up = number_atan2(number_from_int(1), number_from_int(0)); // +pi/2
        CHECK(number_equal(up, halfpi));

        Num_t down = number_atan2(number_from_int(-1), number_from_int(0)); // -pi/2
        Num_t neghalfpi = number_neg(halfpi);
        CHECK(number_equal(down, neghalfpi));

        Num_t left = number_atan2(number_from_int(0), number_from_int(-1)); // pi
        CHECK(number_equal(left, pi));

        Num_t origin_r = number_atan2(number_from_int(0), number_from_int(1)); // 0
        CHECK(number_is_zero(origin_r));

        Num_t pi4 = number_mul(pi, NUMBER_SMALL(1, 4));
        Num_t a11 = number_atan2(number_from_int(1), number_from_int(1)); // pi/4
        CHECK(number_equal(a11, pi4));

        Num_t pi34 = number_mul(pi, NUMBER_SMALL(3, 4));
        Num_t a1m1 = number_atan2(number_from_int(1), number_from_int(-1)); // 3*pi/4
        CHECK(number_equal(a1m1, pi34));

        CHECK(number_is_error(number_atan2(number_from_int(0), number_from_int(0))));
        expect_str(number_atan2(number_from_int(1), number_from_int(2)), 6, "0.463648", 0);
    }

    // --- gcd/lcm: integers, sign normalization, zeros, rationals, bigints ---
    {
        expect_str(number_gcd(number_from_int(12), number_from_int(18)), 0, "6", 1);
        expect_str(number_lcm(number_from_int(12), number_from_int(18)), 0, "36", 1);
        expect_str(number_gcd(number_from_int(-12), number_from_int(18)), 0, "6", 1);
        expect_str(number_lcm(number_from_int(-4), number_from_int(6)), 0, "12", 1);
        expect_str(number_gcd(number_from_int(0), number_from_int(5)), 0, "5", 1);
        expect_str(number_gcd(number_from_int(5), number_from_int(0)), 0, "5", 1);
        expect_str(number_gcd(number_from_int(0), number_from_int(0)), 0, "0", 1);
        expect_str(number_lcm(number_from_int(0), number_from_int(5)), 0, "0", 1);
        expect_sym(number_gcd(number_from_ratio(1, 2), number_from_ratio(1, 3)), "1/6");
        expect_str(number_lcm(number_from_ratio(1, 2), number_from_ratio(1, 3)), 0, "1", 1);

        Num_t p100 = number_pow(number_from_int(2), number_from_int(100));
        Num_t p60 = number_pow(number_from_int(2), number_from_int(60));
        Num_t g = number_gcd(p100, p60); // 2^60
        CHECK(number_equal(g, p60));
        Num_t l = number_lcm(p100, p60); // 2^100
        CHECK(number_equal(l, p100));

        Num_t s2 = number_sqrt2(), two = number_from_int(2);
        CHECK(number_is_error(number_gcd(s2, two))); // irrational operand -> error
        CHECK(number_is_error(number_lcm(two, s2)));
    }

    // --- min/max: exact where decidable, `digits` fallback for ties ---
    {
        expect_str(number_min(number_from_int(3), number_from_int(5), 10), 0, "3", 1);
        expect_str(number_max(number_from_int(3), number_from_int(5), 10), 0, "5", 1);
        // 1/3 == 0.3333... is larger than 0.333 exactly; the exact compare
        // decides it (digits is not consulted).
        expect_sym(number_min(number_from_ratio(1, 3), number_from_ratio(333, 1000), 2), "333/1000");
        expect_sym(number_max(number_from_ratio(1, 3), number_from_ratio(333, 1000), 2), "1/3");
        // A tie returns the first operand.
        expect_str(number_min(number_from_int(5), number_from_int(5), 4), 0, "5", 1);

        Num_t pi = number_pi(), three = number_from_int(3);
        Num_t mn = number_min(pi, three, 10); // pi > 3
        CHECK(number_equal(mn, three));
        Num_t mx = number_max(pi, three, 10);
        CHECK(number_equal(mx, pi));

        Num_t s2 = number_sqrt2(), f14 = number_from_ratio(7, 5); // sqrt(2) > 1.4
        Num_t mx2 = number_max(s2, f14, 10);
        CHECK(number_equal(mx2, s2));

        CHECK(number_is_error(number_min(NUMBER_ERROR, number_from_int(5), 3)));
        CHECK(number_is_error(number_max(number_from_int(5), NUMBER_ERROR, 3)));
    }

    printf("all %d checks passed\n", checks);
    return 0;
}