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

Add Equal contract to the stdlib #1203

Merged
merged 3 commits into from
Mar 28, 2023
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
9 changes: 3 additions & 6 deletions src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ macro_rules! generate_accessor {
};
}

/// Accessors to the builtin contracts.
pub mod contract {
/// Accessors to the builtin contracts and other internals that aren't accessible from user code.
pub mod internals {
use super::*;

// `dyn` is a reserved keyword in rust
Expand All @@ -135,10 +135,7 @@ pub mod contract {
generate_accessor!(forall_tail);
generate_accessor!(dyn_tail);
generate_accessor!(empty_tail);
}

pub mod internals {
use super::*;
generate_accessor!(contract_equal);

generate_accessor!(rec_default);
generate_accessor!(rec_force);
Expand Down
38 changes: 19 additions & 19 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ fn get_var_contract(

impl EnumRows {
fn subcontract(&self) -> Result<RichTerm, UnboundTypeVariableError> {
use crate::stdlib::contract;
use crate::stdlib::internals;

let mut cases = HashMap::new();
let mut has_tail = false;
Expand Down Expand Up @@ -752,14 +752,14 @@ impl EnumRows {
mk_app!(
Term::Match {
cases,
default: Some(mk_app!(contract::enum_fail(), mk_term::var(label_arg))),
default: Some(mk_app!(internals::enum_fail(), mk_term::var(label_arg))),
},
mk_term::var(value_arg)
)
};
let case = mk_fun!(label_arg, value_arg, case_body);

Ok(mk_app!(contract::enums(), case))
Ok(mk_app!(internals::enums(), case))
}

pub fn iter(&self) -> EnumRowsIterator<EnumRows> {
Expand All @@ -775,7 +775,7 @@ impl RecordRows {
pol: Polarity,
sy: &mut i32,
) -> Result<RichTerm, UnboundTypeVariableError> {
use crate::stdlib::contract;
use crate::stdlib::internals;

// We begin by building a record whose arguments are contracts
// derived from the types of the statically known fields.
Expand All @@ -794,16 +794,16 @@ impl RecordRows {
// Now that we've dealt with the row extends, we just need to
// work out the tail.
let tail = match &rrows.0 {
RecordRowsF::Empty => contract::empty_tail(),
RecordRowsF::TailDyn => contract::dyn_tail(),
RecordRowsF::Empty => internals::empty_tail(),
RecordRowsF::TailDyn => internals::dyn_tail(),
RecordRowsF::TailVar(id) => get_var_contract(&vars, id)?,
// Safety: the while above excludes that `tail` can have the form `Extend`.
RecordRowsF::Extend { .. } => unreachable!(),
};

let rec = RichTerm::from(Term::Record(RecordData::with_field_values(fcs)));

Ok(mk_app!(contract::record(), rec, tail))
Ok(mk_app!(internals::record(), rec, tail))
}

/// Find a nested binding in a record row type. The nested field is given as a list of
Expand Down Expand Up @@ -903,19 +903,19 @@ impl Types {
pol: Polarity,
sy: &mut i32,
) -> Result<RichTerm, UnboundTypeVariableError> {
use crate::stdlib::contract;
use crate::stdlib::internals;

let ctr = match self.types {
TypeF::Dyn => contract::dynamic(),
TypeF::Number => contract::num(),
TypeF::Bool => contract::bool(),
TypeF::String => contract::string(),
TypeF::Dyn => internals::dynamic(),
TypeF::Number => internals::num(),
TypeF::Bool => internals::bool(),
TypeF::String => internals::string(),
//TODO: optimization: have a specialized contract for `Array Dyn`, to avoid mapping an
//always successful contract on each element.
TypeF::Array(ref ty) => mk_app!(contract::array(), ty.subcontract(vars, pol, sy)?),
TypeF::Array(ref ty) => mk_app!(internals::array(), ty.subcontract(vars, pol, sy)?),
TypeF::Symbol => panic!("Are you trying to check a Sym at runtime?"),
TypeF::Arrow(ref s, ref t) => mk_app!(
contract::func(),
internals::func(),
s.subcontract(vars.clone(), pol.flip(), sy)?,
t.subcontract(vars, pol, sy)?
),
Expand All @@ -930,14 +930,14 @@ impl Types {

let sealing_key = Term::SealingKey(*sy);
let contract = match var_kind {
Type => mk_app!(contract::forall_var(), sealing_key.clone()),
EnumRows | RecordRows => mk_app!(contract::forall_tail(), sealing_key.clone()),
Type => mk_app!(internals::forall_var(), sealing_key.clone()),
EnumRows | RecordRows => mk_app!(internals::forall_tail(), sealing_key.clone()),
};
vars.insert(*var, contract);

*sy += 1;
mk_app!(
contract::forall(),
internals::forall(),
sealing_key,
Term::from(pol),
body.subcontract(vars, pol, sy)?
Expand All @@ -946,9 +946,9 @@ impl Types {
TypeF::Enum(ref erows) => erows.subcontract()?,
TypeF::Record(ref rrows) => rrows.subcontract(vars, pol, sy)?,
TypeF::Dict(ref ty) => {
mk_app!(contract::dyn_record(), ty.subcontract(vars, pol, sy)?)
mk_app!(internals::dyn_record(), ty.subcontract(vars, pol, sy)?)
}
TypeF::Wildcard(_) => contract::dynamic(),
TypeF::Wildcard(_) => internals::dynamic(),
};

Ok(ctr)
Expand Down
49 changes: 49 additions & 0 deletions stdlib/contract.ncl
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
{
contract = {
Equal
| doc m%"
`Equal` is a contract enforcing equality to a given constant.

`Equal` is lazy over arrays and records. When checking such values,
`Equal` doesn't test for equality of elements right away (it just tests
that the size is the same for arrays and that fields are the same for
records), but returns a new value where an equality contract has been
pushed down each element.

# Example

```nickel
1 + 4 | Equal 5
=> 5
4 | Equal 5
=> ERROR (contract broken by a value)
```
"%
= fun constant =>
%typeof% constant
|> match {
`Record =>
#TODO: lazy version for records. The difficulty is not to make it lazy
#right now, but to make it preserve recursivity as well. We might need
#something like a recursivity-preserving map, or a primop for
# per-field contract application, like %array_lazy_assume%.
from_predicate ((==) constant),
`Array =>
fun label' value =>
if %typeof% value == `Array then
let value_length = %length% value in
if value_length == %length% constant then
%generate% value_length (fun i =>
contract.apply (Equal (%elem_at% constant i)) label' value)
else
label'
|> label.with_message "array length mismatch (expected `%{%to_str% (%length% constant)}`, got `%{%to_str% value_length}`)"
|> label.append_note "`Equal some_array` requires that the checked value is equal to the array `some_array`, but their lengths differ."
|> blame
else
label'
|> label.with_message "expected Array, got %{%to_str% (%typeof% value)}"
|> label.append_note "`Equal some_array` requires that the checked value is equal to the array `some_array`, but the provided value isn't an array."
|> blame,
# Outside of lazy data structure, we just use (==)
_ => from_predicate ((==) constant),
},

blame
| doc m%"
Raise blame for a given label.
Expand Down
7 changes: 7 additions & 0 deletions stdlib/internals.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,11 @@

"$rec_force" = fun value => %rec_force% (%force% value),
"$rec_default" = fun value => %rec_default% (%force% value),

# Provide access to contract.Equal within the initial environement. Merging
# makes use of `contract.Equal`, but it can't blindly substitute such an
# expression, because `contract` might have been redefined locally. Putting it
# in an internal value prefixed with `$` makes it accessible from the initial
# environment and prevents it from being shadowed.
"$stdlib_contract_equal" = contract.Equal,
}
14 changes: 14 additions & 0 deletions stdlib/record.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -195,5 +195,19 @@
|> to_array
|> array.filter (fun { field, value } => f field value)
|> from_array,

length
: forall a. {_: a} -> Number
| doc m%"
Returns the number of fields in a record. This count doesn't include
fields both marked as optional and without a definition.

Because of the need to filter empty optional fields, the cost of
`length` is linear in the size of the record.
"%
= fun record =>
record
|> fields
|> array.length,
}
}