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