Skip to content

Commit

Permalink
Add empty optional fields-aware record operator variants (#1876)
Browse files Browse the repository at this point in the history
* Add fields_all primop (doesn't ignore empty opts)

* Add empty-opt aware record op variants to stdlib

The design guideline around empty optional fields has been to make most
normal record operation ignore them, to avoid issues like trying to list
the field of a record and access them just to fail on an empty optional
introduced by a contract, which we shouldn't care about.

However, in some cases (typically operating on record contracts), users
might want to actually take empty optional fields into account. Most
primops were already existing in two variants internally (ignore empty
opts and consider all fields). This commit just makes them available as
clearly documented stdlib functions.

* Add `has_field_all`, change `_all` -> `_with_opts`

The previous commit introducing variants of record operators that don't
ignore empty optional fields missed `has_field`, whose variant is added
by this commit.

As decided by the team in the weekly meeting, the `_all` suffix
introduced in the previous commit has been changed to `_with_opts`,
which is a bit longer but less confusing and more explicit.
  • Loading branch information
yannham authored Mar 29, 2024
1 parent 4dc7696 commit b2e7713
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 35 deletions.
4 changes: 2 additions & 2 deletions core/src/eval/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,10 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
UnaryOp::FieldsOf() => match_sharedterm!(match (t) {
UnaryOp::FieldsOf(op_kind) => match_sharedterm!(match (t) {
Term::Record(record) => {
let fields_as_terms: Array = record
.field_names(RecordOpKind::IgnoreEmptyOpt)
.field_names(op_kind)
.into_iter()
.map(mk_term::string)
.collect();
Expand Down
20 changes: 11 additions & 9 deletions core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,8 @@ UOp: UnaryOp = {
"deep_seq" => UnaryOp::DeepSeq(),
"op force" => UnaryOp::Force{ ignore_not_exported: false },
"length" => UnaryOp::ArrayLength(),
"fields" => UnaryOp::FieldsOf(),
"fields" => UnaryOp::FieldsOf(RecordOpKind::IgnoreEmptyOpt),
"fields_with_opts" => UnaryOp::FieldsOf(RecordOpKind::ConsiderAllFields),
"values" => UnaryOp::ValuesOf(),
"str_trim" => UnaryOp::StrTrim(),
"str_chars" => UnaryOp::StrChars(),
Expand Down Expand Up @@ -1063,9 +1064,9 @@ BOpPre: BinaryOp = {
"seal" => BinaryOp::Seal(),
"go_field" => BinaryOp::GoField(),
"has_field" => BinaryOp::HasField(RecordOpKind::IgnoreEmptyOpt),
"has_field_all" => BinaryOp::HasField(RecordOpKind::ConsiderAllFields),
"has_field_with_opts" => BinaryOp::HasField(RecordOpKind::ConsiderAllFields),
"field_is_defined" => BinaryOp::FieldIsDefined(RecordOpKind::IgnoreEmptyOpt),
"field_is_defined_all" => BinaryOp::FieldIsDefined(RecordOpKind::ConsiderAllFields),
"field_is_defined_with_opts" => BinaryOp::FieldIsDefined(RecordOpKind::ConsiderAllFields),
"elem_at" => BinaryOp::ArrayElemAt(),
"hash" => BinaryOp::Hash(),
"serialize" => BinaryOp::Serialize(),
Expand All @@ -1079,14 +1080,14 @@ BOpPre: BinaryOp = {
pending_contracts: Default::default(),
op_kind: RecordOpKind::IgnoreEmptyOpt,
},
"record_insert_all" => BinaryOp::DynExtend {
"record_insert_with_opts" => BinaryOp::DynExtend {
ext_kind: RecordExtKind::WithValue,
metadata: Default::default(),
pending_contracts: Default::default(),
op_kind: RecordOpKind::ConsiderAllFields,
},
"record_remove" => BinaryOp::DynRemove(RecordOpKind::IgnoreEmptyOpt),
"record_remove_all" => BinaryOp::DynRemove(RecordOpKind::ConsiderAllFields),
"record_remove_with_opts" => BinaryOp::DynRemove(RecordOpKind::ConsiderAllFields),
"label_with_message" => BinaryOp::LabelWithMessage(),
"label_with_notes" => BinaryOp::LabelWithNotes(),
"label_append_note" => BinaryOp::LabelAppendNote(),
Expand Down Expand Up @@ -1286,15 +1287,16 @@ extern {
"record_map" => Token::Normal(NormalToken::RecordMap),
"record_empty_with_tail" => Token::Normal(NormalToken::RecordEmptyWithTail),
"record_insert" => Token::Normal(NormalToken::RecordInsert),
"record_insert_all" => Token::Normal(NormalToken::RecordInsertAll),
"record_insert_with_opts" => Token::Normal(NormalToken::RecordInsertWithOpts),
"record_remove" => Token::Normal(NormalToken::RecordRemove),
"record_remove_all" => Token::Normal(NormalToken::RecordRemoveAll),
"record_remove_with_opts" => Token::Normal(NormalToken::RecordRemoveWithOpts),
"record_seal_tail" => Token::Normal(NormalToken::RecordSealTail),
"record_unseal_tail" => Token::Normal(NormalToken::RecordUnsealTail),
"seq" => Token::Normal(NormalToken::Seq),
"deep_seq" => Token::Normal(NormalToken::DeepSeq),
"length" => Token::Normal(NormalToken::Length),
"fields" => Token::Normal(NormalToken::FieldsOf),
"fields_with_opts" => Token::Normal(NormalToken::FieldsOfWithOpts),
"values" => Token::Normal(NormalToken::ValuesOf),
"pow" => Token::Normal(NormalToken::Pow),
"rec_force_op" => Token::Normal(NormalToken::RecForceOp),
Expand All @@ -1304,9 +1306,9 @@ extern {
"lookup_type_variable" => Token::Normal(NormalToken::LookupTypeVar),

"has_field" => Token::Normal(NormalToken::HasField),
"has_field_all" => Token::Normal(NormalToken::HasFieldAll),
"has_field_with_opts" => Token::Normal(NormalToken::HasFieldWithOpts),
"field_is_defined" => Token::Normal(NormalToken::FieldIsDefined),
"field_is_defined_all" => Token::Normal(NormalToken::FieldIsDefinedAll),
"field_is_defined_with_opts" => Token::Normal(NormalToken::FieldIsDefinedWithOpts),
"map" => Token::Normal(NormalToken::Map),
"generate" => Token::Normal(NormalToken::ArrayGen),
"elem_at" => Token::Normal(NormalToken::ElemAt),
Expand Down
19 changes: 11 additions & 8 deletions core/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,12 @@ pub enum NormalToken<'input> {
RecordMap,
#[token("%record_insert%")]
RecordInsert,
#[token("%record_insert_all%")]
RecordInsertAll,
#[token("%record_insert_with_opts%")]
RecordInsertWithOpts,
#[token("%record_remove%")]
RecordRemove,
#[token("%record_remove_all%")]
RecordRemoveAll,
#[token("%record_remove_with_opts%")]
RecordRemoveWithOpts,
#[token("%record_empty_with_tail%")]
RecordEmptyWithTail,
#[token("%record_seal_tail%")]
Expand All @@ -249,6 +249,9 @@ pub enum NormalToken<'input> {
Length,
#[token("%fields%")]
FieldsOf,
#[token("%fields_with_opts%")]
FieldsOfWithOpts,

#[token("%values%")]
ValuesOf,
#[token("%pow%")]
Expand All @@ -258,8 +261,8 @@ pub enum NormalToken<'input> {

#[token("%has_field%")]
HasField,
#[token("%has_field_all%")]
HasFieldAll,
#[token("%has_field_with_opts%")]
HasFieldWithOpts,
#[token("%map%")]
Map,
#[token("%elem_at%")]
Expand All @@ -272,8 +275,8 @@ pub enum NormalToken<'input> {
RecDefaultOp,
#[token("%field_is_defined%")]
FieldIsDefined,
#[token("%field_is_defined_all%")]
FieldIsDefinedAll,
#[token("%field_is_defined_with_opts%")]
FieldIsDefinedWithOpts,

#[token("merge")]
Merge,
Expand Down
15 changes: 9 additions & 6 deletions core/src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ pub enum UnaryOp {
ChunksConcat(),

/// Return the names of the fields of a record as a string array.
FieldsOf(),
FieldsOf(RecordOpKind),

/// Return the values of the fields of a record as an array.
ValuesOf(),
Expand Down Expand Up @@ -1425,7 +1425,8 @@ impl fmt::Display for UnaryOp {
ArrayLength() => write!(f, "length"),
ArrayGen() => write!(f, "generate"),
ChunksConcat() => write!(f, "chunks_concat"),
FieldsOf() => write!(f, "fields"),
FieldsOf(RecordOpKind::IgnoreEmptyOpt) => write!(f, "fields"),
FieldsOf(RecordOpKind::ConsiderAllFields) => write!(f, "fields_with_opts"),
ValuesOf() => write!(f, "values"),
StrTrim() => write!(f, "str_trim"),
StrChars() => write!(f, "str_chars"),
Expand Down Expand Up @@ -1703,14 +1704,16 @@ impl fmt::Display for BinaryOp {
DynExtend {
op_kind: RecordOpKind::ConsiderAllFields,
..
} => write!(f, "record_insert_all"),
} => write!(f, "record_insert_with_opts"),
DynRemove(RecordOpKind::IgnoreEmptyOpt) => write!(f, "record_remove"),
DynRemove(RecordOpKind::ConsiderAllFields) => write!(f, "record_remove_all"),
DynRemove(RecordOpKind::ConsiderAllFields) => write!(f, "record_remove_with_opts"),
DynAccess() => write!(f, "dyn_access"),
HasField(RecordOpKind::IgnoreEmptyOpt) => write!(f, "has_field"),
HasField(RecordOpKind::ConsiderAllFields) => write!(f, "has_field_all"),
HasField(RecordOpKind::ConsiderAllFields) => write!(f, "has_field_with_opts"),
FieldIsDefined(RecordOpKind::IgnoreEmptyOpt) => write!(f, "field_is_defined"),
FieldIsDefined(RecordOpKind::ConsiderAllFields) => write!(f, "field_is_defined_all"),
FieldIsDefined(RecordOpKind::ConsiderAllFields) => {
write!(f, "field_is_defined_with_opts")
}
ArrayConcat() => write!(f, "array_concat"),
ArrayElemAt() => write!(f, "elem_at"),
Merge(_) => write!(f, "merge"),
Expand Down
2 changes: 1 addition & 1 deletion core/src/typecheck/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub fn get_uop_type(
// This should not happen, as ChunksConcat() is only produced during evaluation.
UnaryOp::ChunksConcat() => panic!("cannot type ChunksConcat()"),
// forall a. { _: a } -> Array Str
UnaryOp::FieldsOf() => {
UnaryOp::FieldsOf(_) => {
let ty_a = state.table.fresh_type_uvar(var_level);

(
Expand Down
Loading

0 comments on commit b2e7713

Please sign in to comment.