tvix_eval/value/
thunk.rs

1//! This module implements the runtime representation of Thunks.
2//!
3//! Thunks are a special kind of Nix value, similar to a 0-argument
4//! closure that yields some value. Thunks are used to implement the
5//! lazy evaluation behaviour of Nix:
6//!
7//! Whenever the compiler determines that an expression should be
8//! evaluated lazily, it creates a thunk instead of compiling the
9//! expression value directly. At any point in the runtime where the
10//! actual value of a thunk is required, it is "forced", meaning that
11//! the encompassing computation takes place and the thunk takes on
12//! its new value.
13//!
14//! Thunks have interior mutability to be able to memoise their
15//! computation. Once a thunk is evaluated, its internal
16//! representation becomes the result of the expression. It is legal
17//! for the runtime to replace a thunk object directly with its value
18//! object, but when forcing a thunk, the runtime *must* mutate the
19//! memoisable slot.
20
21use rustc_hash::FxHashSet;
22use std::{
23    cell::{Ref, RefCell, RefMut},
24    fmt::Debug,
25    rc::Rc,
26};
27
28use crate::{
29    errors::ErrorKind,
30    opcode::Op,
31    upvalues::Upvalues,
32    value::Closure,
33    vm::generators::{self, GenCo},
34    Value,
35};
36
37use super::{Lambda, TotalDisplay};
38use codemap::Span;
39
40/// Internal representation of a suspended native thunk.
41struct SuspendedNative(Box<dyn Fn() -> Result<Value, ErrorKind>>);
42
43impl Debug for SuspendedNative {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "SuspendedNative({:p})", self.0)
46    }
47}
48
49/// Internal representation of the different states of a thunk.
50///
51/// Upvalues must be finalised before leaving the initial state
52/// (Suspended or RecursiveClosure).  The [`value()`] function may
53/// not be called until the thunk is in the final state (Evaluated).
54#[derive(Debug)]
55enum ThunkRepr {
56    /// Thunk is closed over some values, suspended and awaiting
57    /// execution.
58    Suspended {
59        lambda: Rc<Lambda>,
60        upvalues: Rc<Upvalues>,
61        span: Span,
62    },
63
64    /// Thunk is a suspended native computation.
65    Native(SuspendedNative),
66
67    /// Thunk currently under-evaluation; encountering a blackhole
68    /// value means that infinite recursion has occured.
69    Blackhole {
70        /// Span at which the thunk was first forced.
71        forced_at: Span,
72
73        /// Span at which the thunk was originally suspended.
74        suspended_at: Option<Span>,
75
76        /// Span of the first instruction of the actual code inside
77        /// the thunk.
78        content_span: Option<Span>,
79    },
80
81    // TODO(amjoseph): consider changing `Value` to `Rc<Value>` to avoid
82    // expensive clone()s in Thunk::force().
83    /// Fully evaluated thunk.
84    Evaluated(Value),
85}
86
87impl ThunkRepr {
88    fn debug_repr(&self) -> String {
89        match self {
90            ThunkRepr::Evaluated(v) => format!("thunk(val|{v})"),
91            ThunkRepr::Blackhole { .. } => "thunk(blackhole)".to_string(),
92            ThunkRepr::Native(_) => "thunk(native)".to_string(),
93            ThunkRepr::Suspended { lambda, .. } => format!("thunk({:p})", *lambda),
94        }
95    }
96
97    /// Return the Value within a fully-evaluated ThunkRepr; panics
98    /// if the thunk is not fully-evaluated.
99    fn expect(self) -> Value {
100        match self {
101            ThunkRepr::Evaluated(value) => value,
102            ThunkRepr::Blackhole { .. } => panic!("Thunk::expect() called on a black-holed thunk"),
103            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
104                panic!("Thunk::expect() called on a suspended thunk")
105            }
106        }
107    }
108
109    fn expect_ref(&self) -> &Value {
110        match self {
111            ThunkRepr::Evaluated(value) => value,
112            ThunkRepr::Blackhole { .. } => panic!("Thunk::expect() called on a black-holed thunk"),
113            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
114                panic!("Thunk::expect() called on a suspended thunk")
115            }
116        }
117    }
118
119    pub fn is_forced(&self) -> bool {
120        match self {
121            ThunkRepr::Evaluated(Value::Thunk(_)) => false,
122            ThunkRepr::Evaluated(_) => true,
123            _ => false,
124        }
125    }
126}
127
128/// A thunk is created for any value which requires non-strict
129/// evaluation due to self-reference or lazy semantics (or both).
130/// Every reference cycle involving `Value`s will contain at least
131/// one `Thunk`.
132#[derive(Clone, Debug)]
133pub struct Thunk(Rc<RefCell<ThunkRepr>>);
134
135impl Thunk {
136    pub fn new_closure(lambda: Rc<Lambda>) -> Self {
137        Thunk(Rc::new(RefCell::new(ThunkRepr::Evaluated(Value::Closure(
138            Rc::new(Closure {
139                upvalues: Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
140                lambda: lambda.clone(),
141            }),
142        )))))
143    }
144
145    pub fn new_suspended(lambda: Rc<Lambda>, span: Span) -> Self {
146        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
147            upvalues: Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
148            lambda: lambda.clone(),
149            span,
150        })))
151    }
152
153    pub fn new_suspended_native(native: Box<dyn Fn() -> Result<Value, ErrorKind>>) -> Self {
154        Thunk(Rc::new(RefCell::new(ThunkRepr::Native(SuspendedNative(
155            native,
156        )))))
157    }
158
159    /// Helper function to create a [`Thunk`] that calls a function given as the
160    /// [`Value`] `callee` with the argument `arg` when it is forced. This is
161    /// particularly useful in builtin implementations if the result of calling
162    /// a function does not need to be forced immediately, because e.g. it is
163    /// stored in an attribute set.
164    pub fn new_suspended_call(callee: Value, arg: Value, span: Span) -> Self {
165        let mut lambda = Lambda::default();
166
167        let arg_idx = lambda.chunk().push_constant(arg);
168        let f_idx = lambda.chunk().push_constant(callee);
169
170        // This is basically a recreation of compile_apply():
171        // We need to push the argument onto the stack and then the function.
172        // The function (not the argument) needs to be forced before calling.
173        lambda.chunk.push_op(Op::Constant, span);
174        lambda.chunk.push_uvarint(arg_idx.0 as u64);
175        lambda.chunk.push_op(Op::Constant, span);
176        lambda.chunk.push_uvarint(f_idx.0 as u64);
177        lambda.chunk.push_op(Op::Force, span);
178        lambda.chunk.push_op(Op::Call, span);
179
180        // Inform the VM that the chunk has ended
181        lambda.chunk.push_op(Op::Return, span);
182
183        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
184            upvalues: Rc::new(Upvalues::with_capacity(0)),
185            lambda: Rc::new(lambda),
186            span,
187        })))
188    }
189
190    fn prepare_blackhole(&self, forced_at: Span) -> ThunkRepr {
191        match &*self.0.borrow() {
192            ThunkRepr::Suspended { span, lambda, .. } => ThunkRepr::Blackhole {
193                forced_at,
194                suspended_at: Some(*span),
195                content_span: Some(lambda.chunk.first_span()),
196            },
197
198            _ => ThunkRepr::Blackhole {
199                forced_at,
200                suspended_at: None,
201                content_span: None,
202            },
203        }
204    }
205
206    pub async fn force(myself: Thunk, co: GenCo, span: Span) -> Result<Value, ErrorKind> {
207        Self::force_(myself, &co, span).await
208    }
209
210    pub async fn force_(mut myself: Thunk, co: &GenCo, span: Span) -> Result<Value, ErrorKind> {
211        // This vector of "thunks which point to the thunk-being-forced", to
212        // be updated along with it, is necessary in order to write this
213        // function in iterative (and later, mutual-tail-call) form.
214        let mut also_update: Vec<Rc<RefCell<ThunkRepr>>> = vec![];
215
216        loop {
217            // If the current thunk is already fully evaluated, return its evaluated
218            // value. The VM will continue running the code that landed us here.
219            if myself.is_forced() {
220                let val = myself.unwrap_or_clone();
221                for other_thunk in also_update.into_iter() {
222                    other_thunk.replace(ThunkRepr::Evaluated(val.clone()));
223                }
224                return Ok(val);
225            }
226
227            // Begin evaluation of this thunk by marking it as a blackhole, meaning
228            // that any other forcing frame encountering this thunk before its
229            // evaluation is completed detected an evaluation cycle.
230            let inner = myself.0.replace(myself.prepare_blackhole(span));
231
232            match inner {
233                // If there was already a blackhole in the thunk, this is an
234                // evaluation cycle.
235                ThunkRepr::Blackhole {
236                    forced_at,
237                    suspended_at,
238                    content_span,
239                } => {
240                    return Err(ErrorKind::InfiniteRecursion {
241                        first_force: forced_at,
242                        suspended_at,
243                        content_span,
244                    })
245                }
246
247                // If there is a native function stored in the thunk, evaluate it
248                // and replace this thunk's representation with the result.
249                ThunkRepr::Native(native) => {
250                    let value = native.0()?;
251                    myself.0.replace(ThunkRepr::Evaluated(value));
252                    continue;
253                }
254
255                // When encountering a suspended thunk, request that the VM enters
256                // it and produces the result.
257                ThunkRepr::Suspended {
258                    lambda,
259                    upvalues,
260                    span,
261                } => {
262                    // TODO(amjoseph): use #[tailcall::mutual] here.  This can
263                    // be turned into a tailcall to vm::execute_bytecode() by
264                    // passing `also_update` to it.
265                    let value = generators::request_enter_lambda(co, lambda, upvalues, span).await;
266                    myself.0.replace(ThunkRepr::Evaluated(value));
267                    continue;
268                }
269
270                // nested thunks -- try to flatten before forcing
271                ThunkRepr::Evaluated(Value::Thunk(inner_thunk)) => {
272                    match Rc::try_unwrap(inner_thunk.0) {
273                        Ok(refcell) => {
274                            // we are the only reference to the inner thunk,
275                            // so steal it
276                            myself.0.replace(refcell.into_inner());
277                            continue;
278                        }
279                        Err(rc) => {
280                            let inner_thunk = Thunk(rc);
281                            if inner_thunk.is_forced() {
282                                // tail call to force the inner thunk; note that
283                                // this means the outer thunk remains unforced
284                                // even after calling force() on it; however the
285                                // next time it is forced we will be one
286                                // thunk-forcing closer to it being
287                                // fully-evaluated.
288                                myself
289                                    .0
290                                    .replace(ThunkRepr::Evaluated(inner_thunk.value().clone()));
291                                continue;
292                            }
293                            also_update.push(myself.0.clone());
294                            myself = inner_thunk;
295                            continue;
296                        }
297                    }
298                }
299
300                ThunkRepr::Evaluated(val) => {
301                    return Ok(val);
302                }
303            }
304        }
305    }
306
307    pub fn finalise(&self, stack: &[Value]) {
308        self.upvalues_mut().resolve_deferred_upvalues(stack);
309    }
310
311    pub fn is_evaluated(&self) -> bool {
312        matches!(*self.0.borrow(), ThunkRepr::Evaluated(_))
313    }
314
315    pub fn is_suspended(&self) -> bool {
316        matches!(
317            *self.0.borrow(),
318            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_)
319        )
320    }
321
322    /// Returns true if forcing this thunk will not change it.
323    pub fn is_forced(&self) -> bool {
324        self.0.borrow().is_forced()
325    }
326
327    /// Returns a reference to the inner evaluated value of a thunk.
328    /// It is an error to call this on a thunk that has not been
329    /// forced, or is not otherwise known to be fully evaluated.
330    // Note: Due to the interior mutability of thunks this is
331    // difficult to represent in the type system without impacting the
332    // API too much.
333    pub fn value(&self) -> Ref<'_, Value> {
334        Ref::map(self.0.borrow(), |thunk| match thunk {
335            ThunkRepr::Evaluated(value) => value,
336            ThunkRepr::Blackhole { .. } => panic!("Thunk::value called on a black-holed thunk"),
337            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
338                panic!("Thunk::value called on a suspended thunk")
339            }
340        })
341    }
342
343    /// Returns the inner evaluated value of a thunk, cloning it if
344    /// the Rc has more than one strong reference.  It is an error
345    /// to call this on a thunk that has not been forced, or is not
346    /// otherwise known to be fully evaluated.
347    pub fn unwrap_or_clone(self) -> Value {
348        match Rc::try_unwrap(self.0) {
349            Ok(refcell) => refcell.into_inner().expect(),
350            Err(rc) => Ref::map(rc.borrow(), |thunkrepr| thunkrepr.expect_ref()).clone(),
351        }
352    }
353
354    pub fn upvalues(&self) -> Ref<'_, Upvalues> {
355        Ref::map(self.0.borrow(), |thunk| match thunk {
356            ThunkRepr::Suspended { upvalues, .. } => upvalues.as_ref(),
357            ThunkRepr::Evaluated(Value::Closure(c)) => &c.upvalues,
358            _ => panic!("upvalues() on non-suspended thunk"),
359        })
360    }
361
362    pub fn upvalues_mut(&self) -> RefMut<'_, Upvalues> {
363        RefMut::map(self.0.borrow_mut(), |thunk| match thunk {
364            ThunkRepr::Suspended { upvalues, .. } => Rc::get_mut(upvalues).unwrap(),
365            ThunkRepr::Evaluated(Value::Closure(c)) => Rc::get_mut(
366                &mut Rc::get_mut(c).unwrap().upvalues,
367            )
368            .expect(
369                "upvalues_mut() was called on a thunk which already had multiple references to it",
370            ),
371            thunk => panic!("upvalues() on non-suspended thunk: {thunk:?}"),
372        })
373    }
374
375    /// Do not use this without first reading and understanding
376    /// `tvix/docs/value-pointer-equality.md`.
377    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
378        if Rc::ptr_eq(&self.0, &other.0) {
379            return true;
380        }
381        match &*self.0.borrow() {
382            ThunkRepr::Evaluated(Value::Closure(c1)) => match &*other.0.borrow() {
383                ThunkRepr::Evaluated(Value::Closure(c2)) => Rc::ptr_eq(c1, c2),
384                _ => false,
385            },
386            _ => false,
387        }
388    }
389
390    /// Helper function to format thunks in observer output.
391    pub(crate) fn debug_repr(&self) -> String {
392        self.0.borrow().debug_repr()
393    }
394}
395
396impl TotalDisplay for Thunk {
397    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result {
398        if !set.insert(self) {
399            return f.write_str("<CYCLE>");
400        }
401
402        match &*self.0.borrow() {
403            ThunkRepr::Evaluated(v) => v.total_fmt(f, set),
404            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => f.write_str("<CODE>"),
405            other => write!(f, "internal[{}]", other.debug_repr()),
406        }
407    }
408}
409
410/// A wrapper type for tracking which thunks have already been seen
411/// in a context. This is necessary for printing and deeply forcing
412/// cyclic non-diverging data structures like `rec { f = [ f ]; }`.
413/// This is separate from the ThunkRepr::Blackhole mechanism, which
414/// detects diverging data structures like `(rec { f = f; }).f`.
415///
416/// The inner `HashSet` is not available on the outside, as it would be
417/// potentially unsafe to interact with the pointers in the set.
418#[derive(Default)]
419pub struct ThunkSet(FxHashSet<*const ThunkRepr>);
420
421impl ThunkSet {
422    /// Check whether the given thunk has already been seen. Will mark the thunk
423    /// as seen otherwise.
424    pub fn insert(&mut self, thunk: &Thunk) -> bool {
425        let ptr: *const ThunkRepr = thunk.0.as_ptr();
426        self.0.insert(ptr)
427    }
428}