Skip to main content

nash_parse/
exposing.rs

1//! Exposing list parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Module.hs` (exposing, exposingHelp, chompExposed, privacy).
4//!
5//! Parses exposing lists like:
6//! - `(..)` - expose everything
7//! - `(foo, bar)` - expose specific values
8//! - `(Foo, Bar(..))` - expose types, optionally with constructors
9//! - `((+), (-))` - expose operators
10
11use nash_region::Region;
12use nash_source::{Exposed, Exposing, Privacy};
13
14use crate::Parser;
15use crate::error;
16
17impl<'a> Parser<'a> {
18    /// Parse an exposing list.
19    ///
20    /// Mirrors Elm's `exposing`:
21    /// ```text
22    /// exposing = '(' ( '..' | exposed { ',' exposed } ) ')'
23    /// ```
24    pub fn exposing(&mut self) -> Result<Exposing<'a>, error::Exposing> {
25        // Opening paren
26        self.word1(b'(', error::Exposing::Start)?;
27        self.chomp_and_check_indent(error::Exposing::Space, error::Exposing::IndentValue)?;
28
29        // Either ".." for open, or explicit list
30        self.one_of(
31            error::Exposing::Value,
32            vec![
33                // (..)
34                Box::new(|p: &mut Parser<'a>| {
35                    p.word2(b'.', b'.', error::Exposing::Value)?;
36                    p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::IndentEnd)?;
37                    p.word1(b')', error::Exposing::End)?;
38                    Ok(Exposing::Open)
39                }),
40                // Explicit list
41                Box::new(|p: &mut Parser<'a>| {
42                    let exposed = p.chomp_exposed()?;
43                    p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::IndentEnd)?;
44                    p.exposing_help(vec![exposed])
45                }),
46            ],
47        )
48    }
49
50    /// Parse remaining exposed items after the first.
51    ///
52    /// Mirrors Elm's `exposingHelp`.
53    fn exposing_help(
54        &mut self,
55        mut rev_exposed: Vec<&'a Exposed<'a>>,
56    ) -> Result<Exposing<'a>, error::Exposing> {
57        loop {
58            let rev_exposed_for_fallback = rev_exposed.clone();
59
60            let result = self.one_of(
61                error::Exposing::End,
62                vec![
63                    // More items: , exposed
64                    Box::new(|p: &mut Parser<'a>| {
65                        p.word1(b',', error::Exposing::End)?;
66                        p.chomp_and_check_indent(
67                            error::Exposing::Space,
68                            error::Exposing::IndentValue,
69                        )?;
70                        let exposed = p.chomp_exposed()?;
71                        p.chomp_and_check_indent(
72                            error::Exposing::Space,
73                            error::Exposing::IndentEnd,
74                        )?;
75                        rev_exposed.push(exposed);
76                        Ok(ExposingHelpState::Continue)
77                    }),
78                    // End: )
79                    Box::new(|p: &mut Parser<'a>| {
80                        p.word1(b')', error::Exposing::End)?;
81                        let exposed_slice = p.alloc_slice_copy(&rev_exposed_for_fallback);
82                        Ok(ExposingHelpState::Done(Exposing::Explicit(exposed_slice)))
83                    }),
84                ],
85            )?;
86
87            match result {
88                ExposingHelpState::Continue => continue,
89                ExposingHelpState::Done(exposing) => return Ok(exposing),
90            }
91        }
92    }
93
94    /// Parse a single exposed item.
95    ///
96    /// Mirrors Elm's `chompExposed`:
97    /// - lowercase name (value)
98    /// - (operator)
99    /// - Uppercase name with optional (..)
100    fn chomp_exposed(&mut self) -> Result<&'a Exposed<'a>, error::Exposing> {
101        let start = self.get_position();
102
103        self.one_of(
104            error::Exposing::Value,
105            vec![
106                // lowercase value
107                Box::new(|p: &mut Parser<'a>| {
108                    let name = p.lower_name(error::Exposing::Value)?;
109                    let located = p.add_end(start, name);
110                    Ok(p.alloc(Exposed::Lower(located)))
111                }),
112                // (operator)
113                Box::new(|p: &mut Parser<'a>| {
114                    p.word1(b'(', error::Exposing::Value)?;
115                    let op = p.operator(error::Exposing::Operator, |bad_op, row, col| {
116                        error::Exposing::OperatorReserved(bad_op, row, col)
117                    })?;
118                    p.word1(b')', error::Exposing::OperatorRightParen)?;
119                    let end = p.get_position();
120                    Ok(p.alloc(Exposed::Operator {
121                        region: Region::new(start, end),
122                        op,
123                    }))
124                }),
125                // Uppercase type
126                Box::new(|p: &mut Parser<'a>| {
127                    let name = p.upper_name(error::Exposing::Value)?;
128                    let located = p.add_end(start, name);
129                    p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::IndentEnd)?;
130                    let privacy = p.privacy()?;
131                    Ok(p.alloc(Exposed::Upper {
132                        name: located,
133                        privacy,
134                    }))
135                }),
136            ],
137        )
138    }
139
140    /// Parse optional (..) after a type name.
141    ///
142    /// Mirrors Elm's `privacy`.
143    fn privacy(&mut self) -> Result<Privacy, error::Exposing> {
144        self.one_of_with_fallback(
145            vec![Box::new(|p: &mut Parser<'a>| {
146                p.word1(b'(', error::Exposing::TypePrivacy)?;
147                p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::TypePrivacy)?;
148                let start = p.get_position();
149                p.word2(b'.', b'.', error::Exposing::TypePrivacy)?;
150                let end = p.get_position();
151                p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::TypePrivacy)?;
152                p.word1(b')', error::Exposing::TypePrivacy)?;
153                Ok(Privacy::Public(Region::new(start, end)))
154            })],
155            Privacy::Private,
156        )
157    }
158}
159
160/// Internal state for exposing_help loop.
161enum ExposingHelpState<'a> {
162    Continue,
163    Done(Exposing<'a>),
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use bumpalo::Bump;
170    use indoc::indoc;
171
172    macro_rules! assert_exposing_snapshot {
173        ($input:expr) => {{
174            let input = indoc!($input);
175            let bump = Bump::new();
176            let src = bump.alloc_str(input);
177            let mut parser = Parser::new(&bump, src.as_bytes());
178            let result = parser.exposing();
179            match result {
180                Ok(ref exposing) => {
181                    insta::with_settings!({
182                        description => format!("Code:\n\n{}", input),
183                        omit_expression => true,
184                    }, {
185                        insta::assert_debug_snapshot!(exposing);
186                    });
187                }
188                Err(e) => {
189                    panic!("Expected successful parse, got error: {:?}", e);
190                }
191            }
192        }};
193    }
194
195    #[test]
196    fn exposing_open() {
197        assert_exposing_snapshot!("(..)");
198    }
199
200    #[test]
201    fn exposing_single_value() {
202        assert_exposing_snapshot!("(foo)");
203    }
204
205    #[test]
206    fn exposing_multiple_values() {
207        assert_exposing_snapshot!("(foo, bar, baz)");
208    }
209
210    #[test]
211    fn exposing_type_private() {
212        assert_exposing_snapshot!("(Foo)");
213    }
214
215    #[test]
216    fn exposing_type_public() {
217        assert_exposing_snapshot!("(Foo(..))");
218    }
219
220    #[test]
221    fn exposing_operator() {
222        assert_exposing_snapshot!("((+))");
223    }
224
225    #[test]
226    fn exposing_multiple_operators() {
227        assert_exposing_snapshot!("((+), (-), (++))");
228    }
229
230    #[test]
231    fn exposing_mixed() {
232        assert_exposing_snapshot!("(foo, Bar, Baz(..), (+))");
233    }
234
235    #[test]
236    fn exposing_with_spaces() {
237        assert_exposing_snapshot!("( foo , bar )");
238    }
239
240    // Note: Multiline exposing lists are tested indirectly through module/import tests,
241    // since they require proper indent context to be set by the caller.
242}