diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop index 0c37b625..50d598e5 100644 --- a/src/grammar.lalrpop +++ b/src/grammar.lalrpop @@ -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 = { , Ident => RichTerm::from(Term::Var(<>)), "`" => RichTerm::from(Term::Enum(<>)), - "{" "}" => { - 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) - }) + "{" ";")*> "}" => { + let fields = fields.into_iter().chain(last.into_iter()); + RichTerm::from(build_record(fields)) }, "[" ",")*> "]" => { let terms : Vec = terms.into_iter() @@ -270,8 +254,8 @@ Atom: RichTerm = { } }; -RecordField: Either<(Ident, RichTerm), (RichTerm, RichTerm)> = { - "=" => { +RecordField: (FieldPathElem, RichTerm) = { + "=" => { 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) }, - )?> => { 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))) - }, - "$" > "=" => { - 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 = { + ".")*> => { + let mut elems = elems; + elems.push(last); + elems + } +}; + +FieldPathElem: FieldPathElem = { + => FieldPathElem::Ident(<>), + "\"" "\"" => FieldPathElem::Ident(Ident(<>)), + "$" > => FieldPathElem::Expr(<>), +}; + Pattern: Ident = { Ident, }; @@ -356,8 +344,8 @@ ChunkLiteral : String = => { 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; -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 = { diff --git a/src/parser/utils.rs b/src/parser/utils.rs index ea29d3ad..f9728cb6 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -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, + 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(fields: I) -> Term +where + I: IntoIterator, +{ + 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 {