nickel_lang_core/eval/mod.rs
1//! Evaluation of a Nickel term.
2//!
3//! Implementation of the Nickel abstract machine. Note that this machine is not currently
4//! formalized somewhere and is just a convenient name to designate the current implementation.
5//!
6//! # The Nickel Abstract Machine
7//!
8//! The abstract machine is a stack machine composed of the following elements:
9//!
10//! - The term being currently evaluated
11//! - The main stack, storing arguments, cache indices and pending computations (continuations)
12//! - A pair of [environment][Environment], mapping identifiers to [closures][Closure]:
13//! - The initial environment contains builtin functions accessible from anywhere, is immutable
14//! and alive during the whole evaluation
15//! - The local environment contains the variables in scope of the current term and is subject
16//! to garbage collection (currently reference counting based)
17//! - A [callstack], mainly for error reporting purpose
18//!
19//! Depending on the shape of the current term, the following actions are performed:
20//!
21//! ## Core calculus
22//!
23//! - **variable (`id`)**: the term bound to `id` in the environment is fetched, and an update index is
24//! pushed on the stack to indicate that once this term has been evaluated, the cached value for
25//! this variable must be updated
26//! - **application `func arg`**: a closure containing the argument and the current environment is pushed on
27//! the stack and the VM proceed with the evaluation of `func`
28//! - **`let id = term in body`**: `term` is bound to `id` in the environment and the VM proceeds
29//! with the evaluation of `body`
30//! - **`fun id => body`**: tries to pop an argument from the stack. If there is one, we bind it to
31//! `id` in the environment and proceed with the body of the function. Otherwise, we are done:
32//! the end result is a (unapplied) function
33//! - **Index on stack**: if the evaluation of the current term is done, and there is one (or
34//! several) index on the stack, this means we have to perform an evaluation cache update.
35//! Consecutive indices are popped from the stack and are updated to point to the current evaluated
36//! term.
37//! - **Import**: import must have been resolved before the evaluation starts. An unresolved import
38//! causes an [`crate::error::EvalErrorKind::InternalError`]. A resolved import, identified by a
39//! `FileId`, is retrieved from the import resolver and evaluation proceeds.
40//!
41//! ## Operators
42//!
43//! Operators are strict by definition. To evaluate say `exp1 + exp2`, the following steps have to
44//! be performed:
45//!
46//! - `exp1` needs to be evaluated. The result must be saved somewhere, together with the resulting
47//! environment
48//! - same thing for `exp2`
49//! - Finally, the implementation of `+` can proceed with the computation
50//!
51//! We detail the case of binary operators, as the case of unary ones is similar and simpler.
52//!
53//! - **Op(op, first, second)**: pushes an `OpFirst` element on the stack, which saves the operator
54//! `op`, the second argument `second` and the current environment, and proceed with the evaluation
55//! of `first`
56//! - **OpFirst on stack**: if the evaluation of the current term is done and there is an `OpFirst`
57//! marker on the stack, then:
58//! 1. Extract the saved operator, the second argument and the environment `env2` from the
59//! marker
60//! 2. Push an `OpSecond` marker, saving the operator and the evaluated form of the first
61//! argument with its environment
62//! 3. Proceed with the evaluation of the second argument in environment `env2`
63//! - **OpSecond on stack**: once the second term is evaluated, we can get back the operator and the
64//! first term evaluated, and forward all both arguments evaluated and their respective
65//! environment to the specific implementation of the operator (located in [operation], or in
66//! [merge] for `merge`).
67//!
68//! # Garbage collection
69//!
70//! Currently the machine relies on Rust's reference counting to manage memory. Precisely, the
71//! environment stores `Rc<RefCell<Closure>>` objects, which are reference-counted pointers to a
72//! mutable memory cell. This means that we do not deep copy everything everywhere, but this is
73//! probably suboptimal for a functional language and is unable to collect cyclic data, which may
74//! appear inside recursive records. A dedicated garbage collector is probably something to
75//! consider at some point.
76use crate::{
77 cache::{CacheHub as ImportCaches, ImportResolver},
78 closurize::{Closurize, closurize_rec_record},
79 environment::Environment as GenericEnvironment,
80 error::{
81 Error, EvalCtxt, EvalError, EvalErrorData, EvalErrorKind, PointedExportErrorData, Reporter,
82 warning::Warning,
83 },
84 files::{FileId, Files},
85 identifier::{Ident, LocIdent},
86 metrics::{increment, measure_runtime},
87 position::{PosIdx, PosTable},
88 program::FieldPath,
89 term::{
90 BinaryOp, BindingType, FunData, Import, RecordOpKind, RuntimeContract, StrChunk, Term,
91 UnaryOp, make as mk_term,
92 record::{Field, RecordData},
93 string::NickelString,
94 },
95 transform::gen_pending_contracts,
96 typ::UnboundTypeVariableError,
97};
98
99use std::{
100 io::Write,
101 mem::ManuallyDrop,
102 ops::{Deref, DerefMut},
103};
104
105pub mod cache;
106pub mod callstack;
107pub mod contract_eq;
108pub mod fixpoint;
109pub mod merge;
110pub mod operation;
111pub mod stack;
112pub mod value;
113
114use callstack::*;
115use stack::{
116 Op1ContItem, Op2FirstContItem, OpNContItem, PrimopAppInfo, SealedCont, Stack, StrAccItem,
117};
118use value::{Container, EnumVariantData, NickelValue, ValueContent, ValueContentRef};
119
120use self::cache::{Cache, CacheIndex};
121
122impl AsRef<Vec<StackElem>> for CallStack {
123 fn as_ref(&self) -> &Vec<StackElem> {
124 &self.0
125 }
126}
127
128/// The context of the Nickel virtual machine. The context stores external state that might need to
129/// outlive one VM instance. The virtual machine typically borrows the context mutably.
130pub struct VmContext<R: ImportResolver, C: Cache> {
131 /// The interface used to resolve and fetch imports.
132 pub import_resolver: R,
133 /// The stream for writing trace output.
134 pub trace: Box<dyn Write>,
135 /// A collector for warnings. Currently we only collect warnings and not errors; errors
136 /// terminate evaluation (or typechecking, or whatever) immediately, and so they just
137 /// get early-returned in a `Result`.
138 pub reporter: Box<dyn Reporter<(Warning, Files)>>,
139 /// The evaluation cache.
140 pub cache: C,
141 /// The position table, mapping position indices to spans.
142 pub pos_table: PosTable,
143}
144
145impl<R: ImportResolver, C: Cache> VmContext<R, C> {
146 /// Creates a new VM context from a resolver, a trace sink and a reporter. Creates a new empty
147 /// position table and eval cache.
148 pub fn new(
149 import_resolver: R,
150 trace: impl Write + 'static,
151 reporter: impl Reporter<(Warning, Files)> + 'static,
152 ) -> Self {
153 Self::new_with_pos_table(import_resolver, PosTable::new(), trace, reporter)
154 }
155
156 /// Creates a new VM context from a resolver, a position table, a trace sink and a reporter.
157 /// Creates a new empty eval cache.
158 pub fn new_with_pos_table(
159 import_resolver: R,
160 pos_table: PosTable,
161 trace: impl Write + 'static,
162 reporter: impl Reporter<(Warning, Files)> + 'static,
163 ) -> Self {
164 VmContext {
165 import_resolver,
166 trace: Box::new(trace),
167 reporter: Box::new(reporter),
168 cache: C::new(),
169 pos_table,
170 }
171 }
172}
173
174/// Many functions in [self] and submodules returns an error of type [EvalErrorKind]. However, this
175/// error is large, so we box it to avoid performance issues related to large error variant (and to
176/// satisfy clippy).
177pub type ErrorKind = Box<EvalErrorKind>;
178
179impl From<UnboundTypeVariableError> for ErrorKind {
180 fn from(err: UnboundTypeVariableError) -> Self {
181 Box::new(err.into())
182 }
183}
184
185impl From<PointedExportErrorData> for ErrorKind {
186 fn from(err: PointedExportErrorData) -> Self {
187 Box::new(err.into())
188 }
189}
190
191impl<C: Cache> VmContext<ImportCaches, C> {
192 /// Prepares the underlying program for evaluation (load the stdlib, typecheck, transform,
193 /// etc.).
194 pub fn prepare_eval(&mut self, main_id: FileId) -> Result<NickelValue, Error> {
195 self.prepare_eval_impl(main_id, true)
196 }
197
198 /// Same as [Self::prepare_eval], but skip typechecking.
199 pub fn prepare_eval_only(&mut self, main_id: FileId) -> Result<NickelValue, Error> {
200 self.prepare_eval_impl(main_id, false)
201 }
202
203 fn prepare_eval_impl(
204 &mut self,
205 main_id: FileId,
206 typecheck: bool,
207 ) -> Result<NickelValue, Error> {
208 measure_runtime!(
209 "runtime:prepare_stdlib",
210 self.import_resolver.prepare_stdlib(&mut self.pos_table)?
211 );
212
213 measure_runtime!(
214 "runtime:prepare_main",
215 if typecheck {
216 self.import_resolver.prepare(&mut self.pos_table, main_id)
217 } else {
218 self.import_resolver
219 .prepare_eval_only(&mut self.pos_table, main_id)
220 }
221 )?;
222
223 // Unwrap: closurization only fails if the input wasn't parsed, and we just
224 // parsed it.
225 self.import_resolver
226 .closurize(&mut self.cache, main_id)
227 .unwrap();
228
229 Ok(self.import_resolver.get(main_id).unwrap())
230 }
231}
232
233/// The Nickel virtual machine.
234///
235/// # Drop
236///
237/// The virtual machine implements [Drop]. The stack is unwinded by default upon dropping, which
238/// amounts to calling [VirtualMachine::reset()], and avoids leaving thunks in the blackholed state
239/// on abort. If you don't need unwinding and don't want to pay for it (though it doesn't cost
240/// anything for successful executions), see [NoUnwindVirtualMachine].
241pub struct VirtualMachine<'ctxt, R: ImportResolver, C: Cache> {
242 context: &'ctxt mut VmContext<R, C>,
243 /// The main stack, storing arguments, cache indices and pending computations.
244 stack: Stack<C>,
245 /// The call stack, for error reporting.
246 call_stack: CallStack,
247 /// The initial environment containing stdlib and builtin functions accessible from anywhere
248 initial_env: Environment,
249}
250
251impl<'ctxt, R: ImportResolver, C: Cache> Drop for VirtualMachine<'ctxt, R, C> {
252 fn drop(&mut self) {
253 self.reset();
254 }
255}
256
257impl<'ctxt, R: ImportResolver, C: Cache> VirtualMachine<'ctxt, R, C> {
258 /// Creates a new VM with an empty initial environment. See [Self::new] for initialization of
259 /// the initial environment when `R` is instantiated to [crate::cache::CacheHub].
260 pub fn new_empty_env(context: &'ctxt mut VmContext<R, C>) -> Self {
261 VirtualMachine {
262 context,
263 call_stack: Default::default(),
264 stack: Stack::new(),
265 initial_env: Environment::new(),
266 }
267 }
268
269 pub fn warn(&mut self, warning: Warning) {
270 self.context
271 .reporter
272 .report((warning, self.context.import_resolver.files().clone()));
273 }
274
275 /// Reset the state of the machine (stacks, eval mode and state of cached elements) to prepare
276 /// for another evaluation round.
277 pub fn reset(&mut self) {
278 self.call_stack.0.clear();
279 self.stack.unwind(&mut self.context.cache);
280 }
281
282 pub fn import_resolver(&self) -> &R {
283 &self.context.import_resolver
284 }
285
286 pub fn import_resolver_mut(&mut self) -> &mut R {
287 &mut self.context.import_resolver
288 }
289
290 /// Evaluate a Nickel expression. Wrapper around [VirtualMachine::eval_closure] that starts
291 /// from an empty local environment and drops the final environment.
292 pub fn eval(&mut self, value: NickelValue) -> Result<NickelValue, EvalError> {
293 self.eval_closure(value.into()).map(|closure| closure.value)
294 }
295
296 /// Fully evaluate a Nickel term: the result is not a WHNF but to a value with all variables
297 /// substituted.
298 pub fn eval_full(&mut self, value: NickelValue) -> Result<NickelValue, EvalError> {
299 self.eval_full_closure(value.into())
300 .map(|result| result.value)
301 }
302
303 /// Same as [Self::eval_full], but takes a closure as an argument instead of a term.
304 pub fn eval_full_closure(&mut self, closure: Closure) -> Result<Closure, EvalError> {
305 self.eval_deep_closure_impl(closure, false)
306 .map(|result| Closure {
307 value: subst(
308 &self.context.pos_table,
309 &self.context.cache,
310 result.value,
311 &self.initial_env,
312 &result.env,
313 ),
314 env: result.env,
315 })
316 }
317
318 /// Like [Self::eval_full], but skips evaluating record fields marked `not_exported`.
319 pub fn eval_full_for_export(&mut self, value: NickelValue) -> Result<NickelValue, EvalError> {
320 self.eval_deep_closure_impl(value.into(), true)
321 .map(|result| {
322 subst(
323 &self.context.pos_table,
324 &self.context.cache,
325 result.value,
326 &self.initial_env,
327 &result.env,
328 )
329 })
330 }
331
332 /// Same as [Self::eval_full_for_export], but takes a closure as an argument instead of a term.
333 pub fn eval_full_for_export_closure(
334 &mut self,
335 closure: Closure,
336 ) -> Result<NickelValue, EvalError> {
337 self.eval_deep_closure_impl(closure, true).map(|result| {
338 subst(
339 &self.context.pos_table,
340 &self.context.cache,
341 result.value,
342 &self.initial_env,
343 &result.env,
344 )
345 })
346 }
347
348 /// Fully evaluates a Nickel term like `eval_full`, but does not substitute all variables.
349 pub fn eval_deep(&mut self, value: NickelValue) -> Result<NickelValue, EvalError> {
350 self.eval_deep_closure_impl(value.into(), false)
351 .map(|result| result.value)
352 }
353
354 /// Same as [Self::eval_deep], but take a closure as an argument instead of a term.
355 pub fn eval_deep_closure(&mut self, closure: Closure) -> Result<NickelValue, EvalError> {
356 self.eval_deep_closure_impl(closure, false)
357 .map(|result| result.value)
358 }
359
360 /// Use a specific initial environment for evaluation. Usually, [VmContext::prepare_eval] is
361 /// populating the initial environment. But in some cases, such as testing or benchmarks, we
362 /// might want to use a different one.
363 ///
364 /// Return the new virtual machine with the updated initial environment.
365 pub fn with_initial_env(mut self, env: Environment) -> Self {
366 self.initial_env = env;
367 self
368 }
369
370 fn eval_deep_closure_impl(
371 &mut self,
372 mut closure: Closure,
373 for_export: bool,
374 ) -> Result<Closure, EvalError> {
375 closure.value = mk_term::op1(
376 UnaryOp::Force {
377 ignore_not_exported: for_export,
378 },
379 closure.value,
380 );
381
382 self.eval_closure(closure)
383 }
384
385 /// Take a term and a field path, and evaluate until the corresponding field can be extracted.
386 /// Return the resulting field in its final environment.
387 ///
388 /// Note that this method doesn't evaluate the content of the field itself. Calling it with an
389 /// empty path simply returns the original expression unevaluated in an empty environment. If
390 /// you need to evaluate the value later, don't forget to apply pending contracts stored in the
391 /// field.
392 ///
393 /// For example, extracting `foo.bar.baz` on a term `exp` will evaluate `exp` to a record and
394 /// try to extract the field `foo`. If anything goes wrong (the result isn't a record or the
395 /// field `bar` doesn't exist), a proper error is raised. Otherwise,
396 /// [Self::extract_field_closure] applies the same recipe recursively and evaluate the content
397 /// of the `foo` field extracted from `exp` to a record, tries to extract `bar`, and so on.
398 pub fn extract_field_closure(
399 &mut self,
400 closure: Closure,
401 path: &FieldPath,
402 ) -> Result<(Field, Environment), EvalError> {
403 self.extract_field_impl(closure, path, false)
404 }
405
406 /// Same as [Self::extract_field_closure], but also requires that the field value is defined
407 /// and returns the value directly.
408 ///
409 /// This method also applies potential pending contracts to the value.
410 ///
411 /// In theory, extracting the value could be done manually after calling to
412 /// [Self::extract_field_closure], instead of needing a separate method.
413 ///
414 /// However, once [Self::extract_field_closure] returns, most contextual information required
415 /// to raise a proper error if the field is missing (e.g. positions) has been lost. So, if the
416 /// returned field's value is `None`, we would have a hard time reporting a good error message.
417 /// On the other hand, [Self::extract_field_value_closure] raises the error earlier, when the
418 /// context is still available.
419 pub fn extract_field_value_closure(
420 &mut self,
421 closure: Closure,
422 path: &FieldPath,
423 ) -> Result<Closure, EvalError> {
424 let (field, env) = self.extract_field_impl(closure, path, true)?;
425
426 // unwrap(): by definition, extract_field_impl(_, _, true) ensure that
427 // `field.value.is_some()`
428 let value = field.value.unwrap();
429 let pos_idx = value.pos_idx();
430
431 let value_with_ctr =
432 RuntimeContract::apply_all(value, field.pending_contracts.iter().cloned(), pos_idx);
433
434 let Closure { value, env } = self.eval_closure(Closure {
435 value: value_with_ctr,
436 env,
437 })?;
438
439 Ok(Closure { value, env })
440 }
441
442 fn extract_field_impl(
443 &mut self,
444 closure: Closure,
445 path: &FieldPath,
446 require_defined: bool,
447 ) -> Result<(Field, Environment), EvalError> {
448 let mut prev_pos_idx = closure.value.pos_idx();
449
450 let mut field: Field = closure.value.into();
451 let mut path = path.0.iter().peekable();
452 let mut env = closure.env;
453
454 let Some(mut prev_id) = path.peek().cloned() else {
455 return Ok((field, env));
456 };
457
458 for id in path {
459 let Some(current_value) = field.value else {
460 return self.throw_with_ctxt(EvalErrorKind::MissingFieldDef {
461 id: *prev_id,
462 metadata: field.metadata.into_inner(),
463 pos_record: prev_pos_idx,
464 pos_access: PosIdx::NONE,
465 });
466 };
467
468 // We evaluate the fields' value, either to handle the next ident of the
469 // path, or to show the value if we are treating the last ident of the path
470
471 prev_pos_idx = current_value.pos_idx();
472
473 let curr_value_with_ctr =
474 RuntimeContract::apply_all(current_value, field.pending_contracts, prev_pos_idx);
475
476 let current_evaled = self.eval_closure(Closure {
477 value: curr_value_with_ctr,
478 env,
479 })?;
480
481 env = current_evaled.env;
482
483 match current_evaled.value.content_ref() {
484 ValueContentRef::Record(container) => {
485 let Some(next_field) = container.get(*id).cloned() else {
486 let pos_op = self.context.pos_table.push(id.pos);
487
488 return self.throw_with_ctxt(EvalErrorKind::FieldMissing {
489 id: *id,
490 field_names: container.field_names(RecordOpKind::IgnoreEmptyOpt),
491 operator: "extract_field".to_owned(),
492 pos_record: prev_pos_idx,
493 // TODO: we need to push back the position in the table, which isn't
494 // too bad, but is a bit useless. Maybe we should have a runtime
495 // version of identifiers with `PosIdx` instead of `TermPos`?
496 pos_op,
497 });
498 };
499
500 field = next_field;
501 }
502 _ => {
503 //unwrap(): if we enter this pattern branch, `field.value` must be `Some(_)`
504 return self.throw_with_ctxt(EvalErrorKind::QueryNonRecord {
505 pos: prev_pos_idx,
506 id: *id,
507 value: current_evaled.value,
508 });
509 }
510 }
511
512 prev_id = id;
513 }
514
515 if field.value.is_none() && require_defined {
516 return self.throw_with_ctxt(EvalErrorKind::MissingFieldDef {
517 id: *prev_id,
518 metadata: field.metadata.into_inner(),
519 pos_record: prev_pos_idx,
520 pos_access: PosIdx::NONE,
521 });
522 }
523
524 Ok((field, env))
525 }
526
527 pub fn query(&mut self, v: NickelValue, path: &FieldPath) -> Result<Field, EvalError> {
528 self.query_closure(v.into(), path)
529 }
530
531 /// Generates an error context from this virtual machine. This consumes the callstack, which
532 /// doesn't have to be preserved after an error. However, we have to preserve the position
533 /// table, since the VM could be reset and used for another evaluation round. The latter is
534 /// copied.
535 fn eval_ctxt(&mut self) -> EvalCtxt {
536 EvalCtxt {
537 call_stack: std::mem::take(&mut self.call_stack),
538 pos_table: self.context.pos_table.clone(),
539 }
540 }
541
542 /// Wraps [Self::err_with_ctxt] in the `Err` variant.
543 fn throw_with_ctxt<T>(&mut self, error: EvalErrorKind) -> Result<T, EvalError> {
544 Err(self.err_with_ctxt(error))
545 }
546
547 /// Wraps an evaluation error [crate::error::EvalError] with the current evaluation context
548 /// ([Self::eval_ctxt]) to make a [crate::error::EvalError].
549 fn err_with_ctxt(&mut self, error: EvalErrorKind) -> EvalError {
550 Box::new(EvalErrorData {
551 error,
552 ctxt: self.eval_ctxt(),
553 })
554 }
555
556 /// Same as [VirtualMachine::query], but starts from a closure instead of a term in an empty
557 /// environment.
558 pub fn query_closure(
559 &mut self,
560 closure: Closure,
561 path: &FieldPath,
562 ) -> Result<Field, EvalError> {
563 // extract_field does almost what we want, but for querying, we evaluate the content of the
564 // field as well in order to print a potential default value.
565 let (mut field, env) = self.extract_field_closure(closure, path)?;
566
567 if let Some(value) = field.value.take() {
568 let pos_idx = value.pos_idx();
569
570 let value_with_ctr =
571 RuntimeContract::apply_all(value, field.pending_contracts.iter().cloned(), pos_idx);
572
573 field.value = Some(
574 self.eval_closure(Closure {
575 value: value_with_ctr,
576 env,
577 })?
578 .value,
579 );
580 }
581
582 Ok(field)
583 }
584
585 fn enter_cache_index(
586 &mut self,
587 var: Option<LocIdent>,
588 mut idx: CacheIndex,
589 pos_idx: PosIdx,
590 env: Environment,
591 ) -> Result<Closure, ErrorKind> {
592 // idx may be a 1-counted RC, so we make sure we drop any reference to it from `env`, which
593 // is going to be discarded anyway
594 std::mem::drop(env);
595
596 match self.context.cache.get_update_index(&mut idx) {
597 Ok(Some(idx_upd)) => self.stack.push_update_index(idx_upd),
598 Ok(None) => {}
599 Err(_blackholed_error) => {
600 return Err(Box::new(EvalErrorKind::InfiniteRecursion(
601 self.call_stack.clone(),
602 pos_idx,
603 )));
604 }
605 }
606
607 if let Some(var) = var {
608 self.call_stack.enter_var(var, pos_idx);
609 }
610
611 // If we are fetching a recursive field from the environment that doesn't have
612 // a definition, we complete the error with the additional information of where
613 // it was accessed:
614 let closure = self.context.cache.get(idx);
615
616 let value = match closure.value.content_ref() {
617 ValueContentRef::Term(Term::RuntimeError(data))
618 if matches!(&**data, EvalErrorKind::MissingFieldDef { .. }) =>
619 {
620 let EvalErrorKind::MissingFieldDef {
621 id,
622 metadata,
623 pos_record,
624 pos_access: PosIdx::NONE,
625 } = (**data).clone()
626 else {
627 unreachable!();
628 };
629
630 NickelValue::term(
631 Term::RuntimeError(Box::new(EvalErrorKind::MissingFieldDef {
632 id,
633 metadata,
634 pos_record,
635 pos_access: pos_idx,
636 })),
637 pos_idx,
638 )
639 }
640 _ => closure.value,
641 };
642
643 Ok(Closure { value, ..closure })
644 }
645
646 /// Fetches a closure from the local or the initial environment, or fails with
647 /// [crate::error::EvalError::UnboundIdentifier] with either the variable position, or the
648 /// provided fallback position if the former isn't defined.
649 fn get_var(
650 &self,
651 id: LocIdent,
652 env: &Environment,
653 pos_idx: PosIdx,
654 ) -> Result<CacheIndex, ErrorKind> {
655 get_var(&self.context.pos_table, id, &self.initial_env, env, pos_idx)
656 }
657
658 /// The main loop of evaluation.
659 ///
660 /// Implement the evaluation loop of the core language. The specific implementations of
661 /// primitive operations is delegated to the modules [operation] and [merge].
662 ///
663 /// # Arguments
664 ///
665 /// - `clos`: the closure to evaluate
666 /// - `initial_env`: the initial environment containing the stdlib items.
667 /// Accessible from anywhere in the program.
668 ///
669 /// # Return
670 ///
671 /// Either:
672 /// - an evaluation error
673 /// - the evaluated term with its final environment
674 pub fn eval_closure(&mut self, closure: Closure) -> Result<Closure, EvalError> {
675 self.eval_closure_impl(closure)
676 .map_err(|err| self.err_with_ctxt(*err))
677 }
678
679 /// Actual implementation of [Self::eval_closure]. We use this indirection mostly to use the
680 /// `?` operator on [crate::error::EvalError], and only add the missing context to make it
681 /// an [crate:error::EvalError] once at the end.
682 fn eval_closure_impl(&mut self, mut closure: Closure) -> Result<Closure, ErrorKind> {
683 #[cfg(feature = "metrics")]
684 let start_time = std::time::Instant::now();
685
686 // See https://github.com/rust-lang/rust-clippy/issues/15987.
687 #[allow(clippy::let_and_return)]
688 let result = loop {
689 let Closure { value, mut env } = closure;
690 let pos_idx = value.pos_idx();
691 let has_cont_on_stack = self.stack.is_top_idx() || self.stack.is_top_cont();
692
693 closure = match value.content_ref() {
694 ValueContentRef::Thunk(_) => {
695 // unwrap(): we know that `value` is a thunk in this branch
696 self.enter_cache_index(None, value.try_into_thunk().unwrap(), pos_idx, env)?
697 }
698 ValueContentRef::Term(Term::Sealed(data)) => {
699 let closure = Closure {
700 value: NickelValue::term(Term::Sealed(data.clone()), pos_idx),
701 env: env.clone(),
702 };
703
704 // Update at the original index (the index which holds the result of the op) in
705 // both cases, even if we continue with a seq.
706 //
707 // We do this because we are on a `Sealed` term which is in weak head normal
708 // form, and if we don't, we will be unwrapping a `Sealed` term and assigning
709 // the "unsealed" value to the result of the `Seq` operation. See also:
710 // https://github.com/tweag/nickel/issues/123
711 update_at_indices(&mut self.context.cache, &mut self.stack, &closure);
712
713 // We have to peek the stack to see what operation is coming next and decide
714 // what to do.
715 //
716 // - If it's `unseal`, then we proceed with its evaluation, as `unseal` legitly
717 // operates on sealed terms.
718 // - `seq` is the only primitive operation allowed to see through a sealed
719 // term. Indeed, `seq`-ing doesn't violate parametricity, `seq`-ing shouldn't
720 // be observable (that is, adding seq shouldn't change the semantics and
721 // suddenly make a program blame), and it's useful in practice to seq sealed
722 // terms such as in the implementation of `std.fold_left` to ensure we don't
723 // accumulate thunks in memory.
724 // - If it's anything else, we raise an error right away because the
725 // corresponding polymorphic contract has been violated: a function tried to
726 // use a polymorphic sealed value.
727 match self.stack.peek_sealed_cont() {
728 SealedCont::Unseal => self.continue_op(closure)?,
729 SealedCont::Seq => {
730 // evaluate / `Seq` the inner value.
731 Closure {
732 value: data.inner.clone(),
733 env,
734 }
735 }
736 SealedCont::Other => {
737 // This operation should not be allowed to evaluate a sealed term
738 break Err(Box::new(EvalErrorKind::BlameError {
739 evaluated_arg: data.label.get_evaluated_arg(&self.context.cache),
740 label: (*data.label).clone(),
741 }));
742 }
743 }
744 }
745 ValueContentRef::Term(Term::Var(id)) => {
746 let idx = self.get_var(*id, &env, pos_idx)?;
747 self.enter_cache_index(Some(*id), idx, pos_idx, env)?
748 }
749 ValueContentRef::Term(Term::App(data)) => {
750 self.call_stack.enter_app(&self.context.pos_table, pos_idx);
751
752 self.stack.push_arg(
753 Closure {
754 value: data.arg.clone(),
755 env: env.clone(),
756 },
757 pos_idx,
758 );
759 Closure {
760 value: data.head.clone(),
761 env,
762 }
763 }
764 ValueContentRef::Term(Term::Let(data)) => {
765 let mut indices = Vec::new();
766 let init_env = env.clone();
767
768 for (x, bound) in &data.bindings {
769 let bound_closure = Closure {
770 value: bound.clone(),
771 env: init_env.clone(),
772 };
773
774 let idx = self
775 .context
776 .cache
777 .add(bound_closure, data.attrs.binding_type.clone());
778
779 // Patch the environment with the (x <- closure) binding
780 if data.attrs.rec {
781 indices.push(idx.clone());
782 }
783
784 env.insert(x.ident(), idx);
785 }
786
787 for idx in indices {
788 self.context.cache.patch(idx, |cl| cl.env = env.clone());
789 }
790
791 Closure {
792 value: data.body.clone(),
793 env,
794 }
795 }
796 ValueContentRef::Term(Term::Op1(data)) => {
797 self.stack.push_op1_cont(Op1ContItem {
798 op: data.op.clone(),
799 orig_pos_arg: data.arg.pos_idx(),
800 app_info: PrimopAppInfo {
801 call_stack_size: self.call_stack.len(),
802 pos_idx,
803 },
804 });
805
806 Closure {
807 value: data.arg.clone(),
808 env,
809 }
810 }
811 ValueContentRef::Term(Term::Op2(data)) => {
812 self.stack.push_op2_first_cont(Op2FirstContItem {
813 op: data.op.clone(),
814 app_info: PrimopAppInfo {
815 call_stack_size: self.call_stack.len(),
816 pos_idx,
817 },
818 arg2: Closure {
819 value: data.arg2.clone(),
820 env: env.clone(),
821 },
822 orig_pos_arg1: data.arg1.pos_idx(),
823 });
824
825 Closure {
826 value: data.arg1.clone(),
827 env,
828 }
829 }
830 ValueContentRef::Term(Term::OpN(data)) => {
831 // Arguments are passed as a stack to the operation continuation, so we reverse
832 // the original list.
833 let mut args_iter = data.args.iter();
834 let fst_arg = args_iter.next().ok_or_else(|| {
835 EvalErrorKind::NotEnoughArgs(data.op.arity(), data.op.to_string(), pos_idx)
836 })?;
837
838 let pending: Vec<Closure> = args_iter
839 .rev()
840 .map(|value| Closure {
841 value: value.clone(),
842 env: env.clone(),
843 })
844 .collect();
845
846 self.stack.push_opn_cont(OpNContItem {
847 op: data.op.clone(),
848 app_info: PrimopAppInfo {
849 call_stack_size: self.call_stack.len(),
850 pos_idx,
851 },
852 evaluated: Vec::with_capacity(pending.len() + 1),
853 pending,
854 current_pos_idx: fst_arg.pos_idx(),
855 });
856
857 Closure {
858 value: fst_arg.clone(),
859 env,
860 }
861 }
862 ValueContentRef::Term(Term::StrChunks(chunks)) => {
863 let mut chunks_iter = chunks.iter().cloned();
864 match chunks_iter.next_back() {
865 None => NickelValue::string(NickelString::new(), pos_idx).into(),
866 Some(chunk) => {
867 let (arg, indent) = match chunk {
868 StrChunk::Literal(s) => (NickelValue::string_posless(s), 0),
869 StrChunk::Expr(e, indent) => (e, indent),
870 };
871
872 self.stack.push_str_chunks(chunks_iter);
873 self.stack.push_str_acc(StrAccItem {
874 acc: String::new(),
875 env: env.clone(),
876 // unwrap(): we don't expect an indentation of `u32::MAX` lines...
877 curr_indent: indent.try_into().unwrap(),
878 curr_pos: arg.pos_idx(),
879 });
880
881 // TODO: we should set up the stack properly, instead of allocating an
882 // `op1` term here.
883 Closure {
884 value: NickelValue::term(
885 Term::op1(UnaryOp::ChunksConcat, arg),
886 pos_idx,
887 ),
888 env,
889 }
890 }
891 }
892 }
893 ValueContentRef::Term(Term::Closurize(value)) => {
894 match value.content_ref() {
895 ValueContentRef::Array(Container::Empty)
896 | ValueContentRef::Record(Container::Empty) => value.clone(),
897 ValueContentRef::Array(Container::Alloc(array_data)) => {
898 // This *should* make it unnecessary to call closurize in [operation].
899 // See the comment on the `BinaryOp::ArrayConcat` match arm.
900 let array = array_data
901 .array
902 .iter()
903 .cloned()
904 .map(|t| t.closurize(&mut self.context.cache, env.clone()))
905 .collect();
906
907 let pending_contracts = array_data
908 .pending_contracts
909 .iter()
910 .cloned()
911 .map(|ctr| {
912 RuntimeContract::new(
913 ctr.contract
914 .closurize(&mut self.context.cache, env.clone()),
915 ctr.label,
916 )
917 })
918 .collect();
919
920 NickelValue::array(array, pending_contracts, pos_idx)
921 }
922 ValueContentRef::Record(Container::Alloc(data)) => NickelValue::record(
923 // unwrap(): we treated the empty record case already
924 data.clone().closurize(&mut self.context.cache, env),
925 pos_idx,
926 ),
927 ValueContentRef::EnumVariant(data) => {
928 let EnumVariantData { tag, arg } = data.clone();
929 let arg = arg.map(|arg| arg.closurize(&mut self.context.cache, env));
930 NickelValue::enum_variant(tag, arg, pos_idx)
931 }
932 _ => {
933 // This case is a red flag (it should be unreachable), but
934 // isn't a blocker per se, so we only fail in debug mode.
935 debug_assert!(false, "trying to closurize a non-container value");
936 value.clone()
937 }
938 }
939 // We can use an empty environment for a freshly closurized value
940 .into()
941 }
942 ValueContentRef::Term(Term::RecRecord(data)) => {
943 // We start by closurizing the fields, which might not be if the record is
944 // coming out of the parser.
945
946 // We must avoid re-closurizing a recursive record that is already closurized
947 // (coming from `merge`, for example), as the current representation is broken
948 // if we add a new indirection. This should ideally be encoded in the Rust
949 // type, once we have a different representation for runtime evaluation,
950 // instead of relying on invariants. But for now, we have to live with it.
951 let (mut static_part, dyn_fields) = if !data.closurized {
952 let includes_as_terms: Result<Vec<_>, _> = data
953 .includes
954 .iter()
955 .map(|incl| -> Result<_, ErrorKind> {
956 let field = Field {
957 value: Some(
958 NickelValue::from(self.get_var(
959 incl.ident,
960 &env,
961 PosIdx::NONE,
962 )?)
963 .with_pos(&mut self.context.pos_table, incl.ident.pos),
964 ),
965 metadata: incl.metadata.clone().into(),
966 pending_contracts: Vec::new(),
967 };
968
969 Ok((
970 incl.ident,
971 gen_pending_contracts::with_pending_contracts(
972 &mut self.context.pos_table,
973 field,
974 )?,
975 ))
976 })
977 .collect();
978
979 // We assume that the parser doesn't allow conflicts between field
980 // definitions and includes (the same field is defined in both). This
981 // restriction might be lifted in the future (we would probably merge the
982 // included field and the other definition pieces), but for now it's
983 // simpler this way.
984 let mut record = data.record.clone();
985 record.fields.extend(includes_as_terms?);
986
987 closurize_rec_record(
988 &mut self.context.cache,
989 record,
990 data.dyn_fields.clone(),
991 data.deps.clone(),
992 env,
993 )
994 } else {
995 // In a record that has been already closurized, we expect include
996 // expressions to be evaluated away.
997 debug_assert!(data.includes.is_empty());
998 (data.record.clone(), data.dyn_fields.clone())
999 };
1000
1001 let rec_env = fixpoint::rec_env(
1002 &mut self.context.cache,
1003 static_part.fields.iter(),
1004 pos_idx,
1005 );
1006
1007 for rt in static_part.fields.values_mut() {
1008 fixpoint::patch_field(&mut self.context.cache, rt, &rec_env);
1009 }
1010
1011 // Transform the static part `{stat1 = val1, ..., statn = valn}` and the
1012 // dynamic part `{exp1 = dyn_val1, ..., expm = dyn_valm}` to a sequence of
1013 // extensions
1014 //
1015 // ```
1016 // %record/insert% exp1
1017 // (...
1018 // (%record/insert% expn {stat1 = val1, ..., statn = valn} dyn_valn)
1019 // ...)
1020 // dyn_val1
1021 //
1022 // ```
1023 //
1024 // The `dyn_val` are given access to the recursive environment, but the
1025 // recursive environment only contains the static fields, and not the dynamic
1026 // fields.
1027 let extended = dyn_fields.into_iter().fold(
1028 NickelValue::record(static_part, pos_idx),
1029 |acc, (name_as_term, mut field)| {
1030 let pos_dyn_field = field
1031 .value
1032 .as_ref()
1033 .map(NickelValue::pos_idx)
1034 .unwrap_or_else(|| name_as_term.pos_idx());
1035
1036 fixpoint::patch_field(&mut self.context.cache, &mut field, &rec_env);
1037
1038 let ext_kind = field.extension_kind();
1039 let Field {
1040 metadata,
1041 value,
1042 pending_contracts,
1043 } = field;
1044
1045 let extend = mk_term::op2(
1046 BinaryOp::RecordInsert {
1047 metadata,
1048 pending_contracts,
1049 ext_kind,
1050 op_kind: RecordOpKind::ConsiderAllFields,
1051 },
1052 name_as_term,
1053 acc,
1054 );
1055
1056 match value {
1057 Some(value) => NickelValue::term(
1058 Term::app(extend, value),
1059 pos_dyn_field.to_inherited(),
1060 ),
1061 None => extend,
1062 }
1063 },
1064 );
1065
1066 extended.with_pos_idx(pos_idx).into()
1067 }
1068 ValueContentRef::Term(Term::ResolvedImport(id)) => {
1069 increment!(format!("import:{id:?}"));
1070
1071 if let Some(val) = self.context.import_resolver.get(*id) {
1072 val.into()
1073 } else {
1074 break Err(Box::new(EvalErrorKind::InternalError(
1075 format!("Resolved import not found ({id:?})"),
1076 pos_idx,
1077 )));
1078 }
1079 }
1080 ValueContentRef::Term(Term::Import(Import::Path { path, .. })) => {
1081 break Err(Box::new(EvalErrorKind::InternalError(
1082 format!("Unresolved import ({})", path.to_string_lossy()),
1083 pos_idx,
1084 )));
1085 }
1086 ValueContentRef::Term(Term::Import(Import::Package { id })) => {
1087 return Err(Box::new(EvalErrorKind::InternalError(
1088 format!("Unresolved package import ({id})"),
1089 pos_idx,
1090 )));
1091 }
1092 ValueContentRef::Term(Term::ParseError(parse_error)) => {
1093 break Err(Box::new(EvalErrorKind::ParseError((**parse_error).clone())));
1094 }
1095 ValueContentRef::Term(Term::RuntimeError(error)) => {
1096 break Err(error.clone());
1097 }
1098 // For now, we simply erase annotations at runtime. They aren't accessible anyway
1099 // (as opposed to field metadata) and don't change the operational semantics, as
1100 // long as we generate the corresponding contract application when consuming it.
1101 //
1102 // The situation could change if we want to implement optimizations such as
1103 // avoiding repeated contract application. Annotations could then be a good way of
1104 // remembering which contracts have been applied to a value.
1105 ValueContentRef::Term(Term::Annotated(data)) => {
1106 increment!("contract:free-standing(annotated)");
1107
1108 // We apply the contract coming from the static type annotation separately as
1109 // it is optimized.
1110 let static_contract = data.annot.static_contract(&mut self.context.pos_table);
1111 let contracts = data.annot.pending_contracts(&mut self.context.pos_table)?;
1112 let pos_inner = data.inner.pos_idx();
1113 let inner = data.inner.clone();
1114
1115 let inner_with_static = if let Some(static_ctr) = static_contract {
1116 static_ctr?.apply(inner, pos_inner)
1117 } else {
1118 inner
1119 };
1120
1121 let inner_with_ctr =
1122 RuntimeContract::apply_all(inner_with_static, contracts, pos_inner);
1123
1124 Closure {
1125 value: inner_with_ctr,
1126 env,
1127 }
1128 }
1129 // Function call if there's no continuation on the stack (otherwise, the function
1130 // is just an argument to a primop or to put in the eval cache)
1131 ValueContentRef::Term(Term::Fun(FunData { arg, body })) if !has_cont_on_stack => {
1132 if let Some((idx, pos_app)) = self.stack.pop_arg_as_idx(&mut self.context.cache)
1133 {
1134 self.call_stack.enter_fun(&self.context.pos_table, pos_app);
1135 env.insert(arg.ident(), idx);
1136 Closure {
1137 value: body.clone(),
1138 env,
1139 }
1140 } else {
1141 break Ok(Closure { value, env });
1142 }
1143 }
1144 // At this point, we've evaluated the current term to a weak head normal form.
1145 _ => {
1146 let evaluated = Closure { value, env };
1147
1148 // If there is a cache index update frame on the stack, we proceed with the
1149 // update of the corresponding cached value.
1150 if self.stack.is_top_idx() {
1151 update_at_indices(&mut self.context.cache, &mut self.stack, &evaluated);
1152 evaluated
1153 }
1154 // If there is a primitive operator continuation on the stack, we proceed with
1155 // the continuation.
1156 else if self.stack.is_top_cont() {
1157 self.continue_op(evaluated)?
1158 }
1159 // Otherwise, if the stack is non-empty, this is an ill-formed application (we
1160 // are supposed to evaluate an application, but the left hand side isn't a
1161 // function)
1162 else if let Some((arg, pos_app)) = self.stack.pop_arg(&self.context.cache) {
1163 break Err(Box::new(EvalErrorKind::NotAFunc(
1164 evaluated.value,
1165 arg.value,
1166 pos_app,
1167 )));
1168 }
1169 // Finally, if the stack is empty, it's all good: it just means we are done
1170 // evaluating.
1171 else {
1172 break Ok(evaluated);
1173 }
1174 }
1175 };
1176 };
1177
1178 increment!("runtime:eval", start_time.elapsed().as_millis() as u64);
1179
1180 result
1181 }
1182
1183 /// Evaluate a term, but attempt to continue on errors.
1184 ///
1185 /// This differs from `VirtualMachine::eval_full` in three ways:
1186 /// - We try to accumulate errors instead of bailing out. When recursing into record
1187 /// fields and array elements, we keep evaluating subsequent elements even if one
1188 /// fails.
1189 /// - We only return the accumulated errors; we don't return the eval'ed term.
1190 /// - We support a recursion limit, to limit the number of times we recurse into
1191 /// arrays or records.
1192 pub fn eval_permissive(
1193 &mut self,
1194 value: NickelValue,
1195 recursion_limit: usize,
1196 ) -> Vec<EvalError> {
1197 fn inner<R: ImportResolver, C: Cache>(
1198 this: &mut VirtualMachine<R, C>,
1199 acc: &mut Vec<EvalError>,
1200 value: NickelValue,
1201 recursion_limit: usize,
1202 ) {
1203 if recursion_limit == 0 {
1204 return;
1205 }
1206
1207 let pos_idx = value.pos_idx();
1208 match this.eval(value) {
1209 Err(e) => {
1210 acc.push(e);
1211 this.reset();
1212 }
1213 Ok(val) => match val.content_ref() {
1214 ValueContentRef::Array(Container::Alloc(data)) => {
1215 for elt in data.array.iter() {
1216 // After eval_closure, all the array elements are
1217 // closurized already, so we don't need to do any tracking
1218 // of the env.
1219 let value_with_ctr = RuntimeContract::apply_all(
1220 elt.clone(),
1221 data.pending_contracts.iter().cloned(),
1222 elt.pos_idx(),
1223 );
1224 inner(this, acc, value_with_ctr, recursion_limit.saturating_sub(1));
1225 }
1226 }
1227 ValueContentRef::Record(Container::Alloc(data)) => {
1228 for (id, field) in &data.fields {
1229 if let Some(v) = &field.value {
1230 let value_with_ctr = RuntimeContract::apply_all(
1231 v.clone(),
1232 field.pending_contracts.iter().cloned(),
1233 v.pos_idx(),
1234 );
1235 inner(this, acc, value_with_ctr, recursion_limit.saturating_sub(1));
1236 } else {
1237 acc.push(this.err_with_ctxt(EvalErrorKind::MissingFieldDef {
1238 id: *id,
1239 metadata: field.metadata.clone_inner(),
1240 pos_record: pos_idx,
1241 pos_access: PosIdx::NONE,
1242 }));
1243 }
1244 }
1245 }
1246 ValueContentRef::EnumVariant(EnumVariantData { arg: Some(arg), .. }) => {
1247 inner(this, acc, arg.clone(), recursion_limit.saturating_sub(1));
1248 }
1249 _ => {}
1250 },
1251 }
1252 }
1253
1254 let mut ret = Vec::new();
1255 inner(self, &mut ret, value, recursion_limit);
1256 ret
1257 }
1258}
1259
1260impl<'ctxt, C: Cache> VirtualMachine<'ctxt, ImportCaches, C> {
1261 /// Creates a new VM, and automatically fills the initial environment with
1262 /// `context.import_resolver.mk_initial_env()`.
1263 pub fn new(context: &'ctxt mut VmContext<ImportCaches, C>) -> Self {
1264 let initial_env = context.import_resolver.mk_eval_env(&mut context.cache);
1265
1266 VirtualMachine {
1267 context,
1268 call_stack: Default::default(),
1269 stack: Stack::new(),
1270 initial_env,
1271 }
1272 }
1273}
1274
1275/// An wrapper around [VirtualMachine] which doesn't unwind the VM stack upon destruction.
1276///
1277/// Unwinding ensures all thunks are properly cleaned from their previous state if the VM abort,
1278/// which makes it possible to run subsequent evaluations (whether in the same or another VM
1279/// instance). Since VM are light instances that are built on the spot, the default behavior is to
1280/// unwind upon dropping, to avoid bad surprises (and unwinding is virtually free if the evaluation
1281/// succeeds, since the stack is then empty).
1282///
1283/// However, it could happen that in some workflows, one wishes to avoid the cost of unwinding. In
1284/// that case, use this wrapper instead of [VirtualMachine].
1285pub struct NoUnwindVirtualMachine<'ctxt, R: ImportResolver, C: Cache>(
1286 ManuallyDrop<VirtualMachine<'ctxt, R, C>>,
1287);
1288
1289impl<'ctxt, R: ImportResolver, C: Cache> NoUnwindVirtualMachine<'ctxt, R, C> {
1290 pub fn new(vm: VirtualMachine<'ctxt, R, C>) -> Self {
1291 Self(ManuallyDrop::new(vm))
1292 }
1293}
1294
1295impl<'ctxt, R: ImportResolver, C: Cache> Deref for NoUnwindVirtualMachine<'ctxt, R, C> {
1296 type Target = VirtualMachine<'ctxt, R, C>;
1297
1298 fn deref(&self) -> &Self::Target {
1299 self.0.deref()
1300 }
1301}
1302
1303impl<'ctxt, R: ImportResolver, C: Cache> DerefMut for NoUnwindVirtualMachine<'ctxt, R, C> {
1304 fn deref_mut(&mut self) -> &mut Self::Target {
1305 self.0.deref_mut()
1306 }
1307}
1308
1309/// A closure, which is a value packed with its environment.
1310#[derive(PartialEq, Clone)]
1311pub struct Closure {
1312 pub value: NickelValue,
1313 pub env: Environment,
1314}
1315
1316impl std::fmt::Debug for Closure {
1317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1318 f.debug_struct("Closure")
1319 .field("body", &self.value)
1320 .finish_non_exhaustive()
1321 }
1322}
1323
1324impl From<NickelValue> for Closure {
1325 fn from(value: NickelValue) -> Self {
1326 Closure {
1327 value,
1328 env: Environment::new(),
1329 }
1330 }
1331}
1332
1333#[allow(type_alias_bounds)] // TODO: Look into this warning.
1334pub type Environment = GenericEnvironment<Ident, CacheIndex>;
1335
1336/// Raised when trying to build an environment from a term which is not a record.
1337#[derive(Clone, Debug)]
1338pub enum EnvBuildError {
1339 NotARecord(NickelValue),
1340}
1341
1342/// Add the bindings of a record to an environment. Ignore the fields defined by interpolation as
1343/// well, fields without definition and `include` expressions.
1344pub fn env_add_record<C: Cache>(
1345 cache: &mut C,
1346 env: &mut Environment,
1347 closure: Closure,
1348) -> Result<(), EnvBuildError> {
1349 if closure.value.is_inline_empty_record() {
1350 return Ok(());
1351 }
1352
1353 // We unwrap potential `Closurize` sprinkled in unevaluated terms, which is typically the case
1354 // for the stdlib.
1355 let value = if let Some(Term::Closurize(inner)) = closure.value.as_term() {
1356 inner
1357 } else {
1358 &closure.value
1359 };
1360
1361 let record = match value.content_ref() {
1362 ValueContentRef::Record(Container::Empty) => return Ok(()),
1363 ValueContentRef::Record(Container::Alloc(record)) => record,
1364 ValueContentRef::Term(Term::RecRecord(data)) => &data.record,
1365 _ => return Err(EnvBuildError::NotARecord(closure.value)),
1366 };
1367
1368 let ext = record.fields.iter().filter_map(|(id, field)| {
1369 field.value.as_ref().map(|value| {
1370 (
1371 id.ident(),
1372 cache.add(
1373 Closure {
1374 value: value.clone(),
1375 env: closure.env.clone(),
1376 },
1377 BindingType::Normal,
1378 ),
1379 )
1380 })
1381 });
1382
1383 env.extend(ext);
1384 Ok(())
1385}
1386
1387/// Bind a closure in an environment.
1388pub fn env_add<C: Cache>(
1389 cache: &mut C,
1390 env: &mut Environment,
1391 id: LocIdent,
1392 value: NickelValue,
1393 local_env: Environment,
1394) {
1395 let closure = Closure {
1396 value,
1397 env: local_env,
1398 };
1399 env.insert(id.ident(), cache.add(closure, BindingType::Normal));
1400}
1401
1402/// Pop and update all the indices on the top of the stack with the given closure.
1403fn update_at_indices<C: Cache>(cache: &mut C, stack: &mut Stack<C>, closure: &Closure) {
1404 while let Some(idx) = stack.pop_update_index() {
1405 cache.update(closure.clone(), idx);
1406 }
1407}
1408
1409/// Free-standing implementation of [VirtualMachine::get_var].
1410fn get_var(
1411 pos_table: &PosTable,
1412 id: LocIdent,
1413 initial_env: &Environment,
1414 env: &Environment,
1415 pos_idx: PosIdx,
1416) -> Result<CacheIndex, ErrorKind> {
1417 env.get(&id.ident())
1418 .or_else(|| initial_env.get(&id.ident()))
1419 .cloned()
1420 .ok_or(Box::new(EvalErrorKind::UnboundIdentifier(
1421 id,
1422 id.pos.or(pos_table.get(pos_idx)),
1423 )))
1424}
1425
1426/// Recursively substitute each variable occurrence of a term for its value in the environment.
1427pub fn subst<C: Cache>(
1428 pos_table: &PosTable,
1429 cache: &C,
1430 value: NickelValue,
1431 initial_env: &Environment,
1432 env: &Environment,
1433) -> NickelValue {
1434 use value::lens::TermContent;
1435
1436 let pos_idx = value.pos_idx();
1437
1438 match value.content() {
1439 lens @ (ValueContent::Null(_)
1440 | ValueContent::Bool(_)
1441 | ValueContent::Number(_)
1442 | ValueContent::String(_)
1443 | ValueContent::CustomContract(_)
1444 | ValueContent::Label(_)
1445 // We could recurse in types, because types can contain terms which would then be subject
1446 // to substitution. Not recursing should be fine, though, because a type in term position
1447 // turns into a contract, and we don't substitute inside contracts either currently.
1448 | ValueContent::Type(_)
1449 | ValueContent::ForeignId(_)
1450 | ValueContent::SealingKey(_)) => lens.restore(),
1451 ValueContent::Thunk(lens) => {
1452 let closure = cache.get(lens.take());
1453 subst(pos_table, cache, closure.value, initial_env, &closure.env)
1454 }
1455 ValueContent::Record(lens) => {
1456 let Container::Alloc(record) = lens.take() else {
1457 return NickelValue::empty_record().with_pos_idx(pos_idx);
1458 };
1459
1460 let record = record.map_defined_values(|_, value| subst(pos_table, cache, value, initial_env, env));
1461
1462 //unwrap(): we didn't change the size of the record, so whether it was inline or not,
1463 //its position index should be of the according type
1464 NickelValue::record(record, pos_idx)
1465 }
1466 ValueContent::Array(lens) => {
1467 let Container::Alloc(array_data) = lens.take() else {
1468 return NickelValue::empty_array().with_pos_idx(pos_idx);
1469 };
1470
1471 let array = array_data.array.into_iter()
1472 .map(|t| subst(pos_table, cache, t, initial_env, env))
1473 .collect();
1474
1475 NickelValue::array(array, array_data.pending_contracts, pos_idx)
1476 }
1477 ValueContent::EnumVariant(lens) => {
1478 let EnumVariantData { tag, arg } = lens.take();
1479 let arg = arg.map(|arg| subst(pos_table, cache, arg, initial_env, env));
1480
1481 NickelValue::enum_variant(tag, arg, pos_idx)
1482 }
1483 ValueContent::Term(content) => {
1484 match content {
1485 TermContent::Var(lens) => {
1486 let id = lens.take();
1487
1488 get_var(pos_table, id, env, initial_env, PosIdx::NONE)
1489 .map(|idx| {
1490 let closure = cache.get(idx);
1491 subst(pos_table, cache, closure.value, initial_env, &closure.env)
1492 })
1493 .unwrap_or_else(|_| NickelValue::term(Term::Var(id), pos_idx))
1494 }
1495 lens @ (TermContent::ParseError(_) | TermContent::RuntimeError(_)
1496 // Do not substitute under lambdas: mutually recursive function could cause an infinite
1497 // loop. Although avoidable, this requires some care and is not currently needed.
1498 | TermContent::Fun(..)
1499 | TermContent::Import(_)
1500 | TermContent::ResolvedImport(_)) => lens.restore(),
1501 TermContent::Let(lens) => {
1502 let mut data = lens.take();
1503
1504 for (_key, val) in &mut data.bindings {
1505 let prev = std::mem::take(val);
1506 *val = subst(pos_table, cache, prev, initial_env, env);
1507 }
1508 data.body = subst(pos_table, cache, data.body, initial_env, env);
1509
1510 NickelValue::term(Term::Let(data), pos_idx)
1511 }
1512 TermContent::App(lens) => {
1513 let mut data = lens.take();
1514 data.head = subst(pos_table, cache, data.head, initial_env, env);
1515 data.arg = subst(pos_table, cache, data.arg, initial_env, env);
1516
1517 NickelValue::term(Term::App(data), pos_idx)
1518 }
1519 TermContent::Op1(lens) => {
1520 let mut data = lens.take();
1521 data.arg = subst(pos_table, cache, data.arg, initial_env, env);
1522
1523 NickelValue::term(Term::Op1(data), pos_idx)
1524 }
1525 TermContent::Op2(lens) => {
1526 let mut data = lens.take();
1527 data.arg1 = subst(pos_table, cache, data.arg1, initial_env, env);
1528 data.arg2 = subst(pos_table, cache, data.arg2, initial_env, env);
1529
1530 NickelValue::term(Term::Op2(data), pos_idx)
1531 }
1532 TermContent::OpN(lens) => {
1533 let mut data = lens.take();
1534 data.args = data.args
1535 .into_iter()
1536 .map(|t| subst(pos_table, cache, t, initial_env, env))
1537 .collect();
1538
1539 NickelValue::term(Term::OpN(data), pos_idx)
1540 }
1541 TermContent::Sealed(lens) => {
1542 let mut data = lens.take();
1543 data.inner = subst(pos_table, cache, data.inner, initial_env, env);
1544 NickelValue::term(Term::Sealed(data), pos_idx)
1545 }
1546 // Currently, we downright ignore `include` expressions. However, one could argue that
1547 // substituting `foo` for `bar` in `{include foo}` should result in `{foo = bar}`.
1548 TermContent::RecRecord(lens) => {
1549 let mut data = lens.take();
1550
1551 data.record = data.record
1552 .map_defined_values(|_, value| subst(pos_table, cache, value, initial_env, env));
1553
1554 data.dyn_fields = data.dyn_fields
1555 .into_iter()
1556 .map(|(id_t, field)| {
1557 (
1558 subst(pos_table, cache, id_t, initial_env, env),
1559 field.map_value(|v| subst(pos_table, cache, v, initial_env, env)),
1560 )
1561 })
1562 .collect();
1563
1564 NickelValue::term(Term::RecRecord(data), pos_idx)
1565 }
1566 TermContent::StrChunks(lens) => {
1567 let chunks = lens.take()
1568 .into_iter()
1569 .map(|chunk| match chunk {
1570 chunk @ StrChunk::Literal(_) => chunk,
1571 StrChunk::Expr(t, indent) => StrChunk::Expr(
1572 subst(pos_table, cache, t, initial_env, env),
1573 indent,
1574 ),
1575 })
1576 .collect();
1577
1578 NickelValue::term(Term::StrChunks(chunks), pos_idx)
1579 }
1580 TermContent::Annotated(lens) => {
1581 let mut data = lens.take();
1582 data.inner = subst(pos_table, cache, data.inner, initial_env, env);
1583 // Currently, there is no interest in replacing variables inside contracts, thus we
1584 // limit the work of `subst`.
1585 NickelValue::term(Term::Annotated(data), pos_idx)
1586 }
1587 TermContent::Closurize(lens) => {
1588 NickelValue::term(Term::Closurize(subst(pos_table, cache, lens.take(), initial_env, env)), pos_idx)
1589 }
1590 }
1591 }
1592 }
1593}
1594
1595#[cfg(test)]
1596mod tests;