lib/strconv: f64tos Ryū shortest float→string (#106 fold-5a)

Graduate f64tos from the lossy fixed-point placeholder to Ryū shortest-
round-trippable (ftos.ha:432 + ftos_ryu.ha). f64tos(n:f64) str, G-format
(void/NONE) — the faithful documented subset (full parametric fftosf is
dead code for G/void/NONE → deferred #64). Ryū core decomposed: struct-
RETURN + scalar params (Hare's r128 idiom; avoids tuple-ABI #163-166).
f64 powers tables [15][2]/[13][2]u64 (2D #156). Zero float literals.
Both E+F encode paths reachable+tested, no dead code.

Graduation (lib-note "don't keep both"): old lossy f64tos deleted; fmt
fprintf_f64_huge "huge"→"9.5e18" (improvement). 5 value-faithful filed-
bug dodges (byte-id, documented): #167/#169/#170/#43/#144. Test 908.
Make test 186/186 incl 990-997 byte-id + combined_ww_fresh.

Drew's strconv 5-fold plan — primary completion (f64tos). f32tos coda
#67 (behind #143); parametric ftosf #64.
This commit is contained in:
2026-05-27 15:39:03 +09:00
parent 81796cd533
commit 0e66073e32
11 changed files with 3219 additions and 762 deletions

View File

@@ -113,7 +113,7 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \
selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \
selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \
selfhost/cmd/wcc/cgendecl.ww \
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/wwdump_ww.d
@@ -135,7 +135,7 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \
selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \
selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \
selfhost/cmd/wcc/cgendecl.ww \
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/w6c_ww.d
@@ -322,7 +322,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_intdiv_signed \
$(BIN)/test_strings_run \
$(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \
$(BIN)/test_decimal_run $(BIN)/test_stof_run \
$(BIN)/test_decimal_run $(BIN)/test_stof_run $(BIN)/test_ftos_run \
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
$(BIN)/test_base32_run $(BIN)/test_base64_run \
$(BIN)/test_adler32_run $(BIN)/test_crc16_run \
@@ -1050,6 +1050,10 @@ $(BIN)/test_stof_run: test/wcc/909_stof_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_ftos_run: test/wcc/908_ftos_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_strings_run: test/wcc/966_strings_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<

View File

@@ -526,11 +526,13 @@ fn formatraw(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = {
//
// Mods scope: width / alignment / pad / sign honored via
// the same raw-then-pad machinery as i64. `prec` ignored —
// strconv.f64tos has no precision knob; graduate when a
// Ryū-shaped strconv lands and mods grows ffmt / fflags.
// `base` ignored (Hare does too — base is int-only).
// NaN/±Inf print as whatever strconv emits today (garbage);
// strconv detection blocked on f64↔u64 bit-reinterpret cgen.
// strconv.f64tos is shortest-G with no precision knob; grows a
// precision path when mods gains ffmt / fflags (the parametric
// ftosf, strconv task #64). `base` ignored (Hare does too —
// base is int-only). NaN/±Inf now render as "nan"/"infinity"
// (Ryū graduation, fold-5); the uniform sign-peel below would
// turn a SHOW_POS nan into "+nan" (a minor fmt-layer edge, not
// strconv's — strconv emits a bare "nan").
let view: str = strconv.f64tos(v);
let neg_flag: bool = false;
if (view.len > 0 && view.ptr[0] == 45u8) { // '-'

View File

@@ -563,7 +563,8 @@ fn closedstream(s: *io.stream) void = {
match (c) { case void => {}; case io.closed => fail(); };
};
// Pins the strconv.f64tos "huge" fallback through the fmt dispatch.
// Pins a large magnitude through the fmt dispatch — post-Ryū-graduation
// (fold-5) this is scientific notation, not the old lossy "huge" fallback.
// Catches any regression where mods accidentally pre-truncate the view.
@test fn fprintf_f64_huge() void = {
let mem: memio.state;
@@ -571,10 +572,10 @@ fn closedstream(s: *io.stream) void = {
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 9.5e18);
match (r) {
case let n: i32 => { if (n != 4) { fail(); }; };
case let n: i32 => { if (n != 6) { fail(); }; }; // "9.5e18"
case io.closed => fail();
};
if (!streq(memio.string(&mem), "huge")) { fail(); };
if (!streq(memio.string(&mem), "9.5e18")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};

549
lib/strconv/ftos.ww Normal file
View File

@@ -0,0 +1,549 @@
// strconv — float→string via Ryū (shortest round-trippable decimal).
// Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) +
// ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams,
// https://doi.org/10.1145/3192366.3192369 — Hare translated it from the
// reference C (https://github.com/ulfjack/ryu); ww follows Hare.
//
// SCOPE — the f64tos shortest-representation subset (Hare's ffmt::G,
// prec=void, fflags::NONE). Two deferrals:
// - f32tos (ftos.ha:448) + its f32 Ryū sub-path (f32todecf32 +
// mulpow5inv/pow5_divpow2 + mulshift32 + the *32 helpers + the
// F32_POW5_*_BITCOUNT defs) → fold-5b (task #67), gated on the #143
// f32-arg-push cgen fix: f32tos must call math.f32bits(n), passing
// an f32 arg, which spills MOVSD (cstage) vs MOVSS (wwstage) →
// 990-997 byte-id break. Not isolatable (f32bits IS the reinterpret),
// only fixable. f32tos IS a real Hare entry — it ships in #67.
// - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs
// io::handle/memio + a `(size|io::error)?` per appendrune (#158);
// for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback
// machinery is provably dead code — `ok` is always true → init_dec/
// compute_round/round unreachable — which bootstrap-coverage rejects).
// The lib note blesses "a documented subset". This file ships ZERO float
// literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the
// wwdump TK_FLOAT embedding concern is moot.
//
// Decomposition divergences (the #163-166 tuple/struct-ABI cluster —
// ww's partial tuple support miscompiles the shapes Hare uses; the
// WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's
// own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for
// u128mul, and fold-4/stof.ww decomposed likewise):
// - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params
// `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple
// return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return
// reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO
// struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww).
// - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)`
// return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the
// math/floats.ww frexpf64 precedent).
// - dead `mulshift64` (tuple-param, never called) + dead
// `F32/F64_DECIMAL_DIGITS` dropped.
//
// Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha):
// - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local
// (stof.ww hex_to_bits precedent).
// - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block.
// comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound
// local + block. `assert()` → `os.assert(cond,msg)`.
// - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]`
// (#155 / #156, stof.ww eisel_lemire precedent).
// - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64
// at each use (Hare promotes; ww is strict — int-machine-word note).
// - a `(N: uint)` cast embedded inside an array subscript `[ ]` is
// rejected by the ww parser → hoist to a named local before the
// index (decimal.ww "hoist size casts" note); see init_dec_mant_exp
// + encode_e_dec.
package strconv;
import math;
import os;
// ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's
// "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition.
// (u128mul + the r128 struct live in stof.ww, fold-4's first consumer;
// reused in-package here.)
fn u128rshift(lo: u64, hi: u64, s: u32) u64 = {
os.assert(s <= 64u32, "strconv.u128rshift: s > 64");
return (hi << (64u64 - (s: u64))) | (lo >> (s: u64));
};
// ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value.
fn pow5fac(v: u64) u32 = {
let value: u64 = v;
let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64)
let n_div_5: u64 = 3689348814741910323u64;
let count: u32 = 0u32;
for (true) {
os.assert(value != 0u64, "strconv.pow5fac: value == 0");
value *= m_inv_5;
if (value > n_div_5) { break; };
count += 1u32;
};
return count;
};
// ref/hare/strconv/ftos_ryu.ha:64.
fn ibool(b: bool) u8 = {
if (b) { return 1u8; };
return 0u8;
};
// ref/hare/strconv/ftos_ryu.ha:66-67.
fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; };
// ref/hare/strconv/ftos_ryu.ha:69.
fn pow2multiple(v: u64, p: u32) bool = {
os.assert(v > 0u64, "strconv.pow2multiple: v == 0");
os.assert(p < 64u32, "strconv.pow2multiple: p >= 64");
return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64;
};
// ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple.
// Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return →
// this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound
// `v_minus` + block.
type ryuv = struct { vp: u64, vr: u64, vm: u64 };
fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = {
let mm: u64 = m << 1u64;
let r0: r128 = u128mul(mm, mul0);
let r1: r128 = u128mul(mm, mul1);
let lo: u64 = r0.lo;
let tmp: u64 = r0.hi;
let mid: u64 = tmp + r1.lo;
let hi: u64 = r1.hi + (ibool(mid < tmp): u64);
let lo2: u64 = lo + mul0;
let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64);
let hi2: u64 = hi + (ibool(mid2 < mid): u64);
let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32));
let v_minus: u64 = 0u64;
if (mm_shift == 1u32) {
let lo3: u64 = lo - mul0;
let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64);
let hi3: u64 = hi - (ibool(mid3 > mid): u64);
v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32));
} else {
let lo3: u64 = lo + lo;
let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64);
let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64);
let lo4: u64 = lo3 - mul0;
let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64);
let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64);
v_minus = u128rshift(mid4, hi4, ((j - 64): u32));
};
let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32));
return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus };
};
// ref/hare/strconv/ftos_ryu.ha:140.
fn log2pow5(e: u32) u32 = {
os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528");
return (e * 1217359u32) >> 19u32;
};
// ref/hare/strconv/ftos_ryu.ha:145-147.
fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; };
fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); };
// ref/hare/strconv/ftos_ryu.ha:149.
fn log10pow2(e: u32) u32 = {
os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650");
return (e * 78913u32) >> 18u32;
};
// ref/hare/strconv/ftos_ryu.ha:154.
fn log10pow5(e: u32) u32 = {
os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620");
return (e * 732923u32) >> 20u32;
};
// ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the
// inverse power of five. 2-tuple kept (works); row-bind → double-index.
fn f64computeinvpow5(i: u32) (u64, u64) = {
let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32);
let base2: u32 = base * (POW5_TABLE_SZ: u32);
let off: u32 = base2 - i;
if (off == 0u32) {
return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]);
};
let m: u64 = POW5_TABLE[off];
let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]);
let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64);
let high1: u64 = r1.hi;
let low1: u64 = r1.lo;
let high0: u64 = r0.hi;
let low0: u64 = r0.lo;
let sum: u64 = high0 + low1;
if (sum < high0) {
high1 += 1u64;
};
let delta: u32 = pow5bits(base2) - pow5bits(i);
let res0: u64 = u128rshift(low0, sum, delta) + 1u64 +
(((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64);
let res1: u64 = u128rshift(sum, high1, delta);
return (res0, res1);
};
// ref/hare/strconv/ftos_ryu.ha:246.
fn f64computepow5(i: u32) (u64, u64) = {
let base: u32 = i / (POW5_TABLE_SZ: u32);
let base2: u32 = base * (POW5_TABLE_SZ: u32);
let off: u32 = i - base2;
if (off == 0u32) {
return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]);
};
let m: u64 = POW5_TABLE[off];
let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]);
let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]);
let high1: u64 = r1.hi;
let low1: u64 = r1.lo;
let high0: u64 = r0.hi;
let low0: u64 = r0.lo;
let sum: u64 = high0 + low1;
if (sum < high0) {
high1 += 1u64;
};
let delta: u32 = pow5bits(i) - pow5bits(base2);
let res0: u64 = u128rshift(low0, sum, delta) +
(((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64);
let res1: u64 = u128rshift(sum, high1, delta);
return (res0, res1);
};
// ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64:
// value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32
// (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second
// field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww
// byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value
// always fits i32 (cast at the init_dec_mant_exp call site).
type decf64 = struct { mantissa: u64, exponent: i64 };
// ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw
// IEEE-754 fields of an f64.
fn f64todecf64(mantissa: u64, exponent: u32) decf64 = {
let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32;
let m2: u64 = 0u64;
if (exponent == 0u32) {
e2 = 1i32 - e2;
m2 = mantissa;
} else {
e2 = (exponent: i32) - e2;
m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa;
};
let accept_bounds: bool = (m2 & 1u64) == 0u64;
let mv: u64 = 4u64 * m2;
let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32;
let vp: u64 = 0u64;
let vr: u64 = 0u64;
let vm: u64 = 0u64;
let e10: i32 = 0i32;
let vm_trailing_zeros: bool = false;
let vr_trailing_zeros: bool = false;
if (e2 >= 0i32) {
let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32);
e10 = q: i32;
let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32;
let i: i32 = -e2 + ((q + k): i32);
let pow5 = f64computeinvpow5(q);
let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift);
vp = res.vp; vr = res.vr; vm = res.vm;
if (q <= 21u32) {
if ((mv - 5u64 * (mv / 5u64)) == 0u64) {
vr_trailing_zeros = pow5multiple(mv, q);
} else if (accept_bounds) {
vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q);
} else {
vp -= (ibool(pow5multiple(mv + 2u64, q)): u64);
};
};
} else {
let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32);
e10 = e2 + (q: i32);
let i: i32 = -e2 - (q: i32);
let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32);
let j: i32 = (q: i32) - k;
let pow5 = f64computepow5(i: u32);
let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift);
vp = res.vp; vr = res.vr; vm = res.vm;
if (q <= 1u32) {
vr_trailing_zeros = true;
if (accept_bounds) {
vm_trailing_zeros = mm_shift == 1u32;
} else {
vp -= 1u64;
};
} else if (q < 63u32) {
vr_trailing_zeros = pow2multiple(mv, q);
};
};
let removed: i32 = 0i32;
let last_removed_digit: u8 = 0u8;
let output: u64 = 0u64;
if (vm_trailing_zeros || vr_trailing_zeros) {
for (true) {
let vpby10: u64 = vp / 10u64;
let vmby10: u64 = vm / 10u64;
if (vpby10 <= vmby10) { break; };
let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32);
let vrby10: u64 = vr / 10u64;
let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32);
vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32);
vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8);
last_removed_digit = (vrmod10: u8);
vr = vrby10; vp = vpby10; vm = vmby10;
removed += 1i32;
};
if (vm_trailing_zeros) {
for (true) {
let vmby10: u64 = vm / 10u64;
let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32);
if (vmmod10 != 0u32) { break; };
let vpby10: u64 = vp / 10u64;
let vrby10: u64 = vr / 10u64;
let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32);
vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8);
last_removed_digit = (vrmod10: u8);
vr = vrby10; vp = vpby10; vm = vmby10;
removed += 1i32;
};
};
if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) {
last_removed_digit = 4u8; // round to even
};
let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros));
let cond2: bool = last_removed_digit >= 5u8;
output = vr + (ibool(cond1 || cond2): u64);
} else {
let round_up: bool = false;
let vpby100: u64 = vp / 100u64;
let vmby100: u64 = vm / 100u64;
if (vpby100 > vmby100) {
let vrby100: u64 = vr / 100u64;
let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32);
round_up = vrmod100 >= 50u32;
vr = vrby100; vp = vpby100; vm = vmby100;
removed += 2i32;
};
for (true) {
let vmby10: u64 = vm / 10u64;
let vpby10: u64 = vp / 10u64;
if (vpby10 <= vmby10) { break; };
let vrby10: u64 = vr / 10u64;
let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32);
round_up = vrmod10 >= 5u32;
vr = vrby10; vp = vpby10; vm = vmby10;
removed += 1i32;
};
output = vr + (ibool(vr == vm || round_up): u64);
};
let exp: i32 = e10 + removed;
return decf64 { exponent = (exp: i64), mantissa = output };
};
// ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void /
// fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase
// arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE
// for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always
// G) and are NOT ported — porting them stubbed would be untested dead
// code. The parametric ftosf/ffmt/fflags surface is deferred (task #64;
// needs a parametric consumer + io::handle + #158). ====
// ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17).
fn declen(n: u64) uint = {
os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17");
if (n >= 100000000000000000u64) { return (18u32: uint); };
if (n >= 10000000000000000u64) { return (17u32: uint); };
if (n >= 1000000000000000u64) { return (16u32: uint); };
if (n >= 100000000000000u64) { return (15u32: uint); };
if (n >= 10000000000000u64) { return (14u32: uint); };
if (n >= 1000000000000u64) { return (13u32: uint); };
if (n >= 100000000000u64) { return (12u32: uint); };
if (n >= 10000000000u64) { return (11u32: uint); };
if (n >= 1000000000u64) { return (10u32: uint); };
if (n >= 100000000u64) { return (9u32: uint); };
if (n >= 10000000u64) { return (8u32: uint); };
if (n >= 1000000u64) { return (7u32: uint); };
if (n >= 100000u64) { return (6u32: uint); };
if (n >= 10000u64) { return (5u32: uint); };
if (n >= 1000u64) { return (4u32: uint); };
if (n >= 100u64) { return (3u32: uint); };
if (n >= 10u64) { return (2u32: uint); };
return (1u32: uint);
};
// ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent)
// into the decimal `d`. `mantissa` is mutated in Hare → local `mant`.
fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = {
// Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded
// inside an array subscript (decimal.ww "hoist size casts" note).
let U_ZERO: uint = (0u32: uint);
let U_ONE: uint = (1u32: uint);
let mant: u64 = mantissa;
let dl: uint = declen(mant);
let i: uint = U_ZERO;
for (i < dl) {
d.digits[dl - i - U_ONE] = (mant % 10u64): u8;
mant /= 10u64;
i += U_ONE;
};
d.nd = (dl: size);
d.dp = (dl: i32) + exponent;
};
// ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the
// *tos static-buffer convention replaces Hare's io::handle sink).
fn putstr(buf: []u8, out: i32, s: str) i32 = {
let o: i32 = out;
let k: i32 = 0i32;
for (k < s.len) {
buf[o] = s[k];
o += 1i32;
k += 1i32;
};
return o;
};
// ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable
// logic only). Writes into `buf` at cursor `out`, returns the new cursor.
fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = {
let o: i32 = out;
let lo: i32 = 0i32;
if (d.dp <= 0i32) { lo = d.dp - 1i32; };
let hi: i32 = d.dp;
if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); };
if (hi > (d.nd: i32) && d.dp <= 0i32) {
hi = (d.nd: i32);
} else if (hi > d.dp && d.dp > 0i32) {
hi = d.dp;
if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); };
};
let i: i32 = lo;
for (i < hi) {
if (i == d.dp) {
buf[o] = 46u8; // '.'
o += 1i32;
};
if (0i32 <= i && i < (d.nd: i32)) {
buf[o] = (d.digits[i] + 48u8): u8;
} else {
buf[o] = 48u8; // '0'
};
o += 1i32;
i += 1i32;
};
return o;
};
// ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable
// logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad.
fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = {
let o: i32 = out;
os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0");
buf[o] = (d.digits[0] + 48u8): u8;
o += 1i32;
if ((d.nd: i32) > 1i32) {
buf[o] = 46u8; // '.'
o += 1i32;
};
let i: size = (1u64: size);
for (i < d.nd) {
buf[o] = (d.digits[i] + 48u8): u8;
o += 1i32;
i += (1u64: size);
};
buf[o] = 101u8; // 'e'
o += 1i32;
let e: i32 = d.dp - 1i32;
if (e < 0i32) {
e = -e;
buf[o] = 45u8; // '-'
o += 1i32;
};
// Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`).
let U_ONE: uint = (1u32: uint);
let U_TWO: uint = (2u32: uint);
let U_THREE: uint = (3u32: uint);
let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits
let l: uint = declen(e: u64);
let k: uint = (0u32: uint);
for (k < l) {
ebuf[U_TWO - k] = (e % 10i32): u8;
e /= 10i32;
k += U_ONE;
};
let m: uint = U_THREE - l;
for (m < U_THREE) {
buf[o] = (ebuf[m] + 48u8): u8;
o += 1i32;
m += U_ONE;
};
return o;
};
// ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a
// view into a static buffer overwritten on the next call (the *tos
// convention; see strings.dup to retain). Equivalent to Hare's ftosf
// with format G + precision void. The fftosf G/void/NONE path is inlined
// (the parametric surface is deferred — task #64).
//
// Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' +
// exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer
// emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw
// split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused.
let f64tos_buf: [32]u8;
export fn f64tos(n: f64) str = {
let bits: u64 = math.f64bits(n);
let mantissa: u64 = bits & math.F64_MANTISSA_MASK;
let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32;
let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64;
let special: bool = exponent == (math.F64_EXPONENT_MASK: u32);
let o: i32 = 0i32;
let r: str;
r.ptr = &f64tos_buf[0];
// NaN carries no sign prefix (ftos.ha:331-333, before sign handling).
if (special && mantissa != 0u64) {
o = putstr(f64tos_buf[0:32], o, "nan");
r.len = o;
return r;
};
if (sign) {
f64tos_buf[o] = 45u8; // '-'
o += 1i32;
};
if (special) {
o = putstr(f64tos_buf[0:32], o, "infinity");
r.len = o;
return r;
};
if (exponent == 0u32 && mantissa == 0u64) {
f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE)
o += 1i32;
r.len = o;
return r;
};
let d = decimal { ... };
// Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a
// scalar-field read of a LOCAL struct (`d.nd`) to a bogus global
// symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers
// correctly in both stages (the stof.ww/decimal.ww *decimal precedent)
// — #170. The init/trim/encode calls already took &d; route via pd.
let pd: *decimal = &d;
let dd: decf64 = f64todecf64(mantissa, exponent);
init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32));
// ok = !ffpoint(NONE) || ... is always true → no multiprecision
// fallback (ftos.ha:365). f == G → trim (ftos.ha:386).
trim(pd);
if (pd.nd == (0u64: size)) {
f64tos_buf[o] = 48u8; // rounded to zero
o += 1i32;
} else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) {
o = encode_e_dec(pd, f64tos_buf[0:32], o);
} else {
o = encode_f_dec(pd, f64tos_buf[0:32], o);
};
r.len = o;
return r;
};

