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, ParamType, TagType},
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
63/// Parses and typechecks an MPL query into a Query object.
64#[allow(clippy::result_large_err)]
65pub fn compile(query: &str) -> Result<Query, CompileError> {
66    // stage 1: parse
67    let mut parse = MPLParser::parse(Rule::file, query).map_err(ParseError::from)?;
68    let mut query = parser::Parser::default().parse_query(&mut parse)?;
69    // stage 2: typecheck
70    let mut visitor = ParamTypecheckVisitor {};
71    visitor.walk(&mut query)?;
72    // stage 3: group check
73    let mut visitor = GroupCheckVisitor::default();
74    visitor.walk(&mut query)?;
75
76    Ok(query)
77}
78/// Type error
79#[derive(Debug, thiserror::Error, Diagnostic)]
80pub enum GroupError {
81    /// groups are not a subset of the previous groups
82    #[error("invalid groups: {next_groups:?} is not a subset of {prev_groups:?}")]
83    InvalidGroups {
84        /// the previous groups
85        next_groups: HashSet<String>,
86        /// the location of the next groups
87        next_span: Box<SourceSpan>,
88        /// the current groups
89        prev_groups: HashSet<String>,
90        /// the location of the previous groups
91        prev_span: Box<SourceSpan>,
92    },
93}
94struct GroupCheckVisitor {
95    groups: Option<HashSet<String>>,
96    span: SourceSpan,
97    stack: Vec<(SourceSpan, Option<HashSet<String>>)>,
98}
99
100impl Default for GroupCheckVisitor {
101    fn default() -> Self {
102        Self {
103            groups: None,
104            span: SourceSpan::new(SourceOffset::from_location("", 0, 0), 0),
105            stack: Vec::new(),
106        }
107    }
108}
109impl GroupCheckVisitor {
110    fn check_group_by(
111        &mut self,
112        tags: &[String],
113        span: SourceSpan,
114    ) -> Result<VisitRes, GroupError> {
115        let next_groups: HashSet<String> = tags.iter().cloned().collect();
116        let Some(prev_groups) = self.groups.take() else {
117            self.groups = Some(next_groups);
118            self.span = span;
119            return Ok(VisitRes::Walk);
120        };
121        if !next_groups.is_subset(&prev_groups) {
122            return Err(GroupError::InvalidGroups {
123                next_groups,
124                next_span: Box::new(span),
125                prev_groups,
126                prev_span: Box::new(self.span),
127            });
128        }
129        self.groups = Some(next_groups);
130        self.span = span;
131        Ok(VisitRes::Walk)
132    }
133}
134
135impl QueryVisitor for GroupCheckVisitor {
136    type Error = GroupError;
137    fn visit(&mut self, _: &mut Query) -> Result<VisitRes, Self::Error> {
138        self.stack.push((self.span, self.groups.take()));
139        Ok(VisitRes::Walk)
140    }
141    fn leave(&mut self, _: &mut Query) -> Result<(), Self::Error> {
142        let Some((span, groups)) = self.stack.pop() else {
143            return Ok(());
144        };
145        self.span = span;
146        self.groups = groups;
147        Ok(())
148    }
149    fn visit_group_by(&mut self, group_by: &mut query::GroupBy) -> Result<VisitRes, Self::Error> {
150        self.check_group_by(&group_by.tags, group_by.span)
151    }
152    fn visit_bucket_by(
153        &mut self,
154        bucket_by: &mut query::BucketBy,
155    ) -> Result<VisitRes, Self::Error> {
156        self.check_group_by(&bucket_by.tags, bucket_by.span)
157    }
158}
159impl QueryWalker for GroupCheckVisitor {}
160
161/// Type error
162#[derive(Debug, thiserror::Error, Diagnostic)]
163pub enum TypeError {
164    /// Type mismatch
165    #[error(
166        "The param ${param_name} has type {actual}, but was used in context that expects one of: {}",
167        expected.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
168    )]
169    #[diagnostic(code(mpl_lang::typemismatch))]
170    #[allow(unused_assignments)]
171    TypeMismatch {
172        /// The location of the param used
173        #[label("param")]
174        use_span: SourceSpan,
175        /// The location where the param was declared
176        #[label("param declaration")]
177        declaration_span: SourceSpan,
178        /// The param name
179        param_name: String,
180        /// The expected type(s)
181        expected: Vec<ParamType>,
182        /// The actual type
183        actual: ParamType,
184    },
185}
186
187struct ParamTypecheckVisitor {}
188
189impl ParamTypecheckVisitor {
190    fn assert_param_type<T>(
191        value: &Parameterized<T>,
192        expected: Vec<ParamType>,
193    ) -> Result<(), TypeError> {
194        if let Parameterized::Param { span, param } = value
195            && !expected.contains(&param.typ)
196        {
197            return Err(TypeError::TypeMismatch {
198                use_span: *span,
199                declaration_span: param.span,
200                param_name: param.name.clone(),
201                expected,
202                actual: param.typ,
203            });
204        }
205
206        Ok(())
207    }
208}
209
210impl QueryVisitor for ParamTypecheckVisitor {
211    type Error = TypeError;
212
213    fn visit_dataset(
214        &mut self,
215        dataset: &mut Parameterized<Dataset>,
216    ) -> Result<VisitRes, Self::Error> {
217        Self::assert_param_type(dataset, vec![ParamType::Dataset]).map(|()| VisitRes::Walk)
218    }
219
220    fn visit_align(&mut self, align: &mut query::Align) -> Result<VisitRes, Self::Error> {
221        Self::assert_param_type(&align.time, vec![ParamType::Duration]).map(|()| VisitRes::Walk)
222    }
223
224    fn visit_bucket_by(
225        &mut self,
226        bucket_by: &mut query::BucketBy,
227    ) -> Result<VisitRes, Self::Error> {
228        Self::assert_param_type(&bucket_by.time, vec![ParamType::Duration]).map(|()| VisitRes::Walk)
229    }
230
231    fn visit_cmp(&mut self, _field: &mut String, cmp: &mut Cmp) -> Result<VisitRes, Self::Error> {
232        let tag_value_param_types = vec![
233            ParamType::Tag(TagType::String),
234            ParamType::Tag(TagType::Int),
235            ParamType::Tag(TagType::Float),
236            ParamType::Tag(TagType::Bool),
237        ];
238
239        match cmp {
240            Cmp::Is(_) => Ok(VisitRes::Walk),
241            Cmp::Eq(value) => {
242                if let Parameterized::Param { span, param } = value
243                    && param.typ == ParamType::Regex
244                {
245                    // we have a regex param in an eq
246                    // this happens because we cannot detect this in pest
247                    //
248                    // this is | filter foo == #/bar/ vs | filter foo == $bar_re
249                    *cmp = Cmp::RegEx(Parameterized::Param {
250                        span: *span,
251                        param: param.clone(),
252                    });
253                    return Ok(VisitRes::Walk);
254                }
255
256                Self::assert_param_type(value, tag_value_param_types).map(|()| VisitRes::Walk)
257            }
258            Cmp::Ne(value) => {
259                if let Parameterized::Param { span, param } = value
260                    && param.typ == ParamType::Regex
261                {
262                    // we have a regex param in ne
263                    // this happens because we cannot detect this in pest
264                    //
265                    // this is | filter foo != #/bar/ vs | filter foo != $bar_re
266                    *cmp = Cmp::RegExNot(Parameterized::Param {
267                        span: *span,
268                        param: param.clone(),
269                    });
270                    return Ok(VisitRes::Walk);
271                }
272
273                Self::assert_param_type(value, tag_value_param_types).map(|()| VisitRes::Walk)
274            }
275            Cmp::Gt(value) | Cmp::Ge(value) | Cmp::Lt(value) | Cmp::Le(value) => {
276                Self::assert_param_type(value, tag_value_param_types).map(|()| VisitRes::Walk)
277            }
278            Cmp::RegEx(value) | Cmp::RegExNot(value) => {
279                Self::assert_param_type(value, vec![ParamType::Regex]).map(|()| VisitRes::Walk)
280            }
281        }
282    }
283}
284
285impl QueryWalker for ParamTypecheckVisitor {}
286
287#[cfg(feature = "examples")]
288pub mod examples {
289    //! Examples used in tests and documentation
290    macro_rules! example {
291        ($name:expr) => {
292            (
293                concat!($name),
294                include_str!(concat!("../tests/examples/", $name, ".mpl")),
295            )
296        };
297    }
298
299    /// Language specification
300    pub const SPEC: &str = include_str!("../spec.md");
301
302    /// MPL examples used in tests and documentation
303    pub const MPL: [(&str, &str); 17] = [
304        example!("align-rate"),
305        example!("as"),
306        example!("enrich"),
307        example!("filtered-histogram"),
308        example!("histogram_rate"),
309        example!("histogram"),
310        example!("map-gt"),
311        example!("map-mul"),
312        example!("nested-enrich"),
313        example!("parser-error"),
314        example!("rate"),
315        example!("replace_labels"),
316        example!("set"),
317        example!("slo-histogram"),
318        example!("slo-ingest-rate"),
319        example!("slo"),
320        example!("sum_rate"),
321    ];
322}