Skip to main content

mpl_lang/
lib.rs

1//! The `MPL` query language
2#![deny(
3    warnings,
4    clippy::pedantic,
5    clippy::unwrap_used,
6    clippy::large_futures,
7    missing_docs
8)]
9#![allow(clippy::missing_errors_doc)]
10#![allow(unused_assignments)] // We need this for the type error
11
12mod parser;
13pub use parser::{MPLParser, Rule};
14
15pub mod enc_regex;
16pub mod errors;
17pub mod linker;
18pub mod query;
19mod stdlib;
20pub mod tags;
21pub mod time;
22pub mod types;
23pub mod visitor;
24
25#[cfg(test)]
26mod tests;
27
28use std::{
29    collections::{HashMap, HashSet},
30    hash::BuildHasher,
31};
32
33pub use errors::ParseError;
34use miette::{Diagnostic, SourceOffset, SourceSpan};
35use pest::Parser as _;
36pub use query::Query;
37
38pub use stdlib::STDLIB;
39
40use crate::{
41    query::{Cmp, Expr, Filter, ParamDeclaration, ParamType, TagType, TerminalParamType, Warnings},
42    types::{Dataset, Parameterized},
43    visitor::{QueryVisitor, QueryWalker, VisitRes},
44};
45
46/// Compile error
47#[derive(Debug, thiserror::Error, Diagnostic)]
48pub enum CompileError {
49    /// Parse error
50    #[error(transparent)]
51    #[diagnostic(transparent)]
52    Parse(#[from] ParseError),
53    /// Typecheck error
54    #[error(transparent)]
55    #[diagnostic(transparent)]
56    Type(#[from] TypeError),
57    /// Groupcheck error
58    #[error(transparent)]
59    #[diagnostic(transparent)]
60    Group(#[from] GroupError),
61
62    /// Option error
63    #[error(transparent)]
64    #[diagnostic(transparent)]
65    Ifdef(#[from] IfdefError),
66}
67
68/// Parses and typechecks an MPL query into a Query object.
69#[allow(clippy::result_large_err)]
70pub fn compile<S: BuildHasher>(
71    query: &str,
72    system_params: HashMap<String, ParamType, S>,
73) -> Result<(Query, Warnings), CompileError> {
74    // stage 1: parse
75    let mut parse = MPLParser::parse(Rule::file, query).map_err(ParseError::from)?;
76    let (mut query, warnings) = parser::Parser::default().parse_query(&mut parse, system_params)?;
77    // stage 2: typecheck
78    let mut visitor = ParamTypecheckVisitor {};
79    visitor.walk(&mut query)?;
80    // stage 3: group check
81    let mut visitor = GroupCheckVisitor::default();
82    visitor.walk(&mut query)?;
83
84    let mut visitor = OptionCheckVisitor::default();
85    visitor.walk(&mut query)?;
86
87    Ok((query, warnings))
88}
89/// Type error
90#[derive(Debug, thiserror::Error, Diagnostic)]
91pub enum GroupError {
92    /// groups are not a subset of the previous groups
93    #[error("invalid groups: {next_groups:?} is not a subset of {prev_groups:?}")]
94    InvalidGroups {
95        /// the previous groups
96        next_groups: HashSet<String>,
97        /// the location of the next groups
98        next_span: Box<SourceSpan>,
99        /// the current groups
100        prev_groups: HashSet<String>,
101        /// the location of the previous groups
102        prev_span: Box<SourceSpan>,
103    },
104}
105
106#[derive(Default)]
107struct OptionCheckVisitor {
108    ifdef_param: Option<ParamDeclaration>,
109    seen_param: Option<ParamDeclaration>,
110}
111
112/// Ifdef error
113#[derive(Debug, thiserror::Error, Diagnostic)]
114pub enum IfdefError {
115    /// Usage of optional parameter outside of ifdef
116    #[error("{} is optional and used outside of ifdef", param.name)]
117    OptionalOutsideOfIfdef {
118        /// The source location
119        #[label("{}", param.name)]
120        span: SourceSpan,
121        /// The param declaration
122        param: ParamDeclaration,
123    },
124    /// Usage of optional parameter when it's not referenced
125    #[error("{} is used in a ifdef guard but not referenced inside of it", param.name)]
126    OptionalNotUsed {
127        /// The source location
128        #[label("{}", param.name)]
129        span: SourceSpan,
130        /// The param declaration
131        param: ParamDeclaration,
132    },
133}
134
135impl QueryVisitor for OptionCheckVisitor {
136    type Error = IfdefError;
137    fn visit_ifdef(
138        &mut self,
139        param: &mut ParamDeclaration,
140        _filter: &mut Filter,
141        _else_filter: &mut Option<Filter>,
142    ) -> Result<VisitRes, Self::Error> {
143        self.ifdef_param = Some(param.clone());
144        self.seen_param = None;
145        Ok(VisitRes::Walk)
146    }
147    fn leave_ifdef(
148        &mut self,
149        param: &mut ParamDeclaration,
150        _filter: &mut Filter,
151        _else_filter: &mut Option<Filter>,
152    ) -> Result<(), Self::Error> {
153        if self.ifdef_param != self.seen_param {
154            return Err(IfdefError::OptionalNotUsed {
155                span: param.span,
156                param: param.clone(),
157            });
158        }
159        self.ifdef_param = None;
160        Ok(())
161    }
162    fn visit_expr(&mut self, value: &mut Expr) -> Result<VisitRes, Self::Error> {
163        if let Expr::Param { span, param } = value
164            && param.is_optional()
165        {
166            self.seen_param = Some(param.clone());
167            if self.seen_param != self.ifdef_param {
168                return Err(IfdefError::OptionalOutsideOfIfdef {
169                    span: *span,
170                    param: param.clone(),
171                });
172            }
173        }
174        Ok(VisitRes::Walk)
175    }
176    fn visit_parameterized_regex(
177        &mut self,
178        regex: &mut Parameterized<enc_regex::EncodableRegex>,
179    ) -> Result<VisitRes, Self::Error> {
180        if let Parameterized::Param { span, param } = regex
181            && param.is_optional()
182        {
183            self.seen_param = Some(param.clone());
184            if self.seen_param != self.ifdef_param {
185                return Err(IfdefError::OptionalOutsideOfIfdef {
186                    span: *span,
187                    param: param.clone(),
188                });
189            }
190        }
191        Ok(VisitRes::Walk)
192    }
193}
194
195impl QueryWalker for OptionCheckVisitor {}
196
197struct GroupCheckVisitor {
198    groups: Option<HashSet<String>>,
199    span: SourceSpan,
200    stack: Vec<(SourceSpan, Option<HashSet<String>>)>,
201}
202
203impl Default for GroupCheckVisitor {
204    fn default() -> Self {
205        Self {
206            groups: None,
207            span: SourceSpan::new(SourceOffset::from_location("", 0, 0), 0),
208            stack: Vec::new(),
209        }
210    }
211}
212impl GroupCheckVisitor {
213    fn check_group_by(
214        &mut self,
215        tags: &[String],
216        span: SourceSpan,
217    ) -> Result<VisitRes, GroupError> {
218        let next_groups: HashSet<String> = tags.iter().cloned().collect();
219        let Some(prev_groups) = self.groups.take() else {
220            self.groups = Some(next_groups);
221            self.span = span;
222            return Ok(VisitRes::Walk);
223        };
224        if !next_groups.is_subset(&prev_groups) {
225            return Err(GroupError::InvalidGroups {
226                next_groups,
227                next_span: Box::new(span),
228                prev_groups,
229                prev_span: Box::new(self.span),
230            });
231        }
232        self.groups = Some(next_groups);
233        self.span = span;
234        Ok(VisitRes::Walk)
235    }
236}
237
238impl QueryVisitor for GroupCheckVisitor {
239    type Error = GroupError;
240    fn visit(&mut self, _: &mut Query) -> Result<VisitRes, Self::Error> {
241        self.stack.push((self.span, self.groups.take()));
242        Ok(VisitRes::Walk)
243    }
244    fn leave(&mut self, _: &mut Query) -> Result<(), Self::Error> {
245        let Some((span, groups)) = self.stack.pop() else {
246            return Ok(());
247        };
248        self.span = span;
249        self.groups = groups;
250        Ok(())
251    }
252    fn visit_group_by(&mut self, group_by: &mut query::GroupBy) -> Result<VisitRes, Self::Error> {
253        self.check_group_by(&group_by.tags, group_by.span)
254    }
255    fn visit_bucket_by(
256        &mut self,
257        bucket_by: &mut query::BucketBy,
258    ) -> Result<VisitRes, Self::Error> {
259        self.check_group_by(&bucket_by.tags, bucket_by.span)
260    }
261}
262impl QueryWalker for GroupCheckVisitor {}
263
264/// Type error
265#[derive(Debug, thiserror::Error, Diagnostic)]
266pub enum TypeError {
267    /// Type mismatch
268    #[error(
269        "The param ${param_name} has type {actual}, but was used in context that expects one of: {}",
270        expected.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
271    )]
272    #[diagnostic(code(mpl_lang::typemismatch))]
273    #[allow(unused_assignments)]
274    TypeMismatch {
275        /// The location of the param used
276        #[label("param")]
277        use_span: SourceSpan,
278        /// The location where the param was declared
279        #[label("param declaration")]
280        declaration_span: SourceSpan,
281        /// The param name
282        param_name: String,
283        /// The expected type(s)
284        expected: Vec<TerminalParamType>,
285        /// The actual type
286        actual: TerminalParamType,
287    },
288}
289
290struct ParamTypecheckVisitor {}
291
292impl ParamTypecheckVisitor {
293    fn assert_param_type(
294        param: &ParamDeclaration,
295        use_span: SourceSpan,
296        expected: &[TerminalParamType],
297    ) -> Result<(), TypeError> {
298        if !expected.contains(&param.typ()) {
299            return Err(TypeError::TypeMismatch {
300                use_span,
301                declaration_span: param.span,
302                param_name: param.name.clone(),
303                expected: expected.to_vec(),
304                actual: param.typ(),
305            });
306        }
307
308        Ok(())
309    }
310
311    fn assert_param<T>(
312        value: &Parameterized<T>,
313        expected: &[TerminalParamType],
314    ) -> Result<(), TypeError> {
315        let Parameterized::Param { span, param } = value else {
316            return Ok(());
317        };
318        Self::assert_param_type(param, *span, expected)
319    }
320}
321
322impl QueryVisitor for ParamTypecheckVisitor {
323    type Error = TypeError;
324
325    fn visit_dataset(
326        &mut self,
327        dataset: &mut Parameterized<Dataset>,
328    ) -> Result<VisitRes, Self::Error> {
329        Self::assert_param(dataset, &[TerminalParamType::Dataset]).map(|()| VisitRes::Walk)
330    }
331
332    fn visit_align(&mut self, align: &mut query::Align) -> Result<VisitRes, Self::Error> {
333        if let Some(time) = &align.time {
334            Self::assert_param(time, &[TerminalParamType::Duration]).map(|()| VisitRes::Walk)
335        } else {
336            Ok(VisitRes::Walk)
337        }
338    }
339
340    fn visit_bucket_by(
341        &mut self,
342        bucket_by: &mut query::BucketBy,
343    ) -> Result<VisitRes, Self::Error> {
344        if let Some(time) = &bucket_by.time {
345            Self::assert_param(time, &[TerminalParamType::Duration]).map(|()| VisitRes::Walk)
346        } else {
347            Ok(VisitRes::Walk)
348        }
349    }
350
351    fn visit_cmp(&mut self, _field: &mut String, cmp: &mut Cmp) -> Result<VisitRes, Self::Error> {
352        const TAG_VALUE_PARAM_TYPES: [TerminalParamType; 4] = [
353            TerminalParamType::Tag(TagType::String),
354            TerminalParamType::Tag(TagType::Int),
355            TerminalParamType::Tag(TagType::Float),
356            TerminalParamType::Tag(TagType::Bool),
357        ];
358
359        match cmp {
360            Cmp::Is(_)
361            | Cmp::In(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_))
362            | Cmp::Eq(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_))
363            | Cmp::Ne(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_))
364            | Cmp::Gt(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_))
365            | Cmp::Ge(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_))
366            | Cmp::Lt(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_))
367            | Cmp::Le(Expr::Const(_) | Expr::String(_) | Expr::Tag(_) | Expr::Array(_)) => {
368                Ok(VisitRes::Walk)
369            }
370            Cmp::In(Expr::Param { span, param }) => {
371                Self::assert_param_type(param, *span, &[TerminalParamType::Tag(TagType::Array)])
372                    .map(|()| VisitRes::Walk)
373            }
374            Cmp::Eq(Expr::Param { span, param }) => {
375                if param.typ() == TerminalParamType::Regex {
376                    // we have a regex param in an eq
377                    // this happens because we cannot detect this in pest
378                    //
379                    // this is | filter foo == #/bar/ vs | filter foo == $bar_re
380                    *cmp = Cmp::RegEx(Parameterized::Param {
381                        span: *span,
382                        param: param.clone(),
383                    });
384                    return Ok(VisitRes::Walk);
385                }
386
387                Self::assert_param_type(param, *span, &TAG_VALUE_PARAM_TYPES)
388                    .map(|()| VisitRes::Walk)
389            }
390            Cmp::Ne(Expr::Param { span, param }) => {
391                if param.typ() == TerminalParamType::Regex {
392                    // we have a regex param in ne
393                    // this happens because we cannot detect this in pest
394                    //
395                    // this is | filter foo != #/bar/ vs | filter foo != $bar_re
396                    *cmp = Cmp::RegExNot(Parameterized::Param {
397                        span: *span,
398                        param: param.clone(),
399                    });
400                    return Ok(VisitRes::Walk);
401                }
402
403                Self::assert_param_type(param, *span, &TAG_VALUE_PARAM_TYPES)
404                    .map(|()| VisitRes::Walk)
405            }
406            Cmp::Gt(Expr::Param { span, param })
407            | Cmp::Ge(Expr::Param { span, param })
408            | Cmp::Lt(Expr::Param { span, param })
409            | Cmp::Le(Expr::Param { span, param }) => {
410                Self::assert_param_type(param, *span, &TAG_VALUE_PARAM_TYPES)
411                    .map(|()| VisitRes::Walk)
412            }
413
414            Cmp::RegEx(value) | Cmp::RegExNot(value) => {
415                Self::assert_param(value, &[TerminalParamType::Regex]).map(|()| VisitRes::Walk)
416            }
417        }
418    }
419}
420
421impl QueryWalker for ParamTypecheckVisitor {}
422
423#[cfg(feature = "examples")]
424pub mod examples {
425    //! Examples used in tests and documentation
426
427    macro_rules! example {
428        ($name:expr) => {
429            (
430                concat!($name),
431                include_str!(concat!("../tests/examples/", $name, ".mpl")),
432            )
433        };
434    }
435
436    /// Language specification
437    pub const SPEC: &str = include_str!("../spec.md");
438
439    /// MPL examples used in tests and documentation
440    pub const MPL: [(&str, &str); 22] = [
441        example!("align-rate"),
442        example!("as"),
443        example!("enrich"),
444        example!("extend"),
445        example!("filtered-histogram"),
446        example!("group-by"),
447        example!("histogram"),
448        example!("histogram_rate"),
449        example!("ifdef"),
450        example!("ifdef-else"),
451        example!("inf"),
452        example!("map-gt"),
453        example!("map-mul"),
454        example!("nested-enrich"),
455        example!("parser-error"),
456        example!("rate"),
457        example!("replace_labels"),
458        example!("set"),
459        example!("slo"),
460        example!("slo-histogram"),
461        example!("slo-ingest-rate"),
462        example!("sum_rate"),
463    ];
464}