mirror of
https://github.com/tweag/nickel.git
synced 2024-09-11 11:47:03 +03:00
Merge pull request #551 from tweag/refactor/eval
[Refactor] Break eval into submodules
This commit is contained in:
commit
00819963fd
@ -9,7 +9,7 @@ use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle};
|
||||
use lalrpop_util::ErrorRecovery;
|
||||
|
||||
use crate::{
|
||||
eval::CallStack,
|
||||
eval::callstack::CallStack,
|
||||
identifier::Ident,
|
||||
label,
|
||||
label::ty_path,
|
||||
|
234
src/eval/callstack.rs
Normal file
234
src/eval/callstack.rs
Normal file
@ -0,0 +1,234 @@
|
||||
//! In a lazy language like Nickel, there are no well delimited stack frames due to how function
|
||||
//! application is evaluated. Additional information about the history of function calls is thus
|
||||
//! stored in a call stack solely for better error reporting.
|
||||
use super::IdentKind;
|
||||
use crate::{
|
||||
identifier::Ident,
|
||||
position::{RawSpan, TermPos},
|
||||
};
|
||||
use codespan::FileId;
|
||||
|
||||
/// A call stack, saving the history of function calls.
|
||||
#[derive(PartialEq, Clone, Default, Debug)]
|
||||
pub struct CallStack(pub Vec<StackElem>);
|
||||
|
||||
/// Basic description of a function call. Used for error reporting.
|
||||
pub struct CallDescr {
|
||||
/// The name of the called function, if any.
|
||||
pub head: Option<Ident>,
|
||||
/// The position of the application.
|
||||
pub span: RawSpan,
|
||||
}
|
||||
|
||||
/// A call stack element.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum StackElem {
|
||||
/// A function body was entered. The position is the position of the original application.
|
||||
Fun(TermPos),
|
||||
/// An application was evaluated.
|
||||
App(TermPos),
|
||||
/// A variable was entered.
|
||||
Var {
|
||||
kind: IdentKind,
|
||||
id: Ident,
|
||||
pos: TermPos,
|
||||
},
|
||||
/// A record field was entered.
|
||||
Field {
|
||||
id: Ident,
|
||||
pos_record: TermPos,
|
||||
pos_field: TermPos,
|
||||
pos_access: TermPos,
|
||||
},
|
||||
}
|
||||
|
||||
impl CallStack {
|
||||
pub fn new() -> Self {
|
||||
CallStack(Vec::new())
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that a var was entered.
|
||||
pub fn enter_var(&mut self, kind: IdentKind, id: Ident, pos: TermPos) {
|
||||
self.0.push(StackElem::Var { kind, id, pos });
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that an application was entered.
|
||||
pub fn enter_app(&mut self, pos: TermPos) {
|
||||
// We ignore application without positions, which have been generated by the interpreter.
|
||||
if pos.is_def() {
|
||||
self.0.push(StackElem::App(pos));
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that during the evaluation an application, the function part was
|
||||
/// finally evaluated to an expression of the form `fun x => body`, and that the body of this
|
||||
/// function was entered.
|
||||
pub fn enter_fun(&mut self, pos: TermPos) {
|
||||
// We ignore application without positions, which have been generated by the interpreter.
|
||||
if pos.is_def() {
|
||||
self.0.push(StackElem::Fun(pos));
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that a record field was entered.
|
||||
pub fn enter_field(
|
||||
&mut self,
|
||||
id: Ident,
|
||||
pos_record: TermPos,
|
||||
pos_field: TermPos,
|
||||
pos_access: TermPos,
|
||||
) {
|
||||
self.0.push(StackElem::Field {
|
||||
id,
|
||||
pos_record,
|
||||
pos_field,
|
||||
pos_access,
|
||||
});
|
||||
}
|
||||
|
||||
/// Process a raw callstack by aggregating elements belonging to the same call. Return a list
|
||||
/// of call descriptions from the most nested/recent to the least nested/recent, together with
|
||||
/// the last pending call, if any.
|
||||
///
|
||||
/// Recall that when a call `f arg` is evaluated, the following events happen:
|
||||
/// 1. `arg` is pushed on the evaluation stack.
|
||||
/// 2. `f` is evaluated.
|
||||
/// 3. Hopefully, the result of this evaluation is a function `Func(id, body)`. `arg` is popped
|
||||
/// from the stack, bound to `id` in the environment, and `body is entered`.
|
||||
///
|
||||
/// For error reporting purpose, we want to be able to determine the chain of nested calls leading
|
||||
/// to the current code path at any moment. To do so, the Nickel abstract machine maintains a
|
||||
/// callstack via this basic mechanism:
|
||||
/// 1. When an application is evaluated, push a marker with the position of the application on the callstack.
|
||||
/// 2. When a function body is entered, push a marker with the position of the original application on the
|
||||
/// callstack.
|
||||
/// 3. When a variable is evaluated, push a marker with its name and position on the callstack.
|
||||
/// 4. When a record field is accessed, push a marker with its name and position on the
|
||||
/// callstack too.
|
||||
///
|
||||
/// Both field and variable are useful to determine the name of a called function, when there
|
||||
/// is one. The resulting stack is not suited to be reported to the user for the following
|
||||
/// reasons:
|
||||
///
|
||||
/// 1. One call spans several items on the callstack. First the application is entered (pushing
|
||||
/// an `App`), then possibly variables or other application are evaluated until we
|
||||
/// eventually reach a function for the left hand side. Then body of this function is
|
||||
/// entered (pushing a `Fun`).
|
||||
/// 2. Because of currying, multi-ary applications span several objects on the callstack.
|
||||
/// Typically, `(fun x y => x + y) arg1 arg2` spans two `App` and two `Fun` elements in the
|
||||
/// form `App1 App2 Fun2 Fun1`, where the position span of `App1` includes the position span
|
||||
/// of `App2`. We want to group them as one call.
|
||||
/// 3. The callstack includes calls to builtin contracts. These calls are inserted implicitly
|
||||
/// by the abstract machine and are not written explicitly by the user. Showing them is
|
||||
/// confusing and clutters the call chain, so we get rid of them too.
|
||||
///
|
||||
/// This is the role of `group_by_calls`, which filter out unwanted elements and groups
|
||||
/// callstack elements into atomic call elements represented by [`CallDescr`].
|
||||
///
|
||||
/// The final call description list is reversed such that the most nested calls, which are
|
||||
/// usually the most relevant to understand the error, are printed first.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `contract_id`: the `FileId` of the source containing standard contracts, to filter their
|
||||
/// calls out.
|
||||
pub fn group_by_calls(
|
||||
self: &CallStack,
|
||||
contract_id: FileId,
|
||||
) -> (Vec<CallDescr>, Option<CallDescr>) {
|
||||
// We filter out calls and accesses made from within the builtin contracts, as well as
|
||||
// generated variables introduced by program transformations.
|
||||
let it = self.0.iter().filter(|elem| match elem {
|
||||
StackElem::Var {id, ..} if id.is_generated() => false,
|
||||
StackElem::Var{ pos: TermPos::Original(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::Var{pos: TermPos::Inherited(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::Fun(TermPos::Original(RawSpan { src_id, .. }))
|
||||
| StackElem::Field {pos_access: TermPos::Original(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::Field {pos_access: TermPos::Inherited(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::App(TermPos::Original(RawSpan { src_id, .. }))
|
||||
// We avoid applications (Fun/App) with inherited positions. Such calls include
|
||||
// contracts applications which add confusing call items whose positions don't point to
|
||||
// an actual call in the source.
|
||||
if *src_id != contract_id =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// We maintain a stack of active calls (whose head is being evaluated). When encountering
|
||||
// an identifier (variable or record field), we see if it could serve as a function name
|
||||
// for the current active call. When a `Fun` is encountered, we check if this correspond to
|
||||
// the current active call, and if it does, the call description is moved to a stack of
|
||||
// processed calls.
|
||||
//
|
||||
// We also merge subcalls, in the sense that subcalls of larger calls are not considered
|
||||
// separately. `app1` is a subcall of `app2` if the position of `app1` is included in the
|
||||
// one of `app2` and the starting index is equal. We want `f a b c` to be reported as only
|
||||
// one big call to `f` rather than three nested calls `f a`, `f a b`, and `f a b c`.
|
||||
let mut pending: Vec<CallDescr> = Vec::new();
|
||||
let mut entered: Vec<CallDescr> = Vec::new();
|
||||
|
||||
for elt in it {
|
||||
match elt {
|
||||
StackElem::Var { id, pos, .. }
|
||||
| StackElem::Field {
|
||||
id,
|
||||
pos_access: pos,
|
||||
..
|
||||
} => {
|
||||
match pending.last_mut() {
|
||||
Some(CallDescr {
|
||||
head: ref mut head @ None,
|
||||
span: span_call,
|
||||
}) if pos.unwrap() <= *span_call => *head = Some(id.clone()),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
StackElem::App(pos) => {
|
||||
let span = pos.unwrap();
|
||||
match pending.last() {
|
||||
Some(CallDescr {
|
||||
span: span_call, ..
|
||||
}) if span <= *span_call && span.start == span_call.start => (),
|
||||
_ => pending.push(CallDescr { head: None, span }),
|
||||
}
|
||||
}
|
||||
StackElem::Fun(pos) => {
|
||||
let span = pos.unwrap();
|
||||
if pending
|
||||
.last()
|
||||
.map(|cdescr| cdescr.span == span)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
entered.push(pending.pop().unwrap());
|
||||
}
|
||||
// Otherwise, we are most probably entering a subcall () of the currently
|
||||
// active call (e.g. in an multi-ary application `f g h`, a subcall would be `f
|
||||
// g`). In any case, we do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entered.reverse();
|
||||
(entered, pending.pop())
|
||||
}
|
||||
|
||||
/// Return the length of the callstack. Wrapper for `callstack.0.len()`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Truncate the callstack at a certain size. Used e.g. to quickly drop the elements introduced
|
||||
/// during the strict evaluation of the operand of a primitive operator. Wrapper for
|
||||
/// `callstack.0.truncate(len)`.
|
||||
pub fn truncate(&mut self, len: usize) {
|
||||
self.0.truncate(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallStack> for Vec<StackElem> {
|
||||
fn from(cs: CallStack) -> Self {
|
||||
cs.0
|
||||
}
|
||||
}
|
256
src/eval/lazy.rs
Normal file
256
src/eval/lazy.rs
Normal file
@ -0,0 +1,256 @@
|
||||
//! Thunks and associated devices used to implement lazy evaluation.
|
||||
use super::{Closure, IdentKind};
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
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 {
|
||||
inner: InnerThunkData,
|
||||
state: ThunkState,
|
||||
}
|
||||
|
||||
/// The part of [ThunkData] responsible for storing the closure itself. It can either be:
|
||||
/// - A standard thunk, that is destructively updated once and for all
|
||||
/// - A revertible thunk, that can be restored to its original expression. Used to implement
|
||||
/// recursive merging of records and overriding (see the [RFC
|
||||
/// overriding](https://github.com/tweag/nickel/pull/330))
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum InnerThunkData {
|
||||
Standard(Closure),
|
||||
Revertible {
|
||||
orig: Rc<Closure>,
|
||||
cached: Rc<Closure>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ThunkData {
|
||||
/// Create new standard thunk data.
|
||||
pub fn new(closure: Closure) -> Self {
|
||||
ThunkData {
|
||||
inner: InnerThunkData::Standard(closure),
|
||||
state: ThunkState::Suspended,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new revertible thunk data.
|
||||
pub fn new_rev(orig: Closure) -> Self {
|
||||
let rc = Rc::new(orig);
|
||||
|
||||
ThunkData {
|
||||
inner: InnerThunkData::Revertible {
|
||||
orig: rc.clone(),
|
||||
cached: rc,
|
||||
},
|
||||
state: ThunkState::Suspended,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the closure currently cached.
|
||||
pub fn closure(&self) -> &Closure {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(ref closure) => closure,
|
||||
InnerThunkData::Revertible { ref cached, .. } => cached,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable reference to the closure currently cached.
|
||||
pub fn closure_mut(&mut self) -> &mut Closure {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(ref mut closure) => closure,
|
||||
InnerThunkData::Revertible { ref mut cached, .. } => Rc::make_mut(cached),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the data and return the cached closure.
|
||||
pub fn into_closure(self) -> Closure {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(closure) => closure,
|
||||
InnerThunkData::Revertible { orig, cached } => {
|
||||
std::mem::drop(orig);
|
||||
Rc::try_unwrap(cached).unwrap_or_else(|rc| (*rc).clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the cached closure.
|
||||
pub fn update(&mut self, new: Closure) {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(ref mut closure) => *closure = new,
|
||||
InnerThunkData::Revertible { ref mut cached, .. } => *cached = Rc::new(new),
|
||||
}
|
||||
|
||||
self.state = ThunkState::Evaluated;
|
||||
}
|
||||
|
||||
/// Create fresh unevaluated thunk data from `self`, reverted to its original state before the
|
||||
/// first update. For standard thunk data, the content is unchanged and the state is conserved:
|
||||
/// in this case, `revert()` is the same as `clone()`.
|
||||
pub fn revert(&self) -> Self {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(_) => self.clone(),
|
||||
InnerThunkData::Revertible { ref orig, .. } => ThunkData {
|
||||
inner: InnerThunkData::Revertible {
|
||||
orig: Rc::clone(orig),
|
||||
cached: Rc::clone(orig),
|
||||
},
|
||||
state: ThunkState::Suspended,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thunk.
|
||||
///
|
||||
/// A thunk is a shared suspended computation. It is the primary device for the implementation of
|
||||
/// lazy evaluation.
|
||||
///
|
||||
/// For the implementation of recursive merging, some thunks need to be revertible, in the sense
|
||||
/// that we must be able to revert to the original expression before update. Those are called
|
||||
/// revertible thunks. Most expressions don't need revertible thunks as their evaluation will
|
||||
/// always give the same result, but some others, such as the ones containing recursive references
|
||||
/// inside a record may be invalidated by merging, and thus need to store the unaltered original
|
||||
/// expression. Those aspects are mainly handled in [InnerThunkData].
|
||||
#[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 {
|
||||
/// Create a new standard thunk.
|
||||
pub fn new(closure: Closure, ident_kind: IdentKind) -> Self {
|
||||
Thunk {
|
||||
data: Rc::new(RefCell::new(ThunkData::new(closure))),
|
||||
ident_kind,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new revertible thunk.
|
||||
pub fn new_rev(closure: Closure, ident_kind: IdentKind) -> Self {
|
||||
Thunk {
|
||||
data: Rc::new(RefCell::new(ThunkData::new_rev(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 mk_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> {
|
||||
Ref::map(self.data.borrow(), |data| data.closure())
|
||||
}
|
||||
|
||||
/// Mutably borrow the inner closure. Panic if there is any other active borrow.
|
||||
pub fn borrow_mut(&mut self) -> RefMut<'_, Closure> {
|
||||
RefMut::map(self.data.borrow_mut(), |data| data.closure_mut())
|
||||
}
|
||||
|
||||
/// 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().into_closure(),
|
||||
Err(rc) => rc.borrow().closure().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a fresh unevaluated thunk from `self`, reverted to its original state before the
|
||||
/// first update. For a standard thunk, the content is unchanged and the state is conserved: in
|
||||
/// this case, `revert()` is the same as `clone()`.
|
||||
pub fn revert(&self) -> Self {
|
||||
Thunk {
|
||||
data: Rc::new(RefCell::new(self.data.borrow().revert())),
|
||||
ident_kind: self.ident_kind,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if a thunk is worth being put on the stack for future update.
|
||||
///
|
||||
/// Typically, WHNFs and enriched values will not be evaluated to a simpler expression and are not
|
||||
/// worth updating.
|
||||
pub fn should_update(&self) -> bool {
|
||||
let term = &self.borrow().body.term;
|
||||
!term.is_whnf() && !term.is_metavalue()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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().update(closure);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
//! Evaluation of a Nickel term.
|
||||
//!
|
||||
//! The implementation of the Nickel abstract machine which evaluates a term. Note that this
|
||||
//! machine is not currently formalized somewhere and is just a convenient name to designate the
|
||||
//! current implementation.
|
||||
@ -92,487 +93,19 @@ use crate::{
|
||||
error::EvalError,
|
||||
identifier::Ident,
|
||||
mk_app,
|
||||
operation::{continuate_operation, OperationCont},
|
||||
position::RawSpan,
|
||||
position::TermPos,
|
||||
stack::Stack,
|
||||
term::{make as mk_term, BinaryOp, BindingType, MetaValue, RichTerm, StrChunk, Term, UnaryOp},
|
||||
};
|
||||
use codespan::FileId;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
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,
|
||||
}
|
||||
pub mod callstack;
|
||||
pub mod lazy;
|
||||
pub mod merge;
|
||||
pub mod operation;
|
||||
pub mod stack;
|
||||
|
||||
/// The mutable data stored inside a thunk.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ThunkData {
|
||||
inner: InnerThunkData,
|
||||
state: ThunkState,
|
||||
}
|
||||
|
||||
/// The part of [ThunkData] responsible for storing the closure itself. It can either be:
|
||||
/// - A standard thunk, that is destructively updated once and for all
|
||||
/// - A revertible thunk, that can be restored to its original expression. Used to implement
|
||||
/// recursive merging of records and overriding (see the [RFC
|
||||
/// overriding](https://github.com/tweag/nickel/pull/330))
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum InnerThunkData {
|
||||
Standard(Closure),
|
||||
Revertible {
|
||||
orig: Rc<Closure>,
|
||||
cached: Rc<Closure>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ThunkData {
|
||||
/// Create new standard thunk data.
|
||||
pub fn new(closure: Closure) -> Self {
|
||||
ThunkData {
|
||||
inner: InnerThunkData::Standard(closure),
|
||||
state: ThunkState::Suspended,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new revertible thunk data.
|
||||
pub fn new_rev(orig: Closure) -> Self {
|
||||
let rc = Rc::new(orig);
|
||||
|
||||
ThunkData {
|
||||
inner: InnerThunkData::Revertible {
|
||||
orig: rc.clone(),
|
||||
cached: rc,
|
||||
},
|
||||
state: ThunkState::Suspended,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the closure currently cached.
|
||||
pub fn closure(&self) -> &Closure {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(ref closure) => closure,
|
||||
InnerThunkData::Revertible { ref cached, .. } => cached,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable reference to the closure currently cached.
|
||||
pub fn closure_mut(&mut self) -> &mut Closure {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(ref mut closure) => closure,
|
||||
InnerThunkData::Revertible { ref mut cached, .. } => Rc::make_mut(cached),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the data and return the cached closure.
|
||||
pub fn into_closure(self) -> Closure {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(closure) => closure,
|
||||
InnerThunkData::Revertible { orig, cached } => {
|
||||
std::mem::drop(orig);
|
||||
Rc::try_unwrap(cached).unwrap_or_else(|rc| (*rc).clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the cached closure.
|
||||
pub fn update(&mut self, new: Closure) {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(ref mut closure) => *closure = new,
|
||||
InnerThunkData::Revertible { ref mut cached, .. } => *cached = Rc::new(new),
|
||||
}
|
||||
|
||||
self.state = ThunkState::Evaluated;
|
||||
}
|
||||
|
||||
/// Create fresh unevaluated thunk data from `self`, reverted to its original state before the
|
||||
/// first update. For standard thunk data, the content is unchanged and the state is conserved:
|
||||
/// in this case, `revert()` is the same as `clone()`.
|
||||
pub fn revert(&self) -> Self {
|
||||
match self.inner {
|
||||
InnerThunkData::Standard(_) => self.clone(),
|
||||
InnerThunkData::Revertible { ref orig, .. } => ThunkData {
|
||||
inner: InnerThunkData::Revertible {
|
||||
orig: Rc::clone(orig),
|
||||
cached: Rc::clone(orig),
|
||||
},
|
||||
state: ThunkState::Suspended,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thunk.
|
||||
///
|
||||
/// A thunk is a shared suspended computation. It is the primary device for the implementation of
|
||||
/// lazy evaluation.
|
||||
///
|
||||
/// For the implementation of recursive merging, some thunks need to be revertible, in the sense
|
||||
/// that we must be able to revert to the original expression before update. Those are called
|
||||
/// revertible thunks. Most expressions don't need revertible thunks as their evaluation will
|
||||
/// always give the same result, but some others, such as the ones containing recursive references
|
||||
/// inside a record may be invalidated by merging, and thus need to store the unaltered original
|
||||
/// expression. Those aspects are mainly handled in [InnerThunkData].
|
||||
#[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 {
|
||||
/// Create a new standard thunk.
|
||||
pub fn new(closure: Closure, ident_kind: IdentKind) -> Self {
|
||||
Thunk {
|
||||
data: Rc::new(RefCell::new(ThunkData::new(closure))),
|
||||
ident_kind,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new revertible thunk.
|
||||
pub fn new_rev(closure: Closure, ident_kind: IdentKind) -> Self {
|
||||
Thunk {
|
||||
data: Rc::new(RefCell::new(ThunkData::new_rev(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 mk_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> {
|
||||
Ref::map(self.data.borrow(), |data| data.closure())
|
||||
}
|
||||
|
||||
/// Mutably borrow the inner closure. Panic if there is any other active borrow.
|
||||
pub fn borrow_mut(&mut self) -> RefMut<'_, Closure> {
|
||||
RefMut::map(self.data.borrow_mut(), |data| data.closure_mut())
|
||||
}
|
||||
|
||||
/// 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().into_closure(),
|
||||
Err(rc) => rc.borrow().closure().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a fresh unevaluated thunk from `self`, reverted to its original state before the
|
||||
/// first update. For a standard thunk, the content is unchanged and the state is conserved: in
|
||||
/// this case, `revert()` is the same as `clone()`.
|
||||
pub fn revert(&self) -> Self {
|
||||
Thunk {
|
||||
data: Rc::new(RefCell::new(self.data.borrow().revert())),
|
||||
ident_kind: self.ident_kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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().update(closure);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A call stack, saving the history of function calls.
|
||||
///
|
||||
/// In a lazy language like Nickel, there are no well delimited stack frames due to how function
|
||||
/// application is evaluated. Additional information about the history of function calls is thus
|
||||
/// stored in the call stack solely for better error reporting.
|
||||
#[derive(PartialEq, Clone, Default, Debug)]
|
||||
pub struct CallStack(pub Vec<StackElem>);
|
||||
|
||||
/// Basic description of a function call. Used for error reporting.
|
||||
pub struct CallDescr {
|
||||
/// The name of the called function, if any.
|
||||
pub head: Option<Ident>,
|
||||
/// The position of the application.
|
||||
pub span: RawSpan,
|
||||
}
|
||||
|
||||
/// A call stack element.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum StackElem {
|
||||
/// A function body was entered. The position is the position of the original application.
|
||||
Fun(TermPos),
|
||||
/// An application was evaluated.
|
||||
App(TermPos),
|
||||
/// A variable was entered.
|
||||
Var {
|
||||
kind: IdentKind,
|
||||
id: Ident,
|
||||
pos: TermPos,
|
||||
},
|
||||
/// A record field was entered.
|
||||
Field {
|
||||
id: Ident,
|
||||
pos_record: TermPos,
|
||||
pos_field: TermPos,
|
||||
pos_access: TermPos,
|
||||
},
|
||||
}
|
||||
|
||||
impl CallStack {
|
||||
pub fn new() -> Self {
|
||||
CallStack(Vec::new())
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that a var was entered.
|
||||
pub fn enter_var(&mut self, kind: IdentKind, id: Ident, pos: TermPos) {
|
||||
self.0.push(StackElem::Var { kind, id, pos });
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that an application was entered.
|
||||
pub fn enter_app(&mut self, pos: TermPos) {
|
||||
// We ignore application without positions, which have been generated by the interpreter.
|
||||
if pos.is_def() {
|
||||
self.0.push(StackElem::App(pos));
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that during the evaluation an application, the function part was
|
||||
/// finally evaluated to an expression of the form `fun x => body`, and that the body of this
|
||||
/// function was entered.
|
||||
pub fn enter_fun(&mut self, pos: TermPos) {
|
||||
// We ignore application without positions, which have been generated by the interpreter.
|
||||
if pos.is_def() {
|
||||
self.0.push(StackElem::Fun(pos));
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a marker to indicate that a record field was entered.
|
||||
pub fn enter_field(
|
||||
&mut self,
|
||||
id: Ident,
|
||||
pos_record: TermPos,
|
||||
pos_field: TermPos,
|
||||
pos_access: TermPos,
|
||||
) {
|
||||
self.0.push(StackElem::Field {
|
||||
id,
|
||||
pos_record,
|
||||
pos_field,
|
||||
pos_access,
|
||||
});
|
||||
}
|
||||
|
||||
/// Process a raw callstack by aggregating elements belonging to the same call. Return a list
|
||||
/// of call descriptions from the most nested/recent to the least nested/recent, together with
|
||||
/// the last pending call, if any.
|
||||
///
|
||||
/// Recall that when a call `f arg` is evaluated, the following events happen:
|
||||
/// 1. `arg` is pushed on the evaluation stack.
|
||||
/// 2. `f` is evaluated.
|
||||
/// 3. Hopefully, the result of this evaluation is a function `Func(id, body)`. `arg` is popped
|
||||
/// from the stack, bound to `id` in the environment, and `body is entered`.
|
||||
///
|
||||
/// For error reporting purpose, we want to be able to determine the chain of nested calls leading
|
||||
/// to the current code path at any moment. To do so, the Nickel abstract machine maintains a
|
||||
/// callstack via this basic mechanism:
|
||||
/// 1. When an application is evaluated, push a marker with the position of the application on the callstack.
|
||||
/// 2. When a function body is entered, push a marker with the position of the original application on the
|
||||
/// callstack.
|
||||
/// 3. When a variable is evaluated, push a marker with its name and position on the callstack.
|
||||
/// 4. When a record field is accessed, push a marker with its name and position on the
|
||||
/// callstack too.
|
||||
///
|
||||
/// Both field and variable are useful to determine the name of a called function, when there
|
||||
/// is one. The resulting stack is not suited to be reported to the user for the following
|
||||
/// reasons:
|
||||
///
|
||||
/// 1. One call spans several items on the callstack. First the application is entered (pushing
|
||||
/// an `App`), then possibly variables or other application are evaluated until we
|
||||
/// eventually reach a function for the left hand side. Then body of this function is
|
||||
/// entered (pushing a `Fun`).
|
||||
/// 2. Because of currying, multi-ary applications span several objects on the callstack.
|
||||
/// Typically, `(fun x y => x + y) arg1 arg2` spans two `App` and two `Fun` elements in the
|
||||
/// form `App1 App2 Fun2 Fun1`, where the position span of `App1` includes the position span
|
||||
/// of `App2`. We want to group them as one call.
|
||||
/// 3. The callstack includes calls to builtin contracts. These calls are inserted implicitly
|
||||
/// by the abstract machine and are not written explicitly by the user. Showing them is
|
||||
/// confusing and clutters the call chain, so we get rid of them too.
|
||||
///
|
||||
/// This is the role of `group_by_calls`, which filter out unwanted elements and groups
|
||||
/// callstack elements into atomic call elements represented by [`CallDescr`].
|
||||
///
|
||||
/// The final call description list is reversed such that the most nested calls, which are
|
||||
/// usually the most relevant to understand the error, are printed first.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `contract_id`: the `FileId` of the source containing standard contracts, to filter their
|
||||
/// calls out.
|
||||
pub fn group_by_calls(
|
||||
self: &CallStack,
|
||||
contract_id: FileId,
|
||||
) -> (Vec<CallDescr>, Option<CallDescr>) {
|
||||
// We filter out calls and accesses made from within the builtin contracts, as well as
|
||||
// generated variables introduced by program transformations.
|
||||
let it = self.0.iter().filter(|elem| match elem {
|
||||
StackElem::Var {id, ..} if id.is_generated() => false,
|
||||
StackElem::Var{ pos: TermPos::Original(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::Var{pos: TermPos::Inherited(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::Fun(TermPos::Original(RawSpan { src_id, .. }))
|
||||
| StackElem::Field {pos_access: TermPos::Original(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::Field {pos_access: TermPos::Inherited(RawSpan { src_id, .. }), ..}
|
||||
| StackElem::App(TermPos::Original(RawSpan { src_id, .. }))
|
||||
// We avoid applications (Fun/App) with inherited positions. Such calls include
|
||||
// contracts applications which add confusing call items whose positions don't point to
|
||||
// an actual call in the source.
|
||||
if *src_id != contract_id =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// We maintain a stack of active calls (whose head is being evaluated). When encountering
|
||||
// an identifier (variable or record field), we see if it could serve as a function name
|
||||
// for the current active call. When a `Fun` is encountered, we check if this correspond to
|
||||
// the current active call, and if it does, the call description is moved to a stack of
|
||||
// processed calls.
|
||||
//
|
||||
// We also merge subcalls, in the sense that subcalls of larger calls are not considered
|
||||
// separately. `app1` is a subcall of `app2` if the position of `app1` is included in the
|
||||
// one of `app2` and the starting index is equal. We want `f a b c` to be reported as only
|
||||
// one big call to `f` rather than three nested calls `f a`, `f a b`, and `f a b c`.
|
||||
let mut pending: Vec<CallDescr> = Vec::new();
|
||||
let mut entered: Vec<CallDescr> = Vec::new();
|
||||
|
||||
for elt in it {
|
||||
match elt {
|
||||
StackElem::Var { id, pos, .. }
|
||||
| StackElem::Field {
|
||||
id,
|
||||
pos_access: pos,
|
||||
..
|
||||
} => {
|
||||
match pending.last_mut() {
|
||||
Some(CallDescr {
|
||||
head: ref mut head @ None,
|
||||
span: span_call,
|
||||
}) if pos.unwrap() <= *span_call => *head = Some(id.clone()),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
StackElem::App(pos) => {
|
||||
let span = pos.unwrap();
|
||||
match pending.last() {
|
||||
Some(CallDescr {
|
||||
span: span_call, ..
|
||||
}) if span <= *span_call && span.start == span_call.start => (),
|
||||
_ => pending.push(CallDescr { head: None, span }),
|
||||
}
|
||||
}
|
||||
StackElem::Fun(pos) => {
|
||||
let span = pos.unwrap();
|
||||
if pending
|
||||
.last()
|
||||
.map(|cdescr| cdescr.span == span)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
entered.push(pending.pop().unwrap());
|
||||
}
|
||||
// Otherwise, we are most probably entering a subcall () of the currently
|
||||
// active call (e.g. in an multi-ary application `f g h`, a subcall would be `f
|
||||
// g`). In any case, we do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entered.reverse();
|
||||
(entered, pending.pop())
|
||||
}
|
||||
|
||||
/// Return the length of the callstack. Wrapper for `callstack.0.len()`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Truncate the callstack at a certain size. Used e.g. to quickly drop the elements introduced
|
||||
/// during the strict evaluation of the operand of a primitive operator. Wrapper for
|
||||
/// `callstack.0.truncate(len)`.
|
||||
pub fn truncate(&mut self, len: usize) {
|
||||
self.0.truncate(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallStack> for Vec<StackElem> {
|
||||
fn from(cs: CallStack) -> Self {
|
||||
cs.0
|
||||
}
|
||||
}
|
||||
use callstack::*;
|
||||
use lazy::*;
|
||||
use operation::{continuate_operation, OperationCont};
|
||||
use stack::Stack;
|
||||
|
||||
/// Kind of an identifier.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@ -635,14 +168,6 @@ pub fn env_add(env: &mut Environment, id: Ident, rt: RichTerm, local_env: Enviro
|
||||
env.insert(id, Thunk::new(closure, IdentKind::Let));
|
||||
}
|
||||
|
||||
/// Determine if a thunk is worth being put on the stack for future update.
|
||||
///
|
||||
/// Typically, WHNFs and enriched values will not be evaluated to a simpler expression and are not
|
||||
/// worth updating.
|
||||
fn should_update(t: &Term) -> bool {
|
||||
!t.is_whnf() && !t.is_metavalue()
|
||||
}
|
||||
|
||||
/// Evaluate a Nickel term. Wrapper around [eval_closure](fn.eval_closure.html) that starts from an
|
||||
/// empty local environment and drops the final environment.
|
||||
pub fn eval<R>(
|
||||
@ -790,7 +315,7 @@ where
|
||||
std::mem::drop(env); // thunk may be a 1RC pointer
|
||||
|
||||
if thunk.state() != ThunkState::Evaluated {
|
||||
if should_update(&thunk.borrow().body.term) {
|
||||
if thunk.should_update() {
|
||||
match thunk.mk_update_frame() {
|
||||
Ok(thunk_upd) => stack.push_thunk(thunk_upd),
|
||||
Err(BlackholedError) => {
|
||||
@ -1350,362 +875,4 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cache::resolvers::{DummyResolver, SimpleResolver};
|
||||
use crate::error::ImportError;
|
||||
use crate::label::Label;
|
||||
use crate::parser::{grammar, lexer};
|
||||
use crate::term::make as mk_term;
|
||||
use crate::term::{BinaryOp, StrChunk, UnaryOp};
|
||||
use crate::transform::import_resolution::resolve_imports;
|
||||
use crate::{mk_app, mk_fun};
|
||||
use codespan::Files;
|
||||
|
||||
/// Evaluate a term without import support.
|
||||
fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
|
||||
eval(t, &Environment::new(), &mut DummyResolver {}).map(Term::from)
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Option<RichTerm> {
|
||||
let id = Files::new().add("<test>", String::from(s));
|
||||
|
||||
grammar::TermParser::new()
|
||||
.parse_term(id, lexer::Lexer::new(&s))
|
||||
.map(RichTerm::without_pos)
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_over_values() {
|
||||
let num = Term::Num(45.3);
|
||||
assert_eq!(Ok(num.clone()), eval_no_import(num.into()));
|
||||
|
||||
let boolean = Term::Bool(true);
|
||||
assert_eq!(Ok(boolean.clone()), eval_no_import(boolean.into()));
|
||||
|
||||
let lambda = mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x")));
|
||||
assert_eq!(Ok(lambda.as_ref().clone()), eval_no_import(lambda.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blame_panics() {
|
||||
let label = Label::dummy();
|
||||
if let Err(EvalError::BlameError(l, ..)) =
|
||||
eval_no_import(mk_term::op1(UnaryOp::Blame(), Term::Lbl(label.clone())))
|
||||
{
|
||||
assert_eq!(l, label);
|
||||
} else {
|
||||
panic!("This evaluation should've returned a BlameError!");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn lone_var_panics() {
|
||||
eval_no_import(mk_term::var("unbound")).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_fun_are_applicable() {
|
||||
eval_no_import(mk_app!(Term::Bool(true), Term::Num(45.))).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_app() {
|
||||
let t = mk_app!(mk_term::id(), Term::Num(5.0));
|
||||
assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_let() {
|
||||
let t = mk_term::let_in("x", Term::Num(5.0), mk_term::var("x"));
|
||||
assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_ite() {
|
||||
let t = mk_term::if_then_else(Term::Bool(true), Term::Num(5.0), Term::Bool(false));
|
||||
assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_plus() {
|
||||
let t = mk_term::op2(BinaryOp::Plus(), Term::Num(5.0), Term::Num(7.5));
|
||||
assert_eq!(Ok(Term::Num(12.5)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asking_for_various_types() {
|
||||
let num = mk_term::op1(UnaryOp::IsNum(), Term::Num(45.3));
|
||||
assert_eq!(Ok(Term::Bool(true)), eval_no_import(num));
|
||||
|
||||
let boolean = mk_term::op1(UnaryOp::IsBool(), Term::Bool(true));
|
||||
assert_eq!(Ok(Term::Bool(true)), eval_no_import(boolean));
|
||||
|
||||
let lambda = mk_term::op1(
|
||||
UnaryOp::IsFun(),
|
||||
mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x"))),
|
||||
);
|
||||
assert_eq!(Ok(Term::Bool(true)), eval_no_import(lambda));
|
||||
}
|
||||
|
||||
fn mk_default(t: RichTerm) -> Term {
|
||||
use crate::term::MergePriority;
|
||||
|
||||
let mut meta = MetaValue::from(t);
|
||||
meta.priority = MergePriority::Default;
|
||||
Term::MetaValue(meta)
|
||||
}
|
||||
|
||||
fn mk_docstring<S>(t: RichTerm, s: S) -> Term
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let mut meta = MetaValue::from(t);
|
||||
meta.doc.replace(s.into());
|
||||
Term::MetaValue(meta)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enriched_terms_unwrapping() {
|
||||
let t = mk_default(mk_default(mk_docstring(Term::Bool(false).into(), "a").into()).into())
|
||||
.into();
|
||||
assert_eq!(Ok(Term::Bool(false)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_enriched_default() {
|
||||
let t = mk_term::op2(
|
||||
BinaryOp::Merge(),
|
||||
Term::Num(1.0),
|
||||
mk_default(Term::Num(2.0).into()),
|
||||
);
|
||||
assert_eq!(Ok(Term::Num(1.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_incompatible_defaults() {
|
||||
let t = mk_term::op2(
|
||||
BinaryOp::Merge(),
|
||||
mk_default(Term::Num(1.0).into()),
|
||||
mk_default(Term::Num(2.0).into()),
|
||||
);
|
||||
|
||||
eval_no_import(t).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports() {
|
||||
let mut resolver = SimpleResolver::new();
|
||||
resolver.add_source(String::from("two"), String::from("1 + 1"));
|
||||
resolver.add_source(String::from("lib"), String::from("{f = true}"));
|
||||
resolver.add_source(String::from("bad"), String::from("^$*/.23ab 0°@"));
|
||||
resolver.add_source(
|
||||
String::from("nested"),
|
||||
String::from("let x = import \"two\" in x + 1"),
|
||||
);
|
||||
resolver.add_source(
|
||||
String::from("cycle"),
|
||||
String::from("let x = import \"cycle_b\" in {a = 1, b = x.a}"),
|
||||
);
|
||||
resolver.add_source(
|
||||
String::from("cycle_b"),
|
||||
String::from("let x = import \"cycle\" in {a = x.a}"),
|
||||
);
|
||||
|
||||
fn mk_import<R>(
|
||||
var: &str,
|
||||
import: &str,
|
||||
body: RichTerm,
|
||||
resolver: &mut R,
|
||||
) -> Result<RichTerm, ImportError>
|
||||
where
|
||||
R: ImportResolver,
|
||||
{
|
||||
resolve_imports(
|
||||
mk_term::let_in(var, mk_term::import(import), body),
|
||||
resolver,
|
||||
)
|
||||
.map(|(t, _)| t)
|
||||
}
|
||||
|
||||
// let x = import "does_not_exist" in x
|
||||
match mk_import("x", "does_not_exist", mk_term::var("x"), &mut resolver).unwrap_err() {
|
||||
ImportError::IOError(_, _, _) => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// let x = import "bad" in x
|
||||
match mk_import("x", "bad", mk_term::var("x"), &mut resolver).unwrap_err() {
|
||||
ImportError::ParseErrors(_, _) => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// let x = import "two" in x
|
||||
assert_eq!(
|
||||
eval(
|
||||
mk_import("x", "two", mk_term::var("x"), &mut resolver).unwrap(),
|
||||
&Environment::new(),
|
||||
&mut resolver
|
||||
)
|
||||
.map(Term::from)
|
||||
.unwrap(),
|
||||
Term::Num(2.0)
|
||||
);
|
||||
|
||||
// let x = import "lib" in x.f
|
||||
assert_eq!(
|
||||
eval(
|
||||
mk_import(
|
||||
"x",
|
||||
"lib",
|
||||
mk_term::op1(UnaryOp::StaticAccess(Ident::from("f")), mk_term::var("x")),
|
||||
&mut resolver,
|
||||
)
|
||||
.unwrap(),
|
||||
&Environment::new(),
|
||||
&mut resolver
|
||||
)
|
||||
.map(Term::from)
|
||||
.unwrap(),
|
||||
Term::Bool(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolation_simple() {
|
||||
let mut chunks = vec![
|
||||
StrChunk::Literal(String::from("Hello")),
|
||||
StrChunk::expr(
|
||||
mk_term::op2(
|
||||
BinaryOp::StrConcat(),
|
||||
mk_term::string(", "),
|
||||
mk_term::string("World!"),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
StrChunk::Literal(String::from(" How")),
|
||||
StrChunk::expr(mk_term::if_then_else(
|
||||
Term::Bool(true),
|
||||
mk_term::string(" are"),
|
||||
mk_term::string(" is"),
|
||||
)),
|
||||
StrChunk::Literal(String::from(" you?")),
|
||||
];
|
||||
chunks.reverse();
|
||||
|
||||
let t: RichTerm = Term::StrChunks(chunks).into();
|
||||
assert_eq!(
|
||||
eval_no_import(t),
|
||||
Ok(Term::Str(String::from("Hello, World! How are you?")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolation_nested() {
|
||||
let mut inner_chunks = vec![
|
||||
StrChunk::Literal(String::from(" How")),
|
||||
StrChunk::expr(
|
||||
Term::Op2(
|
||||
BinaryOp::StrConcat(),
|
||||
mk_term::string(" ar"),
|
||||
mk_term::string("e"),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
StrChunk::expr(mk_term::if_then_else(
|
||||
Term::Bool(true),
|
||||
mk_term::string(" you"),
|
||||
mk_term::string(" me"),
|
||||
)),
|
||||
];
|
||||
inner_chunks.reverse();
|
||||
|
||||
let mut chunks = vec![
|
||||
StrChunk::Literal(String::from("Hello, World!")),
|
||||
StrChunk::expr(Term::StrChunks(inner_chunks).into()),
|
||||
StrChunk::Literal(String::from("?")),
|
||||
];
|
||||
chunks.reverse();
|
||||
|
||||
let t: RichTerm = Term::StrChunks(chunks).into();
|
||||
assert_eq!(
|
||||
eval_no_import(t),
|
||||
Ok(Term::Str(String::from("Hello, World! How are you?")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_env() {
|
||||
let mut global_env = Environment::new();
|
||||
let mut resolver = DummyResolver {};
|
||||
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).map(Term::from),
|
||||
Ok(Term::Num(2.0))
|
||||
);
|
||||
|
||||
let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("g"));
|
||||
assert_eq!(
|
||||
eval(t, &global_env, &mut resolver).map(Term::from),
|
||||
Ok(Term::Num(1.0))
|
||||
);
|
||||
|
||||
// Shadowing of global environment
|
||||
let t = mk_term::let_in("g", Term::Num(2.0), mk_term::var("g"));
|
||||
assert_eq!(
|
||||
eval(t, &global_env, &mut resolver).map(Term::from),
|
||||
Ok(Term::Num(2.0))
|
||||
);
|
||||
}
|
||||
|
||||
fn mk_env(bindings: Vec<(&str, RichTerm)>) -> Environment {
|
||||
bindings
|
||||
.into_iter()
|
||||
.map(|(id, t)| {
|
||||
(
|
||||
id.into(),
|
||||
Thunk::new(Closure::atomic_closure(t), IdentKind::Let),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substitution() {
|
||||
let global_env = mk_env(vec![
|
||||
("glob1", Term::Num(1.0).into()),
|
||||
("glob2", parse("\"Glob2\"").unwrap()),
|
||||
("glob3", Term::Bool(false).into()),
|
||||
]);
|
||||
let env = mk_env(vec![
|
||||
("loc1", Term::Bool(true).into()),
|
||||
("loc2", parse("if glob3 then glob1 else glob2").unwrap()),
|
||||
]);
|
||||
|
||||
let t = parse("let x = 1 in if loc1 then 1 + loc2 else glob3").unwrap();
|
||||
assert_eq!(
|
||||
subst(t, &global_env, &env),
|
||||
parse("let x = 1 in if true then 1 + (if false then 1 else \"Glob2\") else false")
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let t =
|
||||
parse("switch {`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
subst(t, &global_env, &env),
|
||||
parse("switch {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
@ -6,15 +6,15 @@
|
||||
//! the functions [`process_unary_operation`](fn.process_unary_operation.html) and
|
||||
//! [`process_binary_operation`](fn.process_binary_operation.html) receive evaluated operands and
|
||||
//! implement the actual semantics of operators.
|
||||
use super::merge;
|
||||
use super::merge::{merge, MergeMode};
|
||||
use super::stack::Stack;
|
||||
use crate::error::EvalError;
|
||||
use crate::eval::{subst, CallStack, Closure, Environment};
|
||||
use crate::identifier::Ident;
|
||||
use crate::label::ty_path;
|
||||
use crate::merge;
|
||||
use crate::merge::{merge, MergeMode};
|
||||
use crate::mk_record;
|
||||
use crate::position::TermPos;
|
||||
use crate::stack::Stack;
|
||||
use crate::term::make as mk_term;
|
||||
use crate::term::{BinaryOp, NAryOp, RichTerm, StrChunk, Term, UnaryOp};
|
||||
use crate::transform::Closurizable;
|
@ -1,8 +1,8 @@
|
||||
//! Define the main evaluation stack of the Nickel abstract machine and related operations.
|
||||
//!
|
||||
//! See [eval](../eval/index.html).
|
||||
use super::operation::OperationCont;
|
||||
use crate::eval::{Closure, Environment, IdentKind, Thunk, ThunkUpdateFrame};
|
||||
use crate::operation::OperationCont;
|
||||
use crate::position::TermPos;
|
||||
use crate::term::{RichTerm, StrChunk};
|
||||
|
355
src/eval/tests.rs
Normal file
355
src/eval/tests.rs
Normal file
@ -0,0 +1,355 @@
|
||||
use super::*;
|
||||
use crate::cache::resolvers::{DummyResolver, SimpleResolver};
|
||||
use crate::error::ImportError;
|
||||
use crate::label::Label;
|
||||
use crate::parser::{grammar, lexer};
|
||||
use crate::term::make as mk_term;
|
||||
use crate::term::{BinaryOp, StrChunk, UnaryOp};
|
||||
use crate::transform::import_resolution::resolve_imports;
|
||||
use crate::{mk_app, mk_fun};
|
||||
use codespan::Files;
|
||||
|
||||
/// Evaluate a term without import support.
|
||||
fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
|
||||
eval(t, &Environment::new(), &mut DummyResolver {}).map(Term::from)
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Option<RichTerm> {
|
||||
let id = Files::new().add("<test>", String::from(s));
|
||||
|
||||
grammar::TermParser::new()
|
||||
.parse_term(id, lexer::Lexer::new(&s))
|
||||
.map(RichTerm::without_pos)
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_over_values() {
|
||||
let num = Term::Num(45.3);
|
||||
assert_eq!(Ok(num.clone()), eval_no_import(num.into()));
|
||||
|
||||
let boolean = Term::Bool(true);
|
||||
assert_eq!(Ok(boolean.clone()), eval_no_import(boolean.into()));
|
||||
|
||||
let lambda = mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x")));
|
||||
assert_eq!(Ok(lambda.as_ref().clone()), eval_no_import(lambda.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blame_panics() {
|
||||
let label = Label::dummy();
|
||||
if let Err(EvalError::BlameError(l, ..)) =
|
||||
eval_no_import(mk_term::op1(UnaryOp::Blame(), Term::Lbl(label.clone())))
|
||||
{
|
||||
assert_eq!(l, label);
|
||||
} else {
|
||||
panic!("This evaluation should've returned a BlameError!");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn lone_var_panics() {
|
||||
eval_no_import(mk_term::var("unbound")).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_fun_are_applicable() {
|
||||
eval_no_import(mk_app!(Term::Bool(true), Term::Num(45.))).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_app() {
|
||||
let t = mk_app!(mk_term::id(), Term::Num(5.0));
|
||||
assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_let() {
|
||||
let t = mk_term::let_in("x", Term::Num(5.0), mk_term::var("x"));
|
||||
assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_ite() {
|
||||
let t = mk_term::if_then_else(Term::Bool(true), Term::Num(5.0), Term::Bool(false));
|
||||
assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_plus() {
|
||||
let t = mk_term::op2(BinaryOp::Plus(), Term::Num(5.0), Term::Num(7.5));
|
||||
assert_eq!(Ok(Term::Num(12.5)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asking_for_various_types() {
|
||||
let num = mk_term::op1(UnaryOp::IsNum(), Term::Num(45.3));
|
||||
assert_eq!(Ok(Term::Bool(true)), eval_no_import(num));
|
||||
|
||||
let boolean = mk_term::op1(UnaryOp::IsBool(), Term::Bool(true));
|
||||
assert_eq!(Ok(Term::Bool(true)), eval_no_import(boolean));
|
||||
|
||||
let lambda = mk_term::op1(
|
||||
UnaryOp::IsFun(),
|
||||
mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x"))),
|
||||
);
|
||||
assert_eq!(Ok(Term::Bool(true)), eval_no_import(lambda));
|
||||
}
|
||||
|
||||
fn mk_default(t: RichTerm) -> Term {
|
||||
use crate::term::MergePriority;
|
||||
|
||||
let mut meta = MetaValue::from(t);
|
||||
meta.priority = MergePriority::Default;
|
||||
Term::MetaValue(meta)
|
||||
}
|
||||
|
||||
fn mk_docstring<S>(t: RichTerm, s: S) -> Term
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let mut meta = MetaValue::from(t);
|
||||
meta.doc.replace(s.into());
|
||||
Term::MetaValue(meta)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enriched_terms_unwrapping() {
|
||||
let t =
|
||||
mk_default(mk_default(mk_docstring(Term::Bool(false).into(), "a").into()).into()).into();
|
||||
assert_eq!(Ok(Term::Bool(false)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_enriched_default() {
|
||||
let t = mk_term::op2(
|
||||
BinaryOp::Merge(),
|
||||
Term::Num(1.0),
|
||||
mk_default(Term::Num(2.0).into()),
|
||||
);
|
||||
assert_eq!(Ok(Term::Num(1.0)), eval_no_import(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_incompatible_defaults() {
|
||||
let t = mk_term::op2(
|
||||
BinaryOp::Merge(),
|
||||
mk_default(Term::Num(1.0).into()),
|
||||
mk_default(Term::Num(2.0).into()),
|
||||
);
|
||||
|
||||
eval_no_import(t).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports() {
|
||||
let mut resolver = SimpleResolver::new();
|
||||
resolver.add_source(String::from("two"), String::from("1 + 1"));
|
||||
resolver.add_source(String::from("lib"), String::from("{f = true}"));
|
||||
resolver.add_source(String::from("bad"), String::from("^$*/.23ab 0°@"));
|
||||
resolver.add_source(
|
||||
String::from("nested"),
|
||||
String::from("let x = import \"two\" in x + 1"),
|
||||
);
|
||||
resolver.add_source(
|
||||
String::from("cycle"),
|
||||
String::from("let x = import \"cycle_b\" in {a = 1, b = x.a}"),
|
||||
);
|
||||
resolver.add_source(
|
||||
String::from("cycle_b"),
|
||||
String::from("let x = import \"cycle\" in {a = x.a}"),
|
||||
);
|
||||
|
||||
fn mk_import<R>(
|
||||
var: &str,
|
||||
import: &str,
|
||||
body: RichTerm,
|
||||
resolver: &mut R,
|
||||
) -> Result<RichTerm, ImportError>
|
||||
where
|
||||
R: ImportResolver,
|
||||
{
|
||||
resolve_imports(
|
||||
mk_term::let_in(var, mk_term::import(import), body),
|
||||
resolver,
|
||||
)
|
||||
.map(|(t, _)| t)
|
||||
}
|
||||
|
||||
// let x = import "does_not_exist" in x
|
||||
match mk_import("x", "does_not_exist", mk_term::var("x"), &mut resolver).unwrap_err() {
|
||||
ImportError::IOError(_, _, _) => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// let x = import "bad" in x
|
||||
match mk_import("x", "bad", mk_term::var("x"), &mut resolver).unwrap_err() {
|
||||
ImportError::ParseErrors(_, _) => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// let x = import "two" in x
|
||||
assert_eq!(
|
||||
eval(
|
||||
mk_import("x", "two", mk_term::var("x"), &mut resolver).unwrap(),
|
||||
&Environment::new(),
|
||||
&mut resolver
|
||||
)
|
||||
.map(Term::from)
|
||||
.unwrap(),
|
||||
Term::Num(2.0)
|
||||
);
|
||||
|
||||
// let x = import "lib" in x.f
|
||||
assert_eq!(
|
||||
eval(
|
||||
mk_import(
|
||||
"x",
|
||||
"lib",
|
||||
mk_term::op1(UnaryOp::StaticAccess(Ident::from("f")), mk_term::var("x")),
|
||||
&mut resolver,
|
||||
)
|
||||
.unwrap(),
|
||||
&Environment::new(),
|
||||
&mut resolver
|
||||
)
|
||||
.map(Term::from)
|
||||
.unwrap(),
|
||||
Term::Bool(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolation_simple() {
|
||||
let mut chunks = vec![
|
||||
StrChunk::Literal(String::from("Hello")),
|
||||
StrChunk::expr(
|
||||
mk_term::op2(
|
||||
BinaryOp::StrConcat(),
|
||||
mk_term::string(", "),
|
||||
mk_term::string("World!"),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
StrChunk::Literal(String::from(" How")),
|
||||
StrChunk::expr(mk_term::if_then_else(
|
||||
Term::Bool(true),
|
||||
mk_term::string(" are"),
|
||||
mk_term::string(" is"),
|
||||
)),
|
||||
StrChunk::Literal(String::from(" you?")),
|
||||
];
|
||||
chunks.reverse();
|
||||
|
||||
let t: RichTerm = Term::StrChunks(chunks).into();
|
||||
assert_eq!(
|
||||
eval_no_import(t),
|
||||
Ok(Term::Str(String::from("Hello, World! How are you?")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolation_nested() {
|
||||
let mut inner_chunks = vec![
|
||||
StrChunk::Literal(String::from(" How")),
|
||||
StrChunk::expr(
|
||||
Term::Op2(
|
||||
BinaryOp::StrConcat(),
|
||||
mk_term::string(" ar"),
|
||||
mk_term::string("e"),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
StrChunk::expr(mk_term::if_then_else(
|
||||
Term::Bool(true),
|
||||
mk_term::string(" you"),
|
||||
mk_term::string(" me"),
|
||||
)),
|
||||
];
|
||||
inner_chunks.reverse();
|
||||
|
||||
let mut chunks = vec![
|
||||
StrChunk::Literal(String::from("Hello, World!")),
|
||||
StrChunk::expr(Term::StrChunks(inner_chunks).into()),
|
||||
StrChunk::Literal(String::from("?")),
|
||||
];
|
||||
chunks.reverse();
|
||||
|
||||
let t: RichTerm = Term::StrChunks(chunks).into();
|
||||
assert_eq!(
|
||||
eval_no_import(t),
|
||||
Ok(Term::Str(String::from("Hello, World! How are you?")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_env() {
|
||||
let mut global_env = Environment::new();
|
||||
let mut resolver = DummyResolver {};
|
||||
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).map(Term::from),
|
||||
Ok(Term::Num(2.0))
|
||||
);
|
||||
|
||||
let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("g"));
|
||||
assert_eq!(
|
||||
eval(t, &global_env, &mut resolver).map(Term::from),
|
||||
Ok(Term::Num(1.0))
|
||||
);
|
||||
|
||||
// Shadowing of global environment
|
||||
let t = mk_term::let_in("g", Term::Num(2.0), mk_term::var("g"));
|
||||
assert_eq!(
|
||||
eval(t, &global_env, &mut resolver).map(Term::from),
|
||||
Ok(Term::Num(2.0))
|
||||
);
|
||||
}
|
||||
|
||||
fn mk_env(bindings: Vec<(&str, RichTerm)>) -> Environment {
|
||||
bindings
|
||||
.into_iter()
|
||||
.map(|(id, t)| {
|
||||
(
|
||||
id.into(),
|
||||
Thunk::new(Closure::atomic_closure(t), IdentKind::Let),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substitution() {
|
||||
let global_env = mk_env(vec![
|
||||
("glob1", Term::Num(1.0).into()),
|
||||
("glob2", parse("\"Glob2\"").unwrap()),
|
||||
("glob3", Term::Bool(false).into()),
|
||||
]);
|
||||
let env = mk_env(vec![
|
||||
("loc1", Term::Bool(true).into()),
|
||||
("loc2", parse("if glob3 then glob1 else glob2").unwrap()),
|
||||
]);
|
||||
|
||||
let t = parse("let x = 1 in if loc1 then 1 + loc2 else glob3").unwrap();
|
||||
assert_eq!(
|
||||
subst(t, &global_env, &env),
|
||||
parse("let x = 1 in if true then 1 + (if false then 1 else \"Glob2\") else false").unwrap()
|
||||
);
|
||||
|
||||
let t = parse("switch {`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
subst(t, &global_env, &env),
|
||||
parse("switch {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! A label is a value holding metadata relative to contract checking. It gives the user useful
|
||||
//! information about the context of a contract failure.
|
||||
use crate::eval::Thunk;
|
||||
use crate::eval::lazy::Thunk;
|
||||
use crate::position::{RawSpan, TermPos};
|
||||
use crate::types::{AbsType, Types};
|
||||
use codespan::Files;
|
||||
|
@ -5,14 +5,11 @@ pub mod error;
|
||||
pub mod eval;
|
||||
pub mod identifier;
|
||||
pub mod label;
|
||||
pub mod merge;
|
||||
pub mod operation;
|
||||
pub mod parser;
|
||||
pub mod position;
|
||||
pub mod program;
|
||||
pub mod repl;
|
||||
pub mod serialize;
|
||||
pub mod stack;
|
||||
pub mod stdlib;
|
||||
pub mod term;
|
||||
pub mod transform;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::cache::ImportResolver;
|
||||
use crate::eval::{Closure, Environment, IdentKind, Thunk};
|
||||
use crate::eval::{lazy::Thunk, Closure, Environment, IdentKind};
|
||||
use crate::identifier::Ident;
|
||||
use crate::term::{Contract, RichTerm, Term, TraverseMethod};
|
||||
use crate::types::{AbsType, Types};
|
||||
|
Loading…
Reference in New Issue
Block a user