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))))