|
|
|
@@ -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}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|