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}