rtlola_frontend/
lowering.rs

1use std::collections::{BTreeSet, HashMap};
2use std::iter::zip;
3
4use itertools::Itertools;
5use num::ToPrimitive;
6use rtlola_hir::hir::{
7    ActivationCondition, Aggregation, ArithLogOp, ConcretePacingType, ConcreteValueType, Constant,
8    DepAnaTrait, DiscreteAggr, Expression, ExpressionKind, FnExprKind, Inlined,
9    InstanceAggregation, InstanceSelection, Literal, MemBoundTrait, Offset, OrderedTrait, Origin,
10    OutputKind, SlidingAggr, StreamAccessKind, StreamReference, TypedTrait, WidenExprKind, Window,
11    WindowReference,
12};
13use rtlola_hir::{CompleteMode, RtLolaHir};
14use rtlola_parser::ast::{InstanceOperation, Tag, WindowOperation};
15use rtlola_reporting::Span;
16
17use crate::mir::{self, Close, Eval, EvalClause, Mir, PacingLocality, PacingType, Spawn, Trigger};
18
19impl Mir {
20    /// Generates an Mir from a complete Hir.
21    pub fn from_hir(hir: RtLolaHir<CompleteMode>) -> Mir {
22        let sr_map: HashMap<StreamReference, StreamReference> = hir
23            .inputs()
24            .sorted_by(|a, b| Ord::cmp(&a.sr(), &b.sr()))
25            .enumerate()
26            .map(|(new_ref, i)| (i.sr(), StreamReference::In(new_ref)))
27            .chain(
28                hir.outputs()
29                    .sorted_by(|a, b| Ord::cmp(&a.sr(), &b.sr()))
30                    .enumerate()
31                    .map(|(new_ref, o)| (o.sr(), StreamReference::Out(new_ref))),
32            )
33            .collect();
34
35        let inputs = hir
36            .inputs()
37            .sorted_by(|a, b| Ord::cmp(&a.sr(), &b.sr()))
38            .map(|i| {
39                let sr = i.sr();
40                mir::InputStream {
41                    name: i.name.clone(),
42                    ty: Self::lower_value_type(&hir.stream_type(sr).value_ty),
43                    accessed_by: Self::lower_accessed_streams(
44                        &sr_map,
45                        hir.direct_accessed_by_with(sr),
46                    ),
47                    aggregated_by: hir
48                        .aggregated_by(sr)
49                        .into_iter()
50                        .map(|(sr, origin, wr)| (sr_map[&sr], origin, wr))
51                        .collect(),
52                    aggregates: Vec::new(),
53                    layer: hir.stream_layers(sr),
54                    memory_bound: hir.memory_bound(sr),
55                    reference: sr_map[&sr],
56                    tags: Self::lower_tags(&i.tags),
57                    #[cfg(feature = "spanned")]
58                    tags_span: i.tags.iter().map(|(k, v)| (k.clone(), v.span)).collect(),
59                    #[cfg(feature = "spanned")]
60                    span: i.span(),
61                }
62            })
63            .collect::<Vec<mir::InputStream>>();
64        assert!(
65            inputs
66                .iter()
67                .enumerate()
68                .all(|(idx, i)| idx == i.reference.in_ix()),
69            "SRefs need to enumerated from 0 to the number of streams"
70        );
71
72        let outputs = hir.outputs().map(|o| {
73            let sr = o.sr();
74            mir::OutputStream {
75                name: o.name(),
76                kind: o.kind.clone(),
77                ty: Self::lower_value_type(&hir.stream_type(sr).value_ty),
78                spawn: Self::lower_spawn(&hir, &sr_map, sr),
79                eval: Self::lower_eval(&hir, &sr_map, sr),
80                close: Self::lower_close(&hir, &sr_map, sr),
81                accesses: Self::lower_accessed_streams(&sr_map, hir.direct_accesses_with(sr)),
82                accessed_by: Self::lower_accessed_streams(&sr_map, hir.direct_accessed_by_with(sr)),
83                aggregated_by: hir
84                    .aggregated_by(sr)
85                    .into_iter()
86                    .map(|(sr, origin, wr)| (sr_map[&sr], origin, wr))
87                    .collect(),
88                aggregates: hir
89                    .aggregates(sr)
90                    .into_iter()
91                    .map(|(sr, origin, wr)| (sr_map[&sr], origin, wr))
92                    .collect(),
93                memory_bound: hir.memory_bound(sr),
94                layer: hir.stream_layers(sr),
95                reference: sr_map[&sr],
96                params: Self::lower_parameters(
97                    hir.output(sr).expect("is output stream").params(),
98                    &hir,
99                    sr,
100                ),
101                tags: Self::lower_tags(&o.tags),
102                #[cfg(feature = "spanned")]
103                tags_span: o.tags.iter().map(|(k, v)| (k.clone(), v.span)).collect(),
104                #[cfg(feature = "spanned")]
105                span: o.span(),
106            }
107        });
108
109        let outputs = outputs
110            .sorted_by(|a, b| Ord::cmp(&a.reference, &b.reference))
111            .collect::<Vec<_>>();
112
113        assert!(
114            outputs
115                .iter()
116                .enumerate()
117                .all(|(idx, o)| idx == o.reference.out_ix()),
118            "SRefs need to enumerated from 0 to the number of streams"
119        );
120
121        let time_driven = outputs
122            .iter()
123            .filter(|o| hir.is_periodic(o.reference))
124            .map(|o| Self::lower_periodic(&hir, &sr_map, o.reference))
125            .collect::<Vec<mir::TimeDrivenStream>>();
126        let event_driven = outputs
127            .iter()
128            .filter(|o| hir.is_event(o.reference))
129            .map(|o| Self::lower_event_based(&hir, &sr_map, o.reference))
130            .collect::<Vec<mir::EventDrivenStream>>();
131
132        let discrete_windows = hir
133            .discrete_windows()
134            .into_iter()
135            .sorted_by(|a, b| Ord::cmp(&a.reference().idx(), &b.reference().idx()))
136            .map(|win| Self::lower_discrete_window(&hir, &sr_map, win))
137            .collect::<Vec<mir::DiscreteWindow>>();
138
139        assert!(
140            discrete_windows
141                .iter()
142                .enumerate()
143                .all(|(idx, w)| idx == w.reference.idx()),
144            "WRefs need to enumerate from 0 to the number of discrete windows"
145        );
146
147        let sliding_windows = hir
148            .sliding_windows()
149            .into_iter()
150            .sorted_by(|a, b| Ord::cmp(&a.reference().idx(), &b.reference().idx()))
151            .map(|win| Self::lower_sliding_window(&hir, &sr_map, win))
152            .collect::<Vec<mir::SlidingWindow>>();
153        assert!(
154            sliding_windows
155                .iter()
156                .enumerate()
157                .all(|(idx, w)| idx == w.reference.idx()),
158            "WRefs need to enumerate from 0 to the number of sliding windows"
159        );
160
161        let instance_aggregations = hir
162            .instance_aggregations()
163            .into_iter()
164            .sorted_by(|a, b| Ord::cmp(&a.reference().idx(), &b.reference().idx()))
165            .map(|win| Self::lower_instance_aggregation(&hir, &sr_map, win))
166            .collect::<Vec<mir::InstanceAggregation>>();
167        assert!(
168            instance_aggregations
169                .iter()
170                .enumerate()
171                .all(|(idx, w)| idx == w.reference.idx()),
172            "WRefs need to enumerate from 0 to the number of discrete windows"
173        );
174
175        let triggers = outputs
176            .iter()
177            .filter_map(|output| {
178                matches!(&output.kind, OutputKind::Trigger(_)).then_some(output.reference)
179            })
180            .enumerate()
181            .map(|(trigger_reference, output_reference)| Trigger {
182                trigger_reference,
183                output_reference,
184            })
185            .collect();
186
187        let global_tags = Self::lower_tags(hir.global_tags());
188        #[cfg(feature = "spanned")]
189        let global_tags_span = hir
190            .global_tags()
191            .iter()
192            .map(|(k, v)| (k.clone(), v.span))
193            .collect();
194
195        Mir {
196            inputs,
197            outputs,
198            time_driven,
199            event_driven,
200            discrete_windows,
201            sliding_windows,
202            instance_aggregations,
203            triggers,
204            global_tags,
205            #[cfg(feature = "spanned")]
206            global_tags_span,
207        }
208    }
209
210    fn lower_event_based(
211        hir: &RtLolaHir<CompleteMode>,
212        sr_map: &HashMap<StreamReference, StreamReference>,
213        sr: StreamReference,
214    ) -> mir::EventDrivenStream {
215        if let ConcretePacingType::Event(ac) = hir.stream_type(sr).eval_pacing {
216            mir::EventDrivenStream {
217                reference: sr_map[&sr],
218                ac: Self::lower_activation_condition(&ac, sr_map),
219            }
220        } else {
221            unreachable!()
222        }
223    }
224
225    fn lower_activation_condition(
226        ac: &ActivationCondition,
227        sr_map: &HashMap<StreamReference, StreamReference>,
228    ) -> mir::ActivationCondition {
229        let lower_conjunction = |conjs: &BTreeSet<StreamReference>| -> mir::ActivationCondition {
230            if conjs.len() == 1 {
231                let sref = conjs.iter().next().unwrap();
232                mir::ActivationCondition::Stream(sr_map[sref])
233            } else {
234                mir::ActivationCondition::Conjunction(
235                    conjs
236                        .iter()
237                        .map(|sr| mir::ActivationCondition::Stream(sr_map[sr]))
238                        .collect(),
239                )
240            }
241        };
242
243        match ac {
244            ActivationCondition::Models(disjuncts) if disjuncts.len() == 1 => {
245                let conj = disjuncts.iter().next().unwrap();
246                lower_conjunction(conj)
247            }
248            ActivationCondition::Models(disjuncts) => mir::ActivationCondition::Disjunction(
249                disjuncts.iter().map(lower_conjunction).collect(),
250            ),
251            ActivationCondition::True => mir::ActivationCondition::True,
252        }
253    }
254
255    fn lower_periodic(
256        hir: &RtLolaHir<CompleteMode>,
257        sr_map: &HashMap<StreamReference, StreamReference>,
258        sr: StreamReference,
259    ) -> mir::TimeDrivenStream {
260        match &hir.stream_type(sr).eval_pacing {
261            ConcretePacingType::FixedGlobalPeriodic(f) => mir::TimeDrivenStream {
262                reference: sr_map[&sr],
263                frequency: *f,
264                locality: PacingLocality::Global,
265            },
266            ConcretePacingType::FixedLocalPeriodic(f) => mir::TimeDrivenStream {
267                reference: sr_map[&sr],
268                frequency: *f,
269                locality: PacingLocality::Local,
270            },
271            _ => unreachable!(),
272        }
273    }
274
275    fn lower_pacing_type(
276        cpt: ConcretePacingType,
277        sr_map: &HashMap<StreamReference, StreamReference>,
278    ) -> PacingType {
279        match cpt {
280            ConcretePacingType::Event(ac) => {
281                PacingType::Event(Self::lower_activation_condition(&ac, sr_map))
282            }
283            ConcretePacingType::FixedLocalPeriodic(freq) => PacingType::LocalPeriodic(freq),
284            ConcretePacingType::FixedGlobalPeriodic(freq) => PacingType::GlobalPeriodic(freq),
285            ConcretePacingType::Constant => PacingType::Constant,
286            other => {
287                unreachable!("Ensured by pacing type checker: {:?}", other)
288            }
289        }
290    }
291
292    fn lower_spawn(
293        hir: &RtLolaHir<CompleteMode>,
294        sr_map: &HashMap<StreamReference, StreamReference>,
295        sr: StreamReference,
296    ) -> Spawn {
297        let ty = hir.stream_type(sr);
298        let spawn_pacing = Self::lower_pacing_type(ty.spawn_pacing, sr_map);
299        let hir_spawn_expr = hir.spawn_expr(sr);
300        let hir_spawn_condition = hir.spawn_cond(sr);
301        let spawn_cond = hir_spawn_condition.map(|expr| Self::lower_expr(hir, sr_map, expr));
302        let spawn_expression = hir_spawn_expr.map(|expr| Self::lower_expr(hir, sr_map, expr));
303        #[cfg(feature = "spanned")]
304        let spawn_span = hir.spawn(sr).map(|s| s.span).unwrap_or(Span::Unknown);
305        Spawn {
306            expression: spawn_expression,
307            pacing: spawn_pacing,
308            condition: spawn_cond,
309            #[cfg(feature = "spanned")]
310            span: spawn_span,
311        }
312    }
313
314    fn lower_eval(
315        hir: &RtLolaHir<CompleteMode>,
316        sr_map: &HashMap<StreamReference, StreamReference>,
317        sr: StreamReference,
318    ) -> Eval {
319        assert_eq!(
320            hir.eval_expr(sr).unwrap().len(),
321            hir.eval_cond(sr).unwrap().len()
322        );
323
324        let clauses = zip(hir.eval_expr(sr).unwrap(), hir.eval_cond(sr).unwrap())
325            .enumerate()
326            .map(|(idx, (expr, cond))| {
327                let expr = Self::lower_expr(hir, sr_map, expr);
328                let condition = cond.map(|f| Self::lower_expr(hir, sr_map, f));
329                let pacing = Self::lower_pacing_type(hir.eval_pacing_type(sr, idx), sr_map);
330                #[cfg(feature = "spanned")]
331                let eval_span = hir.eval(sr).unwrap()[idx].span;
332                EvalClause {
333                    pacing,
334                    condition,
335                    expression: expr,
336                    #[cfg(feature = "spanned")]
337                    span: eval_span,
338                }
339            })
340            .collect();
341
342        let eval_pacing = Self::lower_pacing_type(hir.stream_type(sr).eval_pacing, sr_map);
343        Eval {
344            clauses,
345            eval_pacing,
346        }
347    }
348
349    fn lower_close(
350        hir: &RtLolaHir<CompleteMode>,
351        sr_map: &HashMap<StreamReference, StreamReference>,
352        sr: StreamReference,
353    ) -> Close {
354        let (close, close_pacing, close_self_ref, _close_span) = hir
355            .close_cond(sr)
356            .map(|expr| {
357                let cpt = hir.stream_type(sr).close_pacing;
358                let close_self_ref = matches!(
359                    hir.expr_type(expr.id()).spawn_pacing,
360                    ConcretePacingType::Event(_)
361                        | ConcretePacingType::FixedGlobalPeriodic(_)
362                        | ConcretePacingType::FixedLocalPeriodic(_)
363                );
364                let close_span = hir.close(sr).unwrap().span;
365                (
366                    Some(Self::lower_expr(hir, sr_map, expr)),
367                    Self::lower_pacing_type(cpt, sr_map),
368                    close_self_ref,
369                    close_span,
370                )
371            })
372            .unwrap_or((None, PacingType::Constant, false, Span::Unknown));
373        Close {
374            condition: close,
375            pacing: close_pacing,
376            has_self_reference: close_self_ref,
377            #[cfg(feature = "spanned")]
378            span: _close_span,
379        }
380    }
381
382    fn lower_tags(tags: &HashMap<String, Tag>) -> HashMap<String, Option<String>> {
383        tags.iter()
384            .map(|(key, tag)| (key.to_owned(), tag.value.to_owned()))
385            .collect()
386    }
387
388    fn lower_sliding_window(
389        hir: &RtLolaHir<CompleteMode>,
390        sr_map: &HashMap<StreamReference, StreamReference>,
391        win: &Window<SlidingAggr>,
392    ) -> mir::SlidingWindow {
393        let origin = Self::lower_window_origin(hir, win.reference(), win.caller);
394        mir::SlidingWindow {
395            target: sr_map[&win.target],
396            caller: sr_map[&win.caller],
397            duration: win.aggr.duration,
398            num_buckets: hir.num_buckets(win.reference()),
399            bucket_size: hir.bucket_size(win.reference()),
400            wait: win.aggr.wait,
401            op: Self::lower_window_operation(win.aggr.op),
402            reference: win.reference(),
403            ty: Self::lower_value_type(&hir.expr_type(win.id()).value_ty),
404            pacing: Self::lower_origin_pacing(hir, win.caller, &origin, sr_map),
405            origin,
406        }
407    }
408
409    fn lower_discrete_window(
410        hir: &RtLolaHir<CompleteMode>,
411        sr_map: &HashMap<StreamReference, StreamReference>,
412        win: &Window<DiscreteAggr>,
413    ) -> mir::DiscreteWindow {
414        let origin = Self::lower_window_origin(hir, win.reference(), win.caller);
415        mir::DiscreteWindow {
416            target: sr_map[&win.target],
417            caller: sr_map[&win.caller],
418            duration: win.aggr.duration,
419            wait: win.aggr.wait,
420            op: Self::lower_window_operation(win.aggr.op),
421            reference: win.reference(),
422            ty: Self::lower_value_type(&hir.expr_type(win.id()).value_ty),
423            pacing: Self::lower_origin_pacing(hir, win.caller, &origin, sr_map),
424            origin,
425        }
426    }
427
428    fn lower_instance_aggregation(
429        hir: &RtLolaHir<CompleteMode>,
430        sr_map: &HashMap<StreamReference, StreamReference>,
431        win: &InstanceAggregation,
432    ) -> mir::InstanceAggregation {
433        let origin = Self::lower_window_origin(hir, win.reference(), win.caller);
434        mir::InstanceAggregation {
435            target: sr_map[&win.target],
436            caller: sr_map[&win.caller],
437            reference: win.reference(),
438            selection: Self::lower_instance_selection(&win.selection, hir, win.reference(), sr_map),
439            aggr: Self::lower_instance_operation(win.aggr),
440            ty: Self::lower_value_type(&hir.expr_type(win.id()).value_ty),
441            pacing: Self::lower_origin_pacing(hir, win.caller, &origin, sr_map),
442            origin,
443        }
444    }
445
446    fn lower_window_origin(
447        hir: &RtLolaHir<CompleteMode>,
448        window: WindowReference,
449        caller: StreamReference,
450    ) -> Origin {
451        hir.aggregates(caller)
452            .iter()
453            .find_map(|(_sr, origin, wref)| (*wref == window).then_some(*origin))
454            .unwrap()
455    }
456
457    fn lower_origin_pacing(
458        hir: &RtLolaHir<CompleteMode>,
459        sr: StreamReference,
460        origin: &Origin,
461        sr_map: &HashMap<StreamReference, StreamReference>,
462    ) -> PacingType {
463        let ty = hir.stream_type(sr);
464        let pacing = match origin {
465            Origin::Spawn => ty.spawn_pacing,
466            Origin::Filter(i) | Origin::Eval(i) => hir.eval_pacing_type(sr, *i),
467            Origin::Close => ty.close_pacing,
468        };
469        Self::lower_pacing_type(pacing, sr_map)
470    }
471
472    fn lower_value_type(ty: &ConcreteValueType) -> mir::Type {
473        match ty {
474            ConcreteValueType::Bool => mir::Type::Bool,
475            ConcreteValueType::Integer8 => mir::Type::Int(mir::IntTy::Int8),
476            ConcreteValueType::Integer16 => mir::Type::Int(mir::IntTy::Int16),
477            ConcreteValueType::Integer32 => mir::Type::Int(mir::IntTy::Int32),
478            ConcreteValueType::Integer64 => mir::Type::Int(mir::IntTy::Int64),
479            ConcreteValueType::Integer128 => mir::Type::Int(mir::IntTy::Int128),
480            ConcreteValueType::Integer256 => mir::Type::Int(mir::IntTy::Int256),
481            ConcreteValueType::UInteger8 => mir::Type::UInt(mir::UIntTy::UInt8),
482            ConcreteValueType::UInteger16 => mir::Type::UInt(mir::UIntTy::UInt16),
483            ConcreteValueType::UInteger32 => mir::Type::UInt(mir::UIntTy::UInt32),
484            ConcreteValueType::UInteger64 => mir::Type::UInt(mir::UIntTy::UInt64),
485            ConcreteValueType::UInteger128 => mir::Type::UInt(mir::UIntTy::UInt128),
486            ConcreteValueType::UInteger256 => mir::Type::UInt(mir::UIntTy::UInt256),
487            ConcreteValueType::Float32 => mir::Type::Float(mir::FloatTy::Float32),
488            ConcreteValueType::Float64 => mir::Type::Float(mir::FloatTy::Float64),
489            ConcreteValueType::Fixed64_32 => mir::Type::Fixed(mir::FixedTy::Fixed64_32),
490            ConcreteValueType::Fixed32_16 => mir::Type::Fixed(mir::FixedTy::Fixed32_16),
491            ConcreteValueType::Fixed16_8 => mir::Type::Fixed(mir::FixedTy::Fixed16_8),
492            ConcreteValueType::UFixed64_32 => mir::Type::UFixed(mir::FixedTy::Fixed64_32),
493            ConcreteValueType::UFixed32_16 => mir::Type::UFixed(mir::FixedTy::Fixed32_16),
494            ConcreteValueType::UFixed16_8 => mir::Type::UFixed(mir::FixedTy::Fixed16_8),
495            ConcreteValueType::Tuple(elements) => {
496                let elements = elements
497                    .iter()
498                    .map(Self::lower_value_type)
499                    .collect::<Vec<_>>();
500                mir::Type::Tuple(elements)
501            }
502            ConcreteValueType::TString => mir::Type::String,
503            ConcreteValueType::Byte => mir::Type::Bytes,
504            ConcreteValueType::Option(v) => mir::Type::Option(Box::new(Self::lower_value_type(v))),
505        }
506    }
507
508    fn lower_expr(
509        hir: &RtLolaHir<CompleteMode>,
510        sr_map: &HashMap<StreamReference, StreamReference>,
511        expr: &Expression,
512    ) -> mir::Expression {
513        let ty = Self::lower_value_type(&hir.expr_type(expr.id()).value_ty);
514        mir::Expression {
515            kind: Self::lower_expression_kind(hir, sr_map, &expr.kind, &ty),
516            ty,
517            #[cfg(feature = "spanned")]
518            span: expr.span(),
519        }
520    }
521
522    fn lower_expression_kind(
523        hir: &RtLolaHir<CompleteMode>,
524        sr_map: &HashMap<StreamReference, StreamReference>,
525        expr: &ExpressionKind,
526        ty: &mir::Type,
527    ) -> mir::ExpressionKind {
528        match expr {
529            ExpressionKind::LoadConstant(constant) => {
530                mir::ExpressionKind::LoadConstant(Self::lower_constant(constant, ty))
531            }
532            ExpressionKind::ArithLog(op, args) => {
533                let op = Self::lower_arith_log_op(*op);
534                let args = args
535                    .iter()
536                    .map(|arg| Self::lower_expr(hir, sr_map, arg))
537                    .collect::<Vec<mir::Expression>>();
538                mir::ExpressionKind::ArithLog(op, args)
539            }
540            ExpressionKind::StreamAccess(sr, kind, para) => mir::ExpressionKind::StreamAccess {
541                target: sr_map[sr],
542                access_kind: Self::lower_stream_access_kind(*kind),
543                parameters: para
544                    .iter()
545                    .map(|p| Self::lower_expr(hir, sr_map, p))
546                    .collect(),
547            },
548            ExpressionKind::ParameterAccess(sr, para) => {
549                mir::ExpressionKind::ParameterAccess(sr_map[sr], *para)
550            }
551            ExpressionKind::LambdaParameterAccess { wref, pref } => {
552                mir::ExpressionKind::LambdaParameterAccess {
553                    wref: *wref,
554                    pref: *pref,
555                }
556            }
557            ExpressionKind::Ite {
558                condition,
559                consequence,
560                alternative,
561            } => {
562                let condition = Box::new(Self::lower_expr(hir, sr_map, condition));
563                let consequence = Box::new(Self::lower_expr(hir, sr_map, consequence));
564                let alternative = Box::new(Self::lower_expr(hir, sr_map, alternative));
565                mir::ExpressionKind::Ite {
566                    condition,
567                    consequence,
568                    alternative,
569                }
570            }
571            ExpressionKind::Tuple(elements) => {
572                let elements = elements
573                    .iter()
574                    .map(|element| Self::lower_expr(hir, sr_map, element))
575                    .collect::<Vec<mir::Expression>>();
576                mir::ExpressionKind::Tuple(elements)
577            }
578            ExpressionKind::TupleAccess(tuple, element_pos) => {
579                let tuple = Box::new(Self::lower_expr(hir, sr_map, tuple));
580                let element_pos = *element_pos;
581                mir::ExpressionKind::TupleAccess(tuple, element_pos)
582            }
583            ExpressionKind::Function(kind) => {
584                let FnExprKind { name, args, .. } = kind;
585                match name.as_ref() {
586                    "cast" => {
587                        assert_eq!(args.len(), 1);
588                        let expr = Box::new(Self::lower_expr(hir, sr_map, &args[0]));
589                        mir::ExpressionKind::Convert { expr }
590                    }
591                    _ => {
592                        let args = args
593                            .iter()
594                            .map(|arg| Self::lower_expr(hir, sr_map, arg))
595                            .collect::<Vec<mir::Expression>>();
596                        mir::ExpressionKind::Function(name.clone(), args)
597                    }
598                }
599            }
600            ExpressionKind::Widen(kind) => {
601                let WidenExprKind { expr, .. } = kind;
602                let expr = Box::new(Self::lower_expr(hir, sr_map, expr));
603                mir::ExpressionKind::Convert { expr }
604            }
605            ExpressionKind::Default { expr, default } => {
606                let expr = Box::new(Self::lower_expr(hir, sr_map, expr));
607                let default = Box::new(Self::lower_expr(hir, sr_map, default));
608                mir::ExpressionKind::Default { expr, default }
609            }
610        }
611    }
612
613    fn lower_constant(constant: &Constant, ty: &mir::Type) -> mir::Constant {
614        match constant {
615            Constant::Basic(lit) => Self::lower_constant_literal(lit, ty),
616            Constant::Inlined(Inlined { lit, .. }) => Self::lower_constant_literal(lit, ty),
617        }
618    }
619
620    fn lower_constant_literal(constant: &Literal, ty: &mir::Type) -> mir::Constant {
621        match constant {
622            Literal::Str(s) => mir::Constant::Str(s.clone()),
623            Literal::Bool(b) => mir::Constant::Bool(*b),
624            Literal::Integer(i) => match ty {
625                mir::Type::Int(_) => mir::Constant::Int(*i),
626                mir::Type::UInt(_) => mir::Constant::UInt(*i as u64),
627                _ => unreachable!(),
628            },
629            Literal::SInt(i) => {
630                //TODO rewrite to 128 bytes
631                match ty {
632                    mir::Type::Int(_) => mir::Constant::Int(*i as i64),
633                    mir::Type::UInt(_) => mir::Constant::UInt(*i as u64),
634                    _ => unreachable!(),
635                }
636            }
637            Literal::Decimal(f) => match ty {
638                mir::Type::Float(_) => mir::Constant::Float(f.to_f64().unwrap()),
639                mir::Type::Fixed(_) | mir::Type::UFixed(_) => mir::Constant::Decimal(*f),
640                _ => unreachable!(),
641            },
642        }
643    }
644
645    fn lower_arith_log_op(op: ArithLogOp) -> mir::ArithLogOp {
646        match op {
647            ArithLogOp::Not => mir::ArithLogOp::Not,
648            ArithLogOp::Neg => mir::ArithLogOp::Neg,
649            ArithLogOp::Add => mir::ArithLogOp::Add,
650            ArithLogOp::Sub => mir::ArithLogOp::Sub,
651            ArithLogOp::Mul => mir::ArithLogOp::Mul,
652            ArithLogOp::Div => mir::ArithLogOp::Div,
653            ArithLogOp::Rem => mir::ArithLogOp::Rem,
654            ArithLogOp::Pow => mir::ArithLogOp::Pow,
655            ArithLogOp::And => mir::ArithLogOp::And,
656            ArithLogOp::Or => mir::ArithLogOp::Or,
657            ArithLogOp::BitXor => mir::ArithLogOp::BitXor,
658            ArithLogOp::BitAnd => mir::ArithLogOp::BitAnd,
659            ArithLogOp::BitOr => mir::ArithLogOp::BitOr,
660            ArithLogOp::BitNot => mir::ArithLogOp::BitNot,
661            ArithLogOp::Shl => mir::ArithLogOp::Shl,
662            ArithLogOp::Shr => mir::ArithLogOp::Shr,
663            ArithLogOp::Eq => mir::ArithLogOp::Eq,
664            ArithLogOp::Lt => mir::ArithLogOp::Lt,
665            ArithLogOp::Le => mir::ArithLogOp::Le,
666            ArithLogOp::Ne => mir::ArithLogOp::Ne,
667            ArithLogOp::Ge => mir::ArithLogOp::Ge,
668            ArithLogOp::Gt => mir::ArithLogOp::Gt,
669        }
670    }
671
672    fn lower_window_operation(op: WindowOperation) -> mir::WindowOperation {
673        match op {
674            WindowOperation::Count => mir::WindowOperation::Count,
675            WindowOperation::Min => mir::WindowOperation::Min,
676            WindowOperation::Max => mir::WindowOperation::Max,
677            WindowOperation::Sum => mir::WindowOperation::Sum,
678            WindowOperation::Product => mir::WindowOperation::Product,
679            WindowOperation::Average => mir::WindowOperation::Average,
680            WindowOperation::Integral => mir::WindowOperation::Integral,
681            WindowOperation::Conjunction => mir::WindowOperation::Conjunction,
682            WindowOperation::Disjunction => mir::WindowOperation::Disjunction,
683            WindowOperation::Last => mir::WindowOperation::Last,
684            WindowOperation::Variance => mir::WindowOperation::Variance,
685            WindowOperation::Covariance => mir::WindowOperation::Covariance,
686            WindowOperation::StandardDeviation => mir::WindowOperation::StandardDeviation,
687            WindowOperation::NthPercentile(x) => mir::WindowOperation::NthPercentile(x),
688            WindowOperation::TrueRatio => unreachable!("True Ratio is Syntactic Sugar"),
689        }
690    }
691
692    fn lower_instance_operation(op: InstanceOperation) -> mir::InstanceOperation {
693        match op {
694            InstanceOperation::Count => mir::InstanceOperation::Count,
695            InstanceOperation::Min => mir::InstanceOperation::Min,
696            InstanceOperation::Max => mir::InstanceOperation::Max,
697            InstanceOperation::Sum => mir::InstanceOperation::Sum,
698            InstanceOperation::Product => mir::InstanceOperation::Product,
699            InstanceOperation::Average => mir::InstanceOperation::Average,
700            InstanceOperation::Conjunction => mir::InstanceOperation::Conjunction,
701            InstanceOperation::Disjunction => mir::InstanceOperation::Disjunction,
702            InstanceOperation::Variance => mir::InstanceOperation::Variance,
703            InstanceOperation::Covariance => mir::InstanceOperation::Covariance,
704            InstanceOperation::StandardDeviation => mir::InstanceOperation::StandardDeviation,
705            InstanceOperation::NthPercentile(x) => mir::InstanceOperation::NthPercentile(x),
706            InstanceOperation::TrueRatio => unreachable!("True Ratio is Syntactic Sugar"),
707        }
708    }
709
710    fn lower_instance_selection(
711        sel: &InstanceSelection,
712        hir: &RtLolaHir<CompleteMode>,
713        wref: WindowReference,
714        sr_map: &HashMap<StreamReference, StreamReference>,
715    ) -> mir::InstanceSelection {
716        match sel {
717            InstanceSelection::Fresh => mir::InstanceSelection::Fresh,
718            InstanceSelection::All => mir::InstanceSelection::All,
719            InstanceSelection::FilteredFresh { parameters, cond } => {
720                let target = hir.single_instance_aggregation(wref).target;
721                mir::InstanceSelection::FilteredFresh {
722                    parameters: Self::lower_parameters(parameters.iter(), hir, target),
723                    cond: Box::new(Self::lower_expr(hir, sr_map, cond)),
724                }
725            }
726            InstanceSelection::FilteredAll { parameters, cond } => {
727                let target = hir.single_instance_aggregation(wref).target;
728                mir::InstanceSelection::FilteredAll {
729                    parameters: Self::lower_parameters(parameters.iter(), hir, target),
730                    cond: Box::new(Self::lower_expr(hir, sr_map, cond)),
731                }
732            }
733        }
734    }
735
736    fn lower_stream_access_kind(kind: StreamAccessKind) -> mir::StreamAccessKind {
737        match kind {
738            StreamAccessKind::Sync => mir::StreamAccessKind::Sync,
739            StreamAccessKind::DiscreteWindow(wref) => mir::StreamAccessKind::DiscreteWindow(wref),
740            StreamAccessKind::SlidingWindow(wref) => mir::StreamAccessKind::SlidingWindow(wref),
741            StreamAccessKind::InstanceAggregation(wref) => {
742                mir::StreamAccessKind::InstanceAggregation(wref)
743            }
744            StreamAccessKind::Hold => mir::StreamAccessKind::Hold,
745            StreamAccessKind::Offset(o) => mir::StreamAccessKind::Offset(Self::lower_offset(o)),
746            StreamAccessKind::Get => mir::StreamAccessKind::Get,
747            StreamAccessKind::Fresh => mir::StreamAccessKind::Fresh,
748        }
749    }
750
751    fn lower_offset(offset: Offset) -> mir::Offset {
752        match offset {
753            Offset::FutureDiscrete(o) => mir::Offset::Future(o),
754            Offset::PastDiscrete(o) => mir::Offset::Past(o),
755            Offset::FutureRealTime(_) | Offset::PastRealTime(_) => {
756                unreachable!("Real-time Lookups should be already transformed to discrete lookups.")
757            }
758        }
759    }
760
761    fn lower_accessed_streams(
762        sr_map: &HashMap<StreamReference, StreamReference>,
763        streams: Vec<(StreamReference, Vec<(Origin, StreamAccessKind)>)>,
764    ) -> Vec<(StreamReference, Vec<(Origin, mir::StreamAccessKind)>)> {
765        streams
766            .into_iter()
767            .map(|(sref, kinds)| {
768                (
769                    sr_map[&sref],
770                    kinds
771                        .into_iter()
772                        .map(|(origin, kind)| (origin, Self::lower_stream_access_kind(kind)))
773                        .collect(),
774                )
775            })
776            .collect()
777    }
778
779    fn lower_parameters<'a>(
780        params: impl Iterator<Item = &'a rtlola_hir::hir::Parameter>,
781        hir: &RtLolaHir<CompleteMode>,
782        sr: StreamReference,
783    ) -> Vec<mir::Parameter> {
784        params
785            .map(|parameter| mir::Parameter {
786                name: parameter.name.clone(),
787                ty: Self::lower_value_type(&hir.get_parameter_type(sr, parameter.index())),
788                idx: parameter.index(),
789                #[cfg(feature = "spanned")]
790                span: parameter.span(),
791            })
792            .collect()
793    }
794}
795
796#[cfg(test)]
797#[cfg(not(feature = "spanned"))]
798mod tests {
799    use num::rational::Rational64 as Rational;
800    use num::FromPrimitive;
801    use rtlola_hir::config::FrontendConfig;
802    use rtlola_parser::ParserConfig;
803    use uom::si::frequency::hertz;
804    use uom::si::rational64::Frequency as UOM_Frequency;
805
806    use super::*;
807    use crate::mir::IntTy::Int8;
808    use crate::mir::{PacingType, Stream};
809
810    fn lower_spec(spec: &str) -> (RtLolaHir<CompleteMode>, mir::RtLolaMir) {
811        lower_spec_with_config((&ParserConfig::for_string(spec.into())).into())
812    }
813
814    fn lower_spec_with_config(config: FrontendConfig) -> (RtLolaHir<CompleteMode>, mir::RtLolaMir) {
815        let ast = config
816            .parser_config()
817            .parse()
818            .unwrap_or_else(|e| panic!("{:?}", e));
819        let hir = rtlola_hir::fully_analyzed(ast, &config).expect("Invalid AST:");
820        (hir.clone(), Mir::from_hir(hir))
821    }
822
823    #[test]
824    fn check_event_based_streams() {
825        let spec = "input a: Float64\ninput b:Float64\noutput c := a + b\noutput d := a.hold().defaults(to:0.0) + b\noutput e := a + 9.0\ntrigger d < e\ntrigger a < 5.0";
826        let (hir, mir) = lower_spec(spec);
827
828        assert_eq!(mir.inputs.len(), 2);
829        assert_eq!(mir.outputs.len(), 5);
830        assert_eq!(mir.event_driven.len(), 5);
831        assert_eq!(mir.time_driven.len(), 0);
832        assert_eq!(mir.discrete_windows.len(), 0);
833        assert_eq!(mir.sliding_windows.len(), 0);
834        assert_eq!(mir.triggers.len(), 2);
835        let hir_a = hir.inputs().find(|i| i.name == "a".to_string()).unwrap();
836        let mir_a = mir
837            .inputs
838            .iter()
839            .find(|i| i.name == "a".to_string())
840            .unwrap();
841        assert_eq!(hir_a.sr(), mir_a.reference);
842        let hir_d = hir.outputs().find(|i| i.name() == "d".to_string()).unwrap();
843        let mir_d = mir
844            .outputs
845            .iter()
846            .find(|i| i.name == "d".to_string())
847            .unwrap();
848        assert_eq!(hir_d.sr(), mir_d.reference);
849    }
850
851    #[test]
852    fn check_time_driven_streams() {
853        let spec = "input a: Int64\ninput b:Int64\noutput c @1Hz:= a.aggregate(over: 2s, using: sum)+ b.aggregate(over: 4s, using:sum)\noutput d @4Hz:= a.hold().defaults(to:0) + b.hold().defaults(to: 0)\noutput e @0.5Hz := a.aggregate(over: 4s, using: sum) + 9\ntrigger d < e";
854        let (hir, mir) = lower_spec(spec);
855
856        assert_eq!(mir.inputs.len(), 2);
857        assert_eq!(mir.outputs.len(), 4);
858        assert_eq!(mir.event_driven.len(), 0);
859        assert_eq!(mir.time_driven.len(), 4);
860        assert_eq!(mir.discrete_windows.len(), 0);
861        assert_eq!(mir.sliding_windows.len(), 3);
862        assert_eq!(mir.triggers.len(), 1);
863        let hir_a = hir.inputs().find(|i| i.name == "a".to_string()).unwrap();
864        let mir_a = mir
865            .inputs
866            .iter()
867            .find(|i| i.name == "a".to_string())
868            .unwrap();
869        assert_eq!(hir_a.sr(), mir_a.reference);
870        let hir_d = hir.outputs().find(|i| i.name() == "d".to_string()).unwrap();
871        let mir_d = mir
872            .outputs
873            .iter()
874            .find(|i| i.name == "d".to_string())
875            .unwrap();
876        assert_eq!(hir_d.sr(), mir_d.reference);
877    }
878
879    #[test]
880    fn check_stream_with_parameter() {
881        let spec = "input a: Int8\n\
882        output d(para) spawn with a when a > 6 eval @a with para";
883        let (hir, mir) = lower_spec(spec);
884
885        assert_eq!(mir.inputs.len(), 1);
886        assert_eq!(mir.outputs.len(), 1);
887        assert_eq!(mir.event_driven.len(), 1);
888        assert_eq!(mir.time_driven.len(), 0);
889        assert_eq!(mir.discrete_windows.len(), 0);
890        assert_eq!(mir.sliding_windows.len(), 0);
891        assert_eq!(mir.triggers.len(), 0);
892        let hir_a = hir.inputs().find(|i| i.name == "a".to_string()).unwrap();
893        let mir_a = mir
894            .inputs
895            .iter()
896            .find(|i| i.name == "a".to_string())
897            .unwrap();
898        assert_eq!(hir_a.sr(), mir_a.reference);
899        let hir_d = hir.outputs().find(|i| i.name() == "d".to_string()).unwrap();
900        let mir_d = mir
901            .outputs
902            .iter()
903            .find(|i| i.name == "d".to_string())
904            .unwrap();
905        assert_eq!(hir_d.sr(), mir_d.reference);
906        assert_eq!(
907            &mir_d.spawn.expression,
908            &Some(mir::Expression {
909                kind: mir::ExpressionKind::StreamAccess {
910                    target: mir_a.reference,
911                    parameters: vec![],
912                    access_kind: mir::StreamAccessKind::Sync,
913                },
914                ty: mir::Type::Int(Int8),
915            })
916        );
917        assert!(matches!(
918            &mir_d.spawn.condition,
919            &Some(mir::Expression {
920                kind: mir::ExpressionKind::ArithLog(mir::ArithLogOp::Gt, _),
921                ty: _,
922            })
923        ));
924        assert_eq!(
925            &mir_d.spawn.pacing,
926            &PacingType::Event(mir::ActivationCondition::Stream(mir_a.reference))
927        );
928        assert_eq!(
929            &mir_d.eval.clauses[0].expression,
930            &mir::Expression {
931                kind: mir::ExpressionKind::ParameterAccess(mir_d.reference, 0),
932                ty: mir::Type::Int(Int8),
933            }
934        );
935    }
936
937    #[test]
938    fn check_spawn_filter_close() {
939        let spec = "input a: Int8\n\
940        output d spawn @1Hz when a.hold().defaults(to:0) > 6 eval @a when a = 42 with a close when a = 1337";
941        let (_, mir) = lower_spec(spec);
942
943        assert_eq!(mir.inputs.len(), 1);
944        assert_eq!(mir.outputs.len(), 1);
945        assert_eq!(mir.event_driven.len(), 1);
946        assert_eq!(mir.time_driven.len(), 0);
947        assert_eq!(mir.discrete_windows.len(), 0);
948        assert_eq!(mir.sliding_windows.len(), 0);
949        assert_eq!(mir.triggers.len(), 0);
950
951        let mir_d = mir
952            .outputs
953            .iter()
954            .find(|i| i.name == "d".to_string())
955            .unwrap();
956
957        assert!(mir_d.spawn.expression.is_none());
958        assert!(matches!(
959            &mir_d.spawn.condition,
960            Some(mir::Expression {
961                kind: mir::ExpressionKind::ArithLog(mir::ArithLogOp::Gt, _),
962                ty: _,
963            })
964        ));
965        assert_eq!(
966            mir_d.spawn.pacing,
967            PacingType::GlobalPeriodic(UOM_Frequency::new::<hertz>(Rational::from_u8(1).unwrap()))
968        );
969        assert!(matches!(
970            mir_d.eval.clauses[0].condition,
971            Some(mir::Expression {
972                kind: mir::ExpressionKind::ArithLog(mir::ArithLogOp::Eq, _),
973                ty: _,
974            })
975        ));
976        assert!(matches!(
977            mir_d.close.condition,
978            Some(mir::Expression {
979                kind: mir::ExpressionKind::ArithLog(mir::ArithLogOp::Eq, _),
980                ty: _,
981            })
982        ));
983    }
984
985    #[test]
986    fn test_periodic_trigger() {
987        let spec = "input a: Bool\n\
988        trigger @1Hz a.hold(or: false)";
989        let (_, mir) = lower_spec(spec);
990
991        assert_eq!(mir.inputs.len(), 1);
992        assert_eq!(mir.outputs.len(), 1);
993        assert_eq!(mir.event_driven.len(), 0);
994        assert_eq!(mir.time_driven.len(), 1);
995        assert_eq!(mir.discrete_windows.len(), 0);
996        assert_eq!(mir.sliding_windows.len(), 0);
997        assert_eq!(mir.triggers.len(), 1);
998    }
999
1000    #[test]
1001    fn test_instance_window() {
1002        let spec = "input a: Int32\n\
1003        output b(p: Bool) spawn with a == 42 eval with a\n\
1004        output c @1Hz := b(false).aggregate(over: 1s, using: sum)";
1005        let (_, mir) = lower_spec(spec);
1006
1007        let expr = mir.outputs[1].eval.clauses[0].expression.kind.clone();
1008        assert!(
1009            matches!(expr, mir::ExpressionKind::StreamAccess {target: _, parameters: paras, access_kind: mir::StreamAccessKind::SlidingWindow(_)} if paras.len() == 1)
1010        );
1011    }
1012
1013    #[test]
1014    fn test_cast_lowering() {
1015        let spec = "input a: Int64\n\
1016        output b := cast<Int64, Float64>(a)";
1017        let (_, mir) = lower_spec(spec);
1018        assert!(matches!(
1019            mir.outputs[0].eval.clauses[0].expression.kind,
1020            mir::ExpressionKind::Convert { expr: _ }
1021        ));
1022    }
1023
1024    #[test]
1025    fn test_multiple_eval_clauses() {
1026        let spec = "input a: Int64\ninput b: Int64\ninput c: Bool\n\
1027        output d eval @(c&&a) when c with a eval @(c&&b) when !c with b";
1028        let (_, mir) = lower_spec(spec);
1029        assert_eq!(mir.outputs.len(), 1);
1030        assert_eq!(mir.inputs.len(), 3);
1031        assert_eq!(mir.triggers.len(), 0);
1032
1033        let output = &mir.outputs[0];
1034        assert_eq!(output.eval.clauses.len(), 2);
1035        assert_eq!(
1036            output.eval.eval_pacing,
1037            PacingType::Event(mir::ActivationCondition::Disjunction(vec![
1038                mir::ActivationCondition::Conjunction(vec![
1039                    // TODO:??
1040                    mir::ActivationCondition::Stream(StreamReference::In(0)),
1041                    mir::ActivationCondition::Stream(StreamReference::In(1)),
1042                    mir::ActivationCondition::Stream(StreamReference::In(2)),
1043                ]),
1044                mir::ActivationCondition::Conjunction(vec![
1045                    mir::ActivationCondition::Stream(StreamReference::In(0)),
1046                    mir::ActivationCondition::Stream(StreamReference::In(2)),
1047                ]),
1048                mir::ActivationCondition::Conjunction(vec![
1049                    mir::ActivationCondition::Stream(StreamReference::In(1)),
1050                    mir::ActivationCondition::Stream(StreamReference::In(2)),
1051                ]),
1052            ]))
1053        );
1054        assert_eq!(
1055            output.eval.clauses[0].pacing,
1056            PacingType::Event(mir::ActivationCondition::Conjunction(vec![
1057                mir::ActivationCondition::Stream(StreamReference::In(0)),
1058                mir::ActivationCondition::Stream(StreamReference::In(2)),
1059            ]))
1060        );
1061        assert_eq!(
1062            output.eval.clauses[1].pacing,
1063            PacingType::Event(mir::ActivationCondition::Conjunction(vec![
1064                mir::ActivationCondition::Stream(StreamReference::In(1)),
1065                mir::ActivationCondition::Stream(StreamReference::In(2)),
1066            ]))
1067        );
1068    }
1069
1070    #[test]
1071    fn spawn_close_ac_only() {
1072        let spec = "input a : UInt64\noutput b spawn @a eval @true with true";
1073        let (_, mir) = lower_spec(spec);
1074        assert!(mir.outputs[0].is_spawned());
1075
1076        let spec = "input a : UInt64\noutput b spawn when a == 0 eval @Global(1Hz) with true close @Local(5s)";
1077        let (_, mir) = lower_spec(spec);
1078        assert!(mir.outputs[0].is_closed());
1079    }
1080
1081    #[test]
1082    fn tagged_streams() {
1083        let spec = "#[key=\"value\", key2=\"value2\"]\n\
1084        input a : Bool
1085        #[warning]
1086        trigger a";
1087        let (_, mir) = lower_spec(spec);
1088        let input_tags = &mir.inputs[0].tags;
1089        assert_eq!(input_tags.len(), 2);
1090        assert!(input_tags["key"].as_ref().unwrap() == "value");
1091        assert!(input_tags["key2"].as_ref().unwrap() == "value2");
1092        let trigger_tags = &mir.outputs[0].tags;
1093        assert_eq!(trigger_tags.len(), 1);
1094        assert_eq!(trigger_tags["warning"], None);
1095    }
1096
1097    #[test]
1098    fn global_tags() {
1099        let spec = "#![key=\"value\"]\n\
1100        #![warning]
1101        input a : Bool";
1102        let (_, mir) = lower_spec(spec);
1103        let global_tags = &mir.global_tags;
1104        assert_eq!(global_tags.len(), 2);
1105        assert_eq!(global_tags["key"].as_ref().unwrap(), "value");
1106        assert_eq!(global_tags["warning"].as_ref(), None);
1107    }
1108
1109    #[test]
1110    fn lower_true_ratio_aggregation() {
1111        let spec = "input a: Bool\n\
1112        output b (p) spawn with a eval when a = p with a\n\
1113        output c (p) spawn with a eval @1Hz with b(p).aggregate(over: 1s, using: true_ratio).defaults(to: 0.0)\n";
1114        let (_, _) = lower_spec(spec);
1115    }
1116
1117    #[test]
1118    fn lower_true_ratio_instance_aggregation() {
1119        let spec = "input a: UInt8\n\
1120        output a' (p) spawn @a with a eval @a when a = p with a + p > 5\n\
1121        output b eval @1Hz with a'.aggregate(over_instances: all, using: true_ratio).defaults(to: 0.0)";
1122        let (_, _) = lower_spec(spec);
1123    }
1124
1125    #[test]
1126    fn test_probability() {
1127        let spec = "input a : UInt64\n\
1128        output c := prob(of: a > 10, given: a < 5)";
1129        let (_, _) = lower_spec(spec);
1130    }
1131
1132    #[test]
1133    fn test_probability_parameterized() {
1134        let spec = r#"import math
1135
1136input id : Int64
1137input sensible_feature: String
1138input associated_feature: Int64
1139input decision: Bool
1140input idDec: Int64
1141
1142/// Database
1143output sensible_feature_per(user)
1144  spawn with id
1145  eval when id == user  with sensible_feature
1146  close @(decision & idDec) when user == idDec
1147
1148output relation_per(f)
1149    spawn with sensible_feature
1150    eval with prob(of: decision, given: sensible_feature_per(idDec).hold(or: "D") == f)
1151
1152output statParity
1153    eval @true with abs(relation_per("M").hold(or: 1.0) - relation_per("F").hold(or: 1.0))
1154
1155trigger statParity > 0.1"#;
1156        let (_, _) = lower_spec(spec);
1157    }
1158}