Skip to main content

nash_parse/pattern/
mod.rs

1//! Pattern parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Pattern.hs`.
4//!
5//! Provides:
6//! - `pattern_term` - atomic patterns (wildcard, var, ctor, literal, record, tuple, list)
7//! - `pattern_expr` - full patterns including cons (::), as-patterns, and ctor with args
8
9mod list;
10mod record;
11mod term;
12mod tuple;
13
14use bumpalo::collections::Vec as BumpVec;
15use nash_region::{Located, Position, Region};
16use nash_source::Pattern;
17
18use crate::Parser;
19use crate::error;
20
21impl<'a> Parser<'a> {
22    /// Parse an atomic pattern (no cons, as, or ctor args).
23    ///
24    /// Mirrors Elm's `Pattern.term`:
25    /// ```haskell
26    /// term =
27    ///   do  start <- getPosition
28    ///       oneOf E.PStart
29    ///         [ record start
30    ///         , tuple start
31    ///         , list start
32    ///         , termHelp start
33    ///         ]
34    /// ```
35    pub fn pattern_term(&mut self) -> Result<&'a Located<Pattern<'a>>, error::Pattern<'a>> {
36        let start = self.get_position();
37
38        self.one_of(
39            error::Pattern::Start,
40            vec![
41                Box::new(|p: &mut Parser<'a>| p.pattern_record(start)),
42                Box::new(|p| p.pattern_tuple(start)),
43                Box::new(|p| p.pattern_list(start)),
44                Box::new(|p| p.pattern_term_help(start)),
45            ],
46        )
47    }
48
49    /// Parse a full pattern expression including cons (::), as-patterns, and ctor with args.
50    ///
51    /// Returns `(pattern, end)` where `end` is the position at the end of the pattern
52    /// (before any trailing whitespace was chomped). This is important for indent checking.
53    ///
54    /// Mirrors Elm's `Pattern.expression`:
55    /// ```haskell
56    /// expression :: Space.Parser E.Pattern Src.Pattern
57    /// expression =
58    ///   do  start <- getPosition
59    ///       ePart <- exprPart
60    ///       exprHelp start [] ePart
61    /// ```
62    pub fn pattern_expr(
63        &mut self,
64    ) -> Result<(&'a Located<Pattern<'a>>, Position), error::Pattern<'a>> {
65        let start = self.get_position();
66        let (first_pattern, first_end) = self.pattern_expr_part()?;
67        self.pattern_expr_help(start, first_pattern, first_end)
68    }
69
70    /// Parse a pattern expression part (term or ctor with args).
71    ///
72    /// Mirrors Elm's `exprPart`.
73    fn pattern_expr_part(
74        &mut self,
75    ) -> Result<(&'a Located<Pattern<'a>>, Position), error::Pattern<'a>> {
76        let start = self.get_position();
77
78        // Check if it starts with uppercase (constructor that might have args)
79        if matches!(self.peek(), Some(b) if b.is_ascii_uppercase()) {
80            let ctor_start = self.pos;
81            self.advance();
82            self.chomp_inner_chars();
83
84            // Check for qualification
85            let (region, module, name) = if self.is_dot_upper() || self.is_dot_lower() {
86                self.pattern_ctor_qualified_parts(start, ctor_start)?
87            } else {
88                let name = self.slice_from(ctor_start);
89                let end = self.get_position();
90                (Region::new(start, end), None, name)
91            };
92
93            // Now try to parse arguments
94            self.pattern_ctor_with_args(start, region, module, name)
95        } else {
96            // Regular term - chomp whitespace but return the end position
97            // from BEFORE chomping (extracted from pattern's region)
98            let pattern = self.pattern_term()?;
99            let end = pattern.region.end;
100            self.chomp(error::Pattern::Space)?;
101            Ok((pattern, end))
102        }
103    }
104
105    /// Parse qualified constructor parts, returning (region, module, name).
106    fn pattern_ctor_qualified_parts(
107        &mut self,
108        start: Position,
109        ctor_start: usize,
110    ) -> Result<(Region, Option<&'a str>, &'a str), error::Pattern<'a>> {
111        let (row, col) = self.position();
112
113        // Keep chomping Module.Module... until we hit the final name
114        loop {
115            if self.is_dot_upper() {
116                self.advance(); // consume dot
117                self.advance(); // consume first uppercase char
118                self.chomp_inner_chars();
119            } else if self.is_dot_lower() {
120                // Qualified lowercase - error for patterns
121                return Err(error::Pattern::Start(row, col));
122            } else {
123                break;
124            }
125        }
126
127        let full = self.slice_from(ctor_start);
128        let end = self.get_position();
129        let region = Region::new(start, end);
130
131        if let Some(last_dot) = full.rfind('.') {
132            let module = &full[..last_dot];
133            let name = &full[last_dot + 1..];
134            Ok((region, Some(module), name))
135        } else {
136            Ok((region, None, full))
137        }
138    }
139
140    /// Parse constructor with potential arguments.
141    fn pattern_ctor_with_args(
142        &mut self,
143        start: Position,
144        region: Region,
145        module: Option<&'a str>,
146        name: &'a str,
147    ) -> Result<(&'a Located<Pattern<'a>>, Position), error::Pattern<'a>> {
148        let mut end = self.get_position();
149        self.chomp(error::Pattern::Space)?;
150
151        let mut args: BumpVec<'a, &'a Located<Pattern<'a>>> = BumpVec::new_in(self.bump);
152
153        // Try to parse arguments (terms only, not full expressions)
154        loop {
155            let arg_result = self.one_of_with_fallback(
156                vec![Box::new(|p: &mut Parser<'a>| {
157                    // Check indent before trying to parse arg
158                    let (check_row, check_col) = p.position();
159                    p.check_indent(check_row, check_col, error::Pattern::IndentStart)?;
160
161                    let arg = p.pattern_term()?;
162                    Ok(Some(arg))
163                })],
164                None,
165            )?;
166
167            match arg_result {
168                Some(arg) => {
169                    args.push(arg);
170                    end = self.get_position();
171                    self.chomp(error::Pattern::Space)?;
172                }
173                None => break,
174            }
175        }
176
177        let args_slice = args.into_bump_slice();
178        let pattern = match module {
179            Some(module) => Pattern::CtorQual {
180                region,
181                module,
182                name,
183                args: args_slice,
184            },
185            None => Pattern::Ctor {
186                region,
187                name,
188                args: args_slice,
189            },
190        };
191
192        Ok((self.add_end(start, pattern), end))
193    }
194
195    /// Parse the rest of a pattern expression (cons and as).
196    ///
197    /// Returns `(pattern, end)` where `end` is the position at the end of the pattern.
198    ///
199    /// Mirrors Elm's `exprHelp`.
200    fn pattern_expr_help(
201        &mut self,
202        start: Position,
203        pattern: &'a Located<Pattern<'a>>,
204        end: Position,
205    ) -> Result<(&'a Located<Pattern<'a>>, Position), error::Pattern<'a>> {
206        let mut patterns: BumpVec<'a, &'a Located<Pattern<'a>>> = BumpVec::new_in(self.bump);
207        let mut current = pattern;
208        let mut current_end = end;
209
210        loop {
211            // Check indent at the end position (before chomp), not current parser position.
212            // If indent check fails, we're done - return what we have.
213            if self
214                .check_indent(
215                    current_end.line,
216                    current_end.column,
217                    error::Pattern::IndentStart,
218                )
219                .is_err()
220            {
221                let result = self.build_cons_chain(&mut patterns, current);
222                return Ok((result, current_end));
223            }
224
225            // Try to parse :: or as
226            let result = self.one_of_with_fallback(
227                vec![
228                    // Cons: `::`
229                    Box::new(|p: &mut Parser<'a>| {
230                        p.word2(0x3A, 0x3A, error::Pattern::Start)?; // ::
231                        p.chomp_and_check_indent(error::Pattern::Space, error::Pattern::IndentStart)?;
232
233                        let (next_pattern, next_end) = p.pattern_expr_part()?;
234                        Ok(ConsOrAs::Cons(next_pattern, next_end))
235                    }),
236                    // As: `as name`
237                    Box::new(|p: &mut Parser<'a>| {
238                        // Check for "as" keyword
239                        let (row, col) = p.position();
240                        if !p.remaining().starts_with(b"as")
241                            || matches!(p.peek_at(2), Some(b) if b.is_ascii_alphanumeric() || b == b'_')
242                        {
243                            return Err(error::Pattern::Start(row, col));
244                        }
245                        p.advance_by(2);
246
247                        p.chomp_and_check_indent(error::Pattern::Space, error::Pattern::IndentAlias)?;
248
249                        let name_start = p.get_position();
250                        let name = p.lower_name(error::Pattern::Alias)?;
251                        let name_end = p.get_position();
252                        p.chomp(error::Pattern::Space)?;
253
254                        let alias = p.add_end(name_start, name);
255                        Ok(ConsOrAs::As(alias, name_end))
256                    }),
257                ],
258                ConsOrAs::Done,
259            )?;
260
261            match result {
262                ConsOrAs::Cons(next_pattern, next_end) => {
263                    patterns.push(current);
264                    current = next_pattern;
265                    current_end = next_end;
266                }
267                ConsOrAs::As(alias, alias_end) => {
268                    // Build up cons chain first
269                    let base = self.build_cons_chain(&mut patterns, current);
270                    // Then wrap in alias
271                    return Ok((
272                        self.add_end(
273                            start,
274                            Pattern::Alias {
275                                pattern: base,
276                                name: alias,
277                            },
278                        ),
279                        alias_end,
280                    ));
281                }
282                ConsOrAs::Done => {
283                    // Build up cons chain
284                    let result = self.build_cons_chain(&mut patterns, current);
285                    return Ok((result, current_end));
286                }
287            }
288        }
289    }
290
291    /// Build a cons chain from accumulated patterns.
292    fn build_cons_chain(
293        &self,
294        patterns: &mut BumpVec<'a, &'a Located<Pattern<'a>>>,
295        tail: &'a Located<Pattern<'a>>,
296    ) -> &'a Located<Pattern<'a>> {
297        if patterns.is_empty() {
298            tail
299        } else {
300            // Build right-to-left: a :: b :: c => Cons(a, Cons(b, c))
301            let mut result = tail;
302            while let Some(head) = patterns.pop() {
303                let region = Region::new(
304                    Position::new(head.region.start.line, head.region.start.column),
305                    Position::new(result.region.end.line, result.region.end.column),
306                );
307                result = self.alloc(Located::at(region, Pattern::Cons { head, tail: result }));
308            }
309            result
310        }
311    }
312}
313
314/// Helper enum for pattern expression parsing.
315enum ConsOrAs<'a> {
316    Cons(&'a Located<Pattern<'a>>, Position),
317    As(&'a Located<&'a str>, Position),
318    Done,
319}
320
321/// Snapshot test macro for successful pattern parsing.
322#[cfg(test)]
323macro_rules! assert_pattern_snapshot {
324    ($code:expr) => {{
325        let bump = bumpalo::Bump::new();
326        let src = bump.alloc_str(indoc::indoc!($code));
327        let mut parser = $crate::Parser::new(&bump, src.as_bytes());
328        let (result, _end) = parser.pattern_expr().expect("expected successful parse");
329
330        insta::with_settings!({
331            description => format!("Code:\n\n{}", indoc::indoc!($code)),
332            omit_expression => true,
333        }, {
334            insta::assert_debug_snapshot!(result);
335        });
336    }};
337}
338
339/// Snapshot test macro for pattern parse errors.
340#[cfg(test)]
341macro_rules! assert_pattern_error_snapshot {
342    ($code:expr) => {{
343        let bump = bumpalo::Bump::new();
344        let src = bump.alloc_str(indoc::indoc!($code));
345        let mut parser = $crate::Parser::new(&bump, src.as_bytes());
346        let result = parser.pattern_expr().expect_err("expected parse error");
347
348        insta::with_settings!({
349            description => format!("Code:\n\n{}", indoc::indoc!($code)),
350            omit_expression => true,
351        }, {
352            insta::assert_debug_snapshot!(result);
353        });
354    }};
355}
356
357#[cfg(test)]
358pub(crate) use assert_pattern_error_snapshot;
359#[cfg(test)]
360pub(crate) use assert_pattern_snapshot;
361
362#[cfg(test)]
363mod tests {
364    // Cons patterns
365    #[test]
366    fn cons_simple() {
367        assert_pattern_snapshot!("head :: tail");
368    }
369
370    #[test]
371    fn cons_multiple() {
372        assert_pattern_snapshot!("a :: b :: c");
373    }
374
375    #[test]
376    fn cons_with_empty_list() {
377        assert_pattern_snapshot!("head :: []");
378    }
379
380    // As patterns
381    #[test]
382    fn as_simple() {
383        assert_pattern_snapshot!("x as foo");
384    }
385
386    #[test]
387    fn as_with_tuple() {
388        assert_pattern_snapshot!("(a, b) as pair");
389    }
390
391    #[test]
392    fn as_with_cons() {
393        assert_pattern_snapshot!("head :: tail as list");
394    }
395
396    // Constructor with args
397    #[test]
398    fn ctor_with_one_arg() {
399        assert_pattern_snapshot!("Just x");
400    }
401
402    #[test]
403    fn ctor_with_multiple_args() {
404        assert_pattern_snapshot!("Node left value right");
405    }
406
407    #[test]
408    fn ctor_qualified_with_args() {
409        assert_pattern_snapshot!("Maybe.Just x");
410    }
411
412    #[test]
413    fn ctor_nested() {
414        assert_pattern_snapshot!("Just (Just x)");
415    }
416
417    // Complex combinations
418    #[test]
419    fn complex_pattern() {
420        assert_pattern_snapshot!("Just x :: rest as list");
421    }
422}