naga_oil/compose/
error.rs

1use std::{borrow::Cow, collections::HashMap, ops::Range};
2
3use codespan_reporting::{
4    diagnostic::{Diagnostic, Label},
5    files::SimpleFile,
6    term,
7    term::termcolor::WriteColor,
8};
9use thiserror::Error;
10use tracing::trace;
11
12use super::{preprocess::PreprocessOutput, Composer, ShaderDefValue};
13use crate::{compose::SPAN_SHIFT, redirect::RedirectError};
14
15#[derive(Debug)]
16pub enum ErrSource {
17    Module {
18        name: String,
19        offset: usize,
20        defs: HashMap<String, ShaderDefValue>,
21    },
22    Constructing {
23        path: String,
24        source: String,
25        offset: usize,
26    },
27}
28
29impl ErrSource {
30    pub fn path<'a>(&'a self, composer: &'a Composer) -> &'a String {
31        match self {
32            ErrSource::Module { name, .. } => &composer.module_sets.get(name).unwrap().file_path,
33            ErrSource::Constructing { path, .. } => path,
34        }
35    }
36
37    pub fn source<'a>(&'a self, composer: &'a Composer) -> Cow<'a, String> {
38        match self {
39            ErrSource::Module { name, defs, .. } => {
40                let raw_source = &composer.module_sets.get(name).unwrap().sanitized_source;
41                let Ok(PreprocessOutput {
42                    preprocessed_source: source,
43                    ..
44                }) = composer.preprocessor.preprocess(raw_source, defs)
45                else {
46                    return Default::default();
47                };
48
49                Cow::Owned(source)
50            }
51            ErrSource::Constructing { source, .. } => Cow::Borrowed(source),
52        }
53    }
54
55    pub fn offset(&self) -> usize {
56        match self {
57            ErrSource::Module { offset, .. } | ErrSource::Constructing { offset, .. } => *offset,
58        }
59    }
60}
61
62#[derive(Debug, Error)]
63#[error("Composer error: {inner}")]
64pub struct ComposerError {
65    #[source]
66    pub inner: ComposerErrorInner,
67    pub source: ErrSource,
68}
69
70#[derive(Debug, Error)]
71pub enum ComposerErrorInner {
72    #[error("{0}")]
73    ImportParseError(String, usize),
74    #[error("required import '{0}' not found")]
75    ImportNotFound(String, usize),
76    #[error("{0}")]
77    WgslParseError(naga::front::wgsl::ParseError),
78    #[cfg(feature = "glsl")]
79    #[error("{0:?}")]
80    GlslParseError(naga::front::glsl::ParseErrors),
81    #[error("naga_oil bug, please file a report: failed to convert imported module IR back into WGSL for use with WGSL shaders: {0}")]
82    WgslBackError(naga::back::wgsl::Error),
83    #[cfg(feature = "glsl")]
84    #[error("naga_oil bug, please file a report: failed to convert imported module IR back into GLSL for use with GLSL shaders: {0}")]
85    GlslBackError(naga::back::glsl::Error),
86    #[error("naga_oil bug, please file a report: composer failed to build a valid header: {0}")]
87    HeaderValidationError(naga::WithSpan<naga::valid::ValidationError>),
88    #[error("failed to build a valid final module: {0}")]
89    ShaderValidationError(naga::WithSpan<naga::valid::ValidationError>),
90    #[error(
91        "Not enough '# endif' lines. Each if statement should be followed by an endif statement."
92    )]
93    NotEnoughEndIfs(usize),
94    #[error("Too many '# endif' lines. Each endif should be preceded by an if statement.")]
95    TooManyEndIfs(usize),
96    #[error("'#else' without preceding condition.")]
97    ElseWithoutCondition(usize),
98    #[error("Unknown shader def operator: '{operator}'")]
99    UnknownShaderDefOperator { pos: usize, operator: String },
100    #[error("Unknown shader def: '{shader_def_name}'")]
101    UnknownShaderDef { pos: usize, shader_def_name: String },
102    #[error(
103        "Invalid shader def comparison for '{shader_def_name}': expected {expected}, got {value}"
104    )]
105    InvalidShaderDefComparisonValue {
106        pos: usize,
107        shader_def_name: String,
108        expected: String,
109        value: String,
110    },
111    #[error("multiple inconsistent shader def values: '{def}'")]
112    InconsistentShaderDefValue { def: String },
113    #[error("Attempted to add a module with no #define_import_path")]
114    NoModuleName,
115    #[error("source contains internal decoration string, results probably won't be what you expect. if you have a legitimate reason to do this please file a report")]
116    DecorationInSource(Range<usize>),
117    #[error("naga oil only supports glsl 440 and 450")]
118    GlslInvalidVersion(usize),
119    #[error("invalid override :{0}")]
120    RedirectError(#[from] RedirectError),
121    #[error(
122        "override is invalid as `{name}` is not virtual (this error can be disabled with feature 'override_any')"
123    )]
124    OverrideNotVirtual { name: String, pos: usize },
125    #[error(
126        "Composable module identifiers must not require substitution according to naga writeback rules: `{original}`"
127    )]
128    InvalidIdentifier { original: String, at: naga::Span },
129    #[error("Invalid value for `#define`d shader def {name}: {value}")]
130    InvalidShaderDefDefinitionValue {
131        name: String,
132        value: String,
133        pos: usize,
134    },
135    #[error("#define statements are only allowed at the start of the top-level shaders")]
136    DefineInModule(usize),
137    #[error("Invalid WGSL directive '{directive}' at line {position}: {reason}")]
138    InvalidWgslDirective {
139        directive: String,
140        position: usize,
141        reason: String,
142    },
143}
144
145struct ErrorSources<'a> {
146    current: Option<&'a (dyn std::error::Error + 'static)>,
147}
148
149impl<'a> ErrorSources<'a> {
150    fn of(error: &'a dyn std::error::Error) -> Self {
151        Self {
152            current: error.source(),
153        }
154    }
155}
156
157impl<'a> Iterator for ErrorSources<'a> {
158    type Item = &'a (dyn std::error::Error + 'static);
159
160    fn next(&mut self) -> Option<Self::Item> {
161        let current = self.current;
162        self.current = self.current.and_then(std::error::Error::source);
163        current
164    }
165}
166
167// impl<'a> FusedIterator for ErrorSources<'a> {}
168
169impl ComposerError {
170    /// format a Composer error
171    pub fn emit_to_string(&self, composer: &Composer) -> String {
172        composer.undecorate(&self.emit_to_string_internal(composer))
173    }
174
175    fn emit_to_string_internal(&self, composer: &Composer) -> String {
176        let path = self.source.path(composer);
177        let source = self.source.source(composer);
178        let source_offset = self.source.offset();
179
180        trace!("source:\n~{}~", source);
181        trace!("source offset: {}", source_offset);
182
183        let map_span = |rng: Range<usize>| -> Range<usize> {
184            ((rng.start & ((1 << SPAN_SHIFT) - 1)).saturating_sub(source_offset))
185                ..((rng.end & ((1 << SPAN_SHIFT) - 1)).saturating_sub(source_offset))
186        };
187
188        let files = SimpleFile::new(path, source.as_str());
189        let config = term::Config::default();
190        let (labels, notes) = match &self.inner {
191            ComposerErrorInner::DecorationInSource(range) => {
192                (vec![Label::primary((), range.clone())], vec![])
193            }
194            ComposerErrorInner::HeaderValidationError(v)
195            | ComposerErrorInner::ShaderValidationError(v) => (
196                v.spans()
197                    .map(|(span, desc)| {
198                        trace!(
199                            "mapping span {:?} -> {:?}",
200                            span.to_range().unwrap_or(0..0),
201                            map_span(span.to_range().unwrap_or(0..0))
202                        );
203                        Label::primary((), map_span(span.to_range().unwrap_or(0..0)))
204                            .with_message(desc.to_owned())
205                    })
206                    .collect(),
207                ErrorSources::of(&v)
208                    .map(|source| source.to_string())
209                    .collect(),
210            ),
211            ComposerErrorInner::ImportNotFound(msg, pos) => (
212                vec![Label::primary((), *pos..*pos)],
213                vec![format!("missing import '{msg}'")],
214            ),
215            ComposerErrorInner::ImportParseError(msg, pos) => (
216                vec![Label::primary((), *pos..*pos)],
217                vec![format!("invalid import spec: '{msg}'")],
218            ),
219            ComposerErrorInner::WgslParseError(e) => (
220                e.labels()
221                    .map(|(range, msg)| {
222                        Label::primary((), map_span(range.to_range().unwrap_or(0..0)))
223                            .with_message(msg)
224                    })
225                    .collect(),
226                vec![e.message().to_owned()],
227            ),
228            #[cfg(feature = "glsl")]
229            ComposerErrorInner::GlslParseError(e) => (
230                e.errors
231                    .iter()
232                    .map(|naga::front::glsl::Error { kind, meta }| {
233                        Label::primary((), map_span(meta.to_range().unwrap_or(0..0)))
234                            .with_message(kind.to_string())
235                    })
236                    .collect(),
237                vec![],
238            ),
239            ComposerErrorInner::NotEnoughEndIfs(pos)
240            | ComposerErrorInner::TooManyEndIfs(pos)
241            | ComposerErrorInner::ElseWithoutCondition(pos)
242            | ComposerErrorInner::UnknownShaderDef { pos, .. }
243            | ComposerErrorInner::UnknownShaderDefOperator { pos, .. }
244            | ComposerErrorInner::InvalidShaderDefComparisonValue { pos, .. }
245            | ComposerErrorInner::OverrideNotVirtual { pos, .. }
246            | ComposerErrorInner::GlslInvalidVersion(pos)
247            | ComposerErrorInner::DefineInModule(pos)
248            | ComposerErrorInner::InvalidShaderDefDefinitionValue { pos, .. }
249            | ComposerErrorInner::InvalidWgslDirective { position: pos, .. } => {
250                (vec![Label::primary((), *pos..*pos)], vec![])
251            }
252            ComposerErrorInner::WgslBackError(e) => {
253                return format!("{path}: wgsl back error: {e}");
254            }
255            #[cfg(feature = "glsl")]
256            ComposerErrorInner::GlslBackError(e) => {
257                return format!("{path}: glsl back error: {e}");
258            }
259            ComposerErrorInner::InconsistentShaderDefValue { def } => {
260                return format!("{path}: multiple inconsistent shader def values: '{def}'");
261            }
262            ComposerErrorInner::RedirectError(..) => (
263                vec![Label::primary((), 0..0)],
264                vec![format!("override error")],
265            ),
266            ComposerErrorInner::NoModuleName => {
267                return format!(
268                    "{path}: no #define_import_path declaration found in composable module"
269                );
270            }
271            ComposerErrorInner::InvalidIdentifier { at, .. } => (
272                vec![Label::primary((), map_span(at.to_range().unwrap_or(0..0)))
273                    .with_message(self.inner.to_string())],
274                vec![],
275            ),
276        };
277
278        let diagnostic = Diagnostic::error()
279            .with_message(self.inner.to_string())
280            .with_labels(labels)
281            .with_notes(notes);
282
283        let mut msg = Vec::with_capacity(256);
284
285        let mut color_writer;
286        let mut no_color_writer;
287        let writer: &mut dyn WriteColor = if supports_color() {
288            color_writer = term::termcolor::Ansi::new(&mut msg);
289            &mut color_writer
290        } else {
291            no_color_writer = term::termcolor::NoColor::new(&mut msg);
292            &mut no_color_writer
293        };
294
295        term::emit(writer, &config, &files, &diagnostic).expect("cannot write error");
296
297        String::from_utf8_lossy(&msg).into_owned()
298    }
299}
300
301#[cfg(any(test, target_arch = "wasm32"))]
302fn supports_color() -> bool {
303    false
304}
305
306// termcolor doesn't expose this logic when using custom buffers
307#[cfg(not(any(test, target_arch = "wasm32")))]
308fn supports_color() -> bool {
309    // TODO: https://github.com/tokio-rs/tracing/issues/3378
310    false
311}