nickel_lang_core/term/pattern/
compile.rs

1//! Compilation of pattern matching down to pattern-less Nickel code.
2//!
3//! # Algorithm
4//!
5//! Compiling patterns amounts to generate a decision tree - concretely, a term composed mostly of
6//! nested if-then-else - which either succeeds to match a value and returns the bindings of
7//! pattern variables, or fails and returns `null`.
8//!
9//! Compilation of pattern matching is a well-studied problem in the literature, where efficient
10//! algorithms try to avoid the duplication of checks by "grouping" them in a smart way. A standard
11//! resource on this topic is the paper [_Compiling Pattern Matching to Good Decision
12//! Trees_](https://dl.acm.org/doi/10.1145/1411304.1411311) by Luc Maranget.
13//!
14//! The current version of pattern compilation in Nickel is naive: it simply compiles each pattern
15//! to a checking expression and tries them all until one works. We don't expect pattern matching
16//! to be relevant for performance anytime soon (allegedly, there are much more impacting aspects
17//! to handle before that). We might revisit this in the future if pattern matching turns out to be
18//! a bottleneck.
19//!
20//! Most building blocks are generated programmatically rather than written out as e.g. members of
21//! the [crate::stdlib::internals] module. While clunkier, this makes it easier to change
22//! the compilation strategy in the future and is more efficient in the current setting (combining
23//! building blocks from the standard library would require much more function applications, while
24//! we can generate inlined versions on-the-fly here).
25use super::*;
26use crate::{
27    metrics::increment,
28    mk_app,
29    term::{
30        make, record::FieldMetadata, BinaryOp, MatchBranch, MatchData, NAryOp, RecordExtKind,
31        RecordOpKind, RichTerm, Term, UnaryOp,
32    },
33};
34
35/// Generate a standard `%record/insert%` primop as generated by the parser.
36fn record_insert() -> BinaryOp {
37    BinaryOp::RecordInsert {
38        ext_kind: RecordExtKind::WithValue,
39        metadata: Default::default(),
40        pending_contracts: Default::default(),
41        // We don't really care for optional fields here and we don't need to filter them out
42        op_kind: RecordOpKind::ConsiderAllFields,
43    }
44}
45
46/// Generate a Nickel expression which inserts a new binding in the working dictionary.
47///
48/// `%record/insert% "<id>" bindings_id value_id`
49fn insert_binding(id: LocIdent, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
50    mk_app!(
51        make::op2(
52            record_insert(),
53            Term::Str(id.label().into()),
54            Term::Var(bindings_id)
55        ),
56        Term::Var(value_id)
57    )
58}
59
60/// Generate a Nickel expression which update the `REST_FIELD` field of the working bindings by
61/// remove the `field` from it.
62///
63/// ```nickel
64/// %record/insert% "<REST_FIELD>"
65///   (%record/remove% "<REST_FIELD>" bindings_id)
66///   (%record/remove% "<field>"
67///     (%static_access(REST_FIELD) bindings_id)
68///   )
69/// ```
70fn remove_from_rest(rest_field: LocIdent, field: LocIdent, bindings_id: LocIdent) -> RichTerm {
71    let rest = make::op1(UnaryOp::RecordAccess(rest_field), Term::Var(bindings_id));
72
73    let rest_shrinked = make::op2(
74        BinaryOp::RecordRemove(RecordOpKind::ConsiderAllFields),
75        Term::Str(field.label().into()),
76        rest,
77    );
78
79    let bindings_shrinked = make::op2(
80        BinaryOp::RecordRemove(RecordOpKind::ConsiderAllFields),
81        Term::Str(rest_field.into()),
82        Term::Var(bindings_id),
83    );
84
85    mk_app!(
86        make::op2(
87            record_insert(),
88            Term::Str(rest_field.into()),
89            bindings_shrinked,
90        ),
91        rest_shrinked
92    )
93}
94
95/// Generate a Nickel expression which checks if a field is defined in a record provided as a
96/// variable, and if not, insert a default value. Return the result (either the original record
97/// unchanged, or the original record with the default value). The resulting record is guaranteed
98/// to have the `field` defined. The implementation uses merging to avoid dropping the contracts
99/// and other metadata affected to `field`, if the field exists but has no definition.
100///
101/// More precisely, [with_default_value] generates the following code:
102///
103/// ```nickel
104/// if !(%record/field_is_defined% "<field>" record_id) then
105///   if %record/has_field% "<field>" record_id then
106///     record_id & { "<field>" = default }
107///   else
108///     # Merging is potentially more costly, and we can just fallback to record insertion here.
109///     %record/insert% "<field>" record_id default
110/// else
111///   record_id
112/// ```
113pub(crate) fn with_default_value(
114    record_id: LocIdent,
115    field: LocIdent,
116    default: RichTerm,
117) -> RichTerm {
118    let field_not_defined = make::op1(
119        UnaryOp::BoolNot,
120        make::op2(
121            BinaryOp::RecordFieldIsDefined(RecordOpKind::ConsiderAllFields),
122            Term::Str(field.label().into()),
123            Term::Var(record_id),
124        ),
125    );
126
127    let has_field = make::op2(
128        BinaryOp::RecordHasField(RecordOpKind::ConsiderAllFields),
129        Term::Str(field.label().into()),
130        Term::Var(record_id),
131    );
132
133    let insert = mk_app!(
134        make::op2(
135            record_insert(),
136            Term::Str(field.into()),
137            make::var(record_id)
138        ),
139        default.clone()
140    );
141
142    let inner_let_if = make::if_then_else(
143        has_field,
144        update_with_merge(record_id, field, Field::from(default)),
145        insert,
146    );
147
148    make::if_then_else(field_not_defined, inner_let_if, Term::Var(record_id))
149}
150
151/// Update a record field by merging it with a singleton record containing the new value.
152///
153/// ```nickel
154/// record_id & { "<id>" = <field> }
155/// ```
156fn update_with_merge(record_id: LocIdent, id: LocIdent, field: Field) -> RichTerm {
157    use crate::{
158        label::{MergeKind, MergeLabel},
159        term::IndexMap,
160    };
161
162    let annot = field.metadata.annotation.clone();
163    let pos_value = field.value.as_ref().and_then(|value| value.pos.into_opt());
164
165    let singleton = Term::Record(RecordData {
166        fields: IndexMap::from([(id, field)]),
167        ..Default::default()
168    });
169    // Right now, patterns are compiled on-the-fly during evaluation. We thus need to
170    // perform the gen_pending_contract transformation manually, or the contracts will
171    // just be ignored. One step suffices, as we create a singleton record that doesn't
172    // contain other non-transformed records (the default value, if any, has been
173    // transformed normally).
174    //
175    // unwrap(): typechecking ensures that there are no unbound variables at this point
176    let singleton =
177        crate::transform::gen_pending_contracts::transform_one(singleton.into()).unwrap();
178
179    let span = annot
180        .iter()
181        .filter_map(|labeled_ty| labeled_ty.label.span)
182        .chain(pos_value)
183        // We fuse all the definite spans together.
184        // unwrap(): all span should come from the same file
185        .reduce(|span1, span2| span1.fuse(span2).unwrap());
186
187    let merge_label = MergeLabel {
188        span,
189        kind: MergeKind::Standard,
190    };
191
192    make::op2(
193        BinaryOp::Merge(merge_label),
194        Term::Var(record_id),
195        singleton,
196    )
197}
198
199pub trait CompilePart {
200    /// Compile part of a broader pattern to a Nickel expression with two free variables (which
201    /// is equivalent to a function of two arguments):
202    ///
203    /// 1. The value being matched on (`value_id`)
204    /// 2. A dictionary of the current assignment of pattern variables to sub-expressions of the
205    ///    matched expression
206    ///
207    /// The compiled expression must return either `null` if the pattern doesn't match, or a
208    /// dictionary mapping pattern variables to the corresponding sub-expressions of the
209    /// matched value if the pattern matched with success.
210    ///
211    /// Although the `value` and `bindings` could be passed as [crate::term::RichTerm] in all
212    /// generality, forcing them to be variable makes it less likely that the compilation
213    /// duplicates sub-expressions: because the value and the bindings must always be passed in
214    /// a variable, they are free to share without risk of duplicating work.
215    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm;
216}
217
218impl CompilePart for Pattern {
219    // Compilation of the top-level pattern wrapper (code between < and > is Rust code, think
220    // a template of some sort):
221    //
222    // < if let Some(alias) = alias { >
223    //   let bindings = %record/insert% <alias> bindings arg in
224    // < } >
225    // <pattern_data.compile()> arg bindings
226    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
227        // The last instruction
228        // <pattern_data.compile()>
229        let continuation = self.data.compile_part(value_id, bindings_id);
230
231        // Either
232        //
233        // let bindings = %record/insert% <alias> bindings arg in
234        // continuation
235        //
236        // if `alias` is set, or just `continuation` otherwise.
237        if let Some(alias) = self.alias {
238            make::let_one_in(
239                bindings_id,
240                insert_binding(alias, value_id, bindings_id),
241                continuation,
242            )
243        } else {
244            continuation
245        }
246    }
247}
248
249impl CompilePart for PatternData {
250    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
251        match self {
252            PatternData::Wildcard => Term::Var(bindings_id).into(),
253            PatternData::Any(id) => {
254                // %record/insert% "<id>" value_id bindings_id
255                insert_binding(*id, value_id, bindings_id)
256            }
257            PatternData::Record(pat) => pat.compile_part(value_id, bindings_id),
258            PatternData::Array(pat) => pat.compile_part(value_id, bindings_id),
259            PatternData::Enum(pat) => pat.compile_part(value_id, bindings_id),
260            PatternData::Constant(pat) => pat.compile_part(value_id, bindings_id),
261            PatternData::Or(pat) => pat.compile_part(value_id, bindings_id),
262        }
263    }
264}
265
266impl CompilePart for ConstantPattern {
267    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
268        self.data.compile_part(value_id, bindings_id)
269    }
270}
271
272impl CompilePart for ConstantPatternData {
273    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
274        let compile_constant = |nickel_type: &str, value: Term| {
275            // if %typeof% value_id == '<nickel_type> && value_id == <value> then
276            //   bindings_id
277            // else
278            //   null
279
280            // %typeof% value_id == '<nickel_type>
281            let type_matches = make::op2(
282                BinaryOp::Eq,
283                make::op1(UnaryOp::Typeof, Term::Var(value_id)),
284                Term::Enum(nickel_type.into()),
285            );
286
287            // value_id == <value>
288            let value_matches = make::op2(BinaryOp::Eq, Term::Var(value_id), value);
289
290            // <type_matches> && <value_matches>
291            let if_condition = mk_app!(make::op1(UnaryOp::BoolAnd, type_matches), value_matches);
292
293            make::if_then_else(if_condition, Term::Var(bindings_id), Term::Null)
294        };
295
296        match self {
297            ConstantPatternData::Bool(b) => compile_constant("Bool", Term::Bool(*b)),
298            ConstantPatternData::Number(n) => compile_constant("Number", Term::Num(n.clone())),
299            ConstantPatternData::String(s) => compile_constant("String", Term::Str(s.clone())),
300            ConstantPatternData::Null => compile_constant("Other", Term::Null),
301        }
302    }
303}
304
305impl CompilePart for OrPattern {
306    // Compilation of or patterns.
307    //
308    //  <fold pattern in patterns
309    //   - cont is the accumulator
310    //   - initial accumulator is `null`
311    //  >
312    //
313    //  let prev_bindings = cont in
314    //
315    //  # if one of the previous patterns already matched, we just stop here and return the
316    //  # resulting updated bindings. Otherwise, we try the current one
317    //  if prev_bindings != null then
318    //    prev_bindings
319    //  else
320    //    <pattern.compile(value_id, bindings_id)>
321    //  <end fold>
322    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
323        self.patterns
324            .iter()
325            .fold(Term::Null.into(), |cont, pattern| {
326                let prev_bindings = LocIdent::fresh();
327
328                let is_prev_not_null = make::op1(
329                    UnaryOp::BoolNot,
330                    make::op2(BinaryOp::Eq, Term::Var(prev_bindings), Term::Null),
331                );
332
333                let if_block = make::if_then_else(
334                    is_prev_not_null,
335                    Term::Var(prev_bindings),
336                    pattern.compile_part(value_id, bindings_id),
337                );
338
339                make::let_one_in(prev_bindings, cont, if_block)
340            })
341    }
342}
343
344impl CompilePart for RecordPattern {
345    // Compilation of the top-level record pattern wrapper.
346    //
347    // To check that the value doesn't contain extra fields, or to capture the rest of the
348    // record when using the `..rest` construct, we need to remove matched fields from the
349    // original value at each stage and thread this working value in addition to the bindings.
350    //
351    // We don't have tuples, and to avoid adding an indirection (by storing the current state
352    // as `{rest, bindings}` where bindings itself is a record), we store this rest alongside
353    // the bindings in a special field which is a freshly generated identifier. This is an
354    // implementation detail which isn't very hard to change, should we have to.
355    //
356    // if %typeof% value_id == 'Record
357    //   let final_bindings_id =
358    //     <fold (field, value) in fields
359    //      - cont is the accumulator
360    //      - initial accumulator is `%record/insert% "<REST_FIELD>" bindings_id value_id`
361    //      >
362    //
363    //     # If there is a default value, we must set it before the %record/field_is_defined% check below,
364    //     # because the default acts like if the original matched value always have this field
365    //     # defined
366    //     <if field.default.is_some()>
367    //       let value_id = <with_default_value value_id field default> in
368    //     <end if>
369    //
370    //      if %record/field_is_defined% field value_id then
371    //         <if !field_pat.annotation.is_empty()>
372    //           let value_id = value_id & { "<field>" | <field_pat.annotation> } in
373    //         <end if>
374    //
375    //         let local_bindings_id = cont in
376    //
377    //         if local_bindings_id == null then
378    //           null
379    //         else
380    //           let local_value_id = %static_access(field)% (%static_access(REST_FIELD)% local_bindings_id)
381    //           let local_bindings_id = <remove_from_rest(field, local_bindings_id)> in
382    //           <field.compile_part(local_value_id, local_bindings_id)>
383    //       else
384    //         null
385    //     <end fold>
386    //   in
387    //
388    //   if final_bindings_id == null then
389    //     null
390    //   else
391    //     <if self.tail is empty>
392    //       # if tail is empty, check that the value doesn't contain extra fields
393    //       if (%static_access% <REST_FIELD> final_bindings_id) != {} then
394    //         null
395    //       else
396    //         %record/remove% "<REST>" final_bindings_id
397    //     <else if self.tail is capture(rest)>
398    //       # move the rest from REST_FIELD to rest, and remove REST_FIELD
399    //       %record/remove% "<REST>"
400    //         (%record/insert% <rest>
401    //           final_bindings_id
402    //           (%static_access% <REST_FIELD> final_bindings_id)
403    //         )
404    //     <else if self.tail is open>
405    //       %record/remove% "<REST>" final_bindings_id
406    //     <end if>
407    // else
408    //   null
409    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
410        let rest_field = LocIdent::fresh();
411
412        // `%record/insert% "<REST>" bindings_id value_id`
413        let init_bindings = mk_app!(
414            make::op2(
415                record_insert(),
416                Term::Str(rest_field.into()),
417                Term::Var(bindings_id)
418            ),
419            Term::Var(value_id)
420        );
421
422        // The fold block:
423        //
424        // <fold (field, value) in fields
425        //  - cont is the accumulator
426        //  - initial accumulator is `%record/insert% "<REST>" bindings_id value_id`
427        // >
428        //
429        // # If there is a default value, we must set it before the %record/field_is_defined% check below,
430        // # because the default acts like if the original matched value always have this field
431        // # defined
432        // <if field.default.is_some()>
433        //   let value_id = <with_default_value value_id field default> in
434        // <end if>
435        //
436        // if %record/field_is_defined% field value_id then
437        //   # If the field is present, we apply the potential contracts coming from user-provided
438        //   # annotations before anything else. We just offload the actual work to `&`
439        //   <if !field_pat.annotation.is_empty() >
440        //     let value_id = value_id & { "<field>" | <field_pat.annotation> } in
441        //   <end if>
442        //
443        //   let local_bindings_id = cont in
444        //
445        //   if local_bindings_id == null then
446        //     null
447        //   else
448        //     let local_value_id = %static_access(field)% (%static_access(REST_FIELD)% local_bindings_id) in
449        //     let local_bindings_id = <remove_from_rest(field, local_bindings_id)> in
450        //     <field.compile_part(local_value_id, local_bindings_id)>
451        let fold_block: RichTerm = self.patterns.iter().fold(init_bindings, |cont, field_pat| {
452            let field = field_pat.matched_id;
453            let local_bindings_id = LocIdent::fresh();
454            let local_value_id = LocIdent::fresh();
455
456            // let bindings_id = <remove_from_rest(field, local_bindings_id)> in
457            // <field.compile_part(local_value_id, local_bindings_id)>
458            let updated_bindings_let = make::let_one_in(
459                local_bindings_id,
460                remove_from_rest(rest_field, field, local_bindings_id),
461                field_pat
462                    .pattern
463                    .compile_part(local_value_id, local_bindings_id),
464            );
465
466            // %static_access(field)% (%static_access(REST_FIELD)% local_bindings_id)
467            let extracted_value = make::op1(
468                UnaryOp::RecordAccess(field),
469                make::op1(
470                    UnaryOp::RecordAccess(rest_field),
471                    Term::Var(local_bindings_id),
472                ),
473            );
474
475            // let local_value_id = <extracted_value> in <updated_bindings_let>
476            let inner_else_block =
477                make::let_one_in(local_value_id, extracted_value, updated_bindings_let);
478
479            // The innermost if:
480            //
481            // if local_bindings_id == null then
482            //   null
483            // else
484            //  <inner_else_block>
485            let inner_if = make::if_then_else(
486                make::op2(BinaryOp::Eq, Term::Var(local_bindings_id), Term::Null),
487                Term::Null,
488                inner_else_block,
489            );
490
491            // let local_bindings_id = cont in <value_let>
492            let binding_cont_let = make::let_one_in(local_bindings_id, cont, inner_if);
493
494            // <if !field.annotation.is_empty()>
495            //   let value_id = <update_with_merge...> in <binding_cont_let>
496            // <end if>
497            let optional_merge = if !field_pat.annotation.is_empty() {
498                make::let_one_in(
499                    value_id,
500                    update_with_merge(
501                        value_id,
502                        field,
503                        Field::from(FieldMetadata {
504                            annotation: field_pat.annotation.clone(),
505                            ..Default::default()
506                        }),
507                    ),
508                    binding_cont_let,
509                )
510            } else {
511                binding_cont_let
512            };
513
514            // %record/field_is_defined% field value_id
515            let has_field = make::op2(
516                BinaryOp::RecordFieldIsDefined(RecordOpKind::ConsiderAllFields),
517                Term::Str(field.label().into()),
518                Term::Var(value_id),
519            );
520
521            // if <has_field> then <optional_merge> else null
522            let enclosing_if = make::if_then_else(has_field, optional_merge, Term::Null);
523
524            // <if field_pat.default.is_some()>
525            //   let value_id = <with_default_value value_id field default> in
526            // <end if>
527            if let Some(default) = field_pat.default.as_ref() {
528                make::let_one_in(
529                    value_id,
530                    with_default_value(value_id, field, default.clone()),
531                    enclosing_if,
532                )
533            } else {
534                enclosing_if
535            }
536        });
537
538        // %typeof% value_id == 'Record
539        let is_record: RichTerm = make::op2(
540            BinaryOp::Eq,
541            make::op1(UnaryOp::Typeof, Term::Var(value_id)),
542            Term::Enum("Record".into()),
543        );
544
545        let final_bindings_id = LocIdent::fresh();
546
547        // %record/remove% "<REST>" final_bindings_id
548        let bindings_without_rest = make::op2(
549            BinaryOp::RecordRemove(RecordOpKind::ConsiderAllFields),
550            Term::Str(rest_field.into()),
551            Term::Var(final_bindings_id),
552        );
553
554        // the else block which depends on the tail of the record pattern
555        let tail_block = match self.tail {
556            // if (%static_access% <REST_FIELD> final_bindings_id) != {} then
557            //   null
558            // else
559            //   %record/remove% "<REST>" final_bindings_id
560            TailPattern::Empty => make::if_then_else(
561                make::op1(
562                    UnaryOp::BoolNot,
563                    make::op2(
564                        BinaryOp::Eq,
565                        make::op1(
566                            UnaryOp::RecordAccess(rest_field),
567                            Term::Var(final_bindings_id),
568                        ),
569                        Term::Record(RecordData::empty()),
570                    ),
571                ),
572                Term::Null,
573                bindings_without_rest,
574            ),
575            // %record/remove% "<REST>"
576            //   (%record/insert% <rest>
577            //     final_bindings_id
578            //     (%static_access% <REST_FIELD> final_bindings_id)
579            //   )
580            TailPattern::Capture(rest) => make::op2(
581                BinaryOp::RecordRemove(RecordOpKind::ConsiderAllFields),
582                Term::Str(rest_field.into()),
583                mk_app!(
584                    make::op2(
585                        record_insert(),
586                        Term::Str(rest.label().into()),
587                        Term::Var(final_bindings_id),
588                    ),
589                    make::op1(
590                        UnaryOp::RecordAccess(rest_field),
591                        Term::Var(final_bindings_id)
592                    )
593                ),
594            ),
595            // %record/remove% "<REST>" final_bindings_id
596            TailPattern::Open => bindings_without_rest,
597        };
598
599        // the last `final_bindings_id != null` guard:
600        //
601        // if final_bindings_id == null then
602        //   null
603        // else
604        //   <tail_block>
605        let guard_tail_block = make::if_then_else(
606            make::op2(BinaryOp::Eq, Term::Var(final_bindings_id), Term::Null),
607            Term::Null,
608            tail_block,
609        );
610
611        // The let enclosing the fold block and the final block:
612        // let final_bindings_id = <fold_block> in <tail_block>
613        let outer_let = make::let_one_in(final_bindings_id, fold_block, guard_tail_block);
614
615        // if <is_record> then <outer_let> else null
616        make::if_then_else(is_record, outer_let, Term::Null)
617    }
618}
619
620impl CompilePart for ArrayPattern {
621    // Compilation of an array pattern.
622    //
623    // let value_len = %array/length% value_id in
624    //
625    // <if self.is_open()>
626    // if %typeof% value_id == 'Array && value_len >= <self.patterns.len()>
627    // <else>
628    // if %typeof% value_id == 'Array && value_len == <self.patterns.len()>
629    // <end if>
630    //
631    //   let final_bindings_id =
632    //     <fold (idx, elem_pat) in 0..self.patterns.len()
633    //      - cont is the accumulator
634    //      - initial accumulator is `bindings_id`
635    //      >
636    //
637    //       let local_bindings_id = cont in
638    //       if local_bindings_id == null then
639    //         null
640    //       else
641    //         let local_value_id = %array/at% <idx> value_id in
642    //         <elem_pat.compile_part(local_value_id, local_bindings_id)>
643    //
644    //     <end fold>
645    //   in
646    //
647    //   if final_bindings_id == null then
648    //     null
649    //   else
650    //     <if self.tail is capture(rest)>
651    //       %record/insert%
652    //         <rest>
653    //         final_bindings_id
654    //         (%array/slice% <self.patterns.len()> value_len value_id)
655    //     <else>
656    //       final_bindings_id
657    //     <end if>
658    // else
659    //   null
660    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
661        let value_len_id = LocIdent::fresh();
662        let pats_len = Term::Num(self.patterns.len().into());
663
664        //     <fold (idx) in 0..self.patterns.len()
665        //      - cont is the accumulator
666        //      - initial accumulator is `bindings_id`
667        //      >
668        //
669        //       let local_bindings_id = cont in
670        //       if local_bindings_id == null then
671        //         null
672        //       else
673        //         let local_value_id = %array/at% <idx> value_id in
674        //         <self.patterns[idx].compile_part(local_value_id, local_bindings_id)>
675        //
676        //     <end fold>
677        let fold_block: RichTerm = self.patterns.iter().enumerate().fold(
678            Term::Var(bindings_id).into(),
679            |cont, (idx, elem_pat)| {
680                let local_bindings_id = LocIdent::fresh();
681                let local_value_id = LocIdent::fresh();
682
683                // <self.patterns[idx].compile_part(local_value_id, local_bindings_id)>
684                let updated_bindings_let = elem_pat.compile_part(local_value_id, local_bindings_id);
685
686                // %array/at% idx value_id
687                let extracted_value = make::op2(
688                    BinaryOp::ArrayAt,
689                    Term::Var(value_id),
690                    Term::Num(idx.into()),
691                );
692
693                // let local_value_id = <extracted_value> in <updated_bindings_let>
694                let inner_else_block =
695                    make::let_one_in(local_value_id, extracted_value, updated_bindings_let);
696
697                // The innermost if:
698                //
699                // if local_bindings_id == null then
700                //   null
701                // else
702                //  <inner_else_block>
703                let inner_if = make::if_then_else(
704                    make::op2(BinaryOp::Eq, Term::Var(local_bindings_id), Term::Null),
705                    Term::Null,
706                    inner_else_block,
707                );
708
709                // let local_bindings_id = cont in <inner_if>
710                make::let_one_in(local_bindings_id, cont, inner_if)
711            },
712        );
713
714        // %typeof% value_id == 'Array
715        let is_array: RichTerm = make::op2(
716            BinaryOp::Eq,
717            make::op1(UnaryOp::Typeof, Term::Var(value_id)),
718            Term::Enum("Array".into()),
719        );
720
721        let comp_op = if self.is_open() {
722            BinaryOp::GreaterOrEq
723        } else {
724            BinaryOp::Eq
725        };
726
727        // <is_array> && value_len <comp_op> <self.patterns.len()>
728        let outer_check = mk_app!(
729            make::op1(UnaryOp::BoolAnd, is_array),
730            make::op2(comp_op, Term::Var(value_len_id), pats_len.clone(),)
731        );
732
733        let final_bindings_id = LocIdent::fresh();
734
735        // the else block which depends on the tail of the record pattern
736        let tail_block = match self.tail {
737            // final_bindings_id
738            TailPattern::Empty | TailPattern::Open => make::var(final_bindings_id),
739            // %record/insert%
740            //    <rest>
741            //    final_bindings_id
742            //    (%array/slice% <self.patterns.len()> value_len value_id)
743            TailPattern::Capture(rest) => mk_app!(
744                make::op2(
745                    record_insert(),
746                    Term::Str(rest.label().into()),
747                    Term::Var(final_bindings_id),
748                ),
749                make::opn(
750                    NAryOp::ArraySlice,
751                    vec![pats_len, Term::Var(value_len_id), Term::Var(value_id)]
752                )
753            ),
754        };
755
756        // the last `final_bindings_id != null` guard:
757        //
758        // if final_bindings_id == null then
759        //   null
760        // else
761        //   <tail_block>
762        let guard_tail_block = make::if_then_else(
763            make::op2(BinaryOp::Eq, Term::Var(final_bindings_id), Term::Null),
764            Term::Null,
765            tail_block,
766        );
767
768        // The let enclosing the fold block and the let binding `final_bindings_id`:
769        // let final_bindings_id = <fold_block> in <tail_block>
770        let outer_let = make::let_one_in(final_bindings_id, fold_block, guard_tail_block);
771
772        // if <outer_check> then <outer_let> else null
773        let outer_if = make::if_then_else(outer_check, outer_let, Term::Null);
774
775        // finally, we need to bind `value_len_id` to the length of the array
776        make::let_one_in(
777            value_len_id,
778            make::op1(UnaryOp::ArrayLength, Term::Var(value_id)),
779            outer_if,
780        )
781    }
782}
783
784impl CompilePart for EnumPattern {
785    fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
786        // %enum/get_tag% value_id == '<self.tag>
787        let tag_matches = make::op2(
788            BinaryOp::Eq,
789            make::op1(UnaryOp::EnumGetTag, Term::Var(value_id)),
790            Term::Enum(self.tag),
791        );
792
793        if let Some(pat) = &self.pattern {
794            // if %enum/is_variant% value_id && %enum/get_tag% value_id == '<self.tag> then
795            //   let value_id = %enum/get_arg% value_id in
796            //   <pattern.compile(value_id, bindings_id)>
797            // else
798            //   null
799
800            // %enum/is_variant% value_id && <tag_matches>
801            let if_condition = mk_app!(
802                make::op1(
803                    UnaryOp::BoolAnd,
804                    make::op1(UnaryOp::EnumIsVariant, Term::Var(value_id)),
805                ),
806                tag_matches
807            );
808
809            make::if_then_else(
810                if_condition,
811                make::let_one_in(
812                    value_id,
813                    make::op1(UnaryOp::EnumGetArg, Term::Var(value_id)),
814                    pat.compile_part(value_id, bindings_id),
815                ),
816                Term::Null,
817            )
818        } else {
819            // if %typeof% value_id == 'Enum && !(%enum/is_variant% value_id) && <tag_matches> then
820            //   bindings_id
821            // else
822            //   null
823
824            // %typeof% value_id == 'Enum
825            let is_enum = make::op2(
826                BinaryOp::Eq,
827                make::op1(UnaryOp::Typeof, Term::Var(value_id)),
828                Term::Enum("Enum".into()),
829            );
830
831            // !(%enum/is_variant% value_id)
832            let is_enum_tag = make::op1(
833                UnaryOp::BoolNot,
834                make::op1(UnaryOp::EnumIsVariant, Term::Var(value_id)),
835            );
836
837            // <is_enum> && <is_enum_tag> && <tag_matches>
838            let if_condition = mk_app!(
839                make::op1(UnaryOp::BoolAnd, is_enum,),
840                mk_app!(make::op1(UnaryOp::BoolAnd, is_enum_tag,), tag_matches)
841            );
842
843            make::if_then_else(if_condition, Term::Var(bindings_id), Term::Null)
844        }
845    }
846}
847
848pub trait Compile {
849    /// Compile a match expression to a Nickel expression with the provided `value_id` as a
850    /// free variable (representing a placeholder for the matched expression).
851    fn compile(self, value: RichTerm, pos: TermPos) -> RichTerm;
852}
853
854impl Compile for MatchData {
855    // Compilation of a full match expression (code between < and > is Rust code, think of it as a
856    // kind of templating). Note that some special cases compile differently as optimizations.
857    //
858    // let value_id = value in
859    //
860    // <fold (pattern, body) in branches.rev()
861    //  - cont is the accumulator
862    //  - initial accumulator is the default branch (or error if not default branch)
863    // >
864    //    let init_bindings_id = {} in
865    //    let bindings_id = <pattern.compile()> value_id init_bindings_id in
866    //
867    //    if bindings_id == null then
868    //      cont
869    //    else
870    //      # this primop evaluates body with an environment extended with bindings_id
871    //      %pattern_branch% body bindings_id
872    fn compile(mut self, value: RichTerm, pos: TermPos) -> RichTerm {
873        increment!("pattern_compile");
874
875        if self.branches.iter().all(|branch| {
876            // While we could get something working even with a guard, it's a bit more work and
877            // there's no current incentive to do so (a guard on a tags-only match is arguably less
878            // common, as such patterns don't bind any variable). For the time being, we just
879            // exclude guards from the tags-only optimization.
880            matches!(
881                branch.pattern.data,
882                PatternData::Enum(EnumPattern { pattern: None, .. }) | PatternData::Wildcard
883            ) && branch.guard.is_none()
884        }) {
885            let wildcard_pat = self.branches.iter().enumerate().find_map(
886                |(
887                    idx,
888                    MatchBranch {
889                        pattern,
890                        guard,
891                        body,
892                    },
893                )| {
894                    if matches!((&pattern.data, guard), (PatternData::Wildcard, None)) {
895                        Some((idx, body.clone()))
896                    } else {
897                        None
898                    }
899                },
900            );
901
902            // If we find a wildcard pattern, we record its index in order to discard all the
903            // patterns coming after the wildcard, because they are unreachable.
904            let default = if let Some((idx, body)) = wildcard_pat {
905                self.branches.truncate(idx + 1);
906                Some(body)
907            } else {
908                None
909            };
910
911            let tags_only = self
912                .branches
913                .into_iter()
914                .filter_map(
915                    |MatchBranch {
916                         pattern,
917                         guard: _,
918                         body,
919                     }| {
920                        if let PatternData::Enum(EnumPattern { tag, .. }) = pattern.data {
921                            Some((tag, body))
922                        } else {
923                            None
924                        }
925                    },
926                )
927                .collect();
928
929            return TagsOnlyMatch {
930                branches: tags_only,
931                default,
932            }
933            .compile(value, pos);
934        }
935
936        let error_case = RichTerm::new(
937            Term::RuntimeError(EvalError::NonExhaustiveMatch {
938                value: value.clone(),
939                pos,
940            }),
941            pos,
942        );
943
944        let value_id = LocIdent::fresh();
945
946        // The fold block:
947        //
948        // <for branch in branches.rev()
949        //  - cont is the accumulator
950        //  - initial accumulator is the default branch (or error if not default branch)
951        // >
952        //    let init_bindings_id = {} in
953        //    let bindings_id = <pattern.compile_part(value_id, init_bindings)> in
954        //
955        //    if bindings_id == null || !<guard> then
956        //      cont
957        //    else
958        //      # this primop evaluates body with an environment extended with bindings_id
959        //      %pattern_branch% body bindings_id
960        let fold_block = self
961            .branches
962            .into_iter()
963            .rev()
964            .fold(error_case, |cont, branch| {
965                let init_bindings_id = LocIdent::fresh();
966                let bindings_id = LocIdent::fresh();
967
968                // inner if condition:
969                // bindings_id == null || !<guard>
970                let inner_if_cond = make::op2(BinaryOp::Eq, Term::Var(bindings_id), Term::Null);
971                let inner_if_cond = if let Some(guard) = branch.guard {
972                    // the guard must be evaluated in the same environment as the body of the
973                    // branch, as it might use bindings introduced by the pattern. Since `||` is
974                    // lazy in Nickel, we know that `bindings_id` is not null if the guard
975                    // condition is ever evaluated.
976                    let guard_cond = mk_app!(
977                        make::op1(UnaryOp::PatternBranch, Term::Var(bindings_id)),
978                        guard
979                    );
980
981                    mk_app!(
982                        make::op1(UnaryOp::BoolOr, inner_if_cond),
983                        make::op1(UnaryOp::BoolNot, guard_cond)
984                    )
985                } else {
986                    inner_if_cond
987                };
988
989                // inner if block:
990                //
991                // if bindings_id == null then
992                //   cont
993                // else
994                //   # this primop evaluates body with an environment extended with bindings_id
995                //   %pattern_branch% bindings_id body
996                let inner = make::if_then_else(
997                    inner_if_cond,
998                    cont,
999                    mk_app!(
1000                        make::op1(UnaryOp::PatternBranch, Term::Var(bindings_id),),
1001                        branch.body
1002                    ),
1003                );
1004
1005                // The two initial chained let-bindings:
1006                //
1007                // let init_bindings_id = {} in
1008                // let bindings_id = <pattern.compile_part(value_id, init_bindings)> in
1009                // <inner>
1010                make::let_one_in(
1011                    init_bindings_id,
1012                    Term::Record(RecordData::empty()),
1013                    make::let_one_in(
1014                        bindings_id,
1015                        branch.pattern.compile_part(value_id, init_bindings_id),
1016                        inner,
1017                    ),
1018                )
1019            });
1020
1021        // let value_id = value in <fold_block>
1022        make::let_one_in(value_id, value, fold_block)
1023    }
1024}
1025
1026/// Simple wrapper used to implement specialization of match statements when all of the patterns
1027/// are enum tags. Instead of a sequence of conditionals (which has linear time complexity), we use
1028/// a special primops based on a hashmap, which has amortized constant time complexity.
1029struct TagsOnlyMatch {
1030    branches: Vec<(LocIdent, RichTerm)>,
1031    default: Option<RichTerm>,
1032}
1033
1034impl Compile for TagsOnlyMatch {
1035    fn compile(self, value: RichTerm, pos: TermPos) -> RichTerm {
1036        increment!("pattern_comile(tags_only_match)");
1037
1038        // We simply use the corresponding specialized primop in that case.
1039        let match_op = mk_app!(
1040            make::op1(
1041                UnaryOp::TagsOnlyMatch {
1042                    has_default: self.default.is_some()
1043                },
1044                value
1045            )
1046            .with_pos(pos),
1047            Term::Record(RecordData::with_field_values(self.branches.into_iter()))
1048        );
1049
1050        let match_op = if let Some(default) = self.default {
1051            mk_app!(match_op, default)
1052        } else {
1053            match_op
1054        };
1055
1056        match_op.with_pos(pos)
1057    }
1058}