Skip to main content

nash_parse/expression/
mod.rs

1//! Expression parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Expression.hs`.
4
5use bumpalo::collections::Vec as BumpVec;
6use nash_region::{Located, Position, Region};
7use nash_source::{BinOpOperand, Expr};
8
9use crate::Parser;
10use crate::error;
11
12mod accessor;
13mod case;
14mod if_;
15mod lambda;
16mod let_;
17mod list;
18mod number;
19mod record;
20mod string;
21mod tuple;
22mod variable;
23
24impl<'a> Parser<'a> {
25    /// Parse a full expression, returning the expression and end position.
26    ///
27    /// Mirrors Elm's `expression`:
28    /// ```haskell
29    /// expression =
30    ///   do  start <- getPosition
31    ///       oneOf E.Start
32    ///         [ let_ start
33    ///         , if_ start
34    ///         , case_ start
35    ///         , function start
36    ///         , do  expr <- possiblyNegativeTerm start
37    ///               ...
38    ///         ]
39    /// ```
40    ///
41    /// Currently implements: lambda, possiblyNegativeTerm + function application.
42    /// TODO: let, if, case, operators
43    pub fn expression(&mut self) -> Result<(&'a Located<Expr<'a>>, Position), error::Expr<'a>> {
44        let start = self.get_position();
45
46        self.one_of(
47            error::Expr::Start,
48            vec![
49                // Let: let defs in expr
50                Box::new(|p: &mut Parser<'a>| p.let_(start)),
51                // Case: case expr of pattern -> branch ...
52                Box::new(|p: &mut Parser<'a>| p.case_(start)),
53                // If: if cond then expr else expr
54                Box::new(|p: &mut Parser<'a>| p.if_(start)),
55                // Lambda: \args -> body
56                Box::new(|p: &mut Parser<'a>| p.lambda(start)),
57                // Term (possibly negated) with function application
58                Box::new(|p| {
59                    let expr = p.possibly_negative_term(start)?;
60                    let end = p.get_position();
61                    p.chomp(error::Expr::Space)?;
62                    p.chomp_expr_end(start, expr, vec![], end)
63                }),
64            ],
65        )
66    }
67
68    /// Handle function application and binary operators.
69    ///
70    /// Mirrors Elm's `chompExprEnd`:
71    /// ```haskell
72    /// chompExprEnd start (State ops expr args end) =
73    ///   oneOfWithFallback
74    ///     [ -- argument
75    ///       do  Space.checkIndent end E.Start
76    ///           arg <- term
77    ///           ...
78    ///     , -- operator
79    ///       do  Space.checkIndent end E.Start
80    ///           op <- addLocation (Symbol.operator ...)
81    ///           ...
82    ///     ]
83    ///     -- done: finalize with toCall and possibly wrap in Binops
84    /// ```
85    pub(crate) fn chomp_expr_end(
86        &mut self,
87        start: Position,
88        expr: &'a Located<Expr<'a>>,
89        args: Vec<&'a Located<Expr<'a>>>,
90        end: Position,
91    ) -> Result<(&'a Located<Expr<'a>>, Position), error::Expr<'a>> {
92        // Track ops for binary operator chains
93        let mut ops: BumpVec<'a, &'a BinOpOperand<'a>> = BumpVec::new_in(self.bump);
94        let mut current_expr = expr;
95        let mut current_args = args;
96        let mut current_end = end;
97
98        loop {
99            let state_for_fallback = (ops.clone(), current_expr, current_args.clone(), current_end);
100
101            let result = self.one_of_with_fallback(
102                vec![
103                    // argument - function application
104                    Box::new(|p: &mut Parser<'a>| {
105                        let (row, col) = p.position();
106                        p.check_indent(row, col, error::Expr::Start)?;
107                        let arg = p.term()?;
108                        let new_end = p.get_position();
109                        p.chomp(error::Expr::Space)?;
110
111                        let mut new_args = current_args.clone();
112                        new_args.push(arg);
113
114                        Ok(ExprEndState::MoreArgs(new_args, new_end))
115                    }),
116                    // operator
117                    Box::new(|p: &mut Parser<'a>| {
118                        let (row, col) = p.position();
119                        p.check_indent(row, col, error::Expr::Start)?;
120
121                        // Save positions for negative-term detection
122                        let op_start = p.get_position();
123
124                        let op = p.add_location_operator(
125                            error::Expr::Start,
126                            error::Expr::OperatorReserved,
127                        )?;
128                        let op_name = op.value;
129                        let op_end = p.get_position();
130
131                        p.chomp_and_check_indent(error::Expr::Space, |row, col| {
132                            error::Expr::IndentOperatorRight(op_name, row, col)
133                        })?;
134
135                        let new_start = p.get_position();
136
137                        // Check for negative term: `-` operator where there's no space before
138                        // but space after (e.g., `a -b` means apply `a` to `-b`)
139                        if op_name == "-"
140                            && current_end != op_start // space before operator
141                            && op_end == new_start
142                        // no space after operator
143                        {
144                            // This is a negative term being passed as an argument
145                            let negated_expr = p.term()?;
146                            let neg_end = p.get_position();
147                            let neg_region = Region::new(op_start, neg_end);
148                            let neg = p.alloc(Located::at(neg_region, Expr::Negate(negated_expr)));
149                            p.chomp(error::Expr::Space)?;
150
151                            let mut new_args = current_args.clone();
152                            new_args.push(neg);
153
154                            Ok(ExprEndState::MoreArgs(new_args, neg_end))
155                        } else {
156                            // Regular binary operator
157                            p.one_of(
158                                |row, col| error::Expr::OperatorRight(op_name, row, col),
159                                vec![
160                                    // Parse a term (possibly negative)
161                                    Box::new(|p: &mut Parser<'a>| {
162                                        let new_expr = p.possibly_negative_term(new_start)?;
163                                        let new_end = p.get_position();
164                                        p.chomp(error::Expr::Space)?;
165
166                                        Ok(ExprEndState::MoreOps(op, new_expr, new_end))
167                                    }),
168                                    // Parse a "final" expression (let, case, if, lambda)
169                                    Box::new(|p: &mut Parser<'a>| {
170                                        let (final_expr, final_end) = p.one_of(
171                                            |row, col| {
172                                                error::Expr::OperatorRight(op_name, row, col)
173                                            },
174                                            vec![
175                                                Box::new(|p: &mut Parser<'a>| p.let_(new_start)),
176                                                Box::new(|p| p.case_(new_start)),
177                                                Box::new(|p| p.if_(new_start)),
178                                                Box::new(|p| p.lambda(new_start)),
179                                            ],
180                                        )?;
181
182                                        Ok(ExprEndState::Final(op, final_expr, final_end))
183                                    }),
184                                ],
185                            )
186                        }
187                    }),
188                ],
189                ExprEndState::Done,
190            )?;
191
192            match result {
193                ExprEndState::MoreArgs(new_args, new_end) => {
194                    current_args = new_args;
195                    current_end = new_end;
196                }
197                ExprEndState::MoreOps(op, new_expr, new_end) => {
198                    // Push (toCall current_expr current_args, op) onto ops
199                    let call_expr = to_call(self, start, current_expr, current_args.clone());
200                    let operand = self.alloc(BinOpOperand {
201                        expr: call_expr,
202                        op,
203                    });
204                    ops.push(operand);
205                    current_expr = new_expr;
206                    current_args = Vec::new();
207                    current_end = new_end;
208                }
209                ExprEndState::Final(op, final_expr, final_end) => {
210                    // Push current and build final Binops
211                    let call_expr = to_call(self, start, current_expr, current_args);
212                    let operand = self.alloc(BinOpOperand {
213                        expr: call_expr,
214                        op,
215                    });
216                    ops.push(operand);
217
218                    let ops_slice = ops.into_bump_slice();
219                    let binops = Expr::BinOps {
220                        operands: ops_slice,
221                        last: final_expr,
222                    };
223                    let result = self.alloc(Located::at(Region::new(start, final_end), binops));
224                    return Ok((result, final_end));
225                }
226                ExprEndState::Done => {
227                    // Finalize - use saved state
228                    let (saved_ops, saved_expr, saved_args, saved_end) = state_for_fallback;
229                    let final_call = to_call(self, start, saved_expr, saved_args);
230
231                    if saved_ops.is_empty() {
232                        return Ok((final_call, saved_end));
233                    } else {
234                        let ops_slice = saved_ops.into_bump_slice();
235                        let binops = Expr::BinOps {
236                            operands: ops_slice,
237                            last: final_call,
238                        };
239                        let result = self.alloc(Located::at(Region::new(start, saved_end), binops));
240                        return Ok((result, saved_end));
241                    }
242                }
243            }
244        }
245    }
246
247    /// Parse possibly negated term: `-term` or `term`.
248    ///
249    /// Mirrors Elm's `possiblyNegativeTerm`:
250    /// ```haskell
251    /// possiblyNegativeTerm start =
252    ///   oneOf E.Start
253    ///     [ do  word1 0x2D {---} E.Start
254    ///           expr <- term
255    ///           addEnd start (Src.Negate expr)
256    ///     , term
257    ///     ]
258    /// ```
259    fn possibly_negative_term(
260        &mut self,
261        start: Position,
262    ) -> Result<&'a Located<Expr<'a>>, error::Expr<'a>> {
263        self.one_of(
264            error::Expr::Start,
265            vec![
266                Box::new(|p: &mut Parser<'a>| {
267                    p.word1(b'-', error::Expr::Start)?;
268                    let expr = p.term()?;
269                    Ok(p.add_end(start, Expr::Negate(expr)))
270                }),
271                Box::new(|p| p.term()),
272            ],
273        )
274    }
275
276    /// Parse a term (atomic expression).
277    ///
278    /// Mirrors Elm's `term`:
279    /// ```haskell
280    /// term =
281    ///   do  start <- getPosition
282    ///       oneOf E.Start
283    ///         [ variable start >>= accessible start
284    ///         , string start
285    ///         , number start
286    ///         , ...
287    ///         ]
288    /// ```
289    pub fn term(&mut self) -> Result<&'a Located<Expr<'a>>, error::Expr<'a>> {
290        let start = self.get_position();
291
292        self.one_of(
293            error::Expr::Start,
294            vec![
295                Box::new(|p: &mut Parser<'a>| {
296                    let expr = p.variable(start)?;
297                    p.accessible(start, expr)
298                }),
299                Box::new(|p| p.string(start)),
300                Box::new(|p| p.number(start)),
301                Box::new(|p| p.list(start)),
302                Box::new(|p| {
303                    let expr = p.record(start)?;
304                    p.accessible(start, expr)
305                }),
306                Box::new(|p| {
307                    let expr = p.tuple(start)?;
308                    p.accessible(start, expr)
309                }),
310                Box::new(|p| p.accessor(start)),
311            ],
312        )
313    }
314}
315
316/// Convert function + args into a Call expression.
317///
318/// Mirrors Elm's `toCall`:
319/// ```haskell
320/// toCall func revArgs =
321///   case revArgs of
322///     [] -> func
323///     lastArg : _ -> A.merge func lastArg (Src.Call func (reverse revArgs))
324/// ```
325fn to_call<'a>(
326    parser: &Parser<'a>,
327    _start: Position,
328    func: &'a Located<Expr<'a>>,
329    args: Vec<&'a Located<Expr<'a>>>,
330) -> &'a Located<Expr<'a>> {
331    if args.is_empty() {
332        func
333    } else {
334        let last_arg = args.last().unwrap();
335        let region = Region::span_across(&func.region, &last_arg.region);
336        let args_slice = parser.alloc_slice_copy(&args);
337        parser.alloc(Located::at(
338            region,
339            Expr::Call {
340                function: func,
341                arguments: args_slice,
342            },
343        ))
344    }
345}
346
347/// State for expression end parsing (function application and binary operators).
348enum ExprEndState<'a> {
349    /// More function arguments accumulated
350    MoreArgs(Vec<&'a Located<Expr<'a>>>, Position),
351    /// Binary operator found, continue parsing chain
352    MoreOps(&'a Located<&'a str>, &'a Located<Expr<'a>>, Position),
353    /// Final expression found (let, case, if, lambda) after operator
354    Final(&'a Located<&'a str>, &'a Located<Expr<'a>>, Position),
355    /// Done parsing, finalize expression
356    Done,
357}
358
359/// Snapshot test macro for successful expression parsing.
360#[cfg(test)]
361macro_rules! assert_expr_snapshot {
362    ($code:expr) => {{
363        let bump = bumpalo::Bump::new();
364        let src = bump.alloc_str(indoc::indoc!($code));
365        let mut parser = $crate::Parser::new(&bump, src.as_bytes());
366        let result = parser.term().expect("expected successful parse");
367
368        insta::with_settings!({
369            description => format!("Code:\n\n{}", indoc::indoc!($code)),
370            omit_expression => true,
371        }, {
372            insta::assert_debug_snapshot!(result);
373        });
374    }};
375}
376
377/// Snapshot test macro for expression parse errors.
378#[cfg(test)]
379macro_rules! assert_expr_error_snapshot {
380    ($code:expr) => {{
381        let bump = bumpalo::Bump::new();
382        let src = bump.alloc_str(indoc::indoc!($code));
383        let mut parser = $crate::Parser::new(&bump, src.as_bytes());
384        let result = parser.term().expect_err("expected parse error");
385
386        insta::with_settings!({
387            description => format!("Code:\n\n{}", indoc::indoc!($code)),
388            omit_expression => true,
389        }, {
390            insta::assert_debug_snapshot!(result);
391        });
392    }};
393}
394
395/// Snapshot test macro for full expression parsing.
396#[cfg(test)]
397macro_rules! assert_expression_snapshot {
398    ($code:expr) => {{
399        let bump = bumpalo::Bump::new();
400        let src = bump.alloc_str(indoc::indoc!($code));
401        let mut parser = $crate::Parser::new(&bump, src.as_bytes());
402        let (result, _end) = parser.expression().expect("expected successful parse");
403
404        insta::with_settings!({
405            description => format!("Code:\n\n{}", indoc::indoc!($code)),
406            omit_expression => true,
407        }, {
408            insta::assert_debug_snapshot!(result);
409        });
410    }};
411}
412
413#[cfg(test)]
414pub(crate) use assert_expr_error_snapshot;
415#[cfg(test)]
416pub(crate) use assert_expr_snapshot;
417#[cfg(test)]
418pub(crate) use assert_expression_snapshot;
419
420#[cfg(test)]
421mod tests {
422    #[test]
423    fn negate_var() {
424        assert_expression_snapshot!("-x");
425    }
426
427    #[test]
428    fn negate_number() {
429        assert_expression_snapshot!("-42");
430    }
431
432    #[test]
433    fn negate_parens() {
434        assert_expression_snapshot!("-(a)");
435    }
436
437    #[test]
438    fn expr_simple_var() {
439        assert_expression_snapshot!("foo");
440    }
441
442    #[test]
443    fn expr_simple_number() {
444        assert_expression_snapshot!("123");
445    }
446
447    #[test]
448    fn application_single() {
449        assert_expression_snapshot!("f x");
450    }
451
452    #[test]
453    fn application_multiple() {
454        assert_expression_snapshot!("f x y z");
455    }
456
457    #[test]
458    fn application_nested() {
459        assert_expression_snapshot!("f (g x)");
460    }
461
462    #[test]
463    fn application_with_record() {
464        assert_expression_snapshot!("f { x = 1 }");
465    }
466
467    // Binary operators
468    #[test]
469    fn binop_simple_add() {
470        assert_expression_snapshot!("a + b");
471    }
472
473    #[test]
474    fn binop_simple_subtract() {
475        assert_expression_snapshot!("a - b");
476    }
477
478    #[test]
479    fn binop_simple_multiply() {
480        assert_expression_snapshot!("a * b");
481    }
482
483    #[test]
484    fn binop_simple_divide() {
485        assert_expression_snapshot!("a / b");
486    }
487
488    #[test]
489    fn binop_chained() {
490        assert_expression_snapshot!("a + b + c");
491    }
492
493    #[test]
494    fn binop_mixed() {
495        assert_expression_snapshot!("a + b * c");
496    }
497
498    #[test]
499    fn binop_with_parens() {
500        assert_expression_snapshot!("(a + b) * c");
501    }
502
503    #[test]
504    fn binop_pipe_right() {
505        assert_expression_snapshot!("a |> b |> c");
506    }
507
508    #[test]
509    fn binop_pipe_left() {
510        assert_expression_snapshot!("c <| b <| a");
511    }
512
513    #[test]
514    fn binop_comparison() {
515        assert_expression_snapshot!("a == b");
516    }
517
518    #[test]
519    fn binop_less_than() {
520        assert_expression_snapshot!("a < b");
521    }
522
523    #[test]
524    fn binop_greater_than() {
525        assert_expression_snapshot!("a > b");
526    }
527
528    #[test]
529    fn binop_append() {
530        assert_expression_snapshot!("a ++ b");
531    }
532
533    #[test]
534    fn binop_cons() {
535        assert_expression_snapshot!("a :: b");
536    }
537
538    #[test]
539    fn binop_logical_and() {
540        assert_expression_snapshot!("a && b");
541    }
542
543    #[test]
544    fn binop_logical_or() {
545        assert_expression_snapshot!("a || b");
546    }
547
548    #[test]
549    fn binop_less_than_or_equal() {
550        assert_expression_snapshot!("a <= b");
551    }
552
553    #[test]
554    fn binop_greater_than_or_equal() {
555        assert_expression_snapshot!("a >= b");
556    }
557
558    #[test]
559    fn binop_not_equal() {
560        assert_expression_snapshot!("a /= b");
561    }
562
563    #[test]
564    fn binop_compose_right() {
565        assert_expression_snapshot!("f >> g");
566    }
567
568    #[test]
569    fn binop_compose_left() {
570        assert_expression_snapshot!("f << g");
571    }
572
573    #[test]
574    fn binop_power() {
575        assert_expression_snapshot!("a ^ b");
576    }
577
578    #[test]
579    fn binop_with_function_application() {
580        assert_expression_snapshot!("f x + g y");
581    }
582
583    #[test]
584    fn binop_with_negation() {
585        assert_expression_snapshot!("a + -b");
586    }
587
588    #[test]
589    fn binop_negative_first() {
590        assert_expression_snapshot!("-a + b");
591    }
592
593    #[test]
594    fn binop_with_let() {
595        assert_expression_snapshot!("a + let x = 1 in x");
596    }
597
598    #[test]
599    fn binop_with_if() {
600        assert_expression_snapshot!("a + if b then c else d");
601    }
602
603    #[test]
604    fn binop_with_case() {
605        assert_expression_snapshot!(
606            r#"
607            a + case x of
608                1 -> y
609                _ -> z
610            "#
611        );
612    }
613
614    #[test]
615    fn binop_with_lambda() {
616        assert_expression_snapshot!("a + \\x -> x");
617    }
618
619    // Operator sections
620    #[test]
621    fn op_section_plus() {
622        assert_expression_snapshot!("(+)");
623    }
624
625    #[test]
626    fn op_section_minus() {
627        assert_expression_snapshot!("(-)");
628    }
629
630    #[test]
631    fn op_section_multiply() {
632        assert_expression_snapshot!("(*)");
633    }
634
635    #[test]
636    fn op_section_append() {
637        assert_expression_snapshot!("(++)");
638    }
639
640    #[test]
641    fn op_section_pipe_right() {
642        assert_expression_snapshot!("(|>)");
643    }
644
645    #[test]
646    fn op_section_cons() {
647        assert_expression_snapshot!("(::)");
648    }
649
650    // Negation vs subtraction
651    #[test]
652    fn application_with_negative_arg() {
653        // `f -x` means f applied to -x (negative x), not f minus x
654        assert_expression_snapshot!("f -x");
655    }
656
657    #[test]
658    fn negation_in_parens() {
659        assert_expression_snapshot!("(-42)");
660    }
661
662    #[test]
663    fn negation_in_tuple() {
664        assert_expression_snapshot!("(-a, b)");
665    }
666}