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::{
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#[derive(Debug, thiserror::Error, Diagnostic)]
51pub enum CompileError {
52 #[error(transparent)]
54 #[diagnostic(transparent)]
55 Parse(#[from] ParseError),
56 #[error(transparent)]
58 #[diagnostic(transparent)]
59 Type(#[from] TypeError),
60 #[error(transparent)]
62 #[diagnostic(transparent)]
63 Group(#[from] GroupError),
64
65 #[error(transparent)]
67 #[diagnostic(transparent)]
68 Ifdef(#[from] IfdefError),
69}
70
71#[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 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 let mut visitor = ParamTypecheckVisitor {};
82 visitor.walk(&mut query)?;
83 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#[derive(Debug, thiserror::Error, Diagnostic)]
94pub enum GroupError {
95 #[error("invalid groups: {next_groups:?} is not a subset of {prev_groups:?}")]
97 InvalidGroups {
98 next_groups: HashSet<String>,
100 next_span: Box<SourceSpan>,
102 prev_groups: HashSet<String>,
104 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#[derive(Debug, thiserror::Error, Diagnostic)]
117pub enum IfdefError {
118 #[error("{} is optional and used outside of ifdef", param.name)]
120 OptionalOutsideOfIfdef {
121 #[label("{}", param.name)]
123 span: SourceSpan,
124 param: ParamDeclaration,
126 },
127 #[error("{} is used in a ifdef guard but not referenced inside of it", param.name)]
129 OptionalNotUsed {
130 #[label("{}", param.name)]
132 span: SourceSpan,
133 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#[derive(Debug, thiserror::Error, Diagnostic)]
270pub enum TypeError {
271 #[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 #[label("param")]
281 use_span: SourceSpan,
282 #[label("param declaration")]
284 declaration_span: SourceSpan,
285 param_name: String,
287 expected: Vec<TerminalParamType>,
289 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(¶m.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 *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 *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 macro_rules! example {
409 ($name:expr) => {
410 (
411 concat!($name),
412 include_str!(concat!("../tests/examples/", $name, ".mpl")),
413 )
414 };
415 }
416
417 pub const SPEC: &str = include_str!("../spec.md");
419
420 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}