1use crate::plan::{PlanNode, PlanNodeKind};
14use crate::typed::{Bound, Dim, ResolvedDimSpec, TypeInfo, TypedExpr, TypedExprKind};
15use improv_core_model::functions::{self, Arg, Kind, Ret};
16use improv_core_model::{
17 BinaryOp, Category, CategoryId, DimensionSpec, Expr, Formula, FuncId, Item, ItemId, Measure,
18 MeasureId, UnaryOp, ValueType,
19};
20use std::collections::HashMap;
21
22#[derive(Debug, thiserror::Error, PartialEq)]
23pub enum CompileError {
24 #[error("unknown measure referenced in formula: {0:?}")]
25 UnknownMeasure(MeasureId),
26 #[error("unknown function: {0:?}")]
27 UnknownFunction(improv_core_model::FuncId),
28 #[error("type mismatch: {0}")]
29 TypeMismatch(String),
30 #[error("dimension mismatch: {0}")]
31 DimensionMismatch(String),
32 #[error("unsupported: {0}")]
33 Unsupported(String),
34}
35
36pub type Result<T> = std::result::Result<T, CompileError>;
37
38pub struct CompileContext<'m> {
41 pub measures: &'m HashMap<MeasureId, Measure>,
42 pub categories: Option<&'m HashMap<CategoryId, Category>>,
45 pub items: Option<&'m HashMap<ItemId, Item>>,
47 pub lambdas: Option<&'m improv_core_model::Model>,
49}
50
51impl<'m> CompileContext<'m> {
52 pub fn new(measures: &'m HashMap<MeasureId, Measure>) -> Self {
54 CompileContext {
55 measures,
56 categories: None,
57 items: None,
58 lambdas: None,
59 }
60 }
61
62 pub fn for_model(model: &'m improv_core_model::Model) -> Self {
64 CompileContext {
65 measures: &model.measures,
66 categories: Some(&model.categories),
67 items: Some(&model.items),
68 lambdas: (!model.lambdas.is_empty()).then_some(model),
69 }
70 }
71
72 pub fn items_of(&self, c: CategoryId) -> Vec<ItemId> {
74 self.categories
75 .and_then(|cs| cs.get(&c))
76 .map(|c| c.items.clone())
77 .unwrap_or_default()
78 }
79
80 pub fn category_name(&self, c: CategoryId) -> String {
82 self.categories
83 .and_then(|cs| cs.get(&c))
84 .map_or_else(|| format!("{c:?}"), |c| c.name.0.clone())
85 }
86}
87
88pub fn compile_formula(
90 ctx: &CompileContext,
91 target: MeasureId,
92 formula: &Formula,
93) -> Result<PlanNode> {
94 if let Some(args) = top_fixpoint(&formula.expr) {
95 return compile_fixpoint(ctx, target, args);
96 }
97 if formula.referenced_measures().contains(&target) {
98 return compile_recurrence(ctx, target, &formula.expr);
99 }
100 let typed = infer(ctx, target, &formula.expr)?;
101 build_plan(ctx, &typed)
102}
103
104fn top_fixpoint(e: &Expr) -> Option<&[Expr]> {
106 match e {
107 Expr::Call(f, args) if f.0 == dim::FIXPOINT => Some(args),
108 _ => None,
109 }
110}
111
112pub const FIXPOINT_MAX_ITER: u32 = 10_000;
114
115fn compile_fixpoint(ctx: &CompileContext, target: MeasureId, args: &[Expr]) -> Result<PlanNode> {
121 let usage = || {
122 CompileError::Unsupported(
123 "FIXPOINT takes (init, LAMBDA(r, body), [max_iter], [tolerance])".into(),
124 )
125 };
126 let [init, body, opts @ ..] = args else {
127 return Err(usage());
128 };
129 if opts.len() > 2 {
130 return Err(usage());
131 }
132 let mut read = init.referenced_measures();
133 read.extend(body.referenced_measures());
134 if read.contains(&target) {
135 return Err(CompileError::Unsupported(
136 "FIXPOINT may not read its own measure: the iterate is the LAMBDA's parameter".into(),
137 ));
138 }
139 let opt = |i: usize, default: f64, what: &str| -> Result<f64> {
140 let Some(e) = opts.get(i) else {
141 return Ok(default);
142 };
143 match crate::scalar::eval(e, &HashMap::new()) {
144 Ok(Some(v)) => v.as_num(),
145 _ => None,
146 }
147 .ok_or_else(|| {
148 CompileError::TypeMismatch(format!("FIXPOINT's {what} must be a constant number"))
149 })
150 };
151 let max_iter = opt(0, 100.0, "max_iter")?;
152 if !(0.0..=f64::from(FIXPOINT_MAX_ITER)).contains(&max_iter) || max_iter.fract() != 0.0 {
153 return Err(CompileError::Unsupported(format!(
154 "FIXPOINT's max_iter must be a whole number from 0 to {FIXPOINT_MAX_ITER}"
155 )));
156 }
157 let tolerance = opt(1, 1e-10, "tolerance")?;
158 if tolerance.is_nan() || tolerance < 0.0 {
160 return Err(CompileError::Unsupported(
161 "FIXPOINT's tolerance must be 0 or more".into(),
162 ));
163 }
164 let (_, _, ty) = fixpoint_parts(ctx, target, init, body, functions::lambda_slot(0, 0))?;
165 Ok(PlanNode {
166 kind: PlanNodeKind::Fixpoint {
167 init: init.clone(),
168 body: body.clone(),
169 max_iter: max_iter as u32,
170 tolerance,
171 },
172 ty,
173 })
174}
175
176pub(crate) fn fixpoint_parts(
184 ctx: &CompileContext,
185 target: MeasureId,
186 init: &Expr,
187 body: &Expr,
188 r: MeasureId,
189) -> Result<(PlanNode, PlanNode, TypeInfo)> {
190 let body = rename_ref(body, functions::lambda_slot(0, 0), r);
191 let t0 = infer(ctx, target, init)?;
192 let mut dim = t0.ty.dim.clone();
193 loop {
194 let mut measures = ctx.measures.clone();
195 measures.insert(
196 r,
197 Measure {
198 id: r,
199 name: improv_core_model::Name("r".into()),
200 value_type: t0.ty.value_type,
201 categories: dim.categories.clone(),
202 kind: improv_core_model::MeasureKind::Input,
203 description: None,
204 },
205 );
206 let bctx = CompileContext {
207 measures: &measures,
208 categories: ctx.categories,
209 items: ctx.items,
210 lambdas: ctx.lambdas,
211 };
212 let tb = infer(&bctx, target, &body).map_err(|e| match e {
213 CompileError::DimensionMismatch(s) if dim.is_scalar() => {
214 CompileError::DimensionMismatch(format!(
215 "{s} (FIXPOINT's r ranges over the initial value's categories, here none: \
216 start from one over them, SEQUENCE(T, x, 0) for x at every item of T)"
217 ))
218 }
219 e => e,
220 })?;
221 if tb.ty.dim != dim {
222 if !dim.is_subset_of(&tb.ty.dim) {
223 return Err(CompileError::DimensionMismatch(format!(
224 "FIXPOINT: the body ranges over {} but the iterate over {}: the body must \
225 keep the iterate's categories",
226 dims_text(ctx, &tb.ty.dim),
227 dims_text(ctx, &dim)
228 )));
229 }
230 dim = tb.ty.dim;
231 continue;
232 }
233 if tb.ty.value_type != t0.ty.value_type && !is_error_only(&t0) {
234 return Err(CompileError::TypeMismatch(format!(
235 "FIXPOINT: the initial value is {:?} but the body yields {:?}",
236 t0.ty.value_type, tb.ty.value_type
237 )));
238 }
239 if ctx.categories.is_none() && !dim.is_scalar() {
240 return Err(CompileError::Unsupported(
241 "FIXPOINT needs the model's categories".into(),
242 ));
243 }
244 let ty = TypeInfo {
245 value_type: tb.ty.value_type,
246 dim: dim.clone(),
247 };
248 let first = if t0.ty.dim == dim {
249 t0
250 } else {
251 let grid = dim
252 .categories
253 .iter()
254 .map(|c| (*c, ctx.items_of(*c)))
255 .collect();
256 TypedExpr {
257 ty: TypeInfo {
258 value_type: t0.ty.value_type,
259 dim,
260 },
261 kind: TypedExprKind::Coalesce {
262 args: vec![t0],
263 grid: Some(grid),
264 },
265 }
266 };
267 return Ok((build_plan(ctx, &first)?, build_plan(&bctx, &tb)?, ty));
268 }
269}
270
271fn rename_ref(e: &Expr, from: MeasureId, to: MeasureId) -> Expr {
273 match e {
274 Expr::Ref(r, s) if *r == from => Expr::Ref(to, s.clone()),
275 Expr::UnaryOp(o, a) => Expr::UnaryOp(*o, Box::new(rename_ref(a, from, to))),
276 Expr::BinaryOp(o, a, b) => Expr::BinaryOp(
277 *o,
278 Box::new(rename_ref(a, from, to)),
279 Box::new(rename_ref(b, from, to)),
280 ),
281 Expr::Call(f, a) => Expr::Call(*f, a.iter().map(|x| rename_ref(x, from, to)).collect()),
282 other => other.clone(),
283 }
284}
285
286pub fn derived_shape(
292 model: &improv_core_model::Model,
293 target: MeasureId,
294 name: &str,
295 formula: &Formula,
296) -> Result<(ValueType, Vec<CategoryId>)> {
297 let ctx = CompileContext::for_model(model);
298 if let Some(args) = top_fixpoint(&formula.expr) {
299 let p = compile_fixpoint(&ctx, target, args)?;
300 return Ok((p.ty.value_type, p.ty.dim.categories));
301 }
302 if !formula.referenced_measures().contains(&target) {
303 let t = infer(&ctx, target, &formula.expr)?;
304 return Ok((t.ty.value_type, t.ty.dim.categories));
305 }
306 let mut cats: Vec<CategoryId> = Vec::new();
308 for m in formula.referenced_measures() {
309 if let Some(measure) = model.measures.get(&m).filter(|_| m != target) {
310 cats.extend(measure.categories.iter().copied());
311 }
312 }
313 let cats = Dim::of(cats).categories;
314 let mut provisional = model.clone();
315 let existing = model.measures.get(&target).map(|m| m.value_type);
316 for vt in existing.into_iter().chain([
317 ValueType::Number,
318 ValueType::Text,
319 ValueType::Boolean,
320 ValueType::DateTime,
321 ]) {
322 provisional.measures.insert(
323 target,
324 Measure {
325 id: target,
326 name: improv_core_model::Name(name.to_string()),
327 value_type: vt,
328 categories: cats.clone(),
329 kind: improv_core_model::MeasureKind::Derived(formula.clone()),
330 description: None,
331 },
332 );
333 let pctx = CompileContext::for_model(&provisional);
334 match compile_recurrence(&pctx, target, &formula.expr) {
335 Ok(_) => return Ok((vt, cats)),
336 Err(CompileError::TypeMismatch(_)) => continue,
337 Err(e) => return Err(e),
338 }
339 }
340 Err(CompileError::TypeMismatch(format!(
341 "{name}'s formula has no consistent type"
342 )))
343}
344
345fn compile_fold(ctx: &CompileContext, last: bool, args: &[Expr]) -> Result<PlanNode> {
350 let (init, x, body) = match args {
351 [i, Expr::Ref(x, spec), b] if spec.over.len() == 1 => (i, (*x, spec.over[0]), b),
352 _ => {
353 return Err(CompileError::Unsupported(
354 "a fold needs (init, x OVER Category, LAMBDA(acc, v, body))".into(),
355 ))
356 }
357 };
358 let (xid, cat) = x;
359 let m = ctx
360 .measures
361 .get(&xid)
362 .ok_or(CompileError::UnknownMeasure(xid))?;
363 if !m.categories.contains(&cat) {
364 return Err(CompileError::DimensionMismatch(format!(
365 "{} does not range over {}",
366 m.name.0,
367 ctx.category_name(cat)
368 )));
369 }
370 let (acc, val) = (functions::lambda_slot(0, 0), functions::lambda_slot(0, 1));
372 let body = rename_ref(body, val, xid);
373 crate::scalar::check(&body).map_err(|e| {
374 CompileError::Unsupported(format!("a fold's LAMBDA body must be scalar: {e}"))
375 })?;
376 crate::scalar::check(init).map_err(|e| {
377 CompileError::Unsupported(format!("a fold's initial value must be scalar: {e}"))
378 })?;
379 let init_t = infer(ctx, xid, init)?;
381 let mut measures = ctx.measures.clone();
382 measures.insert(
383 acc,
384 Measure {
385 id: acc,
386 value_type: init_t.ty.value_type,
387 ..m.clone()
388 },
389 );
390 let tctx = CompileContext {
391 measures: &measures,
392 categories: ctx.categories,
393 items: ctx.items,
394 lambdas: ctx.lambdas,
395 };
396 let typed = infer(&tctx, xid, &body)?;
397 if typed.ty.value_type != init_t.ty.value_type && !is_error_only(&init_t) {
398 return Err(CompileError::TypeMismatch(format!(
399 "a fold's accumulator starts as {:?} but the LAMBDA yields {:?}",
400 init_t.ty.value_type, typed.ty.value_type
401 )));
402 }
403 let mut ids: Vec<MeasureId> = body.referenced_measures();
404 ids.extend(init.referenced_measures());
405 ids.retain(|i| *i != acc);
406 ids.sort();
407 ids.dedup();
408 let own = Dim::of(m.categories.clone());
409 let mut refs = Vec::new();
410 for id in ids {
411 let r = infer_ref(ctx, id, &DimensionSpec::default())?;
412 if !r.ty.dim.is_subset_of(&own) {
413 return Err(CompileError::DimensionMismatch(format!(
414 "a fold over {} may only read measures within its categories",
415 m.name.0
416 )));
417 }
418 refs.push((id, build_plan(ctx, &r)?));
419 }
420 let ty = TypeInfo {
421 value_type: typed.ty.value_type,
422 dim: own.clone(),
423 };
424 let scan = PlanNode {
425 kind: PlanNodeKind::Recurrence {
426 body,
427 refs,
428 slots: vec![(acc, 1, Some(init.clone()))],
429 cat,
430 order: ctx.items_of(cat),
431 },
432 ty: ty.clone(),
433 };
434 if !last {
435 return Ok(scan);
436 }
437 let kept: Vec<CategoryId> = own
438 .categories
439 .iter()
440 .copied()
441 .filter(|c| *c != cat)
442 .collect();
443 Ok(PlanNode {
444 kind: PlanNodeKind::Aggregate {
445 inputs: vec![scan],
446 params: vec![],
447 pred: None,
448 group_by: kept.clone(),
449 func: FuncId(907), order: vec![(cat, ctx.items_of(cat))],
451 window: None,
452 per_item: false,
453 into: Vec::new(),
454 },
455 ty: TypeInfo {
456 value_type: ty.value_type,
457 dim: Dim::of(kept),
458 },
459 })
460}
461
462const SLOT_BASE: u32 = u32::MAX - 1024;
464
465fn compile_recurrence(ctx: &CompileContext, target: MeasureId, expr: &Expr) -> Result<PlanNode> {
472 use improv_core_model::Value;
473 let m = ctx
474 .measures
475 .get(&target)
476 .ok_or(CompileError::UnknownMeasure(target))?;
477 let bad = |why: &str| {
478 CompileError::Unsupported(format!(
479 "{} refers to itself; {why} (write PREV({0}, Category, n, default))",
480 m.name.0
481 ))
482 };
483 let mut slots: Vec<(MeasureId, i64, Option<Expr>)> = Vec::new();
484 let mut cat: Option<CategoryId> = None;
485 fn rewrite(
487 e: &Expr,
488 target: MeasureId,
489 slots: &mut Vec<(MeasureId, i64, Option<Expr>)>,
490 cat: &mut Option<CategoryId>,
491 bad: &dyn Fn(&str) -> CompileError,
492 ) -> Result<Expr> {
493 Ok(match e {
494 Expr::Call(f, args)
495 if f.0 == dim::PREV
496 && matches!(args.first(), Some(Expr::Ref(r, _)) if *r == target) =>
497 {
498 let Some(Expr::Category(c)) = args.get(1) else {
499 return Err(bad("PREV needs a category"));
500 };
501 if cat.is_some_and(|x| x != *c) {
502 return Err(bad("every PREV of it must run along the same category"));
503 }
504 *cat = Some(*c);
505 let n = match args.get(2) {
506 None => 1,
507 Some(Expr::Literal(improv_core_model::Value::Number(n)))
508 if n.fract() == 0.0 && *n >= 1.0 =>
509 {
510 *n as i64
511 }
512 Some(_) => return Err(bad("PREV of itself needs a whole number of items ≥ 1")),
513 };
514 let default = args.get(3).cloned();
515 if default
516 .as_ref()
517 .is_some_and(|d| d.referenced_measures().contains(&target))
518 {
519 return Err(bad("the default must not refer to it"));
520 }
521 let id = MeasureId(SLOT_BASE + slots.len() as u32);
522 slots.push((id, n, default));
523 Expr::Ref(id, DimensionSpec::default())
524 }
525 Expr::Ref(r, _) if *r == target => {
526 return Err(bad("only PREV may read its earlier values"))
527 }
528 Expr::UnaryOp(op, a) => {
529 Expr::UnaryOp(*op, Box::new(rewrite(a, target, slots, cat, bad)?))
530 }
531 Expr::BinaryOp(op, a, b) => Expr::BinaryOp(
532 *op,
533 Box::new(rewrite(a, target, slots, cat, bad)?),
534 Box::new(rewrite(b, target, slots, cat, bad)?),
535 ),
536 Expr::Call(f, args) => Expr::Call(
537 *f,
538 args.iter()
539 .map(|a| rewrite(a, target, slots, cat, bad))
540 .collect::<Result<_>>()?,
541 ),
542 other => other.clone(),
543 })
544 }
545 let body = rewrite(expr, target, &mut slots, &mut cat, &bad)?;
546 let cat = cat.ok_or_else(|| bad("only PREV may read its earlier values"))?;
547 if !m.categories.contains(&cat) {
548 return Err(bad("PREV must run along one of its own categories"));
549 }
550 crate::scalar::check(&body).map_err(|e| bad(&format!("the rest must be scalar: {e}")))?;
551 for (_, _, d) in &slots {
552 if let Some(d) = d {
553 crate::scalar::check(d)
554 .map_err(|e| bad(&format!("the default must be scalar: {e}")))?;
555 }
556 }
557 let mut measures = ctx.measures.clone();
559 for (id, _, _) in &slots {
560 measures.insert(
561 *id,
562 Measure {
563 id: *id,
564 ..m.clone()
565 },
566 );
567 }
568 let typed_ctx = CompileContext {
569 measures: &measures,
570 categories: ctx.categories,
571 items: ctx.items,
572 lambdas: ctx.lambdas,
573 };
574 let typed = infer(&typed_ctx, target, &body)?;
575 if typed.ty.value_type != m.value_type {
576 return Err(CompileError::TypeMismatch(format!(
577 "{} is {:?} but its formula yields {:?}",
578 m.name.0, m.value_type, typed.ty.value_type
579 )));
580 }
581 let mut ids: Vec<MeasureId> = body.referenced_measures();
584 for (_, _, d) in &slots {
585 ids.extend(d.iter().flat_map(|d| d.referenced_measures()));
586 }
587 ids.retain(|i| i.0 < SLOT_BASE);
588 ids.sort();
589 ids.dedup();
590 let mut refs = Vec::new();
591 let mut dim = Dim::scalar();
592 for id in ids {
593 let r = infer_ref(ctx, id, &DimensionSpec::default())?;
594 if !r.ty.dim.is_subset_of(&Dim::of(m.categories.clone())) {
595 return Err(bad("it may only read measures within its own categories"));
596 }
597 dim = dim.union(&r.ty.dim);
598 refs.push((id, build_plan(ctx, &r)?));
599 }
600 if dim != Dim::of(m.categories.clone()) {
601 return Err(bad(
602 "the measures it reads must together range over all of its categories",
603 ));
604 }
605 let _ = Value::Number(0.0);
606 Ok(PlanNode {
607 kind: PlanNodeKind::Recurrence {
608 body,
609 refs,
610 slots,
611 cat,
612 order: ctx.items_of(cat),
613 },
614 ty: TypeInfo {
615 value_type: m.value_type,
616 dim,
617 },
618 })
619}
620
621pub(crate) fn is_aggregation(f: FuncId) -> bool {
623 functions::is_aggregation(f)
624}
625
626pub fn scalar_arity(f: FuncId) -> Option<usize> {
630 functions::by_id(f)
631 .filter(|d| d.kind == Kind::Scalar)
632 .map(|d| d.params.len())
633}
634
635pub(crate) mod lazy {
637 pub const IF: u32 = 30;
638 pub const IFS: u32 = 31;
639 pub const SWITCH: u32 = 32;
640 pub const CHOOSE: u32 = 33;
641 pub const IFERROR: u32 = 34;
642 pub const IFNA: u32 = 35;
643 pub const CLEARERROR: u32 = 36;
644 pub const CLEARZERO: u32 = 37;
645 pub const COALESCE: u32 = 41;
646 pub const FILL: u32 = 919;
648}
649
650#[allow(
652 clippy::only_used_in_recursion,
653 reason = "`target` is threaded for future error context and recursive calls"
654)]
655pub fn infer(ctx: &CompileContext, target: MeasureId, expr: &Expr) -> Result<TypedExpr> {
656 if let Expr::Call(f, _) = expr {
658 if functions::named_lambda_of(*f).is_some() {
659 let model = ctx.lambdas.ok_or(CompileError::UnknownFunction(*f))?;
660 let e = model
661 .expand_lambdas(expr)
662 .map_err(CompileError::Unsupported)?;
663 return infer(ctx, target, &e);
664 }
665 }
666 match expr {
667 Expr::Literal(v) => {
668 let value_type = v.type_of().ok_or_else(|| {
669 CompileError::TypeMismatch("error literal has no value type".into())
670 })?;
671 Ok(TypedExpr {
672 kind: TypedExprKind::Literal(v.clone()),
673 ty: TypeInfo {
674 value_type,
675 dim: Dim::scalar(),
676 },
677 })
678 }
679 Expr::Ref(mid, spec) => {
680 let typed = infer_ref(ctx, *mid, spec)?;
681 let m = &ctx.measures[mid];
685 if let Some(c) = m
686 .categories
687 .iter()
688 .find(|c| !typed.ty.dim.categories.contains(c))
689 {
690 let c = ctx.category_name(*c);
691 return Err(CompileError::DimensionMismatch(format!(
692 "{0}[…] leaves out category {c}, which only an aggregation \
693 can collapse: write SUM({0} OVER {c}) (or AVERAGE/MIN/MAX)",
694 m.name.0
695 )));
696 }
697 Ok(typed)
698 }
699 Expr::Category(c) => Err(CompileError::TypeMismatch(format!(
700 "category {} is not a value; use it as a function argument (POSITION({0}))",
701 ctx.category_name(*c)
702 ))),
703 Expr::UnaryOp(op, e) => {
704 let inner = infer(ctx, target, e)?;
705 let want = match op {
706 UnaryOp::Neg => ValueType::Number,
707 UnaryOp::Not => ValueType::Boolean,
708 };
709 if inner.ty.value_type != want {
710 return Err(CompileError::TypeMismatch(format!(
711 "unary {op:?} expects {want:?}, got {:?}",
712 inner.ty.value_type
713 )));
714 }
715 let ty = inner.ty.clone();
716 Ok(TypedExpr {
717 kind: TypedExprKind::UnaryOp(*op, Box::new(inner)),
718 ty,
719 })
720 }
721 Expr::BinaryOp(op, l, r) => {
722 let lt = infer(ctx, target, l)?;
723 let rt = infer(ctx, target, r)?;
724 let value_type = binop_result_type(*op, <, &rt)?;
725
726 let shares = lt
733 .ty
734 .dim
735 .categories
736 .iter()
737 .any(|c| rt.ty.dim.categories.contains(c));
738 let dim = if lt.ty.dim.is_subset_of(&rt.ty.dim) {
739 rt.ty.dim.clone()
740 } else if rt.ty.dim.is_subset_of(<.ty.dim) {
741 lt.ty.dim.clone()
742 } else if shares
743 || l.referenced_measures().is_empty()
744 || r.referenced_measures().is_empty()
745 {
746 lt.ty.dim.union(&rt.ty.dim)
747 } else {
748 return Err(CompileError::DimensionMismatch(format!(
749 "{} ranges over {} and {} over {}: they share no category, so no \
750 cell has both (neither operand dim is a subset of the other). Reduce \
751 one first, e.g. SUM({} OVER {}), or give them a category in common",
752 operand_name(ctx, l),
753 dims_text(ctx, <.ty.dim),
754 operand_name(ctx, r),
755 dims_text(ctx, &rt.ty.dim),
756 operand_name(ctx, r),
757 rt.ty
758 .dim
759 .categories
760 .first()
761 .map_or_else(String::new, |c| ctx.category_name(*c)),
762 )));
763 };
764 Ok(TypedExpr {
765 kind: TypedExprKind::BinaryOp(*op, Box::new(lt), Box::new(rt)),
766 ty: TypeInfo { value_type, dim },
767 })
768 }
769 Expr::Call(func, args) if !is_aggregation(*func) => {
770 let def = functions::by_id(*func).ok_or(CompileError::UnknownFunction(*func))?;
771 if !def.accepts(args.len()) {
772 return Err(CompileError::Unsupported(format!(
773 "{} expects {}, got {} argument(s)",
774 def.name,
775 def.signature(),
776 args.len()
777 )));
778 }
779 if def.id.0 == dim::SCAN || def.id.0 == dim::REDUCE {
780 let plan = compile_fold(ctx, def.id.0 == dim::REDUCE, args)?;
781 let ty = plan.ty.clone();
782 return Ok(TypedExpr {
783 kind: TypedExprKind::Plan(Box::new(plan)),
784 ty,
785 });
786 }
787 if def.id.0 == dim::FIXPOINT {
788 return Err(CompileError::Unsupported(
789 "FIXPOINT must be a measure's whole formula: define it as its own measure"
790 .into(),
791 ));
792 }
793 if let Some(t) = infer_dimensional(ctx, target, def, args)? {
794 return Ok(t);
795 }
796 if def.kind == Kind::Lazy {
797 return infer_lazy(ctx, target, def, args);
798 }
799 let typed_args: Vec<TypedExpr> = args
800 .iter()
801 .map(|a| infer(ctx, target, a))
802 .collect::<Result<_>>()?;
803 for (i, a) in typed_args.iter().enumerate() {
804 check_arg(def, i, a)?;
805 }
806 let dim = union_dim(ctx, def.name, typed_args.iter().map(|a| &a.ty.dim))?;
807 let Ret::Fixed(value_type) = def.ret else {
808 unreachable!("only lazy functions unify their return type")
809 };
810 Ok(TypedExpr {
811 kind: TypedExprKind::Call(*func, typed_args),
812 ty: TypeInfo { value_type, dim },
813 })
814 }
815 Expr::Call(func, args) => infer_aggregate(ctx, target, *func, args),
816 }
817}
818
819mod dim {
821 pub const POSITION: u32 = 900;
822 pub const ITEM: u32 = 901;
823 pub const ID: u32 = 902;
824 pub const ITEMCOUNT: u32 = 903;
825 pub const PREV: u32 = 904;
826 pub const NEXT: u32 = 905;
827 pub const SCAN: u32 = 911;
828 pub const REDUCE: u32 = 912;
829 pub const RELABEL: u32 = 920;
830 pub const SWAP: u32 = 921;
831 pub const FIXPOINT: u32 = 922;
832}
833
834fn infer_dimensional(
837 ctx: &CompileContext,
838 target: MeasureId,
839 def: &functions::FnDef,
840 args: &[Expr],
841) -> Result<Option<TypedExpr>> {
842 let cat = |e: &Expr| match e {
843 Expr::Category(c) => Ok(*c),
844 _ => Err(CompileError::TypeMismatch(format!(
845 "{} takes a category name ({})",
846 def.name,
847 def.signature()
848 ))),
849 };
850 let items_of = |c: CategoryId| {
851 let items = ctx.items_of(c);
852 if items.is_empty() && ctx.categories.is_none() {
853 return Err(CompileError::Unsupported(format!(
854 "{} needs the model's categories",
855 def.name
856 )));
857 }
858 Ok(items)
859 };
860 let name_of = |i: ItemId| {
861 ctx.items
862 .and_then(|m| m.get(&i))
863 .map_or_else(|| format!("{}", i.0), |it| it.name.0.clone())
864 };
865 let items_node = |c: CategoryId, vals: Vec<(ItemId, improv_core_model::Value)>, vt| TypedExpr {
866 kind: TypedExprKind::Items(c, vals),
867 ty: TypeInfo {
868 value_type: vt,
869 dim: Dim::of(vec![c]),
870 },
871 };
872 use improv_core_model::Value;
873 Ok(Some(match def.id.0 {
874 dim::POSITION => {
875 let c = cat(&args[0])?;
876 let vals = items_of(c)?
877 .into_iter()
878 .enumerate()
879 .map(|(i, it)| (it, Value::Number(i as f64 + 1.0)))
880 .collect();
881 items_node(c, vals, ValueType::Number)
882 }
883 dim::ITEM | dim::ID => {
884 let c = cat(&args[0])?;
885 let vals = items_of(c)?
886 .into_iter()
887 .map(|it| (it, Value::Text(name_of(it))))
888 .collect();
889 items_node(c, vals, ValueType::Text)
890 }
891 dim::ITEMCOUNT => {
892 let c = cat(&args[0])?;
893 let n = items_of(c)?.len();
894 TypedExpr {
895 kind: TypedExprKind::Literal(Value::Number(n as f64)),
896 ty: TypeInfo {
897 value_type: ValueType::Number,
898 dim: Dim::scalar(),
899 },
900 }
901 }
902 dim::PREV | dim::NEXT => {
903 let input = infer(ctx, target, &args[0])?;
904 let c = cat(&args[1])?;
905 if !input.ty.dim.categories.contains(&c) {
906 return Err(CompileError::DimensionMismatch(format!(
907 "{}: the value does not range over {}",
908 def.name,
909 ctx.category_name(c)
910 )));
911 }
912 let n = match args.get(2) {
913 None => 1,
914 Some(Expr::Literal(Value::Number(n))) if n.fract() == 0.0 && *n >= 0.0 => *n as i64,
915 Some(Expr::UnaryOp(UnaryOp::Neg, inner)) if matches!(**inner, Expr::Literal(Value::Number(n)) if n.fract() == 0.0) =>
916 {
917 let Expr::Literal(Value::Number(n)) = **inner else {
918 unreachable!()
919 };
920 -(n as i64)
921 }
922 Some(_) => {
923 return Err(CompileError::Unsupported(format!(
924 "{} takes a whole number of items as its third argument",
925 def.name
926 )))
927 }
928 };
929 let default = match args.get(3) {
930 Some(d) => {
931 let d = infer(ctx, target, d)?;
932 if d.ty.value_type != input.ty.value_type && !is_error_only(&d) {
933 return Err(CompileError::TypeMismatch(format!(
934 "{} default must be {:?}, got {:?}",
935 def.name, input.ty.value_type, d.ty.value_type
936 )));
937 }
938 if !d.ty.dim.is_subset_of(&input.ty.dim) {
939 return Err(CompileError::DimensionMismatch(format!(
940 "{} default ranges over more categories than the value",
941 def.name
942 )));
943 }
944 Some(Box::new(d))
945 }
946 None => None,
947 };
948 let ty = input.ty.clone();
949 TypedExpr {
950 kind: TypedExprKind::Shift {
951 input: Box::new(input),
952 cat: c,
953 offset: if def.id.0 == dim::PREV { -n } else { n },
954 default,
955 },
956 ty,
957 }
958 }
959 dim::RELABEL | dim::SWAP => {
960 let input = infer(ctx, target, &args[0])?;
961 let (a, b) = (cat(&args[1])?, cat(&args[2])?);
962 let has = |c| input.ty.dim.categories.contains(&c);
963 let swap = def.id.0 == dim::SWAP;
964 let (an, bn) = (ctx.category_name(a), ctx.category_name(b));
965 if a == b {
966 return Err(CompileError::DimensionMismatch(format!(
967 "{}: {an} is both categories",
968 def.name
969 )));
970 }
971 if !has(a) || swap && !has(b) {
972 let missing = if has(a) { &bn } else { &an };
973 return Err(CompileError::DimensionMismatch(format!(
974 "{}: the value does not range over {missing}",
975 def.name
976 )));
977 }
978 if !swap && has(b) {
979 return Err(CompileError::DimensionMismatch(format!(
980 "RELABEL: the value already ranges over {bn}; SWAP exchanges two \
981 categories it has"
982 )));
983 }
984 let pairs = |from: CategoryId, to: CategoryId| -> Result<Vec<(ItemId, ItemId)>> {
986 let names: HashMap<String, ItemId> =
987 items_of(to)?.into_iter().map(|i| (name_of(i), i)).collect();
988 Ok(items_of(from)?
989 .into_iter()
990 .filter_map(|i| names.get(&name_of(i)).map(|j| (i, *j)))
991 .collect())
992 };
993 let mut maps = vec![(a, b, pairs(a, b)?)];
994 if swap {
995 maps.push((b, a, pairs(b, a)?));
996 }
997 let mut cats = input.ty.dim.categories.clone();
998 if !swap {
999 cats.retain(|c| *c != a);
1000 cats.push(b);
1001 }
1002 let ty = TypeInfo {
1003 value_type: input.ty.value_type,
1004 dim: Dim::of(cats),
1005 };
1006 TypedExpr {
1007 kind: TypedExprKind::Remap {
1008 input: Box::new(input),
1009 maps,
1010 },
1011 ty,
1012 }
1013 }
1014 _ => return Ok(None),
1015 }))
1016}
1017
1018fn infer_aggregate(
1022 ctx: &CompileContext,
1023 target: MeasureId,
1024 func: FuncId,
1025 args: &[Expr],
1026) -> Result<TypedExpr> {
1027 let def = functions::by_id(func).ok_or(CompileError::UnknownFunction(func))?;
1028 let mut pred = None;
1029 let mut window = None;
1030 let mut into: Vec<CategoryId> = Vec::new();
1031 let mut rest: Vec<&Expr> = Vec::new();
1032 for a in args {
1033 match a {
1034 Expr::Call(id, k) if *id == functions::INTO => {
1035 for c in k {
1036 match c {
1037 Expr::Category(c) if !into.contains(c) => into.push(*c),
1038 _ => return Err(CompileError::Unsupported("malformed INTO".into())),
1039 }
1040 }
1041 if into.is_empty() {
1042 return Err(CompileError::Unsupported("malformed INTO".into()));
1043 }
1044 }
1045 Expr::Call(id, p) if *id == functions::WHERE => {
1046 let t = infer(ctx, target, &p[0])?;
1047 if t.ty.value_type != ValueType::Boolean {
1048 return Err(CompileError::TypeMismatch(format!(
1049 "WHERE needs a Boolean condition, got {:?}",
1050 t.ty.value_type
1051 )));
1052 }
1053 pred = Some(Box::new(t));
1054 }
1055 Expr::Call(id, b) if *id == functions::WINDOW => {
1056 window = Some((bound(&b[0])?, bound(&b[1])?));
1057 }
1058 other => rest.push(other),
1059 }
1060 }
1061 if !def.accepts(rest.len()) {
1062 return Err(CompileError::Unsupported(format!(
1063 "{} expects {}, got {} argument(s)",
1064 def.name,
1065 def.signature(),
1066 rest.len()
1067 )));
1068 }
1069 let mut inputs = Vec::new();
1070 let mut params = Vec::new();
1071 let leading = match def.over {
1075 functions::Over::Leading(min) => Some((
1076 min,
1077 rest.iter()
1078 .enumerate()
1079 .filter(|(i, a)| def.reduced(*i, a))
1080 .count(),
1081 )),
1082 _ => None,
1083 };
1084 let slot = |i: usize| match leading {
1085 Some((min, n)) if i >= n => i - n + min,
1086 Some((min, _)) if i >= min => min - 1,
1087 _ => i,
1088 };
1089 for (i, a) in rest.iter().enumerate() {
1090 let si = slot(i);
1091 if def.reduced(i, a) {
1092 let t = match a {
1093 Expr::Ref(mid, spec) => infer_ref(ctx, *mid, spec)?,
1095 _ => {
1096 return Err(CompileError::Unsupported(format!(
1097 "{} argument {} must be a measure reference",
1098 def.name,
1099 i + 1
1100 )))
1101 }
1102 };
1103 check_arg(def, si, &t)?;
1104 inputs.push(t);
1105 } else {
1106 let t = infer(ctx, target, a)?;
1107 check_arg(def, si, &t)?;
1108 params.push(t);
1109 }
1110 }
1111 let first = inputs.first().ok_or_else(|| {
1112 CompileError::Unsupported(format!("{} needs a measure OVER a category", def.name))
1113 })?;
1114 let over = aggregation_over(ctx, first);
1119 let mut full = first_full_dim(ctx, first);
1120 let mut kept = first.ty.dim.clone();
1121 for t in &inputs[1..] {
1122 if over.is_empty() && first_full_dim(ctx, t) != full {
1125 return Err(CompileError::DimensionMismatch(format!(
1126 "{}: the measures reduced together must range over the same categories, \
1127 but one ranges over {} and another over {}, and neither is reduced",
1128 def.name,
1129 dims_text(ctx, &full),
1130 dims_text(ctx, &first_full_dim(ctx, t)),
1131 )));
1132 }
1133 if aggregation_over(ctx, t) != over {
1134 return Err(CompileError::DimensionMismatch(format!(
1135 "{}: the measures reduced together must reduce the same categories, \
1136 but one is reduced over {} and another over {}",
1137 def.name,
1138 dims_text(ctx, &Dim::of(over.clone())),
1139 dims_text(ctx, &Dim::of(aggregation_over(ctx, t))),
1140 )));
1141 }
1142 full = full.union(&first_full_dim(ctx, t));
1143 kept = kept.union(&t.ty.dim);
1144 }
1145 match (def.kind == Kind::Array, into.first()) {
1148 (true, None) => {
1149 return Err(CompileError::Unsupported(format!(
1150 "{} returns a list: say where it lands, {}(… INTO Category)",
1151 def.name, def.name
1152 )))
1153 }
1154 (false, Some(_)) => {
1155 return Err(CompileError::Unsupported(format!(
1156 "{} returns one value; INTO is for list results",
1157 def.name
1158 )))
1159 }
1160 _ => {}
1161 }
1162 for &k in &into {
1163 if full.categories.contains(&k) {
1164 return Err(CompileError::DimensionMismatch(format!(
1165 "{}: INTO {} must be a category the measure does not range over",
1166 def.name,
1167 ctx.category_name(k)
1168 )));
1169 }
1170 }
1171 let mut kept: Vec<CategoryId> = kept.categories;
1172 kept.extend(into.iter().copied());
1173 if window.is_some() || def.kind == Kind::PerItem {
1174 if over.len() != 1 {
1175 return Err(CompileError::Unsupported(format!(
1176 "{} over a window or per item takes exactly one OVER category",
1177 def.name
1178 )));
1179 }
1180 kept.push(over[0]);
1181 }
1182 let dim = Dim::of(kept);
1183 if let Some(p) = &pred {
1184 if !p.ty.dim.is_subset_of(&full) {
1185 return Err(CompileError::DimensionMismatch(format!(
1186 "{}: the WHERE condition ranges over categories the measure does not",
1187 def.name
1188 )));
1189 }
1190 }
1191 let mut dim = dim;
1196 for p in ¶ms {
1197 if p.ty.dim.categories.iter().any(|c| over.contains(c)) {
1198 return Err(CompileError::DimensionMismatch(format!(
1199 "{}: a parameter must not range over the reduced categories",
1200 def.name
1201 )));
1202 }
1203 dim = dim.union(&p.ty.dim);
1204 }
1205 let value_type = match def.ret {
1206 Ret::Fixed(v) => v,
1207 Ret::Unify => first.ty.value_type,
1208 Ret::Arg(i) => {
1210 let (mut ins, mut ps) = (inputs.iter(), params.iter());
1211 (0..rest.len())
1212 .map(|j| {
1213 if def.reduced(j, rest[j]) {
1214 ins.next()
1215 } else {
1216 ps.next()
1217 }
1218 })
1219 .nth(i)
1220 .flatten()
1221 .map_or(first.ty.value_type, |t| t.ty.value_type)
1222 }
1223 };
1224 Ok(TypedExpr {
1225 kind: TypedExprKind::Aggregate {
1226 func,
1227 inputs,
1228 params,
1229 over,
1230 pred,
1231 window,
1232 into,
1233 },
1234 ty: TypeInfo { value_type, dim },
1235 })
1236}
1237
1238fn first_full_dim(ctx: &CompileContext, t: &TypedExpr) -> Dim {
1241 t.ty.dim.union(&Dim::of(aggregation_over(ctx, t)))
1242}
1243
1244fn bound(e: &Expr) -> Result<Bound> {
1246 use improv_core_model::Value;
1247 match e {
1248 Expr::Literal(Value::Text(t)) if t == "FIRST" => Ok(Bound::First),
1249 Expr::Literal(Value::Text(t)) if t == "LAST" => Ok(Bound::Last),
1250 Expr::Literal(Value::Number(n)) if n.fract() == 0.0 => Ok(Bound::Offset(*n as i64)),
1251 _ => Err(CompileError::Unsupported("malformed window bound".into())),
1252 }
1253}
1254
1255fn check_arg(def: &functions::FnDef, i: usize, a: &TypedExpr) -> Result<()> {
1257 let want = match def.param(i) {
1258 Arg::Any => return Ok(()),
1259 Arg::Number => ValueType::Number,
1260 Arg::Text => ValueType::Text,
1261 Arg::Boolean => ValueType::Boolean,
1262 Arg::Date => ValueType::DateTime,
1263 Arg::DateLike if a.ty.value_type == ValueType::Number => return Ok(()),
1265 Arg::DateLike => ValueType::DateTime,
1266 Arg::Category => {
1267 return Err(CompileError::TypeMismatch(format!(
1268 "{} argument {} must be a category name ({})",
1269 def.name,
1270 i + 1,
1271 def.signature()
1272 )))
1273 }
1274 };
1275 if a.ty.value_type == want || is_error_only(a) {
1277 return Ok(());
1278 }
1279 Err(CompileError::TypeMismatch(format!(
1280 "{} argument {} must be {want:?}, got {:?} ({})",
1281 def.name,
1282 i + 1,
1283 a.ty.value_type,
1284 def.signature()
1285 )))
1286}
1287
1288fn dims_text(ctx: &CompileContext, d: &Dim) -> String {
1290 if d.categories.is_empty() {
1291 return "no category".into();
1292 }
1293 let names: Vec<String> = d.categories.iter().map(|c| ctx.category_name(*c)).collect();
1294 format!("[{}]", names.join(", "))
1295}
1296
1297fn operand_name(ctx: &CompileContext, e: &Expr) -> String {
1300 match e {
1301 Expr::Ref(id, _) => ctx
1302 .measures
1303 .get(id)
1304 .map_or_else(|| "this expression".into(), |m| m.name.0.clone()),
1305 _ => "this expression".into(),
1306 }
1307}
1308
1309fn union_dim<'a>(
1312 ctx: &CompileContext,
1313 what: &str,
1314 dims: impl IntoIterator<Item = &'a Dim>,
1315) -> Result<Dim> {
1316 let mut dim = Dim::scalar();
1317 for d in dims {
1318 if !(dim.is_subset_of(d) || d.is_subset_of(&dim)) {
1319 return Err(CompileError::DimensionMismatch(format!(
1320 "{what} arguments not broadcastable: one ranges over {}, another over {}; \
1321 an argument's categories must contain or be contained in the others'",
1322 dims_text(ctx, &dim),
1323 dims_text(ctx, d)
1324 )));
1325 }
1326 dim = dim.union(d);
1327 }
1328 Ok(dim)
1329}
1330
1331fn is_error_only(e: &TypedExpr) -> bool {
1334 match &e.kind {
1335 TypedExprKind::Call(f, _) => matches!(f.0, 53 | 54),
1336 TypedExprKind::Literal(v) => v.is_error(),
1337 _ => false,
1338 }
1339}
1340
1341fn call(id: u32, args: Vec<Expr>) -> Expr {
1342 Expr::Call(FuncId(id), args)
1343}
1344
1345fn na() -> Expr {
1346 call(53, vec![])
1347}
1348
1349fn value_error() -> Expr {
1350 call(
1351 54,
1352 vec![Expr::Literal(improv_core_model::Value::Text(
1353 "#VALUE!".into(),
1354 ))],
1355 )
1356}
1357
1358fn infer_lazy(
1368 ctx: &CompileContext,
1369 target: MeasureId,
1370 def: &functions::FnDef,
1371 args: &[Expr],
1372) -> Result<TypedExpr> {
1373 if def.id.0 == lazy::IF {
1374 return infer_if(ctx, target, args);
1375 }
1376 if def.id.0 == lazy::COALESCE || def.id.0 == lazy::FILL {
1377 return infer_coalesce(ctx, target, def, args);
1378 }
1379 infer(ctx, target, &desugar_lazy(def.id, args)?)
1380}
1381
1382fn infer_coalesce(
1393 ctx: &CompileContext,
1394 target: MeasureId,
1395 def: &functions::FnDef,
1396 args: &[Expr],
1397) -> Result<TypedExpr> {
1398 let typed: Vec<TypedExpr> = args
1399 .iter()
1400 .map(|a| infer(ctx, target, a))
1401 .collect::<Result<_>>()?;
1402 let mut value_type = None;
1403 for t in typed.iter().filter(|t| !is_error_only(t)) {
1404 match value_type {
1405 None => value_type = Some(t.ty.value_type),
1406 Some(v) if v != t.ty.value_type => {
1407 return Err(CompileError::TypeMismatch(format!(
1408 "{} arguments must have the same type, got {v:?} and {:?}",
1409 def.name, t.ty.value_type
1410 )))
1411 }
1412 Some(_) => {}
1413 }
1414 }
1415 let value_type = value_type.unwrap_or(typed[0].ty.value_type);
1416 let dim = union_dim(ctx, def.name, typed.iter().map(|a| &a.ty.dim))?;
1417 let grid = if def.id.0 == lazy::FILL {
1418 if dim != typed[0].ty.dim {
1419 return Err(CompileError::DimensionMismatch(format!(
1420 "FILL's default ranges over {}, beyond x's {}",
1421 dims_text(ctx, &dim),
1422 dims_text(ctx, &typed[0].ty.dim)
1423 )));
1424 }
1425 if ctx.categories.is_none() && !dim.is_scalar() {
1426 return Err(CompileError::Unsupported(
1427 "FILL needs the model's categories".into(),
1428 ));
1429 }
1430 Some(
1431 dim.categories
1432 .iter()
1433 .map(|c| (*c, ctx.items_of(*c)))
1434 .collect(),
1435 )
1436 } else {
1437 None
1438 };
1439 Ok(TypedExpr {
1440 kind: TypedExprKind::Coalesce { args: typed, grid },
1441 ty: TypeInfo { value_type, dim },
1442 })
1443}
1444
1445pub(crate) fn desugar_lazy(id: FuncId, args: &[Expr]) -> Result<Expr> {
1448 let if_ = |c: Expr, a: Expr, b: Option<Expr>| {
1449 let mut v = vec![c, a];
1450 v.extend(b);
1451 call(lazy::IF, v)
1452 };
1453 let not = |e: Expr| Expr::UnaryOp(UnaryOp::Not, Box::new(e));
1454 Ok(match id.0 {
1455 lazy::IFS => {
1456 if !args.len().is_multiple_of(2) {
1457 return Err(CompileError::Unsupported(
1458 "IFS takes test/value pairs".into(),
1459 ));
1460 }
1461 args.chunks(2)
1462 .rev()
1463 .fold(na(), |acc, p| if_(p[0].clone(), p[1].clone(), Some(acc)))
1464 }
1465 lazy::SWITCH => {
1466 let (e, rest) = args.split_first().expect("SWITCH has an expression");
1467 let (pairs, default) = if rest.len() % 2 == 1 {
1468 (&rest[..rest.len() - 1], rest.last().cloned())
1469 } else {
1470 (rest, None)
1471 };
1472 pairs
1473 .chunks(2)
1474 .rev()
1475 .fold(default.unwrap_or_else(na), |acc, p| {
1476 let test =
1477 Expr::BinaryOp(BinaryOp::Eq, Box::new(e.clone()), Box::new(p[0].clone()));
1478 if_(test, p[1].clone(), Some(acc))
1479 })
1480 }
1481 lazy::CHOOSE => {
1482 let (i, vals) = args.split_first().expect("CHOOSE has an index");
1483 let idx = call(204, vec![i.clone()]); vals.iter()
1485 .enumerate()
1486 .rev()
1487 .fold(value_error(), |acc, (k, v)| {
1488 let n = Expr::Literal(improv_core_model::Value::Number(k as f64 + 1.0));
1489 let test = Expr::BinaryOp(BinaryOp::Eq, Box::new(idx.clone()), Box::new(n));
1490 if_(test, v.clone(), Some(acc))
1491 })
1492 }
1493 lazy::IFERROR | lazy::IFNA => {
1494 let test = call(
1495 if id.0 == lazy::IFERROR { 50 } else { 52 },
1496 vec![args[0].clone()],
1497 );
1498 if_(test, args[1].clone(), Some(args[0].clone()))
1499 }
1500 lazy::CLEARERROR => {
1501 let test = call(50, vec![args[0].clone()]);
1502 match args.get(1) {
1503 Some(alt) => if_(test, alt.clone(), Some(args[0].clone())),
1504 None => if_(not(test), args[0].clone(), None),
1505 }
1506 }
1507 lazy::CLEARZERO => {
1508 let zero = Expr::Literal(improv_core_model::Value::Number(0.0));
1509 let test = Expr::BinaryOp(BinaryOp::Eq, Box::new(args[0].clone()), Box::new(zero));
1510 match args.get(1) {
1511 Some(alt) => if_(test, alt.clone(), Some(args[0].clone())),
1512 None => if_(not(test), args[0].clone(), None),
1513 }
1514 }
1515 _ => return Err(CompileError::UnknownFunction(id)),
1516 })
1517}
1518
1519fn infer_if(ctx: &CompileContext, target: MeasureId, args: &[Expr]) -> Result<TypedExpr> {
1526 let typed: Vec<TypedExpr> = args
1527 .iter()
1528 .map(|a| infer(ctx, target, a))
1529 .collect::<Result<_>>()?;
1530 let (test, branches) = typed.split_first().expect("IF has a test");
1531 if test.ty.value_type != ValueType::Boolean && !is_error_only(test) {
1532 return Err(CompileError::TypeMismatch(format!(
1533 "IF test must be Boolean, got {:?}",
1534 test.ty.value_type
1535 )));
1536 }
1537 let value_type = match branches {
1538 [a] => a.ty.value_type,
1539 [a, b] if is_error_only(a) => b.ty.value_type,
1540 [a, b] if is_error_only(b) || a.ty.value_type == b.ty.value_type => a.ty.value_type,
1541 [a, b] => {
1542 return Err(CompileError::TypeMismatch(format!(
1543 "IF branches must have the same type, got {:?} and {:?}",
1544 a.ty.value_type, b.ty.value_type
1545 )))
1546 }
1547 _ => unreachable!("arity checked by the registry"),
1548 };
1549 let dim = union_dim(ctx, "IF", typed.iter().map(|a| &a.ty.dim))?;
1550 let spans = |b: &TypedExpr| test.ty.dim.union(&b.ty.dim) == dim;
1551 if test.ty.dim != dim && !branches.iter().any(spans) {
1552 return Err(CompileError::DimensionMismatch(format!(
1553 "IF ranges over {}, but neither the test with the then-branch nor with the \
1554 else-branch covers it",
1555 dims_text(ctx, &dim)
1556 )));
1557 }
1558 Ok(TypedExpr {
1559 kind: TypedExprKind::Call(FuncId(lazy::IF), typed),
1560 ty: TypeInfo { value_type, dim },
1561 })
1562}
1563
1564fn infer_ref(ctx: &CompileContext, mid: MeasureId, spec: &DimensionSpec) -> Result<TypedExpr> {
1567 let measure = ctx
1568 .measures
1569 .get(&mid)
1570 .ok_or(CompileError::UnknownMeasure(mid))?;
1571 let result: Vec<_> = measure
1572 .categories
1573 .iter()
1574 .copied()
1575 .filter(|c| spec.by.is_empty() || spec.by.contains(c))
1576 .filter(|c| !spec.except.contains(c))
1577 .filter(|c| !spec.over.contains(c))
1578 .collect();
1579 Ok(TypedExpr {
1580 kind: TypedExprKind::Ref(
1581 mid,
1582 ResolvedDimSpec {
1583 by: spec.by.clone(),
1584 over: spec.over.clone(),
1585 except: spec.except.clone(),
1586 },
1587 ),
1588 ty: TypeInfo {
1589 value_type: measure.value_type,
1590 dim: Dim::of(result),
1591 },
1592 })
1593}
1594
1595fn aggregation_over(ctx: &CompileContext, arg: &TypedExpr) -> Vec<CategoryId> {
1599 match &arg.kind {
1600 TypedExprKind::Ref(mid, spec) => {
1601 let own = ctx.measures.get(mid).map(|m| &m.categories);
1602 spec.over
1603 .iter()
1604 .copied()
1605 .filter(|c| own.is_some_and(|o| o.contains(c)))
1606 .collect()
1607 }
1608 _ => Vec::new(),
1609 }
1610}
1611
1612fn binop_result_type(op: BinaryOp, l: &TypedExpr, r: &TypedExpr) -> Result<ValueType> {
1613 use BinaryOp::*;
1614 let (lt, rt) = (l.ty.value_type, r.ty.value_type);
1615 use ValueType::{DateTime as D, Number as N};
1616 match (op, lt, rt) {
1617 (Add, D, N) | (Add, N, D) | (Sub, D, N) => return Ok(D),
1619 (Sub, D, D) => return Ok(N),
1620 _ => {}
1621 }
1622 match op {
1623 Add | Sub | Mul | Div | Pow => {
1624 require(
1625 lt == ValueType::Number && rt == ValueType::Number,
1626 op,
1627 "Number",
1628 )?;
1629 Ok(ValueType::Number)
1630 }
1631 Concat => Ok(ValueType::Text),
1633 And | Or => {
1634 require(
1635 lt == ValueType::Boolean && rt == ValueType::Boolean,
1636 op,
1637 "Boolean",
1638 )?;
1639 Ok(ValueType::Boolean)
1640 }
1641 Eq | Ne | Lt | Le | Gt | Ge => {
1642 require(lt == rt, op, "matching operand types")?;
1643 Ok(ValueType::Boolean)
1644 }
1645 }
1646}
1647
1648fn require(ok: bool, op: BinaryOp, want: &str) -> Result<()> {
1649 if ok {
1650 Ok(())
1651 } else {
1652 Err(CompileError::TypeMismatch(format!(
1653 "{op:?} requires {want}"
1654 )))
1655 }
1656}
1657
1658#[allow(
1660 clippy::only_used_in_recursion,
1661 reason = "`ctx` kept in the signature for symmetry and future func-registry lookups"
1662)]
1663pub fn build_plan(ctx: &CompileContext, typed: &TypedExpr) -> Result<PlanNode> {
1664 let ty = typed.ty.clone();
1665 let kind = match &typed.kind {
1666 TypedExprKind::Literal(v) => PlanNodeKind::Literal(v.clone()),
1667 TypedExprKind::Ref(mid, _spec) => {
1668 PlanNodeKind::InputMeasure(*mid)
1672 }
1673 TypedExprKind::UnaryOp(op, e) => PlanNodeKind::MapUnary(*op, Box::new(build_plan(ctx, e)?)),
1674 TypedExprKind::BinaryOp(op, l, r) => {
1675 let lp = build_plan(ctx, l)?;
1676 let rp = build_plan(ctx, r)?;
1677 let is_scalar_lit =
1680 |e: &TypedExpr| matches!(e.kind, TypedExprKind::Literal(_)) && e.ty.dim.is_scalar();
1681 if l.ty.dim == r.ty.dim || is_scalar_lit(l) || is_scalar_lit(r) {
1682 PlanNodeKind::MapBinary(*op, Box::new(lp), Box::new(rp))
1683 } else {
1684 let join_keys: Vec<_> =
1689 l.ty.dim
1690 .categories
1691 .iter()
1692 .copied()
1693 .filter(|c| r.ty.dim.categories.contains(c))
1694 .collect();
1695 let (lp_aligned, rp_aligned) = split_join(lp, rp, join_keys, &ty);
1696 PlanNodeKind::MapBinary(*op, Box::new(lp_aligned), Box::new(rp_aligned))
1697 }
1698 }
1699 TypedExprKind::Call(func, args) if func.0 == lazy::IF => {
1700 let mut plans = args
1701 .iter()
1702 .map(|a| build_plan(ctx, a))
1703 .collect::<Result<Vec<_>>>()?
1704 .into_iter();
1705 let test = plans.next().expect("IF has a test");
1706 let then = plans.next().expect("IF has a then-branch");
1707 let otherwise = plans.next();
1708 let spanning = if test.ty.dim == ty.dim {
1711 None
1712 } else if test.ty.dim.union(&then.ty.dim) == ty.dim {
1713 Some(0)
1714 } else {
1715 Some(1)
1716 };
1717 PlanNodeKind::If {
1718 test: Box::new(test),
1719 then: Box::new(then),
1720 otherwise: otherwise.map(Box::new),
1721 spanning,
1722 }
1723 }
1724 TypedExprKind::Call(func, args) if !is_aggregation(*func) => {
1725 let arg_plans = args
1728 .iter()
1729 .map(|a| build_plan(ctx, a))
1730 .collect::<Result<Vec<_>>>()?;
1731 PlanNodeKind::FuncCall {
1732 func: *func,
1733 args: arg_plans,
1734 }
1735 }
1736 TypedExprKind::Call(func, _) => return Err(CompileError::UnknownFunction(*func)),
1737 TypedExprKind::Aggregate {
1738 func,
1739 inputs,
1740 params,
1741 over,
1742 pred,
1743 window,
1744 into,
1745 } => {
1746 let per_item = functions::by_id(*func).is_some_and(|d| d.kind == Kind::PerItem);
1747 let group_by = if !into.is_empty() {
1748 ty.dim
1750 .categories
1751 .iter()
1752 .copied()
1753 .filter(|c| !into.contains(c))
1754 .collect()
1755 } else if window.is_some() || per_item {
1756 ty.dim
1759 .categories
1760 .iter()
1761 .copied()
1762 .filter(|c| !over.contains(c))
1763 .collect()
1764 } else {
1765 ty.dim.categories.clone()
1766 };
1767 PlanNodeKind::Aggregate {
1768 inputs: inputs
1769 .iter()
1770 .map(|a| build_plan(ctx, a))
1771 .collect::<Result<_>>()?,
1772 params: params
1773 .iter()
1774 .map(|a| build_plan(ctx, a))
1775 .collect::<Result<_>>()?,
1776 pred: pred
1777 .as_ref()
1778 .map(|p| build_plan(ctx, p).map(Box::new))
1779 .transpose()?,
1780 group_by,
1781 func: *func,
1782 order: over.iter().map(|c| (*c, ctx.items_of(*c))).collect(),
1783 window: *window,
1784 per_item,
1785 into: into.iter().map(|k| (*k, ctx.items_of(*k))).collect(),
1786 }
1787 }
1788 TypedExprKind::Items(c, vals) => PlanNodeKind::Items(*c, vals.clone()),
1789 TypedExprKind::Coalesce { args, grid } => PlanNodeKind::Coalesce {
1790 args: args
1791 .iter()
1792 .map(|a| build_plan(ctx, a))
1793 .collect::<Result<_>>()?,
1794 grid: grid.clone(),
1795 },
1796 TypedExprKind::Remap { input, maps } => PlanNodeKind::Remap {
1797 input: Box::new(build_plan(ctx, input)?),
1798 maps: maps.clone(),
1799 },
1800 TypedExprKind::Plan(p) => return Ok((**p).clone()),
1801 TypedExprKind::Shift {
1802 input,
1803 cat,
1804 offset,
1805 default,
1806 } => PlanNodeKind::Shift {
1807 input: Box::new(build_plan(ctx, input)?),
1808 cat: *cat,
1809 offset: *offset,
1810 order: ctx.items_of(*cat),
1811 default: default
1812 .as_ref()
1813 .map(|d| build_plan(ctx, d).map(Box::new))
1814 .transpose()?,
1815 },
1816 };
1817 Ok(PlanNode { kind, ty })
1818}
1819
1820fn split_join(
1825 lp: PlanNode,
1826 rp: PlanNode,
1827 join_keys: Vec<improv_core_model::CategoryId>,
1828 union_ty: &TypeInfo,
1829) -> (PlanNode, PlanNode) {
1830 let right_leaf = rp.clone();
1831 let join = PlanNode {
1832 kind: PlanNodeKind::Join {
1833 left: Box::new(lp),
1834 right: Box::new(rp),
1835 join_keys,
1836 },
1837 ty: union_ty.clone(),
1838 };
1839 (join, right_leaf)
1840}
1841
1842#[cfg(test)]
1843#[cfg_attr(coverage_nightly, coverage(off))]
1844mod tests {
1845 use super::*;
1846 use improv_core_model::{CategoryId, DimensionSpec, ItemId, MeasureKind, Name, Value};
1847
1848 const TIME: CategoryId = CategoryId(1);
1849 const PRODUCT: CategoryId = CategoryId(2);
1850 const REGION: CategoryId = CategoryId(3);
1851 const PRICE: MeasureId = MeasureId(100); const QTY: MeasureId = MeasureId(101); const HEADCOUNT: MeasureId = MeasureId(102); fn num_measure(id: MeasureId, name: &str, cats: Vec<CategoryId>) -> Measure {
1856 Measure {
1857 id,
1858 name: Name(name.into()),
1859 value_type: ValueType::Number,
1860 categories: cats,
1861 kind: MeasureKind::Input,
1862 description: None,
1863 }
1864 }
1865
1866 fn fixture() -> HashMap<MeasureId, Measure> {
1867 let mut m = HashMap::new();
1868 m.insert(PRICE, num_measure(PRICE, "Price", vec![PRODUCT]));
1869 m.insert(QTY, num_measure(QTY, "Quantity", vec![TIME, PRODUCT]));
1870 m.insert(HEADCOUNT, num_measure(HEADCOUNT, "Headcount", vec![REGION]));
1871 m
1872 }
1873
1874 fn refr(id: MeasureId) -> Expr {
1875 Expr::Ref(id, DimensionSpec::default())
1876 }
1877
1878 #[test]
1880 fn revenue_mul_infers_union_dim_and_joins() {
1881 let measures = fixture();
1882 let ctx = CompileContext::new(&measures);
1883 let expr = Expr::BinaryOp(BinaryOp::Mul, Box::new(refr(PRICE)), Box::new(refr(QTY)));
1884
1885 let typed = infer(&ctx, MeasureId(200), &expr).expect("infer");
1886 assert_eq!(typed.ty.value_type, ValueType::Number);
1887 assert_eq!(typed.ty.dim, Dim::of(vec![TIME, PRODUCT]), "union dim");
1888
1889 let plan = build_plan(&ctx, &typed).expect("plan");
1890 match plan.kind {
1891 PlanNodeKind::MapBinary(BinaryOp::Mul, left, _right) => match left.kind {
1892 PlanNodeKind::Join { join_keys, .. } => {
1893 assert_eq!(join_keys, vec![PRODUCT], "join on shared Product");
1894 }
1895 other => panic!("expected Join feeding MapBinary, got {other:?}"),
1896 },
1897 other => panic!("expected MapBinary(Mul), got {other:?}"),
1898 }
1899 }
1900
1901 #[test]
1903 fn disjoint_dims_error() {
1904 let measures = fixture();
1905 let ctx = CompileContext::new(&measures);
1906 let expr = Expr::BinaryOp(
1907 BinaryOp::Add,
1908 Box::new(refr(PRICE)), Box::new(refr(HEADCOUNT)), );
1911 let err = infer(&ctx, MeasureId(200), &expr).unwrap_err();
1912 assert!(matches!(err, CompileError::DimensionMismatch(_)), "{err:?}");
1913 }
1914
1915 #[test]
1917 fn sum_over_time_reduces_dim() {
1918 let measures = fixture();
1919 let ctx = CompileContext::new(&measures);
1920 let arg = Expr::Ref(
1922 QTY,
1923 DimensionSpec {
1924 over: vec![TIME],
1925 ..Default::default()
1926 },
1927 );
1928 let expr = Expr::Call(FuncId(1), vec![arg]); let typed = infer(&ctx, MeasureId(200), &expr).expect("infer");
1931 assert_eq!(typed.ty.dim, Dim::of(vec![PRODUCT]), "Time collapsed");
1932
1933 let plan = build_plan(&ctx, &typed).expect("plan");
1934 match plan.kind {
1935 PlanNodeKind::Aggregate { group_by, func, .. } => {
1936 assert_eq!(group_by, vec![PRODUCT]);
1937 assert_eq!(func, FuncId(1));
1938 }
1939 other => panic!("expected Aggregate, got {other:?}"),
1940 }
1941 }
1942
1943 #[test]
1947 fn by_list_must_not_drop_categories_outside_an_aggregation() {
1948 let measures = fixture();
1949 let ctx = CompileContext::new(&measures);
1950 let by = |cats: Vec<CategoryId>| {
1951 Expr::Ref(
1952 QTY,
1953 DimensionSpec {
1954 by: cats,
1955 ..Default::default()
1956 },
1957 )
1958 };
1959 let err = infer(&ctx, MeasureId(200), &by(vec![PRODUCT])).unwrap_err();
1960 match err {
1961 CompileError::DimensionMismatch(msg) => {
1962 assert!(msg.contains("SUM(Quantity OVER"), "{msg}")
1963 }
1964 other => panic!("expected DimensionMismatch, got {other:?}"),
1965 }
1966 let full = infer(&ctx, MeasureId(200), &by(vec![TIME, PRODUCT])).expect("full list");
1967 assert_eq!(full.ty.dim, Dim::of(vec![TIME, PRODUCT]));
1968 let agg = Expr::Call(
1969 FuncId(1),
1970 vec![Expr::Ref(
1971 QTY,
1972 DimensionSpec {
1973 by: vec![PRODUCT],
1974 over: vec![TIME],
1975 except: vec![],
1976 },
1977 )],
1978 );
1979 let typed = infer(&ctx, MeasureId(200), &agg).expect("aggregated");
1980 assert_eq!(typed.ty.dim, Dim::of(vec![PRODUCT]));
1981 }
1982
1983 #[test]
1985 fn unknown_measure_errors() {
1986 let measures = fixture();
1987 let ctx = CompileContext::new(&measures);
1988 let err = infer(&ctx, MeasureId(200), &refr(MeasureId(999))).unwrap_err();
1989 assert_eq!(err, CompileError::UnknownMeasure(MeasureId(999)));
1990 }
1991
1992 #[test]
1995 fn literal_and_unary_and_type_errors() {
1996 let measures = fixture();
1997 let ctx = CompileContext::new(&measures);
1998
1999 let lit = infer(&ctx, MeasureId(200), &Expr::Literal(Value::Number(3.0))).unwrap();
2000 assert_eq!(lit.ty.value_type, ValueType::Number);
2001 assert!(lit.ty.dim.is_scalar());
2002 assert!(matches!(
2003 build_plan(&ctx, &lit).unwrap().kind,
2004 PlanNodeKind::Literal(Value::Number(_))
2005 ));
2006
2007 let neg = Expr::UnaryOp(UnaryOp::Neg, Box::new(refr(PRICE)));
2008 let t = infer(&ctx, MeasureId(200), &neg).unwrap();
2009 assert_eq!(t.ty.dim, Dim::of(vec![PRODUCT]));
2010 assert!(matches!(
2011 build_plan(&ctx, &t).unwrap().kind,
2012 PlanNodeKind::MapUnary(UnaryOp::Neg, _)
2013 ));
2014
2015 let bad = Expr::UnaryOp(UnaryOp::Not, Box::new(refr(PRICE)));
2017 assert!(matches!(
2018 infer(&ctx, MeasureId(200), &bad),
2019 Err(CompileError::TypeMismatch(_))
2020 ));
2021
2022 let uf = Expr::Call(FuncId(999), vec![refr(PRICE)]);
2024 assert_eq!(
2025 infer(&ctx, MeasureId(200), &uf),
2026 Err(CompileError::UnknownFunction(FuncId(999)))
2027 );
2028
2029 let f = Formula::new(Expr::BinaryOp(
2031 BinaryOp::Mul,
2032 Box::new(refr(PRICE)),
2033 Box::new(refr(QTY)),
2034 ));
2035 let plan = compile_formula(&ctx, MeasureId(200), &f).unwrap();
2036 assert!(matches!(
2037 plan.kind,
2038 PlanNodeKind::MapBinary(BinaryOp::Mul, ..)
2039 ));
2040 }
2041
2042 #[test]
2045 fn scalar_abs_infers_and_lowers() {
2046 let measures = fixture();
2047 let ctx = CompileContext::new(&measures);
2048 let expr = Expr::Call(FuncId(10), vec![refr(PRICE)]); let typed = infer(&ctx, MeasureId(200), &expr).expect("infer");
2051 assert_eq!(typed.ty.value_type, ValueType::Number);
2052 assert_eq!(typed.ty.dim, Dim::of(vec![PRODUCT]), "dim follows the arg");
2053
2054 let plan = build_plan(&ctx, &typed).expect("plan");
2055 match plan.kind {
2056 PlanNodeKind::FuncCall { func, args } => {
2057 assert_eq!(func, FuncId(10));
2058 assert_eq!(args.len(), 1);
2059 assert!(matches!(args[0].kind, PlanNodeKind::InputMeasure(PRICE)));
2060 }
2061 other => panic!("expected FuncCall, got {other:?}"),
2062 }
2063 }
2064
2065 #[test]
2067 fn scalar_min2_union_dim_and_errors() {
2068 let measures = fixture();
2069 let ctx = CompileContext::new(&measures);
2070 let ok = Expr::Call(FuncId(20), vec![refr(PRICE), refr(QTY)]);
2072 let typed = infer(&ctx, MeasureId(200), &ok).expect("infer");
2073 assert_eq!(typed.ty.dim, Dim::of(vec![TIME, PRODUCT]), "union dim");
2074
2075 let bad_arity = Expr::Call(FuncId(10), vec![refr(PRICE), refr(QTY)]);
2077 assert!(matches!(
2078 infer(&ctx, MeasureId(200), &bad_arity),
2079 Err(CompileError::Unsupported(_))
2080 ));
2081
2082 let cmp = Expr::BinaryOp(
2084 BinaryOp::Gt,
2085 Box::new(refr(PRICE)),
2086 Box::new(Expr::Literal(Value::Number(1.0))),
2087 );
2088 let bad_type = Expr::Call(FuncId(10), vec![cmp]);
2089 assert!(matches!(
2090 infer(&ctx, MeasureId(200), &bad_type),
2091 Err(CompileError::TypeMismatch(_))
2092 ));
2093
2094 let unknown = Expr::Call(FuncId(999), vec![refr(PRICE)]);
2096 assert_eq!(
2097 infer(&ctx, MeasureId(200), &unknown),
2098 Err(CompileError::UnknownFunction(FuncId(999)))
2099 );
2100 }
2101
2102 fn with_flag() -> (HashMap<MeasureId, Measure>, MeasureId) {
2103 let mut measures = fixture();
2104 let flag = MeasureId(103);
2105 measures.insert(
2106 flag,
2107 Measure {
2108 value_type: ValueType::Boolean,
2109 ..num_measure(flag, "Flag", vec![PRODUCT])
2110 },
2111 );
2112 (measures, flag)
2113 }
2114
2115 fn bin(op: BinaryOp, l: Expr, r: Expr) -> Expr {
2116 Expr::BinaryOp(op, Box::new(l), Box::new(r))
2117 }
2118
2119 fn model_with_items() -> improv_core_model::Model {
2121 let mut m = improv_core_model::Model::new();
2122 m.add_category(TIME, "Time");
2123 m.add_category(PRODUCT, "Product");
2124 m.add_category(REGION, "Region");
2125 m.add_item(ItemId(10), TIME, "2025");
2126 m.add_item(ItemId(11), TIME, "2026");
2127 m.add_item(ItemId(20), PRODUCT, "A");
2128 for (id, meas) in fixture() {
2129 m.measures.insert(id, meas);
2130 }
2131 m
2132 }
2133
2134 fn parse(m: &improv_core_model::Model, text: &str) -> Expr {
2135 improv_core_model::parse_expr(m, text).unwrap().expr
2136 }
2137
2138 #[test]
2140 fn dimensional_and_aggregate_rules() {
2141 let m = model_with_items();
2142 let ctx = CompileContext::for_model(&m);
2143 let ty = |t: &str| infer(&ctx, MeasureId(200), &parse(&m, t));
2144 assert_eq!(ty("POSITION(Time)").unwrap().ty.dim, Dim::of(vec![TIME]));
2145 assert_eq!(ty("@ID(Time)").unwrap().ty.value_type, ValueType::Text);
2146 assert_eq!(ty("@ITEMCOUNT(Time)").unwrap().ty.dim, Dim::scalar());
2147 assert!(matches!(
2149 ty("PREV(Price, Time)"),
2150 Err(CompileError::DimensionMismatch(_))
2151 ));
2152 assert!(matches!(
2153 ty("PREV(Quantity, Time, 1.5)"),
2154 Err(CompileError::Unsupported(_))
2155 ));
2156 assert!(matches!(
2157 ty("PREV(Quantity, Time, Price)"),
2158 Err(CompileError::Unsupported(_))
2159 ));
2160 assert!(ty("PREV(Quantity, Time, -1)").is_ok());
2161 assert!(matches!(
2162 ty("PREV(Quantity, Time, 1, \"x\")"),
2163 Err(CompileError::TypeMismatch(_))
2164 ));
2165 assert!(ty("PREV(Quantity, Time, 1, NA())").is_ok());
2166 assert!(matches!(
2167 ty("PREV(Price + Quantity * 0, Time, 1, Headcount)"),
2168 Err(CompileError::DimensionMismatch(_))
2169 ));
2170 assert!(matches!(
2172 infer(&ctx, MeasureId(200), &Expr::Category(TIME)),
2173 Err(CompileError::TypeMismatch(_))
2174 ));
2175 let bare = CompileContext::new(&m.measures);
2177 assert!(matches!(
2178 infer(&bare, MeasureId(200), &parse(&m, "POSITION(Time)")),
2179 Err(CompileError::Unsupported(_))
2180 ));
2181 assert!(matches!(
2185 ty("SUM(Quantity OVER Time WHERE Quantity)"),
2186 Err(CompileError::TypeMismatch(_))
2187 ));
2188 assert!(matches!(
2189 ty("SUM(Quantity OVER Time WHERE Headcount > 1)"),
2190 Err(CompileError::DimensionMismatch(_))
2191 ));
2192 let w = Expr::Call(
2193 FuncId(1),
2194 vec![
2195 Expr::Ref(
2196 QTY,
2197 DimensionSpec {
2198 over: vec![TIME, PRODUCT],
2199 ..Default::default()
2200 },
2201 ),
2202 Expr::Call(
2203 functions::WINDOW,
2204 vec![Expr::Literal(improv_core_model::Value::Number(0.0)); 2],
2205 ),
2206 ],
2207 );
2208 assert!(matches!(
2209 infer(&ctx, MeasureId(200), &w),
2210 Err(CompileError::Unsupported(_))
2211 ));
2212 let bad_bound = Expr::Call(
2213 FuncId(1),
2214 vec![
2215 Expr::Ref(
2216 QTY,
2217 DimensionSpec {
2218 over: vec![TIME],
2219 ..Default::default()
2220 },
2221 ),
2222 Expr::Call(functions::WINDOW, vec![lit(0.5), lit(0.0)]),
2223 ],
2224 );
2225 assert!(matches!(
2226 infer(&ctx, MeasureId(200), &bad_bound),
2227 Err(CompileError::Unsupported(_))
2228 ));
2229 assert!(matches!(
2230 ty("TEXTJOIN(@ITEM(Time), TRUE, Quantity OVER Time)"),
2231 Err(CompileError::DimensionMismatch(_))
2232 ));
2233 assert!(matches!(
2235 ty("TEXTJOIN(1, TRUE, Quantity OVER Time)"),
2236 Err(CompileError::TypeMismatch(_))
2237 ));
2238 let extra = Expr::Call(
2241 FuncId(906),
2242 vec![
2243 Expr::Ref(
2244 QTY,
2245 DimensionSpec {
2246 over: vec![TIME],
2247 ..Default::default()
2248 },
2249 ),
2250 lit(1.0),
2251 ],
2252 );
2253 assert!(matches!(
2254 infer(&ctx, MeasureId(200), &extra),
2255 Err(CompileError::Unsupported(_))
2256 ));
2257 let not_ref = Expr::Call(FuncId(906), vec![lit(1.0)]);
2258 assert!(matches!(
2259 infer(&ctx, MeasureId(200), ¬_ref),
2260 Err(CompileError::Unsupported(_))
2261 ));
2262 let two = Expr::Call(
2264 FuncId(906),
2265 vec![Expr::Ref(
2266 QTY,
2267 DimensionSpec {
2268 over: vec![TIME],
2269 ..Default::default()
2270 },
2271 )],
2272 );
2273 assert!(infer(&ctx, MeasureId(200), &two).is_ok());
2274 let t = ty("SUM(Quantity OVER Time[FIRST..LAST] WHERE Quantity > 0)").unwrap();
2276 assert_eq!(t.ty.dim, Dim::of(vec![TIME, PRODUCT]));
2277 let plan = build_plan(&ctx, &t).unwrap();
2278 assert!(matches!(
2279 plan.kind,
2280 PlanNodeKind::Aggregate {
2281 window: Some(_),
2282 pred: Some(_),
2283 ..
2284 }
2285 ));
2286 let t = ty("Quantity - PREV(Quantity, Time, 1, 0)").unwrap();
2287 assert!(build_plan(&ctx, &t).is_ok());
2288 }
2289
2290 #[test]
2293 fn recurrence_rules() {
2294 let mut m = model_with_items();
2295 let bal = MeasureId(300);
2296 m.add_measure(num_measure(bal, "Bal", vec![TIME, PRODUCT]));
2297 m.add_measure(num_measure(MeasureId(301), "Flag", vec![TIME, PRODUCT]));
2298 m.measures.get_mut(&MeasureId(301)).unwrap().value_type = ValueType::Boolean;
2299 let compile = |m: &improv_core_model::Model, t: &str| {
2300 let e = parse(m, t);
2301 compile_formula(&CompileContext::for_model(m), bal, &Formula::new(e))
2302 };
2303 let ok = compile(&m, "PREV(Bal, Time, 1, Price) + Quantity").unwrap();
2304 assert!(matches!(ok.kind, PlanNodeKind::Recurrence { .. }));
2305 assert!(compile(&m, "PREV(Bal, Time) + PREV(Bal, Time, 2, 0) + Quantity").is_ok());
2306 for (text, why) in [
2307 ("Bal + 1", "only PREV"),
2308 ("PREV(Bal, Time, 0) + Quantity", "whole number of items"),
2309 (
2310 "PREV(Bal, Time, 1, Bal) + Quantity",
2311 "default must not refer",
2312 ),
2313 (
2314 "PREV(Bal, Time) + PREV(Bal, Product) + Quantity",
2315 "same category",
2316 ),
2317 (
2318 "PREV(Bal, Time) + SUM(Quantity OVER Time)",
2319 "must be scalar",
2320 ),
2321 (
2322 "PREV(Bal, Time, 1, SUM(Price OVER Product)) + Quantity",
2323 "default must be scalar",
2324 ),
2325 ("PREV(Bal, Time) + Price", "together range over"),
2326 ("PREV(Bal, Time) + Headcount", "dimension mismatch"),
2327 ] {
2328 let e = compile(&m, text).unwrap_err().to_string();
2329 assert!(e.contains(why), "{text}: {e}");
2330 }
2331 let e = compile(&m, "IF(Flag, PREV(Bal, Time), 1 > 0)").unwrap_err();
2332 assert!(matches!(e, CompileError::TypeMismatch(_)), "{e:?}");
2333 let mut r = m.clone();
2335 r.measures.get_mut(&bal).unwrap().categories = vec![TIME];
2336 r.measures.get_mut(&QTY).unwrap();
2337 let e = compile_formula(
2338 &CompileContext::for_model(&r),
2339 bal,
2340 &Formula::new(parse(&r, "PREV(Bal, Product) + 1")),
2341 )
2342 .unwrap_err()
2343 .to_string();
2344 assert!(e.contains("own categories"), "{e}");
2345 let f = Formula::new(parse(&m, "PREV(Bal, Time, 1, 0) + Quantity"));
2348 let mut fresh = m.clone();
2349 fresh.measures.remove(&bal);
2350 let pm = fresh.with_provisional(bal, "Bal");
2351 let f2 = Formula::new(parse(&pm, "PREV(Bal, Time, 1, 0) + Quantity"));
2352 assert_eq!(
2353 derived_shape(&fresh, bal, "Bal", &f2).unwrap(),
2354 (ValueType::Number, vec![TIME, PRODUCT])
2355 );
2356 let g = Formula::new(parse(&m, "@ITEM(Time)"));
2357 assert_eq!(
2358 derived_shape(&m, MeasureId(400), "G", &g).unwrap(),
2359 (ValueType::Text, vec![TIME])
2360 );
2361 let _ = f;
2362 let bad = Formula::new(parse(&pm, "PREV(Bal, Time, 1, \"x\") & Bal"));
2363 assert!(derived_shape(&fresh, bal, "Bal", &bad).is_err());
2364 }
2365
2366 #[test]
2369 fn dimension_errors_are_readable() {
2370 let m = model_with_items();
2371 let ctx = CompileContext::for_model(&m);
2372 for (text, want) in [
2373 ("Headcount + Price", "share no category"),
2374 (
2375 "ROUND(Headcount, Price)",
2376 "not broadcastable: one ranges over",
2377 ),
2378 ("CORREL(Price, Headcount OVER Time)", "but one ranges over"),
2379 ] {
2380 let e = infer(&ctx, MeasureId(200), &parse(&m, text))
2381 .unwrap_err()
2382 .to_string();
2383 assert!(e.contains(want), "{text}: {e}");
2384 assert!(!e.contains("CategoryId("), "{text}: {e}");
2385 }
2386 }
2387
2388 #[test]
2390 fn fold_rules() {
2391 let m = model_with_items();
2392 let ctx = CompileContext::for_model(&m);
2393 let ty = |t: &str| infer(&ctx, MeasureId(200), &parse(&m, t));
2394 assert_eq!(
2395 ty("SCAN(0, Quantity OVER Time, LAMBDA(a, v, a + v))")
2396 .unwrap()
2397 .ty
2398 .dim,
2399 Dim::of(vec![TIME, PRODUCT])
2400 );
2401 assert_eq!(
2402 ty("REDUCE(0, Quantity OVER Time, LAMBDA(a, v, a + v))")
2403 .unwrap()
2404 .ty
2405 .dim,
2406 Dim::of(vec![PRODUCT])
2407 );
2408 for (text, why) in [
2409 (
2410 "SCAN(0, Price OVER Time, LAMBDA(a, v, a + v))",
2411 "does not range over",
2412 ),
2413 (
2414 "SCAN(\"x\", Quantity OVER Time, LAMBDA(a, v, v))",
2415 "starts as",
2416 ),
2417 (
2418 "SCAN(0, Quantity OVER Time, LAMBDA(a, v, a + SUM(Price OVER Product)))",
2419 "must be scalar",
2420 ),
2421 (
2422 "SCAN(SUM(Price OVER Product), Quantity OVER Time, LAMBDA(a, v, a))",
2423 "initial value must be scalar",
2424 ),
2425 (
2426 "SCAN(0, Quantity OVER Time, LAMBDA(a, v, a + Headcount))",
2427 "dimension mismatch",
2428 ),
2429 ] {
2430 let e = ty(text).unwrap_err().to_string();
2431 assert!(e.contains(why), "{text}: {e}");
2432 }
2433 let bad = Expr::Call(FuncId(911), vec![lit(0.0)]);
2434 assert!(matches!(
2435 infer(&ctx, MeasureId(200), &bad),
2436 Err(CompileError::Unsupported(_))
2437 ));
2438 }
2439
2440 #[test]
2443 fn date_arithmetic_types() {
2444 let mut measures = fixture();
2445 let when = MeasureId(110);
2446 measures.insert(
2447 when,
2448 Measure {
2449 value_type: ValueType::DateTime,
2450 ..num_measure(when, "When", vec![PRODUCT])
2451 },
2452 );
2453 let ctx = CompileContext::new(&measures);
2454 let ty = |e: &Expr| infer(&ctx, MeasureId(200), e).map(|t| t.ty.value_type);
2455 assert_eq!(
2456 ty(&bin(BinaryOp::Add, refr(when), lit(1.0))),
2457 Ok(ValueType::DateTime)
2458 );
2459 assert_eq!(
2460 ty(&bin(BinaryOp::Add, lit(1.0), refr(when))),
2461 Ok(ValueType::DateTime)
2462 );
2463 assert_eq!(
2464 ty(&bin(BinaryOp::Sub, refr(when), lit(1.0))),
2465 Ok(ValueType::DateTime)
2466 );
2467 assert_eq!(
2468 ty(&bin(BinaryOp::Sub, refr(when), refr(when))),
2469 Ok(ValueType::Number)
2470 );
2471 assert!(ty(&bin(BinaryOp::Mul, refr(when), lit(2.0))).is_err());
2472 assert!(ty(&bin(BinaryOp::Sub, lit(1.0), refr(when))).is_err());
2473 }
2474
2475 #[test]
2477 fn if_family_type_and_dimension_rules() {
2478 let (measures, flag) = with_flag();
2479 let ctx = CompileContext::new(&measures);
2480 let infer_ = |e: &Expr| infer(&ctx, MeasureId(200), e);
2481 let call = |id: u32, args: Vec<Expr>| Expr::Call(FuncId(id), args);
2482 let na = || call(53, vec![]);
2483 let text = |s: &str| Expr::Literal(Value::Text(s.into()));
2484 assert!(matches!(
2486 infer_(&call(30, vec![refr(PRICE), lit(1.0)])),
2487 Err(CompileError::TypeMismatch(_))
2488 ));
2489 assert!(infer_(&call(30, vec![na(), lit(1.0)])).is_ok());
2490 assert!(matches!(
2492 infer_(&call(30, vec![refr(flag), lit(1.0), text("x")])),
2493 Err(CompileError::TypeMismatch(_))
2494 ));
2495 let t = infer_(&call(30, vec![refr(flag), na(), text("x")])).unwrap();
2496 assert_eq!(t.ty.value_type, ValueType::Text);
2497 let t = infer_(&call(30, vec![refr(flag), text("x"), na()])).unwrap();
2498 assert_eq!(t.ty.value_type, ValueType::Text);
2499 let t = infer_(&call(30, vec![refr(flag), lit(0.0), refr(QTY)])).unwrap();
2502 assert_eq!(t.ty.dim, Dim::of(vec![TIME, PRODUCT]));
2503 let plan = build_plan(&ctx, &t).unwrap();
2504 assert!(matches!(
2505 plan.kind,
2506 PlanNodeKind::If {
2507 spanning: Some(1),
2508 ..
2509 }
2510 ));
2511 let t = infer_(&call(30, vec![refr(flag), refr(QTY), lit(0.0)])).unwrap();
2512 assert!(matches!(
2513 build_plan(&ctx, &t).unwrap().kind,
2514 PlanNodeKind::If {
2515 spanning: Some(0),
2516 ..
2517 }
2518 ));
2519 assert!(matches!(
2520 infer_(&call(30, vec![refr(flag), refr(HEADCOUNT), lit(0.0)])),
2521 Err(CompileError::DimensionMismatch(_))
2522 ));
2523 let mut m2 = measures.clone();
2526 let wide = MeasureId(105);
2527 m2.insert(wide, num_measure(wide, "W", vec![TIME, PRODUCT, REGION]));
2528 let tflag = MeasureId(106);
2529 m2.insert(
2530 tflag,
2531 Measure {
2532 value_type: ValueType::Boolean,
2533 ..num_measure(tflag, "TF", vec![TIME])
2534 },
2535 );
2536 let ctx2 = CompileContext::new(&m2);
2537 let e = call(30, vec![refr(tflag), refr(QTY), refr(wide)]);
2538 let t = infer(&ctx2, MeasureId(200), &e).unwrap();
2539 assert!(matches!(
2540 build_plan(&ctx2, &t).unwrap().kind,
2541 PlanNodeKind::If {
2542 spanning: Some(1),
2543 ..
2544 }
2545 ));
2546 assert!(matches!(
2548 infer_(&call(31, vec![refr(flag), lit(1.0), refr(flag)])),
2549 Err(CompileError::Unsupported(_))
2550 ));
2551 assert!(infer_(&call(32, vec![refr(PRICE), lit(1.0), text("a"), text("z")])).is_ok());
2552 assert!(infer_(&call(32, vec![refr(PRICE), lit(1.0), text("a")])).is_ok());
2553 assert!(matches!(
2554 infer_(&call(312, vec![refr(PRICE)])),
2555 Err(CompileError::Unsupported(_))
2556 ));
2557 assert_eq!(
2558 infer_(&call(999, vec![])),
2559 Err(CompileError::UnknownFunction(FuncId(999)))
2560 );
2561 assert!(matches!(
2563 infer_(&call(10, vec![text("x")])),
2564 Err(CompileError::TypeMismatch(_))
2565 ));
2566 assert!(infer_(&call(10, vec![na()])).is_ok());
2567 assert!(matches!(
2568 infer_(&call(22, vec![refr(PRICE), refr(HEADCOUNT)])),
2569 Err(CompileError::DimensionMismatch(_))
2570 ));
2571 assert_eq!(
2573 desugar_lazy(FuncId(10), &[]),
2574 Err(CompileError::UnknownFunction(FuncId(10)))
2575 );
2576 assert!(matches!(
2578 infer_(&Expr::UnaryOp(UnaryOp::Not, Box::new(refr(PRICE)))),
2579 Err(CompileError::TypeMismatch(_))
2580 ));
2581 }
2582
2583 fn lit(n: f64) -> Expr {
2584 Expr::Literal(Value::Number(n))
2585 }
2586
2587 #[test]
2588 fn infer_rejects_ill_typed_and_unbroadcastable_exprs() {
2589 let (measures, flag) = with_flag();
2590 let ctx = CompileContext::new(&measures);
2591 let err = |e: &Expr| infer(&ctx, MeasureId(200), e).unwrap_err();
2592 let msg = |e: &Expr| err(e).to_string();
2593
2594 let e = Expr::Literal(Value::Error(improv_core_model::ValueError::new(
2595 improv_core_model::ValueErrorKind::DivisionByZero,
2596 )));
2597 assert!(msg(&e).contains("error literal"));
2598 assert!(msg(&bin(BinaryOp::Add, refr(flag), lit(1.0))).contains("requires Number"));
2600 assert!(msg(&bin(BinaryOp::And, refr(flag), refr(PRICE))).contains("requires Boolean"));
2601 assert!(msg(&bin(BinaryOp::Or, refr(PRICE), refr(flag))).contains("requires Boolean"));
2602 assert_eq!(
2604 err(&Expr::Call(FuncId(10), vec![refr(MeasureId(999))])),
2605 CompileError::UnknownMeasure(MeasureId(999))
2606 );
2607 assert!(matches!(
2608 err(&Expr::Call(FuncId(20), vec![refr(PRICE), refr(HEADCOUNT)])),
2609 CompileError::DimensionMismatch(_)
2610 ));
2611 assert!(msg(&Expr::Call(FuncId(1), vec![refr(PRICE), refr(PRICE)])).contains("expects"));
2613 assert!(msg(&Expr::Call(FuncId(1), vec![refr(flag)])).contains("must be Number"));
2614 assert!(msg(&Expr::Call(FuncId(22), vec![refr(flag)]))
2616 .contains("SUM argument 1 must be Number"));
2617 }
2618
2619 #[test]
2620 fn infer_accepts_logic_broadcast_calls_and_folds_literals() {
2621 let (measures, flag) = with_flag();
2622 let ctx = CompileContext::new(&measures);
2623 let ok = |e: &Expr| infer(&ctx, MeasureId(200), e).unwrap();
2624
2625 let t = ok(&bin(BinaryOp::And, refr(flag), refr(flag)));
2626 assert_eq!(t.ty.value_type, ValueType::Boolean);
2627 let t = ok(&Expr::Call(FuncId(20), vec![refr(QTY), refr(PRICE)]));
2629 assert_eq!(t.ty.dim, Dim::of(vec![TIME, PRODUCT]));
2630 let e = infer(
2632 &ctx,
2633 MeasureId(200),
2634 &Expr::Call(FuncId(1), vec![bin(BinaryOp::Mul, refr(PRICE), lit(2.0))]),
2635 );
2636 assert!(matches!(e, Err(CompileError::Unsupported(_))));
2637 let t = infer(
2639 &ctx,
2640 MeasureId(200),
2641 &Expr::Call(FuncId(1), vec![refr(PRICE)]),
2642 )
2643 .unwrap();
2644 assert_eq!(t.ty.dim, Dim::of(vec![PRODUCT]));
2645 let typed = ok(&bin(BinaryOp::Mul, lit(2.0), refr(QTY)));
2647 match build_plan(&ctx, &typed).unwrap().kind {
2648 PlanNodeKind::MapBinary(BinaryOp::Mul, l, r) => {
2649 assert!(matches!(l.kind, PlanNodeKind::Literal(_)));
2650 assert_eq!(r.kind, PlanNodeKind::InputMeasure(QTY));
2651 }
2652 other => panic!("expected MapBinary, got {other:?}"),
2653 }
2654 }
2655}