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