104
lib/strconv/ftos_data.ww Normal file
View File

@@ -0,0 +1,104 @@
// strconv — Ryū float→string lookup tables + bit-count constants.
// Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data
// fold (strconv #106 fold-5): no logic, consumed by ftos.ww's
// f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores).
//
// File-organisation divergence: Hare keeps these tables INLINE in
// ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring
// the stof.ww / stof_data.ww split) — same `package strconv`, so the
// tables stay visible to ftos.ww with no qualification.
//
// Spelling divergences (same as stof_data.ww, candidate #130 + rule-12):
// - Hare `const TBL = [...]` → ww module-level `let` (ww has no
// module-`const` keyword; the values are never written).
// - every literal carries its element-width suffix (`u64`/`u32`):
// cstage rejects bare integer literals in `[N]uXX` init while
// wwstage accepts them; the suffixed form is the only shape both
// stages agree on.
// - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened);
// the 2D module-level static-init + double-index read landed in
// #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64.
package strconv;
// ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split
// power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32
// at each use site (Hare promotes a u8 def inside mixed-width arithmetic;
// ww is strict — explicit cast, project_int_machine_word_derived_limits).
// The F32_POW5_*_BITCOUNT siblings (ftos_ryu.ha:162-163) land in fold-5b
// (task #67) with their only consumer, f32todecf32 — omitted here to keep
// this fold dead-code-free.
def F64_POW5_INV_BITCOUNT: u8 = 125u8;
def F64_POW5_BITCOUNT: u8 = 125u8;
// ref/hare/strconv/ftos_ryu.ha:165-181.
let F64_POW5_INV_SPLIT2: [15][2]u64 = [
[1u64, 2305843009213693952u64],
[5955668970331000884u64, 1784059615882449851u64],
[8982663654677661702u64, 1380349269358112757u64],
[7286864317269821294u64, 2135987035920910082u64],
[7005857020398200553u64, 1652639921975621497u64],
[17965325103354776697u64, 1278668206209430417u64],
[8928596168509315048u64, 1978643211784836272u64],
[10075671573058298858u64, 1530901034580419511u64],
[597001226353042382u64, 1184477304306571148u64],
[1527430471115325346u64, 1832889850782397517u64],
[12533209867169019542u64, 1418129833677084982u64],
[5577825024675947042u64, 2194449627517475473u64],
[11006974540203867551u64, 1697873161311732311u64],
[10313493231639821582u64, 1313665730009899186u64],
[12701016819766672773u64, 2032799256770390445u64],
];
// ref/hare/strconv/ftos_ryu.ha:183-188.
let POW5_INV_OFFSETS: [19]u32 = [
0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32,
0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32,
0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32,
0x00000000u32,
];
// ref/hare/strconv/ftos_ryu.ha:190-204.
let F64_POW5_SPLIT2: [13][2]u64 = [
[0u64, 1152921504606846976u64],
[0u64, 1490116119384765625u64],
[1032610780636961552u64, 1925929944387235853u64],
[7910200175544436838u64, 1244603055572228341u64],
[16941905809032713930u64, 1608611746708759036u64],
[13024893955298202172u64, 2079081953128979843u64],
[6607496772837067824u64, 1343575221513417750u64],
[17332926989895652603u64, 1736530273035216783u64],
[13037379183483547984u64, 2244412773384604712u64],
[1605989338741628675u64, 1450417759929778918u64],
[9630225068416591280u64, 1874621017369538693u64],
[665883850346957067u64, 1211445438634777304u64],
[14931890668723713708u64, 1565756531257009982u64],
];
// ref/hare/strconv/ftos_ryu.ha:206-211.
let POW5_OFFSETS: [21]u32 = [
0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32,
0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32,
0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32,
0x55559155u32, 0x51405555u32, 0x00000105u32,
];
// ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in
// f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those
// arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage
// rejects a def-named array length — "array length must be an integer
// literal"; wwstage accepts it but emits an empty DATAW — divergence
// #167, so the literal `26` is the only shape both stages agree on;
// matches decimal.ww's `[800]u8` array-dimension-literal precedent).
def POW5_TABLE_SZ: u8 = 26u8;
// ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is
// commented out in Hare too — it lives implicitly in the SPLIT2 tables).
let POW5_TABLE: [26]u64 = [
1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64,
390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64,
1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64,
762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64,
476837158203125u64, 2384185791015625u64, 11920928955078125u64,
59604644775390625u64, 298023223876953125u64,
];

