Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce validators for building custom contracts #1970

Merged
merged 8 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cli/tests/snapshot/inputs/errors/validator_custom_error.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# capture = 'stderr'
# command = ['eval']
let Is42 = std.contract.from_validator (fun value =>
if value == 42 then
'Ok value
else
'Error {
message = "Value must be 42",
notes = ["This is a first custom note", "This is a second custom note"]
}
) in
43 | Is42
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: contract broken by a value
Value must be 42
┌─ [INPUTS_PATH]/errors/validator_custom_error.ncl:12:1
12 │ 43 | Is42
│ ^^ ---- expected type
│ │
│ applied to this expression
= This is a first custom note
= This is a second custom note
24 changes: 24 additions & 0 deletions core/src/eval/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,25 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
UnaryOp::ContractFromValidator => {
if matches!(&*t, Term::Fun(..) | Term::Match(_)) {
Ok(Closure {
body: RichTerm::new(
Term::CustomContract(CustomContract::Validator(RichTerm {
term: t,
pos,
})),
pos,
),
env,
})
} else {
Err(mk_type_error!(
"contract/from_validator",
"Function or MatchExpression"
))
}
}
UnaryOp::ContractCustom => {
if matches!(&*t, Term::Fun(..) | Term::Match(_)) {
Ok(Closure {
Expand Down Expand Up @@ -1588,6 +1607,11 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
.with_pos(pos1),
env: env1,
}),
Term::CustomContract(CustomContract::Validator(validator)) => Ok(Closure {
body: mk_app!(internals::validator_to_ctr(), validator.clone())
.with_pos(pos1),
env: env1,
}),
Term::Record(..) => {
let closurized = RichTerm {
term: t1,
Expand Down
2 changes: 2 additions & 0 deletions core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,7 @@ UOp: UnaryOp = {
"label/go_array" => UnaryOp::LabelGoArray,
"label/go_dict" => UnaryOp::LabelGoDict,
"contract/from_predicate" => UnaryOp::ContractFromPredicate,
"contract/from_validator" => UnaryOp::ContractFromValidator,
"contract/custom" => UnaryOp::ContractCustom,
"enum/embed" <Ident> => UnaryOp::EnumEmbed(<>),
"array/map" => UnaryOp::ArrayMap,
Expand Down Expand Up @@ -1515,6 +1516,7 @@ extern {
"contract/array_lazy_app" => Token::Normal(NormalToken::ContractArrayLazyApp),
"contract/record_lazy_app" => Token::Normal(NormalToken::ContractRecordLazyApp),
"contract/from_predicate" => Token::Normal(NormalToken::ContractFromPredicate),
"contract/from_validator" => Token::Normal(NormalToken::ContractFromValidator),
"contract/custom" => Token::Normal(NormalToken::ContractCustom),
"op force" => Token::Normal(NormalToken::OpForce),
"blame" => Token::Normal(NormalToken::Blame),
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 @@ -202,6 +202,8 @@ pub enum NormalToken<'input> {
ContractRecordLazyApp,
#[token("%contract/from_predicate%")]
ContractFromPredicate,
#[token("%contract/from_validator%")]
ContractFromValidator,
#[token("%contract/custom%")]
ContractCustom,
#[token("%blame%")]
Expand Down
32 changes: 19 additions & 13 deletions core/src/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ fn needs_parens_in_type_pos(typ: &Type) -> bool {
term.as_ref(),
Term::Fun(..)
| Term::FunPattern(..)
| Term::CustomContract(CustomContract::Predicate(..))
| Term::CustomContract(_)
| Term::Let(..)
| Term::LetPattern(..)
| Term::Op1(UnaryOp::IfThenElse, _)
Expand Down Expand Up @@ -821,22 +821,28 @@ where
Str(v) => allocator.escaped_string(v).double_quotes(),
StrChunks(chunks) => allocator.chunks(chunks, StringRenderStyle::Multiline),
Fun(id, body) => allocator.function(allocator.as_string(id), body),
CustomContract(ContractNode::PartialIdentity(ctr)) => docs![
allocator,
"%contract/custom%",
docs![allocator, allocator.line(), ctr.pretty(allocator).parens()]
// Format this as the primop application `<custom contract constructor> <contract impl>`.
CustomContract(contract_node) => {
let (constructor, contract) = match contract_node {
ContractNode::Predicate(p) => ("%contract/from_predicate%", p),
ContractNode::Validator(v) => ("%contract/from_validator%", v),
ContractNode::PartialIdentity(pid) => ("%contract/custom%", pid),
};

docs![
allocator,
constructor,
docs![
allocator,
allocator.line(),
contract.pretty(allocator).parens()
]
.nest(2)
.group()
],
]
}
FunPattern(pat, body) => allocator.function(allocator.pat_with_parens(pat), body),
// Format this as the application `std.contract.from_predicate <pred>`.
CustomContract(ContractNode::Predicate(pred)) => docs![
allocator,
"%contract/from_predicate%",
docs![allocator, allocator.line(), pred.pretty(allocator).parens()]
.nest(2)
.group()
],
Lbl(_lbl) => allocator.text("%<label>").append(allocator.line()),
Let(id, rt, body, attrs) => docs![
allocator,
Expand Down
1 change: 1 addition & 0 deletions core/src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub mod internals {
generate_accessor!(stdlib_contract_equal);

generate_accessor!(predicate_to_ctr);
generate_accessor!(validator_to_ctr);

generate_accessor!(rec_default);
generate_accessor!(rec_force);
Expand Down
55 changes: 35 additions & 20 deletions core/src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,23 +216,24 @@ 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.
///
/// Custom contracts built from `std.contract.custom` are stored in this node. Note that, for
/// backward compatibility, users can also use naked functions [Term::Fun] as a custom
/// contract. But this is discouraged and will be deprecated in the future.
///
/// The reason for having a separate 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)).
///
/// Also, custom contracts aren't supposed to be applied using the standard function
/// application, because we need to perform additional bookkeeping upon application. So there's
/// no strong incentive to represent them as naked functions.
/// A custom contract built.
yannham marked this conversation as resolved.
Show resolved Hide resolved
///
/// Custom contracts can be partial identities (the most general form, which either blame or
/// return the value with potential delayed checks buried inside), predicates or validator (see
/// [CustomContract].
///
/// Partial identity are built using `std.contract.custom`. Note that, for backward
/// compatibility, users can also use naked functions ([Term::Fun]) for partial identities
/// instead. But this is discouraged and will be deprecated in the future. Indeed, custom
/// contracts aren't supposed to be applied using the standard function application, because we
/// need to perform additional bookkeeping upon application, so there's no good reason to
/// represent them as naked functions.
///
/// Having a separate node lets us leverage the additional information for example to implement
/// a restricted `or` combinator on contracts, which needs to know which contracts support
/// booleans operations (predicates and validators), 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),

Expand Down Expand Up @@ -401,12 +402,16 @@ pub enum BindingType {
/// better error messages in some situations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CustomContract {
/// A generic custom contract, represented as a partial identity function of type `Label -> Dyn
/// -> Dyn`.
PartialIdentity(RichTerm),
/// A contract built from a predicate. The argument is a function of type
/// `Dyn -> Bool`.
Predicate(RichTerm),
/// A contract built from a validator. A validator is a function of type `Dyn -> [| 'Ok, 'Error
/// ReifiedLabel |]` where `ReifiedLabel` is a record with error reporting data (a message,
/// notes, etc.)
Validator(RichTerm),
/// A generic custom contract, represented as a partial identity function of type `Label -> Dyn
/// -> Dyn`.
PartialIdentity(RichTerm),
}

/// A runtime representation of a contract, as a term and a label ready to be applied via
Expand Down Expand Up @@ -1309,6 +1314,10 @@ pub enum UnaryOp {
/// a type constructor for custom contracts.
ContractFromPredicate,

/// Wrap a validator function as a [CustomContract]. You can think of this primop as a type
/// constructor for custom contracts.
ContractFromValidator,

/// Wrap a partial identity function as a [CustomContract]. You can think of this primop as a
/// type constructor for contracts.
ContractCustom,
Expand Down Expand Up @@ -1524,6 +1533,7 @@ impl fmt::Display for UnaryOp {
LabelGoArray => write!(f, "label/go_array"),
LabelGoDict => write!(f, "label/go_dict"),
ContractFromPredicate => write!(f, "contract/from_predicate"),
ContractFromValidator => write!(f, "contract/from_validator"),
ContractCustom => write!(f, "contract/custom"),
Seq => write!(f, "seq"),
DeepSeq => write!(f, "deep_seq"),
Expand Down Expand Up @@ -2109,6 +2119,10 @@ impl Traverse<RichTerm> for RichTerm {
let t = t.traverse(f, order)?;
RichTerm::new(Term::CustomContract(CustomContract::Predicate(t)), pos)
}
Term::CustomContract(CustomContract::Validator(t)) => {
let t = t.traverse(f, order)?;
RichTerm::new(Term::CustomContract(CustomContract::Validator(t)), pos)
}
Term::CustomContract(CustomContract::PartialIdentity(t)) => {
let t = t.traverse(f, order)?;
RichTerm::new(
Expand Down Expand Up @@ -2308,6 +2322,7 @@ impl Traverse<RichTerm> for RichTerm {
Term::Fun(_, t)
| Term::FunPattern(_, t)
| Term::CustomContract(CustomContract::Predicate(t))
| Term::CustomContract(CustomContract::Validator(t))
| Term::CustomContract(CustomContract::PartialIdentity(t))
| Term::EnumVariant { arg: t, .. }
| Term::Op1(_, t)
Expand Down
1 change: 1 addition & 0 deletions core/src/transform/free_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ impl CollectFreeVars for RichTerm {
| Term::Sealed(_, t, _)
| Term::EnumVariant { arg: t, .. }
| Term::CustomContract(CustomContract::Predicate(t))
| Term::CustomContract(CustomContract::Validator(t))
| Term::CustomContract(CustomContract::PartialIdentity(t)) => {
t.collect_free_vars(free_vars)
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/typecheck/mk_uniftype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ macro_rules! mk_uty_arrow {
/// Multi-ary enum row constructor for types implementing `Into<TypeWrapper>`.
/// `mk_uty_enum_row!(id1, .., idn; tail)` correspond to `[| 'id1, .., 'idn; tail |]. With the
/// addition of algebraic data types (enum variants), individual rows can also take an additional
/// type parameter, specificed as a tuple: for example, `mk_uty_enum_row!(id1, (id2, ty2); tail)`
/// type parameter, specified as a tuple: for example, `mk_uty_enum_row!(id1, (id2, ty2); tail)`
/// is `[| 'id1, 'id2 ty2; tail |]`.
#[macro_export]
macro_rules! mk_uty_enum_row {
Expand Down
21 changes: 15 additions & 6 deletions core/src/typecheck/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1600,8 +1600,9 @@ fn walk<V: TypecheckVisitor>(
Term::EnumVariant { arg: t, ..}
| Term::Sealed(_, t, _)
| Term::Op1(_, t)
| Term::CustomContract(CustomContract::Predicate(t)) | Term::CustomContract(CustomContract::PartialIdentity(t))
=> walk(state, ctxt.clone(), visitor, t),
| Term::CustomContract(CustomContract::Predicate(t))
| Term::CustomContract(CustomContract::Validator(t))
| Term::CustomContract(CustomContract::PartialIdentity(t)) => walk(state, ctxt.clone(), visitor, t),
Term::Op2(_, t1, t2) => {
walk(state, ctxt.clone(), visitor, t1)?;
walk(state, ctxt, visitor, t2)
Expand Down Expand Up @@ -1925,7 +1926,7 @@ fn check<V: TypecheckVisitor>(
// Additionally, because this rule can't produce a polymorphic type (it produces a `Dyn`,
// or morally a `Contract` type, if we had one), we don't lose anything by making it a
// check rule, as for e.g. literals.
Term::CustomContract(CustomContract::Predicate(body)) => {
Term::CustomContract(CustomContract::Predicate(t)) => {
// The overall type of a custom contract is currently `Dyn`, as we don't have a better
// one.
ty.unify(mk_uniftype::dynamic(), state, &ctxt)
Expand All @@ -1936,13 +1937,21 @@ fn check<V: TypecheckVisitor>(
state,
ctxt,
visitor,
body,
t,
mk_uniftype::arrow(mk_uniftype::dynamic(), mk_uniftype::bool()),
)
}
Term::CustomContract(CustomContract::Validator(t)) => {
// The overall type of a custom contract is currently `Dyn`, as we don't have a better
// one.
ty.unify(mk_uniftype::dynamic(), state, &ctxt)
.map_err(|err| err.into_typecheck_err(state, rt.pos))?;

check(state, ctxt, visitor, t, operation::validator_type())
}
// See [^predicate-is-check]. We took `Predicate` as an example, but this reasoning applies
// to other kind of custom contracts, such as `PartialIdentity`.
Term::CustomContract(CustomContract::PartialIdentity(body)) => {
Term::CustomContract(CustomContract::PartialIdentity(t)) => {
// The overall type of a custom contract is currently `Dyn`, as we don't have a better
// one.
ty.unify(mk_uniftype::dynamic(), state, &ctxt)
Expand All @@ -1956,7 +1965,7 @@ fn check<V: TypecheckVisitor>(
mk_uniftype::dynamic()
);

check(state, ctxt, visitor, body, partial_id_type)
check(state, ctxt, visitor, t, partial_id_type)
}
Term::Array(terms, _) => {
let ty_elts = state.table.fresh_type_uvar(ctxt.var_level);
Expand Down
31 changes: 31 additions & 0 deletions core/src/typecheck/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ pub fn get_uop_type(
mk_uniftype::dynamic(),
),
// Morally returns a contract, but we don't have a proper type for that yet
// <validator_type()> -> Dyn (see `validator_type()` below for more details)
UnaryOp::ContractFromValidator => (validator_type(), mk_uniftype::dynamic()),
// Morally returns a contract, but we don't have a proper type for that yet
// (Dyn -> Dyn -> Bool) -> Dyn
UnaryOp::ContractCustom => (
mk_uty_arrow!(
Expand Down Expand Up @@ -562,3 +565,31 @@ pub fn get_nop_type(
),
})
}

/// Return the type of a validator, which is one way of representing a custom contract. This static
/// type is more rigid than the actual values accepted by `std.contract.from_validator`, because we
/// can't represent optional fields in the type system. But it's ok to be stricter in statically
/// typed code.
///
/// Also remember that custom contracts shouldn't appear directly in the source code of Nickel:
/// they are built using `std.contract.from_xxx` and `std.contract.custom` functions. We implement
/// typechecking for them mostly because we can (to avoid an `unimplemented!` or a `panic!`), but
/// we don't expect this case to trigger at the moment, so it isn't of the utmost importance.
///
/// The result represents the type `Dyn -> [| 'Ok, 'Error { message: String, notes: Array
/// String } |]`.
pub fn validator_type() -> UnifType {
mk_uty_arrow!(
mk_uniftype::dynamic(),
mk_uty_enum!(
"Ok",
(
"Error",
mk_uty_record!(
("message", mk_uniftype::str()),
("notes", mk_uniftype::array(mk_uniftype::str()))
)
)
)
)
}
Loading