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
167impl ComposerError {
170 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#[cfg(not(any(test, target_arch = "wasm32")))]
308fn supports_color() -> bool {
309 false
311}