From c619697a762e627e18c495fc764cbe01fa885129 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 15:11:31 +0200 Subject: [PATCH] floats iter 2.3: surface print emits shortest round-trippable decimal with .0 fallback --- crates/ailang-surface/src/print.rs | 71 +++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index 8572735..9537eab 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -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}")); + } + } }