tvix_eval/vm/
mod.rs

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