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, Filter, ParamDeclaration, TagType, TerminalParamType},
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(query: &str) -> Result<Query, CompileError> {
71 let mut parse = MPLParser::parse(Rule::file, query).map_err(ParseError::from)?;
73 let mut query = parser::Parser::default().parse_query(&mut parse)?;
74 let mut visitor = ParamTypecheckVisitor {};
76 visitor.walk(&mut query)?;
77 let mut visitor = GroupCheckVisitor::default();
79 visitor.walk(&mut query)?;
80
81 let mut visitor = OptionCheckVisitor::default();
82 visitor.walk(&mut query)?;
83
84 Ok(query)
85}
86#[derive(Debug, thiserror::Error, Diagnostic)]
88pub enum GroupError {
89 #[error("invalid groups: {next_groups:?} is not a subset of {prev_groups:?}")]
91 InvalidGroups {
92 next_groups: HashSet<String>,
94 next_span: Box<SourceSpan>,
96 prev_groups: HashSet<String>,
98 prev_span: Box<SourceSpan>,
100 },
101}
102
103#[derive(Default)]
104struct OptionCheckVisitor {
105 ifdef_param: Option<ParamDeclaration>,
106 seen_param: Option<ParamDeclaration>,
107}
108
109#[derive(Debug, thiserror::Error, Diagnostic)]
111pub enum IfdefError {
112 #[error("{} is optional and used outside of ifdef", param.name)]
114 OptionalOutsideOfIfdef {
115 #[label("{}", param.name)]
117 span: SourceSpan,
118 param: ParamDeclaration,
120 },
121 #[error("{} is used in a ifdef guard but not referenced inside of it", param.name)]
123 OptionalNotUsed {
124 #[label("{}", param.name)]
126 span: SourceSpan,
127 param: ParamDeclaration,
129 },
130}
131
132impl QueryVisitor for OptionCheckVisitor {
133 type Error = IfdefError;
134 fn visit_ifdef(
135 &mut self,
136 param: &mut ParamDeclaration,
137 _filter: &mut Filter,
138 ) -> Result<VisitRes, Self::Error> {
139 self.ifdef_param = Some(param.clone());
140 self.seen_param = None;
141 Ok(VisitRes::Walk)
142 }
143 fn leave_ifdef(
144 &mut self,
145 param: &mut ParamDeclaration,
146 _filter: &mut Filter,
147 ) -> Result<(), Self::Error> {
148 if self.ifdef_param != self.seen_param {
149 return Err(IfdefError::OptionalNotUsed {
150 span: param.span,
151 param: param.clone(),
152 });
153 }
154 self.ifdef_param = None;
155 Ok(())
156 }
157 fn visit_parameterized_value(
158 &mut self,
159 value: &mut Parameterized<tags::TagValue>,
160 ) -> Result<VisitRes, Self::Error> {
161 if let Parameterized::Param { span, param } = value
162 && param.is_optional()
163 {
164 self.seen_param = Some(param.clone());
165 if self.seen_param != self.ifdef_param {
166 return Err(IfdefError::OptionalOutsideOfIfdef {
167 span: *span,
168 param: param.clone(),
169 });
170 }
171 }
172 Ok(VisitRes::Walk)
173 }
174 fn visit_parameterized_regex(
175 &mut self,
176 regex: &mut Parameterized<enc_regex::EncodableRegex>,
177 ) -> Result<VisitRes, Self::Error> {
178 if let Parameterized::Param { span, param } = regex
179 && param.is_optional()
180 {
181 self.seen_param = Some(param.clone());
182 if self.seen_param != self.ifdef_param {
183 return Err(IfdefError::OptionalOutsideOfIfdef {
184 span: *span,
185 param: param.clone(),
186 });
187 }
188 }
189 Ok(VisitRes::Walk)
190 }
191}
192
193impl QueryWalker for OptionCheckVisitor {}
194
195struct GroupCheckVisitor {
196 groups: Option<HashSet<String>>,
197 span: SourceSpan,
198 stack: Vec<(SourceSpan, Option<HashSet<String>>)>,
199}
200
201impl Default for GroupCheckVisitor {
202 fn default() -> Self {
203 Self {
204 groups: None,
205 span: SourceSpan::new(SourceOffset::from_location("", 0, 0), 0),
206 stack: Vec::new(),
207 }
208 }
209}
210impl GroupCheckVisitor {
211 fn check_group_by(
212 &mut self,
213 tags: &[String],
214 span: SourceSpan,
215 ) -> Result<VisitRes, GroupError> {
216 let next_groups: HashSet<String> = tags.iter().cloned().collect();
217 let Some(prev_groups) = self.groups.take() else {
218 self.groups = Some(next_groups);
219 self.span = span;
220 return Ok(VisitRes::Walk);
221 };
222 if !next_groups.is_subset(&prev_groups) {
223 return Err(GroupError::InvalidGroups {
224 next_groups,
225 next_span: Box::new(span),
226 prev_groups,
227 prev_span: Box::new(self.span),
228 });
229 }
230 self.groups = Some(next_groups);
231 self.span = span;
232 Ok(VisitRes::Walk)
233 }
234}
235
236impl QueryVisitor for GroupCheckVisitor {
237 type Error = GroupError;
238 fn visit(&mut self, _: &mut Query) -> Result<VisitRes, Self::Error> {
239 self.stack.push((self.span, self.groups.take()));
240 Ok(VisitRes::Walk)
241 }
242 fn leave(&mut self, _: &mut Query) -> Result<(), Self::Error> {
243 let Some((span, groups)) = self.stack.pop() else {
244 return Ok(());
245 };
246 self.span = span;
247 self.groups = groups;
248 Ok(())
249 }
250 fn visit_group_by(&mut self, group_by: &mut query::GroupBy) -> Result<VisitRes, Self::Error> {
251 self.check_group_by(&group_by.tags, group_by.span)
252 }
253 fn visit_bucket_by(
254 &mut self,
255 bucket_by: &mut query::BucketBy,
256 ) -> Result<VisitRes, Self::Error> {
257 self.check_group_by(&bucket_by.tags, bucket_by.span)
258 }
259}
260impl QueryWalker for GroupCheckVisitor {}
261
262#[derive(Debug, thiserror::Error, Diagnostic)]
264pub enum TypeError {
265 #[error(
267 "The param ${param_name} has type {actual}, but was used in context that expects one of: {}",
268 expected.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
269 )]
270 #[diagnostic(code(mpl_lang::typemismatch))]
271 #[allow(unused_assignments)]
272 TypeMismatch {
273 #[label("param")]
275 use_span: SourceSpan,
276 #[label("param declaration")]
278 declaration_span: SourceSpan,
279 param_name: String,
281 expected: Vec<TerminalParamType>,
283 actual: TerminalParamType,
285 },
286}
287
288struct ParamTypecheckVisitor {}
289
290impl ParamTypecheckVisitor {
291 fn assert_param_type<T>(
292 value: &Parameterized<T>,
293 expected: Vec<TerminalParamType>,
294 ) -> Result<(), TypeError> {
295 if let Parameterized::Param { span, param } = value
296 && !expected.contains(¶m.typ())
297 {
298 return Err(TypeError::TypeMismatch {
299 use_span: *span,
300 declaration_span: param.span,
301 param_name: param.name.clone(),
302 expected,
303 actual: param.typ(),
304 });
305 }
306
307 Ok(())
308 }
309}
310
311impl QueryVisitor for ParamTypecheckVisitor {
312 type Error = TypeError;
313
314 fn visit_dataset(
315 &mut self,
316 dataset: &mut Parameterized<Dataset>,
317 ) -> Result<VisitRes, Self::Error> {
318 Self::assert_param_type(dataset, vec![TerminalParamType::Dataset]).map(|()| VisitRes::Walk)
319 }
320
321 fn visit_align(&mut self, align: &mut query::Align) -> Result<VisitRes, Self::Error> {
322 Self::assert_param_type(&align.time, vec![TerminalParamType::Duration])
323 .map(|()| VisitRes::Walk)
324 }
325
326 fn visit_bucket_by(
327 &mut self,
328 bucket_by: &mut query::BucketBy,
329 ) -> Result<VisitRes, Self::Error> {
330 Self::assert_param_type(&bucket_by.time, vec![TerminalParamType::Duration])
331 .map(|()| VisitRes::Walk)
332 }
333
334 fn visit_cmp(&mut self, _field: &mut String, cmp: &mut Cmp) -> Result<VisitRes, Self::Error> {
335 let tag_value_param_types = vec![
336 TerminalParamType::Tag(TagType::String),
337 TerminalParamType::Tag(TagType::Int),
338 TerminalParamType::Tag(TagType::Float),
339 TerminalParamType::Tag(TagType::Bool),
340 ];
341
342 match cmp {
343 Cmp::Is(_) => Ok(VisitRes::Walk),
344 Cmp::Eq(value) => {
345 if let Parameterized::Param { span, param } = value
346 && param.typ() == TerminalParamType::Regex
347 {
348 *cmp = Cmp::RegEx(Parameterized::Param {
353 span: *span,
354 param: param.clone(),
355 });
356 return Ok(VisitRes::Walk);
357 }
358
359 Self::assert_param_type(value, tag_value_param_types).map(|()| VisitRes::Walk)
360 }
361 Cmp::Ne(value) => {
362 if let Parameterized::Param { span, param } = value
363 && param.typ() == TerminalParamType::Regex
364 {
365 *cmp = Cmp::RegExNot(Parameterized::Param {
370 span: *span,
371 param: param.clone(),
372 });
373 return Ok(VisitRes::Walk);
374 }
375
376 Self::assert_param_type(value, tag_value_param_types).map(|()| VisitRes::Walk)
377 }
378 Cmp::Gt(value) | Cmp::Ge(value) | Cmp::Lt(value) | Cmp::Le(value) => {
379 Self::assert_param_type(value, tag_value_param_types).map(|()| VisitRes::Walk)
380 }
381 Cmp::RegEx(value) | Cmp::RegExNot(value) => {
382 Self::assert_param_type(value, vec![TerminalParamType::Regex])
383 .map(|()| VisitRes::Walk)
384 }
385 }
386 }
387}
388
389impl QueryWalker for ParamTypecheckVisitor {}
390
391#[cfg(feature = "examples")]
392pub mod examples {
393 macro_rules! example {
395 ($name:expr) => {
396 (
397 concat!($name),
398 include_str!(concat!("../tests/examples/", $name, ".mpl")),
399 )
400 };
401 }
402
403 pub const SPEC: &str = include_str!("../spec.md");
405
406 pub const MPL: [(&str, &str); 18] = [
408 example!("align-rate"),
409 example!("as"),
410 example!("enrich"),
411 example!("filtered-histogram"),
412 example!("histogram_rate"),
413 example!("histogram"),
414 example!("ifdef"),
415 example!("map-gt"),
416 example!("map-mul"),
417 example!("nested-enrich"),
418 example!("parser-error"),
419 example!("rate"),
420 example!("replace_labels"),
421 example!("set"),
422 example!("slo-histogram"),
423 example!("slo-ingest-rate"),
424 example!("slo"),
425 example!("sum_rate"),
426 ];
427}