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}