1#![allow(unused_assignments)] use std::fmt;
5
6use miette::{Diagnostic, SourceSpan};
7use pest::{
8 error::{Error as PestError, ErrorVariant, InputLocation, LineColLocation},
9 iterators::Pair,
10};
11use strsim::jaro;
12
13use crate::parser::Rule;
14
15#[derive(thiserror::Error, Debug, Diagnostic)]
17pub enum ParseError {
18 #[error("MPL syntax error: {message}")]
20 #[diagnostic(code(mpl_lang::syntax_error))]
21 SyntaxError {
22 #[label("{message}")]
24 span: SourceSpan,
25 message: String,
27 #[help]
29 suggestion: Option<Suggestion>,
30 },
31
32 #[error("This feature is not supported at the moment: {rule:?}")]
33 #[diagnostic(
35 code(mpl_lang::not_supported),
36 help("This feature may be added in a future version")
37 )]
38 NotSupported {
39 #[label("unsupported: {rule:?}")]
41 span: SourceSpan,
42 rule: Rule,
44 },
45
46 #[error("Unexpected rule: {rule:?} expected one of {expected:?}")]
48 #[diagnostic(code(mpl_lang::unexpected_rule))]
49 Unexpected {
50 #[label("unexpected {rule:?}")]
52 span: SourceSpan,
53 rule: Rule,
55 expected: Vec<Rule>,
57 },
58
59 #[error("Found unexpected tokens: {rules:?}")]
61 #[diagnostic(code(mpl_lang::unexpected_tokens))]
62 UnexpectedTokens {
63 #[label("unexpected tokens")]
65 span: SourceSpan,
66 rules: Vec<Rule>,
68 },
69
70 #[error("Unexpected end of input")]
72 #[diagnostic(
73 code(mpl_lang::unexpected_eof),
74 help("The query appears to be incomplete")
75 )]
76 EOF {
77 #[label("expected more input here")]
79 span: SourceSpan,
80 },
81
82 #[error("Invalid float: {0}")]
84 #[diagnostic(code(mpl_lang::invalid_float))]
85 InvalidFloat(#[from] std::num::ParseFloatError),
86
87 #[error("Invalid integer: {0}")]
89 #[diagnostic(code(mpl_lang::invalid_integer))]
90 InvalidInteger(#[from] std::num::ParseIntError),
91
92 #[error("Invalid bool: {0}")]
94 #[diagnostic(code(mpl_lang::invalid_bool))]
95 InvalidBool(#[from] std::str::ParseBoolError),
96
97 #[error("Invalid date: {0}")]
99 #[diagnostic(code(mpl_lang::invalid_date))]
100 InvalidDate(#[from] chrono::ParseError),
101
102 #[error("Invalid Regex: {0}")]
104 #[diagnostic(code(mpl_lang::invalid_regex))]
105 InvalidRegex(#[from] regex::Error),
106
107 #[error("Unsupported align function: {name}")]
109 #[diagnostic(
110 code(mpl_lang::unsupported_align_function),
111 help("Check the documentation for available align functions")
112 )]
113 UnsupportedAlignFunction {
114 #[label("unknown function")]
116 span: SourceSpan,
117 name: String,
119 },
120
121 #[error("Unsupported group function: {name}")]
123 #[diagnostic(
124 code(mpl_lang::unsupported_group_function),
125 help("Check the documentation for available group functions")
126 )]
127 UnsupportedGroupFunction {
128 #[label("unknown function")]
130 span: SourceSpan,
131 name: String,
133 },
134
135 #[error("Unsupported compute function: {name}")]
137 #[diagnostic(
138 code(mpl_lang::unsupported_compute_function),
139 help("Check the documentation for available compute functions")
140 )]
141 UnsupportedComputeFunction {
142 #[label("unknown function")]
144 span: SourceSpan,
145 name: String,
147 },
148
149 #[error("Unsupported bucket function: {name}")]
151 #[diagnostic(
152 code(mpl_lang::unsupported_bucket_function),
153 help(
154 "Available functions: histogram, interpolate_delta_histogram, interpolate_cumulative_histogram"
155 )
156 )]
157 UnsupportedBucketFunction {
158 #[label("unknown function")]
160 span: SourceSpan,
161 name: String,
163 },
164
165 #[error("Unsupported map evaluation: {name}")]
167 #[diagnostic(
168 code(mpl_lang::unsupported_map_evaluation),
169 help("Check the documentation for available map operations")
170 )]
171 UnsupportedMapEvaluation {
172 #[label("unknown operation")]
174 span: SourceSpan,
175 name: String,
177 },
178
179 #[error("Unsupported map function: {name}")]
181 #[diagnostic(
182 code(mpl_lang::unsupported_map_function),
183 help("Check the documentation for available map functions")
184 )]
185 UnsupportedMapFunction {
186 #[label("unknown function")]
188 span: SourceSpan,
189 name: String,
191 },
192
193 #[error("Unsupported regexp comparison: {op}")]
195 #[diagnostic(
196 code(mpl_lang::unsupported_regexp_comparison),
197 help("Use '==' or '!=' for regex comparisons")
198 )]
199 UnsupportedRegexpComparison {
200 #[label("invalid operator")]
202 span: SourceSpan,
203 op: String,
205 },
206
207 #[error("Unsupported tag comparison: {op}")]
209 #[diagnostic(
210 code(mpl_lang::unsupported_tag_comparison),
211 help("Supported operators: ==, !=, >, >=, <, <=")
212 )]
213 UnsupportedTagComparison {
214 #[label("invalid operator")]
216 span: SourceSpan,
217 op: String,
219 },
220
221 #[error("Not implemented: {0}")]
223 #[diagnostic(
224 code(mpl_lang::not_implemented),
225 help("This feature is planned but not yet implemented")
226 )]
227 NotImplemented(&'static str),
228
229 #[error("String construction error: {0}")]
231 #[diagnostic(code(mpl_lang::strumbra_error))]
232 StrumbraError(#[from] strumbra::Error),
233
234 #[error("Unreachable error: {0}")]
236 #[diagnostic(
237 code(mpl_lang::unreachable),
238 help("This error should never be reached")
239 )]
240 Unreachable(&'static str),
241
242 #[error("The param ${param} is defined multiple times")]
244 #[diagnostic(
245 code(mpl_lang::param_defined_multiple_times),
246 help("This param has been defined more than once")
247 )]
248 ParamDefinedMultipleTimes {
249 #[label("duplicate definition")]
251 span: SourceSpan,
252 param: String,
254 },
255
256 #[error("The param ${param} is not defined")]
258 #[diagnostic(code(mpl_lang::undefined_param))]
259 UndefinedParam {
260 #[label("undefined param")]
262 span: SourceSpan,
263 param: String,
265 },
266 #[error("The type {tpe} is not a valid type for tags")]
268 #[diagnostic(code(mpl_lang::invalid_tag_type))]
269 InvalidTagType {
270 #[label("invalid type")]
272 span: miette::SourceSpan,
273 tpe: String,
275 },
276}
277
278impl From<PestError<Rule>> for ParseError {
279 fn from(err: PestError<Rule>) -> Self {
280 let (start, mut len) = match err.location {
281 InputLocation::Pos(pos) => (pos, 0),
282 InputLocation::Span((start, end)) => (start, end - start),
283 };
284
285 let (message, suggestion) = match &err.variant {
286 ErrorVariant::ParsingError {
287 positives,
288 negatives,
289 } => {
290 let mut msg = String::new();
291 if !positives.is_empty() {
292 msg.push_str("expected ");
293 msg.push_str(&friendly_rules(positives));
294 }
295 if !negatives.is_empty() {
296 if !msg.is_empty() {
297 msg.push_str(", ");
298 }
299 msg.push_str("but found ");
300 msg.push_str(&friendly_rules(negatives));
301 }
302
303 let line_pos = match &err.line_col {
304 LineColLocation::Pos((_, col)) | LineColLocation::Span((_, col), _) => {
305 col.saturating_sub(1)
306 }
307 };
308 let suggestion = generate_suggestion(err.line(), line_pos, positives);
309
310 if len == 0 {
312 len = token_length(err.line(), line_pos);
313 }
314
315 (msg, suggestion)
316 }
317 ErrorVariant::CustomError { message } => (message.clone(), None),
318 };
319
320 ParseError::SyntaxError {
321 span: SourceSpan::new(start.into(), len),
322 message,
323 suggestion,
324 }
325 }
326}
327
328pub(crate) fn pair_to_source_span(pair: &Pair<Rule>) -> SourceSpan {
330 let span = pair.as_span();
331 let start = span.start();
332 let len = span.end() - start;
333 SourceSpan::new(start.into(), len)
334}
335
336fn friendly_rules(rules: &[Rule]) -> String {
338 let names: Vec<_> = rules.iter().copied().map(friendly_rule).collect();
339
340 match names.len() {
341 0 => String::new(),
342 1 => names[0].clone(),
343 2 => format!("{} or {}", names[0], names[1]),
344 _ => {
345 let last = &names[names.len() - 1];
346 let rest = &names[..names.len() - 1];
347 format!("{}, or {last}", rest.join(", "))
348 }
349 }
350}
351
352fn friendly_rule(rule: Rule) -> String {
354 match rule {
355 Rule::EOI => "end of query".to_string(),
357 Rule::pipe_keyword => "\"|\" (pipe)".to_string(),
358
359 Rule::time_range => "time range (e.g., [1h..])".to_string(),
361 Rule::time_relative => "relative time (e.g., 5m, 1h, 7d)".to_string(),
362 Rule::time_timestamp => "timestamp".to_string(),
363 Rule::time_rfc_3339 => "RFC3339 timestamp".to_string(),
364 Rule::time_modifier => "time modifier".to_string(),
365
366 Rule::filter_keyword => "\"where\" keyword".to_string(),
368 Rule::r#as => "\"as\" keyword".to_string(),
369
370 Rule::cmp => "comparison operator (==, !=, <, >, <=, >=)".to_string(),
372 Rule::cmp_re => "regex operator (==, !=)".to_string(),
373 Rule::regex => "regex pattern (e.g., /pattern/)".to_string(),
374
375 Rule::value => "value (string, number, or bool)".to_string(),
377 Rule::string => "string value".to_string(),
378 Rule::number => "number".to_string(),
379 Rule::bool => "bool (true or false)".to_string(),
380
381 Rule::plain_ident => "identifier".to_string(),
383 Rule::escaped_ident => "escaped identifier".to_string(),
384 Rule::source => "source metric".to_string(),
385 Rule::metric_name => "metric name".to_string(),
386 Rule::metric_id => "metric identifier (e.g., dataset:metric)".to_string(),
387 Rule::dataset => "dataset name".to_string(),
388
389 Rule::align => "\"align\" operation".to_string(),
391 Rule::group_by => "\"group by\" operation".to_string(),
392 Rule::bucket_by => "\"bucket by\" operation".to_string(),
393 Rule::map => "\"map\" operation".to_string(),
394 Rule::replace => "\"replace\" operation".to_string(),
395 Rule::join => "\"join\" operation".to_string(),
396
397 Rule::simple_query => "simple query".to_string(),
399 Rule::compute_query => "compute query".to_string(),
400
401 Rule::directive => "directive".to_string(),
403
404 Rule::param => "param".to_string(),
406 Rule::param_ident => "param identifier".to_string(),
407 Rule::param_type => "param type".to_string(),
408
409 Rule::func => "function".to_string(),
411 Rule::compute_fn => "compute function".to_string(),
412 Rule::bucket_by_fn => {
413 "bucket function (histogram, interpolate_delta_histogram)".to_string()
414 }
415 Rule::bucket_by_with_conversion_fn => {
416 "bucket function (interpolate_cumulative_histogram)".to_string()
417 }
418 Rule::bucket_conversion => "conversion method (rate, increase)".to_string(),
419 Rule::bucket_specs => "bucket specifications".to_string(),
420 Rule::bucket_fn_call | Rule::bucket_fn_call_simple => "bucket function call".to_string(),
421 Rule::bucket_fn_call_with_conversion => "bucket function call with conversion".to_string(),
422
423 Rule::filter_rule => "filter rule".to_string(),
425 Rule::filter_expr => "filter expression".to_string(),
426 Rule::sample_expr => "sample expression".to_string(),
427 Rule::value_filter => "value filter".to_string(),
428 Rule::regex_filter => "regex filter".to_string(),
429 Rule::kw_is => "\"is\" keyword".to_string(),
430 Rule::is_filter => "type filter (e.g., is string)".to_string(),
431 Rule::tag_type => "tag type (string, int, float, or bool)".to_string(),
432
433 Rule::tags => "tags (comma-separated field names)".to_string(),
435 Rule::tag => "tag name".to_string(),
436
437 _ => {
439 let name = format!("{rule:?}");
440 name.to_lowercase().replace('_', " ")
441 }
442 }
443}
444
445#[derive(Debug, Clone)]
447pub struct Suggestion(String);
448
449impl Suggestion {
450 #[must_use]
452 pub fn suggestion(&self) -> &str {
453 &self.0
454 }
455}
456
457impl fmt::Display for Suggestion {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 write!(f, "Did you mean \"{}\"?", self.0)
460 }
461}
462
463fn generate_suggestion(
465 line: &str,
466 error_pos: usize,
467 expected_rules: &[Rule],
468) -> Option<Suggestion> {
469 let actual_token = extract_token(line, error_pos)?;
470
471 if actual_token.len() < 2 {
472 return None;
473 }
474
475 let possible_keywords = rules_keywords(expected_rules);
476
477 let mut best_match: Option<(&str, f64)> = None;
478
479 for keyword in &possible_keywords {
480 let similarity = jaro(&actual_token.to_lowercase(), &keyword.to_lowercase());
481
482 if similarity > 0.8 {
483 if let Some((_, best_score)) = best_match {
484 if similarity > best_score {
485 best_match = Some((keyword, similarity));
486 }
487 } else {
488 best_match = Some((keyword, similarity));
489 }
490 }
491 }
492
493 best_match.map(|(keyword, _)| Suggestion(keyword.to_string()))
494}
495
496fn extract_token(line: &str, pos: usize) -> Option<String> {
498 let chars: Vec<char> = line.chars().collect();
499
500 if pos >= chars.len() {
501 return None;
502 }
503
504 let mut start = pos;
506 while start > 0 && chars[start - 1].is_alphanumeric() {
507 start -= 1;
508 }
509
510 let mut end = pos;
512 while end < chars.len() && chars[end].is_alphanumeric() {
513 end += 1;
514 }
515
516 if start < end {
517 Some(chars[start..end].iter().collect())
518 } else {
519 None
520 }
521}
522
523fn token_length(line: &str, pos: usize) -> usize {
525 let chars: Vec<char> = line.chars().collect();
526
527 if pos >= chars.len() {
528 return 0;
529 }
530
531 if !chars[pos].is_alphanumeric() {
532 return 1;
533 }
534
535 let mut end = pos;
536 while end < chars.len() && chars[end].is_alphanumeric() {
537 end += 1;
538 }
539
540 end - pos
541}
542
543fn rules_keywords(rules: &[Rule]) -> Vec<&'static str> {
545 let mut keywords = Vec::new();
546
547 for rule in rules {
548 match rule {
549 Rule::filter_keyword => {
550 keywords.push("where");
551 keywords.push("filter");
552 }
553 Rule::r#as => keywords.push("as"),
554 Rule::align => keywords.push("align"),
555 Rule::group_by => keywords.push("group"),
556 Rule::bucket_by => keywords.push("bucket"),
557 Rule::map => keywords.push("map"),
558 Rule::replace => keywords.push("replace"),
559 Rule::join => keywords.push("join"),
560 Rule::kw_is => keywords.push("is"),
561 Rule::tag_type => {
562 keywords.push("string");
563 keywords.push("int");
564 keywords.push("float");
565 keywords.push("bool");
566 }
567 _ => {}
568 }
569 }
570
571 keywords
572}