Floats as written, never fused; fma and muladd; f32/f64 bits, f16 and bf16

- a * b + c is two roundings on every processor and --cpu: the IR has no fast-math flags, and clang
  compiles it with -ffp-contract=on (checked in the assembly: never fused, also with FMA hardware)
- fma(a, b, c) / v.fma(b, c): one rounding, always (llvm.fma)
- muladd(a, b, c) / v.muladd(b, c): the fastest way here (llvm.fmuladd), what v.fma was before
- f32_bits / f32_from_bits / f64_bits / f64_from_bits: bitcasts, also of vectors
- f16_to_f32 / f32_to_f16 (LLVM half), bf16_to_f32 / f32_to_bf16 (integer ops, ties to even, NaN kept)
- docs (en, pl): what is guaranteed about floats, the new built-ins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
This commit is contained in:
Claude 2026-11-01 15:25:40 +00:00
commit 0ceaaa27c4
No known key found for this signature in database
13 changed files with 373 additions and 22 deletions

View file

@ -5572,6 +5572,12 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if ei >= 0 {
return enumFrom(g, c, ei)
}
if ["fma", "muladd"].contains(n) {
return fmaCall(g, c) // Finch's, also for f32 and vectors (C's fma is the same for one double)
}
if ["f32_bits", "f32_from_bits", "f64_bits", "f64_from_bits", "f16_to_f32", "f32_to_f16", "bf16_to_f32", "f32_to_bf16"].contains(n) && !g.ci.fns.has(n) {
return bitsCall(g, c)
}
if g.ci.fns.has(n) && !["min", "max", "abs", "clamp"].contains(n) {
return callC(g, c, g.ci.fns[n]) // (C's abs only takes an int; Finch's takes any number)
}
@ -6647,8 +6653,14 @@ fn laneBytes(ast.Type vt) -> str {
return str(bitsOf(vt.elem[0]) / 8)
}
// v8f32, v32i8: how LLVM's vector intrinsics are named for this type
// v8f32, v32i8 (or f32, i64 for a plain number): how LLVM's intrinsics are named for this type
fn vecSuffix(ast.Type vt) -> str {
if vt.kind != "vec" {
if isFloat(vt) {
return "f" + str(bitsOf(vt))
}
return "i" + str(bitsOf(vt))
}
if isFloat(vt.elem[0]) {
return "v" + str(vt.count) + "f" + str(bitsOf(vt.elem[0]))
}
@ -6984,17 +6996,15 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
need(g, m, 0)
return vecAbs(g, v)
}
if n == "sqrt" || n == "floor" || n == "ceil" || n == "round" || n == "fma" {
if n == "sqrt" || n == "floor" || n == "ceil" || n == "round" || n == "fma" || n == "muladd" {
if !isFloat(lane) {
fail(g, m, n + "() is for f32 and f64 lanes, but this is " + ast.show(vt))
}
if n == "fma" {
if n == "fma" || n == "muladd" {
need(g, m, 2)
b := asVec(g, expr(g, m.kids[1]), vt, "fma", m.kids[1])
c := asVec(g, expr(g, m.kids[2]), vt, "fma", m.kids[2])
// fmuladd: one fused instruction where the processor has one (FMA, NEON), a multiply and an add where
// it doesn't (llvm.fma would call a slow library function there, to round only once)
return Val(v: vecIntrinsic(g, lt, "fmuladd", vt, [lt, lt, lt], [v.v, b.v, c.v], ""), ty: vt)
b := asVec(g, expr(g, m.kids[1]), vt, n, m.kids[1])
c := asVec(g, expr(g, m.kids[2]), vt, n, m.kids[2])
return Val(v: fusedOp(g, n, vt, v.v, b.v, c.v), ty: vt)
}
need(g, m, 0)
return Val(v: vecIntrinsic(g, lt, n, vt, [lt], [v.v], ""), ty: vt)
@ -7028,6 +7038,178 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
emit(g, "store " + lt + " " + v.v + ", ptr " + ep + ", align " + laneBytes(vt))
return Val(ty: T("void"))
}
fail(g, m, "a " + ast.show(vt) + " has no method '" + n + "' (it has: sum, min, max, dot, abs, sqrt, floor, ceil, round, fma, store)")
fail(g, m, "a " + ast.show(vt) + " has no method '" + n + "' (it has: sum, min, max, dot, abs, sqrt, floor, ceil, round, fma, muladd, store)")
return v
}
// ---------- fused multiply-add, and numbers as bits ----------
//
// Finch never fuses on its own: a * b + c is a multiply and an add, two roundings, on every processor and with every
// --cpu (the IR has no fast-math flags, and clang compiles it with -ffp-contract=on, which fuses only what asks to be).
// Fusing is asked for by name:
// fma(a, b, c) one rounding, always (one instruction with FMA hardware, a library call without it)
// muladd(a, b, c) whichever is faster here: fused where the processor can, a multiply and an add where it can't
// the llvm.fma / llvm.fmuladd call for a number or a vector type
fn fusedOp(ptr[Gen] g, str name, ast.Type t, str a, str b, str c) -> str {
lt := llty(g, t)
which := "fma"
if name == "muladd" {
which = "fmuladd"
}
return vecIntrinsic(g, lt, which, t, [lt, lt, lt], [a, b, c], "")
}
fn fmaCall(ptr[Gen] g, ast.Node c) -> Val {
n := c.name
if g.index {
noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0)
}
checkArgs(g, c, 0, 3, n)
[]Val vals
for a in c.kids {
v := expr(g, a)
if !isNumber(v.ty) && v.ty.kind != "vec" {
fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty))
}
vals.push(v)
}
// the type all three take: a vector if one is, else the float type they fit
t := T("void")
for v in vals {
if v.ty.kind == "vec" {
t = v.ty
}
}
if t.kind == "vec" {
if !isFloat(t.elem[0]) {
fail(g, c, n + "(...) is for f32 and f64 lanes, but this is " + ast.show(t))
}
[]str vs
for i in 0..3 {
vs.push(asVec(g, vals[i], t, n, c.kids[i]).v)
}
return Val(v: fusedOp(g, n, t, vs[0], vs[1], vs[2]), ty: t)
}
// f32 when an f32 takes part and nothing wider does (numbers written in the code adapt), else float
narrow := false
wide := false
for v in vals {
if v.ty.kind == "f32" {
narrow = true
} else if !v.lit {
wide = true
}
}
t = T("float")
if narrow && !wide {
t = T("f32")
}
[]str vs
for i in 0..3 {
vs.push(coerce(g, vals[i], t, c.kids[i], "each number of " + n + "(...)").v)
}
return Val(v: fusedOp(g, n, t, vs[0], vs[1], vs[2]), ty: t)
}
// the same shape as t (a number, or a vector of as many lanes) with lanes of another kind
fn shaped(ast.Type t, str lane) -> ast.Type {
if t.kind != "vec" {
return T(lane)
}
out := ast.Type(kind: "vec", count: t.count)
out.elem.push(T(lane))
return out
}
// a constant in every lane of t (or just the number)
fn konst(ptr[Gen] g, ast.Type t, str n) -> str {
if t.kind != "vec" {
return n
}
return splat(g, n, t)
}
// f32_bits(x) -> u32 and back, f64 the same; f16 and bf16 (as the u16 that holds them) to f32 and back.
// Each works on a number or on a vector, lane by lane: f16_to_f32(u16x8) is an f32x8.
fn bitsCall(ptr[Gen] g, ast.Node c) -> Val {
n := c.name
if g.index {
noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0)
}
checkArgs(g, c, 0, 1, n)
v := expr(g, c.kids[0])
from := "" // the lane type it takes
to := ""
if n == "f32_bits" || n == "f32_to_f16" || n == "f32_to_bf16" {
from = "f32"
to = "u32"
if n != "f32_bits" {
to = "u16"
}
} else if n == "f64_bits" {
from = "float"
to = "u64"
} else if n == "f32_from_bits" {
from = "u32"
to = "f32"
} else if n == "f64_from_bits" {
from = "u64"
to = "float"
} else {
from = "u16"
to = "f32"
}
lane := v.ty
if v.ty.kind == "vec" {
lane = v.ty.elem[0]
}
// the bits may come as a signed number of the same size (i32 for u32), and a number written in the code fits
if isInt(T(from)) && isInt(lane) && bitsOf(lane) == bitsOf(T(from)) {
v = Val(v: v.v, ty: shaped(v.ty, from))
} else if v.ty.kind != "vec" && v.lit {
v = coerce(g, v, T(from), c.kids[0], "the value of " + n + "(...)")
} else if lane.kind != from {
fail(g, c.kids[0], n + "(...) takes " + ast.show(T(from)) + " (or a vector of them), but this is " + ast.show(v.ty))
}
src := shaped(v.ty, from)
out := shaped(v.ty, to)
il := llty(g, src)
ol := llty(g, out)
if n == "f32_bits" || n == "f64_bits" || n == "f32_from_bits" || n == "f64_from_bits" {
return Val(v: op(g, "bitcast " + il + " " + v.v + " to " + ol), ty: out)
}
if n == "bf16_to_f32" {
// bf16 is the top half of an f32
wide := shaped(v.ty, "u32")
wl := llty(g, wide)
z := op(g, "zext " + il + " " + v.v + " to " + wl)
sh := op(g, "shl " + wl + " " + z + ", " + konst(g, wide, "16"))
return Val(v: op(g, "bitcast " + wl + " " + sh + " to " + ol), ty: out)
}
if n == "f32_to_bf16" {
// the top half, rounded to the nearest (ties to even); a NaN stays a NaN
wide := shaped(v.ty, "u32")
wl := llty(g, wide)
u := op(g, "bitcast " + il + " " + v.v + " to " + wl)
hi := op(g, "lshr " + wl + " " + u + ", " + konst(g, wide, "16"))
odd := op(g, "and " + wl + " " + hi + ", " + konst(g, wide, "1"))
bias := op(g, "add " + wl + " " + odd + ", " + konst(g, wide, "32767"))
rounded := op(g, "lshr " + wl + " " + op(g, "add " + wl + " " + u + ", " + bias) + ", " + konst(g, wide, "16"))
quiet := op(g, "or " + wl + " " + hi + ", " + konst(g, wide, "64"))
isNan := op(g, "fcmp uno " + il + " " + v.v + ", " + v.v)
pickd := op(g, "select " + llty(g, shaped(v.ty, "bool")) + " " + isNan + ", " + wl + " " + quiet + ", " + wl + " " + rounded)
return Val(v: op(g, "trunc " + wl + " " + pickd + " to " + ol), ty: out)
}
// f16: LLVM's half type (one instruction with F16C or on ARM64, a small library function elsewhere)
hl := "half"
if v.ty.kind == "vec" {
hl = "<" + str(v.ty.count) + " x half>"
}
if n == "f16_to_f32" {
h := op(g, "bitcast " + il + " " + v.v + " to " + hl)
return Val(v: op(g, "fpext " + hl + " " + h + " to " + ol), ty: out)
}
h := op(g, "fptrunc " + il + " " + v.v + " to " + hl)
return Val(v: op(g, "bitcast " + hl + " " + h + " to " + ol), ty: out)
}