floats iter 4.4: codegen neg/int_to_float/float_to_int_truncate/is_nan + float_to_str-deferred
This commit is contained in:
@@ -472,7 +472,13 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user