Skip to main content

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}