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}