Skip to main content

tensor_wasm_jit/
lower_signature.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! Cranelift [`Signature`] → [`crate::lowered_ir::LoweredSignature`] conversion
5//! (wave 2 of the Pliron pipeline, task W2.2).
6//!
7//! This module bridges the Cranelift ABI surface — `Signature`, `AbiParam`,
8//! `ArgumentPurpose`, `CallConv` — to the pure-Rust `LoweredSignature` used
9//! by every wave-1 lowering family (`lower_arith`, `lower_float`, etc.). The
10//! conversion is intentionally narrow:
11//!
12//! - Only `ArgumentPurpose::Normal` params/returns are admitted. Struct-return
13//!   pointers, VM-context pointers, and struct-by-value args are host-ABI
14//!   shapes the wave-1 `LoweredOp` IR has no representation for and they
15//!   would not survive PTX codegen.
16//! - Only `SystemV` / `Fast` / `Cold` call-convs are admitted. The other
17//!   variants (Windows fastcall, Wasm-flavoured, probestack, etc.) carry
18//!   host-only fixups (callee-saved sets, shadow space, frame-chain
19//!   probing) that the GPU side cannot honour.
20//! - Type mapping mirrors [`crate::lowered_ir::LoweredType`] exactly. Types
21//!   outside that set (notably `I128`, `F16`, `F128`) surface as a
22//!   structured [`SignatureLoweringError::UnsupportedType`].
23//!
24//! [`Signature`]: cranelift_codegen::ir::Signature
25
26#![cfg(feature = "cuda-oxide-backend")]
27
28use cranelift_codegen::ir::{self, AbiParam, ArgumentPurpose, Signature};
29use cranelift_codegen::isa::CallConv;
30
31use crate::lowered_ir::{LoweredSignature, LoweredType};
32
33/// Errors when lowering a Cranelift signature.
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35pub enum SignatureLoweringError {
36    /// A parameter or return type has no `LoweredType` equivalent.
37    #[error("unsupported type at {position}: {ty}")]
38    UnsupportedType {
39        /// `"param N"` or `"return N"`, with `N` the 0-based index.
40        position: String,
41        /// The offending Cranelift type, formatted via its `Display` impl.
42        ty: String,
43    },
44    /// A parameter or return slot has an `ArgumentPurpose` other than
45    /// `Normal` (e.g. struct-return pointer, VM context). These ABI
46    /// fixups have no equivalent in the wave-1 `LoweredOp` IR.
47    #[error("unsupported argument purpose at {position}: {purpose}")]
48    UnsupportedArgumentPurpose {
49        /// `"param N"` or `"return N"`.
50        position: String,
51        /// Cranelift purpose, formatted via its `Display` impl.
52        purpose: String,
53    },
54    /// The calling convention requires a host-only fixup that PTX cannot honor.
55    #[error("unsupported call conv: {conv}")]
56    UnsupportedCallConv {
57        /// The Cranelift call conv display.
58        conv: String,
59    },
60}
61
62/// Convert a Cranelift signature to a [`LoweredSignature`].
63///
64/// Maps each [`AbiParam::value_type`] through [`cranelift_type_to_lowered`].
65/// Rejects sigs with [`AbiParam::purpose`] other than
66/// [`ArgumentPurpose::Normal`] — struct-return slots, VM-context pointers,
67/// and struct-by-value args aren't representable in the wave-1 `LoweredOp`
68/// IR. Rejects call-convs other than [`CallConv::SystemV`], [`CallConv::Fast`],
69/// or [`CallConv::Cold`]; kernel candidates aren't expected to use the
70/// host-only variants (Windows fastcall, AppleAarch64, Wasm flavours, …).
71///
72/// # Errors
73///
74/// - [`SignatureLoweringError::UnsupportedCallConv`] for call-convs outside
75///   the `SystemV` / `Fast` / `Cold` allowlist.
76/// - [`SignatureLoweringError::UnsupportedArgumentPurpose`] for any param
77///   or return whose `purpose` is not `Normal`.
78/// - [`SignatureLoweringError::UnsupportedType`] for any param or return
79///   whose `value_type` is outside the [`LoweredType`] set (notably
80///   `I128`, `F16`, `F128`, or any dynamic vector type).
81pub fn lower_signature(sig: &Signature) -> Result<LoweredSignature, SignatureLoweringError> {
82    // Call-conv check first so we fail closed before we walk operands.
83    if !is_supported_call_conv(sig.call_conv) {
84        return Err(SignatureLoweringError::UnsupportedCallConv {
85            conv: sig.call_conv.to_string(),
86        });
87    }
88
89    let mut params = Vec::with_capacity(sig.params.len());
90    for (i, p) in sig.params.iter().enumerate() {
91        params.push(lower_abi_param(p, "param", i)?);
92    }
93
94    let mut returns = Vec::with_capacity(sig.returns.len());
95    for (i, r) in sig.returns.iter().enumerate() {
96        returns.push(lower_abi_param(r, "return", i)?);
97    }
98
99    Ok(LoweredSignature { params, returns })
100}
101
102/// Lower a single [`AbiParam`] (param or return slot), tagging any error
103/// with the caller-supplied positional label (`"param"` / `"return"`) plus
104/// the 0-based slot index.
105fn lower_abi_param(
106    p: &AbiParam,
107    kind: &str,
108    idx: usize,
109) -> Result<LoweredType, SignatureLoweringError> {
110    if p.purpose != ArgumentPurpose::Normal {
111        return Err(SignatureLoweringError::UnsupportedArgumentPurpose {
112            position: format!("{kind} {idx}"),
113            purpose: p.purpose.to_string(),
114        });
115    }
116    cranelift_type_to_lowered(p.value_type).ok_or_else(|| SignatureLoweringError::UnsupportedType {
117        position: format!("{kind} {idx}"),
118        ty: p.value_type.to_string(),
119    })
120}
121
122/// True for call-convs whose calling-side contract is representable on the
123/// GPU. `SystemV` / `Fast` / `Cold` only — the host-only variants
124/// (`WindowsFastcall`, `AppleAarch64`, `WasmtimeSystemV`, `Winch`, `Tail`,
125/// `Probestack`) all require register-save or probestack fixups PTX has
126/// no equivalent for.
127fn is_supported_call_conv(conv: CallConv) -> bool {
128    matches!(conv, CallConv::SystemV | CallConv::Fast | CallConv::Cold)
129}
130
131/// Convert one Cranelift [`Type`] to a [`LoweredType`].
132///
133/// Returns `None` for types the wave-1 `LoweredOp` IR cannot represent:
134///
135/// - `I128`, `F16`, `F128` — out of [`LoweredType`]'s scalar range.
136/// - Dynamic vectors (`is_dynamic_vector()` true) — vec scaling factor
137///   isn't modelled.
138/// - Vectors whose lane type is itself unsupported (e.g. `I128x2` if it
139///   ever appears).
140/// - The `INVALID` sentinel.
141/// - Any other future addition to Cranelift's `Type` enum.
142///
143/// Reference (pointer) types `R32` / `R64` map to [`LoweredType::Ptr`] —
144/// the wave-1 IR is pointer-width-agnostic, so both widths collapse to
145/// the same opaque variant.
146///
147/// [`Type`]: cranelift_codegen::ir::Type
148pub fn cranelift_type_to_lowered(ty: ir::Type) -> Option<LoweredType> {
149    use ir::types as t;
150
151    // Scalar integers first (the common case).
152    if ty == t::I8 {
153        return Some(LoweredType::I8);
154    }
155    if ty == t::I16 {
156        return Some(LoweredType::I16);
157    }
158    if ty == t::I32 {
159        return Some(LoweredType::I32);
160    }
161    if ty == t::I64 {
162        return Some(LoweredType::I64);
163    }
164    // I128 is intentionally rejected: PTX has no native 128-bit integer
165    // and the wave-1 LoweredType has no I128 variant.
166
167    // Scalar floats.
168    if ty == t::F32 {
169        return Some(LoweredType::F32);
170    }
171    if ty == t::F64 {
172        return Some(LoweredType::F64);
173    }
174    // F16 / F128 fall through to the unsupported tail.
175
176    // Reference types — both widths collapse to the opaque `Ptr` variant.
177    // The wave-1 IR doesn't distinguish 32-bit vs 64-bit device pointers
178    // at the type level; the downstream PTX backend selects the correct
179    // address-space register width from target metadata.
180    if ty == t::R32 || ty == t::R64 {
181        return Some(LoweredType::Ptr);
182    }
183
184    // SIMD vectors. Lane type must itself be one of the supported scalars
185    // and the lane count must fit in `u8` (Cranelift vectors are at most
186    // 256 lanes today). Dynamic vectors are rejected because their lane
187    // count isn't statically known and the wave-1 IR doesn't carry the
188    // scaling-factor GlobalValue.
189    if ty.is_vector() && !ty.is_dynamic_vector() {
190        let lane_ty = cranelift_type_to_lowered(ty.lane_type())?;
191        // Only scalar lanes are legal — `LoweredType` forbids nested
192        // vectors. `is_int`/`is_float` cover I8..I64 / F32..F64.
193        if !(lane_ty.is_int() || lane_ty.is_float()) {
194            return None;
195        }
196        let lanes_u32 = ty.lane_count();
197        let lanes: u8 = u8::try_from(lanes_u32).ok()?;
198        return Some(LoweredType::V128 {
199            lane_type: Box::new(lane_ty),
200            lanes,
201        });
202    }
203
204    None
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use cranelift_codegen::ir::{types, AbiParam, ArgumentPurpose, Signature};
211    use cranelift_codegen::isa::CallConv;
212
213    // ---- cranelift_type_to_lowered: one test per branch -----------------
214
215    #[test]
216    fn maps_i8() {
217        assert_eq!(cranelift_type_to_lowered(types::I8), Some(LoweredType::I8));
218    }
219
220    #[test]
221    fn maps_i16() {
222        assert_eq!(
223            cranelift_type_to_lowered(types::I16),
224            Some(LoweredType::I16)
225        );
226    }
227
228    #[test]
229    fn maps_i32() {
230        assert_eq!(
231            cranelift_type_to_lowered(types::I32),
232            Some(LoweredType::I32)
233        );
234    }
235
236    #[test]
237    fn maps_i64() {
238        assert_eq!(
239            cranelift_type_to_lowered(types::I64),
240            Some(LoweredType::I64)
241        );
242    }
243
244    #[test]
245    fn rejects_i128() {
246        // I128 is in Cranelift's type system but outside LoweredType's
247        // scalar range. The wrapper translates this `None` into an
248        // UnsupportedType error.
249        assert_eq!(cranelift_type_to_lowered(types::I128), None);
250    }
251
252    #[test]
253    fn maps_f32() {
254        assert_eq!(
255            cranelift_type_to_lowered(types::F32),
256            Some(LoweredType::F32)
257        );
258    }
259
260    #[test]
261    fn maps_f64() {
262        assert_eq!(
263            cranelift_type_to_lowered(types::F64),
264            Some(LoweredType::F64)
265        );
266    }
267
268    #[test]
269    fn rejects_f16() {
270        assert_eq!(cranelift_type_to_lowered(types::F16), None);
271    }
272
273    #[test]
274    fn rejects_f128() {
275        assert_eq!(cranelift_type_to_lowered(types::F128), None);
276    }
277
278    #[test]
279    fn maps_r32_to_ptr() {
280        assert_eq!(
281            cranelift_type_to_lowered(types::R32),
282            Some(LoweredType::Ptr)
283        );
284    }
285
286    #[test]
287    fn maps_r64_to_ptr() {
288        assert_eq!(
289            cranelift_type_to_lowered(types::R64),
290            Some(LoweredType::Ptr)
291        );
292    }
293
294    #[test]
295    fn maps_i8x16() {
296        assert_eq!(
297            cranelift_type_to_lowered(types::I8X16),
298            Some(LoweredType::V128 {
299                lane_type: Box::new(LoweredType::I8),
300                lanes: 16,
301            })
302        );
303    }
304
305    #[test]
306    fn maps_i16x8() {
307        assert_eq!(
308            cranelift_type_to_lowered(types::I16X8),
309            Some(LoweredType::V128 {
310                lane_type: Box::new(LoweredType::I16),
311                lanes: 8,
312            })
313        );
314    }
315
316    #[test]
317    fn maps_i32x4() {
318        assert_eq!(
319            cranelift_type_to_lowered(types::I32X4),
320            Some(LoweredType::V128 {
321                lane_type: Box::new(LoweredType::I32),
322                lanes: 4,
323            })
324        );
325    }
326
327    #[test]
328    fn maps_i64x2() {
329        assert_eq!(
330            cranelift_type_to_lowered(types::I64X2),
331            Some(LoweredType::V128 {
332                lane_type: Box::new(LoweredType::I64),
333                lanes: 2,
334            })
335        );
336    }
337
338    #[test]
339    fn maps_f32x4() {
340        assert_eq!(
341            cranelift_type_to_lowered(types::F32X4),
342            Some(LoweredType::V128 {
343                lane_type: Box::new(LoweredType::F32),
344                lanes: 4,
345            })
346        );
347    }
348
349    #[test]
350    fn maps_f64x2() {
351        assert_eq!(
352            cranelift_type_to_lowered(types::F64X2),
353            Some(LoweredType::V128 {
354                lane_type: Box::new(LoweredType::F64),
355                lanes: 2,
356            })
357        );
358    }
359
360    #[test]
361    fn rejects_invalid_type() {
362        assert_eq!(cranelift_type_to_lowered(types::INVALID), None);
363    }
364
365    // ---- lower_signature: empty + happy path + error paths --------------
366
367    #[test]
368    fn lowers_empty_signature() {
369        let sig = Signature::new(CallConv::SystemV);
370        let lowered = lower_signature(&sig).expect("empty sig lowers");
371        assert!(lowered.params.is_empty());
372        assert!(lowered.returns.is_empty());
373        assert_eq!(lowered, LoweredSignature::default());
374    }
375
376    #[test]
377    fn lowers_i32_f32_to_i64() {
378        // fn(I32, F32) -> I64
379        let mut sig = Signature::new(CallConv::SystemV);
380        sig.params.push(AbiParam::new(types::I32));
381        sig.params.push(AbiParam::new(types::F32));
382        sig.returns.push(AbiParam::new(types::I64));
383
384        let lowered = lower_signature(&sig).expect("happy-path sig lowers");
385        assert_eq!(
386            lowered,
387            LoweredSignature {
388                params: vec![LoweredType::I32, LoweredType::F32],
389                returns: vec![LoweredType::I64],
390            }
391        );
392    }
393
394    #[test]
395    fn rejects_i128_param_at_position_zero() {
396        let mut sig = Signature::new(CallConv::SystemV);
397        sig.params.push(AbiParam::new(types::I128));
398        sig.returns.push(AbiParam::new(types::I32));
399
400        let err = lower_signature(&sig).expect_err("I128 must be rejected");
401        match err {
402            SignatureLoweringError::UnsupportedType { position, ty } => {
403                assert_eq!(position, "param 0");
404                // Cranelift's Display for I128 is "i128".
405                assert_eq!(ty, types::I128.to_string());
406            }
407            other => panic!("expected UnsupportedType, got {other:?}"),
408        }
409    }
410
411    #[test]
412    fn rejects_i128_return_at_position_one() {
413        // Use a non-zero return index to confirm the positional label
414        // tracks the slot index, not just "0".
415        let mut sig = Signature::new(CallConv::SystemV);
416        sig.params.push(AbiParam::new(types::I32));
417        sig.returns.push(AbiParam::new(types::I32));
418        sig.returns.push(AbiParam::new(types::I128));
419
420        let err = lower_signature(&sig).expect_err("I128 return must be rejected");
421        match err {
422            SignatureLoweringError::UnsupportedType { position, ty } => {
423                assert_eq!(position, "return 1");
424                assert_eq!(ty, types::I128.to_string());
425            }
426            other => panic!("expected UnsupportedType, got {other:?}"),
427        }
428    }
429
430    #[test]
431    fn lowers_v128_return_with_lane_info() {
432        // fn() -> F32X4 — confirms vector returns survive with lane info.
433        let mut sig = Signature::new(CallConv::SystemV);
434        sig.returns.push(AbiParam::new(types::F32X4));
435
436        let lowered = lower_signature(&sig).expect("vector return lowers");
437        assert_eq!(
438            lowered,
439            LoweredSignature {
440                params: vec![],
441                returns: vec![LoweredType::V128 {
442                    lane_type: Box::new(LoweredType::F32),
443                    lanes: 4,
444                }],
445            }
446        );
447    }
448
449    #[test]
450    fn lowers_v128_param() {
451        // fn(I32X4) -> () — vector params too.
452        let mut sig = Signature::new(CallConv::SystemV);
453        sig.params.push(AbiParam::new(types::I32X4));
454
455        let lowered = lower_signature(&sig).expect("vector param lowers");
456        assert_eq!(
457            lowered.params,
458            vec![LoweredType::V128 {
459                lane_type: Box::new(LoweredType::I32),
460                lanes: 4,
461            }]
462        );
463        assert!(lowered.returns.is_empty());
464    }
465
466    #[test]
467    fn rejects_struct_return_purpose() {
468        let mut sig = Signature::new(CallConv::SystemV);
469        sig.params
470            .push(AbiParam::special(types::I64, ArgumentPurpose::StructReturn));
471
472        let err = lower_signature(&sig).expect_err("StructReturn must be rejected");
473        match err {
474            SignatureLoweringError::UnsupportedArgumentPurpose { position, purpose } => {
475                assert_eq!(position, "param 0");
476                assert_eq!(purpose, ArgumentPurpose::StructReturn.to_string());
477            }
478            other => panic!("expected UnsupportedArgumentPurpose, got {other:?}"),
479        }
480    }
481
482    #[test]
483    fn rejects_vmcontext_purpose() {
484        let mut sig = Signature::new(CallConv::SystemV);
485        sig.params.push(AbiParam::new(types::I32));
486        sig.params
487            .push(AbiParam::special(types::I64, ArgumentPurpose::VMContext));
488
489        let err = lower_signature(&sig).expect_err("VMContext must be rejected");
490        match err {
491            SignatureLoweringError::UnsupportedArgumentPurpose { position, .. } => {
492                assert_eq!(position, "param 1");
493            }
494            other => panic!("expected UnsupportedArgumentPurpose, got {other:?}"),
495        }
496    }
497
498    #[test]
499    fn accepts_systemv_fast_cold_callconv() {
500        for cc in [CallConv::SystemV, CallConv::Fast, CallConv::Cold] {
501            let sig = Signature::new(cc);
502            assert!(
503                lower_signature(&sig).is_ok(),
504                "callconv {cc:?} should be accepted",
505            );
506        }
507    }
508
509    #[test]
510    fn rejects_windows_fastcall_callconv() {
511        let sig = Signature::new(CallConv::WindowsFastcall);
512        let err = lower_signature(&sig).expect_err("WindowsFastcall must be rejected");
513        match err {
514            SignatureLoweringError::UnsupportedCallConv { conv } => {
515                assert_eq!(conv, CallConv::WindowsFastcall.to_string());
516            }
517            other => panic!("expected UnsupportedCallConv, got {other:?}"),
518        }
519    }
520
521    #[test]
522    fn rejects_wasmtime_systemv_callconv() {
523        let sig = Signature::new(CallConv::WasmtimeSystemV);
524        assert!(matches!(
525            lower_signature(&sig),
526            Err(SignatureLoweringError::UnsupportedCallConv { .. })
527        ));
528    }
529
530    #[test]
531    fn callconv_check_runs_before_param_check() {
532        // Even with a valid I128 (which would otherwise yield
533        // UnsupportedType), the call-conv error wins.
534        let mut sig = Signature::new(CallConv::WindowsFastcall);
535        sig.params.push(AbiParam::new(types::I128));
536        assert!(matches!(
537            lower_signature(&sig),
538            Err(SignatureLoweringError::UnsupportedCallConv { .. })
539        ));
540    }
541
542    #[test]
543    fn error_display_includes_position_and_type() {
544        let err = SignatureLoweringError::UnsupportedType {
545            position: "param 0".to_string(),
546            ty: "i128".to_string(),
547        };
548        let s = err.to_string();
549        assert!(s.contains("param 0"));
550        assert!(s.contains("i128"));
551    }
552}