//! 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).", ); }