Skip to main content

cubecl_core/frontend/
debug.rs

1use alloc::{string::String, vec::Vec};
2use cubecl_ir::{
3    dialect::general::PrintfOp,
4    pliron::{location::Source, value::Value},
5};
6
7use crate::ir::Scope;
8
9use super::CubeDebug;
10
11// The macro calls these functions with plain values: a guard or a closure for each expression
12// adds unwind paths and instances to every expand function, which multiplies the compile memory
13// of crates with many generic `#[cube]` functions.
14
15/// Moves the current `#[cube]` function to `line` and `column`. The macro calls it after the
16/// operands of an expression, before the expression emits its op, so no position needs to be
17/// restored.
18pub fn debug_pos_expand(scope: &Scope, line: u32, column: u32) {
19    if let Some(debug) = scope.debug_state() {
20        debug.set_pos(line, column);
21    }
22}
23
24/// Calls a function from `line` and `column` of the current `#[cube]` function.
25pub fn debug_call_expand<C>(
26    scope: &Scope,
27    line: u32,
28    column: u32,
29    call: impl FnOnce(&Scope) -> C,
30) -> C {
31    debug_pos_expand(scope, line, column);
32    call(scope)
33}
34
35/// Opens the frame of the `#[cube]` function `name`, defined at `line` and `column` of `file`.
36/// The frame closes when the returned guard drops.
37pub fn debug_source_expand<'a>(
38    scope: &'a Scope,
39    name: &'static str,
40    file: &'static str,
41    source_text: &'static str,
42    line: u32,
43    column: u32,
44) -> DebugFrame<'a> {
45    let Some(debug) = scope.debug_state() else {
46        return DebugFrame { scope: None };
47    };
48    let path = file.replace('\\', "/");
49    debug.add_source(&path, source_text);
50    debug.enter_fn(name, Source::new_from_file(scope.ctx(), path), line, column);
51    DebugFrame { scope: Some(scope) }
52}
53
54/// Closes the frame that [`debug_source_expand`] opened.
55pub struct DebugFrame<'a> {
56    scope: Option<&'a Scope>,
57}
58
59impl Drop for DebugFrame<'_> {
60    fn drop(&mut self) {
61        if let Some(debug) = self.scope.and_then(Scope::debug_state) {
62            debug.exit_fn();
63        }
64    }
65}
66
67/// A variable that the macro names only when its type implements [`CubeDebug`].
68///
69/// A `#[cube]` function can bind a value of a plain Rust type, for example a comptime enum that
70/// another `#[cube]` function returns. That value has no name to set, and must not need
71/// [`CubeDebug`]. The macro calls `debug_var` on `&DebugVar`: method resolution takes
72/// [`DebugVarNamed`] when it applies, else [`DebugVarUnnamed`].
73#[doc(hidden)]
74pub struct DebugVar<'a, E>(pub &'a E);
75
76/// Names a [`DebugVar`] whose type implements [`CubeDebug`].
77#[doc(hidden)]
78pub trait DebugVarNamed {
79    fn debug_var(&self, scope: &Scope, name: &'static str);
80}
81
82impl<E: CubeDebug> DebugVarNamed for DebugVar<'_, E> {
83    fn debug_var(&self, scope: &Scope, name: &'static str) {
84        if scope.debug_state().is_some() {
85            self.0.set_debug_name(scope, name);
86        }
87    }
88}
89
90/// Leaves a [`DebugVar`] of any other type without a name.
91#[doc(hidden)]
92pub trait DebugVarUnnamed {
93    fn debug_var(&self, _scope: &Scope, _name: &'static str) {}
94}
95
96impl<E> DebugVarUnnamed for &DebugVar<'_, E> {}
97
98/// Prints a formatted message using the print debug layer in Vulkan, or `printf` in CUDA.
99pub fn printf_expand(scope: &Scope, format_string: impl Into<String>, args: Vec<Value>) {
100    scope.register(&PrintfOp::new(scope.ctx_mut(), format_string.into(), args));
101}
102
103/// Print a formatted message using the target's debug print facilities. The format string is target
104/// specific, but Vulkan and CUDA both use the C++ conventions. WGSL isn't currently supported.
105#[macro_export]
106macro_rules! debug_print {
107    ($format:literal, $($args:expr),*) => {
108        {
109            let _ = $format;
110            $(let _ = $args;)*
111        }
112    };
113    ($format:literal, $($args:expr,)*) => {
114        $crate::debug_print!($format, $($args),*);
115    };
116}
117
118/// Print a formatted message using the target's debug print facilities. The format string is target
119/// specific, but Vulkan and CUDA both use the C++ conventions. WGSL isn't currently supported.
120#[macro_export]
121macro_rules! __expand_debug_print {
122    ($scope:expr, $format:expr, $($args:expr),*) => {
123        {
124            let args = $crate::__private::vec![$($crate::ir::ExpandValue::from($args).read_value($scope)),*];
125            $crate::frontend::printf_expand($scope, $format, args);
126        }
127    };
128    ($format:literal, $($args:expr,)*) => {
129        $crate::__expand_debug_print!($format, $($args),*)
130    };
131}
132
133pub mod cube_comment {
134    use alloc::string::ToString;
135
136    use cubecl_ir::{Scope, dialect::general::CommentOp};
137
138    pub fn expand(scope: &Scope, content: &str) {
139        scope.register(&CommentOp::new(scope.ctx_mut(), content.to_string()));
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use crate as cubecl;
146    use crate::prelude::*;
147    use alloc::vec::Vec;
148    use cubecl_ir::{
149        pliron::{linked_list::ContainsLinkedList, location::Location},
150        settings::{DebugInfo, Dim3, ExecutionMode, KernelSettings},
151    };
152
153    /// The line where the frame of `double` opens, and the line of its statement.
154    const DOUBLE_LINES: [u32; 2] = [line!() + 1, line!() + 3];
155    #[cube]
156    fn double(x: u32) -> u32 {
157        x + x
158    }
159
160    #[cube]
161    fn quadruple(x: u32) -> u32 {
162        double(x) + double(x)
163    }
164
165    #[cube]
166    fn add_double(x: u32) -> u32 {
167        x + double(x)
168    }
169
170    const TWO_SUMS_LINE: u32 = line!() + 3;
171    #[cube]
172    fn two_sums(x: u32) -> u32 {
173        (x + 1) * (x + 2)
174    }
175
176    /// The line of the statement, and the line of the call that continues it.
177    const SPLIT_LINES: [u32; 2] = [line!() + 4, line!() + 5];
178    #[cube]
179    #[rustfmt::skip]
180    fn split_double(x: u32) -> u32 {
181        x
182            + double(x)
183    }
184
185    /// A `#[cube]` function from a `macro_rules!` body: the operator comes from the invocation,
186    /// so its tokens have another hygiene than the code of the macro.
187    macro_rules! unary_cube_fn {
188        ($name:ident, $op:tt) => {
189            #[cube]
190            fn $name(x: u32) -> u32 {
191                $op(x + 1)
192            }
193        };
194    }
195
196    unary_cube_fn!(not_after_one, !);
197
198    #[cube(no_debug_symbols)]
199    fn plain_double(x: u32) -> u32 {
200        x + x
201    }
202
203    const CALL_PLAIN_LINE: u32 = line!() + 3;
204    #[cube]
205    fn call_plain(x: u32) -> u32 {
206        plain_double(x)
207    }
208
209    #[derive(CubeType, Clone, Copy)]
210    struct Pair {
211        a: u32,
212    }
213
214    const SUM_LINES: [u32; 2] = [line!() + 4, line!() + 5];
215    #[cube]
216    impl Pair {
217        fn sum(self, x: u32) -> u32 {
218            let y = self.a + x;
219            y * x
220        }
221    }
222
223    #[cube]
224    fn use_pair(x: u32) -> u32 {
225        let pair = Pair { a: x };
226        pair.sum(x)
227    }
228
229    /// A plain Rust type: it has no `CubeType` and no `CubeDebug`.
230    #[derive(Clone, Copy, PartialEq, Eq)]
231    enum Sign {
232        Plus,
233        Minus,
234    }
235
236    #[cube]
237    fn pick(#[comptime] sign: Sign) -> comptime_type!(Sign) {
238        sign
239    }
240
241    #[cube]
242    fn signed(x: u32, #[comptime] sign: Sign) -> u32 {
243        let sign = pick(sign);
244        if comptime!(sign == Sign::Minus) {
245            x * x
246        } else {
247            x + x
248        }
249    }
250
251    /// Runs `expand` on a new kernel scope and returns the location of each op it inserted.
252    fn locations(
253        debug_info: DebugInfo,
254        expand: impl FnOnce(&Scope, NativeExpand<u32>),
255    ) -> Vec<Location> {
256        let settings =
257            KernelSettings::new(Dim3::new_single(), ExecutionMode::Checked, AddressType::U32)
258                .debug_info(debug_info);
259        let scope = &Scope::root(settings);
260        let x = NativeExpand::<u32>::from_lit(scope, 2);
261        expand(scope, x);
262
263        let ctx = scope.ctx();
264        let block = scope.state().entry_func.get_entry_block(ctx);
265        let ops = block.deref(ctx).iter(ctx).collect::<Vec<_>>();
266        ops.into_iter()
267            .map(|op| op.deref(ctx).loc().clone())
268            .collect()
269    }
270
271    /// A function name, and a line and a column in the function.
272    type Frame<'a> = (&'a str, u32, u32);
273
274    fn frame(loc: &Location) -> Option<Frame<'_>> {
275        match loc {
276            Location::Named { name, child_loc } => match child_loc.as_ref() {
277                Location::SrcPos { pos, .. } => Some((
278                    name,
279                    u32::try_from(pos.line).ok()?,
280                    u32::try_from(pos.column).ok()?,
281                )),
282                _ => None,
283            },
284            _ => None,
285        }
286    }
287
288    /// The innermost frame and its caller, for each op that a call inserted.
289    fn call_sites(locations: &[Location]) -> Vec<(Frame<'_>, Frame<'_>)> {
290        locations
291            .iter()
292            .filter_map(|loc| match loc {
293                Location::CallSite { callee, caller } => Some((frame(callee)?, frame(caller)?)),
294                _ => None,
295            })
296            .collect()
297    }
298
299    #[test]
300    fn ops_get_the_line_of_their_expression() {
301        let locations = locations(DebugInfo::LineTables, |scope, x| {
302            double::expand(scope, x);
303        });
304        let frames = locations.iter().filter_map(frame).collect::<Vec<_>>();
305        assert!(
306            frames.iter().any(|(_, line, _)| *line == DOUBLE_LINES[1]),
307            "{frames:?}"
308        );
309        for (name, line, _) in frames {
310            assert_eq!(name, "double");
311            assert!(DOUBLE_LINES.contains(&line), "{line}");
312        }
313    }
314
315    #[test]
316    fn inlined_calls_keep_their_call_site() {
317        let locations = locations(DebugInfo::LineTables, |scope, x| {
318            quadruple::expand(scope, x);
319        });
320        let call_sites = call_sites(&locations);
321        // Every op of the two `double` calls records that `quadruple` called it.
322        assert!(!call_sites.is_empty(), "{locations:?}");
323        for ((inner, line, _), (outer, ..)) in call_sites {
324            assert_eq!(inner, "double");
325            assert!(DOUBLE_LINES.contains(&line), "{line}");
326            assert_eq!(outer, "quadruple");
327        }
328    }
329
330    /// The addition after `double(x)` is at the start of `x + double(x)`, not at the call.
331    #[test]
332    fn an_op_after_a_call_gets_the_position_of_its_expression() {
333        let locations = locations(DebugInfo::LineTables, |scope, x| {
334            add_double::expand(scope, x);
335        });
336        let (_, (_, call_line, call_column)) = call_sites(&locations)[0];
337        let (name, line, column) = locations.last().and_then(frame).expect("the addition");
338        assert_eq!((name, line), ("add_double", call_line));
339        assert!(column < call_column, "{column} {call_column}");
340    }
341
342    /// Each op on one line gets the column of its own expression: `x + 1`, `x + 2`, and the
343    /// product, which starts first.
344    #[test]
345    fn ops_on_one_line_get_the_column_of_their_expression() {
346        let locations = locations(DebugInfo::LineTables, |scope, x| {
347            two_sums::expand(scope, x);
348        });
349        let mut columns = locations
350            .iter()
351            .filter_map(frame)
352            .filter(|(_, line, _)| *line == TWO_SUMS_LINE)
353            .map(|(.., column)| column)
354            .collect::<Vec<_>>();
355        let (.., product) = locations.last().and_then(frame).expect("the product");
356        columns.sort_unstable();
357        columns.dedup();
358        assert_eq!(columns.len(), 3, "{locations:?}");
359        assert_eq!(product, columns[0], "{locations:?}");
360    }
361
362    /// A call on another line than its statement records that line as its call site. The op
363    /// after the call gets the line of the statement back.
364    #[test]
365    fn a_call_on_a_continuation_line_keeps_its_line() {
366        let locations = locations(DebugInfo::LineTables, |scope, x| {
367            split_double::expand(scope, x);
368        });
369        let call_sites = call_sites(&locations);
370        assert!(!call_sites.is_empty(), "{locations:?}");
371        for (_, (outer, line, _)) in call_sites {
372            assert_eq!((outer, line), ("split_double", SPLIT_LINES[1]));
373        }
374        let (name, line, _) = locations.last().and_then(frame).expect("the addition");
375        assert_eq!((name, line), ("split_double", SPLIT_LINES[0]));
376    }
377
378    /// The debug code that the macro adds must resolve `scope` also for tokens from a
379    /// `macro_rules!` invocation.
380    #[test]
381    fn a_function_from_macro_rules_gets_locations() {
382        let locations = locations(DebugInfo::LineTables, |scope, x| {
383            not_after_one::expand(scope, x);
384        });
385        assert!(
386            locations
387                .iter()
388                .filter_map(frame)
389                .any(|(name, ..)| name == "not_after_one"),
390            "{locations:?}"
391        );
392    }
393
394    /// A function with `no_debug_symbols` opens no frame. Its ops get the location of the call.
395    #[test]
396    fn a_function_without_debug_symbols_has_the_location_of_its_call() {
397        let locations = locations(DebugInfo::LineTables, |scope, x| {
398            call_plain::expand(scope, x);
399        });
400        assert!(call_sites(&locations).is_empty(), "{locations:?}");
401        let last = locations.last().and_then(frame);
402        assert_eq!(
403            last.map(|(name, line, _)| (name, line)),
404            Some(("call_plain", CALL_PLAIN_LINE))
405        );
406    }
407
408    /// A method of a `#[cube] impl` is one frame, with the lines of its statements.
409    #[test]
410    fn a_method_is_one_frame() {
411        let locations = locations(DebugInfo::LineTables, |scope, x| {
412            use_pair::expand(scope, x);
413        });
414        let call_sites = call_sites(&locations);
415        for line in SUM_LINES {
416            assert!(
417                call_sites
418                    .iter()
419                    .any(|((inner, at, _), (outer, ..))| *inner == "Pair :: sum"
420                        && *at == line
421                        && *outer == "use_pair"),
422                "no op at line {line}: {call_sites:?}"
423            );
424        }
425    }
426
427    /// A variable of a plain Rust type, such as a comptime enum, needs no `CubeDebug`. It gets no
428    /// name, and the ops of the function still get their lines.
429    #[test]
430    fn a_plain_variable_needs_no_cube_debug() {
431        for sign in [Sign::Plus, Sign::Minus] {
432            let locations = locations(DebugInfo::LineTables, |scope, x| {
433                signed::expand(scope, x, sign);
434            });
435            assert!(
436                locations
437                    .iter()
438                    .filter_map(frame)
439                    .any(|(name, ..)| name == "signed"),
440                "{locations:?}"
441            );
442        }
443    }
444
445    #[cube]
446    fn add_one(x: u32) -> u32 {
447        x + 1
448    }
449
450    #[cube]
451    fn sum_indices(n: u32) -> u32 {
452        let mut acc = 0u32;
453        for i in 0..n {
454            acc += add_one(i);
455        }
456        acc
457    }
458
459    /// The loop index is a block argument and has no defining op. Debug data names the parameter
460    /// `x` of `add_one`, so the name must leave such a value as it is.
461    #[test]
462    fn a_block_argument_keeps_its_name() {
463        locations(DebugInfo::Full, |scope, x| {
464            sum_indices::expand(scope, x);
465        });
466    }
467
468    #[test]
469    fn no_locations_without_debug_info() {
470        let locations = locations(DebugInfo::None, |scope, x| {
471            double::expand(scope, x);
472        });
473        assert!(locations.iter().all(Location::is_unknown));
474    }
475}