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}