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 &BytecodeFrame {
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 bytecode 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::BytecodeFrame { 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 BytecodeFrame {
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 BytecodeFrame {
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    /// BytecodeFrame represents the execution of Snix bytecode within a thunk,
202    /// function or closure.
203    BytecodeFrame {
204        /// The bytecode frame itself, separated out into another type to pass it
205        /// around easily.
206        bytecode_frame: BytecodeFrame,
207
208        /// Span from which the bytecode 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::BytecodeFrame { 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 bytecode frame onto the frame stack.
403    fn push_bytecode_frame(&mut self, span: Span, bytecode_frame: BytecodeFrame) {
404        self.frames.push(Frame::BytecodeFrame {
405            span,
406            bytecode_frame,
407        })
408    }
409
410    /// Run the VM's primary (outer) execution loop, continuing execution based
411    /// on the current frame at the top of the frame stack.
412    fn execute(mut self) -> EvalResult<RuntimeResult> {
413        while let Some(frame) = self.frames.pop() {
414            self.reasonable_span = frame.span();
415            let frame_id = self.frames.len();
416
417            match frame {
418                Frame::BytecodeFrame {
419                    bytecode_frame,
420                    span,
421                } => {
422                    self.observer
423                        .observe_enter_bytecode_frame(0, &bytecode_frame.lambda, frame_id);
424
425                    match self.execute_bytecode(span, bytecode_frame) {
426                        Ok(true) => {
427                            self.observer
428                                .observe_exit_bytecode_frame(frame_id, &self.stack);
429                        }
430                        Ok(false) => {
431                            self.observer
432                                .observe_suspend_bytecode_frame(frame_id, &self.stack);
433                        }
434                        Err(err) => return Err(err),
435                    };
436                }
437
438                // Handle generator frames, which can request thunk forcing
439                // during their execution.
440                Frame::Generator {
441                    name,
442                    span,
443                    state,
444                    generator,
445                } => {
446                    self.observer
447                        .observe_enter_generator(frame_id, name, &self.stack);
448
449                    match self.run_generator(name, span, frame_id, state, generator, None) {
450                        Ok(true) => {
451                            self.observer
452                                .observe_exit_generator(frame_id, name, &self.stack)
453                        }
454                        Ok(false) => {
455                            self.observer
456                                .observe_suspend_generator(frame_id, name, &self.stack)
457                        }
458
459                        Err(err) => return Err(err),
460                    };
461                }
462            }
463        }
464
465        // Once no more frames are present, return the stack's top value as the
466        // result.
467        let value = self
468            .stack
469            .pop()
470            .expect("Snix bug: runtime stack empty after execution");
471        Ok(RuntimeResult {
472            value,
473            warnings: self.warnings,
474        })
475    }
476
477    /// Run the VM's inner execution loop, processing Snix bytecode from a
478    /// chunk. This function returns if:
479    ///
480    /// 1. The code has run to the end, and has left a value on the top of the
481    ///    stack. In this case, the frame is not returned to the frame stack.
482    ///
483    /// 2. The code encounters a generator, in which case the frame in its
484    ///    current state is pushed back on the stack, and the generator is left
485    ///    on top of it for the outer loop to execute.
486    ///
487    /// 3. An error is encountered.
488    ///
489    /// This function *must* ensure that it leaves the frame stack in the
490    /// correct order, especially when re-enqueuing a frame to execute.
491    ///
492    /// The return value indicates whether the bytecode has been executed to
493    /// completion, or whether it has been suspended in favour of a generator.
494    fn execute_bytecode(&mut self, span: Span, mut frame: BytecodeFrame) -> EvalResult<bool> {
495        loop {
496            let op = frame.inc_ip();
497            self.observer.observe_execute_op(frame.ip, &op, &self.stack);
498
499            match op {
500                Op::ThunkSuspended | Op::ThunkClosure => {
501                    let idx = frame.read_uvarint() as usize;
502
503                    let blueprint = match &frame.chunk().constants[idx] {
504                        Value::Blueprint(lambda) => lambda.clone(),
505                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
506                    };
507
508                    let upvalues = self.populate_upvalues(&mut frame)?;
509                    debug_assert!(
510                        upvalues.len() == blueprint.upvalue_count,
511                        "TODO: new upvalue count not correct",
512                    );
513
514                    let thunk = if op == Op::ThunkClosure {
515                        debug_assert!(
516                            ((upvalues.len() > 0) || (upvalues.with_stack_len() > 0)),
517                            "OpThunkClosure should not be called for plain lambdas",
518                        );
519                        Thunk::new_closure(blueprint)
520                    } else {
521                        Thunk::new_suspended(blueprint, frame.current_span())
522                    };
523
524                    self.stack.push(Value::Thunk(thunk.clone()));
525
526                    // From this point on we internally mutate the
527                    // upvalues. The closure (if `is_closure`) is
528                    // already in its stack slot, which means that it
529                    // can capture itself as an upvalue for
530                    // self-recursion.
531                    *thunk.upvalues_mut() = upvalues;
532                }
533
534                Op::Force => {
535                    if let Some(Value::Thunk(_)) = self.stack.last() {
536                        let thunk = match self.stack_pop() {
537                            Value::Thunk(t) => t,
538                            _ => unreachable!(),
539                        };
540
541                        let gen_span = frame.current_span();
542
543                        self.push_bytecode_frame(span, frame);
544                        self.enqueue_generator("force", gen_span, |co| {
545                            Thunk::force(thunk, co, gen_span)
546                        });
547
548                        return Ok(false);
549                    }
550                }
551
552                Op::GetUpvalue => {
553                    let idx = UpvalueIdx(frame.read_uvarint() as usize);
554                    let value = frame.upvalue(idx).clone();
555                    self.stack.push(value);
556                }
557
558                // Discard the current frame.
559                Op::Return => {
560                    // TODO(amjoseph): I think this should assert `==` rather
561                    // than `<=` but it fails with the stricter condition.
562                    debug_assert!(self.stack.len() - 1 <= frame.stack_offset);
563                    return Ok(true);
564                }
565
566                Op::Constant => {
567                    let idx = frame.read_uvarint() as usize;
568
569                    debug_assert!(
570                        idx < frame.chunk().constants.len(),
571                        "out of bounds constant at IP {} in {:p}",
572                        frame.ip.0,
573                        frame.lambda
574                    );
575
576                    let c = frame.chunk().constants[idx].clone();
577                    self.stack.push(c);
578                }
579
580                Op::Call => {
581                    let callable = self.stack_pop();
582                    self.call_value(frame.current_span(), Some((span, frame)), callable)?;
583
584                    // exit this loop and let the outer loop enter the new call
585                    return Ok(true);
586                }
587
588                // Remove the given number of elements from the stack,
589                // but retain the top value.
590                Op::CloseScope => {
591                    let count = frame.read_uvarint() as usize;
592                    // Immediately move the top value into the right
593                    // position.
594                    let target_idx = self.stack.len() - 1 - count;
595                    self.stack[target_idx] = self.stack_pop();
596
597                    // Then drop the remaining values.
598                    for _ in 0..(count - 1) {
599                        self.stack.pop();
600                    }
601                }
602
603                Op::Closure => {
604                    let idx = frame.read_uvarint() as usize;
605                    let blueprint = match &frame.chunk().constants[idx] {
606                        Value::Blueprint(lambda) => lambda.clone(),
607                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
608                    };
609
610                    let upvalues = self.populate_upvalues(&mut frame)?;
611                    let upvalue_count = upvalues.len();
612                    debug_assert!(
613                        upvalue_count == blueprint.upvalue_count,
614                        "TODO: new upvalue count not correct in closure",
615                    );
616
617                    debug_assert!(
618                        (upvalue_count > 0 || upvalues.with_stack_len() > 0),
619                        "OpClosure should not be called for plain lambdas"
620                    );
621
622                    self.stack
623                        .push(Value::Closure(Rc::new(Closure::new_with_upvalues(
624                            Rc::new(upvalues),
625                            blueprint,
626                        ))));
627                }
628
629                Op::AttrsSelect => lifted_pop! {
630                    self(key, attrs) => {
631                        let key = key.to_str().with_span(&frame, self)?;
632                        let attrs = attrs.to_attrs().with_span(&frame, self)?;
633
634                        match attrs.select(&key) {
635                            Some(value) => self.stack.push(value.clone()),
636
637                            None => {
638                                return frame.error(
639                                    self,
640                                    ErrorKind::AttributeNotFound {
641                                        name: key.to_str_lossy().into_owned()
642                                    },
643                                );
644                            }
645                        }
646                    }
647                },
648
649                Op::JumpIfFalse => {
650                    let offset = frame.read_u16() as usize;
651                    debug_assert!(offset != 0);
652                    if !self.stack_peek(0).as_bool().with_span(&frame, self)? {
653                        frame.ip += offset;
654                    }
655                }
656
657                Op::JumpIfCatchable => {
658                    let offset = frame.read_u16() as usize;
659                    debug_assert!(offset != 0);
660                    if self.stack_peek(0).is_catchable() {
661                        frame.ip += offset;
662                    }
663                }
664
665                Op::JumpIfNoFinaliseRequest => {
666                    let offset = frame.read_u16() as usize;
667                    debug_assert!(offset != 0);
668                    match self.stack_peek(0) {
669                        Value::FinaliseRequest(finalise) => {
670                            if !finalise {
671                                frame.ip += offset;
672                            }
673                        }
674                        val => panic!(
675                            "Snix bug: OpJumIfNoFinaliseRequest: expected FinaliseRequest, but got {}",
676                            val.type_of()
677                        ),
678                    }
679                }
680
681                Op::Pop => {
682                    self.stack.pop();
683                }
684
685                Op::AttrsTrySelect => {
686                    let key = self.stack_pop().to_str().with_span(&frame, self)?;
687                    let value = match self.stack_pop() {
688                        Value::Attrs(attrs) => match attrs.select(&key) {
689                            Some(value) => value.clone(),
690                            None => Value::AttrNotFound,
691                        },
692
693                        _ => Value::AttrNotFound,
694                    };
695
696                    self.stack.push(value);
697                }
698
699                Op::GetLocal => {
700                    let local_idx = frame.read_uvarint() as usize;
701                    let idx = frame.stack_offset + local_idx;
702                    self.stack.push(self.stack[idx].clone());
703                }
704
705                Op::JumpIfNotFound => {
706                    let offset = frame.read_u16() as usize;
707                    debug_assert!(offset != 0);
708                    if matches!(self.stack_peek(0), Value::AttrNotFound) {
709                        self.stack_pop();
710                        frame.ip += offset;
711                    }
712                }
713
714                Op::Jump => {
715                    let offset = frame.read_u16() as usize;
716                    debug_assert!(offset != 0);
717                    frame.ip += offset;
718                }
719
720                Op::Equal => lifted_pop! {
721                    self(b, a) => {
722                        let gen_span = frame.current_span();
723                        self.push_bytecode_frame(span, frame);
724                        self.enqueue_generator("nix_eq", gen_span, |co| {
725                            a.nix_eq_owned_genco(b, co, PointerEquality::ForbidAll, gen_span)
726                        });
727                        return Ok(false);
728                    }
729                },
730
731                // These assertion operations error out if the stack
732                // top is not of the expected type. This is necessary
733                // to implement some specific behaviours of Nix
734                // exactly.
735                Op::AssertBool => {
736                    let val = self.stack_peek(0);
737                    // TODO(edef): propagate this into is_bool, since bottom values *are* values of any type
738                    if !val.is_catchable() && !val.is_bool() {
739                        return frame.error(
740                            self,
741                            ErrorKind::TypeError {
742                                expected: "bool",
743                                actual: val.type_of(),
744                            },
745                        );
746                    }
747                }
748
749                Op::AssertAttrs => {
750                    let val = self.stack_peek(0);
751                    // TODO(edef): propagate this into is_attrs, since bottom values *are* values of any type
752                    if !val.is_catchable() && !val.is_attrs() {
753                        return frame.error(
754                            self,
755                            ErrorKind::TypeError {
756                                expected: "set",
757                                actual: val.type_of(),
758                            },
759                        );
760                    }
761                }
762
763                Op::Attrs => self.run_attrset(frame.read_uvarint() as usize, &frame)?,
764
765                Op::AttrsUpdate => lifted_pop! {
766                    self(rhs, lhs) => {
767                        let rhs = rhs.to_attrs().with_span(&frame, self)?;
768                        let lhs = lhs.to_attrs().with_span(&frame, self)?;
769                        self.stack.push(Value::attrs(lhs.update(rhs)))
770                    }
771                },
772
773                Op::Invert => lifted_pop! {
774                    self(v) => {
775                        let v = v.as_bool().with_span(&frame, self)?;
776                        self.stack.push(Value::Bool(!v));
777                    }
778                },
779
780                Op::List => {
781                    let count = frame.read_uvarint() as usize;
782                    let list =
783                        NixList::construct(count, self.stack.split_off(self.stack.len() - count));
784
785                    self.stack.push(Value::List(list));
786                }
787
788                Op::JumpIfTrue => {
789                    let offset = frame.read_u16() as usize;
790                    debug_assert!(offset != 0);
791                    if self.stack_peek(0).as_bool().with_span(&frame, self)? {
792                        frame.ip += offset;
793                    }
794                }
795
796                Op::HasAttr => lifted_pop! {
797                    self(key, attrs) => {
798                        let key = key.to_str().with_span(&frame, self)?;
799                        let result = match attrs {
800                            Value::Attrs(attrs) => attrs.contains(&key),
801
802                            // Nix allows use of `?` on non-set types, but
803                            // always returns false in those cases.
804                            _ => false,
805                        };
806
807                        self.stack.push(Value::Bool(result));
808                    }
809                },
810
811                Op::Concat => lifted_pop! {
812                    self(rhs, lhs) => {
813                        let rhs = rhs.to_list().with_span(&frame, self)?.into_inner();
814                        let mut lhs = lhs.to_list().with_span(&frame, self)?.into_inner();
815                        lhs.extend(rhs.into_iter());
816                        self.stack.push(Value::List(lhs.into()))
817                    }
818                },
819
820                Op::ResolveWith => {
821                    let ident = self.stack_pop().to_str().with_span(&frame, self)?;
822
823                    // Re-enqueue this frame.
824                    let op_span = frame.current_span();
825                    self.push_bytecode_frame(span, frame);
826
827                    // Construct a generator frame doing the lookup in constant
828                    // stack space.
829                    let with_stack_len = self.with_stack.len();
830                    let closed_with_stack_len = self
831                        .last_bytecode_frame()
832                        .map(|frame| frame.upvalues.with_stack_len())
833                        .unwrap_or(0);
834
835                    self.enqueue_generator("resolve_with", op_span, |co| {
836                        resolve_with(co, ident.into(), with_stack_len, closed_with_stack_len)
837                    });
838
839                    return Ok(false);
840                }
841
842                Op::Finalise => {
843                    let idx = frame.read_uvarint() as usize;
844                    match &self.stack[frame.stack_offset + idx] {
845                        Value::Closure(_) => panic!("attempted to finalise a closure"),
846                        Value::Thunk(thunk) => thunk.finalise(&self.stack[frame.stack_offset..]),
847                        _ => panic!("attempted to finalise a non-thunk"),
848                    }
849                }
850
851                Op::CoerceToString => {
852                    let kind: CoercionKind = frame.chunk().code[frame.ip.0].into();
853                    frame.ip.0 += 1;
854
855                    let value = self.stack_pop();
856                    let gen_span = frame.current_span();
857                    self.push_bytecode_frame(span, frame);
858
859                    self.enqueue_generator("coerce_to_string", gen_span, |co| {
860                        value.coerce_to_string(co, kind, gen_span)
861                    });
862
863                    return Ok(false);
864                }
865
866                Op::Interpolate => self.run_interpolate(frame.read_uvarint(), &frame)?,
867
868                Op::ValidateClosedFormals => {
869                    let formals = frame.lambda.formals.as_ref().expect(
870                        "OpValidateClosedFormals called within the frame of a lambda without formals",
871                    );
872
873                    let peeked = self.stack_peek(0);
874                    if peeked.is_catchable() {
875                        continue;
876                    }
877
878                    let args = peeked.to_attrs().with_span(&frame, self)?;
879                    for arg in args.keys() {
880                        if !formals.contains(arg) {
881                            return frame.error(
882                                self,
883                                ErrorKind::UnexpectedArgumentFormals {
884                                    arg: arg.clone(),
885                                    formals_span: formals.span,
886                                },
887                            );
888                        }
889                    }
890                }
891
892                Op::Add => lifted_pop! {
893                    self(b, a) => {
894                        let gen_span = frame.current_span();
895                        self.push_bytecode_frame(span, frame);
896
897                        // OpAdd can add not just numbers, but also string-like
898                        // things, which requires more VM logic. This operation is
899                        // evaluated in a generator frame.
900                        self.enqueue_generator("add_values", gen_span, |co| add_values(co, a, b));
901                        return Ok(false);
902                    }
903                },
904
905                Op::Sub => 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::Mul => lifted_pop! {
913                    self(b, a) => {
914                        let result = arithmetic_op!(&a, &b, *).with_span(&frame, self)?;
915                        self.stack.push(result);
916                    }
917                },
918
919                Op::Div => lifted_pop! {
920                    self(b, a) => {
921                        match b {
922                            Value::Integer(0) => return frame.error(self, ErrorKind::DivisionByZero),
923                            Value::Float(0.0_f64) => {
924                                return frame.error(self, ErrorKind::DivisionByZero)
925                            }
926                            _ => {}
927                        };
928
929                        let result = arithmetic_op!(&a, &b, /).with_span(&frame, self)?;
930                        self.stack.push(result);
931                    }
932                },
933
934                Op::Negate => match self.stack_pop() {
935                    Value::Integer(i) => self.stack.push(Value::Integer(-i)),
936                    Value::Float(f) => self.stack.push(Value::Float(-f)),
937                    Value::Catchable(cex) => self.stack.push(Value::Catchable(cex)),
938                    v => {
939                        return frame.error(
940                            self,
941                            ErrorKind::TypeError {
942                                expected: "number (either int or float)",
943                                actual: v.type_of(),
944                            },
945                        );
946                    }
947                },
948
949                Op::Less => cmp_op!(self, frame, span, <),
950                Op::LessOrEq => cmp_op!(self, frame, span, <=),
951                Op::More => cmp_op!(self, frame, span, >),
952                Op::MoreOrEq => cmp_op!(self, frame, span, >=),
953
954                Op::FindFile => match self.stack_pop() {
955                    Value::UnresolvedPath(path) => {
956                        let resolved = self
957                            .nix_search_path
958                            .resolve(&self.io_handle, *path)
959                            .with_span(&frame, self)?;
960                        self.stack.push(resolved.into());
961                    }
962
963                    _ => panic!("Snix bug: OpFindFile called on non-UnresolvedPath"),
964                },
965
966                Op::ResolveHomePath => match self.stack_pop() {
967                    Value::UnresolvedPath(path) => {
968                        // FUTUREWORK: this only works on Linux and Darwin. Other platforms?
969                        let home_dir = self
970                            .io_handle
971                            .as_ref()
972                            .get_env(OsStr::new("HOME"))
973                            .and_then(|h| if h.is_empty() { None } else { Some(h) })
974                            .map(PathBuf::from);
975
976                        match home_dir {
977                            None => {
978                                return frame.error(
979                                    self,
980                                    ErrorKind::RelativePathResolution(
981                                        "failed to determine home directory".into(),
982                                    ),
983                                );
984                            }
985                            Some(mut buf) => {
986                                buf.push(*path);
987                                self.stack.push(buf.into());
988                            }
989                        };
990                    }
991
992                    _ => {
993                        panic!("Snix bug: OpResolveHomePath called on non-UnresolvedPath")
994                    }
995                },
996
997                Op::InterpolatePath => self.run_interpolate_path(frame.read_uvarint(), &frame)?,
998
999                Op::PushWith => self
1000                    .with_stack
1001                    .push(frame.stack_offset + frame.read_uvarint() as usize),
1002
1003                Op::PopWith => {
1004                    self.with_stack.pop();
1005                }
1006
1007                Op::AssertFail => {
1008                    self.stack
1009                        .push(Value::from(CatchableErrorKind::AssertionFailed));
1010                }
1011
1012                // Encountering an invalid opcode is a critical error in the
1013                // VM/compiler.
1014                Op::Invalid => {
1015                    panic!("Snix bug: attempted to execute invalid opcode")
1016                }
1017            }
1018        }
1019    }
1020}
1021
1022/// Implementation of helper functions for the runtime logic above.
1023impl<IO> VM<'_, IO>
1024where
1025    IO: AsRef<dyn EvalIO> + 'static,
1026{
1027    pub(crate) fn stack_pop(&mut self) -> Value {
1028        self.stack.pop().expect("runtime stack empty")
1029    }
1030
1031    fn stack_peek(&self, offset: usize) -> &Value {
1032        &self.stack[self.stack.len() - 1 - offset]
1033    }
1034
1035    fn run_attrset(&mut self, count: usize, frame: &BytecodeFrame) -> EvalResult<()> {
1036        let attrs = NixAttrs::construct(count, self.stack.split_off(self.stack.len() - count * 2))
1037            .with_span(frame, self)?
1038            .map(Value::attrs)
1039            .into();
1040
1041        self.stack.push(attrs);
1042        Ok(())
1043    }
1044
1045    /// Access the last bytecode frame present in the frame stack.
1046    fn last_bytecode_frame(&self) -> Option<&BytecodeFrame> {
1047        for frame in self.frames.iter().rev() {
1048            if let Frame::BytecodeFrame { bytecode_frame, .. } = frame {
1049                return Some(bytecode_frame);
1050            }
1051        }
1052
1053        None
1054    }
1055
1056    /// Push an already constructed warning.
1057    pub fn push_warning(&mut self, warning: EvalWarning) {
1058        self.warnings.push(warning);
1059    }
1060
1061    /// Emit a warning with the given WarningKind and the source span
1062    /// of the current instruction.
1063    pub fn emit_warning(&mut self, kind: WarningKind) {
1064        self.push_warning(EvalWarning {
1065            kind,
1066            span: self.get_span(),
1067        });
1068    }
1069
1070    /// Interpolate string fragments by popping the specified number of
1071    /// fragments of the stack, evaluating them to strings, and pushing
1072    /// the concatenated result string back on the stack.
1073    fn run_interpolate(&mut self, count: u64, frame: &BytecodeFrame) -> EvalResult<()> {
1074        let mut out = BString::default();
1075        // Interpolation propagates the context and union them.
1076        let mut context: NixContext = NixContext::new();
1077
1078        for i in 0..count {
1079            let val = self.stack_pop();
1080            if val.is_catchable() {
1081                for _ in (i + 1)..count {
1082                    self.stack.pop();
1083                }
1084                self.stack.push(val);
1085                return Ok(());
1086            }
1087            let mut nix_string = val.to_contextful_str().with_span(frame, self)?;
1088            out.push_str(nix_string.as_bstr());
1089            if let Some(nix_string_ctx) = nix_string.take_context() {
1090                context.extend(nix_string_ctx.into_iter())
1091            }
1092        }
1093
1094        self.stack
1095            .push(Value::String(NixString::new_context_from(context, out)));
1096        Ok(())
1097    }
1098
1099    /// Interpolate path fragments by popping the specified number of
1100    /// fragments off the stack, evaluating them into a single path,
1101    /// and pushing a Path value back on the stack.
1102    fn run_interpolate_path(&mut self, count: u64, frame: &BytecodeFrame) -> EvalResult<()> {
1103        // Similar pattern to run_interpolate for strings but simpler
1104        let mut path_str = String::new();
1105
1106        for i in 0..count {
1107            let val = self.stack_pop();
1108            if val.is_catchable() {
1109                // If we encounter an error, discard remaining parts and propagate the error
1110                for _ in (i + 1)..count {
1111                    self.stack.pop();
1112                }
1113                self.stack.push(val);
1114                return Ok(());
1115            }
1116
1117            // For path interpolation, we only accept string and path values
1118            match val {
1119                Value::String(s) => {
1120                    path_str.push_str(&s.as_bytes().to_str_lossy());
1121                }
1122                Value::Path(p) => {
1123                    path_str.push_str(&p.to_string_lossy());
1124                }
1125                _ => {
1126                    return frame.error(
1127                        self,
1128                        ErrorKind::TypeError {
1129                            expected: "string or path",
1130                            actual: val.type_of(),
1131                        },
1132                    );
1133                }
1134            }
1135        }
1136
1137        // Create a canonical path from the concatenated string
1138        let path = canon_path(PathBuf::from(path_str));
1139        self.stack.push(Value::Path(Box::new(path)));
1140
1141        Ok(())
1142    }
1143
1144    /// Apply an argument from the stack to a builtin, and attempt to call it.
1145    ///
1146    /// All calls are tail-calls in Snix, as every function application is a
1147    /// separate thunk and OpCall is thus the last result in the thunk.
1148    ///
1149    /// Due to this, once control flow exits this function, the generator will
1150    /// automatically be run by the VM.
1151    fn call_builtin(&mut self, span: Span, mut builtin: Builtin) -> EvalResult<()> {
1152        let builtin_name = builtin.name();
1153        self.observer.observe_enter_builtin(builtin_name);
1154
1155        builtin.apply_arg(self.stack_pop());
1156
1157        match builtin.call() {
1158            // Partially applied builtin is just pushed back on the stack.
1159            BuiltinResult::Partial(partial) => self.stack.push(Value::Builtin(partial)),
1160
1161            // Builtin is fully applied and the generator needs to be run by the VM.
1162            BuiltinResult::Called(name, generator) => self.frames.push(Frame::Generator {
1163                generator,
1164                span,
1165                name,
1166                state: GeneratorState::Running,
1167            }),
1168        }
1169
1170        Ok(())
1171    }
1172
1173    fn call_value(
1174        &mut self,
1175        span: Span,
1176        parent: Option<(Span, BytecodeFrame)>,
1177        callable: Value,
1178    ) -> EvalResult<()> {
1179        match callable {
1180            Value::Builtin(builtin) => self.call_builtin(span, builtin),
1181            Value::Thunk(thunk) => self.call_value(span, parent, thunk.value().clone()),
1182
1183            Value::Closure(closure) => {
1184                let lambda = closure.lambda();
1185                self.observer.observe_tail_call(self.frames.len(), &lambda);
1186
1187                // The stack offset is always `stack.len() - arg_count`, and
1188                // since this branch handles native Nix functions (which always
1189                // take only a single argument and are curried), the offset is
1190                // `stack_len - 1`.
1191                let stack_offset = self.stack.len() - 1;
1192
1193                // Reenqueue the parent frame, which should only have
1194                // `OpReturn` left. Not throwing it away leads to more
1195                // useful error traces.
1196                if let Some((parent_span, parent_frame)) = parent {
1197                    self.push_bytecode_frame(parent_span, parent_frame);
1198                }
1199
1200                self.push_bytecode_frame(
1201                    span,
1202                    BytecodeFrame {
1203                        lambda,
1204                        upvalues: closure.upvalues(),
1205                        ip: CodeIdx(0),
1206                        stack_offset,
1207                    },
1208                );
1209
1210                Ok(())
1211            }
1212
1213            // Attribute sets with a __functor attribute are callable.
1214            val @ Value::Attrs(_) => {
1215                if let Some((parent_span, parent_frame)) = parent {
1216                    self.push_bytecode_frame(parent_span, parent_frame);
1217                }
1218
1219                self.enqueue_generator("__functor call", span, |co| call_functor(co, val));
1220                Ok(())
1221            }
1222
1223            val @ Value::Catchable(_) => {
1224                // the argument that we tried to apply a catchable to
1225                self.stack.pop();
1226                // applying a `throw` to anything is still a `throw`, so we just
1227                // push it back on the stack.
1228                self.stack.push(val);
1229                Ok(())
1230            }
1231
1232            v => Err(ErrorKind::NotCallable(v.type_of())).with_span(span, self),
1233        }
1234    }
1235
1236    /// Populate the upvalue fields of a thunk or closure under construction.
1237    ///
1238    /// See the closely tied function `emit_upvalue_data` in the compiler
1239    /// implementation for details on the argument processing.
1240    fn populate_upvalues(&self, frame: &mut BytecodeFrame) -> EvalResult<Upvalues> {
1241        let data = UpvalueData::from_raw(frame.read_uvarint());
1242        let count = data.count();
1243        let capture_with = data.captures_with();
1244
1245        let mut static_upvalues = vec![];
1246
1247        let with_stack = if capture_with {
1248            // Start the captured with_stack off of the
1249            // current bytecode frame's captured with_stack, ...
1250            let mut captured_with_stack = frame.upvalues.with_stack().clone();
1251            // and extend it to a size that fits the current with_stack
1252            captured_with_stack.reserve_exact(self.with_stack.len());
1253
1254            for idx in &self.with_stack {
1255                captured_with_stack.push(self.stack[*idx].clone());
1256            }
1257
1258            captured_with_stack
1259        } else {
1260            vec![]
1261        };
1262
1263        for _ in 0..count {
1264            let pos = Position(frame.read_uvarint());
1265
1266            if let Some(stack_idx) = pos.runtime_stack_index() {
1267                let idx = frame.stack_offset + stack_idx.0;
1268
1269                let val = match self.stack.get(idx) {
1270                    Some(val) => val.clone(),
1271                    None => {
1272                        // TODO: maybe panic here?
1273                        return frame.error(
1274                            self,
1275                            ErrorKind::SnixBug {
1276                                msg: "upvalue to be captured was missing on stack",
1277                                metadata: Some(Rc::new(json!({
1278                                    "ip": format!("{:#x}", frame.ip.0 - 1),
1279                                    "stack_idx(relative)": stack_idx.0,
1280                                    "stack_idx(absolute)": idx,
1281                                }))),
1282                            },
1283                        );
1284                    }
1285                };
1286
1287                static_upvalues.push(val);
1288            } else if let Some(idx) = pos.runtime_deferred_local() {
1289                static_upvalues.push(Value::DeferredUpvalue(idx));
1290            } else if let Some(idx) = pos.runtime_upvalue_index() {
1291                static_upvalues.push(frame.upvalue(idx).clone());
1292            } else {
1293                panic!("Snix bug: invalid capture position emitted")
1294            }
1295        }
1296
1297        Ok(Upvalues::from_raw_parts(static_upvalues, with_stack))
1298    }
1299}
1300
1301// TODO(amjoseph): de-asyncify this
1302/// Resolve a dynamically bound identifier (through `with`) by looking
1303/// for matching values in the with-stacks carried at runtime.
1304async fn resolve_with(
1305    co: GenCo,
1306    ident: BString,
1307    vm_with_len: usize,
1308    upvalue_with_len: usize,
1309) -> Result<Value, ErrorKind> {
1310    /// Fetch and force a value on the with-stack from the VM.
1311    async fn fetch_forced_with(co: &GenCo, idx: usize) -> Value {
1312        match co.yield_(VMRequest::WithValue(idx)).await {
1313            VMResponse::Value(value) => value,
1314            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1315        }
1316    }
1317
1318    /// Fetch and force a value on the *captured* with-stack from the VM.
1319    async fn fetch_captured_with(co: &GenCo, idx: usize) -> Value {
1320        match co.yield_(VMRequest::CapturedWithValue(idx)).await {
1321            VMResponse::Value(value) => value,
1322            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1323        }
1324    }
1325
1326    for with_stack_idx in (0..vm_with_len).rev() {
1327        // TODO(tazjin): is this branch still live with the current with-thunking?
1328        let with = fetch_forced_with(&co, with_stack_idx).await;
1329
1330        if with.is_catchable() {
1331            return Ok(with);
1332        }
1333
1334        match with.to_attrs()?.select(&ident) {
1335            None => continue,
1336            Some(val) => return Ok(val.clone()),
1337        }
1338    }
1339
1340    for upvalue_with_idx in (0..upvalue_with_len).rev() {
1341        let with = fetch_captured_with(&co, upvalue_with_idx).await;
1342
1343        if with.is_catchable() {
1344            return Ok(with);
1345        }
1346
1347        match with.to_attrs()?.select(&ident) {
1348            None => continue,
1349            Some(val) => return Ok(val.clone()),
1350        }
1351    }
1352
1353    Err(ErrorKind::UnknownDynamicVariable(ident.to_string()))
1354}
1355
1356// TODO(amjoseph): de-asyncify this
1357async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
1358    // What we try to do is solely determined by the type of the first value!
1359    let result = match (a, b) {
1360        (Value::Path(p), v) => {
1361            let mut path = p.into_os_string();
1362            match generators::request_string_coerce(
1363                &co,
1364                v,
1365                CoercionKind {
1366                    strong: false,
1367
1368                    // Concatenating a Path with something else results in a
1369                    // Path, so we don't need to import any paths (paths
1370                    // imported by Nix always exist as a string, unless
1371                    // converted by the user). In C++ Nix they even may not
1372                    // contain any string context, the resulting error of such a
1373                    // case can not be replicated by us.
1374                    import_paths: false,
1375                    // FIXME(raitobezarius): per https://b.tvl.fyi/issues/364, this is a usecase
1376                    // for having a `reject_context: true` option here. This didn't occur yet in
1377                    // nixpkgs during my evaluations, therefore, I skipped it.
1378                },
1379            )
1380            .await
1381            {
1382                Ok(vs) => {
1383                    path.push(vs.to_os_str()?);
1384                    crate::value::canon_path(PathBuf::from(path)).into()
1385                }
1386                Err(c) => Value::Catchable(Box::new(c)),
1387            }
1388        }
1389        (Value::String(s1), Value::String(s2)) => Value::String(s1.concat(&s2)),
1390        (Value::String(s1), v) => generators::request_string_coerce(
1391            &co,
1392            v,
1393            CoercionKind {
1394                strong: false,
1395                // Behaves the same as string interpolation
1396                import_paths: true,
1397            },
1398        )
1399        .await
1400        .map(|s2| Value::String(s1.concat(&s2)))
1401        .into(),
1402        (a @ Value::Integer(_), b) | (a @ Value::Float(_), b) => arithmetic_op!(&a, &b, +)?,
1403        (a, b) => {
1404            let r1 = generators::request_string_coerce(
1405                &co,
1406                a,
1407                CoercionKind {
1408                    strong: false,
1409                    import_paths: false,
1410                },
1411            )
1412            .await;
1413            let r2 = generators::request_string_coerce(
1414                &co,
1415                b,
1416                CoercionKind {
1417                    strong: false,
1418                    import_paths: false,
1419                },
1420            )
1421            .await;
1422            match (r1, r2) {
1423                (Ok(s1), Ok(s2)) => Value::String(s1.concat(&s2)),
1424                (Err(c), _) => return Ok(Value::from(c)),
1425                (_, Err(c)) => return Ok(Value::from(c)),
1426            }
1427        }
1428    };
1429
1430    Ok(result)
1431}
1432
1433/// The result of a VM's runtime evaluation.
1434pub struct RuntimeResult {
1435    pub value: Value,
1436    pub warnings: Vec<EvalWarning>,
1437}
1438
1439// TODO(amjoseph): de-asyncify this
1440/// Generator that retrieves the final value from the stack, and deep-forces it
1441/// before returning.
1442async fn final_deep_force(co: GenCo) -> Result<Value, ErrorKind> {
1443    let value = generators::request_stack_pop(&co).await;
1444    Ok(generators::request_deep_force(&co, value).await)
1445}
1446
1447/// Specification for how to handle top-level values returned by evaluation
1448#[derive(Debug, Clone, Copy, Default)]
1449pub enum EvalMode {
1450    /// The default. Values are returned from evaluations as-is, without any extra forcing or
1451    /// special handling.
1452    #[default]
1453    Lazy,
1454
1455    /// Strictly and deeply evaluate top-level values returned by evaluation.
1456    Strict,
1457}
1458
1459pub fn run_lambda<IO>(
1460    nix_search_path: NixSearchPath,
1461    io_handle: IO,
1462    observer: Option<&mut dyn RuntimeObserver>,
1463    source: SourceCode,
1464    globals: Rc<GlobalsMap>,
1465    lambda: Rc<Lambda>,
1466    mode: EvalMode,
1467) -> EvalResult<RuntimeResult>
1468where
1469    IO: AsRef<dyn EvalIO> + 'static,
1470{
1471    // Retain the top-level span of the expression in this lambda, as
1472    // synthetic "calls" in deep_force will otherwise not have a span
1473    // to fall back to.
1474    //
1475    // We exploit the fact that the compiler emits a final instruction
1476    // with the span of the entire file for top-level expressions.
1477    let root_span = lambda.chunk.get_span(CodeIdx(lambda.chunk.code.len() - 1));
1478
1479    let mut vm = VM::new(
1480        nix_search_path,
1481        io_handle,
1482        OptionalRuntimeObserver(observer),
1483        source,
1484        globals,
1485        root_span,
1486    );
1487
1488    // When evaluating strictly, synthesise a frame that will instruct
1489    // the VM to deep-force the final value before returning it.
1490    match mode {
1491        EvalMode::Lazy => {}
1492        EvalMode::Strict => vm.enqueue_generator("final_deep_force", root_span, final_deep_force),
1493    }
1494
1495    vm.frames.push(Frame::BytecodeFrame {
1496        span: root_span,
1497        bytecode_frame: BytecodeFrame {
1498            lambda,
1499            upvalues: Rc::new(Upvalues::with_capacity(0)),
1500            ip: CodeIdx(0),
1501            stack_offset: 0,
1502        },
1503    });
1504
1505    vm.execute()
1506}