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