fix(runtime): mirror surface .0-fallback in ailang_float_to_str
`runtime/str.c::ailang_float_to_str` now applies the `.0`-fallback
already enforced by the surface printer's `write_float_lit`
(crates/ailang-surface/src/print.rs lines 624-637): after the
existing `%g` snprintf, if `x` is finite and the rendered buffer
contains neither `.` nor `e`/`E`, append `.0` so a whole-valued
Float renders with Float-shaped text. The `isfinite(x)` fence
keeps `nan`/`inf`/`-inf` from matching the predicate and turning
into `nan.0`/`inf.0`. The 64-byte stack buffer's truncation guard
is extended by 2 bytes for the fallback path so the defensive
`abort()` discipline holds.
The alternative (document `%g`-without-fallback in the design/
ledger as a deliberate human-friendly contract) was rejected: the
language's stated ethos is machine-readability and round-trip
identity; the surface printer is the reference, runtime drift
from it is a defect not a feature.
Golden updates flow from the contract change. Three fixtures
encoded the old Int-shaped output for Float values:
- `crates/ail/tests/floats_e2e.rs`: `"4\n42\n-1.5\n"` →
`"4.0\n42.0\n-1.5\n"` (1.5+2.5=4.0, int_to_float(42)=42.0).
- `crates/ail/tests/e2e.rs::mut_sum_floats_prints_55`: `"55"` →
`"55.0"`. The accompanying doc comment used to canonicalise the
bug ("`%g` strips the trailing `.0` — so the canonical stdout
for Float 55.0 is `55`"); rewritten to reflect the new contract.
- `examples/mut_sum_floats.ail` docstring: same correction.
Two doc comments are updated where the post-fix contract differs
from the old text but the fixture golden does not change:
- `examples/float_to_str_smoke.ail` docstring (3.5 still renders
as `3.5` because `.` is already present — the fallback does not
fire).
- `crates/ail/tests/e2e.rs::float_to_str_smoke` doc comment (same
observation, made explicit so the trip-wire's intent is clear).
Verified: `cargo test -p ail --test print_float_whole_e2e` green
(was RED at f19b0dd); `cargo test --workspace` 0 failed across
all crates and integration suites.
closes #7
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"iter_id": "bugfix-float-to-str-dot-zero",
|
||||
"date": "2026-05-20",
|
||||
"mode": "mini",
|
||||
"outcome": "DONE",
|
||||
"tasks_total": 1,
|
||||
"tasks_completed": 1,
|
||||
"reloops_per_task": { "1": 0 },
|
||||
"review_loops_spec": 0,
|
||||
"review_loops_quality": 0,
|
||||
"blocked_reason": null
|
||||
}
|
||||
+12
-6
@@ -2702,7 +2702,12 @@ fn int_to_str_smoke() {
|
||||
/// default applies and the dev target's glibc produces the three
|
||||
/// bytes `3.5` (no trailing zeros). If a future target's libc or
|
||||
/// locale produces a different rendering, this assertion is the
|
||||
/// trip-wire that surfaces it.
|
||||
/// trip-wire that surfaces it. Note: post-Gitea-#7 the runtime
|
||||
/// also applies a `.0`-fallback when `%g`'s output for a finite
|
||||
/// double contains neither `.` nor `e`/`E` (mirroring the surface
|
||||
/// printer's `write_float_lit`); `3.5` already contains `.`, so
|
||||
/// the fallback does not fire here and the golden stays at the
|
||||
/// three bytes `3.5`.
|
||||
#[test]
|
||||
fn float_to_str_smoke() {
|
||||
let out = build_and_run("float_to_str_smoke.ail");
|
||||
@@ -2866,12 +2871,13 @@ fn mut_counter_prints_55() {
|
||||
|
||||
/// Float twin of `mut_counter_prints_55`: `sum_helper` returns the
|
||||
/// sum 1.0+...+10.0 = 55.0. The polymorphic `print` routes through
|
||||
/// `Show Float.show` → `float_to_str`'s libc `%g` formatter, which
|
||||
/// strips the trailing `.0` — so the canonical stdout for Float
|
||||
/// 55.0 is `"55"` (matches `examples/floats.ail`'s output of
|
||||
/// `4` for `1.5 + 2.5`).
|
||||
/// `Show Float.show` → `float_to_str`'s libc `%g` formatter plus
|
||||
/// the runtime `.0`-fallback for finite whole-valued doubles whose
|
||||
/// `%g` output lacks `.`/`e`/`E` (Gitea #7), so the canonical
|
||||
/// stdout for Float 55.0 is `"55.0"` (matches `examples/floats.ail`'s
|
||||
/// output of `4.0` for `1.5 + 2.5`).
|
||||
#[test]
|
||||
fn mut_sum_floats_prints_55() {
|
||||
let stdout = build_and_run("mut_sum_floats.ail");
|
||||
assert_eq!(stdout.trim(), "55", "mut_sum_floats must print 55, got {stdout:?}");
|
||||
assert_eq!(stdout.trim(), "55.0", "mut_sum_floats must print 55.0, got {stdout:?}");
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
//!
|
||||
//! `examples/floats.ail.json` is built via the public `ail build`
|
||||
//! CLI and run; stdout is matched against the expected three
|
||||
//! lines (`4`, `42`, `-1.5`). This test exercises the full
|
||||
//! lines (`4.0`, `42.0`, `-1.5`). This test exercises the full
|
||||
//! pipeline — Float literal lowering, polymorphic `+` Float arm,
|
||||
//! `int_to_float` (sitofp), `neg` Float (fneg), and the polymorphic
|
||||
//! `print` for Float (routes through `Show Float.show` →
|
||||
//! `float_to_str`, libc `%g`).
|
||||
//! `float_to_str`, libc `%g` plus the runtime `.0`-fallback for
|
||||
//! whole-valued Float results, per Gitea #7).
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -59,7 +60,7 @@ fn floats_example_prints_expected_stdout() {
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&run.stdout);
|
||||
let expected = "4\n42\n-1.5\n";
|
||||
let expected = "4.0\n42.0\n-1.5\n";
|
||||
assert_eq!(
|
||||
stdout, expected,
|
||||
"stdout mismatch.\nGot:\n{}\nExpected:\n{}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(module float_to_str_smoke
|
||||
(fn main
|
||||
(doc "Iter hs.4: smoke pin for the float_to_str heap-Str builtin. `do io/print_str(float_to_str(3.5))` builds through the whole pipeline (checker resolves float_to_str:(Float)->Str, codegen lowers to call ptr @ailang_float_to_str(double 3.5), runtime/str.c's ailang_float_to_str snprintfs into a heap-Str slab via libc %g, io/print_str's @puts prints from the bytes pointer at offset 8). The exact rendering of 3.5 is libc-target-dependent; on the dev target's clang+glibc with default C locale it is the three bytes `3.5`. Single-value smoke; NaN / +Inf / -Inf edge cases are deferred per the hs.4 plan.")
|
||||
(doc "Iter hs.4: smoke pin for the float_to_str heap-Str builtin. `do io/print_str(float_to_str(3.5))` builds through the whole pipeline (checker resolves float_to_str:(Float)->Str, codegen lowers to call ptr @ailang_float_to_str(double 3.5), runtime/str.c's ailang_float_to_str snprintfs into a heap-Str slab via libc %g plus a `.0`-fallback for finite whole-valued doubles whose %g output lacks `.`/`e`/`E` (Gitea #7), io/print_str's @puts prints from the bytes pointer at offset 8). The exact rendering of 3.5 is libc-target-dependent; on the dev target's clang+glibc with default C locale it is the three bytes `3.5` (the fallback does not fire because `.` is already present). Single-value smoke; NaN / +Inf / -Inf edge cases are deferred per the hs.4 plan.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body (do io/print_str (app float_to_str 3.5)))))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(module mut_sum_floats
|
||||
|
||||
(fn main
|
||||
(doc "Float twin of mut_counter via a tail-recursive helper. Expected stdout: 55 (Float-55.0 via %g).")
|
||||
(doc "Float twin of mut_counter via a tail-recursive helper. Expected stdout: 55.0 (Float 55.0 via %g plus the runtime `.0`-fallback for finite whole-valued doubles, Gitea #7).")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
/* Defined in runtime/rc.c — exposed with default external linkage.
|
||||
* Returns a zero-initialised payload whose `rc_header` sits at
|
||||
@@ -125,6 +126,16 @@ char *ailang_int_to_str(int64_t n) {
|
||||
* oversized. Defensive `abort()` on truncation, same shape as
|
||||
* `ailang_int_to_str`.
|
||||
*
|
||||
* `.0`-fallback (Gitea #7): if `x` is finite and `%g`'s output
|
||||
* contains neither `.` nor `e`/`E`, append `.0` so a whole-valued
|
||||
* Float renders with a Float-shaped suffix instead of Int-shaped
|
||||
* text. Mirrors the surface printer's `write_float_lit` fallback
|
||||
* at `crates/ailang-surface/src/print.rs` lines 624-637, keeping
|
||||
* the surface/runtime round-trip-on-lex symmetry intact. The
|
||||
* `isfinite(x)` fence skips the fallback for `nan`/`inf`/`-inf`,
|
||||
* which would otherwise match the no-`.`-no-`e` predicate and
|
||||
* render as `nan.0`/`inf.0`.
|
||||
*
|
||||
* Returns the payload pointer of a fresh heap-Str slab.
|
||||
*/
|
||||
char *ailang_float_to_str(double x) {
|
||||
@@ -138,6 +149,24 @@ char *ailang_float_to_str(double x) {
|
||||
abort();
|
||||
}
|
||||
uint64_t len = (uint64_t)written;
|
||||
/* `.0`-fallback — see block comment above and the surface
|
||||
* counterpart at crates/ailang-surface/src/print.rs lines
|
||||
* 624-637. `%g` output is ASCII, so plain `memchr` suffices. */
|
||||
if (isfinite(x)
|
||||
&& memchr(buf, '.', len) == NULL
|
||||
&& memchr(buf, 'e', len) == NULL
|
||||
&& memchr(buf, 'E', len) == NULL) {
|
||||
if (len + 2 >= sizeof(buf)) {
|
||||
fprintf(stderr,
|
||||
"ailang_float_to_str: .0-fallback would overflow buffer "
|
||||
"(len=%llu, buffer=%zu) — should be unreachable for any finite double; aborting\n",
|
||||
(unsigned long long)len, sizeof(buf));
|
||||
abort();
|
||||
}
|
||||
buf[len] = '.';
|
||||
buf[len + 1] = '0';
|
||||
len += 2;
|
||||
}
|
||||
char *payload = str_alloc(len);
|
||||
memcpy(payload + 8, buf, len);
|
||||
payload[8 + len] = '\0';
|
||||
|
||||
Reference in New Issue
Block a user