Skip to main content

r/parser/
builder.rs

1//! AST builder functions — converts pest `Pair`s into `Expr` nodes.
2
3use pest::iterators::Pair;
4
5use super::ast::*;
6use super::literals::{parse_complex, parse_number, parse_raw_string, parse_string};
7use super::Rule;
8
9// region: Identifier helpers
10
11fn unescape_backtick_ident(s: &str) -> String {
12    s.replace("\\`", "`")
13}
14
15pub(super) fn parse_ident_str(pair: Pair<Rule>) -> String {
16    let s = pair.as_str();
17    if s.starts_with('`') && s.ends_with('`') {
18        unescape_backtick_ident(&s[1..s.len() - 1])
19    } else {
20        s.to_string()
21    }
22}
23
24pub(super) fn parse_ident_or_string(pair: Pair<Rule>) -> String {
25    let s = pair.as_str();
26    if s.starts_with('`') && s.ends_with('`') {
27        unescape_backtick_ident(&s[1..s.len() - 1])
28    } else if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\''))
29    {
30        super::literals::unescape_string(&s[1..s.len() - 1])
31    } else {
32        s.to_string()
33    }
34}
35
36// endregion
37
38// region: Program and expression dispatch
39
40pub(super) fn build_program(pair: Pair<Rule>) -> Expr {
41    let mut exprs = Vec::new();
42    for p in pair.into_inner() {
43        match p.as_rule() {
44            Rule::expr_seq => {
45                for child in p.into_inner() {
46                    if child.as_rule() == Rule::expr {
47                        exprs.push(build_expr(child));
48                    }
49                }
50            }
51            Rule::EOI => {}
52            _ => {}
53        }
54    }
55    if exprs.len() == 1 {
56        exprs
57            .into_iter()
58            .next()
59            .expect("parser: single-expr program should have one element")
60    } else {
61        Expr::Program(exprs)
62    }
63}
64
65pub(super) fn build_expr(pair: Pair<Rule>) -> Expr {
66    match pair.as_rule() {
67        Rule::expr => build_expr(
68            pair.into_inner()
69                .next()
70                .expect("parser: expr should have inner expression"),
71        ),
72        Rule::help_expr => build_help(pair),
73        Rule::assign_eq_expr => build_assign_eq(pair),
74        Rule::walrus_expr => build_walrus(pair),
75        Rule::assign_left_expr => build_assign_left(pair),
76        Rule::assign_right_expr => build_assign_right(pair),
77        Rule::formula_expr => build_formula(pair),
78        Rule::or_expr => build_binary_left(pair, |op| match op.as_str() {
79            "||" => BinaryOp::OrScalar,
80            "|" => BinaryOp::Or,
81            _ => unreachable!(),
82        }),
83        Rule::and_expr => build_binary_left(pair, |op| match op.as_str() {
84            "&&" => BinaryOp::AndScalar,
85            "&" => BinaryOp::And,
86            _ => unreachable!(),
87        }),
88        Rule::not_expr => build_not(pair),
89        Rule::compare_expr => build_binary_left(pair, |op| match op.as_str() {
90            "==" => BinaryOp::Eq,
91            "!=" => BinaryOp::Ne,
92            "<" => BinaryOp::Lt,
93            ">" => BinaryOp::Gt,
94            "<=" => BinaryOp::Le,
95            ">=" => BinaryOp::Ge,
96            _ => unreachable!(),
97        }),
98        Rule::add_expr => build_binary_left(pair, |op| match op.as_str() {
99            "+" => BinaryOp::Add,
100            "-" => BinaryOp::Sub,
101            _ => unreachable!(),
102        }),
103        Rule::mul_expr => build_binary_left(pair, |op| match op.as_str() {
104            "*" => BinaryOp::Mul,
105            "/" => BinaryOp::Div,
106            _ => unreachable!(),
107        }),
108        Rule::special_pipe_expr => build_special_pipe(pair),
109        Rule::colon_expr => build_colon(pair),
110        Rule::unary_expr => build_unary(pair),
111        Rule::power_expr => build_power(pair),
112        Rule::postfix_expr => build_postfix_expr(pair),
113        Rule::namespace_expr => build_namespace_expr(pair),
114        Rule::primary_expr => build_primary(pair),
115        Rule::keyword_constant => build_primary(pair),
116        _ => build_primary(pair),
117    }
118}
119
120// endregion
121
122// region: Help expression
123
124fn build_help(pair: Pair<Rule>) -> Expr {
125    let mut inner = pair.into_inner();
126    let first = inner
127        .next()
128        .expect("parser: help_expr should have at least one child");
129    if first.as_rule() == Rule::help_expr {
130        // Unary: "?foo" -> help("foo")
131        let topic = extract_help_topic(&first);
132        // Wrap in invisible() so ?foo doesn't print NULL
133        Expr::Call {
134            func: Box::new(Expr::Symbol("invisible".to_string())),
135            args: vec![Arg {
136                name: None,
137                value: Some(Expr::Call {
138                    func: Box::new(Expr::Symbol("help".to_string())),
139                    args: vec![Arg {
140                        name: None,
141                        value: Some(Expr::String(topic)),
142                    }],
143                    span: None,
144                }),
145            }],
146            span: None,
147        }
148    } else {
149        // Binary: expr ? topic -> help("topic", package="expr")
150        // e.g., methods?show -> help("show", package="methods")
151        let pkg_text = first.as_str().trim().to_string();
152        match inner.next() {
153            Some(rhs) => {
154                let topic = extract_help_topic(&rhs);
155                Expr::Call {
156                    func: Box::new(Expr::Symbol("invisible".to_string())),
157                    args: vec![Arg {
158                        name: None,
159                        value: Some(Expr::Call {
160                            func: Box::new(Expr::Symbol("help".to_string())),
161                            args: vec![
162                                Arg {
163                                    name: None,
164                                    value: Some(Expr::String(topic)),
165                                },
166                                Arg {
167                                    name: Some("package".to_string()),
168                                    value: Some(Expr::String(pkg_text)),
169                                },
170                            ],
171                            span: None,
172                        }),
173                    }],
174                    span: None,
175                }
176            }
177            None => build_expr(first),
178        }
179    }
180}
181
182/// Extract the topic name from a help expression for `?foo` or `?foo(...)`.
183/// Strips leading `?`, then extracts the function name from call expressions
184/// (e.g. `plot(1:10)` → `plot`).
185fn extract_help_topic(pair: &Pair<Rule>) -> String {
186    let text = pair.as_str().trim();
187    // Strip leading ? if present
188    let text = text.strip_prefix('?').unwrap_or(text).trim();
189    // If it looks like a call `name(...)`, extract just the name
190    if let Some(paren_pos) = text.find('(') {
191        let name = text[..paren_pos].trim();
192        if !name.is_empty()
193            && name
194                .chars()
195                .all(|c| c.is_alphanumeric() || c == '.' || c == '_')
196        {
197            return name.to_string();
198        }
199    }
200    text.to_string()
201}
202
203// endregion
204
205// region: Assignment expressions
206
207fn build_assign_eq(pair: Pair<Rule>) -> Expr {
208    let mut inner = pair.into_inner();
209    let lhs = build_expr(
210        inner
211            .next()
212            .expect("parser: assign_eq_expr should have LHS"),
213    );
214    match inner.next() {
215        None => lhs,
216        Some(op_pair) => {
217            assert!(op_pair.as_rule() == Rule::eq_assign_op);
218            let rhs = build_expr(
219                inner
220                    .next()
221                    .expect("parser: assign_eq_expr should have RHS after '='"),
222            );
223            Expr::Assign {
224                op: AssignOp::Equals,
225                target: Box::new(lhs),
226                value: Box::new(rhs),
227            }
228        }
229    }
230}
231
232fn build_walrus(pair: Pair<Rule>) -> Expr {
233    let mut inner = pair.into_inner();
234    let lhs = build_expr(inner.next().expect("parser: walrus_expr should have LHS"));
235    match inner.next() {
236        None => lhs,
237        Some(op_pair) => {
238            assert!(op_pair.as_rule() == Rule::walrus_assign_op);
239            let rhs = build_expr(
240                inner
241                    .next()
242                    .expect("parser: walrus_expr should have RHS after ':='"),
243            );
244            Expr::BinaryOp {
245                op: BinaryOp::Special(SpecialOp::Walrus),
246                lhs: Box::new(lhs),
247                rhs: Box::new(rhs),
248            }
249        }
250    }
251}
252
253fn build_assign_left(pair: Pair<Rule>) -> Expr {
254    let mut inner = pair.into_inner();
255    let lhs = build_expr(
256        inner
257            .next()
258            .expect("parser: assign_left_expr should have LHS"),
259    );
260    match inner.next() {
261        None => lhs,
262        Some(op_pair) => {
263            let op = match op_pair.as_str() {
264                "<-" => AssignOp::LeftAssign,
265                "<<-" => AssignOp::SuperAssign,
266                _ => unreachable!(),
267            };
268            let rhs = build_expr(
269                inner
270                    .next()
271                    .expect("parser: assign_left_expr should have RHS after '<-'/'<<-'"),
272            );
273            Expr::Assign {
274                op,
275                target: Box::new(lhs),
276                value: Box::new(rhs),
277            }
278        }
279    }
280}
281
282fn build_assign_right(pair: Pair<Rule>) -> Expr {
283    let mut inner = pair.into_inner();
284    let mut result = build_expr(
285        inner
286            .next()
287            .expect("parser: assign_right_expr should have LHS"),
288    );
289    while let Some(op_pair) = inner.next() {
290        let op = match op_pair.as_str() {
291            "->" => AssignOp::RightAssign,
292            "->>" => AssignOp::RightSuperAssign,
293            _ => unreachable!(),
294        };
295        let target = build_expr(
296            inner
297                .next()
298                .expect("parser: assign_right_expr should have target after '->'/'->>'"),
299        );
300        result = Expr::Assign {
301            op,
302            target: Box::new(target),
303            value: Box::new(result),
304        };
305    }
306    result
307}
308
309// endregion
310
311// region: Formula
312
313fn build_formula(pair: Pair<Rule>) -> Expr {
314    let mut inner = pair.into_inner();
315    let first = inner
316        .next()
317        .expect("parser: formula_expr should have at least one child");
318
319    if first.as_rule() == Rule::formula_expr {
320        // Unary formula: "~" ~ formula_expr
321        let rhs = build_expr(first);
322        Expr::Formula {
323            lhs: None,
324            rhs: Some(Box::new(rhs)),
325        }
326    } else {
327        let lhs = build_expr(first);
328        match inner.next() {
329            None => lhs,
330            Some(op_pair) => {
331                let rhs = build_expr(
332                    inner
333                        .next()
334                        .expect("parser: formula_expr should have RHS after tilde op"),
335                );
336                let remaining: Vec<_> = inner.collect();
337
338                if op_pair.as_rule() == Rule::tilde_op
339                    && op_pair.as_str() == "~"
340                    && remaining.is_empty()
341                {
342                    return Expr::Formula {
343                        lhs: Some(Box::new(lhs)),
344                        rhs: Some(Box::new(rhs)),
345                    };
346                }
347
348                let mut expr = Expr::BinaryOp {
349                    op: map_tilde_op(&op_pair),
350                    lhs: Box::new(lhs),
351                    rhs: Box::new(rhs),
352                };
353
354                let mut remaining = remaining.into_iter();
355                while let Some(next_op) = remaining.next() {
356                    let next_rhs = build_expr(
357                        remaining
358                            .next()
359                            .expect("parser: chained tilde should have RHS after operator"),
360                    );
361                    expr = Expr::BinaryOp {
362                        op: map_tilde_op(&next_op),
363                        lhs: Box::new(expr),
364                        rhs: Box::new(next_rhs),
365                    };
366                }
367
368                expr
369            }
370        }
371    }
372}
373
374fn map_tilde_op(pair: &Pair<Rule>) -> BinaryOp {
375    match pair.as_str() {
376        "~" => BinaryOp::Tilde,
377        "~~" => BinaryOp::DoubleTilde,
378        _ => unreachable!(),
379    }
380}
381
382// endregion
383
384// region: Binary and unary operators
385
386fn build_binary_left(pair: Pair<Rule>, map_op: impl Fn(&Pair<Rule>) -> BinaryOp) -> Expr {
387    let mut inner = pair.into_inner();
388    let mut lhs = build_expr(
389        inner
390            .next()
391            .expect("parser: binary expression should have LHS"),
392    );
393    while let Some(op_pair) = inner.next() {
394        let op = map_op(&op_pair);
395        let rhs = build_expr(
396            inner
397                .next()
398                .expect("parser: binary expression should have RHS after operator"),
399        );
400        lhs = Expr::BinaryOp {
401            op,
402            lhs: Box::new(lhs),
403            rhs: Box::new(rhs),
404        };
405    }
406    lhs
407}
408
409fn build_not(pair: Pair<Rule>) -> Expr {
410    let mut inner = pair.into_inner();
411    let first = inner.next().expect("parser: not_expr should have operand");
412    if first.as_rule() == Rule::compare_expr {
413        build_expr(first)
414    } else {
415        // "!" ~ not_expr
416        let operand = build_expr(first);
417        Expr::UnaryOp {
418            op: UnaryOp::Not,
419            operand: Box::new(operand),
420        }
421    }
422}
423
424fn build_special_pipe(pair: Pair<Rule>) -> Expr {
425    let mut inner = pair.into_inner();
426    let mut lhs = build_expr(
427        inner
428            .next()
429            .expect("parser: special_pipe_expr should have LHS"),
430    );
431    while let Some(op_pair) = inner.next() {
432        let op = match op_pair.as_rule() {
433            Rule::pipe_op => BinaryOp::Pipe,
434            Rule::special_op => match op_pair.as_str() {
435                "%in%" => BinaryOp::Special(SpecialOp::In),
436                "%*%" => BinaryOp::Special(SpecialOp::MatMul),
437                "%x%" => BinaryOp::Special(SpecialOp::Kronecker),
438                "%%" => BinaryOp::Mod,
439                "%/%" => BinaryOp::IntDiv,
440                // %>% is identical to |> — both support _ and . placeholders
441                "%>%" => BinaryOp::Pipe,
442                "%<>%" => BinaryOp::AssignPipe,
443                "%T>%" => BinaryOp::TeePipe,
444                "%$%" => BinaryOp::ExpoPipe,
445                other => BinaryOp::Special(SpecialOp::Other(other.to_string())),
446            },
447            _ => unreachable!(),
448        };
449        let rhs = build_expr(
450            inner
451                .next()
452                .expect("parser: special/pipe expression should have RHS after operator"),
453        );
454        lhs = Expr::BinaryOp {
455            op,
456            lhs: Box::new(lhs),
457            rhs: Box::new(rhs),
458        };
459    }
460    lhs
461}
462
463fn build_colon(pair: Pair<Rule>) -> Expr {
464    let mut inner = pair.into_inner();
465    let mut lhs = build_expr(inner.next().expect("parser: colon_expr should have LHS"));
466    for rhs_pair in inner {
467        lhs = Expr::BinaryOp {
468            op: BinaryOp::Range,
469            lhs: Box::new(lhs),
470            rhs: Box::new(build_expr(rhs_pair)),
471        };
472    }
473    lhs
474}
475
476fn build_unary(pair: Pair<Rule>) -> Expr {
477    let mut inner = pair.into_inner();
478    let first = inner
479        .next()
480        .expect("parser: unary_expr should have operator or operand");
481    match first.as_rule() {
482        Rule::unary_op => {
483            let op = match first.as_str() {
484                "-" => UnaryOp::Neg,
485                "+" => UnaryOp::Pos,
486                _ => unreachable!(),
487            };
488            let operand = build_expr(
489                inner
490                    .next()
491                    .expect("parser: unary_expr should have operand after unary operator"),
492            );
493            Expr::UnaryOp {
494                op,
495                operand: Box::new(operand),
496            }
497        }
498        // "!" at unary level (allows a == !b)
499        Rule::unary_expr => {
500            let operand = build_expr(first);
501            Expr::UnaryOp {
502                op: UnaryOp::Not,
503                operand: Box::new(operand),
504            }
505        }
506        _ => build_expr(first),
507    }
508}
509
510fn build_power(pair: Pair<Rule>) -> Expr {
511    let mut inner = pair.into_inner();
512    let base = build_expr(
513        inner
514            .next()
515            .expect("parser: power_expr should have base expression"),
516    );
517    // Skip the power_op token if present
518    match inner.next() {
519        None => base,
520        Some(next) => {
521            let rhs_pair = if next.as_rule() == Rule::power_op {
522                inner
523                    .next()
524                    .expect("parser: power_expr should have exponent after '^'/'**'")
525            } else {
526                next
527            };
528            Expr::BinaryOp {
529                op: BinaryOp::Pow,
530                lhs: Box::new(base),
531                rhs: Box::new(build_expr(rhs_pair)),
532            }
533        }
534    }
535}
536
537// endregion
538
539// region: Postfix and namespace expressions
540
541fn build_postfix_expr(pair: Pair<Rule>) -> Expr {
542    let mut inner = pair.into_inner();
543    let mut expr = build_expr(
544        inner
545            .next()
546            .expect("parser: postfix_expr should have base expression"),
547    );
548    for suffix in inner {
549        expr = build_postfix_suffix(expr, suffix);
550    }
551    expr
552}
553
554fn build_postfix_suffix(object: Expr, pair: Pair<Rule>) -> Expr {
555    // Unwrap postfix_suffix wrapper if present, capturing span for calls
556    let outer_span = pair.as_span();
557    let pair = if pair.as_rule() == Rule::postfix_suffix {
558        pair.into_inner()
559            .next()
560            .expect("parser: postfix_suffix should have inner suffix kind")
561    } else {
562        pair
563    };
564    match pair.as_rule() {
565        Rule::call_suffix => {
566            let args = pair
567                .into_inner()
568                .filter(|p| p.as_rule() == Rule::arg_list)
569                .flat_map(build_arg_list)
570                .collect();
571            Expr::Call {
572                func: Box::new(object),
573                args,
574                span: Some(crate::parser::ast::Span {
575                    start: u32::try_from(outer_span.start()).unwrap_or(0),
576                    end: u32::try_from(outer_span.end()).unwrap_or(0),
577                }),
578            }
579        }
580        Rule::index1_suffix => {
581            let indices = pair
582                .into_inner()
583                .filter(|p| p.as_rule() == Rule::sub_list)
584                .flat_map(build_sub_list)
585                .collect();
586            Expr::Index {
587                object: Box::new(object),
588                indices,
589            }
590        }
591        Rule::index2_suffix => {
592            let indices = pair
593                .into_inner()
594                .filter(|p| p.as_rule() == Rule::sub_list)
595                .flat_map(build_sub_list)
596                .collect();
597            Expr::IndexDouble {
598                object: Box::new(object),
599                indices,
600            }
601        }
602        Rule::dollar_suffix => {
603            let inner = pair
604                .into_inner()
605                .next()
606                .expect("parser: dollar_suffix should have member name");
607            let name = match inner.as_rule() {
608                Rule::dots => "...".to_string(),
609                _ => parse_ident_or_string(inner),
610            };
611            Expr::Dollar {
612                object: Box::new(object),
613                member: name,
614            }
615        }
616        Rule::slot_suffix => {
617            let inner = pair
618                .into_inner()
619                .next()
620                .expect("parser: slot_suffix should have slot name");
621            let name = parse_ident_str(inner);
622            Expr::Slot {
623                object: Box::new(object),
624                member: name,
625            }
626        }
627        _ => unreachable!("unexpected postfix: {:?}", pair.as_rule()),
628    }
629}
630
631fn build_namespace_expr(pair: Pair<Rule>) -> Expr {
632    let mut inner = pair.into_inner();
633    let mut expr = build_expr(
634        inner
635            .next()
636            .expect("parser: namespace_expr should have base expression"),
637    );
638    for suffix in inner {
639        if suffix.as_rule() == Rule::namespace_suffix {
640            let mut ns_inner = suffix.into_inner();
641            let op_pair = ns_inner
642                .next()
643                .expect("parser: namespace_suffix should have '::' or ':::'");
644            let op_str = op_pair.as_str();
645            let name_pair = ns_inner
646                .next()
647                .expect("parser: namespace_suffix should have name after '::'/':::'");
648            let name = parse_ident_or_string(name_pair);
649            expr = if op_str == ":::" {
650                Expr::NsGetInt {
651                    namespace: Box::new(expr),
652                    name,
653                }
654            } else {
655                Expr::NsGet {
656                    namespace: Box::new(expr),
657                    name,
658                }
659            };
660        }
661    }
662    expr
663}
664
665// endregion
666
667// region: Primary expressions
668
669fn build_primary(pair: Pair<Rule>) -> Expr {
670    let pair = match pair.as_rule() {
671        Rule::primary_expr | Rule::keyword_constant => pair
672            .into_inner()
673            .next()
674            .expect("parser: primary_expr/keyword_constant should have inner content"),
675        _ => pair,
676    };
677
678    match pair.as_rule() {
679        Rule::null_lit => Expr::Null,
680        Rule::na_lit => {
681            let s = pair.as_str();
682            let na_type = if s.starts_with("NA_complex") {
683                NaType::Complex
684            } else if s.starts_with("NA_character") {
685                NaType::Character
686            } else if s.starts_with("NA_real") {
687                NaType::Real
688            } else if s.starts_with("NA_integer") {
689                NaType::Integer
690            } else {
691                NaType::Logical
692            };
693            Expr::Na(na_type)
694        }
695        Rule::inf_lit => Expr::Inf,
696        Rule::nan_lit => Expr::NaN,
697        Rule::bool_lit => {
698            let val = pair.as_str().starts_with('T');
699            Expr::Bool(val)
700        }
701        Rule::complex_number => parse_complex(pair),
702        Rule::number => parse_number(pair),
703        Rule::raw_string => parse_raw_string(pair),
704        Rule::string => parse_string(pair),
705        Rule::dots => Expr::Dots,
706        Rule::dotdot => {
707            let s = pair.as_str();
708            let n: u32 = s[2..].parse().unwrap_or(1);
709            Expr::DotDot(n)
710        }
711        Rule::formula_literal => {
712            let rhs = pair
713                .into_inner()
714                .next()
715                .map(build_expr)
716                .unwrap_or(Expr::Null);
717            Expr::Formula {
718                lhs: None,
719                rhs: Some(Box::new(rhs)),
720            }
721        }
722        Rule::ident => {
723            let name = parse_ident_str(pair);
724            Expr::Symbol(name)
725        }
726        Rule::if_expr => build_if(pair),
727        Rule::for_expr => build_for(pair),
728        Rule::while_expr => build_while(pair),
729        Rule::repeat_expr => {
730            let body = pair
731                .into_inner()
732                .find(|p| p.as_rule() == Rule::expr)
733                .map(build_expr)
734                .unwrap_or(Expr::Null);
735            Expr::Repeat {
736                body: Box::new(body),
737            }
738        }
739        Rule::break_expr => Expr::Break,
740        Rule::next_expr => Expr::Next,
741        Rule::return_expr => {
742            let val = pair
743                .into_inner()
744                .find(|p| p.as_rule() == Rule::expr)
745                .map(|p| Box::new(build_expr(p)));
746            Expr::Return(val)
747        }
748        Rule::function_def | Rule::lambda_def => build_function(pair),
749        Rule::block => build_block(pair),
750        Rule::paren_expr => {
751            let inner = pair
752                .into_inner()
753                .find(|p| p.as_rule() == Rule::expr)
754                .expect("parser: paren_expr should contain an expression");
755            build_expr(inner)
756        }
757        _ => build_expr(pair),
758    }
759}
760
761// endregion
762
763// region: Control flow
764
765fn build_if(pair: Pair<Rule>) -> Expr {
766    let mut exprs: Vec<Expr> = pair
767        .into_inner()
768        .filter(|p| p.as_rule() == Rule::expr)
769        .map(build_expr)
770        .collect();
771    let condition = exprs.remove(0);
772    let then_body = exprs.remove(0);
773    let else_body = if !exprs.is_empty() {
774        Some(Box::new(exprs.remove(0)))
775    } else {
776        None
777    };
778    Expr::If {
779        condition: Box::new(condition),
780        then_body: Box::new(then_body),
781        else_body,
782    }
783}
784
785fn build_for(pair: Pair<Rule>) -> Expr {
786    let inner = pair.into_inner();
787    let mut var = String::new();
788    let mut exprs = Vec::new();
789    for p in inner {
790        match p.as_rule() {
791            Rule::ident => var = parse_ident_str(p),
792            Rule::expr => exprs.push(build_expr(p)),
793            _ => {}
794        }
795    }
796    let iter = exprs.remove(0);
797    let body = exprs.remove(0);
798    Expr::For {
799        var,
800        iter: Box::new(iter),
801        body: Box::new(body),
802    }
803}
804
805fn build_while(pair: Pair<Rule>) -> Expr {
806    let exprs: Vec<Expr> = pair
807        .into_inner()
808        .filter(|p| p.as_rule() == Rule::expr)
809        .map(build_expr)
810        .collect();
811    Expr::While {
812        condition: Box::new(exprs[0].clone()),
813        body: Box::new(exprs[1].clone()),
814    }
815}
816
817fn build_function(pair: Pair<Rule>) -> Expr {
818    let inner = pair.into_inner();
819    let mut params = Vec::new();
820    let mut body = None;
821
822    for p in inner {
823        match p.as_rule() {
824            Rule::param_list => {
825                params = build_param_list(p);
826            }
827            Rule::expr => {
828                body = Some(build_expr(p));
829            }
830            _ => {}
831        }
832    }
833
834    Expr::Function {
835        params,
836        body: Box::new(body.unwrap_or(Expr::Null)),
837    }
838}
839
840fn build_param_list(pair: Pair<Rule>) -> Vec<Param> {
841    pair.into_inner()
842        .filter(|p| p.as_rule() == Rule::param)
843        .map(|p| {
844            let mut inner = p.into_inner();
845            let first = inner
846                .next()
847                .expect("parser: param should have name or dots");
848            if first.as_rule() == Rule::dots {
849                Param {
850                    name: "...".to_string(),
851                    default: None,
852                    is_dots: true,
853                }
854            } else {
855                let name = parse_ident_str(first);
856                // Check for = and default value
857                let default = inner.find(|p| p.as_rule() == Rule::expr).map(build_expr);
858                Param {
859                    name,
860                    default,
861                    is_dots: false,
862                }
863            }
864        })
865        .collect()
866}
867
868fn build_block(pair: Pair<Rule>) -> Expr {
869    let mut exprs = Vec::new();
870    for p in pair.into_inner() {
871        match p.as_rule() {
872            Rule::expr_seq => {
873                for child in p.into_inner() {
874                    if child.as_rule() == Rule::expr {
875                        exprs.push(build_expr(child));
876                    }
877                }
878            }
879            Rule::expr => exprs.push(build_expr(p)),
880            _ => {}
881        }
882    }
883    if exprs.is_empty() {
884        Expr::Null
885    } else {
886        Expr::Block(exprs)
887    }
888}
889
890// endregion
891
892// region: Argument lists
893
894fn build_arg_list(pair: Pair<Rule>) -> Vec<Arg> {
895    pair.into_inner()
896        .filter(|p| p.as_rule() == Rule::arg_slot)
897        .map(|slot| {
898            match slot.into_inner().next() {
899                None => Arg {
900                    name: None,
901                    value: None,
902                }, // empty arg
903                Some(arg_pair) => build_arg_or_sub(arg_pair),
904            }
905        })
906        .collect()
907}
908
909fn build_sub_list(pair: Pair<Rule>) -> Vec<Arg> {
910    pair.into_inner()
911        .filter(|p| p.as_rule() == Rule::sub_slot)
912        .map(|slot| {
913            match slot.into_inner().next() {
914                None => Arg {
915                    name: None,
916                    value: None,
917                }, // empty slot
918                Some(sub_pair) => build_arg_or_sub(sub_pair),
919            }
920        })
921        .collect()
922}
923
924/// Shared logic for both call args and index args — structurally identical.
925fn build_arg_or_sub(pair: Pair<Rule>) -> Arg {
926    let inner_pair = pair
927        .into_inner()
928        .next()
929        .expect("parser: arg/sub should have content");
930    match inner_pair.as_rule() {
931        Rule::named_arg | Rule::named_sub_arg => build_named_arg(inner_pair),
932        _ => Arg {
933            name: None,
934            value: Some(build_expr(inner_pair)),
935        },
936    }
937}
938
939fn build_named_arg(pair: Pair<Rule>) -> Arg {
940    let mut inner = pair.into_inner();
941    let name_pair = inner
942        .next()
943        .expect("parser: named_arg should have arg name");
944    let name = match name_pair.as_rule() {
945        Rule::arg_name => {
946            let inner_name = name_pair
947                .into_inner()
948                .next()
949                .expect("parser: arg_name should have identifier, string, or dots");
950            match inner_name.as_rule() {
951                Rule::dots => "...".to_string(),
952                Rule::dotdot => inner_name.as_str().to_string(),
953                Rule::string => super::literals::parse_string_value(inner_name),
954                _ => parse_ident_str(inner_name),
955            }
956        }
957        _ => parse_ident_str(name_pair),
958    };
959    // Skip named_eq token
960    let value = inner.find(|p| p.as_rule() == Rule::expr).map(build_expr);
961    Arg {
962        name: Some(name),
963        value,
964    }
965}
966
967// endregion