tensor_wasm_jit/lower_float.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! Float-family Cranelift→[`LoweredOp`] lowering (wave 1, task L2).
5//!
6//! This module covers the float rows of the [Cranelift → `dialect-mir`
7//! mapping table](crate::pliron_dialect#mapping-table):
8//!
9//! | Cranelift op | Produces |
10//! |--------------|------------------------------------------------|
11//! | `fadd` | [`LoweredOp::AddF`] |
12//! | `fsub` | [`LoweredOp::SubF`] |
13//! | `fmul` | [`LoweredOp::MulF`] |
14//! | `fdiv` | [`LoweredOp::DivF`] |
15//! | `fma` | [`LoweredOp::Fma`] (`a*b + c`, single rounding)|
16//! | `fneg` | [`LoweredOp::FNeg`] |
17//! | `fabs` | [`LoweredOp::FAbs`] |
18//!
19//! Only `f32` / `f64` scalars are accepted — `f16`, `v128`, or any other
20//! Cranelift type returns [`None`] so the caller can fall back to a
21//! different lowering family (`lower_vector` handles vector float ops in a
22//! sibling module). Per-row Pliron equivalents and PTX semantics are
23//! captured in the mapping table; this module purposely does not repeat
24//! those notes here so the single source of truth stays in
25//! [`crate::pliron_dialect`].
26//!
27//! # Result-id allocation
28//!
29//! Each successful lowering allocates a fresh [`LoweredValueId`] for the
30//! op's SSA result by reading `*next_value_id` and post-incrementing it.
31//! Operand ids come from the caller-owned `value_map` which maps each
32//! Cranelift [`cranelift_codegen::ir::Value`] to the already-assigned
33//! [`LoweredValueId`]. Missing operands are a caller bug and panic — the
34//! lowering walker must populate the map for every block parameter and
35//! every prior instruction's result before recursing into this family.
36//!
37//! # Scope
38//!
39//! - Pure: no side effects beyond `*next_value_id` and reading
40//! `func.dfg`. Safe to call in any order; the caller controls
41//! block-walk ordering.
42//! - Stateless: no module-level statics; multiple lowerings can run
43//! concurrently (one `&mut next_value_id` per pass, as is the
44//! wave-1 convention shared with `lower_arith` etc.).
45
46#![cfg(feature = "cuda-oxide-backend")]
47
48use std::collections::HashMap;
49
50use cranelift_codegen::ir::{self, Function, Inst, Opcode, Value};
51
52use crate::lowered_ir::{LoweredOp, LoweredType, LoweredValueId};
53
54/// Try to lower one Cranelift instruction as a float-family op.
55///
56/// Returns `Some(LoweredOp)` if `inst`'s opcode is one of the seven this
57/// module handles (`fadd`/`fsub`/`fmul`/`fdiv`/`fma`/`fneg`/`fabs`) **and**
58/// its result type is `f32` or `f64`. Returns `None` otherwise — the
59/// caller is expected to consult sibling `lower_*` modules in that case
60/// (vector float ops live in `lower_vector`; non-float ops live in
61/// `lower_arith`, `lower_memory`, etc.).
62///
63/// # Parameters
64///
65/// - `inst`: the Cranelift instruction being lowered.
66/// - `func`: the enclosing function. Used read-only for opcode lookup,
67/// operand fetch (`dfg.inst_args`), and result-type inspection
68/// (`dfg.value_type` on the instruction's first result).
69/// - `value_map`: caller-owned map from already-lowered Cranelift
70/// `Value`s to their assigned [`LoweredValueId`]s. Every operand of
71/// `inst` must be present.
72/// - `next_value_id`: caller-owned monotonic counter for fresh result
73/// ids. On a successful lowering, the current value is consumed as
74/// the new op's `result` field and then incremented by one.
75///
76/// # Panics
77///
78/// Panics if an operand `Value` is missing from `value_map`. This
79/// signals a walker bug (a use-before-def in the caller); the wave-1
80/// design treats it as unrecoverable rather than threading a `Result`
81/// through every per-family lowering.
82pub fn lower_float_inst(
83 inst: Inst,
84 func: &Function,
85 value_map: &HashMap<Value, LoweredValueId>,
86 next_value_id: &mut LoweredValueId,
87) -> Option<LoweredOp> {
88 let opcode = func.dfg.insts[inst].opcode();
89
90 // Quick reject: anything not in our seven-op set returns None
91 // without touching `next_value_id`. Keeps the function side-effect
92 // free on the rejection path so the caller can fall through to the
93 // next lowering family cleanly.
94 match opcode {
95 Opcode::Fadd
96 | Opcode::Fsub
97 | Opcode::Fmul
98 | Opcode::Fdiv
99 | Opcode::Fma
100 | Opcode::Fneg
101 | Opcode::Fabs => {}
102 _ => return None,
103 }
104
105 // Determine the result type. All seven ops produce exactly one
106 // result whose type matches their operands. Reject anything outside
107 // the f32/f64 scalar set — vector float ops (e.g. fadd on f32x4)
108 // are handled by `lower_vector`, and f16 is not in the wave-1
109 // mapping table.
110 let result_value = func.dfg.first_result(inst);
111 let ty = match func.dfg.value_type(result_value) {
112 t if t == ir::types::F32 => LoweredType::F32,
113 t if t == ir::types::F64 => LoweredType::F64,
114 _ => return None,
115 };
116
117 let args = func.dfg.inst_args(inst);
118 let result = *next_value_id;
119 // jit LOW fix (finding 7): standardize on `checked_add(1)?` (the
120 // `lower_arith` idiom) instead of `.expect(...)`. A function exceeding
121 // `u32::MAX` SSA values now surfaces as a structured lowering miss
122 // (`None`, mapped to a `LoweringError` by the driver) rather than
123 // panicking the process.
124 *next_value_id = next_value_id.checked_add(1)?;
125
126 let lowered = match opcode {
127 Opcode::Fadd => LoweredOp::AddF {
128 ty,
129 lhs: lookup(value_map, args[0])?,
130 rhs: lookup(value_map, args[1])?,
131 result,
132 },
133 Opcode::Fsub => LoweredOp::SubF {
134 ty,
135 lhs: lookup(value_map, args[0])?,
136 rhs: lookup(value_map, args[1])?,
137 result,
138 },
139 Opcode::Fmul => LoweredOp::MulF {
140 ty,
141 lhs: lookup(value_map, args[0])?,
142 rhs: lookup(value_map, args[1])?,
143 result,
144 },
145 Opcode::Fdiv => LoweredOp::DivF {
146 ty,
147 lhs: lookup(value_map, args[0])?,
148 rhs: lookup(value_map, args[1])?,
149 result,
150 },
151 Opcode::Fma => LoweredOp::Fma {
152 ty,
153 a: lookup(value_map, args[0])?,
154 b: lookup(value_map, args[1])?,
155 c: lookup(value_map, args[2])?,
156 result,
157 },
158 Opcode::Fneg => LoweredOp::FNeg {
159 ty,
160 src: lookup(value_map, args[0])?,
161 result,
162 },
163 Opcode::Fabs => LoweredOp::FAbs {
164 ty,
165 src: lookup(value_map, args[0])?,
166 result,
167 },
168 // Unreachable: the earlier `match` already filtered the opcode
169 // set down to the seven listed above.
170 _ => unreachable!("opcode already validated by the dispatch match"),
171 };
172
173 Some(lowered)
174}
175
176/// Resolve a Cranelift `Value` to its already-assigned
177/// [`LoweredValueId`]. Returns `None` if the operand was not
178/// pre-mapped (jit S-4: a malformed Cranelift function with a
179/// backward branch whose block-param references a not-yet-seen
180/// value used to panic the worker; now it gracefully degrades to
181/// `lower_float_inst` returning `None`, which the walker treats
182/// as "skip this instruction" — same as for unsupported ops).
183fn lookup(map: &HashMap<Value, LoweredValueId>, v: Value) -> Option<LoweredValueId> {
184 let id = map.get(&v).copied();
185 if id.is_none() {
186 tracing::debug!(
187 target: "tensor_wasm_jit::lower_float",
188 value = ?v,
189 "operand not pre-mapped; skipping float instruction"
190 );
191 }
192 id
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use cranelift_codegen::ir::{
199 types, AbiParam, Block, Function, InstructionData, Signature, UserFuncName, Value,
200 };
201 use cranelift_codegen::isa::CallConv;
202
203 /// Fixture: a one-instruction function and the operand `Value`s
204 /// used to build it. The test asserts against `inst`; operand ids
205 /// are pre-populated into a `value_map` via [`map_operands`] so
206 /// the lowering can resolve them.
207 struct InstFixture {
208 func: Function,
209 inst: Inst,
210 operands: Vec<Value>,
211 }
212
213 /// Build a one-instruction function whose only block has parameters
214 /// of `param_ty`, then append a single instruction described by
215 /// `make_data` (operands pulled from the block params, one per
216 /// `arg_count`). The result type is set by `make_inst_results`
217 /// using `param_ty` as the controlling typevar.
218 fn build_func_with_inst(
219 param_ty: cranelift_codegen::ir::Type,
220 arg_count: usize,
221 make_data: impl FnOnce(&[Value]) -> InstructionData,
222 ) -> InstFixture {
223 let mut sig = Signature::new(CallConv::Fast);
224 for _ in 0..arg_count {
225 sig.params.push(AbiParam::new(param_ty));
226 }
227 sig.returns.push(AbiParam::new(param_ty));
228
229 let mut func = Function::with_name_signature(UserFuncName::testcase("t"), sig);
230 let block: Block = func.dfg.make_block();
231 let mut operands: Vec<Value> = Vec::with_capacity(arg_count);
232 for _ in 0..arg_count {
233 operands.push(func.dfg.append_block_param(block, param_ty));
234 }
235 let data = make_data(&operands);
236 let inst = func.dfg.make_inst(data);
237 func.dfg.make_inst_results(inst, param_ty);
238 InstFixture {
239 func,
240 inst,
241 operands,
242 }
243 }
244
245 /// Map the fixture's operand `Value`s to fresh
246 /// [`LoweredValueId`]s starting at 0. Returns the map and the
247 /// first id not yet used (i.e. the id the lowering should assign
248 /// to the instruction's result).
249 fn map_operands(operands: &[Value]) -> (HashMap<Value, LoweredValueId>, LoweredValueId) {
250 let mut map = HashMap::new();
251 let mut next: LoweredValueId = 0;
252 for v in operands {
253 map.insert(*v, next);
254 next += 1;
255 }
256 (map, next)
257 }
258
259 fn binary(opcode: Opcode, args: [Value; 2]) -> InstructionData {
260 InstructionData::Binary { opcode, args }
261 }
262
263 fn ternary(opcode: Opcode, args: [Value; 3]) -> InstructionData {
264 InstructionData::Ternary { opcode, args }
265 }
266
267 fn unary(opcode: Opcode, arg: Value) -> InstructionData {
268 InstructionData::Unary { opcode, arg }
269 }
270
271 #[test]
272 fn lower_fadd_f32() {
273 let fx = build_func_with_inst(types::F32, 2, |params| {
274 binary(Opcode::Fadd, [params[0], params[1]])
275 });
276 let (map, mut next) = map_operands(&fx.operands);
277 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fadd lowers");
278 match op {
279 LoweredOp::AddF {
280 ty,
281 lhs,
282 rhs,
283 result,
284 } => {
285 assert_eq!(ty, LoweredType::F32);
286 assert_eq!(lhs, 0);
287 assert_eq!(rhs, 1);
288 assert_eq!(result, 2);
289 }
290 other => panic!("expected AddF, got {other:?}"),
291 }
292 assert_eq!(next, 3);
293 }
294
295 #[test]
296 fn lower_fsub_f64() {
297 let fx = build_func_with_inst(types::F64, 2, |params| {
298 binary(Opcode::Fsub, [params[0], params[1]])
299 });
300 let (map, mut next) = map_operands(&fx.operands);
301 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fsub lowers");
302 match op {
303 LoweredOp::SubF { ty, .. } => assert_eq!(ty, LoweredType::F64),
304 other => panic!("expected SubF, got {other:?}"),
305 }
306 }
307
308 #[test]
309 fn lower_fmul_f32() {
310 let fx = build_func_with_inst(types::F32, 2, |params| {
311 binary(Opcode::Fmul, [params[0], params[1]])
312 });
313 let (map, mut next) = map_operands(&fx.operands);
314 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fmul lowers");
315 match op {
316 LoweredOp::MulF { ty, .. } => assert_eq!(ty, LoweredType::F32),
317 other => panic!("expected MulF, got {other:?}"),
318 }
319 }
320
321 #[test]
322 fn lower_fdiv_f64() {
323 let fx = build_func_with_inst(types::F64, 2, |params| {
324 binary(Opcode::Fdiv, [params[0], params[1]])
325 });
326 let (map, mut next) = map_operands(&fx.operands);
327 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fdiv lowers");
328 match op {
329 LoweredOp::DivF { ty, .. } => assert_eq!(ty, LoweredType::F64),
330 other => panic!("expected DivF, got {other:?}"),
331 }
332 }
333
334 #[test]
335 fn lower_fma_f32() {
336 let fx = build_func_with_inst(types::F32, 3, |params| {
337 ternary(Opcode::Fma, [params[0], params[1], params[2]])
338 });
339 let (map, mut next) = map_operands(&fx.operands);
340 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fma lowers");
341 match op {
342 LoweredOp::Fma {
343 ty,
344 a,
345 b,
346 c,
347 result,
348 } => {
349 assert_eq!(ty, LoweredType::F32);
350 assert_eq!(a, 0);
351 assert_eq!(b, 1);
352 assert_eq!(c, 2);
353 assert_eq!(result, 3);
354 }
355 other => panic!("expected Fma, got {other:?}"),
356 }
357 }
358
359 #[test]
360 fn lower_fneg_f64() {
361 let fx = build_func_with_inst(types::F64, 1, |params| unary(Opcode::Fneg, params[0]));
362 let (map, mut next) = map_operands(&fx.operands);
363 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fneg lowers");
364 match op {
365 LoweredOp::FNeg { ty, src, result } => {
366 assert_eq!(ty, LoweredType::F64);
367 assert_eq!(src, 0);
368 assert_eq!(result, 1);
369 }
370 other => panic!("expected FNeg, got {other:?}"),
371 }
372 }
373
374 #[test]
375 fn lower_fabs_f32() {
376 let fx = build_func_with_inst(types::F32, 1, |params| unary(Opcode::Fabs, params[0]));
377 let (map, mut next) = map_operands(&fx.operands);
378 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next).expect("fabs lowers");
379 match op {
380 LoweredOp::FAbs { ty, src, result } => {
381 assert_eq!(ty, LoweredType::F32);
382 assert_eq!(src, 0);
383 assert_eq!(result, 1);
384 }
385 other => panic!("expected FAbs, got {other:?}"),
386 }
387 }
388
389 /// Non-float opcodes (here `iadd`) return `None` and leave the
390 /// id-counter untouched so the caller can fall through to the next
391 /// lowering family.
392 #[test]
393 fn non_float_opcode_returns_none() {
394 let fx = build_func_with_inst(types::I32, 2, |params| {
395 binary(Opcode::Iadd, [params[0], params[1]])
396 });
397 let (map, mut next) = map_operands(&fx.operands);
398 let before = next;
399 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next);
400 assert!(op.is_none(), "iadd must not be claimed by lower_float");
401 assert_eq!(next, before, "counter must not advance on rejection");
402 }
403
404 /// A float opcode on an unsupported type (here a v128 vector
405 /// fadd) is rejected — those rows live in `lower_vector`. The
406 /// caller falls through cleanly.
407 #[test]
408 fn vector_fadd_returns_none() {
409 let fx = build_func_with_inst(types::F32X4, 2, |params| {
410 binary(Opcode::Fadd, [params[0], params[1]])
411 });
412 let (map, mut next) = map_operands(&fx.operands);
413 let before = next;
414 let op = lower_float_inst(fx.inst, &fx.func, &map, &mut next);
415 assert!(
416 op.is_none(),
417 "vector fadd must not be claimed by lower_float"
418 );
419 assert_eq!(next, before);
420 }
421}