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;
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#[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 #[error(transparent)]
64 #[diagnostic(transparent)]
65 Ifdef(#[from] IfdefError),
66}
67
68#[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 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 let mut visitor = ParamTypecheckVisitor {};
79 visitor.walk(&mut query)?;
80 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#[derive(Debug, thiserror::Error, Diagnostic)]
91pub enum GroupError {
92 #[error("invalid groups: {next_groups:?} is not a subset of {prev_groups:?}")]
94 InvalidGroups {
95 next_groups: HashSet<String>,
97 next_span: Box<SourceSpan>,
99 prev_groups: HashSet<String>,
101 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#[derive(Debug, thiserror::Error, Diagnostic)]
114pub enum IfdefError {
115 #[error("{} is optional and used outside of ifdef", param.name)]
117 OptionalOutsideOfIfdef {
118 #[label("{}", param.name)]
120 span: SourceSpan,
121 param: ParamDeclaration,
123 },
124 #[error("{} is used in a ifdef guard but not referenced inside of it", param.name)]
126 OptionalNotUsed {
127 #[label("{}", param.name)]
129 span: SourceSpan,
130 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#[derive(Debug, thiserror::Error, Diagnostic)]
266pub enum TypeError {
267 #[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 #[label("param")]
277 use_span: SourceSpan,
278 #[label("param declaration")]
280 declaration_span: SourceSpan,
281 param_name: String,
283 expected: Vec<TerminalParamType>,
285 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(¶m.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 *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 *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 macro_rules! example {
428 ($name:expr) => {
429 (
430 concat!($name),
431 include_str!(concat!("../tests/examples/", $name, ".mpl")),
432 )
433 };
434 }
435
436 pub const SPEC: &str = include_str!("../spec.md");
438
439 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}