nash_parse/declaration/mod.rs
1//! Declaration parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Declaration.hs`.
4//! Handles value definitions, type annotations, type aliases, custom types, and infix declarations.
5
6mod infix;
7mod type_alias;
8mod union;
9mod value;
10
11use nash_region::{Located, Position};
12use nash_source::{Alias, Comment, Union, Value};
13
14use crate::Parser;
15use crate::error::{self, Decl as DeclErr};
16
17/// A parsed declaration with optional doc comment.
18#[derive(Debug)]
19pub enum Decl<'a> {
20 Value(Option<&'a Comment<'a>>, &'a Located<Value<'a>>),
21 Union(Option<&'a Comment<'a>>, &'a Located<Union<'a>>),
22 Alias(Option<&'a Comment<'a>>, &'a Located<Alias<'a>>),
23}
24
25impl<'a> Parser<'a> {
26 /// Parse a single declaration.
27 ///
28 /// Mirrors Elm's `declaration`:
29 /// ```haskell
30 /// declaration :: Space.Parser E.Decl Decl
31 /// declaration =
32 /// do maybeDocs <- chompDocComment
33 /// start <- getPosition
34 /// oneOf E.DeclStart
35 /// [ typeDecl maybeDocs start
36 /// , portDecl maybeDocs -- skipped in Nash
37 /// , valueDecl maybeDocs start
38 /// ]
39 /// ```
40 pub fn declaration(&mut self) -> Result<(Decl<'a>, Position), error::Decl<'a>> {
41 let maybe_docs = self.chomp_doc_comment()?;
42
43 let start = self.get_position();
44
45 self.one_of(
46 DeclErr::Start,
47 vec![
48 // type alias or type (union)
49 Box::new(|p: &mut Parser<'a>| p.type_decl(maybe_docs, start)),
50 // value definition
51 Box::new(|p| p.value_decl(maybe_docs, start)),
52 ],
53 )
54 }
55
56 /// Parse an optional doc comment `{-| ... -}`.
57 ///
58 /// Mirrors Elm's `chompDocComment`:
59 /// ```haskell
60 /// chompDocComment =
61 /// oneOfWithFallback
62 /// [ do docComment <- Space.docComment E.DeclStart E.DeclSpace
63 /// Space.chomp E.DeclSpace
64 /// Space.checkFreshLine E.DeclFreshLineAfterDocComment
65 /// return (Just docComment)
66 /// ]
67 /// Nothing
68 /// ```
69 fn chomp_doc_comment(&mut self) -> Result<Option<&'a Comment<'a>>, error::Decl<'a>> {
70 self.one_of_with_fallback(
71 vec![Box::new(|p: &mut Parser<'a>| {
72 let doc = p.doc_comment(DeclErr::Start, |space, row, col| {
73 DeclErr::Space(space, row, col)
74 })?;
75 p.chomp(DeclErr::Space)?;
76 p.check_fresh_line(DeclErr::FreshLineAfterDocComment)?;
77 Ok(Some(doc))
78 })],
79 None,
80 )
81 }
82
83 /// Parse a type declaration (alias or union).
84 ///
85 /// Mirrors Elm's `typeDecl`:
86 /// ```haskell
87 /// typeDecl maybeDocs start =
88 /// inContext E.DeclType (Keyword.type_ E.DeclStart) $
89 /// do Space.chompAndCheckIndent E.DT_Space E.DT_IndentName
90 /// oneOf E.DT_Name
91 /// [ inContext E.DT_Alias (Keyword.alias_ E.DT_Name) $ ...
92 /// , specialize E.DT_Union $ ...
93 /// ]
94 /// ```
95 fn type_decl(
96 &mut self,
97 maybe_docs: Option<&'a Comment<'a>>,
98 start: Position,
99 ) -> Result<(Decl<'a>, Position), error::Decl<'a>> {
100 self.in_context(
101 |bump, e, row, col| error::Decl::Type(bump.alloc(e), row, col),
102 |p| p.keyword_type(error::Decl::Start),
103 |p| {
104 p.chomp_and_check_indent(error::DeclType::Space, error::DeclType::IndentName)?;
105
106 p.one_of(
107 error::DeclType::Name,
108 vec![
109 // type alias
110 Box::new(|p: &mut Parser<'a>| {
111 p.in_context(
112 |bump, e, row, col| error::DeclType::Alias(bump.alloc(e), row, col),
113 |p| p.keyword_alias(error::DeclType::Name),
114 |p| {
115 let (alias, end) = p.type_alias_body(start)?;
116 Ok((Decl::Alias(maybe_docs, alias), end))
117 },
118 )
119 }),
120 // type union (custom type)
121 Box::new(|p| {
122 p.specialize(
123 |bump, e, row, col| error::DeclType::Union(bump.alloc(e), row, col),
124 |p| {
125 let (union, end) = p.union_body(start)?;
126 Ok((Decl::Union(maybe_docs, union), end))
127 },
128 )
129 }),
130 ],
131 )
132 },
133 )
134 }
135}
136
137/// Macro for testing successful declaration parsing.
138#[cfg(test)]
139macro_rules! assert_decl_snapshot {
140 ($src:expr) => {{
141 let bump = bumpalo::Bump::new();
142 let src = indoc::indoc!($src);
143 let src_in_arena = bump.alloc_str(src);
144 let mut parser = crate::Parser::new(&bump, src_in_arena.as_bytes());
145 match parser.declaration() {
146 Ok((decl, _end)) => {
147 insta::with_settings!({
148 description => src,
149 omit_expression => true,
150 }, {
151 insta::assert_debug_snapshot!(decl);
152 });
153 }
154 Err(e) => panic!("Expected Ok, got Err: {:?}\n\nSource:\n{}", e, src),
155 }
156 }};
157}
158
159#[cfg(test)]
160pub(crate) use assert_decl_snapshot;