1
1
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:
Yann Hamdaoui 2022-01-06 19:09:36 +01:00 committed by GitHub
commit 00819963fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 864 additions and 855 deletions

View File

@ -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
View 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
View 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
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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
View 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()
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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};