Skip to main content

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;