View File

@@ -261,91 +261,12 @@ export fn stou8(s: str, b: base) (u8 | invalid | overflow) = {
return 0: invalid;
};
// f64tos — convert v to a decimal string. Returns owned str; release
// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is
// fixed-point only, max 6 fractional digits, no NaN/Inf support —
// see graduate-to-Ryū note below).
//
// Surface:
//
// - finite values only. NaN/±Inf detection needs an f64→u64 bit
// reinterpret cast that the cgen doesn't expose yet.
// - fixed-point only, up to 6 fractional digits. Trailing zeros
// after the decimal point are trimmed. Trailing '.' is dropped.
// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast)
// fall back to the literal token "huge". Hare would print these
// in scientific notation via Ryū; we will graduate when the
// compiler grows the bit-reinterpret cast.
//
// Round-trip is therefore lossy past 6 fractional digits.
//
// No float literals in the body — 990's wwdump diff requires this
// file's TK_FLOAT count to match between C and ww front-ends, and
// the ww-side wwdump currently skips TK_FLOAT.fval while the C side
// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses:
// build f64 constants via int-to-f64 casts.
let f64tos_buf: [64]u8;
export fn f64tos(v: f64) str = {
let out: i32 = 0;
let f: f64 = v;
let zero: f64 = 0: f64;
if (f < zero) {
f64tos_buf[out] = 45u8; // '-'
out += 1;
f = -f;
};
// 9e18 is comfortably under I64_MAX (9.22e18). Past this the
// `f: i64` cast wraps and the integer part comes back as garbage.
let cap: f64 = 9000000000000000000i64: f64;
if (f >= cap) {
let s: str = "huge";
let k: i32 = 0;
for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; };
let r: str;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
let ip: i64 = f: i64;
// Fractional part scaled to 6 decimal digits, with round-to-
// nearest via +0.5. (f64 compound assigns mis-lower in cgen —
// use the explicit form, as the rest of lib does.)
let frac: f64 = f - (ip: f64);
let scale: f64 = 1000000: f64;
frac = frac * scale;
let half: f64 = (1: f64) / (2: f64);
let fp: i64 = (frac + half): i64;
// Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer
// part needs to advance.
if (fp >= 1000000) {
ip += 1;
fp = 0;
};
let intstr: str = i64tos(ip, base.DEC);
let k: i32 = 0;
for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; };
if (fp != 0) {
f64tos_buf[out] = 46u8; // '.'
out += 1;
let fracstr: str = u64tos(fp: u64, base.DEC);
// Pad fractional to 6 digits with leading zeros (e.g. 0.05 →
// fp=50000, fracstr="50000", pad one '0' before).
let z: i32 = 6 - fracstr.len;
for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; };
k = 0;
for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; };
// Trim trailing zeros in the fractional part.
for (out > 0) {
if (f64tos_buf[out - 1] != 48u8) { break; };
out -= 1;
};
};
let r: str;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
// f64tos — graduated to the Ryū shortest-round-trippable implementation
// in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version
// (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted
// here per the lib-note graduation rule ("replace in one go, don't keep
// both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b
// (task #67, gated on the #143 f32-arg-push cgen fix).
// strerror — convert an strconv error to a user-readable string.
// Returns owned str; release via os.free. Mirrors Hare's

