1
1
mirror of https://github.com/tweag/nickel.git synced 2024-09-20 16:08:14 +03:00

Improve field access and definition syntax

Support an attribute path syntax to define nested records more easily.
Simplify interpolated field names syntax, which just uses vanilla
strings. Allow piecewise record definitions by merging the potential multiple
values for one field.
This commit is contained in:
Yann Hamdaoui 2021-02-19 11:06:40 +01:00
parent 851145821c
commit d0b246d210
2 changed files with 114 additions and 47 deletions

View File

@ -6,12 +6,12 @@ use crate::mk_app;
use crate::types::{Types, AbsType};
use super::ExtendedTerm;
use super::utils::{StringKind, mk_span, mk_label, strip_indent, SwitchCase,
strip_indent_doc};
FieldPathElem, strip_indent_doc, build_record, elaborate_field_path,
ChunkLiteralPart};
use std::ffi::OsString;
use super::lexer::{Token, NormalToken, StringToken, MultiStringToken, LexicalError};
use std::collections::HashMap;
use std::cmp::min;
use either::*;
use codespan::FileId;
grammar<'input>(src_id: FileId, );
@ -242,25 +242,9 @@ Atom: RichTerm = {
<StrChunks>,
Ident => RichTerm::from(Term::Var(<>)),
"`" <Ident> => RichTerm::from(Term::Enum(<>)),
"{" <fields: (RecordField ";")*> <last: RecordField?> "}" => {
let mut static_map = HashMap::new();
let mut dynamic_fields = Vec::new();
fields
.into_iter()
.map(|x| x.0)
.chain(last.into_iter())
.for_each(|field| match field {
Left((id, t)) => { static_map.insert(id, t) ;}
Right(t) => dynamic_fields.push(t),
});
let static_rec = RichTerm::from(Term::RecRecord(static_map));
dynamic_fields.into_iter().fold(static_rec, |rec, field| {
let (id_t, t) = field;
mk_app!(mk_term::op2(BinaryOp::DynExtend(), id_t, rec), t)
})
"{" <fields: (<RecordField> ";")*> <last: RecordField?> "}" => {
let fields = fields.into_iter().chain(last.into_iter());
RichTerm::from(build_record(fields))
},
"[" <terms: (SpTerm<Atom> ",")*> <last: Term?> "]" => {
let terms : Vec<RichTerm> = terms.into_iter()
@ -270,8 +254,8 @@ Atom: RichTerm = {
}
};
RecordField: Either<(Ident, RichTerm), (RichTerm, RichTerm)> = {
<id: Ident> <ann: TypeAnnot?> "=" <t: Term> => {
RecordField: (FieldPathElem, RichTerm) = {
<path: FieldPath> <ann: TypeAnnot?> "=" <t: Term> => {
let t = if let Some((l, ty, r)) = ann {
let pos = t.pos.clone();
RichTerm::new(Term::Promise(ty.clone(), mk_label(ty, src_id, l, r), t), pos)
@ -280,29 +264,33 @@ RecordField: Either<(Ident, RichTerm), (RichTerm, RichTerm)> = {
t
};
Either::Left((id, t))
elaborate_field_path(path, t)
},
<l: @L> <id: Ident> <meta: MetaAnnot> <r: @R> <t: ("=" <Term>)?> => {
<l: @L> <path: FieldPath> <meta: MetaAnnot> <r: @R> <t: ("=" <Term>)?> => {
let mut meta = meta;
let pos = t.as_ref()
.map(|t| t.pos.clone())
.unwrap_or(Some(mk_span(src_id, l, r)));
meta.value = t;
Either::Left((id, RichTerm::new(Term::MetaValue(meta), pos)))
},
"$" <id: SpTerm<Atom>> <ann: TypeAnnot?> "=" <t: Term> => {
let t = if let Some((l, ty, r)) = ann {
let pos = t.pos.clone();
RichTerm::new(Term::Promise(ty.clone(), mk_label(ty, src_id, l, r), t), pos)
}
else {
t
};
Either::Right((id, t))
let t = RichTerm::new(Term::MetaValue(meta), pos);
elaborate_field_path(path, t)
}
}
FieldPath: Vec<FieldPathElem> = {
<elems: (<FieldPathElem> ".")*> <last: FieldPathElem> => {
let mut elems = elems;
elems.push(last);
elems
}
};
FieldPathElem: FieldPathElem = {
<Ident> => FieldPathElem::Ident(<>),
"\"" <ChunkLiteral> "\"" => FieldPathElem::Ident(Ident(<>)),
"$" <id: SpTerm<Atom>> => FieldPathElem::Expr(<>),
};
Pattern: Ident = {
Ident,
};
@ -356,8 +344,8 @@ ChunkLiteral : String =
<parts: ChunkLiteralPart+> => {
parts.into_iter().fold(String::new(), |mut acc, part| {
match part {
Either::Left(s) => acc.push_str(s),
Either::Right(c) => acc.push(c),
ChunkLiteralPart::Str(s) => acc.push_str(s),
ChunkLiteralPart::Char(c) => acc.push(c),
};
acc
@ -370,13 +358,13 @@ HashBrace = { "#{", "multstr #{" };
Str: String = "\"" <s: ChunkLiteral> "\"" => s;
ChunkLiteralPart: Either<&'input str, char> = {
"str literal" => Either::Left(<>),
"str #" => Either::Left(<>),
"multstr literal" => Either::Left(<>),
"false interpolation" => Either::Left(<>),
"false end" => Either::Left(<>),
"str esc char" => Either::Right(<>),
ChunkLiteralPart: ChunkLiteralPart<'input> = {
"str literal" => ChunkLiteralPart::Str(<>),
"str #" => ChunkLiteralPart::Str(<>),
"multstr literal" => ChunkLiteralPart::Str(<>),
"false interpolation" => ChunkLiteralPart::Str(<>),
"false end" => ChunkLiteralPart::Str(<>),
"str esc char" => ChunkLiteralPart::Char(<>),
};
UOp: UnaryOp = {

View File

@ -1,10 +1,13 @@
use crate::identifier::Ident;
/// A few helpers to generate position spans and labels easily during parsing
use crate::label::Label;
use crate::mk_app;
use crate::position::RawSpan;
use crate::term::{RichTerm, StrChunk};
use crate::term::{make as mk_term, BinaryOp, RichTerm, StrChunk, Term};
use crate::types::Types;
use codespan::FileId;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
/// Distinguish between the standard string separators `"`/`"` and the multi-line string separators
/// `m#"`/`"#m` in the parser.
@ -21,6 +24,82 @@ pub enum SwitchCase {
Default(RichTerm),
}
/// Left hand side of a record field declaration.
#[derive(Clone, Debug)]
pub enum FieldPathElem {
/// A static declaration, quoted or not: `{ foo = .. }` or `{ "$some'field" = .. }`
Ident(Ident),
/// An interpolated expression: `{ $(x ++ "foo") = .. }`
Expr(RichTerm),
}
/// String chunk literal, being either a string or a single char.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ChunkLiteralPart<'input> {
Str(&'input str),
Char(char),
}
/// Elaborate a record field definition made as a path, such as `a.b.c.d = foo`, into a regular
/// flat definition `a = { .. }`.
///
/// # Preconditions
/// - path must be **non-empty**, otherwise this function panics
pub fn elaborate_field_path(
path: Vec<FieldPathElem>,
content: RichTerm,
) -> (FieldPathElem, RichTerm) {
let mut it = path.into_iter();
let fst = it.next().unwrap();
let content = it.rev().fold(content, |acc, path_elem| match path_elem {
FieldPathElem::Ident(id) => {
let mut map = HashMap::new();
map.insert(id, acc);
Term::Record(map).into()
}
FieldPathElem::Expr(exp) => {
let empty = Term::Record(HashMap::new());
mk_app!(mk_term::op2(BinaryOp::DynExtend(), exp, empty), acc)
}
});
(fst, content)
}
/// Build a record from a list of field definitions. If fields are defined several times, the
/// definitions are merged.
pub fn build_record<I>(fields: I) -> Term
where
I: IntoIterator<Item = (FieldPathElem, RichTerm)>,
{
let mut static_map = HashMap::new();
let mut dynamic_fields = Vec::new();
fields.into_iter().for_each(|field| match field {
(FieldPathElem::Ident(id), t) => {
match static_map.entry(id) {
Entry::Occupied(mut occpd) => {
// temporary putting null in the entry to take the previous value.
let prev = occpd.insert(Term::Null.into());
occpd.insert(mk_term::op2(BinaryOp::Merge(), prev, t));
}
Entry::Vacant(vac) => {
vac.insert(t);
}
}
}
(FieldPathElem::Expr(e), t) => dynamic_fields.push((e, t)),
});
dynamic_fields
.into_iter()
.fold(Term::RecRecord(static_map), |rec, field| {
let (id_t, t) = field;
Term::App(mk_term::op2(BinaryOp::DynExtend(), id_t, rec), t)
})
}
/// Make a span from parser byte offsets.
pub fn mk_span(src_id: FileId, l: usize, r: usize) -> RawSpan {
RawSpan {