From 581144a4f97c55652cd29663a2074a0d44c8c700 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 16:15:39 +0200 Subject: [PATCH] floats iter 4.4: codegen neg/int_to_float/float_to_int_truncate/is_nan + float_to_str-deferred --- crates/ail/tests/snapshots/hello.ll | 1 + crates/ail/tests/snapshots/list.ll | 1 + crates/ail/tests/snapshots/max3.ll | 1 + crates/ail/tests/snapshots/sum.ll | 1 + crates/ail/tests/snapshots/ws_main.ll | 1 + crates/ailang-codegen/src/lib.rs | 136 +++++++++++++++++++++++++- 6 files changed, 140 insertions(+), 1 deletion(-) diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index 8748a32..92d8867 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) +declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_hello_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_hello_main_adapter, ptr null } define i8 @ail_hello_main() { diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll index cdd4b06..6fbf429 100644 --- a/crates/ail/tests/snapshots/list.ll +++ b/crates/ail/tests/snapshots/list.ll @@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) +declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_list_sum_list_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_sum_list_adapter, ptr null } @ail_list_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_main_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll index 1801959..55b11b8 100644 --- a/crates/ail/tests/snapshots/max3.ll +++ b/crates/ail/tests/snapshots/max3.ll @@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) +declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_max3_max_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max_adapter, ptr null } @ail_max3_max3_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max3_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll index 9a65908..dde44b5 100644 --- a/crates/ail/tests/snapshots/sum.ll +++ b/crates/ail/tests/snapshots/sum.ll @@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) +declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_sum_sum_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_sum_adapter, ptr null } @ail_sum_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_main_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/ws_main.ll b/crates/ail/tests/snapshots/ws_main.ll index 2952389..943dca9 100644 --- a/crates/ail/tests/snapshots/ws_main.ll +++ b/crates/ail/tests/snapshots/ws_main.ll @@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) +declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_ws_lib_add_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_lib_add_adapter, ptr null } @ail_ws_main_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_main_main_adapter, ptr null } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index a7ac544..936b463 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -472,7 +472,13 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result // Iter 16e: `==` on `Str` lowers to `@strcmp` followed by // `icmp eq i32 0`. NUL-terminated strings make this a one-liner; // libc supplies `strcmp` so no extra link flag is needed. - out.push_str("declare i32 @strcmp(ptr, ptr)\n\n"); + out.push_str("declare i32 @strcmp(ptr, ptr)\n"); + // Floats iter 4.4: saturating fp-to-int intrinsic for + // float_to_int_truncate. NaN → 0, +Inf → i64::MAX, -Inf → + // i64::MIN, finite-out-of-range saturates, finite-in-range + // truncates toward zero. LLVM 12+, always available with + // clang 22. + out.push_str("declare i64 @llvm.fptosi.sat.i64.f64(double)\n\n"); out.push_str(&header); out.push_str(&body); @@ -1789,6 +1795,73 @@ impl<'a> Emitter<'a> { return Ok((dst, "i1".into())); } + // Floats iter 4.4: polymorphic neg + 3 monomorphic fn builtins. + if name == "neg" { + if args.len() != 1 { + return Err(CodegenError::Internal("neg arity".into())); + } + let arg_ty = self.synth_arg_type(&args[0])?; + let (a, _) = self.lower_term(&args[0])?; + let dst = self.fresh_ssa(); + match &arg_ty { + Type::Con { name, .. } if name == "Int" => { + self.body.push_str(&format!(" {dst} = sub i64 0, {a}\n")); + return Ok((dst, "i64".into())); + } + Type::Con { name, .. } if name == "Float" => { + // LLVM 8+ `fneg` correctly handles -0.0. + self.body.push_str(&format!(" {dst} = fneg double {a}\n")); + return Ok((dst, "double".into())); + } + other => return Err(CodegenError::Internal(format!( + "`neg` not supported for type `{}`", + ailang_core::pretty::type_to_string(other) + ))), + } + } + if name == "int_to_float" { + if args.len() != 1 { + return Err(CodegenError::Internal("int_to_float arity".into())); + } + let (a, _) = self.lower_term(&args[0])?; + let dst = self.fresh_ssa(); + self.body.push_str(&format!(" {dst} = sitofp i64 {a} to double\n")); + return Ok((dst, "double".into())); + } + if name == "float_to_int_truncate" { + if args.len() != 1 { + return Err(CodegenError::Internal("float_to_int_truncate arity".into())); + } + let (a, _) = self.lower_term(&args[0])?; + let dst = self.fresh_ssa(); + self.body.push_str(&format!( + " {dst} = call i64 @llvm.fptosi.sat.i64.f64(double {a})\n" + )); + return Ok((dst, "i64".into())); + } + if name == "is_nan" { + if args.len() != 1 { + return Err(CodegenError::Internal("is_nan arity".into())); + } + let (a, _) = self.lower_term(&args[0])?; + let dst = self.fresh_ssa(); + // `fcmp uno x, x` returns `i1 1` iff `x` is NaN — only + // NaN compares unordered against itself. + self.body.push_str(&format!(" {dst} = fcmp uno double {a}, {a}\n")); + return Ok((dst, "i1".into())); + } + if name == "float_to_str" { + // Deferred to iter 5+: needs runtime-allocated Str + // (currently the codegen Str path uses only static + // `@.str_*` globals; no malloc-backed dynamic-Str + // infrastructure). The typecheck path in iter 3 + // installs `float_to_str : (Float) -> Str`; until the + // runtime gets a Str allocator, calling it crashes + // codegen. This is intentional: don't ship an + // unimplemented call to the LLM-author surface. + unimplemented!("Floats milestone iter 5+: float_to_str needs dynamic Str allocation"); + } + // Cross-module call: exactly one dot in the name → resolve via import map. // Logic identical to the typechecker (see `synth` for `Term::Var`). if name.matches('.').count() == 1 { @@ -2101,6 +2174,15 @@ impl<'a> Emitter<'a> { if is_arithmetic_or_comparison_op(name) || name == "==" || name == "not" { return true; } + // Floats iter 4.4: new fn-builtins (`neg`, `int_to_float`, + // `float_to_int_truncate`, `is_nan`, `float_to_str`) lower + // inline in `lower_app`, parallel to the operator path. + if matches!( + name, + "neg" | "int_to_float" | "float_to_int_truncate" | "is_nan" | "float_to_str" + ) { + return true; + } if name.matches('.').count() == 1 { return true; } @@ -3076,4 +3158,56 @@ mod tests { assert!(ir.contains("icmp ne i64"), "Int `!=` regressed: {ir}"); assert!(ir.contains("icmp eq i64"), "Int `==` regressed: {ir}"); } + + /// Floats iter 4.4 RED: four new fn-builtins lower to the spec'd + /// LLVM ops. `neg` polymorphic dispatches to `sub i64 0, %x` for + /// Int and `fneg double %x` for Float (NOT `fsub 0.0, %x`, which + /// is wrong for `-0.0`). `int_to_float` → `sitofp`. `is_nan` → + /// `fcmp uno double %x, %x`. `float_to_int_truncate` → + /// `@llvm.fptosi.sat.i64.f64` intrinsic call. + #[test] + fn lowers_float_fn_builtins() { + use ailang_core::ast::*; + fn fn_def(name: &str, body: Term, ret_ty: Type) -> Def { + Def::Fn(FnDef { + name: name.into(), + ty: Type::Fn { + params: vec![], ret: Box::new(ret_ty), effects: vec![], + param_modes: vec![], ret_mode: ParamMode::Implicit, + }, + params: vec![], body, suppress: vec![], doc: None, + }) + } + fn app1(callee: &str, arg: Term) -> Term { + Term::App { + callee: Box::new(Term::Var { name: callee.into() }), + args: vec![arg], tail: false, + } + } + let neg_int = app1("neg", Term::Lit { lit: Literal::Int { value: 5 } }); + let neg_float = app1("neg", Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }); + let i2f = app1("int_to_float", Term::Lit { lit: Literal::Int { value: 5 } }); + let f2i = app1("float_to_int_truncate", Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }); + let isnan = app1("is_nan", Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }); + let main_def = fn_def("main", Term::Lit { lit: Literal::Unit }, Type::unit()); + let m = Module { + schema: ailang_core::SCHEMA.to_string(), + name: "t".into(), + imports: vec![], + defs: vec![ + fn_def("ni", neg_int, Type::int()), + fn_def("nf", neg_float, Type::float()), + fn_def("c1", i2f, Type::float()), + fn_def("c2", f2i, Type::int()), + fn_def("isn", isnan, Type::bool_()), + main_def, + ], + }; + let ir = emit_ir(&m).unwrap(); + assert!(ir.contains("sub i64 0,"), "neg Int missing: {ir}"); + assert!(ir.contains("fneg double"), "neg Float missing (must use fneg, not fsub-from-zero): {ir}"); + assert!(ir.contains("sitofp i64"), "int_to_float missing: {ir}"); + assert!(ir.contains("@llvm.fptosi.sat.i64.f64"), "float_to_int_truncate intrinsic missing: {ir}"); + assert!(ir.contains("fcmp uno double"), "is_nan missing (must be fcmp uno x, x): {ir}"); + } }