Skip to main content

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                            .filter(|h| !h.is_empty())
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(
1037            count,
1038            self.stack.split_off(self.stack.len() - count * 2),
1039            &frame.chunk().attrsets_pos_spans,
1040        )
1041        .with_span(frame, self)?
1042        .map(Value::attrs)
1043        .into();
1044
1045        self.stack.push(attrs);
1046        Ok(())
1047    }
1048
1049    /// Access the last bytecode frame present in the frame stack.
1050    fn last_bytecode_frame(&self) -> Option<&BytecodeFrame> {
1051        for frame in self.frames.iter().rev() {
1052            if let Frame::BytecodeFrame { bytecode_frame, .. } = frame {
1053                return Some(bytecode_frame);
1054            }
1055        }
1056
1057        None
1058    }
1059
1060    /// Push an already constructed warning.
1061    pub fn push_warning(&mut self, warning: EvalWarning) {
1062        self.warnings.push(warning);
1063    }
1064
1065    /// Emit a warning with the given WarningKind and the source span
1066    /// of the current instruction.
1067    pub fn emit_warning(&mut self, kind: WarningKind) {
1068        self.push_warning(EvalWarning {
1069            kind,
1070            span: self.get_span(),
1071        });
1072    }
1073
1074    /// Interpolate string fragments by popping the specified number of
1075    /// fragments of the stack, evaluating them to strings, and pushing
1076    /// the concatenated result string back on the stack.
1077    fn run_interpolate(&mut self, count: u64, frame: &BytecodeFrame) -> EvalResult<()> {
1078        let mut out = BString::default();
1079        // Interpolation propagates the context and union them.
1080        let mut context: NixContext = NixContext::new();
1081
1082        for i in 0..count {
1083            let val = self.stack_pop();
1084            if val.is_catchable() {
1085                for _ in (i + 1)..count {
1086                    self.stack.pop();
1087                }
1088                self.stack.push(val);
1089                return Ok(());
1090            }
1091            let mut nix_string = val.to_contextful_str().with_span(frame, self)?;
1092            out.push_str(nix_string.as_bstr());
1093            if let Some(nix_string_ctx) = nix_string.take_context() {
1094                context.extend(*nix_string_ctx)
1095            }
1096        }
1097
1098        self.stack
1099            .push(Value::String(NixString::new_context_from(context, out)));
1100        Ok(())
1101    }
1102
1103    /// Interpolate path fragments by popping the specified number of
1104    /// fragments off the stack, evaluating them into a single path,
1105    /// and pushing a Path value back on the stack.
1106    fn run_interpolate_path(&mut self, count: u64, frame: &BytecodeFrame) -> EvalResult<()> {
1107        // Similar pattern to run_interpolate for strings but simpler
1108        let mut path_str = String::new();
1109
1110        for i in 0..count {
1111            let val = self.stack_pop();
1112            if val.is_catchable() {
1113                // If we encounter an error, discard remaining parts and propagate the error
1114                for _ in (i + 1)..count {
1115                    self.stack.pop();
1116                }
1117                self.stack.push(val);
1118                return Ok(());
1119            }
1120
1121            // For path interpolation, we only accept string and path values
1122            match val {
1123                Value::String(s) => {
1124                    path_str.push_str(&s.as_bytes().to_str_lossy());
1125                }
1126                Value::Path(p) => {
1127                    path_str.push_str(&p.to_string_lossy());
1128                }
1129                _ => {
1130                    return frame.error(
1131                        self,
1132                        ErrorKind::TypeError {
1133                            expected: "string or path",
1134                            actual: val.type_of(),
1135                        },
1136                    );
1137                }
1138            }
1139        }
1140
1141        // Create a canonical path from the concatenated string
1142        let path = canon_path(PathBuf::from(path_str));
1143        self.stack.push(Value::Path(Box::new(path)));
1144
1145        Ok(())
1146    }
1147
1148    /// Apply an argument from the stack to a builtin, and attempt to call it.
1149    ///
1150    /// All calls are tail-calls in Snix, as every function application is a
1151    /// separate thunk and OpCall is thus the last result in the thunk.
1152    ///
1153    /// Due to this, once control flow exits this function, the generator will
1154    /// automatically be run by the VM.
1155    fn call_builtin(&mut self, span: Span, mut builtin: Builtin) -> EvalResult<()> {
1156        let builtin_name = builtin.name();
1157        self.observer.observe_enter_builtin(builtin_name);
1158
1159        builtin.apply_arg(self.stack_pop());
1160
1161        match builtin.call() {
1162            // Partially applied builtin is just pushed back on the stack.
1163            BuiltinResult::Partial(partial) => self.stack.push(Value::Builtin(partial)),
1164
1165            // Builtin is fully applied and the generator needs to be run by the VM.
1166            BuiltinResult::Called(name, generator) => self.frames.push(Frame::Generator {
1167                generator,
1168                span,
1169                name,
1170                state: GeneratorState::Running,
1171            }),
1172        }
1173
1174        Ok(())
1175    }
1176
1177    fn call_value(
1178        &mut self,
1179        span: Span,
1180        parent: Option<(Span, BytecodeFrame)>,
1181        callable: Value,
1182    ) -> EvalResult<()> {
1183        match callable {
1184            Value::Builtin(builtin) => self.call_builtin(span, builtin),
1185            Value::Thunk(thunk) => self.call_value(span, parent, thunk.value().clone()),
1186
1187            Value::Closure(closure) => {
1188                let lambda = closure.lambda();
1189                self.observer.observe_tail_call(self.frames.len(), &lambda);
1190
1191                // The stack offset is always `stack.len() - arg_count`, and
1192                // since this branch handles native Nix functions (which always
1193                // take only a single argument and are curried), the offset is
1194                // `stack_len - 1`.
1195                let stack_offset = self.stack.len() - 1;
1196
1197                // Reenqueue the parent frame, which should only have
1198                // `OpReturn` left. Not throwing it away leads to more
1199                // useful error traces.
1200                if let Some((parent_span, parent_frame)) = parent {
1201                    self.push_bytecode_frame(parent_span, parent_frame);
1202                }
1203
1204                self.push_bytecode_frame(
1205                    span,
1206                    BytecodeFrame {
1207                        lambda,
1208                        upvalues: closure.upvalues(),
1209                        ip: CodeIdx(0),
1210                        stack_offset,
1211                    },
1212                );
1213
1214                Ok(())
1215            }
1216
1217            // Attribute sets with a __functor attribute are callable.
1218            val @ Value::Attrs(_) => {
1219                if let Some((parent_span, parent_frame)) = parent {
1220                    self.push_bytecode_frame(parent_span, parent_frame);
1221                }
1222
1223                self.enqueue_generator("__functor call", span, |co| call_functor(co, val));
1224                Ok(())
1225            }
1226
1227            val @ Value::Catchable(_) => {
1228                // the argument that we tried to apply a catchable to
1229                self.stack.pop();
1230                // applying a `throw` to anything is still a `throw`, so we just
1231                // push it back on the stack.
1232                self.stack.push(val);
1233                Ok(())
1234            }
1235
1236            v => Err(ErrorKind::NotCallable(v.type_of())).with_span(span, self),
1237        }
1238    }
1239
1240    /// Populate the upvalue fields of a thunk or closure under construction.
1241    ///
1242    /// See the closely tied function `emit_upvalue_data` in the compiler
1243    /// implementation for details on the argument processing.
1244    fn populate_upvalues(&self, frame: &mut BytecodeFrame) -> EvalResult<Upvalues> {
1245        let data = UpvalueData::from_raw(frame.read_uvarint());
1246        let count = data.count();
1247        let capture_with = data.captures_with();
1248
1249        let mut static_upvalues = vec![];
1250
1251        let with_stack = if capture_with {
1252            // Start the captured with_stack off of the
1253            // current bytecode frame's captured with_stack, ...
1254            let mut captured_with_stack = frame.upvalues.with_stack().clone();
1255            // and extend it to a size that fits the current with_stack
1256            captured_with_stack.reserve_exact(self.with_stack.len());
1257
1258            for idx in &self.with_stack {
1259                captured_with_stack.push(self.stack[*idx].clone());
1260            }
1261
1262            captured_with_stack
1263        } else {
1264            vec![]
1265        };
1266
1267        for _ in 0..count {
1268            let pos = Position(frame.read_uvarint());
1269
1270            if let Some(stack_idx) = pos.runtime_stack_index() {
1271                let idx = frame.stack_offset + stack_idx.0;
1272
1273                let val = match self.stack.get(idx) {
1274                    Some(val) => val.clone(),
1275                    None => {
1276                        // TODO: maybe panic here?
1277                        return frame.error(
1278                            self,
1279                            ErrorKind::SnixBug {
1280                                msg: "upvalue to be captured was missing on stack",
1281                                metadata: Some(Rc::new(json!({
1282                                    "ip": format!("{:#x}", frame.ip.0 - 1),
1283                                    "stack_idx(relative)": stack_idx.0,
1284                                    "stack_idx(absolute)": idx,
1285                                }))),
1286                            },
1287                        );
1288                    }
1289                };
1290
1291                static_upvalues.push(val);
1292            } else if let Some(idx) = pos.runtime_deferred_local() {
1293                static_upvalues.push(Value::DeferredUpvalue(idx));
1294            } else if let Some(idx) = pos.runtime_upvalue_index() {
1295                static_upvalues.push(frame.upvalue(idx).clone());
1296            } else {
1297                panic!("Snix bug: invalid capture position emitted")
1298            }
1299        }
1300
1301        Ok(Upvalues::from_raw_parts(static_upvalues, with_stack))
1302    }
1303}
1304
1305// TODO(amjoseph): de-asyncify this
1306/// Resolve a dynamically bound identifier (through `with`) by looking
1307/// for matching values in the with-stacks carried at runtime.
1308async fn resolve_with(
1309    co: GenCo,
1310    ident: BString,
1311    vm_with_len: usize,
1312    upvalue_with_len: usize,
1313) -> Result<Value, ErrorKind> {
1314    /// Fetch and force a value on the with-stack from the VM.
1315    async fn fetch_forced_with(co: &GenCo, idx: usize) -> Value {
1316        match co.yield_(VMRequest::WithValue(idx)).await {
1317            VMResponse::Value(value) => value,
1318            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1319        }
1320    }
1321
1322    /// Fetch and force a value on the *captured* with-stack from the VM.
1323    async fn fetch_captured_with(co: &GenCo, idx: usize) -> Value {
1324        match co.yield_(VMRequest::CapturedWithValue(idx)).await {
1325            VMResponse::Value(value) => value,
1326            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1327        }
1328    }
1329
1330    for with_stack_idx in (0..vm_with_len).rev() {
1331        // TODO(tazjin): is this branch still live with the current with-thunking?
1332        let with = fetch_forced_with(&co, with_stack_idx).await;
1333
1334        if with.is_catchable() {
1335            return Ok(with);
1336        }
1337
1338        match with.to_attrs()?.select(&ident) {
1339            None => continue,
1340            Some(val) => return Ok(val.clone()),
1341        }
1342    }
1343
1344    for upvalue_with_idx in (0..upvalue_with_len).rev() {
1345        let with = fetch_captured_with(&co, upvalue_with_idx).await;
1346
1347        if with.is_catchable() {
1348            return Ok(with);
1349        }
1350
1351        match with.to_attrs()?.select(&ident) {
1352            None => continue,
1353            Some(val) => return Ok(val.clone()),
1354        }
1355    }
1356
1357    Err(ErrorKind::UnknownDynamicVariable(ident.to_string()))
1358}
1359
1360// TODO(amjoseph): de-asyncify this
1361async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
1362    // What we try to do is solely determined by the type of the first value!
1363    let result = match (a, b) {
1364        (Value::Path(p), v) => {
1365            let mut path = p.into_os_string();
1366            match generators::request_string_coerce(
1367                &co,
1368                v,
1369                CoercionKind {
1370                    strong: false,
1371
1372                    // Concatenating a Path with something else results in a
1373                    // Path, so we don't need to import any paths (paths
1374                    // imported by Nix always exist as a string, unless
1375                    // converted by the user). In C++ Nix they even may not
1376                    // contain any string context, the resulting error of such a
1377                    // case can not be replicated by us.
1378                    import_paths: false,
1379                    // FIXME(raitobezarius): per https://b.tvl.fyi/issues/364, this is a usecase
1380                    // for having a `reject_context: true` option here. This didn't occur yet in
1381                    // nixpkgs during my evaluations, therefore, I skipped it.
1382                },
1383            )
1384            .await
1385            {
1386                Ok(vs) => {
1387                    path.push(vs.to_os_str()?);
1388                    crate::value::canon_path(PathBuf::from(path)).into()
1389                }
1390                Err(c) => Value::Catchable(Box::new(c)),
1391            }
1392        }
1393        (Value::String(s1), Value::String(s2)) => Value::String(s1.concat(&s2)),
1394        (Value::String(s1), v) => generators::request_string_coerce(
1395            &co,
1396            v,
1397            CoercionKind {
1398                strong: false,
1399                // Behaves the same as string interpolation
1400                import_paths: true,
1401            },
1402        )
1403        .await
1404        .map(|s2| Value::String(s1.concat(&s2)))
1405        .into(),
1406        (a @ Value::Integer(_), b) | (a @ Value::Float(_), b) => arithmetic_op!(&a, &b, +)?,
1407        (a, b) => {
1408            let r1 = generators::request_string_coerce(
1409                &co,
1410                a,
1411                CoercionKind {
1412                    strong: false,
1413                    import_paths: false,
1414                },
1415            )
1416            .await;
1417            let r2 = generators::request_string_coerce(
1418                &co,
1419                b,
1420                CoercionKind {
1421                    strong: false,
1422                    import_paths: false,
1423                },
1424            )
1425            .await;
1426            match (r1, r2) {
1427                (Ok(s1), Ok(s2)) => Value::String(s1.concat(&s2)),
1428                (Err(c), _) => return Ok(Value::from(c)),
1429                (_, Err(c)) => return Ok(Value::from(c)),
1430            }
1431        }
1432    };
1433
1434    Ok(result)
1435}
1436
1437/// The result of a VM's runtime evaluation.
1438pub struct RuntimeResult {
1439    pub value: Value,
1440    pub warnings: Vec<EvalWarning>,
1441}
1442
1443// TODO(amjoseph): de-asyncify this
1444/// Generator that retrieves the final value from the stack, and deep-forces it
1445/// before returning.
1446async fn final_deep_force(co: GenCo) -> Result<Value, ErrorKind> {
1447    let value = generators::request_stack_pop(&co).await;
1448    Ok(generators::request_deep_force(&co, value).await)
1449}
1450
1451/// Specification for how to handle top-level values returned by evaluation
1452#[derive(Debug, Clone, Copy, Default)]
1453pub enum EvalMode {
1454    /// The default. Values are returned from evaluations as-is, without any extra forcing or
1455    /// special handling.
1456    #[default]
1457    Lazy,
1458
1459    /// Strictly and deeply evaluate top-level values returned by evaluation.
1460    Strict,
1461}
1462
1463pub fn run_lambda<IO>(
1464    nix_search_path: NixSearchPath,
1465    io_handle: IO,
1466    observer: Option<&mut dyn RuntimeObserver>,
1467    source: SourceCode,
1468    globals: Rc<GlobalsMap>,
1469    lambda: Rc<Lambda>,
1470    mode: EvalMode,
1471) -> EvalResult<RuntimeResult>
1472where
1473    IO: AsRef<dyn EvalIO> + 'static,
1474{
1475    // Retain the top-level span of the expression in this lambda, as
1476    // synthetic "calls" in deep_force will otherwise not have a span
1477    // to fall back to.
1478    //
1479    // We exploit the fact that the compiler emits a final instruction
1480    // with the span of the entire file for top-level expressions.
1481    let root_span = lambda.chunk.get_span(CodeIdx(lambda.chunk.code.len() - 1));
1482
1483    let mut vm = VM::new(
1484        nix_search_path,
1485        io_handle,
1486        OptionalRuntimeObserver(observer),
1487        source,
1488        globals,
1489        root_span,
1490    );
1491
1492    // When evaluating strictly, synthesise a frame that will instruct
1493    // the VM to deep-force the final value before returning it.
1494    match mode {
1495        EvalMode::Lazy => {}
1496        EvalMode::Strict => vm.enqueue_generator("final_deep_force", root_span, final_deep_force),
1497    }
1498
1499    vm.frames.push(Frame::BytecodeFrame {
1500        span: root_span,
1501        bytecode_frame: BytecodeFrame {
1502            lambda,
1503            upvalues: Rc::new(Upvalues::with_capacity(0)),
1504            ip: CodeIdx(0),
1505            stack_offset: 0,
1506        },
1507    });
1508
1509    vm.execute()
1510}