f19b0dd860
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
75 lines
3.0 KiB
Rust
75 lines
3.0 KiB
Rust
//! 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).",
|
|
);
|
|
}
|