Skip to main content

tensor_wasm_jit/
lowering_errors.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! Structured errors produced by the Cranelift → [`LoweredFunction`]
5//! lowering pipeline (RFC 0001 wave 2).
6//!
7//! Each variant carries enough context for a maintainer to grep the call
8//! site and understand the failure. The boundary into
9//! [`crate::pliron_dialect::PlironLoweringError`] is provided so the
10//! wave-2 lowering driver (W2.4) and the `CraneliftLowerer` impl (W2.5)
11//! can bubble these errors through the trait surface.
12//!
13//! [`LoweredFunction`]: crate::lowered_ir::LoweredFunction
14
15#![cfg(feature = "cuda-oxide-backend")]
16
17use cranelift_codegen::ir as cl;
18use thiserror::Error;
19
20// TODO(W2.2): once `crate::lower_signature::SignatureLoweringError` lands,
21// re-introduce the `Signature(#[from] SignatureLoweringError)` variant and
22// update the `From<LoweringError> for PlironLoweringError` mapping so
23// signature errors also reach the trait surface.
24
25/// Structured errors produced by the Cranelift → `LoweredFunction`
26/// lowering pipeline.
27///
28/// Each variant carries enough context for a maintainer to grep the call
29/// site and understand the failure. The wave-2 lowering driver (W2.4)
30/// and `CraneliftLowerer` impl (W2.5) return this error type, which is
31/// then mapped into [`crate::pliron_dialect::PlironLoweringError`] at
32/// the trait boundary.
33#[derive(Debug, Clone, PartialEq, Eq, Error)]
34pub enum LoweringError {
35    /// The opcode has no `LoweredOp` equivalent and isn't on the
36    /// reject-list either — this is a "we forgot to wire it up" path.
37    /// Distinct from [`LoweringError::Rejected`] because it indicates a
38    /// hole, not a deliberate refusal.
39    #[error("lowering: unsupported opcode `{op}` (no lower_* family matched)")]
40    UnsupportedOpcode {
41        /// Cranelift opcode mnemonic.
42        op: String,
43        /// Block + instruction index inside the function, for diagnostics.
44        location: InstLocation,
45    },
46
47    /// A Cranelift [`Value`](cl::Value) or [`Block`](cl::Block) reference
48    /// resolved to an unsupported type.
49    #[error("lowering: unsupported type `{ty}` at {location}")]
50    UnsupportedType {
51        /// Cranelift type as printed via `Display`.
52        ty: String,
53        /// Where the unsupported type appears.
54        location: InstLocation,
55    },
56
57    /// An operand [`Value`](cl::Value) wasn't allocated in the
58    /// `LoweringBuilder` before use. Indicates a use-before-def in the
59    /// walker (caller bug, not user input).
60    #[error("lowering: undefined value `{value:?}` at {location}")]
61    UndefinedValue {
62        /// The Cranelift `Value` that wasn't in the `value_map`.
63        value: cl::Value,
64        /// Where the missing operand appeared.
65        location: InstLocation,
66    },
67
68    /// A branch instruction referred to a [`Block`](cl::Block) that the
69    /// walker didn't visit (so the `block_map` has no entry).
70    #[error("lowering: branch references unknown block `{block:?}` at {location}")]
71    BadBlockReference {
72        /// The unresolved block.
73        block: cl::Block,
74        /// Where the branch appears.
75        location: InstLocation,
76    },
77
78    /// A `LoweredBlock` has no terminator (or has a terminator that isn't
79    /// last).
80    #[error("lowering: malformed terminator in block `{block_id}`")]
81    MalformedTerminator {
82        /// The `LoweredBlockId` of the offending block.
83        block_id: crate::lowered_ir::LoweredBlockId,
84    },
85
86    /// The function was hit by the reject-list (W2.1). Carries the first
87    /// rejection for diagnostics; the full list is available from
88    /// [`crate::reject_list`].
89    #[error("lowering: function rejected by reject-list: {reason}")]
90    Rejected {
91        /// One-line description of the rejection reason.
92        reason: String,
93        /// Where the rejected op appears.
94        location: InstLocation,
95    },
96}
97
98/// Location of a Cranelift instruction inside a function, used for
99/// diagnostics. Cheap to construct and stable across the lowering pass.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct InstLocation {
102    /// Cranelift block id (as displayed: `block0`, `block1`, etc).
103    pub block: u32,
104    /// Position within the block (0-indexed, layout order).
105    pub inst_index: u32,
106}
107
108impl std::fmt::Display for InstLocation {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "block{}[{}]", self.block, self.inst_index)
111    }
112}
113
114impl InstLocation {
115    /// Construct from a Cranelift [`Block`](cl::Block) + a 0-indexed
116    /// instruction position.
117    ///
118    /// Cranelift's `Block` exposes its numeric index via the
119    /// `EntityRef::index()` trait method, which is what gets rendered as
120    /// `block0`/`block1`/... in Cranelift's textual IR. We snapshot that
121    /// index into a `u32` so `InstLocation` stays plain-old-data and
122    /// doesn't drag Cranelift types into error messages.
123    pub fn new(block: cl::Block, inst_index: u32) -> Self {
124        use cranelift_codegen::entity::EntityRef;
125        Self {
126            block: block.index() as u32,
127            inst_index,
128        }
129    }
130}
131
132impl From<LoweringError> for crate::pliron_dialect::PlironLoweringError {
133    /// Map every `LoweringError` variant into
134    /// [`PlironLoweringError::UnsupportedOp`] using the error's `Display`
135    /// representation as the `op` description.
136    ///
137    /// This is intentionally lossy for wave 2: richer mapping (dedicated
138    /// `PlironLoweringError` variants per `LoweringError` kind) comes in
139    /// wave 3 when `PlironLoweringError` grows the necessary surface.
140    /// The point of this `From` impl is to land the boundary now so W2.5
141    /// can bubble driver errors into the trait surface without churn
142    /// later.
143    fn from(err: LoweringError) -> Self {
144        Self::UnsupportedOp {
145            op: err.to_string(),
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::pliron_dialect::PlironLoweringError;
154    use cranelift_codegen::entity::EntityRef;
155
156    fn loc(block: u32, inst_index: u32) -> InstLocation {
157        InstLocation { block, inst_index }
158    }
159
160    #[test]
161    fn inst_location_display_matches_block_index_format() {
162        assert_eq!(loc(5, 3).to_string(), "block5[3]");
163        assert_eq!(loc(0, 0).to_string(), "block0[0]");
164        assert_eq!(loc(42, 17).to_string(), "block42[17]");
165    }
166
167    #[test]
168    fn inst_location_new_round_trips_block_index() {
169        // Cranelift `Block` numbering is exposed through `EntityRef`; the
170        // `new` constructor must snapshot that into the `block` field
171        // verbatim.
172        let block = cl::Block::new(7);
173        let location = InstLocation::new(block, 4);
174        assert_eq!(location.block, 7);
175        assert_eq!(location.inst_index, 4);
176        assert_eq!(location.to_string(), "block7[4]");
177    }
178
179    #[test]
180    fn inst_location_eq() {
181        // PartialEq + Eq let callers compare locations in tests cheaply.
182        assert_eq!(loc(1, 2), loc(1, 2));
183        assert_ne!(loc(1, 2), loc(1, 3));
184        assert_ne!(loc(1, 2), loc(2, 2));
185    }
186
187    #[test]
188    fn unsupported_opcode_displays_op_and_location() {
189        let err = LoweringError::UnsupportedOpcode {
190            op: "iadd_imm".to_string(),
191            location: loc(2, 5),
192        };
193        let s = err.to_string();
194        assert!(s.contains("iadd_imm"), "got: {s}");
195        assert!(s.contains("no lower_* family matched"), "got: {s}");
196    }
197
198    #[test]
199    fn unsupported_type_displays_ty_and_location() {
200        let err = LoweringError::UnsupportedType {
201            ty: "i128".to_string(),
202            location: loc(0, 1),
203        };
204        let s = err.to_string();
205        assert!(s.contains("i128"), "got: {s}");
206        assert!(s.contains("block0[1]"), "got: {s}");
207    }
208
209    #[test]
210    fn undefined_value_displays_value_and_location() {
211        let value = cl::Value::new(13);
212        let err = LoweringError::UndefinedValue {
213            value,
214            location: loc(3, 0),
215        };
216        let s = err.to_string();
217        assert!(s.contains("block3[0]"), "got: {s}");
218        // The Debug repr of `Value` typically renders as `v13`, but we
219        // only care that *some* rendering of the value is in the string.
220        assert!(s.contains("undefined value"), "got: {s}");
221    }
222
223    #[test]
224    fn bad_block_reference_displays_block_and_location() {
225        let block = cl::Block::new(9);
226        let err = LoweringError::BadBlockReference {
227            block,
228            location: loc(1, 7),
229        };
230        let s = err.to_string();
231        assert!(s.contains("block1[7]"), "got: {s}");
232        assert!(s.contains("unknown block"), "got: {s}");
233    }
234
235    #[test]
236    fn malformed_terminator_displays_block_id() {
237        let err = LoweringError::MalformedTerminator { block_id: 4 };
238        let s = err.to_string();
239        assert!(s.contains("malformed terminator"), "got: {s}");
240        assert!(s.contains("4"), "got: {s}");
241    }
242
243    #[test]
244    fn rejected_displays_reason_and_location() {
245        let err = LoweringError::Rejected {
246            reason: "atomic ops not supported".to_string(),
247            location: loc(0, 3),
248        };
249        let s = err.to_string();
250        assert!(s.contains("atomic ops not supported"), "got: {s}");
251        assert!(s.contains("rejected by reject-list"), "got: {s}");
252    }
253
254    #[test]
255    fn lowering_error_eq() {
256        // Two errors built from the same fields must compare equal.
257        let a = LoweringError::UnsupportedOpcode {
258            op: "iadd_imm".to_string(),
259            location: loc(2, 5),
260        };
261        let b = LoweringError::UnsupportedOpcode {
262            op: "iadd_imm".to_string(),
263            location: loc(2, 5),
264        };
265        let c = LoweringError::UnsupportedOpcode {
266            op: "iadd_imm".to_string(),
267            location: loc(2, 6),
268        };
269        assert_eq!(a, b);
270        assert_ne!(a, c);
271
272        // Across variants, different kinds never compare equal even with
273        // matching field values.
274        let d = LoweringError::MalformedTerminator { block_id: 4 };
275        let e = LoweringError::MalformedTerminator { block_id: 4 };
276        let f = LoweringError::MalformedTerminator { block_id: 5 };
277        assert_eq!(d, e);
278        assert_ne!(d, f);
279        assert_ne!(a, d);
280    }
281
282    #[test]
283    fn from_lowering_error_into_pliron_unsupported_op_uses_display() {
284        // The boundary into `PlironLoweringError` must produce
285        // `UnsupportedOp` and use the source error's Display string as
286        // the `op` description.
287        let err = LoweringError::UnsupportedOpcode {
288            op: "iadd_imm".to_string(),
289            location: loc(2, 5),
290        };
291        let display = err.to_string();
292        let pliron: PlironLoweringError = err.into();
293        match pliron {
294            PlironLoweringError::UnsupportedOp { op } => {
295                assert_eq!(op, display);
296                assert!(op.contains("iadd_imm"), "got: {op}");
297            }
298            other => panic!("expected UnsupportedOp, got {other:?}"),
299        }
300    }
301
302    #[test]
303    fn from_lowering_error_into_pliron_for_rejected_variant() {
304        // Sanity check: a non-opcode variant also routes to UnsupportedOp
305        // for now (richer mapping is wave 3).
306        let err = LoweringError::Rejected {
307            reason: "atomics".to_string(),
308            location: loc(0, 0),
309        };
310        let display = err.to_string();
311        let pliron: PlironLoweringError = err.into();
312        match pliron {
313            PlironLoweringError::UnsupportedOp { op } => {
314                assert_eq!(op, display);
315                assert!(op.contains("atomics"), "got: {op}");
316            }
317            other => panic!("expected UnsupportedOp, got {other:?}"),
318        }
319    }
320
321    #[test]
322    fn inst_location_new_uses_entity_ref_index() {
323        // Defence-in-depth: the EntityRef trait is what Cranelift uses to
324        // render `blockN`, and `InstLocation::new` must use the same
325        // numbering so error messages line up with Cranelift's textual
326        // IR.
327        let block = cl::Block::new(123);
328        assert_eq!(block.index(), 123);
329        let location = InstLocation::new(block, 0);
330        assert_eq!(location.block, 123);
331    }
332}