Skip to content

Commit

Permalink
Add a contract node to the AST (#1955)
Browse files Browse the repository at this point in the history
* Add CustomContract node to the AST

This commit continues the work of properly separating custom contracts
in the AST, in order to maintain more run-time information and be able,
in the long run, to perform operations like boolean `or` on a larger
class of contracts and to provide better error messages in some
situations.

This commit focuses on predicates, built using the
`std.contract.from_predicate` which now acts as some kind of type
constructor. `%contract/apply%` then performs the conversion of this
predicate to a generic custom contract (a partial identity function).

Note that this is a backward-incompatible change on paper, because
before one could apply a custom contract built from a predicate as a
function (taking a label and a value, and returning a new value).
However, in practice, it's not officially supported - users are
requested to use `std.contract.apply` to manipulate contracts - and thus
we don't expect actual breakage to happen.

* Update core/src/term/mod.rs

Co-authored-by: jneem <[email protected]>

---------

Co-authored-by: jneem <[email protected]>
  • Loading branch information
yannham and jneem authored Jun 13, 2024
1 parent c7fc4a1 commit d8cdd77
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 59 deletions.
9 changes: 5 additions & 4 deletions core/src/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,12 @@
//! appear inside recursive records. A dedicated garbage collector is probably something to
//! consider at some point.
use crate::identifier::Ident;
use crate::term::string::NickelString;
use crate::{
cache::{Cache as ImportCache, Envs, ImportResolver},
closurize::{closurize_rec_record, Closurize},
environment::Environment as GenericEnvironment,
error::{Error, EvalError},
identifier::Ident,
identifier::LocIdent,
match_sharedterm,
position::TermPos,
Expand All @@ -90,8 +89,9 @@ use crate::{
make as mk_term,
pattern::compile::Compile,
record::{Field, RecordData},
BinaryOp, BindingType, LetAttrs, MatchBranch, MatchData, RecordOpKind, RichTerm,
RuntimeContract, StrChunk, Term, UnaryOp,
string::NickelString,
BinaryOp, BindingType, CustomContract, LetAttrs, MatchBranch, MatchData, RecordOpKind,
RichTerm, RuntimeContract, StrChunk, Term, UnaryOp,
},
};

Expand Down Expand Up @@ -1151,6 +1151,7 @@ pub fn subst<C: Cache>(
// Do not substitute under lambdas: mutually recursive function could cause an infinite
// loop. Although avoidable, this requires some care and is not currently needed.
| v @ Term::Fun(..)
| v @ Term::CustomContract(CustomContract::Predicate(..))
| v @ Term::Lbl(_)
| v @ Term::ForeignId(_)
| v @ Term::SealingKey(_)
Expand Down
23 changes: 23 additions & 0 deletions core/src/eval/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,19 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
_ => Err(mk_type_error!("label_push_diag", "Label")),
})
}
UnaryOp::ContractFromPredicate => {
if let Term::Fun(id, body) = &*t {
Ok(Closure {
body: RichTerm::new(
Term::CustomContract(CustomContract::Predicate(*id, body.clone())),
pos,
),
env,
})
} else {
Err(mk_type_error!("contract_from_predicate", "Function"))
}
}
#[cfg(feature = "nix-experimental")]
UnaryOp::EvalNix => {
if let Term::Str(s) = &*t {
Expand Down Expand Up @@ -1539,6 +1552,16 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
},
env: env1,
}),
Term::CustomContract(CustomContract::Predicate(ref id, ref body)) => {
Ok(Closure {
body: mk_app!(
internals::predicate_to_ctr(),
RichTerm::new(Term::Fun(*id, body.clone()), pos1)
)
.with_pos(pos1),
env: env1,
})
}
Term::Record(..) => {
let closurized = RichTerm {
term: t1,
Expand Down
4 changes: 3 additions & 1 deletion core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ EnumVariantPattern: EnumPattern = {
// A twisted version of EnumPattern made specifically for the branch of an
// or-pattern. As we parse `EnumVariantOrPattern` and treat it specifically in
// an `or` branch (`OrPatternBranch`), we need to remove it from the enum
// pattern rule.
// pattern rule.
EnumPatternOrBranch: EnumPattern = {
EnumVariantNoOrPattern,
// Only a top-level un-parenthesized enum variant pattern can be ambiguous.
Expand Down Expand Up @@ -1081,6 +1081,7 @@ UOp: UnaryOp = {
"label/go_codom" => UnaryOp::LabelGoCodom,
"label/go_array" => UnaryOp::LabelGoArray,
"label/go_dict" => UnaryOp::LabelGoDict,
"contract/from_predicate" => UnaryOp::ContractFromPredicate,
"enum/embed" <Ident> => UnaryOp::EnumEmbed(<>),
"array/map" => UnaryOp::ArrayMap,
"array/generate" => UnaryOp::ArrayGen,
Expand Down Expand Up @@ -1512,6 +1513,7 @@ extern {
"contract/apply" => Token::Normal(NormalToken::ContractApply),
"contract/array_lazy_app" => Token::Normal(NormalToken::ContractArrayLazyApp),
"contract/record_lazy_app" => Token::Normal(NormalToken::ContractRecordLazyApp),
"contract/from_predicate" => Token::Normal(NormalToken::ContractFromPredicate),
"op force" => Token::Normal(NormalToken::OpForce),
"blame" => Token::Normal(NormalToken::Blame),
"label/flip_polarity" => Token::Normal(NormalToken::LabelFlipPol),
Expand Down
2 changes: 2 additions & 0 deletions core/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ pub enum NormalToken<'input> {
ContractArrayLazyApp,
#[token("%contract/record_lazy_apply%")]
ContractRecordLazyApp,
#[token("%contract/from_predicate%")]
ContractFromPredicate,
#[token("%blame%")]
Blame,
#[token("%label/flip_polarity%")]
Expand Down
86 changes: 49 additions & 37 deletions core/src/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ use crate::parser::lexer::KEYWORDS;
use crate::term::{
pattern::*,
record::{Field, FieldMetadata, RecordData},
// Because we use `Term::*`, we need to differentiate `Contract` from `Term::Contract`, so we
// alias the latter
CustomContract as ContractNode,
*,
};
use crate::typ::*;
Expand Down Expand Up @@ -139,6 +142,7 @@ fn needs_parens_in_type_pos(typ: &Type) -> bool {
term.as_ref(),
Term::Fun(..)
| Term::FunPattern(..)
| Term::CustomContract(CustomContract::Predicate(..))
| Term::Let(..)
| Term::LetPattern(..)
| Term::Op1(UnaryOp::IfThenElse, _)
Expand Down Expand Up @@ -251,6 +255,43 @@ where
.enclose(start_delimiter, end_delimiter)
}

/// Print a function, which can have several parameters (represented as nested functions), and
/// where each layer might be a normal function, a pattern matching function or a custom
/// contract. [function] automatically unwrap any of those nested layers to print the function
/// with as many parameters as possible on the left of the `=>` separator.
fn function(
&'a self,
first_param: impl Pretty<'a, Self, A>,
mut body: &RichTerm,
) -> DocBuilder<'a, Self, A> {
let mut builder = docs![self, "fun", self.line(), first_param];

loop {
match body.as_ref() {
Term::Fun(id, rt) | Term::CustomContract(CustomContract::Predicate(id, rt)) => {
builder = docs![self, builder, self.line(), self.as_string(id)];
body = rt;
}
Term::FunPattern(pat, rt) => {
builder = docs![self, builder, self.line(), self.pat_with_parens(pat)];
body = rt;
}
_ => break,
}
}

docs![
self,
builder,
self.line(),
"=>",
self.line(),
body.pretty(self)
]
.nest(2)
.group()
}

fn field_metadata(
&'a self,
metadata: &FieldMetadata,
Expand Down Expand Up @@ -779,49 +820,20 @@ where
Num(n) => allocator.as_string(format!("{}", n.to_sci())),
Str(v) => allocator.escaped_string(v).double_quotes(),
StrChunks(chunks) => allocator.chunks(chunks, StringRenderStyle::Multiline),
Fun(id, rt) => {
let mut params = vec![id];
let mut rt = rt;
while let Fun(id, t) = rt.as_ref() {
params.push(id);
rt = t
}
docs![
allocator,
"fun",
allocator.line(),
allocator.intersperse(
params.iter().map(|p| allocator.as_string(p)),
allocator.line()
),
allocator.line(),
"=>",
allocator.line(),
rt
]
.nest(2)
.group()
}
FunPattern(..) => {
let mut params = vec![];
let mut rt = self;
while let FunPattern(pat, t) = rt {
params.push(allocator.pat_with_parens(pat));
rt = t.as_ref();
}
Fun(id, body) => allocator.function(allocator.as_string(id), body),
FunPattern(pat, body) => allocator.function(allocator.pat_with_parens(pat), body),
// Format this as the application `std.contract.from_predicate <pred>`.
CustomContract(ContractNode::Predicate(id, pred)) => docs![
allocator,
"%contract/from_predicate%",
docs![
allocator,
"fun",
allocator.line(),
allocator.intersperse(params, allocator.line()),
allocator.line(),
"=>",
allocator.line(),
rt
allocator.function(allocator.as_string(id), pred).parens()
]
.nest(2)
.group()
}
],
Lbl(_lbl) => allocator.text("%<label>").append(allocator.line()),
Let(id, rt, body, attrs) => docs![
allocator,
Expand Down
2 changes: 2 additions & 0 deletions core/src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ pub mod internals {

generate_accessor!(stdlib_contract_equal);

generate_accessor!(predicate_to_ctr);

generate_accessor!(rec_default);
generate_accessor!(rec_force);
}
58 changes: 53 additions & 5 deletions core/src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ pub enum Term {
#[serde(skip)]
Type(Type),

/// A custom contract built using e.g. `std.contract.from_predicate`. Currently, custom
/// contracts can be partial identities (the most general form, which either blame or return
/// the value with potential delayed checks buried inside) or a predicate. Ideally, both
/// would fall under then `CustomContract` node.
///
/// For now, we only put predicates built using `std.contract.from_predicate` here.
///
/// The reason for having a separate node (instead of encoding everything as partial identities
/// under a normal `Fun` node) is that we can leverage the metadata for example to implement a
/// restricted `or` combinator on contracts, which needs to know which contracts are built from
/// predicates, or for better error messages in the future when parametric contracts aren't
/// fully applied ([#1460](https://github.com/tweag/nickel/issues/1460)).
#[serde(skip)]
CustomContract(CustomContract),

/// A term that couldn't be parsed properly. Used by the LSP to handle partially valid
/// programs.
#[serde(skip)]
Expand Down Expand Up @@ -361,13 +376,33 @@ pub enum BindingType {
Revertible(FieldDeps),
}

/// A runtime representation of a contract, as a term ready to be applied via `AppContract`
/// together with its label.
/// A term representing a custom contract.
///
/// This term doesn't currently include generic custom contracts (functions `Label -> Dyn -> Dyn`)
/// for backward compatibility reasons. In the future, we want to have all custom contracts
/// represented as [CustomContract]s, requiring the use of a dedicated constructor:
/// `std.contract.from_function`, `std.contract.from_record`, etc in user code. The requirement of
/// this dedicated constructors is unfortunately a breaking change for existing custom contracts
/// oftetn written as bare functions.
///
/// In the meantime, we can put _some_ contracts here without breaking things (the one that are
/// already built using a special constructor, such as `std.contract.from_predicate`). Maintaining
/// those additional data (if a contract came from `from_predicate` or is a bare function) is
/// useful for implementing some contract operations, such as the `or` combinator, or provide
/// better error messages in some situations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CustomContract {
/// A contract built from a predicate. The argument is a function of type
/// `Dyn -> Bool`.
Predicate(LocIdent, RichTerm),
}

/// A runtime representation of a contract, as a term and a label ready to be applied via
/// [BinaryOp::ContractApply].
#[derive(Debug, PartialEq, Clone)]
pub struct RuntimeContract {
/// The pending contract, can be a function or a record.
/// The pending contract, which can be a function, a type, a [CustomContract] or a record.
pub contract: RichTerm,

/// The blame label.
pub label: Label,
}
Expand Down Expand Up @@ -884,7 +919,11 @@ impl Term {
Term::Bool(_) => Some("Bool".to_owned()),
Term::Num(_) => Some("Number".to_owned()),
Term::Str(_) => Some("String".to_owned()),
Term::Fun(_, _) | Term::FunPattern(_, _) => Some("Function".to_owned()),
Term::Fun(_, _)
| Term::FunPattern(_, _)
// We could print a separate type for predicates. For the time being, we just consider
// it to be the function resulting of `$predicate_to_ctr pred`.
| Term::CustomContract(CustomContract::Predicate(..)) => Some("Function".to_owned()),
Term::Match { .. } => Some("MatchExpression".to_owned()),
Term::Lbl(_) => Some("Label".to_owned()),
Term::Enum(_) => Some("EnumTag".to_owned()),
Expand Down Expand Up @@ -936,6 +975,7 @@ impl Term {
| Term::Fun(..)
// match expressions are function
| Term::Match {..}
| Term::CustomContract(CustomContract::Predicate(..))
| Term::Lbl(_)
| Term::Enum(_)
| Term::EnumVariant {..}
Expand Down Expand Up @@ -1007,6 +1047,7 @@ impl Term {
| Term::Array(..)
| Term::Fun(..)
| Term::FunPattern(..)
| Term::CustomContract(CustomContract::Predicate(..))
| Term::App(_, _)
| Term::Match { .. }
| Term::Var(_)
Expand Down Expand Up @@ -1064,6 +1105,7 @@ impl Term {
| Term::LetPattern(..)
| Term::Fun(..)
| Term::FunPattern(..)
| Term::CustomContract(CustomContract::Predicate(..))
| Term::App(..)
| Term::Op1(..)
| Term::Op2(..)
Expand Down Expand Up @@ -1251,6 +1293,10 @@ pub enum UnaryOp {
/// See `GoDom`.
LabelGoDict,

/// Wrap a predicate function as a [CustomContract::Predicate]. You can think of this primop as
/// one type constructor for contracts.
ContractFromPredicate,

/// Force the evaluation of its argument and proceed with the second.
Seq,

Expand Down Expand Up @@ -1461,6 +1507,7 @@ impl fmt::Display for UnaryOp {
LabelGoCodom => write!(f, "label/go_codom"),
LabelGoArray => write!(f, "label/go_array"),
LabelGoDict => write!(f, "label/go_dict"),
ContractFromPredicate => write!(f, "contract/from_predicate"),
Seq => write!(f, "seq"),
DeepSeq => write!(f, "deep_seq"),
ArrayLength => write!(f, "array/length"),
Expand Down Expand Up @@ -2231,6 +2278,7 @@ impl Traverse<RichTerm> for RichTerm {
}),
Term::Fun(_, t)
| Term::FunPattern(_, t)
| Term::CustomContract(CustomContract::Predicate(_, t))
| Term::EnumVariant { arg: t, .. }
| Term::Op1(_, t)
| Term::Sealed(_, t, _) => t.traverse_ref(f, state),
Expand Down
4 changes: 2 additions & 2 deletions core/src/transform/free_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
term::pattern::*,
term::{
record::{Field, FieldDeps, RecordDeps},
IndexMap, MatchBranch, RichTerm, SharedTerm, StrChunk, Term,
CustomContract, IndexMap, MatchBranch, RichTerm, SharedTerm, StrChunk, Term,
},
typ::{RecordRowF, RecordRows, RecordRowsF, Type, TypeF},
};
Expand Down Expand Up @@ -44,7 +44,7 @@ impl CollectFreeVars for RichTerm {
| Term::Enum(_)
| Term::Import(_)
| Term::ResolvedImport(_) => (),
Term::Fun(id, t) => {
Term::Fun(id, t) | Term::CustomContract(CustomContract::Predicate(id, t)) => {
let mut fresh = HashSet::new();

t.collect_free_vars(&mut fresh);
Expand Down
Loading

0 comments on commit d8cdd77

Please sign in to comment.