snix_eval/vm/
mod.rs

1//! This module implements the abstract/virtual machine that runs Snix
2//! bytecode.
3//!
4//! The operation of the VM is facilitated by the [`Frame`] type,
5//! which controls the current execution state of the VM and is
6//! processed within the VM's operating loop.
7//!
8//! A [`VM`] is used by instantiating it with an initial [`Frame`],
9//! then triggering its execution and waiting for the VM to return or
10//! yield an error.
11
12pub mod generators;
13mod macros;
14
15use bstr::{BString, ByteSlice, ByteVec};
16use codemap::Span;
17use rustc_hash::FxHashMap;
18use serde_json::json;
19use std::{
20    cmp::Ordering,
21    ffi::OsStr,
22    path::{Path, PathBuf},
23    rc::Rc,
24};
25
26use crate::{
27    NixString, SourceCode, arithmetic_op,
28    chunk::Chunk,
29    cmp_op,
30    compiler::GlobalsMap,
31    errors::{CatchableErrorKind, Error, ErrorKind, EvalResult},
32    io::EvalIO,
33    lifted_pop,
34    nix_search_path::NixSearchPath,
35    observer::{OptionalRuntimeObserver, RuntimeObserver},
36    opcode::{CodeIdx, Op, Position, UpvalueIdx},
37    upvalues::{UpvalueData, Upvalues},
38    value::{
39        Builtin, BuiltinResult, Closure, CoercionKind, Lambda, NixAttrs, NixContext, NixList,
40        PointerEquality, Thunk, Value, canon_path,
41    },
42    vm::generators::GenCo,
43    warnings::{EvalWarning, WarningKind},
44};
45
46use generators::{Generator, GeneratorState, call_functor};
47
48use self::generators::{VMRequest, VMResponse};
49
50/// Internal helper trait for taking a span from a variety of types, to make use
51/// of `WithSpan` (defined below) more ergonomic at call sites.
52trait GetSpan {
53    fn get_span(self) -> Span;
54}
55
56impl<IO> GetSpan for &VM<'_, IO> {
57    fn get_span(self) -> Span {
58        self.reasonable_span
59    }
60}
61
62impl GetSpan for &CallFrame {
63    fn get_span(self) -> Span {
64        self.current_span()
65    }
66}
67
68impl GetSpan for &Span {
69    fn get_span(self) -> Span {
70        *self
71    }
72}
73
74impl GetSpan for Span {
75    fn get_span(self) -> Span {
76        self
77    }
78}
79
80/// Internal helper trait for ergonomically converting from a `Result<T,
81/// ErrorKind>` to a `Result<T, Error>` using the current span of a call frame,
82/// and chaining the VM's frame stack around it for printing a cause chain.
83trait WithSpan<T, S: GetSpan, IO> {
84    fn with_span(self, top_span: S, vm: &VM<IO>) -> Result<T, Error>;
85}
86
87impl<T, S: GetSpan, IO> WithSpan<T, S, IO> for Result<T, ErrorKind> {
88    fn with_span(self, top_span: S, vm: &VM<IO>) -> Result<T, Error> {
89        match self {
90            Ok(something) => Ok(something),
91            Err(kind) => {
92                let mut error = Error::new(kind, top_span.get_span(), vm.source.clone());
93
94                // Wrap the top-level error in chaining errors for each element
95                // of the frame stack.
96                for frame in vm.frames.iter().rev() {
97                    match frame {
98                        Frame::CallFrame { span, .. } => {
99                            error = Error::new(
100                                ErrorKind::BytecodeError(Box::new(error)),
101                                *span,
102                                vm.source.clone(),
103                            );
104                        }
105                        Frame::Generator { name, span, .. } => {
106                            error = Error::new(
107                                ErrorKind::NativeError {
108                                    err: Box::new(error),
109                                    gen_type: name,
110                                },
111                                *span,
112                                vm.source.clone(),
113                            );
114                        }
115                    }
116                }
117
118                Err(error)
119            }
120        }
121    }
122}
123
124struct CallFrame {
125    /// The lambda currently being executed.
126    lambda: Rc<Lambda>,
127
128    /// Optional captured upvalues of this frame (if a thunk or
129    /// closure if being evaluated).
130    upvalues: Rc<Upvalues>,
131
132    /// Instruction pointer to the instruction currently being
133    /// executed.
134    ip: CodeIdx,
135
136    /// Stack offset, i.e. the frames "view" into the VM's full stack.
137    stack_offset: usize,
138}
139
140impl CallFrame {
141    /// Retrieve an upvalue from this frame at the given index.
142    fn upvalue(&self, idx: UpvalueIdx) -> &Value {
143        &self.upvalues[idx]
144    }
145
146    /// Borrow the chunk of this frame's lambda.
147    fn chunk(&self) -> &Chunk {
148        &self.lambda.chunk
149    }
150
151    /// Increment this frame's instruction pointer and return the operation that
152    /// the pointer moved past.
153    fn inc_ip(&mut self) -> Op {
154        debug_assert!(
155            self.ip.0 < self.chunk().code.len(),
156            "out of bounds code at IP {} in {:p}",
157            self.ip.0,
158            self.lambda
159        );
160
161        let op = self.chunk().code[self.ip.0];
162        self.ip += 1;
163        op.into()
164    }
165
166    /// Read a varint-encoded operand and return it. The frame pointer is
167    /// incremented internally.
168    fn read_uvarint(&mut self) -> u64 {
169        let (arg, size) = self.chunk().read_uvarint(self.ip.0);
170        self.ip += size;
171        arg
172    }
173
174    /// Read a fixed-size u16 and increment the frame pointer.
175    fn read_u16(&mut self) -> u16 {
176        let arg = self.chunk().read_u16(self.ip.0);
177        self.ip += 2;
178        arg
179    }
180
181    /// Construct an error result from the given ErrorKind and the source span
182    /// of the current instruction.
183    pub fn error<T, IO>(&self, vm: &VM<IO>, kind: ErrorKind) -> Result<T, Error> {
184        Err(kind).with_span(self, vm)
185    }
186
187    /// Returns the current span. This is potentially expensive and should only
188    /// be used when actually constructing an error or warning.
189    pub fn current_span(&self) -> Span {
190        self.chunk().get_span(self.ip - 1)
191    }
192}
193
194/// A frame represents an execution state of the VM. The VM has a stack of
195/// frames representing the nesting of execution inside of the VM, and operates
196/// on the frame at the top.
197///
198/// When a frame has been fully executed, it is removed from the VM's frame
199/// stack and expected to leave a result [`Value`] on the top of the stack.
200enum Frame {
201    /// CallFrame represents the execution of Snix bytecode within a thunk,
202    /// function or closure.
203    CallFrame {
204        /// The call frame itself, separated out into another type to pass it
205        /// around easily.
206        call_frame: CallFrame,
207
208        /// Span from which the call frame was launched.
209        span: Span,
210    },
211
212    /// Generator represents a frame that can yield further
213    /// instructions to the VM while its execution is being driven.
214    ///
215    /// A generator is essentially an asynchronous function that can
216    /// be suspended while waiting for the VM to do something (e.g.
217    /// thunk forcing), and resume at the same point.
218    Generator {
219        /// human-readable description of the generator,
220        name: &'static str,
221
222        /// Span from which the generator was launched.
223        span: Span,
224
225        state: GeneratorState,
226
227        /// Generator itself, which can be resumed with `.resume()`.
228        generator: Generator,
229    },
230}
231
232impl Frame {
233    pub fn span(&self) -> Span {
234        match self {
235            Frame::CallFrame { span, .. } | Frame::Generator { span, .. } => *span,
236        }
237    }
238}
239
240#[derive(Default)]
241/// The `ImportCache` holds the `Value` resulting from `import`ing a certain
242/// file, so that the same file doesn't need to be re-evaluated multiple times.
243/// Currently the real path of the imported file (determined using
244/// [`std::fs::canonicalize()`], not to be confused with our
245/// [`crate::value::canon_path()`]) is used to identify the file,
246/// just like C++ Nix does.
247///
248/// Errors while determining the real path are currently just ignored, since we
249/// pass around some fake paths like `/__corepkgs__/fetchurl.nix`.
250///
251/// In the future, we could use something more sophisticated, like file hashes.
252/// However, a consideration is that the eval cache is observable via impurities
253/// like pointer equality and `builtins.trace`.
254struct ImportCache(FxHashMap<PathBuf, Value>);
255
256impl ImportCache {
257    fn get(&self, path: impl AsRef<Path>) -> Option<&Value> {
258        let path = path.as_ref();
259        let path = match std::fs::canonicalize(path).map_err(ErrorKind::from) {
260            Ok(path) => path,
261            Err(_) => path.to_owned(),
262        };
263        self.0.get(&path)
264    }
265
266    fn insert(&mut self, path: PathBuf, value: Value) -> Option<Value> {
267        self.0.insert(
268            match std::fs::canonicalize(path.as_path()).map_err(ErrorKind::from) {
269                Ok(path) => path,
270                Err(_) => path,
271            },
272            value,
273        )
274    }
275}
276
277/// Path import cache, mapping absolute file paths to paths in the store.
278#[derive(Default)]
279struct PathImportCache(FxHashMap<PathBuf, PathBuf>);
280impl PathImportCache {
281    fn get(&self, path: impl AsRef<Path>) -> Option<PathBuf> {
282        self.0.get(path.as_ref()).cloned()
283    }
284
285    fn insert(&mut self, path: PathBuf, imported_path: PathBuf) -> Option<PathBuf> {
286        self.0.insert(path, imported_path)
287    }
288}
289
290struct VM<'o, IO> {
291    /// VM's frame stack, representing the execution contexts the VM is working
292    /// through. Elements are usually pushed when functions are called, or
293    /// thunks are being forced.
294    frames: Vec<Frame>,
295
296    /// The VM's top-level value stack. Within this stack, each code-executing
297    /// frame holds a "view" of the stack representing the slice of the
298    /// top-level stack that is relevant to its operation. This is done to avoid
299    /// allocating a new `Vec` for each frame's stack.
300    pub(crate) stack: Vec<Value>,
301
302    /// Stack indices (absolute indexes into `stack`) of attribute
303    /// sets from which variables should be dynamically resolved
304    /// (`with`).
305    with_stack: Vec<usize>,
306
307    /// Runtime warnings collected during evaluation.
308    warnings: Vec<EvalWarning>,
309
310    /// Import cache, mapping absolute file paths to the value that
311    /// they compile to. Note that this reuses thunks, too!
312    // TODO: should probably be based on a file hash
313    pub import_cache: ImportCache,
314
315    /// Path import cache, mapping absolute file paths to paths in the store.
316    // TODO: should probably be based on a file hash
317    path_import_cache: PathImportCache,
318
319    /// Data structure holding all source code evaluated in this VM,
320    /// used for pretty error reporting.
321    source: SourceCode,
322
323    /// Parsed Nix search path, which is used to resolve `<...>`
324    /// references.
325    nix_search_path: NixSearchPath,
326
327    /// Implementation of I/O operations used for impure builtins and
328    /// features like `import`.
329    io_handle: IO,
330
331    /// Runtime observer which can print traces of runtime operations.
332    observer: OptionalRuntimeObserver<'o>,
333
334    /// Strong reference to the globals, guaranteeing that they are
335    /// kept alive for the duration of evaluation.
336    ///
337    /// This is important because recursive builtins (specifically
338    /// `import`) hold a weak reference to the builtins, while the
339    /// original strong reference is held by the compiler which does
340    /// not exist anymore at runtime.
341    #[allow(dead_code)]
342    globals: Rc<GlobalsMap>,
343
344    /// A reasonably applicable span that can be used for errors in each
345    /// execution situation.
346    ///
347    /// The VM should update this whenever control flow changes take place (i.e.
348    /// entering or exiting a frame to yield control somewhere).
349    reasonable_span: Span,
350
351    /// This field is responsible for handling `builtins.tryEval`. When that
352    /// builtin is encountered, it sends a special message to the VM which
353    /// pushes the frame index that requested to be informed of catchable
354    /// errors in this field.
355    ///
356    /// The frame stack is then laid out like this:
357    ///
358    /// ```notrust
359    /// ┌──┬──────────────────────────┐
360    /// │ 0│ `Result`-producing frame │
361    /// ├──┼──────────────────────────┤
362    /// │-1│ `builtins.tryEval` frame │
363    /// ├──┼──────────────────────────┤
364    /// │..│ ... other frames ...     │
365    /// └──┴──────────────────────────┘
366    /// ```
367    ///
368    /// Control is yielded to the outer VM loop, which evaluates the next frame
369    /// and returns the result itself to the `builtins.tryEval` frame.
370    try_eval_frames: Vec<usize>,
371}
372
373impl<'o, IO> VM<'o, IO>
374where
375    IO: AsRef<dyn EvalIO> + 'static,
376{
377    pub fn new(
378        nix_search_path: NixSearchPath,
379        io_handle: IO,
380        observer: OptionalRuntimeObserver<'o>,
381        source: SourceCode,
382        globals: Rc<GlobalsMap>,
383        reasonable_span: Span,
384    ) -> Self {
385        Self {
386            nix_search_path,
387            io_handle,
388            observer,
389            globals,
390            reasonable_span,
391            source,
392            frames: vec![],
393            stack: vec![],
394            with_stack: vec![],
395            warnings: vec![],
396            import_cache: Default::default(),
397            path_import_cache: Default::default(),
398            try_eval_frames: vec![],
399        }
400    }
401
402    /// Push a call frame onto the frame stack.
403    fn push_call_frame(&mut self, span: Span, call_frame: CallFrame) {
404        self.frames.push(Frame::CallFrame { span, call_frame })
405    }
406
407    /// Run the VM's primary (outer) execution loop, continuing execution based
408    /// on the current frame at the top of the frame stack.
409    fn execute(mut self) -> EvalResult<RuntimeResult> {
410        while let Some(frame) = self.frames.pop() {
411            self.reasonable_span = frame.span();
412            let frame_id = self.frames.len();
413
414            match frame {
415                Frame::CallFrame { call_frame, span } => {
416                    self.observer
417                        .observe_enter_call_frame(0, &call_frame.lambda, frame_id);
418
419                    match self.execute_bytecode(span, call_frame) {
420                        Ok(true) => {
421                            self.observer.observe_exit_call_frame(frame_id, &self.stack);
422                        }
423                        Ok(false) => {
424                            self.observer
425                                .observe_suspend_call_frame(frame_id, &self.stack);
426                        }
427                        Err(err) => return Err(err),
428                    };
429                }
430
431                // Handle generator frames, which can request thunk forcing
432                // during their execution.
433                Frame::Generator {
434                    name,
435                    span,
436                    state,
437                    generator,
438                } => {
439                    self.observer
440                        .observe_enter_generator(frame_id, name, &self.stack);
441
442                    match self.run_generator(name, span, frame_id, state, generator, None) {
443                        Ok(true) => {
444                            self.observer
445                                .observe_exit_generator(frame_id, name, &self.stack)
446                        }
447                        Ok(false) => {
448                            self.observer
449                                .observe_suspend_generator(frame_id, name, &self.stack)
450                        }
451
452                        Err(err) => return Err(err),
453                    };
454                }
455            }
456        }
457
458        // Once no more frames are present, return the stack's top value as the
459        // result.
460        let value = self
461            .stack
462            .pop()
463            .expect("Snix bug: runtime stack empty after execution");
464        Ok(RuntimeResult {
465            value,
466            warnings: self.warnings,
467        })
468    }
469
470    /// Run the VM's inner execution loop, processing Snix bytecode from a
471    /// chunk. This function returns if:
472    ///
473    /// 1. The code has run to the end, and has left a value on the top of the
474    ///    stack. In this case, the frame is not returned to the frame stack.
475    ///
476    /// 2. The code encounters a generator, in which case the frame in its
477    ///    current state is pushed back on the stack, and the generator is left
478    ///    on top of it for the outer loop to execute.
479    ///
480    /// 3. An error is encountered.
481    ///
482    /// This function *must* ensure that it leaves the frame stack in the
483    /// correct order, especially when re-enqueuing a frame to execute.
484    ///
485    /// The return value indicates whether the bytecode has been executed to
486    /// completion, or whether it has been suspended in favour of a generator.
487    fn execute_bytecode(&mut self, span: Span, mut frame: CallFrame) -> EvalResult<bool> {
488        loop {
489            let op = frame.inc_ip();
490            self.observer.observe_execute_op(frame.ip, &op, &self.stack);
491
492            match op {
493                Op::ThunkSuspended | Op::ThunkClosure => {
494                    let idx = frame.read_uvarint() as usize;
495
496                    let blueprint = match &frame.chunk().constants[idx] {
497                        Value::Blueprint(lambda) => lambda.clone(),
498                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
499                    };
500
501                    let upvalues = self.populate_upvalues(&mut frame)?;
502                    debug_assert!(
503                        upvalues.len() == blueprint.upvalue_count,
504                        "TODO: new upvalue count not correct",
505                    );
506
507                    let thunk = if op == Op::ThunkClosure {
508                        debug_assert!(
509                            ((upvalues.len() > 0) || (upvalues.with_stack_len() > 0)),
510                            "OpThunkClosure should not be called for plain lambdas",
511                        );
512                        Thunk::new_closure(blueprint)
513                    } else {
514                        Thunk::new_suspended(blueprint, frame.current_span())
515                    };
516
517                    self.stack.push(Value::Thunk(thunk.clone()));
518
519                    // From this point on we internally mutate the
520                    // upvalues. The closure (if `is_closure`) is
521                    // already in its stack slot, which means that it
522                    // can capture itself as an upvalue for
523                    // self-recursion.
524                    *thunk.upvalues_mut() = upvalues;
525                }
526
527                Op::Force => {
528                    if let Some(Value::Thunk(_)) = self.stack.last() {
529                        let thunk = match self.stack_pop() {
530                            Value::Thunk(t) => t,
531                            _ => unreachable!(),
532                        };
533
534                        let gen_span = frame.current_span();
535
536                        self.push_call_frame(span, frame);
537                        self.enqueue_generator("force", gen_span, |co| {
538                            Thunk::force(thunk, co, gen_span)
539                        });
540
541                        return Ok(false);
542                    }
543                }
544
545                Op::GetUpvalue => {
546                    let idx = UpvalueIdx(frame.read_uvarint() as usize);
547                    let value = frame.upvalue(idx).clone();
548                    self.stack.push(value);
549                }
550
551                // Discard the current frame.
552                Op::Return => {
553                    // TODO(amjoseph): I think this should assert `==` rather
554                    // than `<=` but it fails with the stricter condition.
555                    debug_assert!(self.stack.len() - 1 <= frame.stack_offset);
556                    return Ok(true);
557                }
558
559                Op::Constant => {
560                    let idx = frame.read_uvarint() as usize;
561
562                    debug_assert!(
563                        idx < frame.chunk().constants.len(),
564                        "out of bounds constant at IP {} in {:p}",
565                        frame.ip.0,
566                        frame.lambda
567                    );
568
569                    let c = frame.chunk().constants[idx].clone();
570                    self.stack.push(c);
571                }
572
573                Op::Call => {
574                    let callable = self.stack_pop();
575                    self.call_value(frame.current_span(), Some((span, frame)), callable)?;
576
577                    // exit this loop and let the outer loop enter the new call
578                    return Ok(true);
579                }
580
581                // Remove the given number of elements from the stack,
582                // but retain the top value.
583                Op::CloseScope => {
584                    let count = frame.read_uvarint() as usize;
585                    // Immediately move the top value into the right
586                    // position.
587                    let target_idx = self.stack.len() - 1 - count;
588                    self.stack[target_idx] = self.stack_pop();
589
590                    // Then drop the remaining values.
591                    for _ in 0..(count - 1) {
592                        self.stack.pop();
593                    }
594                }
595
596                Op::Closure => {
597                    let idx = frame.read_uvarint() as usize;
598                    let blueprint = match &frame.chunk().constants[idx] {
599                        Value::Blueprint(lambda) => lambda.clone(),
600                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
601                    };
602
603                    let upvalues = self.populate_upvalues(&mut frame)?;
604                    let upvalue_count = upvalues.len();
605                    debug_assert!(
606                        upvalue_count == blueprint.upvalue_count,
607                        "TODO: new upvalue count not correct in closure",
608                    );
609
610                    debug_assert!(
611                        (upvalue_count > 0 || upvalues.with_stack_len() > 0),
612                        "OpClosure should not be called for plain lambdas"
613                    );
614
615                    self.stack
616                        .push(Value::Closure(Rc::new(Closure::new_with_upvalues(
617                            Rc::new(upvalues),
618                            blueprint,
619                        ))));
620                }
621
622                Op::AttrsSelect => lifted_pop! {
623                    self(key, attrs) => {
624                        let key = key.to_str().with_span(&frame, self)?;
625                        let attrs = attrs.to_attrs().with_span(&frame, self)?;
626
627                        match attrs.select(&key) {
628                            Some(value) => self.stack.push(value.clone()),
629
630                            None => {
631                                return frame.error(
632                                    self,
633                                    ErrorKind::AttributeNotFound {
634                                        name: key.to_str_lossy().into_owned()
635                                    },
636                                );
637                            }
638                        }
639                    }
640                },
641
642                Op::JumpIfFalse => {
643                    let offset = frame.read_u16() as usize;
644                    debug_assert!(offset != 0);
645                    if !self.stack_peek(0).as_bool().with_span(&frame, self)? {
646                        frame.ip += offset;
647                    }
648                }
649
650                Op::JumpIfCatchable => {
651                    let offset = frame.read_u16() as usize;
652                    debug_assert!(offset != 0);
653                    if self.stack_peek(0).is_catchable() {
654                        frame.ip += offset;
655                    }
656                }
657
658                Op::JumpIfNoFinaliseRequest => {
659                    let offset = frame.read_u16() as usize;
660                    debug_assert!(offset != 0);
661                    match self.stack_peek(0) {
662                        Value::FinaliseRequest(finalise) => {
663                            if !finalise {
664                                frame.ip += offset;
665                            }
666                        }
667                        val => panic!(
668                            "Snix bug: OpJumIfNoFinaliseRequest: expected FinaliseRequest, but got {}",
669                            val.type_of()
670                        ),
671                    }
672                }
673
674                Op::Pop => {
675                    self.stack.pop();
676                }
677
678                Op::AttrsTrySelect => {
679                    let key = self.stack_pop().to_str().with_span(&frame, self)?;
680                    let value = match self.stack_pop() {
681                        Value::Attrs(attrs) => match attrs.select(&key) {
682                            Some(value) => value.clone(),
683                            None => Value::AttrNotFound,
684                        },
685
686                        _ => Value::AttrNotFound,
687                    };
688
689                    self.stack.push(value);
690                }
691
692                Op::GetLocal => {
693                    let local_idx = frame.read_uvarint() as usize;
694                    let idx = frame.stack_offset + local_idx;
695                    self.stack.push(self.stack[idx].clone());
696                }
697
698                Op::JumpIfNotFound => {
699                    let offset = frame.read_u16() as usize;
700                    debug_assert!(offset != 0);
701                    if matches!(self.stack_peek(0), Value::AttrNotFound) {
702                        self.stack_pop();
703                        frame.ip += offset;
704                    }
705                }
706
707                Op::Jump => {
708                    let offset = frame.read_u16() as usize;
709                    debug_assert!(offset != 0);
710                    frame.ip += offset;
711                }
712
713                Op::Equal => lifted_pop! {
714                    self(b, a) => {
715                        let gen_span = frame.current_span();
716                        self.push_call_frame(span, frame);
717                        self.enqueue_generator("nix_eq", gen_span, |co| {
718                            a.nix_eq_owned_genco(b, co, PointerEquality::ForbidAll, gen_span)
719                        });
720                        return Ok(false);
721                    }
722                },
723
724                // These assertion operations error out if the stack
725                // top is not of the expected type. This is necessary
726                // to implement some specific behaviours of Nix
727                // exactly.
728                Op::AssertBool => {
729                    let val = self.stack_peek(0);
730                    // TODO(edef): propagate this into is_bool, since bottom values *are* values of any type
731                    if !val.is_catchable() && !val.is_bool() {
732                        return frame.error(
733                            self,
734                            ErrorKind::TypeError {
735                                expected: "bool",
736                                actual: val.type_of(),
737                            },
738                        );
739                    }
740                }
741
742                Op::AssertAttrs => {
743                    let val = self.stack_peek(0);
744                    // TODO(edef): propagate this into is_attrs, since bottom values *are* values of any type
745                    if !val.is_catchable() && !val.is_attrs() {
746                        return frame.error(
747                            self,
748                            ErrorKind::TypeError {
749                                expected: "set",
750                                actual: val.type_of(),
751                            },
752                        );
753                    }
754                }
755
756                Op::Attrs => self.run_attrset(frame.read_uvarint() as usize, &frame)?,
757
758                Op::AttrsUpdate => lifted_pop! {
759                    self(rhs, lhs) => {
760                        let rhs = rhs.to_attrs().with_span(&frame, self)?;
761                        let lhs = lhs.to_attrs().with_span(&frame, self)?;
762                        self.stack.push(Value::attrs(lhs.update(rhs)))
763                    }
764                },
765
766                Op::Invert => lifted_pop! {
767                    self(v) => {
768                        let v = v.as_bool().with_span(&frame, self)?;
769                        self.stack.push(Value::Bool(!v));
770                    }
771                },
772
773                Op::List => {
774                    let count = frame.read_uvarint() as usize;
775                    let list =
776                        NixList::construct(count, self.stack.split_off(self.stack.len() - count));
777
778                    self.stack.push(Value::List(list));
779                }
780
781                Op::JumpIfTrue => {
782                    let offset = frame.read_u16() as usize;
783                    debug_assert!(offset != 0);
784                    if self.stack_peek(0).as_bool().with_span(&frame, self)? {
785                        frame.ip += offset;
786                    }
787                }
788
789                Op::HasAttr => lifted_pop! {
790                    self(key, attrs) => {
791                        let key = key.to_str().with_span(&frame, self)?;
792                        let result = match attrs {
793                            Value::Attrs(attrs) => attrs.contains(&key),
794
795                            // Nix allows use of `?` on non-set types, but
796                            // always returns false in those cases.
797                            _ => false,
798                        };
799
800                        self.stack.push(Value::Bool(result));
801                    }
802                },
803
804                Op::Concat => lifted_pop! {
805                    self(rhs, lhs) => {
806                        let rhs = rhs.to_list().with_span(&frame, self)?.into_inner();
807                        let mut lhs = lhs.to_list().with_span(&frame, self)?.into_inner();
808                        lhs.extend(rhs.into_iter());
809                        self.stack.push(Value::List(lhs.into()))
810                    }
811                },
812
813                Op::ResolveWith => {
814                    let ident = self.stack_pop().to_str().with_span(&frame, self)?;
815
816                    // Re-enqueue this frame.
817                    let op_span = frame.current_span();
818                    self.push_call_frame(span, frame);
819
820                    // Construct a generator frame doing the lookup in constant
821                    // stack space.
822                    let with_stack_len = self.with_stack.len();
823                    let closed_with_stack_len = self
824                        .last_call_frame()
825                        .map(|frame| frame.upvalues.with_stack_len())
826                        .unwrap_or(0);
827
828                    self.enqueue_generator("resolve_with", op_span, |co| {
829                        resolve_with(co, ident.into(), with_stack_len, closed_with_stack_len)
830                    });
831
832                    return Ok(false);
833                }
834
835                Op::Finalise => {
836                    let idx = frame.read_uvarint() as usize;
837                    match &self.stack[frame.stack_offset + idx] {
838                        Value::Closure(_) => panic!("attempted to finalise a closure"),
839                        Value::Thunk(thunk) => thunk.finalise(&self.stack[frame.stack_offset..]),
840                        _ => panic!("attempted to finalise a non-thunk"),
841                    }
842                }
843
844                Op::CoerceToString => {
845                    let kind: CoercionKind = frame.chunk().code[frame.ip.0].into();
846                    frame.ip.0 += 1;
847
848                    let value = self.stack_pop();
849                    let gen_span = frame.current_span();
850                    self.push_call_frame(span, frame);
851
852                    self.enqueue_generator("coerce_to_string", gen_span, |co| {
853                        value.coerce_to_string(co, kind, gen_span)
854                    });
855
856                    return Ok(false);
857                }
858
859                Op::Interpolate => self.run_interpolate(frame.read_uvarint(), &frame)?,
860
861                Op::ValidateClosedFormals => {
862                    let formals = frame.lambda.formals.as_ref().expect(
863                        "OpValidateClosedFormals called within the frame of a lambda without formals",
864                    );
865
866                    let peeked = self.stack_peek(0);
867                    if peeked.is_catchable() {
868                        continue;
869                    }
870
871                    let args = peeked.to_attrs().with_span(&frame, self)?;
872                    for arg in args.keys() {
873                        if !formals.contains(arg) {
874                            return frame.error(
875                                self,
876                                ErrorKind::UnexpectedArgumentFormals {
877                                    arg: arg.clone(),
878                                    formals_span: formals.span,
879                                },
880                            );
881                        }
882                    }
883                }
884
885                Op::Add => lifted_pop! {
886                    self(b, a) => {
887                        let gen_span = frame.current_span();
888                        self.push_call_frame(span, frame);
889
890                        // OpAdd can add not just numbers, but also string-like
891                        // things, which requires more VM logic. This operation is
892                        // evaluated in a generator frame.
893                        self.enqueue_generator("add_values", gen_span, |co| add_values(co, a, b));
894                        return Ok(false);
895                    }
896                },
897
898                Op::Sub => lifted_pop! {
899                    self(b, a) => {
900                        let result = arithmetic_op!(&a, &b, -).with_span(&frame, self)?;
901                        self.stack.push(result);
902                    }
903                },
904
905                Op::Mul => lifted_pop! {
906                    self(b, a) => {
907                        let result = arithmetic_op!(&a, &b, *).with_span(&frame, self)?;
908                        self.stack.push(result);
909                    }
910                },
911
912                Op::Div => lifted_pop! {
913                    self(b, a) => {
914                        match b {
915                            Value::Integer(0) => return frame.error(self, ErrorKind::DivisionByZero),
916                            Value::Float(0.0_f64) => {
917                                return frame.error(self, ErrorKind::DivisionByZero)
918                            }
919                            _ => {}
920                        };
921
922                        let result = arithmetic_op!(&a, &b, /).with_span(&frame, self)?;
923                        self.stack.push(result);
924                    }
925                },
926
927                Op::Negate => match self.stack_pop() {
928                    Value::Integer(i) => self.stack.push(Value::Integer(-i)),
929                    Value::Float(f) => self.stack.push(Value::Float(-f)),
930                    Value::Catchable(cex) => self.stack.push(Value::Catchable(cex)),
931                    v => {
932                        return frame.error(
933                            self,
934                            ErrorKind::TypeError {
935                                expected: "number (either int or float)",
936                                actual: v.type_of(),
937                            },
938                        );
939                    }
940                },
941
942                Op::Less => cmp_op!(self, frame, span, <),
943                Op::LessOrEq => cmp_op!(self, frame, span, <=),
944                Op::More => cmp_op!(self, frame, span, >),
945                Op::MoreOrEq => cmp_op!(self, frame, span, >=),
946
947                Op::FindFile => match self.stack_pop() {
948                    Value::UnresolvedPath(path) => {
949                        let resolved = self
950                            .nix_search_path
951                            .resolve(&self.io_handle, *path)
952                            .with_span(&frame, self)?;
953                        self.stack.push(resolved.into());
954                    }
955
956                    _ => panic!("Snix bug: OpFindFile called on non-UnresolvedPath"),
957                },
958
959                Op::ResolveHomePath => match self.stack_pop() {
960                    Value::UnresolvedPath(path) => {
961                        // FUTUREWORK: this only works on Linux and Darwin. Other platforms?
962                        let home_dir = self
963                            .io_handle
964                            .as_ref()
965                            .get_env(OsStr::new("HOME"))
966                            .and_then(|h| if h.is_empty() { None } else { Some(h) })
967                            .map(PathBuf::from);
968
969                        match home_dir {
970                            None => {
971                                return frame.error(
972                                    self,
973                                    ErrorKind::RelativePathResolution(
974                                        "failed to determine home directory".into(),
975                                    ),
976                                );
977                            }
978                            Some(mut buf) => {
979                                buf.push(*path);
980                                self.stack.push(buf.into());
981                            }
982                        };
983                    }
984
985                    _ => {
986                        panic!("Snix bug: OpResolveHomePath called on non-UnresolvedPath")
987                    }
988                },
989
990                Op::InterpolatePath => self.run_interpolate_path(frame.read_uvarint(), &frame)?,
991
992                Op::PushWith => self
993                    .with_stack
994                    .push(frame.stack_offset + frame.read_uvarint() as usize),
995
996                Op::PopWith => {
997                    self.with_stack.pop();
998                }
999
1000                Op::AssertFail => {
1001                    self.stack
1002                        .push(Value::from(CatchableErrorKind::AssertionFailed));
1003                }
1004
1005                // Encountering an invalid opcode is a critical error in the
1006                // VM/compiler.
1007                Op::Invalid => {
1008                    panic!("Snix bug: attempted to execute invalid opcode")
1009                }
1010            }
1011        }
1012    }
1013}
1014
1015/// Implementation of helper functions for the runtime logic above.
1016impl<IO> VM<'_, IO>
1017where
1018    IO: AsRef<dyn EvalIO> + 'static,
1019{
1020    pub(crate) fn stack_pop(&mut self) -> Value {
1021        self.stack.pop().expect("runtime stack empty")
1022    }
1023
1024    fn stack_peek(&self, offset: usize) -> &Value {
1025        &self.stack[self.stack.len() - 1 - offset]
1026    }
1027
1028    fn run_attrset(&mut self, count: usize, frame: &CallFrame) -> EvalResult<()> {
1029        let attrs = NixAttrs::construct(count, self.stack.split_off(self.stack.len() - count * 2))
1030            .with_span(frame, self)?
1031            .map(Value::attrs)
1032            .into();
1033
1034        self.stack.push(attrs);
1035        Ok(())
1036    }
1037
1038    /// Access the last call frame present in the frame stack.
1039    fn last_call_frame(&self) -> Option<&CallFrame> {
1040        for frame in self.frames.iter().rev() {
1041            if let Frame::CallFrame { call_frame, .. } = frame {
1042                return Some(call_frame);
1043            }
1044        }
1045
1046        None
1047    }
1048
1049    /// Push an already constructed warning.
1050    pub fn push_warning(&mut self, warning: EvalWarning) {
1051        self.warnings.push(warning);
1052    }
1053
1054    /// Emit a warning with the given WarningKind and the source span
1055    /// of the current instruction.
1056    pub fn emit_warning(&mut self, kind: WarningKind) {
1057        self.push_warning(EvalWarning {
1058            kind,
1059            span: self.get_span(),
1060        });
1061    }
1062
1063    /// Interpolate string fragments by popping the specified number of
1064    /// fragments of the stack, evaluating them to strings, and pushing
1065    /// the concatenated result string back on the stack.
1066    fn run_interpolate(&mut self, count: u64, frame: &CallFrame) -> EvalResult<()> {
1067        let mut out = BString::default();
1068        // Interpolation propagates the context and union them.
1069        let mut context: NixContext = NixContext::new();
1070
1071        for i in 0..count {
1072            let val = self.stack_pop();
1073            if val.is_catchable() {
1074                for _ in (i + 1)..count {
1075                    self.stack.pop();
1076                }
1077                self.stack.push(val);
1078                return Ok(());
1079            }
1080            let mut nix_string = val.to_contextful_str().with_span(frame, self)?;
1081            out.push_str(nix_string.as_bstr());
1082            if let Some(nix_string_ctx) = nix_string.take_context() {
1083                context.extend(nix_string_ctx.into_iter())
1084            }
1085        }
1086
1087        self.stack
1088            .push(Value::String(NixString::new_context_from(context, out)));
1089        Ok(())
1090    }
1091
1092    /// Interpolate path fragments by popping the specified number of
1093    /// fragments off the stack, evaluating them into a single path,
1094    /// and pushing a Path value back on the stack.
1095    fn run_interpolate_path(&mut self, count: u64, frame: &CallFrame) -> EvalResult<()> {
1096        // Similar pattern to run_interpolate for strings but simpler
1097        let mut path_str = String::new();
1098
1099        for i in 0..count {
1100            let val = self.stack_pop();
1101            if val.is_catchable() {
1102                // If we encounter an error, discard remaining parts and propagate the error
1103                for _ in (i + 1)..count {
1104                    self.stack.pop();
1105                }
1106                self.stack.push(val);
1107                return Ok(());
1108            }
1109
1110            // For path interpolation, we only accept string and path values
1111            match val {
1112                Value::String(s) => {
1113                    path_str.push_str(&s.as_bytes().to_str_lossy());
1114                }
1115                Value::Path(p) => {
1116                    path_str.push_str(&p.to_string_lossy());
1117                }
1118                _ => {
1119                    return frame.error(
1120                        self,
1121                        ErrorKind::TypeError {
1122                            expected: "string or path",
1123                            actual: val.type_of(),
1124                        },
1125                    );
1126                }
1127            }
1128        }
1129
1130        // Create a canonical path from the concatenated string
1131        let path = canon_path(PathBuf::from(path_str));
1132        self.stack.push(Value::Path(Box::new(path)));
1133
1134        Ok(())
1135    }
1136
1137    /// Apply an argument from the stack to a builtin, and attempt to call it.
1138    ///
1139    /// All calls are tail-calls in Snix, as every function application is a
1140    /// separate thunk and OpCall is thus the last result in the thunk.
1141    ///
1142    /// Due to this, once control flow exits this function, the generator will
1143    /// automatically be run by the VM.
1144    fn call_builtin(&mut self, span: Span, mut builtin: Builtin) -> EvalResult<()> {
1145        let builtin_name = builtin.name();
1146        self.observer.observe_enter_builtin(builtin_name);
1147
1148        builtin.apply_arg(self.stack_pop());
1149
1150        match builtin.call() {
1151            // Partially applied builtin is just pushed back on the stack.
1152            BuiltinResult::Partial(partial) => self.stack.push(Value::Builtin(partial)),
1153
1154            // Builtin is fully applied and the generator needs to be run by the VM.
1155            BuiltinResult::Called(name, generator) => self.frames.push(Frame::Generator {
1156                generator,
1157                span,
1158                name,
1159                state: GeneratorState::Running,
1160            }),
1161        }
1162
1163        Ok(())
1164    }
1165
1166    fn call_value(
1167        &mut self,
1168        span: Span,
1169        parent: Option<(Span, CallFrame)>,
1170        callable: Value,
1171    ) -> EvalResult<()> {
1172        match callable {
1173            Value::Builtin(builtin) => self.call_builtin(span, builtin),
1174            Value::Thunk(thunk) => self.call_value(span, parent, thunk.value().clone()),
1175
1176            Value::Closure(closure) => {
1177                let lambda = closure.lambda();
1178                self.observer.observe_tail_call(self.frames.len(), &lambda);
1179
1180                // The stack offset is always `stack.len() - arg_count`, and
1181                // since this branch handles native Nix functions (which always
1182                // take only a single argument and are curried), the offset is
1183                // `stack_len - 1`.
1184                let stack_offset = self.stack.len() - 1;
1185
1186                // Reenqueue the parent frame, which should only have
1187                // `OpReturn` left. Not throwing it away leads to more
1188                // useful error traces.
1189                if let Some((parent_span, parent_frame)) = parent {
1190                    self.push_call_frame(parent_span, parent_frame);
1191                }
1192
1193                self.push_call_frame(
1194                    span,
1195                    CallFrame {
1196                        lambda,
1197                        upvalues: closure.upvalues(),
1198                        ip: CodeIdx(0),
1199                        stack_offset,
1200                    },
1201                );
1202
1203                Ok(())
1204            }
1205
1206            // Attribute sets with a __functor attribute are callable.
1207            val @ Value::Attrs(_) => {
1208                if let Some((parent_span, parent_frame)) = parent {
1209                    self.push_call_frame(parent_span, parent_frame);
1210                }
1211
1212                self.enqueue_generator("__functor call", span, |co| call_functor(co, val));
1213                Ok(())
1214            }
1215
1216            val @ Value::Catchable(_) => {
1217                // the argument that we tried to apply a catchable to
1218                self.stack.pop();
1219                // applying a `throw` to anything is still a `throw`, so we just
1220                // push it back on the stack.
1221                self.stack.push(val);
1222                Ok(())
1223            }
1224
1225            v => Err(ErrorKind::NotCallable(v.type_of())).with_span(span, self),
1226        }
1227    }
1228
1229    /// Populate the upvalue fields of a thunk or closure under construction.
1230    ///
1231    /// See the closely tied function `emit_upvalue_data` in the compiler
1232    /// implementation for details on the argument processing.
1233    fn populate_upvalues(&self, frame: &mut CallFrame) -> EvalResult<Upvalues> {
1234        let data = UpvalueData::from_raw(frame.read_uvarint());
1235        let count = data.count();
1236        let capture_with = data.captures_with();
1237
1238        let mut static_upvalues = vec![];
1239
1240        let with_stack = if capture_with {
1241            // Start the captured with_stack off of the
1242            // current call frame's captured with_stack, ...
1243            let mut captured_with_stack = frame.upvalues.with_stack().clone();
1244            // and extend it to a size that fits the current with_stack
1245            captured_with_stack.reserve_exact(self.with_stack.len());
1246
1247            for idx in &self.with_stack {
1248                captured_with_stack.push(self.stack[*idx].clone());
1249            }
1250
1251            captured_with_stack
1252        } else {
1253            vec![]
1254        };
1255
1256        for _ in 0..count {
1257            let pos = Position(frame.read_uvarint());
1258
1259            if let Some(stack_idx) = pos.runtime_stack_index() {
1260                let idx = frame.stack_offset + stack_idx.0;
1261
1262                let val = match self.stack.get(idx) {
1263                    Some(val) => val.clone(),
1264                    None => {
1265                        // TODO: maybe panic here?
1266                        return frame.error(
1267                            self,
1268                            ErrorKind::SnixBug {
1269                                msg: "upvalue to be captured was missing on stack",
1270                                metadata: Some(Rc::new(json!({
1271                                    "ip": format!("{:#x}", frame.ip.0 - 1),
1272                                    "stack_idx(relative)": stack_idx.0,
1273                                    "stack_idx(absolute)": idx,
1274                                }))),
1275                            },
1276                        );
1277                    }
1278                };
1279
1280                static_upvalues.push(val);
1281            } else if let Some(idx) = pos.runtime_deferred_local() {
1282                static_upvalues.push(Value::DeferredUpvalue(idx));
1283            } else if let Some(idx) = pos.runtime_upvalue_index() {
1284                static_upvalues.push(frame.upvalue(idx).clone());
1285            } else {
1286                panic!("Snix bug: invalid capture position emitted")
1287            }
1288        }
1289
1290        Ok(Upvalues::from_raw_parts(static_upvalues, with_stack))
1291    }
1292}
1293
1294// TODO(amjoseph): de-asyncify this
1295/// Resolve a dynamically bound identifier (through `with`) by looking
1296/// for matching values in the with-stacks carried at runtime.
1297async fn resolve_with(
1298    co: GenCo,
1299    ident: BString,
1300    vm_with_len: usize,
1301    upvalue_with_len: usize,
1302) -> Result<Value, ErrorKind> {
1303    /// Fetch and force a value on the with-stack from the VM.
1304    async fn fetch_forced_with(co: &GenCo, idx: usize) -> Value {
1305        match co.yield_(VMRequest::WithValue(idx)).await {
1306            VMResponse::Value(value) => value,
1307            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1308        }
1309    }
1310
1311    /// Fetch and force a value on the *captured* with-stack from the VM.
1312    async fn fetch_captured_with(co: &GenCo, idx: usize) -> Value {
1313        match co.yield_(VMRequest::CapturedWithValue(idx)).await {
1314            VMResponse::Value(value) => value,
1315            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1316        }
1317    }
1318
1319    for with_stack_idx in (0..vm_with_len).rev() {
1320        // TODO(tazjin): is this branch still live with the current with-thunking?
1321        let with = fetch_forced_with(&co, with_stack_idx).await;
1322
1323        if with.is_catchable() {
1324            return Ok(with);
1325        }
1326
1327        match with.to_attrs()?.select(&ident) {
1328            None => continue,
1329            Some(val) => return Ok(val.clone()),
1330        }
1331    }
1332
1333    for upvalue_with_idx in (0..upvalue_with_len).rev() {
1334        let with = fetch_captured_with(&co, upvalue_with_idx).await;
1335
1336        if with.is_catchable() {
1337            return Ok(with);
1338        }
1339
1340        match with.to_attrs()?.select(&ident) {
1341            None => continue,
1342            Some(val) => return Ok(val.clone()),
1343        }
1344    }
1345
1346    Err(ErrorKind::UnknownDynamicVariable(ident.to_string()))
1347}
1348
1349// TODO(amjoseph): de-asyncify this
1350async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
1351    // What we try to do is solely determined by the type of the first value!
1352    let result = match (a, b) {
1353        (Value::Path(p), v) => {
1354            let mut path = p.into_os_string();
1355            match generators::request_string_coerce(
1356                &co,
1357                v,
1358                CoercionKind {
1359                    strong: false,
1360
1361                    // Concatenating a Path with something else results in a
1362                    // Path, so we don't need to import any paths (paths
1363                    // imported by Nix always exist as a string, unless
1364                    // converted by the user). In C++ Nix they even may not
1365                    // contain any string context, the resulting error of such a
1366                    // case can not be replicated by us.
1367                    import_paths: false,
1368                    // FIXME(raitobezarius): per https://b.tvl.fyi/issues/364, this is a usecase
1369                    // for having a `reject_context: true` option here. This didn't occur yet in
1370                    // nixpkgs during my evaluations, therefore, I skipped it.
1371                },
1372            )
1373            .await
1374            {
1375                Ok(vs) => {
1376                    path.push(vs.to_os_str()?);
1377                    crate::value::canon_path(PathBuf::from(path)).into()
1378                }
1379                Err(c) => Value::Catchable(Box::new(c)),
1380            }
1381        }
1382        (Value::String(s1), Value::String(s2)) => Value::String(s1.concat(&s2)),
1383        (Value::String(s1), v) => generators::request_string_coerce(
1384            &co,
1385            v,
1386            CoercionKind {
1387                strong: false,
1388                // Behaves the same as string interpolation
1389                import_paths: true,
1390            },
1391        )
1392        .await
1393        .map(|s2| Value::String(s1.concat(&s2)))
1394        .into(),
1395        (a @ Value::Integer(_), b) | (a @ Value::Float(_), b) => arithmetic_op!(&a, &b, +)?,
1396        (a, b) => {
1397            let r1 = generators::request_string_coerce(
1398                &co,
1399                a,
1400                CoercionKind {
1401                    strong: false,
1402                    import_paths: false,
1403                },
1404            )
1405            .await;
1406            let r2 = generators::request_string_coerce(
1407                &co,
1408                b,
1409                CoercionKind {
1410                    strong: false,
1411                    import_paths: false,
1412                },
1413            )
1414            .await;
1415            match (r1, r2) {
1416                (Ok(s1), Ok(s2)) => Value::String(s1.concat(&s2)),
1417                (Err(c), _) => return Ok(Value::from(c)),
1418                (_, Err(c)) => return Ok(Value::from(c)),
1419            }
1420        }
1421    };
1422
1423    Ok(result)
1424}
1425
1426/// The result of a VM's runtime evaluation.
1427pub struct RuntimeResult {
1428    pub value: Value,
1429    pub warnings: Vec<EvalWarning>,
1430}
1431
1432// TODO(amjoseph): de-asyncify this
1433/// Generator that retrieves the final value from the stack, and deep-forces it
1434/// before returning.
1435async fn final_deep_force(co: GenCo) -> Result<Value, ErrorKind> {
1436    let value = generators::request_stack_pop(&co).await;
1437    Ok(generators::request_deep_force(&co, value).await)
1438}
1439
1440/// Specification for how to handle top-level values returned by evaluation
1441#[derive(Debug, Clone, Copy, Default)]
1442pub enum EvalMode {
1443    /// The default. Values are returned from evaluations as-is, without any extra forcing or
1444    /// special handling.
1445    #[default]
1446    Lazy,
1447
1448    /// Strictly and deeply evaluate top-level values returned by evaluation.
1449    Strict,
1450}
1451
1452pub fn run_lambda<IO>(
1453    nix_search_path: NixSearchPath,
1454    io_handle: IO,
1455    observer: Option<&mut dyn RuntimeObserver>,
1456    source: SourceCode,
1457    globals: Rc<GlobalsMap>,
1458    lambda: Rc<Lambda>,
1459    mode: EvalMode,
1460) -> EvalResult<RuntimeResult>
1461where
1462    IO: AsRef<dyn EvalIO> + 'static,
1463{
1464    // Retain the top-level span of the expression in this lambda, as
1465    // synthetic "calls" in deep_force will otherwise not have a span
1466    // to fall back to.
1467    //
1468    // We exploit the fact that the compiler emits a final instruction
1469    // with the span of the entire file for top-level expressions.
1470    let root_span = lambda.chunk.get_span(CodeIdx(lambda.chunk.code.len() - 1));
1471
1472    let mut vm = VM::new(
1473        nix_search_path,
1474        io_handle,
1475        OptionalRuntimeObserver(observer),
1476        source,
1477        globals,
1478        root_span,
1479    );
1480
1481    // When evaluating strictly, synthesise a frame that will instruct
1482    // the VM to deep-force the final value before returning it.
1483    match mode {
1484        EvalMode::Lazy => {}
1485        EvalMode::Strict => vm.enqueue_generator("final_deep_force", root_span, final_deep_force),
1486    }
1487
1488    vm.frames.push(Frame::CallFrame {
1489        span: root_span,
1490        call_frame: CallFrame {
1491            lambda,
1492            upvalues: Rc::new(Upvalues::with_capacity(0)),
1493            ip: CodeIdx(0),
1494            stack_offset: 0,
1495        },
1496    });
1497
1498    vm.execute()
1499}