1
1
mirror of https://github.com/tweag/nickel.git synced 2024-11-10 10:46:49 +03:00

Merge pull request #276 from tweag/refactor/thunks

Thunks black-holing
This commit is contained in:
Yann Hamdaoui 2021-02-05 15:49:17 +00:00 committed by GitHub
commit a832ffed90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 269 additions and 84 deletions

View File

@ -68,6 +68,8 @@ pub enum EvalError {
),
/// An unbound identifier was referenced.
UnboundIdentifier(Ident, Option<RawSpan>),
/// A thunk was entered during its own update.
InfiniteRecursion(CallStack, Option<RawSpan>),
/// An unexpected internal error.
InternalError(String, Option<RawSpan>),
/// Errors occurring rarely enough to not deserve a dedicated variant.
@ -979,6 +981,16 @@ impl ToDiagnostic<FileId> for EvalError {
.with_message("Unbound identifier")
.with_labels(vec![primary_alt(span_opt, ident.clone(), files)
.with_message("this identifier is unbound")])],
EvalError::InfiniteRecursion(_call_stack, span_opt) => {
let labels = span_opt
.as_ref()
.map(|span| vec![primary(span).with_message("recursive reference")])
.unwrap_or_else(Vec::new);
vec![Diagnostic::error()
.with_message("infinite recursion")
.with_labels(labels)]
}
EvalError::Other(msg, span_opt) => {
let labels = span_opt
.as_ref()

View File

@ -95,12 +95,167 @@ use crate::operation::{continuate_operation, OperationCont};
use crate::position::RawSpan;
use crate::stack::Stack;
use crate::term::{make as mk_term, MetaValue, RichTerm, StrChunk, Term, UnaryOp};
use std::cell::RefCell;
use std::cell::{Ref, RefCell, RefMut};
use std::collections::HashMap;
use std::rc::{Rc, Weak};
/// The state of a thunk.
///
/// When created, a thunk is flagged as suspended. When accessed for the first time, a corresponding
/// [`ThunkUpdateFrame`](./struct.ThunkUpdateFrame.html) is pushed on the stack and the thunk is
/// flagged as black-hole. This prevents direct infinite recursions, since if a thunk is
/// re-accessed while still in a black-hole state, we are sure that the evaluation will loop, and
/// we can thus error out before overflowing the stack or looping forever. Finally, once the
/// content of a thunk has been evaluated, the thunk is updated with the new value and flagged as
/// evaluated, so that future accesses won't even push an update frame on the stack.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ThunkState {
Blackholed,
Suspended,
Evaluated,
}
/// The mutable data stored inside a thunk.
#[derive(Clone, Debug, PartialEq)]
pub struct ThunkData {
closure: Closure,
state: ThunkState,
}
impl ThunkData {
pub fn new(closure: Closure) -> Self {
ThunkData {
closure,
state: ThunkState::Suspended,
}
}
}
/// A thunk.
///
/// A thunk is a shared suspended computation. It is the primary device for the implementation of
/// lazy evaluation.
#[derive(Clone, Debug, PartialEq)]
pub struct Thunk {
data: Rc<RefCell<ThunkData>>,
ident_kind: IdentKind,
}
/// A black-holed thunk was accessed, which would lead to infinite recursion.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BlackholedError;
impl Thunk {
pub fn new(closure: Closure, ident_kind: IdentKind) -> Self {
Thunk {
data: Rc::new(RefCell::new(ThunkData::new(closure))),
ident_kind,
}
}
pub fn state(&self) -> ThunkState {
self.data.borrow().state
}
/// Set the state to evaluated.
pub fn set_evaluated(&mut self) {
self.data.borrow_mut().state = ThunkState::Evaluated;
}
/// Generate an update frame from this thunk and set the state to `Blackholed`. Return an
/// error if the thunk was already black-holed.
pub fn to_update_frame(&mut self) -> Result<ThunkUpdateFrame, BlackholedError> {
if self.data.borrow().state == ThunkState::Blackholed {
return Err(BlackholedError);
}
self.data.borrow_mut().state = ThunkState::Blackholed;
Ok(ThunkUpdateFrame {
data: Rc::downgrade(&self.data),
ident_kind: self.ident_kind,
})
}
/// Immutably borrow the inner closure. Panic if there is another active mutable borrow.
pub fn borrow(&self) -> Ref<'_, Closure> {
let (closure, _) = Ref::map_split(self.data.borrow(), |data| {
let ThunkData {
ref closure,
ref state,
} = data;
(closure, state)
});
closure
}
/// Mutably borrow the inner closure. Panic if there is any other active borrow.
pub fn borrow_mut<'a>(&'a mut self) -> RefMut<'_, Closure> {
let (closure, _) = RefMut::map_split(self.data.borrow_mut(), |data| {
let ThunkData {
ref mut closure,
ref mut state,
} = data;
(closure, state)
});
closure
}
/// Get an owned clone of the inner closure.
pub fn get_owned(&self) -> Closure {
self.data.borrow().closure.clone()
}
pub fn ident_kind(&self) -> IdentKind {
self.ident_kind
}
/// Consume the thunk and return an owned closure. Avoid cloning if this thunk is the only
/// reference to the inner closure.
pub fn into_closure(self) -> Closure {
match Rc::try_unwrap(self.data) {
Ok(inner) => inner.into_inner().closure,
Err(rc) => rc.borrow().clone().closure,
}
}
}
/// A thunk update frame.
///
/// A thunk update frame is put on the stack whenever a variable is entered, such that once this
/// variable is evaluated, the corresponding thunk can be updated. It is similar to a thunk but it
/// holds a weak reference to the inner closure, to avoid unnecessarily keeping the underlying
/// closure alive.
#[derive(Clone, Debug)]
pub struct ThunkUpdateFrame {
data: Weak<RefCell<ThunkData>>,
ident_kind: IdentKind,
}
impl ThunkUpdateFrame {
/// Update the corresponding thunk with a closure. Set the state to `Evaluated`
///
/// # Return
///
/// - `true` if the thunk was successfully updated
/// - `false` if the corresponding closure has been dropped since
pub fn update(self, closure: Closure) -> bool {
if let Some(data) = Weak::upgrade(&self.data) {
*data.borrow_mut() = ThunkData {
closure,
state: ThunkState::Evaluated,
};
true
} else {
false
}
}
}
/// An environment, which is a mapping from identifiers to closures.
pub type Environment = HashMap<Ident, (Rc<RefCell<Closure>>, IdentKind)>;
pub type Environment = HashMap<Ident, Thunk>;
/// A call stack, saving the history of function calls.
///
@ -118,7 +273,7 @@ pub enum StackElem {
}
/// Kind of an identifier.
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum IdentKind {
Let(),
Lam(),
@ -154,11 +309,10 @@ pub fn env_add_term(env: &mut Environment, rt: RichTerm) -> Result<(), EnvBuildE
match *term {
Term::Record(bindings) | Term::RecRecord(bindings) => {
let ext = bindings.into_iter().map(|(id, t)| {
let closure = Closure {
body: t,
env: HashMap::new(),
};
(id, (Rc::new(RefCell::new(closure)), IdentKind::Record()))
(
id,
Thunk::new(Closure::atomic_closure(t), IdentKind::Record()),
)
});
env.extend(ext);
@ -174,7 +328,7 @@ pub fn env_add(env: &mut Environment, id: Ident, rt: RichTerm, local_env: Enviro
body: rt,
env: local_env,
};
env.insert(id, (Rc::new(RefCell::new(closure)), IdentKind::Let()));
env.insert(id, Thunk::new(closure, IdentKind::Let()));
}
/// Determine if a thunk is worth being put on the stack for future update.
@ -238,13 +392,12 @@ where
let thunk = env
.get(&id)
.or_else(|| global_env.get(&id))
.map(|(rc, _)| rc.clone())
.expect("eval::eval_meta(): unexpected unbound identifier");
let Closure {
body,
env: local_env,
} = thunk.borrow().clone();
} = thunk.get_owned();
t = body;
env = local_env;
@ -304,29 +457,29 @@ where
let term = *boxed_term;
clos = match term {
Term::Var(x) => {
let (thunk, id_kind) = env
let mut thunk = env
.remove(&x)
.or_else(|| {
global_env
.get(&x)
.map(|(rc, id_kind)| (rc.clone(), id_kind.clone()))
})
.or_else(|| global_env.get(&x).map(Thunk::clone))
.ok_or(EvalError::UnboundIdentifier(x.clone(), pos.clone()))?;
std::mem::drop(env); // thunk may be a 1RC pointer
if should_update(&thunk.borrow().body.term) {
stack.push_thunk(Rc::downgrade(&thunk));
}
call_stack.push(StackElem::Var(id_kind, x, pos));
match Rc::try_unwrap(thunk) {
Ok(c) => {
// thunk was the only strong ref to the closure
c.into_inner()
if thunk.state() != ThunkState::Evaluated {
if should_update(&thunk.borrow().body.term) {
match thunk.to_update_frame() {
Ok(thunk_upd) => stack.push_thunk(thunk_upd),
Err(BlackholedError) => {
return Err(EvalError::InfiniteRecursion(call_stack, pos))
}
}
}
Err(rc) => {
// We need to clone it, there are other strong refs
rc.borrow().clone()
// If the thunk isn't to be updated, directly set the evaluated flag.
else {
thunk.set_evaluated();
}
}
call_stack.push(StackElem::Var(thunk.ident_kind(), x, pos));
thunk.into_closure()
}
Term::App(t1, t2) => {
stack.push_arg(
@ -339,11 +492,11 @@ where
Closure { body: t1, env }
}
Term::Let(x, s, t) => {
let thunk = Rc::new(RefCell::new(Closure {
let closure = Closure {
body: s,
env: env.clone(),
}));
env.insert(x, (Rc::clone(&thunk), IdentKind::Let()));
};
env.insert(x, Thunk::new(closure, IdentKind::Let()));
Closure { body: t, env }
}
Term::Switch(exp, cases, default) => {
@ -441,24 +594,22 @@ where
ts.iter()
.try_fold(HashMap::new(), |mut rec_env, (id, rt)| match rt.as_ref() {
&Term::Var(ref var_id) => {
let (thunk, id_kind) = env.get(var_id).ok_or(
EvalError::UnboundIdentifier(var_id.clone(), rt.pos.clone()),
)?;
rec_env.insert(id.clone(), (thunk.clone(), id_kind.clone()));
let thunk = env.get(var_id).ok_or(EvalError::UnboundIdentifier(
var_id.clone(),
rt.pos.clone(),
))?;
rec_env.insert(id.clone(), thunk.clone());
Ok(rec_env)
}
_ => {
// If we are in this branch, the term must be a constant after the
// share normal form transformation, hence it should not need an
// environment, which is it is dropped.
// environment, which is why it is dropped.
let closure = Closure {
body: rt.clone(),
env: HashMap::new(),
};
rec_env.insert(
id.clone(),
(Rc::new(RefCell::new(closure)), IdentKind::Let()),
);
rec_env.insert(id.clone(), Thunk::new(closure, IdentKind::Let()));
Ok(rec_env)
}
})?;
@ -469,7 +620,7 @@ where
Term::Var(var_id) => {
// We already checked for unbound identifier in the previous fold, so this
// get should always succeed.
let (thunk, _) = env.get(&var_id).unwrap();
let thunk = env.get_mut(&var_id).unwrap();
thunk.borrow_mut().env.extend(rec_env.clone());
(
id,
@ -577,8 +728,7 @@ where
if 0 < stack.count_args() {
let (arg, pos_app) = stack.pop_arg().expect("Condition already checked.");
call_stack.push(StackElem::App(pos_app));
let thunk = Rc::new(RefCell::new(arg));
env.insert(x, (thunk, IdentKind::Lam()));
env.insert(x, Thunk::new(arg, IdentKind::Lam()));
Closure { body: t, env }
} else {
return Ok((Term::Fun(x, t), env));
@ -607,9 +757,7 @@ where
/// Pop and update all the thunks on the top of the stack with the given closure.
fn update_thunks(stack: &mut Stack, closure: &Closure) {
while let Some(thunk) = stack.pop_thunk() {
if let Some(safe_thunk) = Weak::upgrade(&thunk) {
*safe_thunk.borrow_mut() = closure.clone();
}
thunk.update(closure.clone());
}
}
@ -633,8 +781,8 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
Term::Var(id) if !bound.as_ref().contains(&id) => env
.get(&id)
.or_else(|| global_env.get(&id))
.map(|(rc, _)| {
let closure = rc.borrow().clone();
.map(|thunk| {
let closure = thunk.get_owned();
subst_(closure.body, global_env, &closure.env, bound)
})
.unwrap_or_else(|| RichTerm::new(Term::Var(id), pos)),
@ -1109,8 +1257,13 @@ mod tests {
fn global_env() {
let mut global_env = HashMap::new();
let mut resolver = DummyResolver {};
let thunk = Rc::new(RefCell::new(Closure::atomic_closure(Term::Num(1.0).into())));
global_env.insert(Ident::from("g"), (Rc::clone(&thunk), IdentKind::Let()));
global_env.insert(
Ident::from("g"),
Thunk::new(
Closure::atomic_closure(Term::Num(1.0).into()),
IdentKind::Let(),
),
);
let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("x"));
assert_eq!(eval(t, &global_env, &mut resolver), Ok(Term::Num(2.0)));
@ -1129,10 +1282,7 @@ mod tests {
.map(|(id, t)| {
(
id.into(),
(
Rc::new(RefCell::new(Closure::atomic_closure(t))),
IdentKind::Let(),
),
Thunk::new(Closure::atomic_closure(t), IdentKind::Let()),
)
})
.collect()

View File

@ -149,9 +149,6 @@ pub fn query(
global_env: &eval::Environment,
path: Option<String>,
) -> Result<Term, Error> {
use std::cell::RefCell;
use std::rc::Rc;
cache.prepare(file_id, global_env)?;
let t = if let Some(p) = path {
@ -166,15 +163,12 @@ pub fn query(
// Substituting `y` for `t`
let mut env = eval::Environment::new();
let closure = eval::Closure {
body: cache.get_owned(file_id).unwrap(),
env: eval::Environment::new(),
};
env.insert(
eval::env_add(
&mut env,
Ident::from("y"),
(Rc::new(RefCell::new(closure)), eval::IdentKind::Let()),
cache.get_owned(file_id).unwrap(),
eval::Environment::new(),
);
//TODO: why passing an empty global environment?
eval::subst(new_term, &eval::Environment::new(), &env)
} else {
@ -1517,4 +1511,24 @@ too
// that this test fails.
eval_string_full("{y = fun x => x; x = fun y => y}").unwrap();
}
#[test]
fn infinite_loops() {
assert_matches!(
eval_string("{x = x}.x"),
Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
);
assert_matches!(
eval_string("{x = y; y = z; z = x }.x"),
Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
);
assert_matches!(
eval_string("{x = y + z; y = z + x; z = 1}.x"),
Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
);
assert_matches!(
eval_string("{x = (fun a => a + y) 0; y = (fun a => a + x) 0}.x"),
Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
);
}
}

View File

@ -1,12 +1,10 @@
//! Define the main evaluation stack of the Nickel abstract machine and related operations.
//!
//! See [eval](../eval/index.html).
use crate::eval::{Closure, Environment};
use crate::eval::{Closure, Environment, ThunkUpdateFrame};
use crate::operation::OperationCont;
use crate::position::RawSpan;
use crate::term::{RichTerm, StrChunk};
use std::cell::RefCell;
use std::rc::Weak;
/// An element of the stack.
#[derive(Debug)]
@ -22,7 +20,7 @@ pub enum Marker {
/// An argument of an application.
Arg(Closure, Option<RawSpan>),
/// A thunk, which is pointer to a mutable memory cell to be updated.
Thunk(Weak<RefCell<Closure>>),
Thunk(ThunkUpdateFrame),
/// The continuation of a primitive operation.
Cont(
OperationCont,
@ -132,7 +130,7 @@ impl Stack {
self.0.push(Marker::Arg(arg, pos))
}
pub fn push_thunk(&mut self, thunk: Weak<RefCell<Closure>>) {
pub fn push_thunk(&mut self, thunk: ThunkUpdateFrame) {
self.0.push(Marker::Thunk(thunk))
}
@ -176,7 +174,7 @@ impl Stack {
/// Try to pop a thunk from the top of the stack. If `None` is returned, the top element was
/// not a thunk and the stack is left unchanged.
pub fn pop_thunk(&mut self) -> Option<Weak<RefCell<Closure>>> {
pub fn pop_thunk(&mut self) -> Option<ThunkUpdateFrame> {
match self.0.pop() {
Some(Marker::Thunk(thunk)) => Some(thunk),
Some(m) => {
@ -239,7 +237,7 @@ impl Stack {
}
}
/// Check if the top element is an argument.
/// Check if the top element is a thunk.
pub fn is_top_thunk(&self) -> bool {
self.0.last().map(Marker::is_thunk).unwrap_or(false)
}
@ -259,8 +257,9 @@ impl Stack {
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::{IdentKind, Thunk};
use crate::term::{Term, UnaryOp};
use std::rc::Rc;
use assert_matches::assert_matches;
impl Stack {
/// Count the number of thunks at the top of the stack.
@ -287,8 +286,8 @@ mod tests {
}
fn some_thunk_marker() -> Marker {
let rc = Rc::new(RefCell::new(some_closure()));
Marker::Thunk(Rc::downgrade(&rc))
let mut thunk = Thunk::new(some_closure(), IdentKind::Let());
Marker::Thunk(thunk.to_update_frame().unwrap())
}
fn some_cont_marker() -> Marker {
@ -319,13 +318,26 @@ mod tests {
let mut s = Stack::new();
assert_eq!(0, s.count_thunks());
s.push_thunk(Rc::downgrade(&Rc::new(RefCell::new(some_closure()))));
s.push_thunk(Rc::downgrade(&Rc::new(RefCell::new(some_closure()))));
let mut thunk = Thunk::new(some_closure(), IdentKind::Let());
s.push_thunk(thunk.to_update_frame().unwrap());
thunk = Thunk::new(some_closure(), IdentKind::Let());
s.push_thunk(thunk.to_update_frame().unwrap());
assert_eq!(2, s.count_thunks());
s.pop_thunk().expect("Already checked");
assert_eq!(1, s.count_thunks());
}
#[test]
fn thunk_blackhole() {
let mut thunk = Thunk::new(some_closure(), IdentKind::Let());
let thunk_upd = thunk.to_update_frame();
assert_matches!(thunk_upd, Ok(..));
assert_matches!(thunk.to_update_frame(), Err(..));
thunk_upd.unwrap().update(some_closure());
assert_matches!(thunk.to_update_frame(), Ok(..));
}
#[test]
fn pushing_and_poping_conts() {
let mut s = Stack::new();

View File

@ -2,15 +2,13 @@
use crate::cache::ImportResolver;
use crate::error::ImportError;
use crate::eval::{Closure, Environment, IdentKind};
use crate::eval::{Closure, Environment, IdentKind, Thunk};
use crate::identifier::Ident;
use crate::term::{RichTerm, Term};
use crate::types::{AbsType, Types};
use codespan::FileId;
use simple_counter::*;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
generate_counter!(FreshVarCounter, usize);
@ -321,12 +319,11 @@ impl Closurizable for RichTerm {
/// and return this variable as a fresh term.
fn closurize(self, env: &mut Environment, with_env: Environment) -> RichTerm {
let var = fresh_var();
let c = Closure {
let closure = Closure {
body: self,
env: with_env,
};
env.insert(var.clone(), (Rc::new(RefCell::new(c)), IdentKind::Record()));
env.insert(var.clone(), Thunk::new(closure, IdentKind::Record()));
Term::Var(var).into()
}

View File

@ -359,10 +359,10 @@ impl<'a> Envs<'a> {
pub fn mk_global(eval_env: &eval::Environment) -> Environment {
eval_env
.iter()
.map(|(id, (rc, _))| {
.map(|(id, thunk)| {
(
id.clone(),
to_typewrapper(apparent_type(rc.borrow().body.as_ref(), None).into()),
to_typewrapper(apparent_type(thunk.borrow().body.as_ref(), None).into()),
)
})
.collect()