Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions cuda-oxide/crates/cuda-macros/src/launch_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use syn::{
parse::{Parse, ParseStream},
parse_macro_input, parse_quote,
punctuated::Punctuated,
spanned::Spanned,
visit::{self, Visit},
visit_mut::{self, VisitMut},
};
Expand Down Expand Up @@ -443,10 +444,13 @@ pub(crate) struct LoopUnrollAttrVisitor {

impl LoopUnrollAttrVisitor {
/// If `attrs` contains an outer `#[unroll]` / `#[unroll(N)]`, remove it and
/// return the parsed factor. Returns `None` when no `unroll` attribute is
/// present (leaving `attrs` untouched). Records a parse error and returns
/// `None` if the attribute is malformed.
fn take_unroll_factor(&mut self, attrs: &mut Vec<syn::Attribute>) -> Option<ConstU32Expr> {
/// return the parsed factor with the attribute's span. Returns `None` when
/// no `unroll` attribute is present (leaving `attrs` untouched). Records a
/// parse error and returns `None` if the attribute is malformed.
fn take_unroll_factor(
&mut self,
attrs: &mut Vec<syn::Attribute>,
) -> Option<(ConstU32Expr, proc_macro2::Span)> {
let idx = attrs
.iter()
.position(|attr| attr.path().is_ident("unroll"))?;
Expand Down Expand Up @@ -477,14 +481,20 @@ impl LoopUnrollAttrVisitor {
if factor.literal_value.is_none() {
self.const_expressions.push(factor.clone());
}
Some(factor)
Some((factor, attr.span()))
}

/// Build the `__unroll_config::<FACTOR>()` marker statement.
fn marker_stmt(factor: &ConstU32Expr) -> Stmt {
/// Build the `__unroll_config::<FACTOR>()` marker statement. The call
/// carries `span`, the `#[unroll]` attribute's, so diagnostics about the
/// loop point at the attribute. The braces around the factor keep the
/// macro's own span. On the attribute's span rustc treats them as written
/// by the user, and its `unused_braces` lint then reports every literal
/// factor with a fix-it that deletes the attribute.
fn marker_stmt(factor: &ConstU32Expr, span: proc_macro2::Span) -> Stmt {
let expr = &factor.expr;
parse_quote! {
cuda_device::thread::__unroll_config::<{ #expr }>();
let factor_arg = quote! { { #expr } };
syn::parse_quote_spanned! {span=>
cuda_device::thread::__unroll_config::<#factor_arg>();
}
}
}
Expand All @@ -496,18 +506,27 @@ impl VisitMut for LoopUnrollAttrVisitor {
// else falls through to the default recursion below.
match expr {
Expr::ForLoop(for_loop) => {
if let Some(factor) = self.take_unroll_factor(&mut for_loop.attrs) {
for_loop.body.stmts.insert(0, Self::marker_stmt(&factor));
if let Some((factor, span)) = self.take_unroll_factor(&mut for_loop.attrs) {
for_loop
.body
.stmts
.insert(0, Self::marker_stmt(&factor, span));
}
}
Expr::While(while_loop) => {
if let Some(factor) = self.take_unroll_factor(&mut while_loop.attrs) {
while_loop.body.stmts.insert(0, Self::marker_stmt(&factor));
if let Some((factor, span)) = self.take_unroll_factor(&mut while_loop.attrs) {
while_loop
.body
.stmts
.insert(0, Self::marker_stmt(&factor, span));
}
}
Expr::Loop(loop_expr) => {
if let Some(factor) = self.take_unroll_factor(&mut loop_expr.attrs) {
loop_expr.body.stmts.insert(0, Self::marker_stmt(&factor));
if let Some((factor, span)) = self.take_unroll_factor(&mut loop_expr.attrs) {
loop_expr
.body
.stmts
.insert(0, Self::marker_stmt(&factor, span));
}
}
_ => {}
Expand Down
28 changes: 27 additions & 1 deletion cuda-oxide/crates/cuda-macros/src/tests/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::launch_attrs::{
};
use quote::{format_ident, quote};
use reserved_oxide_symbols::INSTANTIATE_PREFIX;
use syn::{ItemFn, Stmt, parse_quote, visit_mut::VisitMut};
use syn::{Expr, ItemFn, Stmt, parse_quote, spanned::Spanned, visit_mut::VisitMut};

#[test]
fn generated_kernel_siblings_preserve_qualified_paths_and_generics() {
Expand Down Expand Up @@ -266,6 +266,32 @@ fn unroll_n_on_for_loop_injects_factor_n_marker() {
);
}

#[test]
fn unroll_marker_takes_the_position_of_its_attribute() {
// `parse_str` keeps real lines; `parse_quote!` gives every span line 0.
let cases = [
("fn k() {\n #[unroll]\n while true {}\n}", 2, 4),
(
"fn k() {\n\n #[unroll(4)]\n for _ in 0..8 {}\n}",
3,
4,
),
("fn k() {\n #[unroll(2)]\n loop { break; }\n}", 2, 2),
];
for (source, line, column) in cases {
let mut func: ItemFn = syn::parse_str(source).expect("test source parses");
rewrite_loop_unroll_attrs(&mut func).expect("valid unroll attribute");
let marker = match &func.block.stmts[0] {
Stmt::Expr(Expr::While(l), _) => &l.body.stmts[0],
Stmt::Expr(Expr::ForLoop(l), _) => &l.body.stmts[0],
Stmt::Expr(Expr::Loop(l), _) => &l.body.stmts[0],
other => panic!("expected a loop, got {}", quote!(#other)),
};
let start = marker.span().start();
assert_eq!((start.line, start.column), (line, column), "{source}");
}
}

#[test]
fn unroll_on_bare_loop_injects_marker() {
let func: ItemFn = parse_quote! {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ note: erroneous constant encountered
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note: the above error was encountered while instantiating `fn __unroll_config::<1>`
--> tests/compile_fail/policy_config_invalid_unroll.rs:12:1
--> tests/compile_fail/policy_config_invalid_unroll.rs:15:5
|
12 | #[kernel]
| ^^^^^^^^^
|
= note: this note originates in the attribute macro `kernel` (in Nightly builds, run with -Z macro-backtrace for more info)
15 | #[unroll(Invalid::UNROLL)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
1 change: 1 addition & 0 deletions cuda-oxide/crates/cuda-macros/tests/macro_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
fn macro_guards() {
let t = trybuild::TestCases::new();
t.pass("tests/pass/const_generic_hygiene.rs");
t.pass("tests/pass/unroll_literal_factor_lint_clean.rs");
t.pass("tests/pass/cuda_module_inline_namespaces.rs");
t.compile_fail("tests/compile_fail/kernel_reserved_name.rs");
t.compile_fail("tests/compile_fail/device_reserved_name.rs");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// A literal `#[unroll(N)]` expands to a marker call whose const argument sits
// in braces. Those braces must keep the macro's own span. On the attribute's
// span rustc treats them as written by the user, and its `unused_braces` lint
// then reports every literal factor in the user's crate, with a fix-it that
// deletes the attribute. This crate denies that lint, so the build fails if
// the braces ever take the attribute's span again.
#![deny(unused_braces)]

use cuda_device::{device, kernel};

#[device]
fn count_down(mut remaining: u32) -> u32 {
let mut steps = 0;
#[unroll]
loop {
if remaining == 0 {
break;
}
remaining -= 1;
steps += 1;
}
steps
}

#[kernel]
fn literal_factors(limit: u32) {
let mut total = 0;
#[unroll(4)]
for i in 0..8 {
total += i;
}
let mut index = 0;
#[unroll(2)]
while index < limit {
index += 1;
}
let _ = (total, count_down(limit));
}

fn main() {
assert_eq!(count_down(3), 3);
}
Loading
Loading