Skip to main content

rucc_codegen/
half.rs

1//! The float that is narrower than any instruction, as the work at a wider one.
2//!
3//! `_Float16` is the other arithmetic type a C program writes that this machine does not compute
4//! with. It is the mirror of [`crate::quad`]: sixteen bits fit in a vector register, so moving one,
5//! passing one and returning one are things this back end has, and everything else has no
6//! instruction behind it, because half precision arithmetic on x86-64 arrived with AVX512FP16 and
7//! that is far above this target's baseline. gcc 16 is in exactly the same position at the same
8//! baseline and does exactly this, so what this pass writes is what gcc writes.
9//!
10//! The difference from the quad is that the half has somewhere to go. Every value of this format
11//! is a value of `float` exactly, since eleven bits of significand and five of exponent fit inside
12//! twenty four and eight with room to spare, so the operation the program wrote is the `float`
13//! operation with a widening in front of it and a narrowing behind it. Only the widening and the
14//! narrowing are calls.
15//!
16//! # Why `float` in the middle is the same answer and not merely a close one
17//!
18//! Because twenty four is at least two times eleven plus two. That is the double rounding bound:
19//! an addition, a subtraction, a multiplication or a division of two values of a format with `p`
20//! significant bits, computed in a format with at least `2p + 2` bits and then rounded to the first
21//! one, is the same value as the operation rounded once. A half has eleven bits and a `float` has
22//! twenty four, which clears the bound by one, so `a + b` at this format really is
23//! `__truncsfhf2(extend(a) + extend(b))` and not an approximation of it. The exponent range is not
24//! in the way either: `float` holds every product and every quotient of two halves, subnormal ones
25//! included, without overflowing or losing a bit to its own subnormal range.
26//!
27//! This is also why the comparisons are not calls the way the quad's are. Widening is exact and
28//! exactness is all a comparison needs, so two halves compare as the two `float`s they widen to,
29//! and every predicate including the unordered ones comes out the same. libgcc does define
30//! `__eqhf2` and `__nehf2`, and gcc calls neither of them on this target for the same reason.
31//!
32//! # The narrowing is where the care goes
33//!
34//! Rounding twice is not rounding once. A `double` that sits a hair above the midpoint between two
35//! halves rounds down to exactly that midpoint in a `float`, and then to even from the midpoint,
36//! which is the other neighbour from the one a single rounding gives. So there is a routine per
37//! source width, `__truncsfhf2`, `__truncdfhf2` and `__trunctfhf2`, and this pass picks the one
38//! that matches what the program actually had rather than going through `float` every time.
39//!
40//! An integer becoming a half goes through `double` for the same reason and is exact all the same.
41//! Every integer a half can represent is below 65536 and is therefore exact in a `double`, and
42//! every integer at or above 65520 is an infinity at this format whatever happened on the way, so
43//! there is no value of any integer width where the intermediate rounding to `double` can change
44//! the answer. That is why this needs no `__floatsihf` family and libgcc has none.
45//!
46//! # What is left alone
47//!
48//! A conversion against the eighty bit format, which is the one thing here with a libgcc routine
49//! that this pass does not call. `__truncxfhf2` exists and would be the right answer, and what
50//! stands in the way is that an eighty bit value travels in the argument area as bytes rather than
51//! in a register, so a call taking one is a different shape from every other call written here. It
52//! is refused by name instead, which is the position [`crate::quad`] takes on the same pair, and it
53//! is `tamnd/rucc#1064`'s row rather than this pass's work today.
54//!
55//! A constant of the format and a negation of one are left alone as well, and those two are not
56//! refusals. [`crate::expand::floats`] already writes a float constant as the integer spelling its
57//! bits and a reinterpretation, and a negation as an exclusive or with the sign bit in a general
58//! purpose register, and both of those are written for every float of sixty four bits or narrower
59//! rather than for the two the machine computes in. So a half reaches them and comes out as the
60//! bits and a `bitcast`, which is what the rule set now has an instruction for.
61//!
62//! A load is left alone too, because the machine has one. `pinsrw` reads sixteen bits straight into
63//! the low lane of a vector register and a rule writes it. A store is not the mirror of that: the
64//! form of `pextrw` that writes memory is SSE4.1, so a store is the bits out through a general
65//! purpose register and an ordinary sixteen bit store after them, which is two instructions and so
66//! this pass's work rather than a rule's.
67
68use rucc_base::Interner;
69use rucc_ir::{
70    CallInfo, Extra, Float, Func, Inst, InstData, MemInfo, MemOrder, Opcode, Param, Restrict,
71    Signature, Type, Value,
72};
73use rucc_target::AbiDescription;
74
75use crate::capability;
76
77/// The format this pass is about.
78const HALF: Float = Float::F16;
79
80/// The format every operation at it is performed at, which is the narrowest one that holds the
81/// answer exactly. See the module's second section for why exactly.
82const WIDE: Float = Float::F32;
83
84/// The format an integer conversion goes through, which is wider than [`WIDE`] because an integer
85/// is not a half and needs the room. See the module's fourth section.
86const THROUGH: Float = Float::F64;
87
88/// The routine the capability table names for this operation at this mode.
89///
90/// Every mode this pass asks about is one no instruction on this machine covers, which is the whole
91/// reason the pass exists, so the table always has an answer. A missing one is the table and this
92/// pass having gone out of step rather than anything a program can reach.
93fn routine(opcode: Opcode, mode: &str) -> &'static str {
94    capability::libcall(opcode, mode)
95        .unwrap_or_else(|| panic!("no routine for `{}` at `{mode}`", opcode.name()))
96}
97
98/// Rewrites every operation at this format into the work at a wider one.
99///
100/// The instructions are collected before any of them is touched, because a rewrite puts
101/// instructions in front of the one it replaces and the walk would otherwise see its own work.
102pub fn calls(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription) {
103    let found: Vec<Inst> =
104        func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
105    for inst in found {
106        match func[inst].opcode {
107            Opcode::FAdd | Opcode::FSub | Opcode::FMul | Opcode::FDiv => {
108                arithmetic(func, names, abi, inst);
109            }
110            Opcode::FCmp => compare(func, names, abi, inst),
111            Opcode::FPExt => widen(func, names, abi, inst),
112            Opcode::FPTrunc => narrow(func, names, abi, inst),
113            Opcode::SIToFP | Opcode::UIToFP => from_integer(func, names, abi, inst),
114            Opcode::FPToSI | Opcode::FPToUI => to_integer(func, names, abi, inst),
115            Opcode::Store => stored(func, inst),
116            _ => {}
117        }
118    }
119}
120
121/// Whether this type is the format.
122fn half(ty: Type) -> bool {
123    ty.is_scalar() && ty.format() == Some(HALF)
124}
125
126/// The type of an instruction's first result, or nothing where it has none.
127fn produced(func: &Func, inst: Inst) -> Option<Type> {
128    func[inst].first_result.map(|value| func[value].ty)
129}
130
131/// The four operations, each of them the `float` one between a widening and a narrowing.
132///
133/// The flags the program wrote are carried onto the operation in the middle, because that is the
134/// operation it asked for: a contraction the program allowed is still allowed of the addition, and
135/// a not a number it promised there would not be is still promised of the same operands. The two
136/// calls carry none, which is the same thing [`crate::quad`] does and for the same reason, since a
137/// call is a call whatever the program said about rounding.
138fn arithmetic(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription, inst: Inst) {
139    let Some(ty) = produced(func, inst) else { return };
140    if !half(ty) {
141        return;
142    }
143    let args = func[func[inst].args].to_vec();
144    let [a, b] = args[..] else { return };
145    let opcode = func[inst].opcode;
146    let flags = func[inst].flags;
147    let a = extended(func, names, abi, inst, a);
148    let b = extended(func, names, abi, inst, b);
149    let args = func.push_values(&[a, b]);
150    let data = InstData { args, flags, ..InstData::new(opcode) };
151    let answer = written(func, inst, data, Type::float(WIDE));
152    into_call(func, names, abi, inst, routine(Opcode::FPTrunc, "f32.f16"), &[answer]);
153}
154
155/// A comparison, as the same comparison of the two `float`s the operands widen to.
156///
157/// The predicate is untouched and the instruction stays a comparison, which is the whole of what
158/// makes this different from the quad's: there the answer comes back from a routine as an integer
159/// and has to be tested against zero, and here the machine has the comparison already and only the
160/// operands had to get to a format it has one at.
161fn compare(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription, inst: Inst) {
162    let args = func[func[inst].args].to_vec();
163    let [a, b] = args[..] else { return };
164    if !half(func[a].ty) || !half(func[b].ty) {
165        return;
166    }
167    let a = extended(func, names, abi, inst, a);
168    let b = extended(func, names, abi, inst, b);
169    func[inst].args = func.push_values(&[a, b]);
170}
171
172/// A half becoming a wider float, which is the one routine and never rounds.
173///
174/// One routine for all three destinations, because the only widening libgcc has from this format is
175/// the one to `float` and the rest of the way is a widening the machine does itself. Nothing is
176/// lost by going in two steps here, unlike in the narrowing direction: both halves of the journey
177/// are exact, so there is no second rounding to get wrong.
178fn widen(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription, inst: Inst) {
179    let Some(ty) = produced(func, inst) else { return };
180    let Some(&arg) = func[func[inst].args].first() else { return };
181    if !half(func[arg].ty) {
182        return;
183    }
184    let routine = routine(Opcode::FPExt, "f16.f32");
185    if ty.format() == Some(WIDE) {
186        into_call(func, names, abi, inst, routine, &[arg]);
187        return;
188    }
189    let wide = call(func, names, abi, inst, routine, &[arg], Type::float(WIDE));
190    becomes(func, inst, Opcode::FPExt, Extra::None, &[wide]);
191}
192
193/// A wider float becoming a half, which is a routine per source width because each of them rounds.
194///
195/// The eighty bit format is not one of them, for the reason the module's last section gives.
196fn narrow(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription, inst: Inst) {
197    let Some(ty) = produced(func, inst) else { return };
198    let Some(&arg) = func[func[inst].args].first() else { return };
199    if !half(ty) {
200        return;
201    }
202    let mode = match func[arg].ty.format() {
203        Some(Float::F32) => "f32.f16",
204        Some(Float::F64) => "f64.f16",
205        Some(Float::F128) => "f128.f16",
206        _ => return,
207    };
208    into_call(func, names, abi, inst, routine(Opcode::FPTrunc, mode), &[arg]);
209}
210
211/// An integer becoming a half, which is the conversion to a `double` and the narrowing of that.
212///
213/// The conversion in the middle is left as the opcode the program wrote, signed or unsigned, so
214/// [`crate::expand::floats`] still gets to widen a narrow integer and to take the unsigned word
215/// apart the way it does for every other conversion. This pass only says which format it lands in
216/// on the way.
217fn from_integer(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription, inst: Inst) {
218    let Some(ty) = produced(func, inst) else { return };
219    let Some(&arg) = func[func[inst].args].first() else { return };
220    let from = func[arg].ty;
221    if !half(ty) || !from.is_int() || !from.is_scalar() {
222        return;
223    }
224    let opcode = func[inst].opcode;
225    let args = func.push_values(&[arg]);
226    let data = InstData { args, ..InstData::new(opcode) };
227    let wide = written(func, inst, data, Type::float(THROUGH));
228    into_call(func, names, abi, inst, routine(Opcode::FPTrunc, "f64.f16"), &[wide]);
229}
230
231/// A half becoming an integer, which is the widening and the conversion the machine has.
232///
233/// `float` rather than `double` on this side, because the widening is exact and the conversion from
234/// there is the same rounding toward zero at either width. The opcode is left alone for the reason
235/// [`from_integer`] leaves it alone.
236fn to_integer(func: &mut Func, names: &mut Interner, abi: &'static AbiDescription, inst: Inst) {
237    let Some(ty) = produced(func, inst) else { return };
238    let Some(&arg) = func[func[inst].args].first() else { return };
239    if !half(func[arg].ty) || !ty.is_int() || !ty.is_scalar() {
240        return;
241    }
242    let opcode = func[inst].opcode;
243    let wide = extended(func, names, abi, inst, arg);
244    becomes(func, inst, opcode, Extra::None, &[wide]);
245}
246
247/// A store of a half, as a store of the sixteen bits spelling it.
248///
249/// The address, the flags and everything the access says about itself stay exactly as they were:
250/// this is the same store of the same two bytes to the same place, said about an integer so that
251/// the rule set has an instruction for it. What it costs is the `pextrw` in front, which is the one
252/// instruction gcc's output has here that the load does not need.
253fn stored(func: &mut Func, inst: Inst) {
254    let args = func[func[inst].args].to_vec();
255    let [value, address] = args[..] else { return };
256    if !half(func[value].ty) {
257        return;
258    }
259    let bits = ahead(func, inst, Opcode::Bitcast, &[value], Type::int(16));
260    func[inst].args = func.push_values(&[bits, address]);
261}
262
263/// One half widened to a `float` in front of an instruction, as the call that does it.
264fn extended(
265    func: &mut Func,
266    names: &mut Interner,
267    abi: &'static AbiDescription,
268    inst: Inst,
269    value: Value,
270) -> Value {
271    let routine = routine(Opcode::FPExt, "f16.f32");
272    call(func, names, abi, inst, routine, &[value], Type::float(WIDE))
273}
274
275/// Turns an instruction into the call that performs it, in place.
276///
277/// In place rather than in front of, because the call produces one value of the type the
278/// instruction already produced, so every reader of it goes on reading the same value.
279///
280/// There is no answer coming back through an address here, which is the one shape [`crate::quad`]
281/// has and this does not. Everything a routine of this family gives back is a half or a `float`,
282/// and no convention passes two or four bytes of anything by reference.
283fn into_call(
284    func: &mut Func,
285    names: &mut Interner,
286    abi: &'static AbiDescription,
287    inst: Inst,
288    routine: &str,
289    args: &[Value],
290) {
291    let Some(ty) = produced(func, inst) else { return };
292    let shape = shaped(func, abi, inst, args);
293    let extra = signature(func, names, routine, &shape, ty);
294    becomes(func, inst, Opcode::Call, extra, &shape.values);
295}
296
297/// A call to a runtime routine written in front of an instruction, and the value it answers.
298fn call(
299    func: &mut Func,
300    names: &mut Interner,
301    abi: &'static AbiDescription,
302    inst: Inst,
303    routine: &str,
304    args: &[Value],
305    ty: Type,
306) -> Value {
307    let shape = shaped(func, abi, inst, args);
308    let extra = signature(func, names, routine, &shape, ty);
309    let args = func.push_values(&shape.values);
310    written(func, inst, InstData { args, extra, ..InstData::new(Opcode::Call) }, ty)
311}
312
313/// A call's operands once the convention has been asked about each of them.
314struct Shape {
315    /// What each operand is in the signature, which is `ptr` for one that became an address.
316    params: Vec<Param>,
317    /// The values the call instruction actually reads, in the same order.
318    values: Vec<Value>,
319}
320
321/// The operands of one call, with the one that a convention may pass by address spilled to the
322/// frame.
323///
324/// Only a `_Float128` can be that one, and only on a convention that passes sixteen bytes of
325/// anything as the address of a copy, which is Windows x64. That is the rule `tamnd/rucc#1331` put
326/// in the ABI description, it is a rule about a call, and a call this pass writes is a call. Every
327/// other operand here is two, four or eight bytes and travels in a register on every convention.
328fn shaped(func: &mut Func, abi: &'static AbiDescription, inst: Inst, args: &[Value]) -> Shape {
329    let mut shape = Shape { params: Vec::new(), values: Vec::new() };
330    for &value in args {
331        let ty = func[value].ty;
332        let size = u64::from(ty.bits().div_ceil(8));
333        if quad(ty) && abi.scalar_is_by_reference(size) {
334            let copy = slot(func, inst);
335            write(func, inst, value, copy);
336            shape.params.push(Param::new(Type::PTR));
337            shape.values.push(copy);
338        } else {
339            shape.params.push(Param::new(ty));
340            shape.values.push(value);
341        }
342    }
343    shape
344}
345
346/// Whether this type is the format that fills a whole vector register, which is the one operand
347/// here a convention may want by address.
348fn quad(ty: Type) -> bool {
349    ty.is_scalar() && ty.format() == Some(Float::F128)
350}
351
352/// The call this shape is, as the `Extra` an instruction carries it in.
353fn signature(
354    func: &mut Func,
355    names: &mut Interner,
356    routine: &str,
357    shape: &Shape,
358    ty: Type,
359) -> Extra {
360    let mut built = Signature::new();
361    built.params = shape.params.clone();
362    built.returns = vec![Param::new(ty)];
363    let signature = func.add_signature(built);
364    let callee = Some(names.intern(routine));
365    let varargs = func.push_abis(&[]);
366    Extra::Call(func.add_call(CallInfo { callee, signature, varargs }))
367}
368
369/// A frame slot the size of a quad, put in front of an instruction.
370fn slot(func: &mut Func, inst: Inst) -> Value {
371    let extra = Extra::Mem(func.add_mem(whole()));
372    written(func, inst, InstData { extra, ..InstData::new(Opcode::Alloca) }, Type::PTR)
373}
374
375/// An access to the whole of one quad, which is the only thing this pass ever puts in a slot.
376fn whole() -> MemInfo {
377    MemInfo {
378        size: 16,
379        align: 16,
380        order: MemOrder::NotAtomic,
381        tbaa: None,
382        owns: 0,
383        restrict: Restrict::NONE,
384    }
385}
386
387/// A store of a quad into a slot, put in front of an instruction.
388fn write(func: &mut Func, inst: Inst, value: Value, into: Value) {
389    let span = func.span(inst);
390    let extra = Extra::Mem(func.add_mem(whole()));
391    let args = func.push_values(&[value, into]);
392    let data = InstData { args, extra, ..InstData::new(Opcode::Store) };
393    let made = func.create_inst(data, &[], span);
394    func.insert_before(made, inst);
395}
396
397/// An instruction of that opcode over those operands, put in front of another one.
398fn ahead(func: &mut Func, inst: Inst, opcode: Opcode, args: &[Value], ty: Type) -> Value {
399    let args = func.push_values(args);
400    written(func, inst, InstData { args, ..InstData::new(opcode) }, ty)
401}
402
403/// Creates an instruction, puts it in front of another one, and reads its value back out.
404fn written(func: &mut Func, inst: Inst, data: InstData, ty: Type) -> Value {
405    let span = func.span(inst);
406    let made = func.create_inst(data, &[ty], span);
407    func.insert_before(made, inst);
408    func[made].first_result.expect("an instruction created with one result has one")
409}
410
411/// Turns an instruction into a different one over different operands, in place.
412fn becomes(func: &mut Func, inst: Inst, opcode: Opcode, extra: Extra, args: &[Value]) {
413    let args = func.push_values(args);
414    let data = &mut func[inst];
415    data.opcode = opcode;
416    data.args = args;
417    data.extra = extra;
418    data.flags = data.flags.intersection(rucc_ir::Flags::legal_on(opcode));
419}