1#![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)] mod 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#[derive(Debug, thiserror::Error, Diagnostic)]
48pub enum CompileError {
49 #[error(transparent)]
51 #[diagnostic(transparent)]
52 Parse(#[from] ParseError),
53 #[error(transparent)]
55 #[diagnostic(transparent)]
56 Type(#[from] TypeError),
57 #[error(transparent)]
59 #[diagnostic(transparent)]
60 Group(#[from] GroupError),
61}
62
63#[allow(clippy::result_large_err)]
65pub fn compile(query: &str) -> Result<Query, CompileError> {
66 let mut parse = MPLParser::parse(Rule::file, query).map_err(ParseError::from)?;
68 let mut query = parser::Parser::default().parse_query(&mut parse)?;
69 let mut visitor = ParamTypecheckVisitor {};
71 visitor.walk(&mut query)?;
72 let mut visitor = GroupCheckVisitor::default();
74 visitor.walk(&mut query)?;
75
76 Ok(query)
77}
78#[derive(Debug, thiserror::Error, Diagnostic)]
80pub enum GroupError {
81 #[error("invalid groups: {next_groups:?} is not a subset of {prev_groups:?}")]
83 InvalidGroups {
84 next_groups: HashSet<String>,
86 next_span: Box<SourceSpan>,
88 prev_groups: HashSet<String>,
90 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#[derive(Debug, thiserror::Error, Diagnostic)]
163pub enum TypeError {
164 #[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 #[label("param")]
174 use_span: SourceSpan,
175 #[label("param declaration")]
177 declaration_span: SourceSpan,
178 param_name: String,
180 expected: Vec<ParamType>,
182 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(¶m.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 *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 *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 macro_rules! example {
291 ($name:expr) => {
292 (
293 concat!($name),
294 include_str!(concat!("../tests/examples/", $name, ".mpl")),
295 )
296 };
297 }
298
299 pub const SPEC: &str = include_str!("../spec.md");
301
302 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}