View File

@@ -0,0 +1,93 @@
// ftostest — exercises lib/strconv/ftos.ww (Hare ftos.ha / ftos_ryu.ha
// Ryū port). Run with `out/bin/ww run lib/strconv/test/ftostest.ww`.
// Same signalled-then-os.exit(signalled+10) pattern as stoftest/
// decimaltest: a non-zero exit names the failing @test (1..N → 11..N+10).
//
// Ports ref/hare/strconv/+test/ftos_test.ha's ffmt::G / prec=void /
// fflags::NONE rows (the f64tos cases) verbatim — these are exactly
// strconv.f64tos's output. encode_f_dec is covered by 13.37/1100/0.011/…
// and encode_e_dec by 1e4/1.1e4/1e-3/1.1e-3/1e-4. nan/±infinity via
// math.f64frombits of the IEEE bit patterns (ww math has no f64 NAN/INF
// const — see math/floats.ww).
//
// f32tos is NOT exercised here: it is deferred to fold-5b (task #67),
// gated on the #143 f32-arg-push cs≠ww cgen fix.
//
// Lives in lib/strconv/test/ so `import strconv` resolves to the
// DIRECTORY (full package), not the strconv.ww FILE — same rationale as
// stoftest/decimaltest.
package strconv;
import strconv;
import os;
import math;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let k: i32 = 0i32;
for (k < a.len) {
if (a[k] != b[k]) { return false; };
k += 1i32;
};
return true;
};
fn chk(n: f64, want: str) bool = {
return streq(f64tos(n), want);
};
// ftos_test.ha:57-64 (G/void/NONE-equivalent) — fixed-point renders.
@test fn f64tos_fixed() void = {
if (!chk(13.37, "13.37")) { fail(); };
if (!chk(12345.0, "12345")) { fail(); };
if (!chk(1100.0, "1100")) { fail(); };
if (!chk(100.0, "100")) { fail(); };
if (!chk(10.0, "10")) { fail(); };
if (!chk(1.0, "1")) { fail(); };
if (!chk(0.3, "0.3")) { fail(); };
if (!chk(0.1, "0.1")) { fail(); };
if (!chk(0.01, "0.01")) { fail(); };
if (!chk(0.011, "0.011")) { fail(); };
if (!chk(1.414, "1.414")) { fail(); };
if (!chk(-6345.972, "-6345.972")) { fail(); };
};
// ftos_test.ha:78-90 — scientific renders (the shortest-G E-dispatch).
@test fn f64tos_scientific() void = {
if (!chk(10000.0, "1e4")) { fail(); };
if (!chk(11000.0, "1.1e4")) { fail(); };
if (!chk(1000.0, "1e3")) { fail(); };
if (!chk(0.001, "1e-3")) { fail(); };
if (!chk(0.0011, "1.1e-3")) { fail(); };
if (!chk(0.0001, "1e-4")) { fail(); };
// dp=-2 < -1 → G dispatches to E (ftos_test.ha:62 is the ffmt::F row;
// shortest-G of 0.0031415 is scientific).
if (!chk(0.0031415, "3.1415e-3")) { fail(); };
};
// ftos_test.ha:12/16/27 — zero, ±infinity, nan.
@test fn f64tos_special() void = {
if (!chk(0.0, "0")) { fail(); };
// sign bit via (1u64 << 63), not the 0x8000000000000000 literal:
// wwstage mis-emits the I64_MIN-magnitude literal (#144 family).
let negzero: f64 = math.f64frombits(1u64 << 63u64);
if (!chk(negzero, "-0")) { fail(); };
let inf: f64 = math.f64frombits(0x7FF0000000000000u64);
if (!chk(inf, "infinity")) { fail(); };
let ninf: f64 = math.f64frombits(0xFFF0000000000000u64);
if (!chk(ninf, "-infinity")) { fail(); };
let nan: f64 = math.f64frombits(0x7FF8000000000000u64);
if (!chk(nan, "nan")) { fail(); };
};
export fn main() i32 = {
signalled = 1; f64tos_fixed();
signalled = 2; f64tos_scientific();
signalled = 3; f64tos_special();
os.exit(0);
return 0;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

55
test/wcc/908_ftos_run.c Normal file
View File

@@ -0,0 +1,55 @@
/*
* 908_ftos_run — execute the lib/strconv/test/ftostest fixture under the
* C-side `ww run` driver and assert exit 0.
*
* Same thin-wrapper shape as 909_stof_run / 922_decimal_run: ftostest.ww
* carries its own `export fn main()` that drives the @test fns (Ryū
* f64tos: fixed + scientific + nan/±inf/zero) and signals which case
* failed via the exit code (signalled + 10).
*
* The fixture lives in lib/strconv/test/ (not lib/strconv/) so `import
* strconv` resolves to the strconv DIRECTORY (full package: strconv.ww +
* decimal.ww + stof_data.ww + stof.ww + ftos_data.ww + ftos.ww) rather
* than shadowing on the strconv.ww FILE — same rationale as 922.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
const char *src = "lib/strconv/test/ftostest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "ftos_run FAIL: %s exited %d\n", src, rc);
return 1;
}
printf("ftos_run: %s ok\n", src);
return 0;
}