Skip to main content

praxis_input_parser/
call.rs

1//! Constructor calls: one argument list, one shape check, one builder (§7.5).
2//!
3//! Two callers construct a `sep(",", int)`: the HIR bridge, walking the rowan
4//! tree of `read sep(",", int)`, and [`crate::body`], parsing the text of a
5//! capture body `{xs:sep(",", int)}`. They meet here: both produce a
6//! [`CallArg`] list, and [`build_call`] is the only thing that turns one into a
7//! [`ParserAst`]. Neither the shape rules nor the builders can drift from each
8//! other, because there is one of each.
9
10use crate::ast::{
11    BlockItem, Constructor, InvalidRepeatCount, ParserAst, RepeatCount, SectionItem, Separator,
12    SkipPolicy,
13};
14use crate::validate::{ArgKind, ValidationError, check_call};
15use praxis_source::{DiagCode, Span};
16
17/// One argument of a constructor call, as written (§7.5).
18#[derive(Clone, Debug)]
19pub enum CallArg {
20    /// A positional parser expression.
21    Parser(ParserAst),
22    /// A positional string literal (`sep`'s separator, `one_of`'s set),
23    /// **already decoded** by `praxis_syntax::literal::unquote_text`.
24    String(String),
25    /// A positional whole-number literal — the `N` of `repeated(P, N)`,
26    /// **already decoded** by `praxis_syntax::numeric::parse_int_literal`.
27    /// Still an `i64` here: whether a number is a usable count is
28    /// [`RepeatCount`]'s question, and it is asked where the span is.
29    Int(i64),
30    /// A bare keyword flag: the `ragged` of `grid(P, ragged, fill: 0)`.
31    Flag(String),
32    /// A named argument `name: parser_expr`.
33    Named { name: String, parser: ParserAst },
34    /// A named argument whose value is a keyword rather than a parser:
35    /// `skip: whitespace`, `fill: 0`.
36    Keyword { name: String, value: String },
37    /// A `name: repeated(...)` argument of a named `sections`. `count` is
38    /// `Some` for the bounded `repeated(P, N)` and `None` for the greedy
39    /// `repeated(P)` — the distinction the position rule below is entirely
40    /// about.
41    RepeatedTail {
42        name: String,
43        parser: ParserAst,
44        count: Option<RepeatCount>,
45    },
46}
47
48impl CallArg {
49    /// This argument's shape, with the payload dropped — what [`check_call`]
50    /// reads.
51    #[must_use]
52    pub fn kind(&self) -> ArgKind {
53        match self {
54            CallArg::Parser(_) => ArgKind::Parser,
55            CallArg::String(_) => ArgKind::String,
56            CallArg::Int(_) => ArgKind::Int,
57            CallArg::Flag(f) => ArgKind::Flag(f.clone()),
58            CallArg::Named { name, .. } => ArgKind::Named(name.clone()),
59            // **Not `Named`.** A `skip:`/`fill:` keyword and a `name: parser`
60            // argument are different shapes. Collapsing them onto one `ArgKind`
61            // would leave `check_call` unable to tell them apart, so `block`,
62            // `choice` and named `sections` would accept a keyword as a
63            // well-shaped named argument their builders have nowhere to put.
64            CallArg::Keyword { name, .. } => ArgKind::Keyword(name.clone()),
65            CallArg::RepeatedTail { name, .. } => ArgKind::RepeatedTail(name.clone()),
66        }
67    }
68}
69
70/// Build the [`ParserAst`] for `ctor(args…)`, or report every reason it cannot
71/// be built.
72///
73/// **Nothing is built before the shape is checked** (IP-07): the argument list
74/// goes through [`check_call`] first, so by the time an arm below runs it has
75/// exactly the arguments §7.5 gives that constructor and there is nothing left
76/// for it to drop.
77///
78/// # Errors
79/// A non-empty [`ValidationError`] list. Each carries the [`DiagCode`] the
80/// caller reports it under.
81pub fn build_call(
82    ctor: Constructor,
83    args: Vec<CallArg>,
84    span: Span,
85) -> Result<ParserAst, Vec<ValidationError>> {
86    if ctor == Constructor::Repeated {
87        // `repeated(...)` is not a parser in its own right — it is the marker
88        // on a named argument of a `sections` call, saying that the field takes
89        // a *group* of sections rather than one. Anywhere else there is nothing
90        // for it to repeat over, so it is reported rather than dropped (IP-09).
91        return Err(vec![ValidationError {
92            span,
93            code: DiagCode::MisplacedRepeatedTail,
94            message: "`repeated(...)` is only a named argument of a `sections` call".to_string(),
95        }]);
96    }
97
98    let kinds: Vec<ArgKind> = args.iter().map(CallArg::kind).collect();
99    let shape_errors = check_call(ctor, &kinds, span);
100    if !shape_errors.is_empty() {
101        return Err(shape_errors);
102    }
103
104    let internal = |what: &str| {
105        vec![ValidationError {
106            span,
107            code: DiagCode::InvalidConstructorArgument,
108            message: format!("`{}` {what}", ctor.keyword()),
109        }]
110    };
111    // **A `_ => {}` arm in a builder is how an argument disappears** (IP-07).
112    // Every arm below is exhaustive over what its shape admits, and an argument
113    // it does not know is *reported* rather than dropped. It should be
114    // unreachable — `check_call` has already run — and the point is precisely
115    // that if it ever is reachable, it is visible.
116    let unexpected = |arg: &CallArg| {
117        vec![ValidationError {
118            span,
119            code: DiagCode::InvalidConstructorArgument,
120            message: format!(
121                "`{}` does not take {}",
122                ctor.keyword(),
123                arg.kind().describe()
124            ),
125        }]
126    };
127
128    match ctor {
129        Constructor::Lines
130        | Constructor::Csv
131        | Constructor::Ws
132        | Constructor::Matrix
133        | Constructor::Optional
134        | Constructor::Scan => {
135            let child = Box::new(sole_parser(args).ok_or_else(|| internal("needs one parser"))?);
136            Ok(match ctor {
137                Constructor::Lines => ParserAst::Lines { child, span },
138                Constructor::Csv => ParserAst::Csv { child, span },
139                Constructor::Ws => ParserAst::Ws { child, span },
140                Constructor::Matrix => ParserAst::Matrix { child, span },
141                Constructor::Optional => ParserAst::Optional { child, span },
142                _ => ParserAst::Scan { child, span },
143            })
144        }
145        Constructor::Sections => {
146            // One name, two shapes: `sections(P)` is homogeneous,
147            // `sections(name: P, …)` is the heterogeneous form.
148            if kinds
149                .iter()
150                .any(|k| matches!(k, ArgKind::Named(_) | ArgKind::RepeatedTail(_)))
151            {
152                build_sections_named(args, span)
153            } else {
154                Ok(ParserAst::Sections {
155                    child: Box::new(sole_parser(args).ok_or_else(|| internal("needs one parser"))?),
156                    span,
157                })
158            }
159        }
160        Constructor::Sep => {
161            let mut separator = None;
162            let mut child = None;
163            for arg in args {
164                match arg {
165                    CallArg::String(s) => separator = Some(s),
166                    CallArg::Parser(p) => child = Some(p),
167                    other => return Err(unexpected(&other)),
168                }
169            }
170            // An empty separator is the one separator that can never advance a
171            // cursor (IP-10), so `Separator::new` refuses it rather than
172            // letting a missing one default to `String::new()`.
173            let separator = Separator::new(separator.as_deref().unwrap_or("")).map_err(|_| {
174                vec![ValidationError {
175                    span,
176                    code: DiagCode::EmptySeparator,
177                    message: "`sep` needs a non-empty separator: an empty one never advances"
178                        .to_string(),
179                }]
180            })?;
181            Ok(ParserAst::Sep {
182                separator,
183                child: Box::new(child.ok_or_else(|| internal("needs an element parser"))?),
184                span,
185            })
186        }
187        Constructor::OneOf => {
188            let mut chars = None;
189            for arg in args {
190                match arg {
191                    CallArg::String(s) => chars = Some(s),
192                    other => return Err(unexpected(&other)),
193                }
194            }
195            Ok(ParserAst::OneOf {
196                chars: chars.ok_or_else(|| internal("needs a character set"))?,
197                span,
198            })
199        }
200        Constructor::Chars => {
201            let mut child = None;
202            let mut skip = SkipPolicy::Whitespace;
203            for arg in args {
204                match arg {
205                    CallArg::Parser(p) => child = Some(p),
206                    // The keyword's name comes from the constructor, not from a
207                    // literal here: `Constructor::keyword_arg` is the one place
208                    // that says `chars` takes `skip:`.
209                    CallArg::Keyword { name, value }
210                        if Some(name.as_str()) == ctor.keyword_arg() =>
211                    {
212                        // An unrecognized policy is reported rather than left at
213                        // the default, so `skip: wihtespace` cannot silently
214                        // run as `whitespace`.
215                        skip = SkipPolicy::from_keyword(&value).ok_or_else(|| {
216                            vec![ValidationError {
217                                span,
218                                code: DiagCode::InvalidConstructorArgument,
219                                // The three names alone are a trap: nothing in
220                                // them says `newlines` is the *broader* policy.
221                                // Each one states what it skips.
222                                message: format!(
223                                    "`skip: {value}` is not a skip policy — `none` (skips {}), \
224                                     `whitespace` (skips {}) or `newlines` (skips {})",
225                                    SkipPolicy::None.skips(),
226                                    SkipPolicy::Whitespace.skips(),
227                                    SkipPolicy::Newlines.skips(),
228                                ),
229                            }]
230                        })?;
231                    }
232                    other => return Err(unexpected(&other)),
233                }
234            }
235            Ok(ParserAst::Characters {
236                child: Box::new(child.ok_or_else(|| internal("needs a character parser"))?),
237                skip,
238                span,
239            })
240        }
241        Constructor::Grid => {
242            let mut child = None;
243            let mut fill = None;
244            for arg in args {
245                match arg {
246                    CallArg::Parser(p) => child = Some(p),
247                    // `fill:` is spelled by `Constructor::keyword_arg` and not
248                    // here, so the name this arm reads and the name the front
249                    // ends mint cannot drift apart.
250                    CallArg::Keyword { name, value }
251                        if Some(name.as_str()) == ctor.keyword_arg() =>
252                    {
253                        // The value arrives as raw source text, so the decode
254                        // lives here, once, and both front ends agree:
255                        // `fill: "-"` reaches the plan as `-` and not as
256                        // `"\"-\""`, quotes and all (IP-08's rule for every
257                        // other parser string literal).
258                        let decoded = praxis_syntax::literal::unquote_text(&value);
259                        // **A keyword argument's value is part of its shape**,
260                        // as `chars`'s `skip:` value is. An empty `fill:` is
261                        // the same unrepresentable value IP-10 refuses one
262                        // field over, where `Separator::new` rejects an empty
263                        // separator because it never advances: a cell of no
264                        // characters pads nothing.
265                        if decoded.is_empty() {
266                            return Err(vec![ValidationError {
267                                span,
268                                code: DiagCode::InvalidConstructorArgument,
269                                message: "`fill:` needs a value to pad a short row with — an \
270                                          empty one fills nothing"
271                                    .to_string(),
272                            }]);
273                        }
274                        fill = Some(decoded);
275                    }
276                    // `ragged` carries nothing: it exists so the shape table
277                    // can *require* it beside `fill:`. Matched here rather than
278                    // swept up by a wildcard, so it is a decision and not a
279                    // leak — and matched against `Constructor::flag_arg`, so
280                    // the flag is `grid`'s and not a bare word this arm agrees
281                    // with by coincidence.
282                    CallArg::Flag(f) if Some(f.as_str()) == ctor.flag_arg() => {}
283                    other => return Err(unexpected(&other)),
284                }
285            }
286            let child = Box::new(child.ok_or_else(|| internal("needs a cell parser"))?);
287            Ok(match fill {
288                Some(fill) => ParserAst::GridRagged { child, fill, span },
289                None => ParserAst::Grid { child, span },
290            })
291        }
292        Constructor::Block => {
293            let mut items = Vec::with_capacity(args.len());
294            for arg in args {
295                match arg {
296                    CallArg::Parser(p) => items.push(BlockItem::Positional(p)),
297                    CallArg::Named { name, parser } => {
298                        items.push(BlockItem::Named { name, parser })
299                    }
300                    other => return Err(unexpected(&other)),
301                }
302            }
303            Ok(ParserAst::Block { items, span })
304        }
305        Constructor::Choice => {
306            let mut cases = Vec::with_capacity(args.len());
307            for arg in args {
308                match arg {
309                    CallArg::Named { name, parser } => cases.push((name, parser)),
310                    other => return Err(unexpected(&other)),
311                }
312            }
313            Ok(ParserAst::Choice { cases, span })
314        }
315        // Refused at the top, before the shape check.
316        Constructor::Repeated => Err(internal("is not a parser")),
317    }
318}
319
320/// Build the `name: repeated(P)` / `name: repeated(P, N)` marker of a named
321/// `sections` (§7.5).
322///
323/// `repeated(...)` is not a parser in its own right — it is the marker on a
324/// named argument of a `sections` call, and the field's parser is the `P`. §7.5
325/// gives the marker a parser and an optional count. That is the whole rule, and
326/// it lives here for the same reason [`build_call`] does: **both front ends
327/// must apply it**.
328///
329/// The shape check is [`check_call`]'s, like every other constructor's, so
330/// ADR-073's "nothing is built before the shape is checked" covers the marker
331/// too and the two front ends cannot disagree about it.
332///
333/// # Errors
334/// A non-empty [`ValidationError`] list: `ConstructorArity` (I022) for a wrong
335/// count of arguments, `InvalidConstructorArgument` (I014) for a wrong kind or
336/// an unusable count.
337pub fn build_repeated_tail(
338    name: String,
339    args: Vec<CallArg>,
340    span: Span,
341) -> Result<CallArg, Vec<ValidationError>> {
342    let kinds: Vec<ArgKind> = args.iter().map(CallArg::kind).collect();
343    let shape_errors = check_call(Constructor::Repeated, &kinds, span);
344    if !shape_errors.is_empty() {
345        return Err(shape_errors);
346    }
347
348    let bad = |message: String| {
349        vec![ValidationError {
350            span,
351            code: DiagCode::InvalidConstructorArgument,
352            message,
353        }]
354    };
355
356    let mut args = args.into_iter();
357    let parser = match args.next() {
358        Some(CallArg::Parser(parser)) => parser,
359        Some(other) => {
360            return Err(bad(format!(
361                "`repeated`'s first argument must be a parser, but it is {}",
362                other.kind().describe()
363            )));
364        }
365        // `check_call` has already reported the empty list.
366        None => return Err(bad("`repeated` needs a parser".to_string())),
367    };
368    let count = match args.next() {
369        None => None,
370        Some(CallArg::Int(n)) => Some(RepeatCount::new(n).map_err(|why| {
371            bad(match why {
372                InvalidRepeatCount::NotPositive => "`repeated`'s count must be at least 1 — a \
373                                                   group of no sections parses nothing"
374                    .to_string(),
375                InvalidRepeatCount::TooLarge => {
376                    "`repeated`'s count must fit in 32 bits".to_string()
377                }
378            })
379        })?),
380        Some(other) => {
381            return Err(bad(format!(
382                "`repeated`'s count must be a whole-number literal, but it is {} — the parser \
383                 plan is built when the program is compiled, so the count cannot be a parser or \
384                 a variable",
385                other.kind().describe()
386            )));
387        }
388    };
389    Ok(CallArg::RepeatedTail {
390        name,
391        parser,
392        count,
393    })
394}
395
396/// The single positional parser of a call `check_call` has already accepted.
397fn sole_parser(args: Vec<CallArg>) -> Option<ParserAst> {
398    match args.into_iter().next() {
399        Some(CallArg::Parser(p)) => Some(p),
400        _ => None,
401    }
402}
403
404/// Build a heterogeneous `sections(name: P, …, tail: repeated(P))` (§7.5).
405///
406/// §7.5: "`repeated(parser)` may appear only as the final named argument."
407/// Both halves are checked here (IP-09): at most one unbounded tail, and it
408/// last. `args` is in source order, which is what makes "final" a checkable
409/// claim.
410///
411/// **The position rule is the unbounded form's alone.** `repeated(P)` consumes
412/// every section that is left, so a field after it could never match — that is
413/// what "final" is an argument *from*, and it is no argument at all about
414/// `repeated(P, N)`, which consumes exactly `N` and leaves the rest. A counted
415/// group is an ordinary named argument in every respect, including being
416/// allowed to be the last one.
417fn build_sections_named(args: Vec<CallArg>, span: Span) -> Result<ParserAst, Vec<ValidationError>> {
418    let unbounded: Vec<usize> = args
419        .iter()
420        .enumerate()
421        .filter(|(_, a)| matches!(a, CallArg::RepeatedTail { count: None, .. }))
422        .map(|(i, _)| i)
423        .collect();
424    if unbounded.len() > 1 {
425        return Err(vec![ValidationError {
426            span,
427            code: DiagCode::MisplacedRepeatedTail,
428            message: "`sections` takes at most one unbounded `repeated(...)` tail".to_string(),
429        }]);
430    }
431    if let Some(&at) = unbounded.first()
432        && at != args.len() - 1
433    {
434        return Err(vec![ValidationError {
435            span,
436            code: DiagCode::MisplacedRepeatedTail,
437            message: "an unbounded `repeated(...)` tail may appear only as the final named \
438                          argument: it consumes every remaining section, so nothing can \
439                          follow it — write `repeated(P, N)` for a group of N sections, which can"
440                .to_string(),
441        }]);
442    }
443
444    let mut fields: Vec<SectionItem> = Vec::new();
445    let mut repeated_tail: Option<(String, Box<ParserAst>)> = None;
446    for arg in args {
447        match arg {
448            CallArg::Named { name, parser } => fields.push(SectionItem::One { name, parser }),
449            CallArg::RepeatedTail {
450                name,
451                parser,
452                count: Some(count),
453            } => fields.push(SectionItem::Counted {
454                name,
455                count,
456                parser,
457            }),
458            CallArg::RepeatedTail {
459                name,
460                parser,
461                count: None,
462            } => {
463                repeated_tail = Some((name, Box::new(parser)));
464            }
465            // `check_call` has already refused a positional, a string or a
466            // keyword here — and this reports rather than drops, because a
467            // `_ => {}` is how a field vanishes from a record in silence.
468            other => {
469                return Err(vec![ValidationError {
470                    span,
471                    code: DiagCode::InvalidConstructorArgument,
472                    message: format!("`sections` does not take {}", other.kind().describe()),
473                }]);
474            }
475        }
476    }
477    Ok(ParserAst::SectionsNamed {
478        fields,
479        repeated_tail,
480        span,
481    })
482}