Skip to main content

nickel_lang_core/eval/
operation.rs

1//! Implementation of primitive operations.
2//!
3//! Define functions which perform the evaluation of primitive operators. The machinery required
4//! for the strict evaluation of the operands is mainly handled by [crate::eval], and marginally in
5//! [`VirtualMachine::continue_op`].
6//!
7//! On the other hand, the functions `process_unary_operation` and `process_binary_operation`
8//! receive evaluated operands and implement the actual semantics of operators.
9use super::{
10    Cache, Closure, Environment, ErrorKind, ImportResolver, VirtualMachine,
11    cache::lazy::Thunk,
12    contract_eq::contract_eq,
13    merge::{self, MergeMode, split},
14    stack::{EqItem, Op1ContItem, Op2FirstContItem, Op2SecondContItem, PrimopAppInfo, StrAccItem},
15    subst,
16    value::{
17        Array, ArrayData, Container, EnumVariantData, NickelValue, TypeData, ValueContentRef,
18        ValueContentRefMut,
19    },
20};
21
22#[cfg(feature = "nix-experimental")]
23use crate::nix_ffi;
24
25use crate::{
26    cache::InputFormat,
27    closurize::Closurize,
28    combine::Combine,
29    error::{EvalErrorKind, IllegalPolymorphicTailAction, Warning},
30    identifier::LocIdent,
31    label::{Polarity, TypeVarData, ty_path},
32    metrics::increment,
33    mk_app, mk_fun, mk_record,
34    position::PosIdx,
35    serialize::{self, ExportFormat, yaml::Listify},
36    stdlib::internals,
37    term::{make as mk_term, record::*, string::NickelString, *},
38};
39
40use base64::Engine;
41use nickel_lang_parser::utils::parse_number_sci;
42
43#[cfg(feature = "metrics")]
44use crate::pretty::PrettyPrintCap;
45
46use malachite::{
47    Integer,
48    base::{
49        num::{arithmetic::traits::Pow, basic::traits::Zero, conversion::traits::RoundingFrom},
50        rounding_modes::RoundingMode,
51    },
52};
53
54use md5::digest::Digest;
55use simple_counter::*;
56use unicode_segmentation::UnicodeSegmentation;
57
58use std::{convert::TryFrom, iter::Extend, rc::Rc};
59
60generate_counter!(FreshVariableCounter, usize);
61
62/// Result of the equality of two terms.
63///
64/// The equality of two terms can either be computed directly for base types (`Number`, `String`,
65/// etc.), in which case `Bool` is returned. Otherwise, composite values such as arrays or records
66/// generate new subequalities, as represented by the last variant as a vector of pairs of terms.
67/// This list should be non-empty (it if was empty, `eq` should have returned `Bool(true)`
68/// directly).  The first element of this non-empty list is encoded as the two first parameters of
69/// `Eqs`, while the last vector parameter is the (potentially empty) tail.
70///
71/// See [`eq`].
72enum EqResult {
73    Bool(bool),
74    Eqs(NickelValue, NickelValue, Vec<(Closure, Closure)>),
75}
76
77/// All the data required to evaluate a unary operation.
78struct Op1EvalData {
79    /// The unary operation to apply.
80    op: UnaryOp,
81    /// The evaluated argument.
82    arg: Closure,
83    /// The position of the original expression of the argument, when it was put on the stack for
84    /// evaluation.
85    orig_pos_arg: PosIdx,
86    /// The position of the primop application.
87    pos_op: PosIdx,
88}
89
90/// All the data required to evaluate a binary operation.
91struct Op2EvalData {
92    /// The binary operation to apply.
93    op: BinaryOp,
94    /// The first argument evaluated.
95    arg1: Closure,
96    /// The second argument evaluated.
97    arg2: Closure,
98    /// The position of the original expression of the first argument, when it was put on the stack
99    /// for evaluation.
100    orig_pos_arg1: PosIdx,
101    /// The position of the original expression of the second argument, when it was put on the
102    /// stack for evaluation.
103    orig_pos_arg2: PosIdx,
104    /// The position of the primop application.
105    pos_op: PosIdx,
106}
107
108struct OpNEvalData {
109    /// The n-ary operation to apply.
110    op: NAryOp,
111    /// The arguments evaluated, together with the position of their original expression, when it
112    /// was put on the stack for evaluation.
113    args: Vec<(Closure, PosIdx)>,
114    /// The position of the primop application.
115    pos_op: PosIdx,
116}
117
118/// A string represention of the type of the first argument of serialization-related primitive
119/// operations. This is a Nickel enum of the supported serialization formats.
120static ENUM_FORMAT: &str = "[| 'Json, 'Toml, 'Yaml, 'YamlDocuments |]";
121
122impl<'ctxt, R: ImportResolver, C: Cache> VirtualMachine<'ctxt, R, C> {
123    /// Proceeds to the next step of the evaluation of a primitive operation.
124    ///
125    /// Depending on the content of the stack, it either starts the evaluation of the next argument
126    /// or finally proceed with the operation if all arguments are evaluated.
127    pub fn continue_op(&mut self, closure: Closure) -> Result<Closure, ErrorKind> {
128        if let Some(op1_cont) = self.stack.pop_op1_cont() {
129            self.call_stack
130                .truncate(op1_cont.app_info.call_stack_size as usize);
131
132            self.eval_op1(Op1EvalData {
133                op: op1_cont.op,
134                arg: closure,
135                orig_pos_arg: op1_cont.orig_pos_arg,
136                pos_op: op1_cont.app_info.pos_idx,
137            })
138        } else if let Some(op2_fst_cont) = self.stack.pop_op2_first_cont() {
139            self.call_stack
140                .truncate(op2_fst_cont.app_info.call_stack_size as usize);
141
142            self.stack.push_op2_second_cont(Op2SecondContItem {
143                op: op2_fst_cont.op,
144                app_info: op2_fst_cont.app_info,
145                arg1_evaled: closure,
146                orig_pos_arg1: op2_fst_cont.orig_pos_arg1,
147                orig_pos_arg2: op2_fst_cont.arg2.value.pos_idx(),
148            });
149
150            Ok(op2_fst_cont.arg2)
151        } else if let Some(op2_snd_cont) = self.stack.pop_op2_second_cont() {
152            self.call_stack
153                .truncate(op2_snd_cont.app_info.call_stack_size as usize);
154
155            self.eval_op2(Op2EvalData {
156                op: op2_snd_cont.op,
157                arg1: op2_snd_cont.arg1_evaled,
158                arg2: closure,
159                orig_pos_arg1: op2_snd_cont.orig_pos_arg1,
160                orig_pos_arg2: op2_snd_cont.orig_pos_arg2,
161                pos_op: op2_snd_cont.app_info.pos_idx,
162            })
163        } else if let Some(mut opn_cont) = self.stack.pop_opn_cont() {
164            self.call_stack
165                .truncate(opn_cont.app_info.call_stack_size as usize);
166
167            opn_cont.evaluated.push((closure, opn_cont.current_pos_idx));
168
169            if let Some(next) = opn_cont.pending.pop() {
170                opn_cont.current_pos_idx = next.value.pos_idx();
171                self.stack.push_opn_cont(opn_cont);
172
173                Ok(next)
174            } else {
175                self.eval_opn(OpNEvalData {
176                    op: opn_cont.op,
177                    args: opn_cont.evaluated,
178                    pos_op: opn_cont.app_info.pos_idx,
179                })
180            }
181        } else {
182            panic!("unexpected state of eval stack in continue_op()");
183        }
184    }
185
186    /// Evaluate a unary operation.
187    ///
188    /// The argument is expected to be evaluated (in WHNF). `pos_op` corresponds to the whole
189    /// operation position, that may be needed for error reporting.
190    fn eval_op1(&mut self, eval_data: Op1EvalData) -> Result<Closure, ErrorKind> {
191        let Op1EvalData {
192            orig_pos_arg,
193            arg: Closure { value, env },
194            pos_op,
195            op,
196        } = eval_data;
197
198        increment!(format!("primop:{op}"));
199
200        let pos = value.pos_idx();
201        let pos_op_inh = eval_data.pos_op.to_inherited();
202
203        macro_rules! mk_type_error {
204            (op_name=$op_name:expr, $expected:expr) => {
205                mk_type_error!(op_name = $op_name, $expected, value = value)
206            };
207            (op_name=$op_name:expr, $expected:expr, value=$value:expr) => {
208                Err(Box::new(EvalErrorKind::UnaryPrimopTypeError {
209                    primop: String::from($op_name),
210                    expected: String::from($expected),
211                    pos_arg: orig_pos_arg,
212                    arg_evaluated: $value,
213                }))
214            };
215            ($expected:expr) => {
216                mk_type_error!(op_name = op.to_string(), $expected)
217            };
218            ($expected:expr, value=$value:expr) => {
219                mk_type_error!(op_name = op.to_string(), $expected, value = $value)
220            };
221            ($expected:expr, value=$value:expr) => {};
222            ($expected:expr, $arg_number:expr) => {
223                mk_type_error!($expected, $arg_number, value = value)
224            };
225            ($expected:expr, $arg_number:expr, value=$value:expr) => {
226                Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
227                    primop: op.to_string(),
228                    expected: String::from($expected),
229                    arg_number: $arg_number,
230                    pos_arg: orig_pos_arg,
231                    arg_evaluated: $value,
232                    pos_op,
233                }))
234            };
235        }
236
237        match op {
238            UnaryOp::IfThenElse => {
239                if let Some(b) = value.as_bool() {
240                    let (fst, ..) = self
241                        .stack
242                        .pop_arg(&self.context.cache)
243                        .expect("if-then-else primop isn't saturated");
244                    let (snd, ..) = self
245                        .stack
246                        .pop_arg(&self.context.cache)
247                        .expect("if-then-else primop isn't saturated");
248
249                    Ok(if b { fst } else { snd })
250                } else {
251                    // Not using mk_type_error! because of a non-uniform message
252                    Err(Box::new(EvalErrorKind::TypeError {
253                        expected: String::from("Bool"),
254                        message: String::from(
255                            "the condition in an if expression must have type Bool",
256                        ),
257                        orig_pos: orig_pos_arg,
258                        term: value,
259                    }))
260                }
261            }
262            UnaryOp::Typeof => Ok(NickelValue::enum_tag(type_tag(&value), pos_op_inh).into()),
263            UnaryOp::Cast => {
264                Ok(NickelValue::enum_variant(type_tag(&value), Some(value), pos_op_inh).into())
265            }
266            UnaryOp::BoolAnd =>
267            // The syntax should not allow partially applied boolean operators.
268            {
269                if let Some((next, ..)) = self.stack.pop_arg(&self.context.cache) {
270                    match value.as_bool() {
271                        Some(true) => Ok(next),
272                        // FIXME: this does not check that the second argument is actually a
273                        // boolean. This means `true && 2` silently evaluates to `2`. This is
274                        // simpler and more efficient, but can make debugging harder. In any case,
275                        // it should be solved only once primary operators have better support for
276                        // laziness in some arguments.
277                        Some(false) => Ok(value.with_pos_idx(pos_op_inh).into()),
278                        _ => mk_type_error!("Bool", 1),
279                    }
280                } else {
281                    Err(Box::new(EvalErrorKind::NotEnoughArgs(
282                        2,
283                        String::from("&&"),
284                        pos_op,
285                    )))
286                }
287            }
288            UnaryOp::BoolOr => {
289                if let Some((next, ..)) = self.stack.pop_arg(&self.context.cache) {
290                    match value.as_bool() {
291                        Some(true) => Ok(value.with_pos_idx(pos_op_inh).into()),
292                        // FIXME: this does not check that the second argument is actually a
293                        // boolean. This means `false || 2` silently evaluates to `2`. This is
294                        // simpler and more efficient, but can make debugging harder. In any case,
295                        // it should be solved only once primary operators have better support for
296                        // laziness in some arguments.
297                        Some(false) => Ok(next),
298                        _ => mk_type_error!("Bool", 1),
299                    }
300                } else {
301                    Err(Box::new(EvalErrorKind::NotEnoughArgs(
302                        2,
303                        String::from("||"),
304                        pos_op,
305                    )))
306                }
307            }
308            UnaryOp::BoolNot => {
309                if let Some(b) = value.as_bool() {
310                    Ok(NickelValue::bool_value_posless(!b)
311                        .with_pos_idx(pos_op_inh)
312                        .into())
313                } else {
314                    mk_type_error!("Bool")
315                }
316            }
317            UnaryOp::Blame => {
318                let Some(label) = value.as_label() else {
319                    return mk_type_error!("Label");
320                };
321
322                Err(Box::new(EvalErrorKind::BlameError {
323                    evaluated_arg: label.get_evaluated_arg(&self.context.cache),
324                    label: label.clone(),
325                }))
326            }
327            UnaryOp::EnumEmbed(_id) => {
328                if value.as_enum_variant().is_some() {
329                    Ok(value.with_pos_idx(pos_op_inh).into())
330                } else {
331                    mk_type_error!("Enum")
332                }
333            }
334            UnaryOp::TagsOnlyMatch { has_default } => {
335                let (cases_closure, ..) = self
336                    .stack
337                    .pop_arg(&self.context.cache)
338                    .expect("missing arg for match");
339
340                let default = if has_default {
341                    Some(
342                        self.stack
343                            .pop_arg(&self.context.cache)
344                            .map(|(clos, ..)| clos)
345                            .expect("missing default case for match"),
346                    )
347                } else {
348                    None
349                };
350
351                if let Some(enum_variant) = value.as_enum_variant()
352                    && enum_variant.arg.is_none()
353                {
354                    let Closure {
355                        value: cases_val,
356                        env: cases_env,
357                    } = cases_closure;
358
359                    let ValueContentRef::Record(container) = cases_val.content_ref() else {
360                        panic!("invalid argument for %match%")
361                    };
362
363                    container
364                        .get(enum_variant.tag)
365                        .map(|field| Closure {
366                            // The record containing the match cases, as well as the match primop
367                            // itself, aren't accessible in the surface language. They are
368                            // generated by the interpreter, and should never contain field without
369                            // definition.
370                            value: field
371                                .value
372                                .as_ref()
373                                .cloned()
374                                .expect("%match% cases must have a definition"),
375                            env: cases_env,
376                        })
377                        .or(default)
378                        .ok_or_else(|| {
379                            Box::new(EvalErrorKind::NonExhaustiveEnumMatch {
380                                expected: container.field_names(RecordOpKind::IgnoreEmptyOpt),
381                                found: NickelValue::enum_variant_posless(enum_variant.tag, None)
382                                    .with_pos_idx(pos),
383                                pos: pos_op_inh,
384                            })
385                        })
386                } else if let Some(clos) = default {
387                    Ok(clos)
388                } else {
389                    mk_type_error!("Enum", 2)
390                }
391            }
392            UnaryOp::LabelFlipPol => {
393                let Some(label) = value.as_label() else {
394                    return mk_type_error!("Label");
395                };
396
397                let mut label = label.clone();
398                label.polarity = label.polarity.flip();
399                Ok(NickelValue::label(label, pos_op_inh).into())
400            }
401            UnaryOp::LabelPol => {
402                if let Some(label) = value.as_label() {
403                    Ok(NickelValue::from(label.polarity)
404                        .with_pos_idx(pos_op_inh)
405                        .into())
406                } else {
407                    mk_type_error!("Label")
408                }
409            }
410            UnaryOp::LabelGoDom => {
411                let Some(label) = value.as_label() else {
412                    return mk_type_error!("Label");
413                };
414
415                let mut label = label.clone();
416                label.path.push(ty_path::Elem::Domain);
417                Ok(NickelValue::label(label, pos_op_inh).into())
418            }
419            UnaryOp::LabelGoCodom => {
420                let Some(label) = value.as_label() else {
421                    return mk_type_error!("Label");
422                };
423
424                let mut label = label.clone();
425                label.path.push(ty_path::Elem::Codomain);
426                Ok(NickelValue::label(label, pos_op_inh).into())
427            }
428            UnaryOp::LabelGoArray => {
429                let Some(label) = value.as_label() else {
430                    return mk_type_error!("Label");
431                };
432
433                let mut label = label.clone();
434                label.path.push(ty_path::Elem::Array);
435                Ok(NickelValue::label(label, pos_op_inh).into())
436            }
437            UnaryOp::LabelGoDict => {
438                let Some(label) = value.as_label() else {
439                    return mk_type_error!("Label");
440                };
441
442                let mut label = label.clone();
443                label.path.push(ty_path::Elem::Dict);
444                Ok(NickelValue::label(label, pos_op_inh).into())
445            }
446            UnaryOp::RecordAccess(id) => {
447                match value.as_record() {
448                    Some(Container::Alloc(record)) => {
449                        // We have to apply potentially pending contracts. Right now, this
450                        // means that repeated field access will re-apply the contract again
451                        // and again, which is not optimal. The same thing happens with array
452                        // contracts. There are several way to improve this, but this is left
453                        // as future work.
454                        match record
455                            .get_value_with_ctrs(&id)
456                            .map_err(|err| err.into_eval_err(pos, pos_op))?
457                        {
458                            Some(value) => {
459                                self.call_stack
460                                    .enter_field(id, pos, value.pos_idx(), pos_op);
461                                Ok(Closure { value, env })
462                            }
463                            None => match record.sealed_tail.as_ref() {
464                                Some(t) if t.has_field(&id.ident()) => {
465                                    Err(Box::new(EvalErrorKind::IllegalPolymorphicTailAccess {
466                                        action: IllegalPolymorphicTailAction::FieldAccess {
467                                            field: id.to_string(),
468                                        },
469                                        evaluated_arg: t
470                                            .label
471                                            .get_evaluated_arg(&self.context.cache),
472                                        label: t.label.clone(),
473                                    }))
474                                }
475                                _ => Err(Box::new(EvalErrorKind::FieldMissing {
476                                    id,
477                                    field_names: record.field_names(RecordOpKind::IgnoreEmptyOpt),
478                                    operator: String::from("(.)"),
479                                    pos_record: pos,
480                                    pos_op,
481                                })),
482                            }, //TODO include the position of operators on the stack
483                        }
484                    }
485                    Some(Container::Empty) => Err(Box::new(EvalErrorKind::FieldMissing {
486                        id,
487                        field_names: Vec::new(),
488                        operator: String::from("(.)"),
489                        pos_record: pos,
490                        pos_op,
491                    })),
492                    None =>
493                    // Not using mk_type_error! because of a non-uniform message
494                    {
495                        Err(Box::new(EvalErrorKind::TypeError {
496                            expected: String::from("Record"),
497                            message: String::from("field access only makes sense for records"),
498                            orig_pos: orig_pos_arg,
499                            term: value,
500                        }))
501                    }
502                }
503            }
504            UnaryOp::RecordFields(op_kind) => {
505                if let Some(container) = value.as_record() {
506                    let fields_as_terms: Array = container
507                        .field_names(op_kind)
508                        .into_iter()
509                        .map(|id| {
510                            NickelValue::string(id.label(), self.context.pos_table.push(id.pos))
511                        })
512                        .collect();
513
514                    Ok(Closure {
515                        value: NickelValue::array(fields_as_terms, Vec::new(), pos_op_inh),
516                        env,
517                    })
518                } else {
519                    mk_type_error!("Record")
520                }
521            }
522            UnaryOp::RecordValues => {
523                if let Some(container) = value.as_record() {
524                    let mut values = container
525                        .into_opt()
526                        .map(|r| r.iter_without_opts())
527                        .into_iter()
528                        .flatten()
529                        .collect::<Result<Vec<_>, _>>()
530                        .map_err(|miss_def_err| miss_def_err.into_eval_err(pos, pos_op))?;
531
532                    values.sort_by_key(|(id, _)| *id);
533                    let terms = values.into_iter().map(|(_, value)| value).collect();
534
535                    Ok(Closure {
536                        // as evaluated records are assumed to be closurized, we can assume that
537                        // the extracted array here is, in turn, also closuried.
538                        value: NickelValue::array(terms, Vec::new(), pos_op_inh),
539                        env,
540                    })
541                } else {
542                    mk_type_error!("Record")
543                }
544            }
545            UnaryOp::ArrayMap => {
546                let (f, _) = self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
547                    Box::new(EvalErrorKind::NotEnoughArgs(
548                        2,
549                        String::from("array/map"),
550                        pos_op,
551                    ))
552                })?;
553
554                let Some(cont) = value.as_array() else {
555                    return mk_type_error!("Array");
556                };
557
558                let Container::Alloc(array_data) = cont else {
559                    return Ok(value.into());
560                };
561
562                let f_as_var = f.value.closurize(&mut self.context.cache, f.env);
563
564                // Array elements are closurized to preserve laziness of data
565                // structures. It maintains the invariant that any data structure only
566                // contain indices (that is, currently, variables).
567                let ts = array_data
568                    .array
569                    .iter()
570                    .cloned()
571                    .map(|t| {
572                        let t_with_ctrs = RuntimeContract::apply_all(
573                            t,
574                            array_data.pending_contracts.iter().cloned(),
575                            pos,
576                        );
577
578                        NickelValue::term(Term::app(f_as_var.clone(), t_with_ctrs), pos_op_inh)
579                            .closurize(&mut self.context.cache, env.clone())
580                    })
581                    .collect();
582
583                Ok(NickelValue::array(ts, Vec::new(), pos_op_inh).into())
584            }
585            UnaryOp::ArrayGen => {
586                let (f, _) = self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
587                    Box::new(EvalErrorKind::NotEnoughArgs(
588                        2,
589                        String::from("array/generate"),
590                        pos_op,
591                    ))
592                })?;
593
594                let Some(n) = value.as_number() else {
595                    return mk_type_error!("Number");
596                };
597
598                if n < &Number::ZERO {
599                    return Err(Box::new(EvalErrorKind::Other(
600                        format!(
601                            "array/generate expects its first argument to be a positive number, got {n}"
602                        ),
603                        pos_op,
604                    )));
605                }
606
607                let Ok(n_int) = u32::try_from(n) else {
608                    return Err(Box::new(EvalErrorKind::Other(
609                        format!(
610                            "array/generate expects its first argument to be an integer \
611                            smaller than {}, got {n}",
612                            u32::MAX,
613                        ),
614                        pos_op,
615                    )));
616                };
617
618                let f_closure = f.value.closurize(&mut self.context.cache, f.env);
619
620                // Array elements are closurized to preserve laziness of data structures. It
621                // maintains the invariant that any data structure only contain indices (that is,
622                // currently, variables).
623                let ts = (0..n_int)
624                    .map(|n| {
625                        mk_app!(f_closure.clone(), NickelValue::number_posless(n))
626                            .closurize(&mut self.context.cache, env.clone())
627                    })
628                    .collect();
629
630                Ok(NickelValue::array(ts, Vec::new(), pos_op_inh).into())
631            }
632            UnaryOp::RecordMap => {
633                let (f, ..) = self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
634                    EvalErrorKind::NotEnoughArgs(2, String::from("record/map"), pos_op)
635                })?;
636
637                let Some(container) = value.as_record() else {
638                    return mk_type_error!("Record");
639                };
640
641                if let Container::Alloc(record) = container {
642                    let record = record.clone();
643                    // While it's certainly possible to allow mapping over
644                    // a record with a sealed tail, it's not entirely obvious
645                    // how that should behave. It's also not clear that this
646                    // is something users will actually need to do, so we've
647                    // decided to prevent this until we have a clearer idea
648                    // of potential use-cases.
649                    if let Some(tail) = record.sealed_tail {
650                        let label = Rc::unwrap_or_clone(tail).label;
651
652                        return Err(Box::new(EvalErrorKind::IllegalPolymorphicTailAccess {
653                            action: IllegalPolymorphicTailAction::Map,
654                            evaluated_arg: label.get_evaluated_arg(&self.context.cache),
655                            label,
656                        }));
657                    }
658
659                    let f_closure = f.value.closurize(&mut self.context.cache, f.env);
660
661                    // As for `ArrayMap` (see above), we closurize the content of fields
662
663                    let fields = record
664                        .fields
665                        .into_iter()
666                        .filter(|(_, field)| !field.is_empty_optional())
667                        .map_values_closurize(&mut self.context.cache, &env, |id, t| {
668                            let pos_idx = t.pos_idx().to_inherited();
669
670                            mk_app!(
671                                f_closure.clone(),
672                                NickelValue::string_posless(id.label()),
673                                t
674                            )
675                            .with_pos_idx(pos_idx)
676                        })
677                        .map_err(|miss_field_err| miss_field_err.into_eval_err(pos, pos_op))?;
678
679                    // By construction, mapping freezes the record. We set the frozen flag so
680                    // that operations that require the record to be frozen don't have to
681                    // perform the work again.
682                    let attrs = record.attrs.frozen();
683
684                    Ok(NickelValue::record(
685                        RecordData {
686                            fields,
687                            attrs,
688                            ..record
689                        },
690                        pos_op_inh,
691                    )
692                    .into())
693                } else {
694                    Ok(value.with_pos_idx(pos_op).into())
695                }
696            }
697            UnaryOp::Seq => self
698                .stack
699                .pop_arg(&self.context.cache)
700                .map(|(next, ..)| next)
701                .ok_or_else(|| {
702                    Box::new(EvalErrorKind::NotEnoughArgs(2, String::from("seq"), pos_op))
703                }),
704            UnaryOp::DeepSeq => {
705                // Build a `NickelValue` that forces a given list of terms, and at the end resumes the
706                // evaluation of the argument on the top of the stack.
707                //
708                // Requires its first argument to be non-empty.
709                fn seq_terms<I>(mut it: I, pos_op_inh: PosIdx) -> NickelValue
710                where
711                    I: Iterator<Item = NickelValue>,
712                {
713                    let first = it
714                        .next()
715                        .expect("expected the argument to be a non-empty iterator");
716
717                    it.fold(
718                        mk_term::op1(UnaryOp::DeepSeq, first).with_pos_idx(pos_op_inh),
719                        |acc, t| {
720                            mk_app!(mk_term::op1(UnaryOp::DeepSeq, t), acc).with_pos_idx(pos_op_inh)
721                        },
722                    )
723                }
724
725                match value.content_ref() {
726                    ValueContentRef::Record(Container::Alloc(record))
727                        if !record.fields.is_empty() =>
728                    {
729                        let defined = record
730                            // `iter_without_opts` takes care of applying pending contracts
731                            .iter_without_opts()
732                            .collect::<Result<Vec<_>, _>>()
733                            .map_err(|missing_def_err| {
734                                missing_def_err.into_eval_err(pos, pos_op)
735                            })?;
736
737                        let terms = defined.into_iter().map(|(_, field)| field);
738
739                        Ok(Closure {
740                            value: seq_terms(terms, pos_op),
741                            env,
742                        })
743                    }
744                    ValueContentRef::Array(Container::Alloc(array_data))
745                        if !array_data.array.is_empty() =>
746                    {
747                        let terms = seq_terms(
748                            array_data.array.iter().map(|t| {
749                                RuntimeContract::apply_all(
750                                    t.clone(),
751                                    array_data.pending_contracts.iter().cloned(),
752                                    pos.to_inherited(),
753                                )
754                                .closurize(&mut self.context.cache, env.clone())
755                            }),
756                            pos_op,
757                        );
758
759                        Ok(terms.into())
760                    }
761                    ValueContentRef::EnumVariant(EnumVariantData {
762                        tag: _,
763                        arg: Some(arg),
764                    }) => Ok(Closure {
765                        value: seq_terms(std::iter::once(arg.clone()), pos_op),
766                        env,
767                    }),
768                    _ => {
769                        if let Some((next, ..)) = self.stack.pop_arg(&self.context.cache) {
770                            Ok(next)
771                        } else {
772                            Err(Box::new(EvalErrorKind::NotEnoughArgs(
773                                2,
774                                String::from("deep_seq"),
775                                pos_op,
776                            )))
777                        }
778                    }
779                }
780            }
781            UnaryOp::ArrayLength => {
782                //TODO[RFC007]: empty array
783                if let Some(container) = value.as_array() {
784                    // A num does not have any free variable so we can drop the environment
785                    Ok(NickelValue::number(container.len(), pos_op_inh).into())
786                } else {
787                    mk_type_error!("Array")
788                }
789            }
790            UnaryOp::ChunksConcat => {
791                let mut str_acc = self.stack.pop_str_acc().expect("invalid stack state: missing string accumulator on the top while evaluating string chunks");
792
793                if let Some(s) = value.to_nickel_string() {
794                    if str_acc.curr_indent != 0 {
795                        let indent_str: String = std::iter::once('\n')
796                            .chain((0..str_acc.curr_indent).map(|_| ' '))
797                            .collect();
798                        str_acc.acc.push_str(&s.as_str().replace('\n', &indent_str))
799                    } else {
800                        str_acc.acc.push_str(s.as_str())
801                    }
802
803                    let mut next_chunk = self.stack.pop_str_chunk();
804                    // Pop consecutive string literals to find the next expression to evaluate
805                    while let Some(StrChunk::Literal(s)) = next_chunk {
806                        str_acc.acc.push_str(&s);
807                        next_chunk = self.stack.pop_str_chunk();
808                    }
809
810                    if let Some(StrChunk::Expr(e, indent)) = next_chunk {
811                        self.stack.push_str_acc(StrAccItem {
812                            acc: str_acc.acc,
813                            // unwrap(): we don't expect an indentation level bigger than `u32::MAX`
814                            curr_indent: indent.try_into().unwrap(),
815                            env: str_acc.env.clone(),
816                            curr_pos: e.pos_idx(),
817                        });
818
819                        str_acc.curr_indent = indent.try_into().unwrap();
820                        str_acc.curr_pos = e.pos_idx();
821
822                        // TODO: we should set up the stack properly, and directly, for the
823                        // continuation instead of allocating a new term here and returning it.
824                        Ok(Closure {
825                            value: NickelValue::term(
826                                Term::op1(UnaryOp::ChunksConcat, e),
827                                pos_op_inh,
828                            ),
829                            env: str_acc.env.clone(),
830                        })
831                    } else {
832                        Ok(
833                            NickelValue::string(std::mem::take(&mut str_acc.acc), pos_op_inh)
834                                .into(),
835                        )
836                    }
837                } else {
838                    // Since the error halts the evaluation, we don't bother cleaning the stack of
839                    // the remaining string chunks.
840                    //
841                    // Not using mk_type_error! because of a non-uniform message
842                    Err(Box::new(EvalErrorKind::TypeError {
843                        expected: String::from("Stringable"),
844                        message: String::from(
845                            "interpolated values must be Stringable (string, number, boolean, enum tag or null)",
846                        ),
847                        orig_pos: str_acc.curr_pos,
848                        term: value,
849                    }))
850                }
851            }
852            UnaryOp::StringTrim => {
853                if let Some(s) = value.as_string() {
854                    Ok(NickelValue::string(s.trim(), pos_op_inh).into())
855                } else {
856                    mk_type_error!("String")
857                }
858            }
859            UnaryOp::StringChars => {
860                if let Some(s) = value.as_string() {
861                    let ts = s.characters();
862                    Ok(NickelValue::array(ts, Vec::new(), pos_op_inh).into())
863                } else {
864                    mk_type_error!("String")
865                }
866            }
867            UnaryOp::StringUppercase => {
868                if let Some(s) = value.as_string() {
869                    Ok(NickelValue::string(s.to_uppercase(), pos_op_inh).into())
870                } else {
871                    mk_type_error!("String")
872                }
873            }
874            UnaryOp::StringLowercase => {
875                if let Some(s) = value.as_string() {
876                    Ok(NickelValue::string(s.to_lowercase(), pos_op_inh).into())
877                } else {
878                    mk_type_error!("String")
879                }
880            }
881            UnaryOp::StringLength => {
882                if let Some(s) = value.as_string() {
883                    let length = s.graphemes(true).count();
884                    Ok(NickelValue::number(length, pos_op_inh).into())
885                } else {
886                    mk_type_error!("String")
887                }
888            }
889            UnaryOp::ToString => value
890                .to_nickel_string()
891                .map(|s| NickelValue::string(s, pos_op_inh).into())
892                .ok_or_else(|| {
893                    Box::new(EvalErrorKind::Other(
894                        format!(
895                            "to_string: can't convert an argument of type {} to string",
896                            value.type_of().unwrap()
897                        ),
898                        pos,
899                    ))
900                }),
901            UnaryOp::NumberFromString => {
902                if let Some(s) = value.as_string() {
903                    let n = parse_number_sci(s).map_err(|_| {
904                        Box::new(EvalErrorKind::Other(
905                            format!(
906                                "number/from_string: invalid number literal `{}`",
907                                s.as_str()
908                            ),
909                            pos,
910                        ))
911                    })?;
912
913                    Ok(NickelValue::number(n, pos_op_inh).into())
914                } else {
915                    mk_type_error!("String")
916                }
917            }
918            UnaryOp::EnumFromString => {
919                if let Some(s) = value.as_string() {
920                    Ok(NickelValue::enum_tag(LocIdent::from(s), pos_op_inh).into())
921                } else {
922                    mk_type_error!("String")
923                }
924            }
925            UnaryOp::StringIsMatch => {
926                if let Some(s) = value.as_string() {
927                    let re = regex::Regex::new(s)
928                        .map_err(|err| Box::new(EvalErrorKind::Other(err.to_string(), pos_op)))?;
929
930                    let matcher = eta_expand(UnaryOp::StringIsMatchCompiled(re.into()), pos_op_inh);
931                    Ok(NickelValue::term(matcher, pos_op_inh).into())
932                } else {
933                    mk_type_error!("String", 1)
934                }
935            }
936            UnaryOp::StringFind => {
937                if let Some(s) = value.as_string() {
938                    let re = regex::Regex::new(s)
939                        .map_err(|err| Box::new(EvalErrorKind::Other(err.to_string(), pos_op)))?;
940
941                    let matcher = eta_expand(UnaryOp::StringFindCompiled(re.into()), pos_op_inh);
942                    Ok(NickelValue::term(matcher, pos_op_inh).into())
943                } else {
944                    mk_type_error!("String", 1)
945                }
946            }
947            UnaryOp::StringFindAll => {
948                if let Some(s) = value.as_string() {
949                    let re = regex::Regex::new(s)
950                        .map_err(|err| Box::new(EvalErrorKind::Other(err.to_string(), pos_op)))?;
951
952                    let matcher = eta_expand(UnaryOp::StringFindAllCompiled(re.into()), pos_op_inh);
953                    Ok(NickelValue::term(matcher, pos_op_inh).into())
954                } else {
955                    mk_type_error!("String", 1)
956                }
957            }
958            UnaryOp::StringIsMatchCompiled(regex) => {
959                if let Some(s) = value.as_string() {
960                    Ok(s.matches_regex(&regex).with_pos_idx(pos_op_inh).into())
961                } else {
962                    mk_type_error!(op_name = "a compiled regular expression match", "String")
963                }
964            }
965            UnaryOp::StringFindCompiled(regex) => {
966                if let Some(s) = value.as_string() {
967                    use crate::term::string::RegexFindResult;
968
969                    let result = match s.find_regex(&regex) {
970                        // This record doesn't need to be closurized, since all values are
971                        // constant.
972                        None => mk_record!(
973                            ("matched", NickelValue::string_posless("")),
974                            ("index", NickelValue::number_posless(-1)),
975                            ("groups", NickelValue::empty_array())
976                        ),
977                        Some(RegexFindResult {
978                            matched: mtch,
979                            index,
980                            groups,
981                        }) => closurize_container(mk_record!(
982                            ("matched", NickelValue::string_posless(mtch)),
983                            ("index", NickelValue::number_posless(index)),
984                            (
985                                "groups",
986                                NickelValue::array_posless(
987                                    Array::from_iter(
988                                        groups
989                                            .into_iter()
990                                            // Unmatched groups get turned into empty strings. It
991                                            // might be nicer to have a 'Some s / 'None instead,
992                                            // but that would be an API break.
993                                            .map(|s| NickelValue::string_posless(
994                                                s.unwrap_or_default()
995                                            ))
996                                    ),
997                                    Vec::new()
998                                )
999                            )
1000                        )),
1001                    };
1002
1003                    Ok(result.with_pos_idx(pos_op_inh).into())
1004                } else {
1005                    mk_type_error!(op_name = "a compiled regular expression match", "String")
1006                }
1007            }
1008            UnaryOp::StringFindAllCompiled(regex) => {
1009                if let Some(s) = value.as_string() {
1010                    let result = NickelValue::array(
1011                        Array::from_iter(s.find_all_regex(&regex).map(|found| {
1012                            closurize_container(mk_record!(
1013                                ("matched", NickelValue::string_posless(found.matched)),
1014                                ("index", NickelValue::number_posless(found.index)),
1015                                (
1016                                    "groups",
1017                                    NickelValue::array_posless(
1018                                        Array::from_iter(
1019                                            found
1020                                                .groups
1021                                                .into_iter()
1022                                                // Unmatched groups get turned into empty strings. It
1023                                                // might be nicer to have a 'Some s / 'None instead,
1024                                                // but that would be an API break.
1025                                                .map(|s| NickelValue::string_posless(
1026                                                    s.unwrap_or_default()
1027                                                ))
1028                                        ),
1029                                        Vec::new(),
1030                                    )
1031                                )
1032                            ))
1033                        })),
1034                        Vec::new(),
1035                        pos_op_inh,
1036                    );
1037
1038                    Ok(result.into())
1039                } else {
1040                    mk_type_error!(op_name = "a compiled regular expression match", "String")
1041                }
1042            }
1043            UnaryOp::Force {
1044                ignore_not_exported,
1045            } => {
1046                /// `Seq` the `terms` iterator and then resume evaluating the `cont` continuation.
1047                fn seq_terms<I>(terms: I, pos: PosIdx, cont: NickelValue) -> NickelValue
1048                where
1049                    I: Iterator<Item = NickelValue>,
1050                {
1051                    terms
1052                        .fold(cont, |acc, t| mk_app!(mk_term::op1(UnaryOp::Seq, t), acc))
1053                        .with_pos_idx(pos)
1054                }
1055
1056                match value.content_ref() {
1057                    ValueContentRef::Record(Container::Alloc(record)) => {
1058                        let fields = record
1059                            .fields
1060                            .iter()
1061                            .filter(|(_, field)| {
1062                                !(field.is_empty_optional()
1063                                    || (ignore_not_exported && field.metadata.not_exported()))
1064                            })
1065                            .map(|(id, field)| (*id, field.clone()))
1066                            .map_values_closurize(&mut self.context.cache, &env, |_, value| {
1067                                mk_term::op1(
1068                                    UnaryOp::Force {
1069                                        ignore_not_exported,
1070                                    },
1071                                    value,
1072                                )
1073                            })
1074                            .map_err(|e| e.into_eval_err(pos, pos_op))?;
1075
1076                        let terms: Vec<NickelValue> = fields
1077                            .values()
1078                            .map(|field| {
1079                                field.value.as_ref().cloned().expect(
1080                                    "map_values_closurize ensures that values without a \
1081                                    definition throw a MissingFieldDefError",
1082                                )
1083                            })
1084                            .collect();
1085
1086                        let pos_inh = pos.to_inherited();
1087                        let cont = NickelValue::record(
1088                            RecordData {
1089                                fields,
1090                                attrs: record.attrs,
1091                                sealed_tail: record.sealed_tail.clone(),
1092                            },
1093                            pos_inh,
1094                        );
1095
1096                        Ok(seq_terms(terms.into_iter(), pos_op, cont).into())
1097                    }
1098                    ValueContentRef::Array(Container::Alloc(array_data)) => {
1099                        //unwrap(): the guard of the pattern exclude empty arrays
1100                        let ArrayData {
1101                            array,
1102                            pending_contracts,
1103                        } = array_data.clone();
1104                        let pos_inh = pos.to_inherited();
1105
1106                        let ts = array
1107                            .into_iter()
1108                            .map(|t| {
1109                                mk_term::op1(
1110                                    UnaryOp::Force {
1111                                        ignore_not_exported,
1112                                    },
1113                                    RuntimeContract::apply_all(
1114                                        t,
1115                                        pending_contracts.iter().cloned(),
1116                                        pos_inh,
1117                                    ),
1118                                )
1119                                .closurize(&mut self.context.cache, env.clone())
1120                            })
1121                            // It's important to collect here, otherwise the two usages below
1122                            // will each do their own .closurize(...) calls and end up with
1123                            // different closures, which means that `cont` won't be properly
1124                            // updated.
1125                            .collect::<Array>();
1126
1127                        let terms = ts.clone().into_iter();
1128                        let cont = NickelValue::array(ts, Vec::new(), pos_inh);
1129
1130                        Ok(seq_terms(terms, pos_op, cont).into())
1131                    }
1132                    ValueContentRef::EnumVariant(data) => {
1133                        let EnumVariantData { tag, arg } = data.clone();
1134
1135                        if let Some(arg) = arg {
1136                            let arg = mk_term::op1(
1137                                UnaryOp::Force {
1138                                    ignore_not_exported,
1139                                },
1140                                arg,
1141                            )
1142                            .closurize(&mut self.context.cache, env.clone());
1143
1144                            let cont = NickelValue::enum_variant(
1145                                tag,
1146                                Some(arg.clone()),
1147                                pos.to_inherited(),
1148                            );
1149
1150                            Ok(Closure {
1151                                value: seq_terms(std::iter::once(arg), pos_op, cont),
1152                                env,
1153                            })
1154                        } else {
1155                            Ok(Closure { value, env })
1156                        }
1157                    }
1158                    _ => Ok(Closure { value, env }),
1159                }
1160            }
1161            UnaryOp::RecordEmptyWithTail => {
1162                let Some(container) = value.as_record() else {
1163                    return mk_type_error!("Record");
1164                };
1165
1166                let mut result = RecordData::empty();
1167                result.sealed_tail = container
1168                    .into_opt()
1169                    .and_then(|record| record.sealed_tail.clone());
1170
1171                Ok(Closure {
1172                    value: NickelValue::record(result, pos_op_inh),
1173                    env,
1174                })
1175            }
1176            UnaryOp::RecordFreeze => {
1177                // If the record is already frozen, there's nothing to do.
1178                if matches!(value.as_record(), Some(Container::Alloc(record)) if record.attrs.frozen)
1179                {
1180                    // A frozen record shouldn't have a polymorphic tail
1181                    debug_assert!(
1182                        // unwrap()s: the pattern in `matches` ensures that both unwrap will
1183                        // succeed.
1184                        value
1185                            .as_record()
1186                            .unwrap()
1187                            .unwrap_alloc()
1188                            .sealed_tail
1189                            .is_none()
1190                    );
1191
1192                    return Ok(Closure { value, env });
1193                }
1194
1195                let Some(container) = value.as_record() else {
1196                    return mk_type_error!("Record");
1197                };
1198
1199                if let Container::Alloc(record) = container {
1200                    // It's not clear what the semantics of freezing a record with a sealed tail
1201                    // would be, as there might be dependencies between the sealed part and the
1202                    // unsealed part. Merging is disallowed on records with tail, so we disallow
1203                    // freezing as well.
1204                    if let Some(tail) = &record.sealed_tail {
1205                        return Err(Box::new(EvalErrorKind::IllegalPolymorphicTailAccess {
1206                            action: IllegalPolymorphicTailAction::Freeze,
1207                            evaluated_arg: tail.label.get_evaluated_arg(&self.context.cache),
1208                            label: tail.label.clone(),
1209                        }));
1210                    }
1211
1212                    let fields = record
1213                        .fields
1214                        .iter()
1215                        .map(|(id, field)| {
1216                            let field = field.clone();
1217
1218                            let value = field.value.map(|value| {
1219                                let pos = value.pos_idx();
1220                                RuntimeContract::apply_all(value, field.pending_contracts, pos)
1221                            });
1222
1223                            let field = Field {
1224                                value,
1225                                pending_contracts: Vec::new(),
1226                                ..field
1227                            }
1228                            .closurize(&mut self.context.cache, env.clone());
1229
1230                            (*id, field)
1231                        })
1232                        .collect();
1233
1234                    let attrs = record.attrs.frozen();
1235
1236                    Ok(Closure {
1237                        value: NickelValue::record(
1238                            RecordData {
1239                                fields,
1240                                attrs,
1241                                sealed_tail: None,
1242                            },
1243                            pos_op_inh,
1244                        ),
1245                        env,
1246                    })
1247                } else {
1248                    // Ditto if the record is empty. We can also drop the environment.
1249                    Ok(value.into())
1250                }
1251            }
1252            UnaryOp::Trace => {
1253                if let Some(s) = value.as_string() {
1254                    let _ = writeln!(self.context.trace, "std.trace: {s}");
1255                    Ok(())
1256                } else {
1257                    mk_type_error!("String")
1258                }?;
1259
1260                self.stack
1261                    .pop_arg(&self.context.cache)
1262                    .map(|(next, ..)| next)
1263                    .ok_or_else(|| {
1264                        Box::new(EvalErrorKind::NotEnoughArgs(
1265                            2,
1266                            String::from("trace"),
1267                            pos_op,
1268                        ))
1269                    })
1270            }
1271            UnaryOp::LabelPushDiag => {
1272                let Some(label) = value.as_label() else {
1273                    return mk_type_error!("Label");
1274                };
1275
1276                let mut label = label.clone();
1277                label.push_diagnostic();
1278                Ok(Closure {
1279                    value: NickelValue::label(label, pos),
1280                    env,
1281                })
1282            }
1283            #[cfg(feature = "nix-experimental")]
1284            UnaryOp::EvalNix => {
1285                if let Some(s) = value.as_string() {
1286                    let base_dir = self
1287                        .context
1288                        .pos_table
1289                        .get(pos_op)
1290                        .into_opt()
1291                        .map(|span| self.import_resolver().get_base_dir_for_nix(span.src_id))
1292                        .unwrap_or_default();
1293
1294                    let json = nix_ffi::eval_to_json(&String::from(s), &base_dir).map_err(|e| {
1295                        Box::new(EvalErrorKind::Other(
1296                            format!("nix code failed to evaluate:\n {}", e.what()),
1297                            pos,
1298                        ))
1299                    })?;
1300
1301                    let result: NickelValue = serde_json::from_str(&json).map_err(|e| {
1302                        Box::new(EvalErrorKind::Other(
1303                            format!("nix produced invalid json: {e}"),
1304                            pos,
1305                        ))
1306                    })?;
1307
1308                    Ok(result.into())
1309                } else {
1310                    // Not using mk_type_error! because of a non-uniform message
1311                    Err(Box::new(EvalErrorKind::TypeError {
1312                        expected: String::from("String"),
1313                        message: String::from("eval_nix takes a string of nix code as an argument"),
1314                        orig_pos: orig_pos_arg,
1315                        term: value,
1316                    }))
1317                }
1318            }
1319            UnaryOp::EnumGetArg => {
1320                if let Some(EnumVariantData { arg: Some(arg), .. }) = value.as_enum_variant() {
1321                    Ok(Closure {
1322                        value: arg.clone(),
1323                        env,
1324                    })
1325                } else {
1326                    mk_type_error!("Enum variant")
1327                }
1328            }
1329            UnaryOp::EnumMakeVariant => {
1330                let Some(tag) = value.as_string() else {
1331                    return mk_type_error!("String");
1332                };
1333
1334                let (arg_clos, _) = self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
1335                    Box::new(EvalErrorKind::NotEnoughArgs(
1336                        2,
1337                        String::from("enum/make_variant"),
1338                        pos,
1339                    ))
1340                })?;
1341                let arg_pos = arg_clos.value.pos_idx();
1342
1343                Ok(NickelValue::enum_variant(
1344                    LocIdent::new(tag).with_pos(self.context.pos_table.get(pos)),
1345                    Some(Thunk::new(arg_clos, arg_pos).into()),
1346                    pos_op_inh,
1347                )
1348                .into())
1349            }
1350            UnaryOp::EnumGetTag => match value.as_enum_variant() {
1351                Some(EnumVariantData { tag, .. }) => {
1352                    Ok(NickelValue::enum_tag(*tag, pos_op_inh).into())
1353                }
1354                _ => mk_type_error!("Enum"),
1355            },
1356            UnaryOp::EnumIsVariant => Ok(NickelValue::bool_value(
1357                value
1358                    .as_enum_variant()
1359                    .is_some_and(|enum_variant| enum_variant.arg.is_some()),
1360                pos_op_inh,
1361            )
1362            .into()),
1363            UnaryOp::ContractCustom => {
1364                let contract = if matches!(value.as_term(), Some(Term::Fun(..))) {
1365                    value.closurize(&mut self.context.cache, env)
1366                } else {
1367                    return mk_type_error!("Function or MatchExpression");
1368                };
1369
1370                Ok(NickelValue::custom_contract(contract, pos_op_inh).into())
1371            }
1372            UnaryOp::ContractPostprocessResult => {
1373                let Some(EnumVariantData {
1374                    tag,
1375                    arg: Some(arg),
1376                }) = value.as_enum_variant()
1377                else {
1378                    return mk_type_error!("[| 'Ok, 'Error _ |]");
1379                };
1380
1381                // We pop the second argument which isn't strict: we don't need to evaluate the
1382                // label if there's no error.
1383                let (label_closure, pos_label) = self.stack.pop_arg(&self.context.cache).unwrap();
1384
1385                match (tag.label(), arg) {
1386                    ("Ok", value) => Ok(Closure {
1387                        value: value.clone(),
1388                        env,
1389                    }),
1390                    ("Error", err_data) => {
1391                        let app_info = PrimopAppInfo {
1392                            call_stack_size: self.call_stack.len(),
1393                            pos_idx: pos_op_inh,
1394                        };
1395                        // In the error case, we first need to force the error data so that
1396                        // primitive values (strings) can be extracted from it, attach the
1397                        // corresponding data to the label, and then blame.
1398                        //
1399                        // To do so, we setup the stack to represent the evaluation context
1400                        // `%contract/blame% (%label/with_error_data% (%force% [.]) label)` and
1401                        // then continue with `err_data`.
1402                        self.stack.push_op1_cont(Op1ContItem {
1403                            op: UnaryOp::Blame,
1404                            app_info,
1405                            orig_pos_arg,
1406                        });
1407                        self.stack.push_op2_first_cont(Op2FirstContItem {
1408                            op: BinaryOp::LabelWithErrorData,
1409                            app_info,
1410                            arg2: label_closure,
1411                            orig_pos_arg1: pos_label,
1412                        });
1413                        self.stack.push_op1_cont(Op1ContItem {
1414                            op: UnaryOp::Force {
1415                                ignore_not_exported: false,
1416                            },
1417                            app_info,
1418                            orig_pos_arg,
1419                        });
1420
1421                        Ok(Closure {
1422                            value: err_data.clone(),
1423                            env,
1424                        })
1425                    }
1426                    _ => mk_type_error!("[| 'Ok, 'Error {..} |]'"),
1427                }
1428            }
1429            UnaryOp::ContractAttachDefaultLabel => {
1430                if !matches!(
1431                    value.as_enum_variant(),
1432                    Some(EnumVariantData { arg: Some(_), .. })
1433                ) {
1434                    return mk_type_error!("[| 'Ok, 'Error _ |]");
1435                }
1436                // The stack should already contain the default label to attach, so push
1437                // the (potential) error data.
1438                self.stack.push_arg(Closure { value, env }, orig_pos_arg);
1439
1440                Ok(Closure {
1441                    value: internals::add_default_check_label(),
1442                    env: Environment::new(),
1443                })
1444            }
1445            UnaryOp::NumberArcCos => self.number_op1(
1446                f64::acos,
1447                Op1EvalData {
1448                    op,
1449                    arg: Closure { value, env },
1450                    orig_pos_arg,
1451                    pos_op,
1452                },
1453            ),
1454            UnaryOp::NumberArcSin => self.number_op1(
1455                f64::asin,
1456                Op1EvalData {
1457                    op,
1458                    arg: Closure { value, env },
1459                    orig_pos_arg,
1460                    pos_op,
1461                },
1462            ),
1463            UnaryOp::NumberArcTan => self.number_op1(
1464                f64::atan,
1465                Op1EvalData {
1466                    op,
1467                    arg: Closure { value, env },
1468                    orig_pos_arg,
1469                    pos_op,
1470                },
1471            ),
1472            UnaryOp::NumberCos => self.number_op1(
1473                f64::cos,
1474                Op1EvalData {
1475                    op,
1476                    arg: Closure { value, env },
1477                    orig_pos_arg,
1478                    pos_op,
1479                },
1480            ),
1481            UnaryOp::NumberSin => self.number_op1(
1482                f64::sin,
1483                Op1EvalData {
1484                    op,
1485                    arg: Closure { value, env },
1486                    orig_pos_arg,
1487                    pos_op,
1488                },
1489            ),
1490            UnaryOp::NumberTan => self.number_op1(
1491                f64::tan,
1492                Op1EvalData {
1493                    op,
1494                    arg: Closure { value, env },
1495                    orig_pos_arg,
1496                    pos_op,
1497                },
1498            ),
1499            UnaryOp::RecDefault => unimplemented!(),
1500            UnaryOp::RecForce => unimplemented!(),
1501        }
1502    }
1503
1504    fn number_op1<F>(&mut self, f: F, eval_data: Op1EvalData) -> Result<Closure, ErrorKind>
1505    where
1506        F: Fn(f64) -> f64,
1507    {
1508        let Op1EvalData {
1509            op,
1510            arg: Closure { value, env: _ },
1511            orig_pos_arg,
1512            pos_op,
1513        } = eval_data;
1514
1515        if let Some(n) = value.as_number() {
1516            let result_as_f64 = f(f64::rounding_from(n, RoundingMode::Nearest).0);
1517            let result = Number::try_from_float_simplest(result_as_f64).map_err(|_| {
1518                Box::new(EvalErrorKind::Other(
1519                    format!(
1520                        "invalid arithmetic operation: \
1521                        {op}({n}) returned {result_as_f64}, \
1522                        but {result_as_f64} isn't representable in Nickel",
1523                    ),
1524                    pos_op,
1525                ))
1526            })?;
1527
1528            Ok(NickelValue::number(result, pos_op.to_inherited()).into())
1529        } else {
1530            Err(Box::new(EvalErrorKind::UnaryPrimopTypeError {
1531                primop: op.to_string(),
1532                expected: String::from("Number"),
1533                pos_arg: orig_pos_arg,
1534                arg_evaluated: value,
1535            }))
1536        }
1537    }
1538
1539    /// Evaluate a binary operation.
1540    ///
1541    /// Both arguments are expected to be evaluated (in WHNF).
1542    fn eval_op2(&mut self, eval_data: Op2EvalData) -> Result<Closure, ErrorKind> {
1543        let Op2EvalData {
1544            op,
1545            arg1: Closure {
1546                value: value1,
1547                env: env1,
1548            },
1549            arg2:
1550                Closure {
1551                    value: mut value2,
1552                    env: env2,
1553                },
1554            orig_pos_arg1,
1555            orig_pos_arg2,
1556            pos_op,
1557        } = eval_data;
1558
1559        increment!(format!("primop:{op}"));
1560
1561        let pos1 = value1.pos_idx();
1562        let pos2 = value2.pos_idx();
1563        let pos_op_inh = pos_op.to_inherited();
1564
1565        macro_rules! mk_type_error {
1566            (op_name=$op_name:expr, $expected:expr, $arg_number:expr, $arg_evaled:expr) => {
1567                Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
1568                    primop: String::from($op_name),
1569                    expected: String::from($expected),
1570                    arg_number: $arg_number,
1571                    pos_arg: {
1572                        match $arg_number {
1573                            1 => orig_pos_arg1,
1574                            2 => orig_pos_arg2,
1575                            _ => unimplemented!(),
1576                        }
1577                    },
1578                    arg_evaluated: $arg_evaled,
1579                    pos_op,
1580                }))
1581            };
1582            ($expected:expr, $arg_number:expr, $arg_evaled:expr) => {
1583                mk_type_error!(
1584                    op_name = op.to_string(),
1585                    $expected,
1586                    $arg_number,
1587                    $arg_evaled
1588                )
1589            };
1590        }
1591
1592        match op {
1593            BinaryOp::Seal => {
1594                let Some(key) = value1.as_sealing_key() else {
1595                    return mk_type_error!("SealingKey", 1, value1);
1596                };
1597
1598                let Some(label) = value2.as_label() else {
1599                    return mk_type_error!("Label", 2, value2);
1600                };
1601
1602                Ok(mk_fun!(
1603                    "x",
1604                    NickelValue::term(
1605                        Term::sealed(*key, mk_term::var("x"), label.clone()),
1606                        pos_op_inh
1607                    )
1608                )
1609                .into())
1610            }
1611            BinaryOp::Plus => self.number_op2(
1612                |n1, n2| n1 + n2,
1613                Op2EvalData {
1614                    op,
1615                    arg1: Closure {
1616                        value: value1,
1617                        env: env1,
1618                    },
1619                    arg2: Closure {
1620                        value: value2,
1621                        env: env2,
1622                    },
1623                    orig_pos_arg1,
1624                    orig_pos_arg2,
1625                    pos_op,
1626                },
1627            ),
1628            BinaryOp::Sub => self.number_op2(
1629                |n1, n2| n1 - n2,
1630                Op2EvalData {
1631                    op,
1632                    arg1: Closure {
1633                        value: value1,
1634                        env: env1,
1635                    },
1636                    arg2: Closure {
1637                        value: value2,
1638                        env: env2,
1639                    },
1640                    orig_pos_arg1,
1641                    orig_pos_arg2,
1642                    pos_op,
1643                },
1644            ),
1645            BinaryOp::Mult => self.number_op2(
1646                |n1, n2| n1 * n2,
1647                Op2EvalData {
1648                    op,
1649                    arg1: Closure {
1650                        value: value1,
1651                        env: env1,
1652                    },
1653                    arg2: Closure {
1654                        value: value2,
1655                        env: env2,
1656                    },
1657                    orig_pos_arg1,
1658                    orig_pos_arg2,
1659                    pos_op,
1660                },
1661            ),
1662            BinaryOp::Div => {
1663                let Some(n1) = value1.as_number() else {
1664                    return mk_type_error!("Number", 1, value1);
1665                };
1666
1667                let Some(n2) = value2.as_number() else {
1668                    return mk_type_error!("Number", 2, value2);
1669                };
1670
1671                if n2 == &Number::ZERO {
1672                    Err(Box::new(EvalErrorKind::Other(
1673                        String::from("division by zero"),
1674                        pos_op,
1675                    )))
1676                } else {
1677                    Ok(NickelValue::number(n1 / n2, pos_op_inh).into())
1678                }
1679            }
1680            BinaryOp::Modulo => {
1681                let Some(n1) = value1.as_number() else {
1682                    return mk_type_error!("Number", 1, value1);
1683                };
1684
1685                let Some(n2) = value2.as_number() else {
1686                    return mk_type_error!("Number", 2, value2);
1687                };
1688
1689                if n2 == &Number::ZERO {
1690                    return Err(Box::new(EvalErrorKind::Other(
1691                        String::from("division by zero (%)"),
1692                        pos2,
1693                    )));
1694                }
1695
1696                // This is the equivalent of `truncate()` for `Number`
1697                let quotient = Number::from(Integer::rounding_from(n1 / n2, RoundingMode::Down).0);
1698
1699                Ok(NickelValue::number(n1 - quotient * n2, pos_op_inh).into())
1700            }
1701            BinaryOp::NumberArcTan2 => {
1702                let Some(n1) = value1.as_number() else {
1703                    return mk_type_error!("Number", 1, value1);
1704                };
1705
1706                let Some(n2) = value2.as_number() else {
1707                    return mk_type_error!("Number", 2, value2);
1708                };
1709
1710                let y = f64::rounding_from(n1, RoundingMode::Nearest).0;
1711                let x = f64::rounding_from(n2, RoundingMode::Nearest).0;
1712
1713                let result_as_f64 = y.atan2(x);
1714
1715                let result = Number::try_from_float_simplest(result_as_f64).map_err(|_| {
1716                    Box::new(EvalErrorKind::Other(
1717                        format!(
1718                            "invalid arithmetic operation: \
1719                            number/arctan2({n1}, {n2}) returned {result_as_f64}, \
1720                            but {result_as_f64} isn't representable in Nickel"
1721                        ),
1722                        pos_op,
1723                    ))
1724                })?;
1725
1726                Ok(NickelValue::number(result, pos_op_inh).into())
1727            }
1728            BinaryOp::NumberLog => {
1729                let Some(n1) = value1.as_number() else {
1730                    return mk_type_error!("Number", 1, value1);
1731                };
1732
1733                let Some(n2) = value2.as_number() else {
1734                    return mk_type_error!("Number", 2, value2);
1735                };
1736
1737                let n = f64::rounding_from(n1, RoundingMode::Nearest).0;
1738
1739                let result_as_f64 = if n2 == &2 {
1740                    n.log2()
1741                } else if n2 == &Number::from(10) {
1742                    n.log10()
1743                } else {
1744                    let base = f64::rounding_from(n2, RoundingMode::Nearest).0;
1745                    n.log(base)
1746                };
1747
1748                let result = Number::try_from_float_simplest(result_as_f64).map_err(|_| {
1749                    Box::new(EvalErrorKind::Other(
1750                        format!(
1751                            "invalid arithmetic operation: \
1752                            number/log({n1}, {n2}) returned {result_as_f64}, \
1753                            but {result_as_f64} isn't representable in Nickel"
1754                        ),
1755                        pos_op,
1756                    ))
1757                })?;
1758
1759                Ok(NickelValue::number(result, pos_op_inh).into())
1760            }
1761            BinaryOp::Pow => {
1762                let Some(n1) = value1.as_number() else {
1763                    return mk_type_error!("Number", 1, value1);
1764                };
1765
1766                let Some(n2) = value2.as_number() else {
1767                    return mk_type_error!("Number", 2, value2);
1768                };
1769
1770                // Malachite's Rationals don't support exponents larger than `u64`. Anyway,
1771                // the result of such an operation would be huge and impractical to
1772                // store.
1773                //
1774                // We first try to convert the rational to an `i64`, in which case the
1775                // power is computed in an exact way.
1776                //
1777                // If the conversion fails, we fallback to converting both the exponent and
1778                // the value to the nearest `f64`, perform the exponentiation, and convert
1779                // the result back to rationals, with a possible loss of precision.
1780                let result = if let Ok(n2_as_i64) = i64::try_from(n2) {
1781                    n1.pow(n2_as_i64)
1782                } else {
1783                    let result_as_f64 = f64::rounding_from(n1, RoundingMode::Nearest)
1784                        .0
1785                        .powf(f64::rounding_from(n2, RoundingMode::Nearest).0);
1786                    // The following conversion fails if the result is NaN or +/-infinity
1787                    Number::try_from_float_simplest(result_as_f64).map_err(|_| {
1788                        Box::new(EvalErrorKind::Other(
1789                            format!(
1790                                "invalid arithmetic operation: \
1791                                        {n1}^{n2} returned {result_as_f64}, \
1792                                        but {result_as_f64} isn't representable in Nickel"
1793                            ),
1794                            pos_op,
1795                        ))
1796                    })?
1797                };
1798
1799                Ok(NickelValue::number(result, pos_op_inh).into())
1800            }
1801            BinaryOp::StringConcat => {
1802                let Some(s1) = value1.as_string() else {
1803                    return mk_type_error!("String", 1, value1);
1804                };
1805
1806                let Some(s2) = value2.as_string() else {
1807                    return mk_type_error!("String", 2, value2);
1808                };
1809
1810                Ok(NickelValue::string(format!("{s1}{s2}"), pos_op_inh).into())
1811            }
1812            BinaryOp::ContractApply | BinaryOp::ContractCheck => {
1813                let app_info = PrimopAppInfo {
1814                    call_stack_size: self.call_stack.len(),
1815                    pos_idx: pos_op_inh,
1816                };
1817
1818                // Performing only one match `if let Term::Type` and putting the call to
1819                // `increment!` there looks sensible at first, but it's annoying to explain to
1820                // rustc and clippy that we match on `typ` but use it only if the `metrics` feature
1821                // is enabled (we get unused variable warning otherwise). It's simpler to just make
1822                // a separate `if` conditionally included.
1823                #[cfg(feature = "metrics")]
1824                if let Some(TypeData { typ, .. }) = value1.as_type() {
1825                    increment!(format!(
1826                        "primop:contract/apply:{}",
1827                        typ.pretty_print_cap(40)
1828                    ));
1829                }
1830
1831                if let Some(TypeData { typ: _, contract }) = value1.as_type() {
1832                    // The contract generation from a static type might return any kind of
1833                    // contract, including e.g. a record or a custom contract. The result needs to
1834                    // be evaluated first, and then passed to `b_op` again. In that case, we don't
1835                    // bother tracking the argument and updating the label: this will be done by
1836                    // the next call to `b_op`.
1837
1838                    // We set the stack to represent the evaluation context `<b_op> [.] label` and
1839                    // proceed to evaluate `<typ.contract()>`
1840                    self.stack.push_op2_first_cont(Op2FirstContItem {
1841                        op,
1842                        app_info,
1843                        arg2: Closure {
1844                            value: value2,
1845                            env: env2,
1846                        },
1847                        orig_pos_arg1,
1848                    });
1849
1850                    return Ok(Closure {
1851                        value: contract.clone(),
1852                        env: env1,
1853                    });
1854                }
1855
1856                let Some(label) = value2.as_label() else {
1857                    return mk_type_error!("Label", 2, value2);
1858                };
1859
1860                let mut label = label.clone();
1861
1862                increment!(format!(
1863                    "contract:originates_from_type {}",
1864                    label.typ.pretty_print_cap(40)
1865                ));
1866
1867                #[cfg(feature = "metrics")]
1868                if let Some(field) = label.field_name {
1869                    increment!(format!("contract:originates_from_field {field}"));
1870                }
1871
1872                // Pop the contract argument to track its cache index in the label for better
1873                // error reporting, and because we might add post-processing steps on the stack
1874                // which need to sit underneath the value and the label (they will be run after
1875                // the contract application is evaluated). We'll just push the value and the
1876                // label back on the stack at the end.
1877                let (idx, stack_value_pos) = self
1878                    .stack
1879                    .pop_arg_as_idx(&mut self.context.cache)
1880                    .ok_or_else(|| {
1881                        Box::new(EvalErrorKind::NotEnoughArgs(
1882                            3,
1883                            String::from("contract/apply"),
1884                            pos_op,
1885                        ))
1886                    })?;
1887
1888                // We update the label and convert it back to a term form that can be cheaply cloned
1889                label.arg_pos = self
1890                    .context
1891                    .cache
1892                    .get_then(idx.clone(), |c| c.value.pos_idx());
1893                label.arg_idx = Some(idx.clone());
1894                let new_label = NickelValue::label(label, pos2);
1895
1896                // If we're evaluating a plain contract application but we are applying
1897                // something with the signature of a custom contract, we need to setup some
1898                // post-processing.
1899                //
1900                // We prepare the stack so that `contract/postprocess_result` will be applied
1901                // afterwards. This primop converts the result of a custom contract `'Ok value`
1902                // or `'Error err_data` to either `value` or a proper contract error with
1903                // `err_data` included in the label.
1904                //
1905                // That is, prepare the stack to represent the evaluation context
1906                // `%contract/postprocess_result% [.] label`
1907                if let (
1908                    ValueContentRef::CustomContract(_) | ValueContentRef::Record(_),
1909                    BinaryOp::ContractApply,
1910                ) = (value1.content_ref(), &op)
1911                {
1912                    self.stack.push_arg(new_label.clone().into(), pos_op_inh);
1913
1914                    self.stack.push_op1_cont(Op1ContItem {
1915                        op: UnaryOp::ContractPostprocessResult,
1916                        app_info,
1917                        orig_pos_arg: pos1.to_inherited(),
1918                    });
1919                }
1920
1921                // Contract checks are allowed to specify a blame location, but they don't
1922                // have to. We insert an op to check if they omitted the blame location and
1923                // put in a default one if not.
1924                //
1925                // Prepare the stack to represent the evaluation context
1926                // `%contract/attach_default_label% [.] label`
1927                if let BinaryOp::ContractCheck = &op {
1928                    self.stack.push_arg(new_label.clone().into(), pos_op_inh);
1929
1930                    self.stack.push_op1_cont(Op1ContItem {
1931                        op: UnaryOp::ContractAttachDefaultLabel,
1932                        app_info,
1933                        orig_pos_arg: pos1.to_inherited(),
1934                    });
1935                }
1936
1937                // Now that we've updated the label, we push the checked value and the new
1938                // label back on the stack, so that they become the two arguments of the
1939                // contract (transformed to something that can be applied directly). That is,
1940                // we prepare the stack to represent the evaluation context `[.] label value`
1941                // and proceed with the evaluation of `functoid`.
1942                self.stack.push_tracked_arg(idx, stack_value_pos);
1943                self.stack.push_arg(new_label.into(), pos2.to_inherited());
1944
1945                // We convert the contract (which can be a custom contract, a record, a naked
1946                // function, etc.) to a form that can be applied to a label and a value.
1947                let functoid = match value1.content_ref() {
1948                    ValueContentRef::Term(Term::Fun(..)) => {
1949                        // Warn on naked function contracts, but not if they came from the
1950                        // stdlib. Some stdlib functions return naked function contracts.
1951                        if let Some(pos) = self.context.pos_table.get(pos1).as_opt_ref()
1952                            && !self.context.import_resolver.files().is_stdlib(pos.src_id)
1953                        {
1954                            self.warn(Warning::NakedFunctionContract {
1955                                func_pos: self.context.pos_table.get(pos1),
1956                                app_pos: self.context.pos_table.get(pos_op),
1957                            });
1958                        }
1959
1960                        if let BinaryOp::ContractApply = op {
1961                            Closure {
1962                                value: value1,
1963                                env: env1,
1964                            }
1965                        } else {
1966                            // Prepare the stack to represent the evaluation context `[.]
1967                            // as_naked` and proceed with `$naked_to_custom`
1968                            self.stack.push_arg(
1969                                Closure {
1970                                    value: value1,
1971                                    env: env1,
1972                                },
1973                                orig_pos_arg1,
1974                            );
1975
1976                            internals::naked_to_custom().into()
1977                        }
1978                    }
1979                    ValueContentRef::CustomContract(ctr) => Closure {
1980                        value: ctr.clone(),
1981                        env: env1,
1982                    },
1983                    ValueContentRef::Record(..) => {
1984                        // Prepare the stack to represent the evaluation context `[.] t1` and
1985                        // proceed with `$record_contract`
1986                        self.stack.push_arg(
1987                            Closure {
1988                                value: value1,
1989                                env: env1,
1990                            },
1991                            orig_pos_arg1,
1992                        );
1993
1994                        internals::record_contract().into()
1995                    }
1996                    _ => return mk_type_error!("Contract", 1, value1),
1997                };
1998
1999                Ok(functoid)
2000            }
2001            BinaryOp::LabelWithErrorData => {
2002                // We need to extract plain values from a Nickel data structure, which most likely
2003                // contains closures at least, even if it's fully evaluated. As for serialization,
2004                // we thus need to fully substitute all variables first.
2005                let value1 = subst(
2006                    &self.context.pos_table,
2007                    &self.context.cache,
2008                    value1,
2009                    &self.initial_env,
2010                    &env1,
2011                );
2012
2013                let Some(label) = value2.as_label() else {
2014                    return mk_type_error!("Label", 2, value2);
2015                };
2016
2017                let mut label = label.clone();
2018
2019                match value1.as_record() {
2020                    Some(Container::Empty) => Ok(NickelValue::label(label, pos2).into()),
2021                    Some(Container::Alloc(record_data)) => {
2022                        // If the contract returned a label as part of its error
2023                        // data, blame that one instead.
2024                        if let Some(user_label) = record_data
2025                            .fields
2026                            .get(&LocIdent::from("blame_location"))
2027                            .and_then(|field| field.value.as_ref())
2028                            .and_then(NickelValue::as_label)
2029                        {
2030                            label = user_label.clone();
2031                        }
2032
2033                        if let Some(msg) = record_data
2034                            .fields
2035                            .get(&LocIdent::from("message"))
2036                            .and_then(|field| field.value.as_ref())
2037                            .and_then(NickelValue::as_string)
2038                        {
2039                            label = label.with_diagnostic_message(msg.clone().into_inner());
2040                        }
2041
2042                        if let Some(notes) = record_data
2043                            .fields
2044                            .get(&LocIdent::from("notes"))
2045                            .and_then(|field| field.value.as_ref())
2046                            .and_then(NickelValue::as_array)
2047                            .and_then(Container::into_opt)
2048                        {
2049                            let notes = notes
2050                                .array
2051                                .iter()
2052                                .map(|element| {
2053                                    if let Some(s) = element.as_string() {
2054                                        Ok(s.clone().into_inner())
2055                                    } else {
2056                                        mk_type_error!("String (notes)", 1, element.clone())
2057                                    }
2058                                })
2059                                .collect::<Result<Vec<_>, _>>()?;
2060
2061                            label = label.with_diagnostic_notes(notes);
2062                        }
2063
2064                        Ok(NickelValue::label(label, pos2).into())
2065                    }
2066                    _ => {
2067                        mk_type_error!("Record", 1, value1)
2068                    }
2069                }
2070            }
2071            BinaryOp::Unseal => {
2072                if let Some(key) = value1.as_sealing_key() {
2073                    // The last argument (lazy, on the stack) of unseal is an expression raising
2074                    // blame. If the keys match, we ignore the blame and thus return a function
2075                    // `const unsealed_term_content`. Otherwise, we return `id`.
2076                    //
2077                    // Since the stack is set up as `[.] blame_expr`, this does ignore the error
2078                    // and proceed with the unsealed term in the happy path, or on the opposite
2079                    // drop the sealed term and proceed with the error on the stack otherwise.
2080                    Ok(if let Some(Term::Sealed(data)) = value2.as_term() {
2081                        if key == &data.key {
2082                            Closure {
2083                                value: mk_fun!(LocIdent::fresh(), data.inner.clone()),
2084                                env: env2,
2085                            }
2086                        } else {
2087                            mk_term::id().into()
2088                        }
2089                    } else {
2090                        mk_term::id().into()
2091                    })
2092                } else {
2093                    mk_type_error!("SealingKey", 1, value1)
2094                }
2095            }
2096            BinaryOp::Eq => {
2097                let c1 = Closure {
2098                    value: value1,
2099                    env: env1,
2100                };
2101                let c2 = Closure {
2102                    value: value2,
2103                    env: env2,
2104                };
2105
2106                match eq(&mut self.context.cache, c1, c2, pos_op_inh)? {
2107                    EqResult::Bool(b) => match (b, self.stack.pop_eq()) {
2108                        (false, _) => {
2109                            self.stack.clear_eqs();
2110                            Ok(NickelValue::bool_value(false, pos_op_inh).into())
2111                        }
2112                        (true, None) => Ok(NickelValue::bool_value(true, pos_op_inh).into()),
2113                        (true, Some(EqItem { arg1, arg2 })) => {
2114                            let v1 = arg1.value.closurize(&mut self.context.cache, arg1.env);
2115                            let v2 = arg2.value.closurize(&mut self.context.cache, arg2.env);
2116
2117                            // TODO: avoid term allocation in evaluation
2118                            Ok(NickelValue::term(Term::op2(BinaryOp::Eq, v1, v2), pos_op).into())
2119                        }
2120                    },
2121                    EqResult::Eqs(v1, v2, subeqs) => {
2122                        self.stack.push_eqs(subeqs.into_iter());
2123
2124                        // TODO: avoid term allocation in evaluation
2125                        Ok(NickelValue::term(Term::op2(BinaryOp::Eq, v1, v2), pos_op).into())
2126                    }
2127                }
2128            }
2129            BinaryOp::LessThan => self.number_cmp2(
2130                |n1, n2| n1 < n2,
2131                Op2EvalData {
2132                    op,
2133                    arg1: Closure {
2134                        value: value1,
2135                        env: env1,
2136                    },
2137                    arg2: Closure {
2138                        value: value2,
2139                        env: env2,
2140                    },
2141                    orig_pos_arg1,
2142                    orig_pos_arg2,
2143                    pos_op,
2144                },
2145            ),
2146            BinaryOp::LessOrEq => self.number_cmp2(
2147                |n1, n2| n1 <= n2,
2148                Op2EvalData {
2149                    op,
2150                    arg1: Closure {
2151                        value: value1,
2152                        env: env1,
2153                    },
2154                    arg2: Closure {
2155                        value: value2,
2156                        env: env2,
2157                    },
2158                    orig_pos_arg1,
2159                    orig_pos_arg2,
2160                    pos_op,
2161                },
2162            ),
2163            BinaryOp::GreaterThan => self.number_cmp2(
2164                |n1, n2| n1 > n2,
2165                Op2EvalData {
2166                    op,
2167                    arg1: Closure {
2168                        value: value1,
2169                        env: env1,
2170                    },
2171                    arg2: Closure {
2172                        value: value2,
2173                        env: env2,
2174                    },
2175                    orig_pos_arg1,
2176                    orig_pos_arg2,
2177                    pos_op,
2178                },
2179            ),
2180            BinaryOp::GreaterOrEq => self.number_cmp2(
2181                |n1, n2| n1 >= n2,
2182                Op2EvalData {
2183                    op,
2184                    arg1: Closure {
2185                        value: value1,
2186                        env: env1,
2187                    },
2188                    arg2: Closure {
2189                        value: value2,
2190                        env: env2,
2191                    },
2192                    orig_pos_arg1,
2193                    orig_pos_arg2,
2194                    pos_op,
2195                },
2196            ),
2197            BinaryOp::LabelGoField => {
2198                let Some(field) = value1.as_string() else {
2199                    return mk_type_error!("String", 1, value1);
2200                };
2201
2202                let Some(label) = value2.as_label() else {
2203                    return mk_type_error!("Label", 2, value2);
2204                };
2205
2206                let mut label = label.clone();
2207                label
2208                    .path
2209                    .push(ty_path::Elem::Field(field.clone().into_inner().into()));
2210                Ok(NickelValue::label(label, pos_op_inh).into())
2211            }
2212            BinaryOp::RecordGet => {
2213                // This error should be impossible to trigger. The parser
2214                // prevents a dynamic field access where the field name is not syntactically
2215                // a string.
2216                let Some(id) = value1.as_string() else {
2217                    return mk_type_error!("String", 1, value1);
2218                };
2219
2220                let Some(container) = value2.as_record() else {
2221                    // Not using mk_type_error! because of a non-uniform message
2222                    return Err(Box::new(EvalErrorKind::TypeError {
2223                        expected: String::from("Record"),
2224                        message: String::from("field access only makes sense for records"),
2225                        orig_pos: orig_pos_arg2,
2226                        term: value2,
2227                    }));
2228                };
2229
2230                let ident = LocIdent::from(id);
2231
2232                let Container::Alloc(record) = container else {
2233                    return Err(Box::new(EvalErrorKind::FieldMissing {
2234                        id: ident,
2235                        field_names: Vec::new(),
2236                        operator: BinaryOp::RecordGet.to_string(),
2237                        pos_record: pos2,
2238                        pos_op,
2239                    }));
2240                };
2241
2242                // We have to apply potential pending contracts. Right now, this
2243                // means that repeated field access will re-apply the contract again
2244                // and again, which is not optimal. The same thing happens with array
2245                // contracts. There are several way to improve this, but this is left
2246                // as future work.
2247                match record
2248                    .get_value_with_ctrs(&ident)
2249                    .map_err(|missing_field_err| missing_field_err.into_eval_err(pos2, pos_op))?
2250                {
2251                    Some(value) => {
2252                        self.call_stack
2253                            .enter_field(ident, pos2, value.pos_idx(), pos_op);
2254                        Ok(Closure { value, env: env2 })
2255                    }
2256                    None => match record.sealed_tail.as_ref() {
2257                        Some(t) if t.has_dyn_field(id) => {
2258                            Err(Box::new(EvalErrorKind::IllegalPolymorphicTailAccess {
2259                                action: IllegalPolymorphicTailAction::FieldAccess {
2260                                    field: id.to_string(),
2261                                },
2262                                evaluated_arg: t.label.get_evaluated_arg(&self.context.cache),
2263                                label: t.label.clone(),
2264                            }))
2265                        }
2266                        _ => Err(Box::new(EvalErrorKind::FieldMissing {
2267                            id: ident,
2268                            field_names: record.field_names(RecordOpKind::IgnoreEmptyOpt),
2269                            operator: BinaryOp::RecordGet.to_string(),
2270                            pos_record: pos2,
2271                            pos_op,
2272                        })),
2273                    },
2274                }
2275            }
2276            BinaryOp::RecordInsert {
2277                metadata,
2278                pending_contracts,
2279                ext_kind,
2280                op_kind,
2281            } => {
2282                // Since we take ownership of the data hidden in `RecordInsert`, we can't pretty
2283                // print it anymore. However we do need a string representation for the error case,
2284                // which we reconstruct here.
2285                let op_name = || {
2286                    BinaryOp::RecordInsert {
2287                        ext_kind,
2288                        op_kind,
2289                        metadata: SharedMetadata::empty(),
2290                        pending_contracts: Vec::new(),
2291                    }
2292                    .to_string()
2293                };
2294
2295                let Some(id) = value1.as_string() else {
2296                    return mk_type_error!(op_name = op_name(), "String", 1, value1);
2297                };
2298
2299                let mut value2 = value2;
2300
2301                if value2.is_inline_empty_record() {
2302                    // We are going to insert in the record, so we make sure that it's an allocated
2303                    // block and not an inline empty record.
2304                    value2 = NickelValue::empty_record_block(pos2);
2305                }
2306
2307                let ValueContentRefMut::Record(Container::Alloc(record)) =
2308                    value2.content_make_mut()
2309                else {
2310                    // Theoretically, we could be in the case `Record(Container::Empty)` here, but
2311                    // we made sure to allocate a record block to specifically exclude this case.
2312                    return mk_type_error!(op_name = op_name(), "Record", 2, value2);
2313                };
2314
2315                // If a defined value is expected for this field, it must be
2316                // provided as an additional argument, so we pop it from the stack
2317                let value = if let RecordExtKind::WithValue = ext_kind {
2318                    let (value_closure, _) =
2319                        self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
2320                            Box::new(EvalErrorKind::NotEnoughArgs(
2321                                3,
2322                                String::from("insert"),
2323                                pos_op,
2324                            ))
2325                        })?;
2326
2327                    let closurized = value_closure
2328                        .value
2329                        .closurize(&mut self.context.cache, value_closure.env);
2330                    Some(closurized)
2331                } else {
2332                    None
2333                };
2334
2335                match record.fields.insert(
2336                    LocIdent::from(id),
2337                    Field {
2338                        value,
2339                        metadata,
2340                        pending_contracts,
2341                    },
2342                ) {
2343                    Some(t)
2344                        if matches!(op_kind, RecordOpKind::ConsiderAllFields)
2345                            || !t.is_empty_optional() =>
2346                    {
2347                        Err(Box::new(EvalErrorKind::Other(
2348                            format!(
2349                                "{}: \
2350                                tried to extend a record with the field {id}, \
2351                                but it already exists",
2352                                op_name(),
2353                            ),
2354                            pos_op,
2355                        )))
2356                    }
2357                    _ => Ok(Closure {
2358                        // Insertion preserves the frozenness
2359                        value: value2,
2360                        env: env2,
2361                    }),
2362                }
2363            }
2364            BinaryOp::RecordRemove(op_kind) => {
2365                let Some(id) = value1.as_string() else {
2366                    return mk_type_error!("String", 1, value1);
2367                };
2368
2369                let mut value2 = value2;
2370
2371                if value2.is_inline_empty_record() {
2372                    // We are going to insert in the record, so we make sure that it's an allocated
2373                    // block and not an inline empty record.
2374                    value2 = NickelValue::empty_record_block(pos2);
2375                }
2376
2377                let ValueContentRefMut::Record(Container::Alloc(record)) =
2378                    value2.content_make_mut()
2379                else {
2380                    // Theoretically, we could be in the case `Record(Container::Empty)` here, but
2381                    // we made sure to allocate a record block to specifically exclude this case.
2382                    return mk_type_error!("Record", 2, value2);
2383                };
2384
2385                let fetched = record.fields.swap_remove(&LocIdent::from(id));
2386
2387                if fetched.is_none()
2388                    || matches!(
2389                        (op_kind, fetched),
2390                        (
2391                            RecordOpKind::IgnoreEmptyOpt,
2392                            Some(Field {
2393                                value: None,
2394                                metadata, ..
2395                            })
2396                        ) if metadata.opt()
2397                    )
2398                {
2399                    match record.sealed_tail.as_ref() {
2400                        Some(t) if t.has_dyn_field(id) => {
2401                            Err(Box::new(EvalErrorKind::IllegalPolymorphicTailAccess {
2402                                action: IllegalPolymorphicTailAction::FieldRemove {
2403                                    field: id.to_string(),
2404                                },
2405                                evaluated_arg: t.label.get_evaluated_arg(&self.context.cache),
2406                                label: t.label.clone(),
2407                            }))
2408                        }
2409                        _ => Err(Box::new(EvalErrorKind::FieldMissing {
2410                            id: id.into(),
2411                            field_names: record.field_names(op_kind),
2412                            operator: String::from("record/remove"),
2413                            pos_record: pos2,
2414                            pos_op,
2415                        })),
2416                    }
2417                } else {
2418                    Ok(Closure {
2419                        value: value2,
2420                        env: env2,
2421                    })
2422                }
2423            }
2424            BinaryOp::RecordHasField(op_kind) => {
2425                let Some(id) = value1.as_string() else {
2426                    return mk_type_error!("String", 1, value1);
2427                };
2428
2429                let Some(container) = value2.as_record() else {
2430                    return mk_type_error!("Record", 2, value2);
2431                };
2432
2433                Ok(NickelValue::bool_value(matches!(
2434                            container.get(id.clone().into()),
2435                            Some(field) if matches!(op_kind, RecordOpKind::ConsiderAllFields) || !field.is_empty_optional()
2436                        ),
2437                    pos_op_inh
2438                ).into())
2439            }
2440            BinaryOp::RecordFieldIsDefined(op_kind) => {
2441                let Some(id) = value1.as_string() else {
2442                    return mk_type_error!("String", 1, value1);
2443                };
2444
2445                let Some(container) = value2.as_record() else {
2446                    return mk_type_error!("Record", 2, value2);
2447                };
2448
2449                Ok(NickelValue::bool_value(
2450                        matches!(
2451                            container.get(id.clone().into()),
2452                            Some(field @ Field { value: Some(_), ..}) if matches!(op_kind, RecordOpKind::ConsiderAllFields) || !field.is_empty_optional()
2453                        ),
2454                        pos_op_inh,
2455                ).into())
2456            }
2457            BinaryOp::ArrayConcat => {
2458                let Some(container1) = value1.as_array() else {
2459                    return mk_type_error!("Array", 1, value1);
2460                };
2461
2462                let Some(container2) = value2.as_array() else {
2463                    return mk_type_error!("Array", 2, value2);
2464                };
2465
2466                // If one of the array is empty, we directly return the other.
2467                let Container::Alloc(array_data1) = container1 else {
2468                    return Ok(value2.into());
2469                };
2470
2471                let Container::Alloc(array_data2) = container2 else {
2472                    return Ok(value1.into());
2473                };
2474
2475                // In all generality, we need to apply the pending contracts on both sides, as they
2476                // can differ. Even if some are common, the order of contracts is meaningful, so
2477                // deduplicating the common part is not trivial.
2478                //
2479                // Still, we can handle the following case: if the pending contracts are the same
2480                // size and are equal pairwise, we can keep them lazy. The typical case is when
2481                // there's only one contract, but it's doesn't cost much to try them all.
2482
2483                if array_data1.pending_contracts.len() == array_data2.pending_contracts.len()
2484                    && array_data1
2485                        .pending_contracts
2486                        .iter()
2487                        .zip(array_data2.pending_contracts.iter())
2488                        .all(|(ctr1, ctr2)| {
2489                            !ctr1.can_have_poly_ctrs()
2490                                && contract_eq(&ctr1.contract, &env1, &ctr2.contract, &env2)
2491                        })
2492                {
2493                    // Cloning a slice is cheap: it's better to clone the left hand side and extend
2494                    // it rather that building a chained iterator.
2495                    let mut result = array_data1.array.clone();
2496                    result.extend(array_data2.array.iter().cloned());
2497
2498                    Ok(NickelValue::array(
2499                        result,
2500                        array_data1.pending_contracts.clone(),
2501                        pos_op_inh,
2502                    )
2503                    .into())
2504                } else {
2505                    // We need to collect in two phases, since the mapped closures capture
2506                    // `&mut self.cache`, so chaining the iterators first wouldn't work.
2507
2508                    // We need to collect in two phases, since the mapped closures capture
2509                    // `&mut self.cache`, so chaining the iterators first wouldn't work.
2510                    //
2511                    // Technically, we could clone `array_data1` and ierate mutably over its
2512                    // elements in hope of sharing some of the structure. However, since it's a
2513                    // clone, the leaves will be shared, and mutable iteration over a shared
2514                    // persistent vector won't have much advantage over collecting a new array from
2515                    // an iterator.
2516                    let mut result: Array = container1
2517                        .iter()
2518                        .cloned()
2519                        .map(|elt| {
2520                            RuntimeContract::apply_all(
2521                                elt,
2522                                container1.iter_pending_contracts().cloned(),
2523                                pos1,
2524                            )
2525                            .closurize(&mut self.context.cache, env1.clone())
2526                        })
2527                        .collect();
2528
2529                    result.extend(container2.iter().cloned().map(|elt| {
2530                        RuntimeContract::apply_all(
2531                            elt,
2532                            container2.iter_pending_contracts().cloned(),
2533                            pos2,
2534                        )
2535                        .closurize(&mut self.context.cache, env2.clone())
2536                    }));
2537
2538                    Ok(NickelValue::array(result, Vec::new(), pos_op_inh).into())
2539                }
2540            }
2541            BinaryOp::ArrayAt => {
2542                let Some(container) = value1.as_array() else {
2543                    return mk_type_error!("Array", 1, value1);
2544                };
2545
2546                let Some(n) = value2.as_number() else {
2547                    return mk_type_error!("Number", 2, value2);
2548                };
2549
2550                let Ok(n_as_usize) = usize::try_from(n) else {
2551                    return Err(Box::new(EvalErrorKind::Other(
2552                        format!(
2553                            "array/at expects its second argument to be a \
2554                                positive integer smaller than {}, got {n}",
2555                            usize::MAX
2556                        ),
2557                        pos_op,
2558                    )));
2559                };
2560
2561                let Container::Alloc(array_data) = container else {
2562                    return Err(Box::new(EvalErrorKind::Other(
2563                        "array/at: index out of bounds. \
2564                        Can't index into an empty array."
2565                            .to_owned(),
2566                        pos_op,
2567                    )));
2568                };
2569
2570                if n_as_usize >= array_data.array.len() {
2571                    return Err(Box::new(EvalErrorKind::Other(
2572                        format!(
2573                            "array/at: index out of bounds. \
2574                                Expected an index between 0 and {}, got {}",
2575                            array_data.array.len(),
2576                            n
2577                        ),
2578                        pos_op,
2579                    )));
2580                }
2581
2582                let elem_with_ctr = RuntimeContract::apply_all(
2583                    array_data.array.get(n_as_usize).unwrap().clone(),
2584                    array_data.pending_contracts.iter().cloned(),
2585                    pos1.to_inherited(),
2586                );
2587
2588                Ok(Closure {
2589                    value: elem_with_ctr,
2590                    env: env1,
2591                })
2592            }
2593            BinaryOp::Merge(merge_label) => self.merge(
2594                value1,
2595                env1,
2596                value2,
2597                env2,
2598                pos_op,
2599                MergeMode::Standard(merge_label),
2600            ),
2601            BinaryOp::Hash => {
2602                let mk_err_fst =
2603                    || mk_type_error!("[| 'Md5, 'Sha1, 'Sha256, 'Sha512 |]", 1, value1.clone());
2604
2605                let Some(enum_data) = value1.as_enum_variant() else {
2606                    return mk_err_fst();
2607                };
2608
2609                if enum_data.arg.is_some() {
2610                    return mk_err_fst();
2611                }
2612
2613                let Some(s) = value2.as_string() else {
2614                    return mk_type_error!("String", 2, value2);
2615                };
2616
2617                let result = match enum_data.tag.as_ref() {
2618                    "Md5" => {
2619                        let mut hasher = md5::Md5::new();
2620                        hasher.update(s.as_ref());
2621                        format!("{:x}", hasher.finalize())
2622                    }
2623                    "Sha1" => {
2624                        let mut hasher = sha1::Sha1::new();
2625                        hasher.update(s.as_ref());
2626                        format!("{:x}", hasher.finalize())
2627                    }
2628                    "Sha256" => {
2629                        let mut hasher = sha2::Sha256::new();
2630                        hasher.update(s.as_ref());
2631                        format!("{:x}", hasher.finalize())
2632                    }
2633                    "Sha512" => {
2634                        let mut hasher = sha2::Sha512::new();
2635                        hasher.update(s.as_ref());
2636                        format!("{:x}", hasher.finalize())
2637                    }
2638                    _ => return mk_err_fst(),
2639                };
2640
2641                Ok(NickelValue::string(result, pos_op_inh).into())
2642            }
2643            BinaryOp::Serialize => {
2644                let mk_err_fst = || mk_type_error!(ENUM_FORMAT, 1, value1.clone());
2645
2646                let Some(enum_data) = value1.as_enum_variant() else {
2647                    return mk_err_fst();
2648                };
2649
2650                if enum_data.arg.is_some() {
2651                    return mk_err_fst();
2652                }
2653
2654                // Serialization needs all variables term to be fully substituted
2655                let initial_env = Environment::new();
2656                let v2_subst = subst(
2657                    &self.context.pos_table,
2658                    &self.context.cache,
2659                    value2,
2660                    &initial_env,
2661                    &env2,
2662                );
2663
2664                let format = match enum_data.tag.to_string().as_str() {
2665                    "Json" => ExportFormat::Json,
2666                    "Yaml" => ExportFormat::Yaml,
2667                    "YamlDocuments" => ExportFormat::YamlDocuments,
2668                    "Toml" => ExportFormat::Toml,
2669                    _ => return mk_err_fst(),
2670                };
2671
2672                serialize::validate(format, &v2_subst)?;
2673
2674                Ok(
2675                    NickelValue::string(serialize::to_string(format, &v2_subst)?, pos_op_inh)
2676                        .into(),
2677                )
2678            }
2679            BinaryOp::Deserialize => {
2680                let mk_err_fst = || mk_type_error!(ENUM_FORMAT, 1, value1.clone());
2681
2682                let Some(enum_data) = value1.as_enum_variant() else {
2683                    return mk_err_fst();
2684                };
2685
2686                if enum_data.arg.is_some() {
2687                    return mk_err_fst();
2688                }
2689
2690                let Some(s) = value2.as_string() else {
2691                    return mk_type_error!("String", 2, value2);
2692                };
2693
2694                let deser: NickelValue = match enum_data.tag.label() {
2695                    "Json" => serde_json::from_str(s).map_err(|err| {
2696                        Box::new(EvalErrorKind::DeserializationError(
2697                            String::from("json"),
2698                            format!("{err}"),
2699                            pos_op,
2700                        ))
2701                    })?,
2702                    // TODO: we could try to generate better error positions here,
2703                    // but it will be some work.
2704                    //
2705                    // We pass `None` to `load_yaml` (so it produces a position-less
2706                    // `NickelValue`) even if we have a position for `s`, because `s` is
2707                    // likely not at offset zero in its file and so `load_yaml` will give
2708                    // the wrong error locations. Were it just a matter of offsetting the
2709                    // error location, this would be easy to fix. Unfortunately getting the
2710                    // locations right would involve handling location shifts caused by
2711                    // escape sequences and interpolation.
2712                    tag @ ("Yaml" | "YamlDocuments") => {
2713                        let listify = if tag == "Yaml" {
2714                            Listify::Auto
2715                        } else {
2716                            Listify::Always
2717                        };
2718                        crate::serialize::yaml::load_yaml_value(
2719                            &mut self.context.pos_table,
2720                            s,
2721                            None,
2722                            listify,
2723                        )
2724                        .map_err(|err| {
2725                            Box::new(EvalErrorKind::DeserializationErrorWithInner {
2726                                format: InputFormat::Yaml,
2727                                inner: err,
2728                                pos: pos_op,
2729                            })
2730                        })?
2731                    }
2732                    "Toml" => toml::from_str(s).map_err(|err| {
2733                        Box::new(EvalErrorKind::DeserializationError(
2734                            String::from("toml"),
2735                            format!("{err}"),
2736                            pos_op,
2737                        ))
2738                    })?,
2739                    _ => return mk_err_fst(),
2740                };
2741
2742                Ok(deser.with_pos_idx(pos_op_inh).into())
2743            }
2744            BinaryOp::StringSplit => self.string_fn2(
2745                |input, sep| NickelValue::array(input.split(sep), Vec::new(), pos_op_inh),
2746                Op2EvalData {
2747                    op,
2748                    arg1: Closure {
2749                        value: value1,
2750                        env: env1,
2751                    },
2752                    arg2: Closure {
2753                        value: value2,
2754                        env: env2,
2755                    },
2756                    orig_pos_arg1,
2757                    orig_pos_arg2,
2758                    pos_op,
2759                },
2760            ),
2761            BinaryOp::StringContains => self.string_fn2(
2762                |s1, s2| NickelValue::bool_value(s1.contains(s2.as_str()), pos_op_inh),
2763                Op2EvalData {
2764                    op,
2765                    arg1: Closure {
2766                        value: value1,
2767                        env: env1,
2768                    },
2769                    arg2: Closure {
2770                        value: value2,
2771                        env: env2,
2772                    },
2773                    orig_pos_arg1,
2774                    orig_pos_arg2,
2775                    pos_op,
2776                },
2777            ),
2778            BinaryOp::StringCompare => {
2779                let as_term_pos = self.context.pos_table.get(pos_op_inh);
2780
2781                self.string_fn2(
2782                    |s1, s2| {
2783                        use std::cmp::Ordering;
2784
2785                        NickelValue::enum_tag(
2786                            LocIdent::new_with_pos(
2787                                match s1.cmp(s2) {
2788                                    Ordering::Less => "Lesser",
2789                                    Ordering::Equal => "Equal",
2790                                    Ordering::Greater => "Greater",
2791                                },
2792                                as_term_pos,
2793                            ),
2794                            pos_op_inh,
2795                        )
2796                    },
2797                    Op2EvalData {
2798                        op,
2799                        arg1: Closure {
2800                            value: value1,
2801                            env: env1,
2802                        },
2803                        arg2: Closure {
2804                            value: value2,
2805                            env: env2,
2806                        },
2807                        orig_pos_arg1,
2808                        orig_pos_arg2,
2809                        pos_op,
2810                    },
2811                )
2812            }
2813            BinaryOp::StringBase64Encode => {
2814                let mk_err_fst = || {
2815                    mk_type_error!(
2816                        "[| 'Standard, 'UrlSafe, 'NoPad, 'UrlSafeNoPad |]",
2817                        1,
2818                        value1.clone()
2819                    )
2820                };
2821
2822                let Some(b64_variant) = value1.as_enum_variant() else {
2823                    return mk_err_fst();
2824                };
2825
2826                if b64_variant.arg.is_some() {
2827                    return mk_err_fst();
2828                }
2829
2830                let Some(s) = value2.as_string() else {
2831                    return mk_type_error!("String", 2, value2);
2832                };
2833
2834                let result = match b64_variant.tag.as_ref() {
2835                    "Standard" => base64::prelude::BASE64_STANDARD.encode(s.as_str()),
2836                    "UrlSafe" => base64::prelude::BASE64_URL_SAFE.encode(s.as_str()),
2837                    "NoPad" => base64::prelude::BASE64_STANDARD_NO_PAD.encode(s.as_str()),
2838                    "UrlSafeNoPad" => base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s.as_str()),
2839                    _ => return mk_err_fst(),
2840                };
2841
2842                Ok(NickelValue::string(result, pos_op_inh).into())
2843            }
2844            BinaryOp::StringBase64Decode => {
2845                let mk_err_fst = || {
2846                    mk_type_error!(
2847                        "[| 'Standard, 'UrlSafe, 'NoPad, 'UrlSafeNoPad |]",
2848                        1,
2849                        value1.clone()
2850                    )
2851                };
2852
2853                let Some(b64_variant) = value1.as_enum_variant() else {
2854                    return mk_err_fst();
2855                };
2856
2857                if b64_variant.arg.is_some() {
2858                    return mk_err_fst();
2859                }
2860
2861                let Some(s) = value2.as_string() else {
2862                    return mk_type_error!("String", 2, value2);
2863                };
2864
2865                let decode_with = |engine: base64::engine::GeneralPurpose| {
2866                    let decode_result = engine
2867                        .decode(s.as_str())
2868                        .map_err(|err| format!("Base64 decoding failed: {err}"))
2869                        .and_then(|decoded_bytes| {
2870                            String::from_utf8(decoded_bytes).map_err(|err| {
2871                                format!("Could not convert decoded value to string: {err}")
2872                            })
2873                        });
2874                    match decode_result {
2875                        Ok(decoded_string) => Ok(NickelValue::enum_variant(
2876                            "Ok",
2877                            Some(NickelValue::string(decoded_string, pos_op_inh)),
2878                            pos_op_inh,
2879                        )
2880                        .into()),
2881                        Err(err) => Ok(NickelValue::enum_variant(
2882                            "Error",
2883                            Some(
2884                                mk_record!(("message", NickelValue::string_posless(err)))
2885                                    .with_pos_idx(pos_op_inh),
2886                            ),
2887                            pos_op_inh,
2888                        )
2889                        .into()),
2890                    }
2891                };
2892
2893                match b64_variant.tag.as_ref() {
2894                    "Standard" => decode_with(base64::prelude::BASE64_STANDARD),
2895                    "UrlSafe" => decode_with(base64::prelude::BASE64_URL_SAFE),
2896                    "NoPad" => decode_with(base64::prelude::BASE64_STANDARD_NO_PAD),
2897                    "UrlSafeNoPad" => decode_with(base64::prelude::BASE64_URL_SAFE_NO_PAD),
2898                    _ => mk_err_fst(),
2899                }
2900            }
2901            BinaryOp::ContractArrayLazyApp => {
2902                let (ctr, _) = self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
2903                    Box::new(EvalErrorKind::NotEnoughArgs(
2904                        3,
2905                        String::from("contract/array_lazy_app"),
2906                        pos_op,
2907                    ))
2908                })?;
2909
2910                let Closure {
2911                    value: ctr_val,
2912                    env: env_ctr,
2913                } = ctr;
2914
2915                let Some(label) = value1.as_label() else {
2916                    return mk_type_error!("Label", 1, value1);
2917                };
2918
2919                if value2.is_inline_empty_array() {
2920                    // We are going to insert in the array, so we make sure that it's an allocated
2921                    // block and not an inline empty array.
2922                    value2 = NickelValue::empty_array_block(pos2);
2923                }
2924
2925                let ValueContentRefMut::Array(Container::Alloc(array_data)) =
2926                    value2.content_make_mut()
2927                else {
2928                    // Theoretically, we could be in the case `Array(Container::Empty)` here, but
2929                    // we made sure to allocate an array block to specifically exclude this case.
2930                    return mk_type_error!("Array", 2, value2);
2931                };
2932
2933                // Preserve the environment of the contract in the resulting array.
2934                let contract = ctr_val.closurize(&mut self.context.cache, env_ctr);
2935                RuntimeContract::push_dedup(
2936                    &mut array_data.pending_contracts,
2937                    &env2,
2938                    RuntimeContract::new(contract, label.clone()),
2939                    &Environment::new(),
2940                );
2941
2942                let array_with_ctr = Closure {
2943                    value: value2,
2944                    env: env2,
2945                };
2946
2947                Ok(array_with_ctr)
2948            }
2949            BinaryOp::ContractRecordLazyApp => {
2950                // The contract is expected to be of type `String -> Contract`: it takes the name
2951                // of the field as a parameter, and returns a contract.
2952                let (
2953                    Closure {
2954                        value: ctr_val,
2955                        env: ctr_env,
2956                    },
2957                    _,
2958                ) = self.stack.pop_arg(&self.context.cache).ok_or_else(|| {
2959                    Box::new(EvalErrorKind::NotEnoughArgs(
2960                        3,
2961                        String::from("contract/record_lazy_app"),
2962                        pos_op,
2963                    ))
2964                })?;
2965
2966                let Some(label) = value1.as_label() else {
2967                    return mk_type_error!("Label", 1, value1);
2968                };
2969
2970                let Some(container) = value2.as_record() else {
2971                    return mk_type_error!("Record", 2, value2);
2972                };
2973
2974                let Container::Alloc(record_data) = container else {
2975                    return Ok(NickelValue::empty_record().with_pos_idx(pos2).into());
2976                };
2977
2978                let mut record_data = record_data.clone();
2979
2980                // Applying a lazy contract unfreezes a record, as frozen record are
2981                // expected to have all their contracts applied and thus an empty list of
2982                // pending contracts.
2983                record_data.attrs.frozen = false;
2984
2985                let mut contract_at_field = |id: LocIdent| {
2986                    let pos = ctr_val.pos_idx();
2987                    mk_app!(
2988                        ctr_val.clone(),
2989                        NickelValue::string(id, self.context.pos_table.push(id.pos))
2990                    )
2991                    .with_pos_idx(pos)
2992                    .closurize(&mut self.context.cache, ctr_env.clone())
2993                };
2994
2995                for (id, field) in record_data.fields.iter_mut() {
2996                    let runtime_ctr = RuntimeContract {
2997                        contract: contract_at_field(*id),
2998                        label: label.clone(),
2999                    };
3000
3001                    RuntimeContract::push_dedup(
3002                        &mut field.pending_contracts,
3003                        &env2,
3004                        runtime_ctr,
3005                        &ctr_env,
3006                    );
3007                }
3008
3009                // IMPORTANT: here, we revert the record back to a `RecRecord`. The
3010                // reason is that applying a contract over fields might change the
3011                // value of said fields (the typical example is adding a value to a
3012                // subrecord via the default value of a contract).
3013                //
3014                // We want recursive occurrences of fields to pick this new value as
3015                // well: hence, we need to recompute the fixpoint, which is done by
3016                // `fixpoint::revert`.
3017                let reverted = super::fixpoint::revert(&mut self.context.cache, record_data);
3018
3019                Ok(NickelValue::term(reverted, pos2).into())
3020            }
3021            BinaryOp::LabelWithMessage => {
3022                let Some(message) = value1.as_string() else {
3023                    return mk_type_error!("String", 1, value1);
3024                };
3025
3026                let ValueContentRefMut::Label(label) = value2.content_make_mut() else {
3027                    return mk_type_error!("Label", 2, value2);
3028                };
3029
3030                label.set_diagnostic_message(message.clone().into_inner());
3031                Ok(value2.into())
3032            }
3033            BinaryOp::LabelWithNotes => {
3034                // We need to extract plain strings from a Nickel array, which most likely
3035                // contains at least generated variables.
3036                // As for serialization, we thus fully substitute all variables first.
3037                let val1_subst = subst(
3038                    &self.context.pos_table,
3039                    &self.context.cache,
3040                    value1.clone(),
3041                    &Environment::new(),
3042                    &env1,
3043                );
3044
3045                let Some(array_data) = val1_subst.as_array() else {
3046                    return mk_type_error!("Array", 1, value1);
3047                };
3048
3049                let ValueContentRefMut::Label(label) = value2.content_make_mut() else {
3050                    return mk_type_error!("Label", 2, value2);
3051                };
3052
3053                let notes = array_data
3054                    .iter()
3055                    .map(|element| {
3056                        if let Some(s) = element.as_string() {
3057                            Ok(s.clone().into_inner())
3058                        } else {
3059                            mk_type_error!("String", 1, element.clone())
3060                        }
3061                    })
3062                    .collect::<Result<Vec<_>, _>>()?;
3063                label.set_diagnostic_notes(notes);
3064
3065                Ok(value2.into())
3066            }
3067            BinaryOp::LabelAppendNote => {
3068                let Some(note) = value1.as_string() else {
3069                    return mk_type_error!("String", 1, value1);
3070                };
3071
3072                let ValueContentRefMut::Label(label) = value2.content_make_mut() else {
3073                    return mk_type_error!("Label", 2, value2);
3074                };
3075
3076                label.append_diagnostic_note(note);
3077                Ok(value2.into())
3078            }
3079            BinaryOp::LabelLookupTypeVar => {
3080                let Some(key) = value1.as_sealing_key() else {
3081                    return mk_type_error!("SealingKey", 1, value1);
3082                };
3083
3084                let Some(label) = value2.as_label() else {
3085                    return mk_type_error!("Label", 2, value2);
3086                };
3087
3088                Ok(NickelValue::from(
3089                    label
3090                        .type_environment
3091                        .iter()
3092                        // We rev to find the latest binding, if there ever were several type
3093                        // variables bound with the same name
3094                        .rev()
3095                        .find_map(|(k, data)| (k == key).then_some(data))
3096                        .unwrap(),
3097                )
3098                .with_pos_idx(pos_op_inh)
3099                .into())
3100            }
3101            BinaryOp::RecordSplitPair => {
3102                let Some(cont1) = value1.as_record() else {
3103                    return mk_type_error!("Record", 1, value1);
3104                };
3105
3106                let Some(cont2) = value2.as_record() else {
3107                    return mk_type_error!("Record", 2, value2);
3108                };
3109
3110                let record1 = cont1.into_opt();
3111                let record2 = cont2.into_opt();
3112
3113                let split::SplitResult {
3114                    left,
3115                    center,
3116                    right,
3117                } = split::split_ref(
3118                    record1
3119                        .as_ref()
3120                        .map(|r| &r.fields)
3121                        .unwrap_or(&Default::default()),
3122                    record2
3123                        .as_ref()
3124                        .map(|r| &r.fields)
3125                        .unwrap_or(&Default::default()),
3126                );
3127
3128                let left_only = NickelValue::record_posless(RecordData {
3129                    fields: left,
3130                    sealed_tail: record1
3131                        .as_ref()
3132                        .map(|r| r.sealed_tail.clone())
3133                        .unwrap_or_default(),
3134                    attrs: record1.as_ref().map(|r| r.attrs).unwrap_or_default(),
3135                });
3136
3137                let right_only = NickelValue::record_posless(RecordData {
3138                    fields: right,
3139                    sealed_tail: record2
3140                        .as_ref()
3141                        .map(|r| r.sealed_tail.clone())
3142                        .unwrap_or_default(),
3143                    attrs: record2.as_ref().map(|r| r.attrs).unwrap_or_default(),
3144                });
3145
3146                let (center1, center2): (IndexMap<LocIdent, Field>, IndexMap<LocIdent, Field>) =
3147                    center
3148                        .into_iter()
3149                        .map(|(id, (left, right))| ((id, left), (id, right)))
3150                        .unzip();
3151
3152                let left_center = NickelValue::record_posless(RecordData {
3153                    fields: center1,
3154                    sealed_tail: None,
3155                    attrs: RecordAttrs::default(),
3156                });
3157
3158                let right_center = NickelValue::record_posless(RecordData {
3159                    fields: center2,
3160                    sealed_tail: None,
3161                    attrs: RecordAttrs::default(),
3162                });
3163
3164                Ok(closurize_container(NickelValue::record(
3165                    RecordData {
3166                        fields: IndexMap::from([
3167                            (LocIdent::from("left_only"), Field::from(left_only)),
3168                            (LocIdent::from("left_center"), Field::from(left_center)),
3169                            (LocIdent::from("right_center"), Field::from(right_center)),
3170                            (LocIdent::from("right_only"), Field::from(right_only)),
3171                        ]),
3172                        attrs: RecordAttrs::default(),
3173                        sealed_tail: None,
3174                    },
3175                    pos_op_inh,
3176                ))
3177                .into())
3178            }
3179            BinaryOp::RecordDisjointMerge => {
3180                let Some(container1) = value1.as_record() else {
3181                    return mk_type_error!("Record", 1, value1);
3182                };
3183
3184                let Some(container2) = value2.as_record() else {
3185                    return mk_type_error!("Record", 2, value2);
3186                };
3187
3188                let (record1, record2) = match (container1, container2) {
3189                    (Container::Alloc(record1), Container::Alloc(record2)) => (record1, record2),
3190                    (Container::Empty, _) => {
3191                        return Ok(Closure {
3192                            value: value2.with_pos_idx(pos_op_inh),
3193                            env: env2,
3194                        });
3195                    }
3196                    (_, Container::Empty) => {
3197                        return Ok(Closure {
3198                            value: value1.with_pos_idx(pos_op_inh),
3199                            env: env1,
3200                        });
3201                    }
3202                };
3203
3204                // As for merge, we refuse to combine two records if one of them has a sealed tail.
3205                // However, if only one of them does, because we don't do any recursive
3206                // re-evaluation here, it's fine to just pick this tail as the tail of the result.
3207                //
3208                // This behavior is actually useful, because disjoint_merge is used in the
3209                // implementation of builtin contracts to combine an unsealed tail with the
3210                // original body of the record. In that case, the unsealed tail might have an
3211                // additional sealed tail itself (tail can be sealed multiple times in a nested
3212                // way), and the right behavior (tm) is to just keep it.
3213                let sealed_tail = match (&record1.sealed_tail, &record2.sealed_tail) {
3214                    (Some(tail1), Some(_)) => {
3215                        return Err(Box::new(EvalErrorKind::IllegalPolymorphicTailAccess {
3216                            action: IllegalPolymorphicTailAction::Merge,
3217                            evaluated_arg: tail1.label.get_evaluated_arg(&self.context.cache),
3218                            label: tail1.label.clone(),
3219                        }));
3220                    }
3221                    (tail1, tail2) => tail1.as_ref().or(tail2.as_ref()).cloned(),
3222                };
3223
3224                // Note that because of record closurization, we assume here that the record data
3225                // of each record are already closurized, so we don't really care about
3226                // environments. Should that invariant change, we might get into trouble (trouble
3227                // meaning undue `UnboundIdentifier` errors).
3228                let mut fields =
3229                    IndexMap::with_capacity(record1.fields.len() + record2.fields.len());
3230
3231                fields.extend(record1.fields.iter().map(|(k, v)| (*k, v.clone())));
3232                fields.extend(record2.fields.iter().map(|(k, v)| (*k, v.clone())));
3233
3234                let attrs = Combine::combine(record1.attrs, record2.attrs);
3235
3236                Ok(NickelValue::record(
3237                    RecordData {
3238                        fields,
3239                        attrs,
3240                        sealed_tail,
3241                    },
3242                    pos_op_inh,
3243                )
3244                .into())
3245            }
3246        }
3247    }
3248
3249    fn number_cmp2<F>(&mut self, f: F, eval_data: Op2EvalData) -> Result<Closure, ErrorKind>
3250    where
3251        F: Fn(&Number, &Number) -> bool,
3252    {
3253        let pos_op_inh = eval_data.pos_op.to_inherited();
3254
3255        self.number_fn2(
3256            |n1, n2| NickelValue::bool_value(f(n1, n2), pos_op_inh),
3257            eval_data,
3258        )
3259    }
3260
3261    fn number_op2<F>(&mut self, f: F, eval_data: Op2EvalData) -> Result<Closure, ErrorKind>
3262    where
3263        F: Fn(&Number, &Number) -> Number,
3264    {
3265        let pos_op_inh = eval_data.pos_op.to_inherited();
3266
3267        self.number_fn2(
3268            |n1, n2| NickelValue::number(f(n1, n2), pos_op_inh),
3269            eval_data,
3270        )
3271    }
3272
3273    fn number_fn2<F>(&mut self, f: F, eval_data: Op2EvalData) -> Result<Closure, ErrorKind>
3274    where
3275        F: Fn(&Number, &Number) -> NickelValue,
3276    {
3277        let Op2EvalData {
3278            op,
3279            arg1: Closure {
3280                value: value1,
3281                env: _,
3282            },
3283            arg2: Closure {
3284                value: value2,
3285                env: _,
3286            },
3287            orig_pos_arg1,
3288            orig_pos_arg2,
3289            pos_op,
3290        } = eval_data;
3291
3292        let Some(n1) = value1.as_number() else {
3293            return Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
3294                primop: op.to_string(),
3295                expected: "Number".to_owned(),
3296                arg_number: 1,
3297                pos_arg: orig_pos_arg1,
3298                arg_evaluated: value1,
3299                pos_op,
3300            }));
3301        };
3302
3303        let Some(n2) = value2.as_number() else {
3304            return Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
3305                primop: op.to_string(),
3306                expected: "Number".to_owned(),
3307                arg_number: 2,
3308                pos_arg: orig_pos_arg2,
3309                arg_evaluated: value2,
3310                pos_op,
3311            }));
3312        };
3313
3314        Ok(f(n1, n2).into())
3315    }
3316
3317    fn string_fn2<F>(&mut self, f: F, eval_data: Op2EvalData) -> Result<Closure, ErrorKind>
3318    where
3319        F: Fn(&NickelString, &NickelString) -> NickelValue,
3320    {
3321        let Op2EvalData {
3322            op,
3323            arg1: Closure {
3324                value: value1,
3325                env: _,
3326            },
3327            arg2: Closure {
3328                value: value2,
3329                env: _,
3330            },
3331            orig_pos_arg1,
3332            orig_pos_arg2,
3333            pos_op,
3334        } = eval_data;
3335
3336        let Some(s1) = value1.as_string() else {
3337            return Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
3338                primop: op.to_string(),
3339                expected: "String".to_owned(),
3340                arg_number: 1,
3341                pos_arg: orig_pos_arg1,
3342                arg_evaluated: value1,
3343                pos_op,
3344            }));
3345        };
3346
3347        let Some(s2) = value2.as_string() else {
3348            return Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
3349                primop: op.to_string(),
3350                expected: "String".to_owned(),
3351                arg_number: 2,
3352                pos_arg: orig_pos_arg2,
3353                arg_evaluated: value2,
3354                pos_op,
3355            }));
3356        };
3357
3358        Ok(f(s1, s2).into())
3359    }
3360
3361    /// Evaluate a n-ary operation.
3362    ///
3363    /// Arguments are expected to be evaluated (in WHNF).
3364    fn eval_opn(&mut self, eval_data: OpNEvalData) -> Result<Closure, ErrorKind> {
3365        let OpNEvalData { op, args, pos_op } = eval_data;
3366        increment!(format!("primop:{op}"));
3367        let pos_op_inh = pos_op.to_inherited();
3368
3369        let mk_type_error =
3370            |expected: &str, arg_number: usize, pos_arg: PosIdx, arg_evaluated: NickelValue| {
3371                Err(Box::new(EvalErrorKind::NAryPrimopTypeError {
3372                    primop: op.to_string(),
3373                    expected: expected.to_owned(),
3374                    arg_number,
3375                    pos_arg,
3376                    arg_evaluated,
3377                    pos_op,
3378                }))
3379            };
3380
3381        // Currently, for fixed arity primitive operators, the parser must ensure that they get
3382        // exactly the right number of argument: if it is not the case, this is a bug, and we panic.
3383        match op {
3384            NAryOp::StringReplace | NAryOp::StringReplaceRegex => {
3385                let mut args_wo_env = args.into_iter().map(|(arg, pos)| (arg.value, pos));
3386                let (arg1, arg_pos1) = args_wo_env.next().unwrap();
3387                let (arg2, arg_pos2) = args_wo_env.next().unwrap();
3388                let (arg3, arg_pos3) = args_wo_env.next().unwrap();
3389                debug_assert!(args_wo_env.next().is_none());
3390
3391                let Some(s) = arg1.as_string() else {
3392                    return mk_type_error("String", 1, arg_pos1, arg1);
3393                };
3394
3395                let Some(from) = arg2.as_string() else {
3396                    return mk_type_error("String", 2, arg_pos2, arg2);
3397                };
3398
3399                let Some(to) = arg3.as_string() else {
3400                    return mk_type_error("String", 3, arg_pos3, arg3);
3401                };
3402
3403                let result = if let NAryOp::StringReplace = op {
3404                    s.replace(from.as_str(), to.as_str())
3405                } else {
3406                    let re = regex::Regex::new(from)
3407                        .map_err(|err| Box::new(EvalErrorKind::Other(err.to_string(), pos_op)))?;
3408
3409                    s.replace_regex(&CompiledRegex(re), to)
3410                };
3411
3412                Ok(NickelValue::string(result, pos_op_inh).into())
3413            }
3414            NAryOp::StringSubstr => {
3415                let mut args_wo_env = args.into_iter().map(|(arg, pos)| (arg.value, pos));
3416                let (arg1, arg_pos1) = args_wo_env.next().unwrap();
3417                let (arg2, arg_pos2) = args_wo_env.next().unwrap();
3418                let (arg3, arg_pos3) = args_wo_env.next().unwrap();
3419                debug_assert!(args_wo_env.next().is_none());
3420
3421                let Some(s) = arg1.as_string() else {
3422                    return mk_type_error("String", 1, arg_pos1, arg1);
3423                };
3424
3425                let Some(start) = arg2.as_number() else {
3426                    return mk_type_error("Number", 2, arg_pos2, arg2);
3427                };
3428
3429                let Some(end) = arg3.as_number() else {
3430                    return mk_type_error("Number", 3, arg_pos3, arg3);
3431                };
3432
3433                s.substring(start, end)
3434                    .map(|substr| NickelValue::string(substr, pos_op_inh).into())
3435                    .map_err(|e| Box::new(EvalErrorKind::Other(format!("{e}"), pos_op)))
3436            }
3437            NAryOp::MergeContract => {
3438                let mut args_iter = args.into_iter();
3439
3440                let (
3441                    Closure {
3442                        value: arg1,
3443                        env: _,
3444                    },
3445                    _,
3446                ) = args_iter.next().unwrap();
3447
3448                let (
3449                    Closure {
3450                        value: arg2,
3451                        env: env2,
3452                    },
3453                    _,
3454                ) = args_iter.next().unwrap();
3455
3456                let (
3457                    Closure {
3458                        value: arg3,
3459                        env: env3,
3460                    },
3461                    _,
3462                ) = args_iter.next().unwrap();
3463
3464                debug_assert!(args_iter.next().is_none());
3465
3466                let Some(label) = arg1.as_label() else {
3467                    return Err(Box::new(EvalErrorKind::InternalError(
3468                        format!(
3469                            "The {op} operator was expecting \
3470                                a first argument of type Label, got {}",
3471                            arg1.type_of().unwrap_or("<unevaluated>")
3472                        ),
3473                        pos_op,
3474                    )));
3475                };
3476
3477                self.merge(
3478                    arg2,
3479                    env2,
3480                    arg3,
3481                    env3,
3482                    pos_op,
3483                    MergeMode::Contract(label.clone()),
3484                )
3485            }
3486            NAryOp::RecordSealTail => {
3487                let mut args = args.into_iter();
3488                let (
3489                    Closure {
3490                        value: arg1,
3491                        env: _,
3492                    },
3493                    arg_pos1,
3494                ) = args.next().unwrap();
3495
3496                let (
3497                    Closure {
3498                        value: arg2,
3499                        env: _,
3500                    },
3501                    arg_pos2,
3502                ) = args.next().unwrap();
3503
3504                let (
3505                    Closure {
3506                        value: mut arg3,
3507                        env: env3,
3508                    },
3509                    arg_pos3,
3510                ) = args.next().unwrap();
3511
3512                let (
3513                    Closure {
3514                        value: arg4,
3515                        env: env4,
3516                    },
3517                    arg_pos4,
3518                ) = args.next().unwrap();
3519
3520                debug_assert!(args.next().is_none());
3521
3522                let Some(s) = arg1.as_sealing_key() else {
3523                    return mk_type_error("SealingKey", 1, arg_pos1, arg1);
3524                };
3525
3526                let Some(label) = arg2.as_label() else {
3527                    return mk_type_error("Label", 2, arg_pos2, arg2);
3528                };
3529
3530                if arg3.is_inline_empty_record() {
3531                    // We are going to insert in the record, so we make sure that it's an allocated
3532                    // block and not an inline empty record.
3533                    arg3 = NickelValue::empty_record_block(arg3.pos_idx());
3534                }
3535
3536                let ValueContentRefMut::Record(Container::Alloc(r)) = arg3.content_make_mut()
3537                else {
3538                    return mk_type_error("Record", 3, arg_pos3, arg3);
3539                };
3540
3541                // Even if the record to seal is empty, the correctness of polymorphic contracts
3542                // relies on the symmetry of sealing/unsealing operations. It's wiser to always
3543                // seal a tail, even when empty.
3544                let Some(tail) = arg4.as_record() else {
3545                    return mk_type_error("Record", 4, arg_pos4, arg4);
3546                };
3547
3548                let tail_closurized = arg4.clone().closurize(&mut self.context.cache, env4);
3549                let fields = tail
3550                    .into_opt()
3551                    .map(|r| r.fields.keys().map(|s| s.ident()).collect())
3552                    .unwrap_or_default();
3553                r.sealed_tail = Some(Rc::new(record::SealedTail::new(
3554                    *s,
3555                    label.clone(),
3556                    tail_closurized,
3557                    fields,
3558                )));
3559
3560                Ok(Closure {
3561                    value: arg3,
3562                    env: env3,
3563                })
3564            }
3565            NAryOp::RecordUnsealTail => {
3566                let mut args = args.into_iter();
3567                let (
3568                    Closure {
3569                        value: arg1,
3570                        env: _,
3571                    },
3572                    arg_pos1,
3573                ) = args.next().unwrap();
3574                let (
3575                    Closure {
3576                        value: arg2,
3577                        env: _,
3578                    },
3579                    arg_pos2,
3580                ) = args.next().unwrap();
3581                let (
3582                    Closure {
3583                        value: arg3,
3584                        env: env3,
3585                    },
3586                    arg_pos3,
3587                ) = args.next().unwrap();
3588
3589                debug_assert!(args.next().is_none());
3590
3591                let Some(s) = arg1.as_sealing_key() else {
3592                    return mk_type_error("SealingKey", 1, arg_pos1, arg1);
3593                };
3594
3595                let Some(label) = arg2.as_label() else {
3596                    return mk_type_error("Label", 2, arg_pos2, arg2);
3597                };
3598
3599                let Some(container) = arg3.as_record() else {
3600                    return mk_type_error("Record", 3, arg_pos3, arg3);
3601                };
3602
3603                container
3604                    .into_opt()
3605                    .and_then(|record| record.sealed_tail.as_ref())
3606                    .and_then(|tail| tail.unseal(s).cloned())
3607                    .ok_or_else(|| {
3608                        Box::new(EvalErrorKind::BlameError {
3609                            evaluated_arg: label.get_evaluated_arg(&self.context.cache),
3610                            label: label.clone(),
3611                        })
3612                    })
3613                    .map(|tail_unsealed| Closure {
3614                        value: tail_unsealed,
3615                        env: env3,
3616                    })
3617            }
3618            NAryOp::LabelInsertTypeVar => {
3619                let mut args = args.into_iter();
3620
3621                let (
3622                    Closure {
3623                        value: arg1,
3624                        env: _,
3625                    },
3626                    arg_pos1,
3627                ) = args.next().unwrap();
3628
3629                let (
3630                    Closure {
3631                        value: arg2,
3632                        env: _,
3633                    },
3634                    arg_pos2,
3635                ) = args.next().unwrap();
3636
3637                let (
3638                    Closure {
3639                        value: mut arg3,
3640                        env: _,
3641                    },
3642                    arg_pos3,
3643                ) = args.next().unwrap();
3644
3645                debug_assert!(args.next().is_none());
3646
3647                let Some(key) = arg1.as_sealing_key() else {
3648                    return mk_type_error("SealingKey", 1, arg_pos1, arg1);
3649                };
3650
3651                let Ok(polarity) = Polarity::try_from(&arg2) else {
3652                    return mk_type_error("Polarity", 2, arg_pos2, arg2);
3653                };
3654
3655                let ValueContentRefMut::Label(label) = arg3.content_make_mut() else {
3656                    return mk_type_error("Label", 3, arg_pos3, arg3);
3657                };
3658
3659                label
3660                    .type_environment
3661                    .push((*key, TypeVarData { polarity }));
3662
3663                Ok(arg3.with_pos_idx(arg_pos3.to_inherited()).into())
3664            }
3665            NAryOp::ArraySlice => {
3666                let mut args = args.into_iter();
3667
3668                let (
3669                    Closure {
3670                        value: arg1,
3671                        env: _,
3672                    },
3673                    arg_pos1,
3674                ) = args.next().unwrap();
3675
3676                let (
3677                    Closure {
3678                        value: arg2,
3679                        env: _,
3680                    },
3681                    arg_pos2,
3682                ) = args.next().unwrap();
3683
3684                let (
3685                    Closure {
3686                        value: mut arg3,
3687                        env: env3,
3688                    },
3689                    arg_pos3,
3690                ) = args.next().unwrap();
3691
3692                debug_assert!(args.next().is_none());
3693
3694                let Some(start) = arg1.as_number() else {
3695                    return mk_type_error("Number", 1, arg_pos1, arg1);
3696                };
3697
3698                let Some(end) = arg2.as_number() else {
3699                    return mk_type_error("Number", 2, arg_pos2, arg2);
3700                };
3701
3702                if arg3.is_inline_empty_array() {
3703                    // We are going to insert in the array, so we make sure that it's an allocated
3704                    // block and not an inline empty array.
3705                    arg3 = NickelValue::empty_array_block(arg3.pos_idx());
3706                }
3707
3708                let ValueContentRefMut::Array(Container::Alloc(ArrayData { array, .. })) =
3709                    arg3.content_make_mut()
3710                else {
3711                    return mk_type_error("Array", 3, arg_pos3, arg3);
3712                };
3713
3714                let Ok(start_as_usize) = usize::try_from(start) else {
3715                    return Err(Box::new(EvalErrorKind::Other(
3716                        format!(
3717                            "{op} expects its first argument (start) to be a \
3718                            positive integer smaller than {}, got {start}",
3719                            usize::MAX
3720                        ),
3721                        pos_op,
3722                    )));
3723                };
3724
3725                let Ok(end_as_usize) = usize::try_from(end) else {
3726                    return Err(Box::new(EvalErrorKind::Other(
3727                        format!(
3728                            "{op} expects its second argument (end) to be a \
3729                            positive integer smaller than {}, got {end}",
3730                            usize::MAX
3731                        ),
3732                        pos_op,
3733                    )));
3734                };
3735
3736                if end_as_usize < start_as_usize || end_as_usize > array.len() {
3737                    return Err(Box::new(EvalErrorKind::Other(
3738                        format!(
3739                            "{op}: index out of bounds. Expected `start <= end <= {}`, but \
3740                            got `start={start}` and `end={end}`.",
3741                            array.len()
3742                        ),
3743                        pos_op,
3744                    )));
3745                }
3746
3747                array.slice(start_as_usize, end_as_usize);
3748
3749                Ok(Closure {
3750                    value: arg3.with_pos_idx(pos_op_inh),
3751                    env: env3,
3752                })
3753            }
3754        }
3755    }
3756}
3757
3758/// The enum tag returned by Typeof and Cast.
3759///
3760/// This function is a less precise version of `v.type_of()`, because `type_tag` has backward
3761/// compatibility guarantees to uphold. Instead of relying on
3762/// [crate::eval::value::NickelValue::type_of], it's safer to duplicate the logic here.
3763fn type_tag(v: &NickelValue) -> &'static str {
3764    match v.content_ref() {
3765        ValueContentRef::Null => "Other",
3766        ValueContentRef::Bool(_) => "Bool",
3767        ValueContentRef::Number(_) => "Number",
3768        ValueContentRef::Array(_) => "Array",
3769        ValueContentRef::Record(_) => "Record",
3770        ValueContentRef::String(_) => "String",
3771        ValueContentRef::Term(term) => match term {
3772            Term::RecRecord(..) => "Record",
3773            Term::Fun(..) => "Function",
3774            _ => "Other",
3775        },
3776        ValueContentRef::Label(_) => "Label",
3777        ValueContentRef::EnumVariant(_) => "Enum",
3778        ValueContentRef::ForeignId(_) => "ForeignId",
3779        ValueContentRef::CustomContract(_) => "CustomContract",
3780        ValueContentRef::Type(_) => "Type",
3781        _ => "Other",
3782    }
3783}
3784
3785/// Compute the equality of two terms, represented as closures.
3786///
3787/// # Parameters
3788///
3789/// - `c1`: the closure of the first operand.
3790/// - `c2`: the closure of the second operand.
3791/// - `pos_op`: the position of the equality operation, used for error diagnostics.
3792///
3793/// # Return
3794///
3795/// If the comparison is successful, returns a bool indicating whether the values were equal,
3796/// otherwise returns an [`EvalError`] indicating that the values cannot be compared (typically two
3797/// functions).
3798///
3799/// # Uncomparable values
3800///
3801/// Comparing two functions is undecidable. Even in simple cases, it's not trivial to handle an
3802/// approximation (functions might capture free variables, you'd need to take eta-conversion into
3803/// account to equate e.g. `fun x => x` and `fun y => y`, etc.).
3804///
3805/// Thus, by default, comparing a function to something else always returns `false`. However, this
3806/// breaks the reflexivity property of equality, which users might rightfully rely on, because `fun
3807/// x => x` isn't equal to itself. Also, comparing two functions is probably never intentional nor
3808/// meaningful: thus we error out when trying to compare two functions. We still allow comparing
3809/// functions to something else, because it's useful to have tests like `if value == 1` or `if
3810/// value == null` typically in contracts without having to defensively check that `value` is a
3811/// function.
3812///
3813/// The same reasoning applies to foreign values (which we don't want to compare for security
3814/// reasons, at least right now, not because we can't).
3815fn eq<C: Cache>(
3816    cache: &mut C,
3817    c1: Closure,
3818    c2: Closure,
3819    pos_op: PosIdx,
3820) -> Result<EqResult, ErrorKind> {
3821    let Closure {
3822        value: value1,
3823        env: env1,
3824    } = c1;
3825
3826    let Closure {
3827        value: value2,
3828        env: env2,
3829    } = c2;
3830
3831    // Take a list of subequalities, and either return `EqResult::Bool(true)` if it is empty, or
3832    // generate an appropriate `EqResult::Eqs` variant with closurized terms in it.
3833    fn gen_eqs<I, C: Cache>(
3834        cache: &mut C,
3835        mut it: I,
3836        env1: Environment,
3837        env2: Environment,
3838    ) -> EqResult
3839    where
3840        I: Iterator<Item = (NickelValue, NickelValue)>,
3841    {
3842        if let Some((v1, v2)) = it.next() {
3843            let eqs = it
3844                .map(|(v1, v2)| {
3845                    (
3846                        Closure {
3847                            value: v1,
3848                            env: env1.clone(),
3849                        },
3850                        Closure {
3851                            value: v2,
3852                            env: env2.clone(),
3853                        },
3854                    )
3855                })
3856                .collect();
3857
3858            EqResult::Eqs(v1.closurize(cache, env1), v2.closurize(cache, env2), eqs)
3859        } else {
3860            EqResult::Bool(true)
3861        }
3862    }
3863
3864    match (value1.content_ref(), value2.content_ref()) {
3865        (ValueContentRef::Null, ValueContentRef::Null) => Ok(EqResult::Bool(true)),
3866        (ValueContentRef::Bool(b1), ValueContentRef::Bool(b2)) => Ok(EqResult::Bool(b1 == b2)),
3867        (ValueContentRef::Number(n1), ValueContentRef::Number(n2)) => Ok(EqResult::Bool(n1 == n2)),
3868        (ValueContentRef::String(s1), ValueContentRef::String(s2)) => Ok(EqResult::Bool(s1 == s2)),
3869        (ValueContentRef::Label(l1), ValueContentRef::Label(l2)) => Ok(EqResult::Bool(l1 == l2)),
3870        (ValueContentRef::SealingKey(k1), ValueContentRef::SealingKey(k2)) => {
3871            Ok(EqResult::Bool(k1 == k2))
3872        }
3873        (
3874            ValueContentRef::EnumVariant(EnumVariantData {
3875                tag: tag1,
3876                arg: None,
3877            }),
3878            ValueContentRef::EnumVariant(EnumVariantData {
3879                tag: tag2,
3880                arg: None,
3881            }),
3882        ) => Ok(EqResult::Bool(tag1.ident() == tag2.ident())),
3883        (
3884            ValueContentRef::EnumVariant(EnumVariantData {
3885                tag: tag1,
3886                arg: Some(arg1),
3887            }),
3888            ValueContentRef::EnumVariant(EnumVariantData {
3889                tag: tag2,
3890                arg: Some(arg2),
3891            }),
3892        ) if tag1.ident() == tag2.ident() => Ok(gen_eqs(
3893            cache,
3894            std::iter::once((arg1.clone(), arg2.clone())),
3895            env1,
3896            env2,
3897        )),
3898        // [^eq-empty-containers] We can't just handle the pattern `(Container::Empty,
3899        // Container::Empty)`, because the first could be the inlined empty record while the second
3900        // is allocated but empty as well (typically the case for the empty open record `{..}`), or
3901        // have only empty optional fields (which equality ignores). Hence we rely on
3902        // `has_only_empty_opts`, which handles both cases.
3903        (ValueContentRef::Record(container1), ValueContentRef::Record(container2))
3904            if container1.has_only_empty_opts() && container2.has_only_empty_opts() =>
3905        {
3906            Ok(EqResult::Bool(true))
3907        }
3908        (
3909            ValueContentRef::Record(Container::Alloc(r1)),
3910            ValueContentRef::Record(Container::Alloc(r2)),
3911        ) => {
3912            let merge::split::SplitResult {
3913                left,
3914                center,
3915                right,
3916            } = merge::split::split_ref(&r1.fields, &r2.fields);
3917
3918            // As for other record operations, we ignore optional fields without a definition.
3919            if !left.values().all(Field::is_empty_optional)
3920                || !right.values().all(Field::is_empty_optional)
3921            {
3922                Ok(EqResult::Bool(false))
3923            } else if center.is_empty() {
3924                Ok(EqResult::Bool(true))
3925            } else {
3926                // We consider undefined values to be equal. We filter out pairs of undefined
3927                // values, but we reject pairs where one of the value is undefined and not the
3928                // other.
3929                let eqs: Result<Vec<_>, _> = center
3930                    .into_iter()
3931                    .filter_map(|(id, (field1, field2))| match (field1, field2) {
3932                        (
3933                            Field {
3934                                value: Some(value1),
3935                                pending_contracts: pending_contracts1,
3936                                ..
3937                            },
3938                            Field {
3939                                value: Some(value2),
3940                                pending_contracts: pending_contracts2,
3941                                ..
3942                            },
3943                        ) => {
3944                            let pos1 = value1.pos_idx();
3945                            let pos2 = value2.pos_idx();
3946
3947                            let value1_with_ctr =
3948                                RuntimeContract::apply_all(value1, pending_contracts1, pos1);
3949                            let value2_with_ctr =
3950                                RuntimeContract::apply_all(value2, pending_contracts2, pos2);
3951                            Some(Ok((value1_with_ctr, value2_with_ctr)))
3952                        }
3953                        (Field { value: None, .. }, Field { value: None, .. }) => None,
3954                        (
3955                            Field {
3956                                value: v1 @ None,
3957                                metadata,
3958                                ..
3959                            },
3960                            Field { value: Some(_), .. },
3961                        )
3962                        | (
3963                            Field {
3964                                value: v1 @ Some(_),
3965                                ..
3966                            },
3967                            Field {
3968                                value: None,
3969                                metadata,
3970                                ..
3971                            },
3972                        ) => {
3973                            let pos_record = if v1.is_none() {
3974                                value1.pos_idx()
3975                            } else {
3976                                value2.pos_idx()
3977                            };
3978
3979                            Some(Err(Box::new(EvalErrorKind::MissingFieldDef {
3980                                id,
3981                                metadata: metadata.into_inner(),
3982                                pos_record,
3983                                pos_access: pos_op,
3984                            })))
3985                        }
3986                    })
3987                    .collect();
3988
3989                Ok(gen_eqs(cache, eqs?.into_iter(), env1, env2))
3990            }
3991        }
3992        // See [^eq-empty-containers]
3993        (ValueContentRef::Array(container1), ValueContentRef::Array(container2))
3994            if container1.is_empty() && container2.is_empty() =>
3995        {
3996            Ok(EqResult::Bool(true))
3997        }
3998        (
3999            ValueContentRef::Array(Container::Alloc(array_data1)),
4000            ValueContentRef::Array(Container::Alloc(array_data2)),
4001        ) if array_data1.array.len() == array_data2.array.len() => {
4002            // Equalities are tested in reverse order, but that shouldn't matter. If it
4003            // does, just do `eqs.rev()`
4004
4005            // We should apply all contracts here, otherwise we risk having wrong values, think
4006            // record contracts with default values, wrapped terms, etc.
4007
4008            let mut eqs = array_data1
4009                .array
4010                .iter()
4011                .cloned()
4012                .map(|elt| {
4013                    let pos = elt.pos_idx().to_inherited();
4014                    RuntimeContract::apply_all(
4015                        elt,
4016                        array_data1.pending_contracts.iter().cloned(),
4017                        pos,
4018                    )
4019                    .closurize(cache, env1.clone())
4020                })
4021                .collect::<Vec<_>>()
4022                .into_iter()
4023                .zip(array_data2.array.iter().cloned().map(|elt| {
4024                    let pos = elt.pos_idx().to_inherited();
4025                    RuntimeContract::apply_all(
4026                        elt,
4027                        array_data2.pending_contracts.iter().cloned(),
4028                        pos,
4029                    )
4030                    .closurize(cache, env2.clone())
4031                }))
4032                .collect::<Vec<_>>();
4033
4034            match eqs.pop() {
4035                None => Ok(EqResult::Bool(true)),
4036                Some((v1, v2)) => {
4037                    let eqs = eqs
4038                        .into_iter()
4039                        .map(|(v1, v2)| (v1.into(), v2.into()))
4040                        .collect::<Vec<(Closure, Closure)>>();
4041
4042                    Ok(EqResult::Eqs(v1, v2, eqs))
4043                }
4044            }
4045        }
4046        // Function-like terms and foreign ids can't be compared together.
4047        (ValueContentRef::ForeignId(_), ValueContentRef::ForeignId(_))
4048        | (ValueContentRef::CustomContract(_), ValueContentRef::CustomContract(_))
4049        | (ValueContentRef::Term(Term::Fun(..)), ValueContentRef::Term(Term::Fun(..))) => {
4050            Err(Box::new(EvalErrorKind::IncomparableValues {
4051                eq_pos: pos_op,
4052                left: value1,
4053                right: value2,
4054            }))
4055        }
4056        (_, _) => Ok(EqResult::Bool(false)),
4057    }
4058}
4059
4060/// Eta-expands a unary operator into a (lazy) function.
4061///
4062/// Regex-based primitive operations are evaluated to a function that captures the compiled regexp,
4063/// to avoid recompiling it at each call. [eta_expand] builds such a closure: given a primary (in
4064/// practice, regex) operator `%op1%`, [eta_expand] will return the expression `fun x => %op1% x`.
4065/// Each intermediate term is given the position index `pos_op`.
4066fn eta_expand(op: UnaryOp, pos_op: PosIdx) -> Term {
4067    let param = LocIdent::fresh();
4068
4069    Term::fun(
4070        param,
4071        NickelValue::term(
4072            Term::op1(op, NickelValue::term(Term::Var(param), pos_op)),
4073            pos_op,
4074        ),
4075    )
4076}
4077
4078trait MapValuesClosurize: Sized {
4079    /// Returns a HashMap from `Ident` to `Field` by:
4080    ///
4081    /// 1. Appplying the pending contracts to each fields
4082    /// 2. Applying the provided function
4083    /// 3. Closurizing each result into the shared environment.
4084    ///
4085    /// Because we applied the pending contracts in 1., they are dropped in the result: all fields
4086    /// have an empty set of pending contracts.
4087    fn map_values_closurize<F, C: Cache>(
4088        self,
4089        cache: &mut C,
4090        env: &Environment,
4091        f: F,
4092    ) -> Result<IndexMap<LocIdent, Field>, record::MissingFieldDefError>
4093    where
4094        F: FnMut(LocIdent, NickelValue) -> NickelValue;
4095}
4096
4097impl<Iter> MapValuesClosurize for Iter
4098where
4099    Iter: IntoIterator<Item = (LocIdent, Field)>,
4100{
4101    fn map_values_closurize<F, C: Cache>(
4102        self,
4103        cache: &mut C,
4104        env: &Environment,
4105        mut f: F,
4106    ) -> Result<IndexMap<LocIdent, Field>, record::MissingFieldDefError>
4107    where
4108        F: FnMut(LocIdent, NickelValue) -> NickelValue,
4109    {
4110        self.into_iter()
4111            .map(|(id, field)| {
4112                let value = field
4113                    .value
4114                    .map(|value| {
4115                        let pos = value.pos_idx();
4116                        let value_with_ctrs = RuntimeContract::apply_all(
4117                            value,
4118                            field.pending_contracts.iter().cloned(),
4119                            pos,
4120                        );
4121                        f(id, value_with_ctrs)
4122                    })
4123                    .ok_or(record::MissingFieldDefErrorData {
4124                        id,
4125                        metadata: field.metadata.clone_inner(),
4126                    })?;
4127
4128                let field = Field {
4129                    value: Some(value),
4130                    pending_contracts: Vec::new(),
4131                    ..field
4132                }
4133                .closurize(cache, env.clone());
4134
4135                Ok((id, field))
4136            })
4137            .collect()
4138    }
4139}
4140
4141/// Wrap a value in a [crate::term::Term::Closurize] operator with the same position index.
4142fn closurize_container(value: NickelValue) -> NickelValue {
4143    let pos_idx = value.pos_idx();
4144    NickelValue::term(Term::Closurize(value), pos_idx)
4145}
4146
4147#[cfg(test)]
4148mod tests {
4149    use super::*;
4150    use crate::{
4151        cache::resolvers::DummyResolver,
4152        error::NullReporter,
4153        eval::{Environment, VmContext, cache::CacheImpl},
4154    };
4155
4156    const NO_APP_INFO: PrimopAppInfo = PrimopAppInfo {
4157        call_stack_size: 0,
4158        pos_idx: PosIdx::NONE,
4159    };
4160
4161    // Initialize a VM with a default context
4162    fn with_vm(test: impl FnOnce(VirtualMachine<'_, DummyResolver, CacheImpl>)) {
4163        let mut vm_ctxt = VmContext::new(DummyResolver {}, std::io::sink(), NullReporter {});
4164        let vm = VirtualMachine::new_empty_env(&mut vm_ctxt);
4165        test(vm);
4166    }
4167
4168    #[test]
4169    fn ite_operation() {
4170        with_vm(|mut vm| {
4171            vm.stack.push_arg(mk_term::integer(5).into(), PosIdx::NONE);
4172            vm.stack.push_arg(mk_term::integer(46).into(), PosIdx::NONE);
4173            vm.stack.push_op1_cont(Op1ContItem {
4174                op: UnaryOp::IfThenElse,
4175                orig_pos_arg: PosIdx::NONE,
4176                app_info: NO_APP_INFO,
4177            });
4178
4179            assert_eq!(
4180                vm.continue_op(NickelValue::bool_true().into()),
4181                Ok(mk_term::integer(46).into())
4182            );
4183            assert_eq!(0, vm.stack.count_args());
4184        });
4185    }
4186
4187    #[test]
4188    fn plus_first_term_operation() {
4189        with_vm(|mut vm| {
4190            vm.stack.push_op2_first_cont(Op2FirstContItem {
4191                op: BinaryOp::Plus,
4192                app_info: NO_APP_INFO,
4193                arg2: Closure {
4194                    value: mk_term::integer(6),
4195                    env: Environment::new(),
4196                },
4197                orig_pos_arg1: PosIdx::NONE,
4198            });
4199
4200            assert_eq!(
4201                vm.continue_op(mk_term::integer(7).into()),
4202                Ok(mk_term::integer(6).into())
4203            );
4204            assert_eq!(1, vm.stack.count_conts());
4205            assert_eq!(
4206                (Op2SecondContItem {
4207                    op: BinaryOp::Plus,
4208                    app_info: NO_APP_INFO,
4209                    arg1_evaled: mk_term::integer(7).into(),
4210                    orig_pos_arg1: PosIdx::NONE,
4211                    orig_pos_arg2: PosIdx::NONE,
4212                }),
4213                vm.stack
4214                    .pop_op2_second_cont()
4215                    .expect("Condition already checked.")
4216            );
4217        });
4218    }
4219
4220    #[test]
4221    fn plus_second_term_operation() {
4222        with_vm(|mut vm| {
4223            vm.stack.push_op2_second_cont(Op2SecondContItem {
4224                op: BinaryOp::Plus,
4225                app_info: NO_APP_INFO,
4226                arg1_evaled: mk_term::integer(7).into(),
4227                orig_pos_arg1: PosIdx::NONE,
4228                orig_pos_arg2: PosIdx::NONE,
4229            });
4230
4231            assert_eq!(
4232                vm.continue_op(mk_term::integer(6).into()),
4233                Ok(Closure {
4234                    value: mk_term::integer(13),
4235                    env: Environment::new()
4236                })
4237            );
4238        });
4239    }
4240}