diff --git a/efmt_core/src/items/expressions.rs b/efmt_core/src/items/expressions.rs index d2bb208..3282f39 100644 --- a/efmt_core/src/items/expressions.rs +++ b/efmt_core/src/items/expressions.rs @@ -1,7 +1,7 @@ //! Erlang expressions. use crate::format::Format; use crate::items::Expr; -use crate::items::components::{Either, Element, Parenthesized}; +use crate::items::components::{Element, Parenthesized}; use crate::items::symbols::OpenBraceSymbol; use crate::items::tokens::{ AtomToken, CharToken, FloatToken, IntegerToken, LexicalToken, SigilStringToken, SymbolToken, diff --git a/efmt_core/src/items/expressions/records.rs b/efmt_core/src/items/expressions/records.rs index 29b4e94..d6fe7d6 100644 --- a/efmt_core/src/items/expressions/records.rs +++ b/efmt_core/src/items/expressions/records.rs @@ -1,13 +1,112 @@ -use crate::format::Format; +use crate::format::{Format, Formatter}; use crate::items::Expr; -use crate::items::components::{Element, RecordLike}; -use crate::items::expressions::Either; -use crate::items::symbols::{DotSymbol, MatchSymbol, SharpSymbol}; -use crate::items::tokens::AtomToken; +use crate::items::components::{Either, Element, Maybe, RecordLike}; +use crate::items::symbols::{ColonSymbol, DotSymbol, MatchSymbol, SharpSymbol}; +use crate::items::tokens::{AtomToken, KeywordToken, VariableToken}; use crate::items::variables::UnderscoreVariable; use crate::parse::{self, Parse, ResumeParse}; use crate::span::Span; +#[derive(Debug, Clone, Span)] +struct RecordName { + token: Either>, + atom: AtomToken, +} + +impl RecordName { + fn as_atom_token(&self) -> &AtomToken { + &self.atom + } +} + +impl Parse for RecordName { + fn parse(ts: &mut parse::TokenStream) -> parse::Result { + if let Ok(token) = ts.parse::() { + return Ok(Self { + atom: token.clone(), + token: Either::A(token), + }); + } + if let Ok(token) = ts.parse::() { + let atom = AtomToken::new( + token.value().as_str(), + token.start_position(), + token.end_position(), + ); + return Ok(Self { + atom, + token: Either::B(Either::A(token)), + }); + } + + let token: VariableToken = ts.parse()?; + if token.value() == "_" { + return Err(parse::Error::unexpected_token(ts, token.into())); + } + let atom = AtomToken::new(token.value(), token.start_position(), token.end_position()); + Ok(Self { + atom, + token: Either::B(Either::B(token)), + }) + } +} + +impl Format for RecordName { + fn format(&self, fmt: &mut Formatter) { + self.token.format(fmt); + } +} + +#[derive(Debug, Clone, Span)] +struct QualifiedRecordName { + module: Maybe<(AtomToken, ColonSymbol)>, + name: RecordName, +} + +impl QualifiedRecordName { + fn as_atom_token(&self) -> &AtomToken { + self.name.as_atom_token() + } +} + +impl Parse for QualifiedRecordName { + fn parse(ts: &mut parse::TokenStream) -> parse::Result { + let module = if ts.peek::<(AtomToken, ColonSymbol)>().is_some() { + Maybe::some(ts.parse()?) + } else { + Maybe::parse_none(ts)? + }; + Ok(Self { + module, + name: ts.parse()?, + }) + } +} + +impl Format for QualifiedRecordName { + fn format(&self, fmt: &mut Formatter) { + self.module.format(fmt); + self.name.format(fmt); + } +} + +#[derive(Debug, Clone, Span, Parse, Format)] +#[expect(clippy::large_enum_variant)] +enum RecordNameRef { + Named(QualifiedRecordName), + Anonymous(UnderscoreVariable), +} + +impl RecordNameRef { + fn as_atom_token(&self) -> Option<&AtomToken> { + if let Self::Named(x) = self { + Some(x.as_atom_token()) + } else { + None + } + } +} + #[derive(Debug, Clone, Span, Parse, Format)] pub enum RecordConstructOrIndexExpr { Construct(Box), @@ -17,8 +116,16 @@ pub enum RecordConstructOrIndexExpr { impl RecordConstructOrIndexExpr { pub fn record_name(&self) -> &AtomToken { match self { - Self::Construct(x) => &x.record.prefix().1, - Self::Index(x) => &x.name, + Self::Construct(x) => x + .record + .prefix() + .1 + .as_atom_token() + .expect("anonymous record has no record name"), + Self::Index(x) => x + .name + .as_atom_token() + .expect("anonymous record has no record name"), } } @@ -46,8 +153,18 @@ pub enum RecordAccessOrUpdateExpr { impl RecordAccessOrUpdateExpr { pub fn record_name(&self) -> &AtomToken { match self { - Self::Access(x) => &x.index.name, - Self::Update(x) => &x.record.prefix().1.1, + Self::Access(x) => x + .index + .name + .as_atom_token() + .expect("anonymous record has no record name"), + Self::Update(x) => x + .record + .prefix() + .1 + .1 + .as_atom_token() + .expect("anonymous record has no record name"), } } @@ -70,7 +187,10 @@ impl RecordAccessOrUpdateExpr { impl ResumeParse for RecordAccessOrUpdateExpr { fn resume_parse(ts: &mut parse::TokenStream, value: Expr) -> parse::Result { - if ts.peek::<(SharpSymbol, (AtomToken, DotSymbol))>().is_some() { + if ts + .peek::<(SharpSymbol, (RecordNameRef, DotSymbol))>() + .is_some() + { ts.resume_parse(value).map(Self::Access) } else { ts.resume_parse(value).map(Self::Update) @@ -80,17 +200,17 @@ impl ResumeParse for RecordAccessOrUpdateExpr { /// `#` `$NAME` `{` (`$FIELD` `,`?)* `}` /// -/// - $NAME: [AtomToken] +/// - $NAME: [AtomToken] | [KeywordToken] | [VariableToken] | [AtomToken] `:` [AtomToken] /// - $FIELD: ([AtomToken] | `_`) `=` [Expr] #[derive(Debug, Clone, Span, Parse, Format)] pub struct RecordConstructExpr { - record: RecordLike<(SharpSymbol, AtomToken), RecordField>, + record: RecordLike<(SharpSymbol, RecordNameRef), RecordField>, } /// `$VALUE` `#` `$NAME` `.` `$FIELD` /// /// - $VALUE: [Expr] -/// - $NAME: [AtomToken] +/// - $NAME: [AtomToken] | [KeywordToken] | [VariableToken] | [AtomToken] `:` [AtomToken] | `_` /// - $FIELD: [AtomToken] #[derive(Debug, Clone, Span, Parse, Format)] pub struct RecordAccessExpr { @@ -109,12 +229,12 @@ impl ResumeParse for RecordAccessExpr { /// `#` `$NAME` `.` `$FIELD` /// -/// - $NAME: [AtomToken] +/// - $NAME: [AtomToken] | [KeywordToken] | [VariableToken] | [AtomToken] `:` [AtomToken] | `_` /// - $FIELD: [AtomToken] #[derive(Debug, Clone, Span, Parse, Format)] pub struct RecordIndexExpr { sharp: SharpSymbol, - name: AtomToken, + name: RecordNameRef, dot: DotSymbol, field: AtomToken, } @@ -122,11 +242,11 @@ pub struct RecordIndexExpr { /// `$VALUE` `#` `$NAME` `{` (`$FIELD` `,`?)* `}` /// /// - $VALUE: [Expr] -/// - $NAME: [AtomToken] +/// - $NAME: [AtomToken] | [KeywordToken] | [VariableToken] | [AtomToken] `:` [AtomToken] | `_` /// - $FIELD: [AtomToken] #[derive(Debug, Clone, Span, Parse, Format)] pub struct RecordUpdateExpr { - record: RecordLike<(Expr, (SharpSymbol, AtomToken)), RecordField>, + record: RecordLike<(Expr, (SharpSymbol, RecordNameRef)), RecordField>, } impl ResumeParse for RecordUpdateExpr { @@ -184,6 +304,9 @@ mod tests { fn record_construct_works() { let texts = [ "#foo{}", + "#state{}", + "#mod:state{}", + "#_{}", indoc::indoc! {" #foo{ module = Mod, @@ -211,7 +334,7 @@ mod tests { #[test] fn record_index_works() { - let texts = ["#foo.bar"]; + let texts = ["#foo.bar", "#mod:foo.bar", "#_.bar"]; for text in texts { crate::assert_format!(text, Expr); } @@ -221,6 +344,8 @@ mod tests { fn record_access_works() { let texts = [ "X#foo.bar", + "X#mod:foo.bar", + "X#_.bar", "(foo())#foo.bar", "N2#nrec2.nrec1#nrec1.nrec0#nrec0.name", ]; @@ -233,11 +358,21 @@ mod tests { fn record_update_works() { let texts = [ "M#foo{}", + "M#mod:foo{}", + "M#_{}", indoc::indoc! {" M#baz{ qux = 1 }#foo.bar"}, indoc::indoc! {" + M#mod:foo{ + qux = 1 + }"}, + indoc::indoc! {" + M#_{ + qux = 1 + }"}, + indoc::indoc! {" M#foo.bar#baz{ qux = 1 }"}, diff --git a/efmt_core/src/items/forms.rs b/efmt_core/src/items/forms.rs index b9aa850..ad22452 100644 --- a/efmt_core/src/items/forms.rs +++ b/efmt_core/src/items/forms.rs @@ -18,9 +18,11 @@ use crate::items::keywords::{ElseKeyword, IfKeyword}; use crate::items::macros::{MacroName, MacroReplacement}; use crate::items::symbols::{ CloseParenSymbol, CloseSquareSymbol, ColonSymbol, CommaSymbol, DotSymbol, DoubleColonSymbol, - HyphenSymbol, MatchSymbol, OpenParenSymbol, OpenSquareSymbol, SlashSymbol, + HyphenSymbol, MatchSymbol, OpenParenSymbol, OpenSquareSymbol, SharpSymbol, SlashSymbol, +}; +use crate::items::tokens::{ + AtomToken, IntegerToken, KeywordToken, LexicalToken, StringToken, VariableToken, }; -use crate::items::tokens::{AtomToken, IntegerToken, LexicalToken, StringToken, VariableToken}; use crate::parse::{Parse, TokenStream}; use crate::span::Span; @@ -56,31 +58,65 @@ impl Format for Form { } } -/// `-` `record` `(` `$NAME` `,` `{` `$FIELD`* `}` `)` `.` -/// -/// - $NAME: [AtomToken] -/// - $FIELD: [AtomToken] (`=` [Expr])? (`::` [Type])? `,`? -#[derive(Debug, Clone, Span, Parse, Format)] -pub struct RecordDecl(AttrLike); +/// `-record(Name, {...}).` | `-record #Name(TVar1, ..., TVarN){...}.` +#[derive(Debug, Clone, Span)] +pub struct RecordDecl(Either); + +impl Parse for RecordDecl { + fn parse(ts: &mut TokenStream) -> crate::parse::Result { + if let Ok(x) = ts.parse() { + return Ok(Self(Either::A(x))); + } + if let Ok(x) = ts.parse() { + return Ok(Self(Either::B(x))); + } + Err(ts.take_last_error().expect("unreachable")) + } +} + +impl Format for RecordDecl { + fn format(&self, fmt: &mut Formatter) { + self.0.format(fmt); + } +} impl RecordDecl { pub fn record_name(&self) -> &AtomToken { - &self.0.value().name + match &self.0 { + Either::A(x) => &x.value().name, + Either::B(x) => x.name.as_atom_token(), + } } pub fn fields(&self) -> &[RecordField] { - self.0.value().fields.get() + match &self.0 { + Either::A(x) => x.value().fields.get(), + Either::B(x) => x.fields.get(), + } + } + + pub fn is_native(&self) -> bool { + matches!(self.0, Either::B(_)) + } + + pub fn type_params(&self) -> Option<&[VariableToken]> { + match &self.0 { + Either::A(_) => None, + Either::B(x) => x.type_params.get().map(|x| x.get()), + } } } +type TupleRecordDecl = AttrLike; + #[derive(Debug, Clone, Span, Parse)] -struct RecordDeclValue { +struct TupleRecordDeclValue { name: AtomToken, comma: CommaSymbol, fields: RecordFieldsLike, } -impl Format for RecordDeclValue { +impl Format for TupleRecordDeclValue { fn format(&self, fmt: &mut Formatter) { fmt.with_scoped_indent(|fmt| { fmt.set_indent(fmt.column()); @@ -92,6 +128,112 @@ impl Format for RecordDeclValue { } } +#[derive(Debug, Clone, Span)] +struct NativeRecordName { + token: Either>, + atom: AtomToken, +} + +impl NativeRecordName { + fn as_atom_token(&self) -> &AtomToken { + &self.atom + } +} + +impl Parse for NativeRecordName { + fn parse(ts: &mut TokenStream) -> crate::parse::Result { + if let Ok(token) = ts.parse::() { + return Ok(Self { + atom: token.clone(), + token: Either::A(token), + }); + } + if let Ok(token) = ts.parse::() { + let atom = AtomToken::new( + token.value().as_str(), + token.start_position(), + token.end_position(), + ); + return Ok(Self { + atom, + token: Either::B(Either::A(token)), + }); + } + + let token: VariableToken = ts.parse()?; + if token.value() == "_" { + return Err(crate::parse::Error::unexpected_token(ts, token.into())); + } + let atom = AtomToken::new(token.value(), token.start_position(), token.end_position()); + Ok(Self { + atom, + token: Either::B(Either::B(token)), + }) + } +} + +impl Format for NativeRecordName { + fn format(&self, fmt: &mut Formatter) { + self.token.format(fmt); + } +} + +#[derive(Debug, Clone, Span)] +struct NativeRecordDecl { + hyphen: HyphenSymbol, + record: RecordAtom, + sharp: SharpSymbol, + name: NativeRecordName, + type_params: Maybe>, + fields: RecordFieldsLike, + dot: DotSymbol, +} + +impl Parse for NativeRecordDecl { + fn parse(ts: &mut TokenStream) -> crate::parse::Result { + let hyphen = ts.parse()?; + let record = ts.parse()?; + let sharp = ts.parse()?; + let name = ts.parse()?; + let type_params = if ts.peek::().is_some() { + Maybe::some(ts.parse()?) + } else { + Maybe::parse_none(ts)? + }; + let fields = ts.parse()?; + let dot = ts.parse()?; + Ok(Self { + hyphen, + record, + sharp, + name, + type_params, + fields, + dot, + }) + } +} + +impl Format for NativeRecordDecl { + fn format(&self, fmt: &mut Formatter) { + let f = |fmt: &mut Formatter| { + self.hyphen.format(fmt); + self.record.format(fmt); + fmt.write_space(); + self.sharp.format(fmt); + self.name.format(fmt); + self.type_params.format(fmt); + self.fields.format(fmt); + self.dot.format(fmt); + }; + if self.contains_newline() { + f(fmt); + } else { + fmt.with_single_line_mode(f); + } + } +} + #[derive(Debug, Clone, Span, Parse, Element)] pub struct RecordField { name: AtomToken, @@ -725,6 +867,10 @@ mod tests { fn record_decl_works() { let texts = [ "-record(foo, {}).", + "-record #state{}.", + "-record #state(V){values = [] :: list(number()), avg = 0.0 :: float()}.", + "-record #div{field}.", + "-record #Tillstand{field}.", indoc::indoc! {" -record(foo, { foo