naga_rust_macros/
lib.rs

1//! This is a proc-macro helper library. Don't use this library directly; use [`naga_rust_embed`]
2//! instead.
3//!
4//! [`naga_rust_embed`]: https://docs.rs/naga-rust-embed
5
6#![allow(missing_docs, reason = "not intended to be used directly")]
7
8use std::error::Error;
9use std::fmt;
10use std::fs;
11use std::path::PathBuf;
12
13use quote::quote;
14use syn::Token;
15
16use naga_rust_back::Config;
17use naga_rust_back::naga;
18
19#[proc_macro]
20pub fn include_wgsl_mr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
21    let ConfigAndStr {
22        config,
23        string: path_literal,
24    } = syn::parse_macro_input!(input as ConfigAndStr);
25
26    match include_wgsl_mr_impl(config, &path_literal) {
27        Ok(expansion) => expansion.into(),
28        Err(error) => error.to_compile_error().into(),
29    }
30}
31
32#[proc_macro]
33pub fn wgsl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
34    let ConfigAndStr {
35        config,
36        string: source_literal,
37    } = syn::parse_macro_input!(input as ConfigAndStr);
38
39    match parse_and_translate(config, source_literal.span(), &source_literal.value()) {
40        Ok(expansion) => expansion.into(),
41        Err(error) => error.to_compile_error().into(),
42    }
43}
44
45/// Returns the input unchanged.
46#[proc_macro_attribute]
47pub fn dummy_attribute(
48    _meta: proc_macro::TokenStream,
49    input: proc_macro::TokenStream,
50) -> proc_macro::TokenStream {
51    input
52}
53
54// -------------------------------------------------------------------------------------------------
55
56struct ConfigAndStr {
57    config: Config,
58    string: syn::LitStr,
59}
60
61impl syn::parse::Parse for ConfigAndStr {
62    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
63        let mut config = macro_default_config();
64        loop {
65            // Try parsing the final string literal.
66            let not_a_string_error = match input.parse::<syn::LitStr>() {
67                Ok(string) => {
68                    // Accept a final optional comma after the string.
69                    if !input.is_empty() {
70                        input.parse::<Token![,]>()?;
71                    }
72                    return Ok(Self { config, string });
73                }
74                Err(e) => e,
75            };
76
77            let option_name = input.parse::<syn::Ident>().map_err(|mut e| {
78                e.combine(not_a_string_error);
79                e
80            })?;
81            input.parse::<Token![=]>()?;
82            match &*option_name.to_string() {
83                "global_struct" => {
84                    config = config.global_struct(input.parse::<syn::Ident>()?.to_string());
85                }
86                // TODO: implement other configuration options
87                _ => {
88                    return Err(syn::Error::new_spanned(
89                        option_name,
90                        "unrecognized configuration option name",
91                    ));
92                }
93            }
94            input.parse::<Token![,]>()?;
95        }
96    }
97}
98
99fn macro_default_config() -> Config {
100    Config::default().runtime_path("::naga_rust_embed::rt")
101}
102
103// -------------------------------------------------------------------------------------------------
104
105fn include_wgsl_mr_impl(
106    config: Config,
107    path_literal: &syn::LitStr,
108) -> Result<proc_macro2::TokenStream, syn::Error> {
109    // We use manifest-relative paths because currently, there is no way to arrange for
110    // source-file-relative paths.
111    let mut absolute_path: PathBuf = PathBuf::from(
112        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by Cargo"),
113    );
114    absolute_path.push(path_literal.value());
115
116    // If this fails then we can't generate the `include_str!` we must generate.
117    let absolute_path_str = absolute_path.to_str().ok_or_else(|| {
118        syn::Error::new_spanned(
119            path_literal,
120            format_args!(
121                "absolute path “{p:?}” must be UTF-8",
122                p = absolute_path.display()
123            ),
124        )
125    })?;
126
127    let wgsl_source_text: String = fs::read_to_string(&absolute_path).map_err(|error| {
128        syn::Error::new_spanned(
129            path_literal,
130            format_args!("failed to read “{absolute_path_str}”: {error}"),
131        )
132    })?;
133
134    let translated_tokens = parse_and_translate(config, path_literal.span(), &wgsl_source_text)?;
135
136    Ok(quote! {
137        // Dummy include_str! call tells the compiler that we depend on this file,
138        // which it would not notice otherwise.
139        const _: &str = include_str!(#absolute_path_str);
140
141        #translated_tokens
142    })
143}
144
145fn parse_and_translate(
146    config: Config,
147    wgsl_source_span: proc_macro2::Span,
148    wgsl_source_text: &str,
149) -> Result<proc_macro2::TokenStream, syn::Error> {
150    let module: naga::Module = naga::front::wgsl::parse_str(wgsl_source_text).map_err(|error| {
151        syn::Error::new(
152            wgsl_source_span,
153            format_args!("failed to parse WGSL text: {}", ErrorChain(&error)),
154        )
155    })?;
156
157    // TODO: allow the user of the macro to configure which validation is done.
158    let module_info: naga::valid::ModuleInfo = naga::valid::Validator::new(
159        naga::valid::ValidationFlags::all(),
160        naga_rust_back::CAPABILITIES,
161    )
162    .subgroup_stages(naga::valid::ShaderStages::all())
163    // TODO: Add support for subgroup operations, then update this.
164    .subgroup_operations(naga::valid::SubgroupOperationSet::empty())
165    .validate(&module)
166    .map_err(|error| {
167        syn::Error::new(
168            wgsl_source_span,
169            format_args!("failed to validate WGSL: {}", ErrorChain(&error)),
170        )
171    })?;
172
173    let translated_source: String = naga_rust_back::write_string(&module, &module_info, config)
174        .map_err(|error| {
175            syn::Error::new(
176                wgsl_source_span,
177                format_args!("failed to translate shader to Rust: {}", ErrorChain(&error)),
178            )
179        })?;
180
181    let translated_tokens: proc_macro2::TokenStream =
182        translated_source.parse().map_err(|error| {
183            syn::Error::new(
184                wgsl_source_span,
185                format_args!(
186                    "internal error: translator did not produce valid Rust: {}",
187                    ErrorChain(&error)
188                ),
189            )
190        })?;
191
192    Ok(translated_tokens)
193}
194
195// -------------------------------------------------------------------------------------------------
196
197/// Formatting wrapper which prints an [`Error`] together with its `source()` chain.
198///
199/// The text begins with the [`fmt::Display`] format of the error.
200#[derive(Clone, Copy, Debug)]
201struct ErrorChain<'a>(&'a (dyn Error + 'a));
202
203impl fmt::Display for ErrorChain<'_> {
204    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
205        format_error_chain(fmt, self.0)
206    }
207}
208
209fn format_error_chain(fmt: &mut fmt::Formatter<'_>, mut error: &(dyn Error + '_)) -> fmt::Result {
210    write!(fmt, "{error}")?;
211    while let Some(source) = error.source() {
212        error = source;
213        write!(fmt, "\n↳ {error}")?;
214    }
215
216    Ok(())
217}