diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs index a3d3509fd3..44c79654c3 100644 --- a/lsp/nls/src/requests/completion.rs +++ b/lsp/nls/src/requests/completion.rs @@ -1049,6 +1049,7 @@ mod tests { contracts: Vec::new(), }, opt: false, + not_exported: false, priority: MergePriority::Neutral, }; diff --git a/src/eval/merge.rs b/src/eval/merge.rs index 3bf2152095..db5a13df10 100644 --- a/src/eval/merge.rs +++ b/src/eval/merge.rs @@ -432,6 +432,8 @@ fn merge_fields<'a, C: Cache, I: DoubleEndedIterator + Clone>( // If one of the record requires this field, then it musn't be optional. The // resulting field is optional iff both are. opt: metadata1.opt && metadata2.opt, + // The resulting field will be suppressed from serialization if either of the fields to be merged is. + not_exported: metadata1.not_exported || metadata2.not_exported, priority, }; diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 30c53afe8d..58238747b2 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -149,6 +149,10 @@ SimpleFieldAnnotAtom: FieldMetadata = { opt: true, ..Default::default() }, + "|" "not_exported" => FieldMetadata { + not_exported: true, + ..Default::default() + }, } // A single field metadata annotation. @@ -1009,6 +1013,7 @@ extern { "doc" => Token::Normal(NormalToken::Doc), "optional" => Token::Normal(NormalToken::Optional), "priority" => Token::Normal(NormalToken::Priority), + "not_exported" => Token::Normal(NormalToken::NotExported), "hash" => Token::Normal(NormalToken::OpHash), "serialize" => Token::Normal(NormalToken::Serialize), diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs index c4fd2646e0..7b617811ad 100644 --- a/src/parser/lexer.rs +++ b/src/parser/lexer.rs @@ -263,6 +263,8 @@ pub enum NormalToken<'input> { Priority, #[token("force")] Force, + #[token("not_exported")] + NotExported, #[token("%hash%")] OpHash, diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs index 9ad67120f6..1ced757c23 100644 --- a/src/parser/uniterm.rs +++ b/src/parser/uniterm.rs @@ -189,6 +189,7 @@ impl UniRecord { contracts, }, opt: false, + not_exported: false, priority: MergePriority::Neutral, }, // At this stage, this field should always be empty. It's a run-time thing, and diff --git a/src/serialize.rs b/src/serialize.rs index e769def5a9..08261df85f 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -103,7 +103,7 @@ where S: Serializer, { let mut entries = record - .iter_without_opts() + .iter_serializable() .collect::, _>>() .map_err(|missing_def_err| { Error::custom(format!( @@ -195,7 +195,7 @@ pub fn validate(format: ExportFormat, t: &RichTerm) -> Result<(), SerializationE Null => Err(SerializationError::UnsupportedNull(format, t.clone())), Bool(_) | Num(_) | Str(_) | Enum(_) => Ok(()), Record(record) => { - record.iter_without_opts().try_for_each(|binding| { + record.iter_serializable().try_for_each(|binding| { // unwrap(): terms must be fully evaluated before being validated for // serialization. Otherwise, it's an internal error. let (_, rt) = binding.unwrap_or_else(|err| panic!("encountered field without definition `{}` during pre-serialization validation", err.id)); @@ -426,6 +426,11 @@ mod tests { "{baz | default = {subfoo | default = !false} & {subbar | default = 1 - 1}}", json!({"baz": {"subfoo": true, "subbar": 0}}) ); + + assert_json_eq!( + "{a = {b | default = {}} & {b.c | not_exported = false} & {b.d = true}}", + json!({"a": {"b": {"d": true}}}) + ); } #[test] diff --git a/src/term/record.rs b/src/term/record.rs index 0ab9e151ad..576d4edae3 100644 --- a/src/term/record.rs +++ b/src/term/record.rs @@ -83,6 +83,8 @@ pub struct FieldMetadata { pub annotation: TypeAnnotation, /// If the field is optional. pub opt: bool, + /// If the field is serialized. + pub not_exported: bool, pub priority: MergePriority, } @@ -133,6 +135,7 @@ impl FieldMetadata { contracts: outer.annotation.contracts, }, opt: outer.opt || inner.opt, + not_exported: outer.not_exported || inner.not_exported, priority, } } @@ -359,22 +362,24 @@ impl RecordData { }) } - /// Return an iterator over the fields' values, ignoring optional fields without - /// definition. Fields that aren't optional but yet don't have a definition are mapped to the - /// error `MissingFieldDefError`. - pub fn iter_without_opts( + /// Return an iterator over the fields' values, ignoring optional fields + /// without definition and fields marked as not_exported. Fields that + /// aren't optional but yet don't have a definition are mapped to the error + /// `MissingFieldDefError`. + pub fn iter_serializable( &self, ) -> impl Iterator> { - self.fields - .iter() - .filter_map(|(id, field)| match field.value { - Some(ref v) => Some(Ok((id, v))), + self.fields.iter().filter_map(|(id, field)| { + debug_assert!(field.pending_contracts.is_empty()); + match field.value { + Some(ref v) if !field.metadata.not_exported => Some(Ok((id, v))), None if !field.metadata.opt => Some(Err(MissingFieldDefError { id: *id, metadata: field.metadata.clone(), })), - None => None, - }) + _ => None, + } + }) } /// Get the value of a field. Ignore optional fields without value: trying to get their value