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}