From f19b0dd8603792d01c24988ab91abbe11ac9ef69 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 20 May 2026 18:19:46 +0200 Subject: [PATCH] =?UTF-8?q?test(runtime):=20RED=20=E2=80=94=20whole-valued?= =?UTF-8?q?=20Float=20must=20render=20with=20`.0`=20suffix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `examples/print_float_whole_smoke.ail` (single `(app print 2.0)`) and `crates/ail/tests/print_float_whole_e2e.rs`. The test builds and runs the fixture through the public `ail build` CLI and asserts stdout is `"2.0\n"`. Currently FAILS with `left: "2\n"`, `right: "2.0\n"` — runtime `ailang_float_to_str` uses libc `snprintf("%g", x)` which strips trailing zeros, so a whole-valued Float prints as Int-shaped text. The surface printer at `crates/ailang-surface/src/print.rs::write_float_lit` (lines 624-637) already enforces the `.0`-fallback property; the runtime drifts. This RED pins the desired symmetry. The GREEN side will mirror the surface fallback in `runtime/str.c::ailang_float_to_str` and update the `floats_e2e.rs` golden (`"4\n42\n-1.5\n"` → `"4.0\n42.0\n-1.5\n"`) plus the doc comments at `crates/ail/tests/e2e.rs` and in the runtime/example files that documented the previous `%g`-without- fallback contract. The alternative (document `%g`-behaviour in design/ ledger §Float semantics as a deliberate human-friendly contract) was rejected: the language's stated ethos is machine-readability + round-trip identity (CLAUDE.md §"AILang — a language for LLM authors"); the surface printer is the reference, runtime drift from it is a defect not a feature. refs #7 --- crates/ail/tests/print_float_whole_e2e.rs | 74 +++++++++++++++++++++++ examples/print_float_whole_smoke.ail | 6 ++ 2 files changed, 80 insertions(+) create mode 100644 crates/ail/tests/print_float_whole_e2e.rs create mode 100644 examples/print_float_whole_smoke.ail diff --git a/crates/ail/tests/print_float_whole_e2e.rs b/crates/ail/tests/print_float_whole_e2e.rs new file mode 100644 index 0000000..8ff96f6 --- /dev/null +++ b/crates/ail/tests/print_float_whole_e2e.rs @@ -0,0 +1,74 @@ +//! Gitea #7 RED pin — whole-valued Float must render with a `.0` suffix. +//! +//! A Float literal whose value is whole (e.g. `2.0`), printed via the +//! polymorphic `print` builtin, monomorphises to the Show-Float path: +//! `Show Float.show` → `float_to_str` → runtime/str.c:: +//! `ailang_float_to_str` → libc `snprintf(buf, sizeof(buf), "%g", x)`. +//! +//! The surface printer at +//! `crates/ailang-surface/src/print.rs::write_float_lit` (lines 624-637) +//! already enforces the property that a finite Float whose `to_string` +//! contains neither `.` nor `e/E` gets a `.0` suffix appended, so the +//! lex round-trip stays on the Float branch. The runtime path lacks +//! that fallback and therefore drifts: a Float-typed `2.0` prints as +//! `2`, which is Int-shaped output and corrupts the LLM author's +//! mental model of typing — a Float in the source can produce +//! Int-looking text in stdout. +//! +//! This test pins the desired property (post-fix): stdout for a single +//! `print 2.0` must be `"2.0\n"`. Pre-fix the test fails with `"2\n"`. +//! +//! Fix site: `runtime/str.c::ailang_float_to_str` — after the `%g` +//! snprintf, if the buffer is finite and contains neither `.` nor +//! `e/E`, append `.0` (mirroring the surface printer). Out of scope +//! for this RED. + +use std::path::Path; +use std::process::Command; + +fn ail_bin() -> &'static str { + env!("CARGO_BIN_EXE_ail") +} + +fn build_and_run(example: &str) -> String { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); + let src = workspace.join("examples").join(example); + let tmp = std::env::temp_dir().join(format!( + "ailang_e2e_{}_{}", + example.replace('.', "_"), + std::process::id() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let out = tmp.join("bin"); + let status = Command::new(ail_bin()) + .args(["build", src.to_str().unwrap(), "-o"]) + .arg(&out) + .status() + .expect("ail build failed to run"); + assert!(status.success(), "ail build failed for {example}"); + let output = Command::new(&out).output().expect("execute binary"); + assert!( + output.status.success(), + "binary {} exited non-zero", + out.display() + ); + String::from_utf8(output.stdout).expect("stdout utf8") +} + +/// A whole-valued Float printed through `Show Float.show` keeps its +/// Float shape on stdout — i.e. the rendering contains a `.0` (or +/// equivalent `.` / `e/E` form) so a re-lex of the output would land +/// back on the Float branch, not on the Int branch. This is the +/// runtime mirror of `write_float_lit`'s surface-side `.0` fallback. +#[test] +fn print_whole_valued_float_renders_with_dot_zero_suffix() { + let out = build_and_run("print_float_whole_smoke.ail"); + assert_eq!( + out, "2.0\n", + "whole-valued Float `2.0` must render with `.0` suffix \ + (mirroring write_float_lit); got `{out}` — the runtime \ + `ailang_float_to_str` is missing the `.0` fallback that \ + the surface printer applies (see Gitea #7).", + ); +} diff --git a/examples/print_float_whole_smoke.ail b/examples/print_float_whole_smoke.ail new file mode 100644 index 0000000..2bb775a --- /dev/null +++ b/examples/print_float_whole_smoke.ail @@ -0,0 +1,6 @@ +(module print_float_whole_smoke + (fn main + (doc "RED pin for Gitea #7: a whole-valued Float (here, the literal 2.0) routed through polymorphic `print` (which monomorphises to Show Float.show -> float_to_str -> runtime/str.c::ailang_float_to_str -> libc snprintf %g) must render with a `.0` suffix, matching the surface printer's write_float_lit fallback (crates/ailang-surface/src/print.rs lines 624-637). The desired stdout for this fixture is the three bytes `2.0\\n`. Pre-fix, libc %g strips trailing zeros and renders `2\\n` — Int-shaped output for a Float value — which violates the surface-runtime round-trip-on-lex symmetry the language relies on.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (app print 2.0))))