floats iter 2.3: surface print emits shortest round-trippable decimal with .0 fallback

This commit is contained in:
2026-05-10 15:11:31 +02:00
parent f62bff08a3
commit c619697a76
+69 -2
View File
@@ -559,13 +559,44 @@ fn write_arm(out: &mut String, arm: &Arm, level: usize) {
out.push(')');
}
/// Render a Float literal's bit pattern as surface text. The output
/// is the shortest round-trippable decimal produced by
/// `f64::to_string` (Grisu3). If the rendered form contains
/// neither `.` nor `e`/`E` (`f64::to_string` returns "1" for
/// `1.0_f64`, "10000000000" for `1e10_f64`), append `.0` so the
/// lexer's `is_int`/`has_dot|has_exp` dispatch routes the token to
/// the Float path on re-lex. This preserves the
/// `lex(print(L)) == L` round-trip property pinned by
/// [`print_then_parse_round_trip_float_literal`].
///
/// Non-finite bit patterns (NaN, ±Inf) cannot be expressed in
/// surface form (per spec section A2 — the lexer rejects all such
/// inputs). A Float literal in the AST whose bits are non-finite
/// must therefore have arrived via Form-A directly. The printer's
/// job is surface output, not a Form-A escape hatch, so we panic
/// rather than emit a non-round-trippable token.
fn write_float_lit(out: &mut String, bits: u64) {
let f = f64::from_bits(bits);
if !f.is_finite() {
panic!(
"write_float_lit: non-finite Float literal {bits:#018x} \
cannot be expressed in surface form"
);
}
let s = f.to_string();
out.push_str(&s);
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
out.push_str(".0");
}
}
fn write_lit(out: &mut String, lit: &Literal) {
match lit {
Literal::Int { value } => out.push_str(&value.to_string()),
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
Literal::Str { value } => write_string_lit(out, value),
Literal::Unit => out.push_str("(lit-unit)"),
Literal::Float { .. } => unimplemented!("Floats milestone iter 2: surface print"),
Literal::Float { bits } => write_float_lit(out, *bits),
}
}
@@ -587,7 +618,7 @@ fn write_pattern(out: &mut String, p: &Pattern) {
// came through the typechecker.
out.push_str("(lit-unit)");
}
Literal::Float { .. } => unimplemented!("Floats milestone iter 2: surface print"),
Literal::Float { bits } => write_float_lit(out, *bits),
}
out.push(')');
}
@@ -733,4 +764,40 @@ mod tests {
};
round_trip(m, "minimal_instance");
}
/// Iter 22-floats.2 RED: `lex(print(L)) == L` round-trip property
/// for Float literals. Six representative bit patterns:
/// `1.5`, `0.0`, `-0.0`, `10.0`, `-0.375`, `1e10`. The round-trip
/// uses the existing `round_trip(Module, &str)` helper, which
/// prints the module to surface form, re-parses it, and asserts
/// canonical bytes match. The Float arm in surface print MUST emit
/// at least one of `.` or `e`/`E` so the lexer recognises the
/// output as a Float (not an Int) — `f64::to_string` returns "1"
/// for `1.0_f64`, "10000000000" for `1e10_f64`, and the printer's
/// post-pass adds `.0` in those cases.
#[test]
fn print_then_parse_round_trip_float_literal() {
use ailang_core::ast::*;
for &bits in &[
0x3ff8_0000_0000_0000u64, // 1.5
0x0000_0000_0000_0000u64, // 0.0
0x8000_0000_0000_0000u64, // -0.0
0x4024_0000_0000_0000u64, // 10.0
0xbfd8_0000_0000_0000u64, // -0.375
0x4202_a05f_2000_0000u64, // 1e10
] {
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "M".into(),
imports: vec![],
defs: vec![Def::Const(ConstDef {
name: "k".into(),
ty: Type::float(),
value: Term::Lit { lit: Literal::Float { bits } },
doc: None,
})],
};
round_trip(m, &format!("float_literal_{bits:#018x}"));
}
}
}