Skip to main content

nash_parse/declaration/
infix.rs

1//! Infix declaration parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Declaration.hs` (infix_).
4//!
5//! Parses declarations like: `infix left 6 (|>) = apR`
6
7use nash_region::Located;
8use nash_source::{Associativity, Infix, Precedence};
9
10use crate::Parser;
11use crate::error::Module as ModuleErr;
12
13impl<'a> Parser<'a> {
14    /// Parse an infix declaration.
15    ///
16    /// Parses: `infix left 6 (|>) = apR`
17    ///
18    /// Mirrors Elm's `infix_`:
19    /// ```haskell
20    /// infix_ :: Parser E.Module (A.Located Src.Infix)
21    /// infix_ =
22    ///   do  start <- getPosition
23    ///       Keyword.infix_ err
24    ///       Space.chompAndCheckIndent _err err
25    ///       associativity <- oneOf err [ left, right, non ]
26    ///       Space.chompAndCheckIndent _err err
27    ///       precedence <- Number.precedence err
28    ///       Space.chompAndCheckIndent _err err
29    ///       word1 0x28 {-(-} err
30    ///       op <- Symbol.operator err _err
31    ///       word1 0x29 {-)-} err
32    ///       Space.chompAndCheckIndent _err err
33    ///       word1 0x3D {-=-} err
34    ///       Space.chompAndCheckIndent _err err
35    ///       name <- Var.lower err
36    ///       end <- getPosition
37    ///       Space.chomp _err
38    ///       Space.checkFreshLine err
39    ///       return (A.at start end (Src.Infix op associativity precedence name))
40    /// ```
41    pub fn infix_decl(&mut self) -> Result<&'a Located<Infix<'a>>, ModuleErr<'a>> {
42        let start = self.get_position();
43        let err = ModuleErr::Infix;
44
45        self.keyword_infix(err)?;
46        self.chomp_and_check_indent(|_, r, c| err(r, c), err)?;
47
48        // Parse associativity
49        let associativity = self.one_of(
50            err,
51            vec![
52                Box::new(|p: &mut Parser<'a>| {
53                    p.keyword_left(err)?;
54                    Ok(Associativity::Left)
55                }),
56                Box::new(|p| {
57                    p.keyword_right(err)?;
58                    Ok(Associativity::Right)
59                }),
60                Box::new(|p| {
61                    p.keyword_non(err)?;
62                    Ok(Associativity::None)
63                }),
64            ],
65        )?;
66
67        self.chomp_and_check_indent(|_, r, c| err(r, c), err)?;
68
69        // Parse precedence (single digit 0-9)
70        let precedence = self.precedence(err)?;
71
72        self.chomp_and_check_indent(|_, r, c| err(r, c), err)?;
73
74        // Parse operator in parentheses
75        self.word1(b'(', err)?;
76        let op = self.operator(err, |_, r, c| err(r, c))?;
77        self.word1(b')', err)?;
78
79        self.chomp_and_check_indent(|_, r, c| err(r, c), err)?;
80
81        // Parse equals
82        self.word1(b'=', err)?;
83
84        self.chomp_and_check_indent(|_, r, c| err(r, c), err)?;
85
86        // Parse function name
87        let name = self.lower_name(err)?;
88
89        // Consume trailing whitespace and check for fresh line
90        self.chomp(|_, r, c| err(r, c))?;
91        self.check_fresh_line(err)?;
92
93        let infix = Infix {
94            op,
95            associativity,
96            precedence,
97            name,
98        };
99
100        Ok(self.add_end(start, infix))
101    }
102
103    /// Parse a precedence digit (0-9).
104    ///
105    /// Mirrors Elm's `Number.precedence`.
106    fn precedence<E>(&mut self, to_error: impl FnOnce(u16, u16) -> E) -> Result<Precedence, E> {
107        match self.peek() {
108            Some(b) if b.is_ascii_digit() => {
109                let value = (b - b'0') as u16;
110                self.advance();
111                Ok(Precedence(value))
112            }
113            _ => {
114                let (row, col) = self.position();
115                Err(to_error(row, col))
116            }
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use crate::Parser;
124
125    /// Macro for testing successful infix declaration parsing.
126    /// Note: infix declarations require a trailing newline.
127    macro_rules! assert_infix_snapshot {
128        ($src:expr) => {{
129            let bump = bumpalo::Bump::new();
130            let src = concat!($src, "\n");
131            let src_in_arena = bump.alloc_str(src);
132            let mut parser = Parser::new(&bump, src_in_arena.as_bytes());
133            match parser.infix_decl() {
134                Ok(infix) => {
135                    insta::with_settings!({
136                        description => $src,
137                        omit_expression => true,
138                    }, {
139                        insta::assert_debug_snapshot!(infix);
140                    });
141                }
142                Err(e) => panic!("Expected Ok, got Err: {:?}\n\nSource:\n{}", e, src),
143            }
144        }};
145    }
146
147    #[test]
148    fn infix_left() {
149        assert_infix_snapshot!("infix left 6 (|>) = apR");
150    }
151
152    #[test]
153    fn infix_right() {
154        assert_infix_snapshot!("infix right 5 (::) = cons");
155    }
156
157    #[test]
158    fn infix_non() {
159        assert_infix_snapshot!("infix non 4 (==) = eq");
160    }
161}