refactor(tvix/eval): non-recursive thunk forcing
Introduces continuation-passing-based trampolining of thunk forcing to avoid recursing when forcing deeply nested expressions. This is required for evaluating large expressions. This change was extracted out of cl/7362. Co-authored-by: Vincent Ambo <tazjin@tvl.su> Co-authored-by: Griffin Smith <grfn@gws.fyi> Change-Id: Ifc1747e712663684b2fff53095de62b8459a47f3 Reviewed-on: https://cl.tvl.fyi/c/depot/+/7551 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI Reviewed-by: tazjin <tazjin@tvl.su>
This commit is contained in:
parent
4cda236c0c
commit
67d508f2ec
2 changed files with 244 additions and 68 deletions
|
|
@ -30,7 +30,7 @@ use crate::{
|
|||
spans::LightSpan,
|
||||
upvalues::Upvalues,
|
||||
value::{Builtin, Closure},
|
||||
vm::VM,
|
||||
vm::{Trampoline, TrampolineAction, VM},
|
||||
Value,
|
||||
};
|
||||
|
||||
|
|
@ -84,10 +84,6 @@ impl Thunk {
|
|||
})))
|
||||
}
|
||||
|
||||
/// Create a new thunk from suspended Rust code.
|
||||
///
|
||||
/// The suspended code will be executed and expected to return a
|
||||
/// value whenever the thunk is forced like any other thunk.
|
||||
pub fn new_suspended_native(
|
||||
native: Rc<Box<dyn Fn(&mut VM) -> Result<Value, ErrorKind>>>,
|
||||
) -> Self {
|
||||
|
|
@ -103,7 +99,7 @@ impl Thunk {
|
|||
None,
|
||||
move |v: Vec<Value>, vm: &mut VM| {
|
||||
// sanity check that only the dummy argument was popped
|
||||
assert_eq!(v.len(), 1);
|
||||
assert!(v.len() == 1);
|
||||
assert!(matches!(v[0], Value::Null));
|
||||
native(vm)
|
||||
},
|
||||
|
|
@ -127,41 +123,103 @@ impl Thunk {
|
|||
})))
|
||||
}
|
||||
|
||||
/// Force a thunk from a context that can't handle trampoline
|
||||
/// continuations, eg outside the VM's normal execution loop. Calling
|
||||
/// `force_trampoline()` instead should be preferred whenever possible.
|
||||
pub fn force(&self, vm: &mut VM) -> Result<(), ErrorKind> {
|
||||
if self.is_forced() {
|
||||
return Ok(());
|
||||
}
|
||||
vm.push(Value::Thunk(self.clone()));
|
||||
let mut trampoline = Self::force_trampoline(vm)?;
|
||||
loop {
|
||||
match trampoline.action {
|
||||
None => (),
|
||||
Some(TrampolineAction::EnterFrame {
|
||||
lambda,
|
||||
upvalues,
|
||||
arg_count,
|
||||
light_span: _,
|
||||
}) => vm.enter_frame(lambda, upvalues, arg_count)?,
|
||||
}
|
||||
match trampoline.continuation {
|
||||
None => break (),
|
||||
Some(cont) => {
|
||||
trampoline = cont(vm)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
vm.pop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Evaluate the content of a thunk, potentially repeatedly, until a
|
||||
/// non-thunk value is returned.
|
||||
///
|
||||
/// This will change the existing thunk (and thus all references to it,
|
||||
/// providing memoization) through interior mutability. In case of nested
|
||||
/// thunks, the intermediate thunk representations are replaced.
|
||||
pub fn force(&self, vm: &mut VM) -> Result<(), ErrorKind> {
|
||||
///
|
||||
/// The thunk to be forced should be at the top of the VM stack,
|
||||
/// and will be left there (but possibly partially forced) when
|
||||
/// this function returns.
|
||||
pub fn force_trampoline(vm: &mut VM) -> Result<Trampoline, ErrorKind> {
|
||||
match vm.pop() {
|
||||
Value::Thunk(thunk) => thunk.force_trampoline_self(vm),
|
||||
v => {
|
||||
vm.push(v);
|
||||
Ok(Trampoline::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn force_trampoline_self(&self, vm: &mut VM) -> Result<Trampoline, ErrorKind> {
|
||||
loop {
|
||||
let mut thunk_mut = self.0.borrow_mut();
|
||||
if !self.is_suspended() {
|
||||
let thunk = self.0.borrow();
|
||||
match *thunk {
|
||||
ThunkRepr::Evaluated(Value::Thunk(ref inner_thunk)) => {
|
||||
let inner_repr = inner_thunk.0.borrow().clone();
|
||||
drop(thunk);
|
||||
self.0.replace(inner_repr);
|
||||
}
|
||||
|
||||
match *thunk_mut {
|
||||
ThunkRepr::Evaluated(Value::Thunk(ref inner_thunk)) => {
|
||||
let inner_repr = inner_thunk.0.borrow().clone();
|
||||
*thunk_mut = inner_repr;
|
||||
ThunkRepr::Evaluated(ref v) => {
|
||||
vm.push(v.clone());
|
||||
return Ok(Trampoline::default());
|
||||
}
|
||||
ThunkRepr::Blackhole => return Err(ErrorKind::InfiniteRecursion),
|
||||
_ => panic!("impossible"),
|
||||
}
|
||||
|
||||
ThunkRepr::Evaluated(_) => return Ok(()),
|
||||
ThunkRepr::Blackhole => return Err(ErrorKind::InfiniteRecursion),
|
||||
|
||||
ThunkRepr::Suspended { .. } => {
|
||||
if let ThunkRepr::Suspended {
|
||||
} else {
|
||||
match self.0.replace(ThunkRepr::Blackhole) {
|
||||
ThunkRepr::Suspended {
|
||||
lambda,
|
||||
upvalues,
|
||||
light_span,
|
||||
} = std::mem::replace(&mut *thunk_mut, ThunkRepr::Blackhole)
|
||||
{
|
||||
drop(thunk_mut);
|
||||
vm.enter_frame(lambda, upvalues, 0).map_err(|e| {
|
||||
ErrorKind::ThunkForce(Box::new(Error {
|
||||
span: light_span.span(),
|
||||
..e
|
||||
}))
|
||||
})?;
|
||||
(*self.0.borrow_mut()) = ThunkRepr::Evaluated(vm.pop())
|
||||
} => {
|
||||
let self_clone = self.clone();
|
||||
return Ok(Trampoline {
|
||||
action: Some(TrampolineAction::EnterFrame {
|
||||
lambda,
|
||||
upvalues: upvalues.clone(),
|
||||
arg_count: 0,
|
||||
light_span: light_span.clone(),
|
||||
}),
|
||||
continuation: Some(Box::new(move |vm| {
|
||||
let should_be_blackhole =
|
||||
self_clone.0.replace(ThunkRepr::Evaluated(vm.pop()));
|
||||
assert!(matches!(should_be_blackhole, ThunkRepr::Blackhole));
|
||||
vm.push(Value::Thunk(self_clone));
|
||||
return Self::force_trampoline(vm).map_err(|kind| Error {
|
||||
kind,
|
||||
span: light_span.span(),
|
||||
});
|
||||
})),
|
||||
});
|
||||
}
|
||||
_ => panic!("impossible"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +233,20 @@ impl Thunk {
|
|||
matches!(*self.0.borrow(), ThunkRepr::Evaluated(_))
|
||||
}
|
||||
|
||||
pub fn is_suspended(&self) -> bool {
|
||||
matches!(*self.0.borrow(), ThunkRepr::Suspended { .. })
|
||||
}
|
||||
|
||||
/// Returns true if forcing this thunk will not change it.
|
||||
pub fn is_forced(&self) -> bool {
|
||||
match *self.0.borrow() {
|
||||
ThunkRepr::Blackhole => panic!("is_forced() called on a blackholed thunk"),
|
||||
ThunkRepr::Evaluated(Value::Thunk(_)) => false,
|
||||
ThunkRepr::Evaluated(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner evaluated value of a thunk.
|
||||
/// It is an error to call this on a thunk that has not been
|
||||
/// forced, or is not otherwise known to be fully evaluated.
|
||||
|
|
@ -183,7 +255,21 @@ impl Thunk {
|
|||
// API too much.
|
||||
pub fn value(&self) -> Ref<Value> {
|
||||
Ref::map(self.0.borrow(), |thunk| match thunk {
|
||||
ThunkRepr::Evaluated(value) => value,
|
||||
ThunkRepr::Evaluated(value) => {
|
||||
/*
|
||||
#[cfg(debug_assertions)]
|
||||
if matches!(
|
||||
value,
|
||||
Value::Closure(Closure {
|
||||
is_finalised: false,
|
||||
..
|
||||
})
|
||||
) {
|
||||
panic!("Thunk::value called on an unfinalised closure");
|
||||
}
|
||||
*/
|
||||
return value;
|
||||
}
|
||||
ThunkRepr::Blackhole => panic!("Thunk::value called on a black-holed thunk"),
|
||||
ThunkRepr::Suspended { .. } => panic!("Thunk::value called on a suspended thunk"),
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue