1#![allow(unused_assignments)] use std::fmt::{self, Write as _};
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, query::ParamDeclaration};
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("{label}")]
24 span: SourceSpan,
25 label: String,
27 message: String,
29 #[help]
31 suggestion: Option<Suggestion>,
32 },
33
34 #[error("This feature is not supported at the moment: {rule:?}")]
35 #[diagnostic(
37 code(mpl_lang::not_supported),
38 help("This feature may be added in a future version")
39 )]
40 NotSupported {
41 #[label("unsupported: {rule:?}")]
43 span: SourceSpan,
44 rule: Rule,
46 },
47
48 #[error("Unexpected rule: {rule:?} expected one of {expected:?}")]
50 #[diagnostic(code(mpl_lang::unexpected_rule))]
51 Unexpected {
52 #[label("unexpected {rule:?}")]
54 span: SourceSpan,
55 rule: Rule,
57 expected: Vec<Rule>,
59 },
60
61 #[error("Found unexpected tokens: {rules:?}")]
63 #[diagnostic(code(mpl_lang::unexpected_tokens))]
64 UnexpectedTokens {
65 #[label("unexpected tokens")]
67 span: SourceSpan,
68 rules: Vec<Rule>,
70 },
71
72 #[error("Unexpected end of input")]
74 #[diagnostic(
75 code(mpl_lang::unexpected_eof),
76 help("The query appears to be incomplete")
77 )]
78 EOF {
79 #[label("expected more input here")]
81 span: SourceSpan,
82 },
83
84 #[error("Invalid float: {0}")]
86 #[diagnostic(code(mpl_lang::invalid_float))]
87 InvalidFloat(#[from] std::num::ParseFloatError),
88
89 #[error("Invalid integer: {0}")]
91 #[diagnostic(code(mpl_lang::invalid_integer))]
92 InvalidInteger(#[from] std::num::ParseIntError),
93
94 #[error("Invalid bool: {0}")]
96 #[diagnostic(code(mpl_lang::invalid_bool))]
97 InvalidBool(#[from] std::str::ParseBoolError),
98
99 #[error("Invalid date: {0}")]
101 #[diagnostic(code(mpl_lang::invalid_date))]
102 InvalidDate(#[from] chrono::ParseError),
103
104 #[error("Invalid Regex: {0}")]
106 #[diagnostic(code(mpl_lang::invalid_regex))]
107 InvalidRegex(#[from] regex::Error),
108
109 #[error("Unsupported align function: {name}")]
111 #[diagnostic(
112 code(mpl_lang::unsupported_align_function),
113 help("Check the documentation for available align functions")
114 )]
115 UnsupportedAlignFunction {
116 #[label("unknown function")]
118 span: SourceSpan,
119 name: String,
121 },
122
123 #[error("Unsupported group function: {name}")]
125 #[diagnostic(
126 code(mpl_lang::unsupported_group_function),
127 help("Check the documentation for available group functions")
128 )]
129 UnsupportedGroupFunction {
130 #[label("unknown function")]
132 span: SourceSpan,
133 name: String,
135 },
136
137 #[error("Unsupported compute function: {name}")]
139 #[diagnostic(
140 code(mpl_lang::unsupported_compute_function),
141 help("Check the documentation for available compute functions")
142 )]
143 UnsupportedComputeFunction {
144 #[label("unknown function")]
146 span: SourceSpan,
147 name: String,
149 },
150
151 #[error("Unsupported bucket function: {name}")]
153 #[diagnostic(
154 code(mpl_lang::unsupported_bucket_function),
155 help(
156 "Available functions: histogram, interpolate_delta_histogram, interpolate_cumulative_histogram"
157 )
158 )]
159 UnsupportedBucketFunction {
160 #[label("unknown function")]
162 span: SourceSpan,
163 name: String,
165 },
166
167 #[error("Unsupported map evaluation: {name}")]
169 #[diagnostic(
170 code(mpl_lang::unsupported_map_evaluation),
171 help("Check the documentation for available map operations")
172 )]
173 UnsupportedMapEvaluation {
174 #[label("unknown operation")]
176 span: SourceSpan,
177 name: String,
179 },
180
181 #[error("Unsupported map function: {name}")]
183 #[diagnostic(
184 code(mpl_lang::unsupported_map_function),
185 help("Check the documentation for available map functions")
186 )]
187 UnsupportedMapFunction {
188 #[label("unknown function")]
190 span: SourceSpan,
191 name: String,
193 },
194
195 #[error("Unsupported regexp comparison: {op}")]
197 #[diagnostic(
198 code(mpl_lang::unsupported_regexp_comparison),
199 help("Use '==' or '!=' for regex comparisons")
200 )]
201 UnsupportedRegexpComparison {
202 #[label("invalid operator")]
204 span: SourceSpan,
205 op: String,
207 },
208
209 #[error("Unsupported tag comparison: {op}")]
211 #[diagnostic(
212 code(mpl_lang::unsupported_tag_comparison),
213 help("Supported operators: ==, !=, >, >=, <, <=, in")
214 )]
215 UnsupportedTagComparison {
216 #[label("invalid operator")]
218 span: SourceSpan,
219 op: String,
221 },
222
223 #[error("`in` requires an array on the right-hand side")]
225 #[diagnostic(
226 code(mpl_lang::in_requires_array),
227 help("Use an array literal (e.g. `in [200, 201]`) or an array-typed param")
228 )]
229 InRequiresArray {
230 #[label("expected an array here")]
232 span: SourceSpan,
233 },
234
235 #[error("Not implemented: {0}")]
237 #[diagnostic(
238 code(mpl_lang::not_implemented),
239 help("This feature is planned but not yet implemented")
240 )]
241 NotImplemented(&'static str),
242
243 #[error("String construction error: {0}")]
245 #[diagnostic(code(mpl_lang::strumbra_error))]
246 StrumbraError(#[from] strumbra::Error),
247
248 #[error("Unreachable error: {0}")]
250 #[diagnostic(
251 code(mpl_lang::unreachable),
252 help("This error should never be reached")
253 )]
254 Unreachable(&'static str),
255
256 #[error("The param ${param} is defined multiple times")]
258 #[diagnostic(
259 code(mpl_lang::param_defined_multiple_times),
260 help("This param has been defined more than once")
261 )]
262 ParamDefinedMultipleTimes {
263 #[label("duplicate definition")]
265 span: SourceSpan,
266 param: String,
268 },
269
270 #[error("The system param ${param} is missing the system prefix")]
286 #[diagnostic(
287 code(mpl_lan::system_param_missing_prefix),
288 help("The system param is missing the `__` prefix")
289 )]
290 SystemParamMissingPrefix {
291 param: String,
293 },
294
295 #[error("The param ${param} is not defined")]
297 #[diagnostic(code(mpl_lang::undefined_param))]
298 UndefinedParam {
299 #[label("undefined param")]
301 span: SourceSpan,
302 param: String,
304 },
305 #[error("The type {tpe} is not a valid type for tags")]
307 #[diagnostic(code(mpl_lang::invalid_tag_type))]
308 InvalidTagType {
309 #[label("invalid type")]
311 span: miette::SourceSpan,
312 tpe: String,
314 },
315 #[error("The parameter {} is not declared as optional", param.name)]
317 #[diagnostic(code(mpl_lang::ifdef_not_optional))]
318 IfdefNotOptional {
319 #[label("param declaration")]
321 span: miette::SourceSpan,
322 param: ParamDeclaration,
324 },
325}
326
327impl From<PestError<Rule>> for ParseError {
328 fn from(err: PestError<Rule>) -> Self {
329 let (start, mut len) = match err.location {
330 InputLocation::Pos(pos) => (pos, 0),
331 InputLocation::Span((start, end)) => (start, end - start),
332 };
333
334 let (label, message, suggestion) = match &err.variant {
335 ErrorVariant::ParsingError {
336 positives,
337 negatives,
338 } => {
339 let mut keywords = Vec::new();
340 let mut operations = Vec::new();
341 let mut other = Vec::new();
342
343 for rule in positives {
344 let name = friendly_rule(*rule);
345 if name.contains("keyword") {
346 keywords.push(name);
347 } else if name.contains("operation") {
348 operations.push(name);
349 } else {
350 other.push(name);
351 }
352 }
353
354 let mut label = String::new();
355 if keywords.is_empty() && operations.is_empty() && other.is_empty() {
356 label.push_str("unexpected token");
357 } else {
358 label.push_str("expected one of:\n");
359 if !keywords.is_empty() {
360 let kws: Vec<_> = keywords
361 .iter()
362 .map(|k| k.trim_end_matches(" keyword"))
363 .collect();
364 let _ = writeln!(label, " keywords: {}", join_with_or(&kws));
365 }
366 if !operations.is_empty() {
367 let ops: Vec<_> = operations
368 .iter()
369 .map(|o| {
370 o.trim_start_matches("a ")
371 .trim_start_matches("an ")
372 .trim_end_matches(" operation")
373 })
374 .collect();
375 let _ = writeln!(label, " operations: {}", join_with_or(&ops));
376 }
377 if !other.is_empty() {
378 for name in &other {
379 let _ = writeln!(label, " - {name}");
380 }
381 }
382 }
383
384 let mut msg = "unexpected token or operation".to_string();
385 if !negatives.is_empty() {
386 if !msg.is_empty() {
387 msg.push_str(" ");
388 }
389 msg.push_str("but found ");
390 msg.push_str(&friendly_rules(negatives));
391 }
392
393 let line_pos = match &err.line_col {
394 LineColLocation::Pos((_, col)) | LineColLocation::Span((_, col), _) => {
395 col.saturating_sub(1)
396 }
397 };
398 let suggestion = generate_suggestion(err.line(), line_pos, positives);
399
400 if len == 0 {
402 len = token_length(err.line(), line_pos);
403 }
404
405 let label = label.trim_end().to_string();
406 (label, msg, suggestion)
407 }
408 ErrorVariant::CustomError { message } => (message.clone(), message.clone(), None),
409 };
410
411 ParseError::SyntaxError {
412 span: SourceSpan::new(start.into(), len),
413 label,
414 message,
415 suggestion,
416 }
417 }
418}
419
420fn join_with_or(items: &[&str]) -> String {
422 match items.len() {
423 0 => String::new(),
424 1 => items[0].to_string(),
425 2 => format!("{} or {}", items[0], items[1]),
426 _ => {
427 let last = items[items.len() - 1];
428 let rest = &items[..items.len() - 1];
429 format!("{}, or {last}", rest.join(", "))
430 }
431 }
432}
433
434pub(crate) fn pair_to_source_span(pair: &Pair<Rule>) -> SourceSpan {
436 let span = pair.as_span();
437 let start = span.start();
438 let len = span.end() - start;
439 SourceSpan::new(start.into(), len)
440}
441
442fn friendly_rules(rules: &[Rule]) -> String {
444 let names: Vec<_> = rules.iter().copied().map(friendly_rule).collect();
445
446 match names.len() {
447 0 => String::new(),
448 1 => names[0].clone(),
449 2 => format!("{} or {}", names[0], names[1]),
450 _ => {
451 let last = &names[names.len() - 1];
452 let rest = &names[..names.len() - 1];
453 format!("{}, or {last}", rest.join(", "))
454 }
455 }
456}
457
458fn friendly_rule(rule: Rule) -> String {
460 match rule {
461 Rule::EOI => "end of query".to_string(),
463 Rule::pipe_keyword => "`|` (pipe)".to_string(),
464
465 Rule::time_range => "time range (e.g., [1h..])".to_string(),
467 Rule::time_relative => "relative time (e.g., 5m, 1h, 7d)".to_string(),
468 Rule::time_timestamp => "timestamp".to_string(),
469 Rule::time_rfc_3339 => "RFC3339 timestamp".to_string(),
470 Rule::time_modifier => "time modifier".to_string(),
471
472 Rule::filter_keyword | Rule::kw_filter => "`filter` keyword".to_string(),
474 Rule::kw_where => "`where` keyword".to_string(),
475 Rule::r#as => "`as` keyword".to_string(),
476
477 Rule::cmp => "a comparison operator (==, !=, <, >, <=, >=)".to_string(),
479 Rule::cmp_re => "a regex operator (==, !=)".to_string(),
480 Rule::regex => "a regex pattern (e.g., #/pattern/)".to_string(),
481
482 Rule::r#const => "value (string, number, bool or array)".to_string(),
484 Rule::string => "string value".to_string(),
485 Rule::number => "number".to_string(),
486 Rule::bool => "bool (true or false)".to_string(),
487
488 Rule::plain_ident => "identifier".to_string(),
490 Rule::escaped_ident => "escaped identifier".to_string(),
491 Rule::source => "source metric".to_string(),
492 Rule::metric_name => "metric name".to_string(),
493 Rule::metric_id => "metric identifier (e.g., dataset:metric)".to_string(),
494 Rule::dataset => "dataset name".to_string(),
495
496 Rule::align => "an align operation".to_string(),
498 Rule::group_by => "a group by operation".to_string(),
499 Rule::bucket_by => "a bucket by operation".to_string(),
500 Rule::map => "a map operation".to_string(),
501 Rule::replace => "a replace operation".to_string(),
502 Rule::join => "a join operation".to_string(),
503 Rule::kw_align => "align keyword".to_string(),
504 Rule::kw_using => "using keyword".to_string(),
505 Rule::kw_over => "over keyword".to_string(),
506 Rule::kw_to => "to keyword".to_string(),
507
508 Rule::simple_query => "simple query".to_string(),
510 Rule::compute_query => "compute query".to_string(),
511
512 Rule::directive => "directive".to_string(),
514
515 Rule::param => "param".to_string(),
517 Rule::param_ident => "param identifier".to_string(),
518 Rule::param_type => {
519 "param type (Duration, Dataset, Regex, string, int, float, bool)".to_string()
520 }
521
522 Rule::func => "function".to_string(),
524 Rule::compute_fn => "compute function".to_string(),
525 Rule::bucket_by_fn => {
526 "bucket function (histogram, interpolate_delta_histogram)".to_string()
527 }
528 Rule::bucket_by_with_conversion_fn => {
529 "bucket function (interpolate_cumulative_histogram)".to_string()
530 }
531 Rule::bucket_conversion => "conversion method (rate, increase)".to_string(),
532 Rule::bucket_specs => "bucket specifications".to_string(),
533 Rule::bucket_fn_call | Rule::bucket_fn_call_simple => "bucket function call".to_string(),
534 Rule::bucket_fn_call_with_conversion => "bucket function call with conversion".to_string(),
535
536 Rule::filter_rule => "filter rule".to_string(),
538 Rule::filter_expr => "filter expression".to_string(),
539 Rule::sample_expr => "sample expression".to_string(),
540 Rule::value_filter => "value filter".to_string(),
541 Rule::regex_filter => "regex filter".to_string(),
542 Rule::kw_is => "`is` keyword".to_string(),
543 Rule::is_filter => "type filter (e.g., is string)".to_string(),
544 Rule::tag_type => "tag type (array, string, int, float, or bool)".to_string(),
545
546 Rule::tags => "tags (comma-separated field names)".to_string(),
548 Rule::tag => "tag name".to_string(),
549
550 _ => {
552 let name = format!("{rule:?}");
553 name.to_lowercase().replace('_', " ")
554 }
555 }
556}
557
558#[derive(Debug, Clone)]
560pub struct Suggestion(String);
561
562impl Suggestion {
563 #[must_use]
565 pub fn suggestion(&self) -> &str {
566 &self.0
567 }
568}
569
570impl fmt::Display for Suggestion {
571 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572 write!(f, "Did you mean \"{}\"?", self.0)
573 }
574}
575
576fn generate_suggestion(
578 line: &str,
579 error_pos: usize,
580 expected_rules: &[Rule],
581) -> Option<Suggestion> {
582 let actual_token = extract_token(line, error_pos)?;
583
584 if actual_token.len() < 2 {
585 return None;
586 }
587
588 let possible_keywords = rules_keywords(expected_rules);
589
590 let mut best_match: Option<(&str, f64)> = None;
591
592 for keyword in &possible_keywords {
593 let similarity = jaro(&actual_token.to_lowercase(), &keyword.to_lowercase());
594
595 if similarity > 0.8 {
596 if let Some((_, best_score)) = best_match {
597 if similarity > best_score {
598 best_match = Some((keyword, similarity));
599 }
600 } else {
601 best_match = Some((keyword, similarity));
602 }
603 }
604 }
605
606 best_match.map(|(keyword, _)| Suggestion(keyword.to_string()))
607}
608
609fn extract_token(line: &str, pos: usize) -> Option<String> {
611 let chars: Vec<char> = line.chars().collect();
612
613 if pos >= chars.len() {
614 return None;
615 }
616
617 let mut pos = pos;
619 while pos < chars.len() && chars[pos].is_whitespace() {
620 pos += 1;
621 }
622
623 if pos >= chars.len() {
624 return None;
625 }
626
627 let mut start = pos;
629 while start > 0 && chars[start - 1].is_alphanumeric() {
630 start -= 1;
631 }
632
633 let mut end = pos;
635 while end < chars.len() && chars[end].is_alphanumeric() {
636 end += 1;
637 }
638
639 if start < end {
640 Some(chars[start..end].iter().collect())
641 } else {
642 None
643 }
644}
645
646fn token_length(line: &str, pos: usize) -> usize {
648 let chars: Vec<char> = line.chars().collect();
649
650 if pos >= chars.len() {
651 return 0;
652 }
653
654 if !chars[pos].is_alphanumeric() {
655 return 1;
656 }
657
658 let mut end = pos;
659 while end < chars.len() && chars[end].is_alphanumeric() {
660 end += 1;
661 }
662
663 end - pos
664}
665
666fn rules_keywords(rules: &[Rule]) -> Vec<&'static str> {
668 let mut keywords = Vec::new();
669
670 for rule in rules {
671 match rule {
672 Rule::filter_keyword | Rule::kw_filter | Rule::kw_where => {
673 keywords.push("where");
674 keywords.push("filter");
675 }
676 Rule::r#as => keywords.push("as"),
677 Rule::align => keywords.push("align"),
678 Rule::group_by => keywords.push("group"),
679 Rule::bucket_by => keywords.push("bucket"),
680 Rule::map => keywords.push("map"),
681 Rule::replace => keywords.push("replace"),
682 Rule::join => keywords.push("join"),
683 Rule::kw_is => keywords.push("is"),
684 Rule::tag_type => {
685 keywords.push("array");
686 keywords.push("string");
687 keywords.push("int");
688 keywords.push("float");
689 keywords.push("bool");
690 }
691 Rule::kw_else => keywords.push("else"),
692 Rule::kw_to => keywords.push("to"),
693 Rule::kw_using => keywords.push("using"),
694 Rule::kw_over => keywords.push("over"),
695
696 _ => {}
697 }
698 }
699
700 keywords
701}