narju/parse/mod.rs
1//! S-expression reader and name-resolution lowering to [`Exp`].
2//!
3//! Source text passes through up to three stages here:
4//!
5//! 1. **Reading** ([`parse_one`] / [`parse_all`]): text to [`Sexp`]
6//! trees. Comments (`;` to end of line) are dropped; the
7//! quote-family reader macros (`'`, `` ` ``, `,`, `,@`) become
8//! `quote` / `quasiquote` / `unquote` list heads.
9//! 2. **Surface desugaring**, applied only at source boundaries:
10//! [`sug`] rewrites the `cadr` family into car/cdr chains
11//! everywhere, including inside quoted data (Pink source is quoted
12//! data); [`expand_qq`] turns quasiquote in *loaded files* into
13//! plain cons/quote source.
14//! 3. **Lowering** ([`lower`]): [`Sexp`] to [`Exp`], resolving each
15//! name to an environment index against a compile-time name stack.
16//! Special forms are matched by head symbol and shape here; a list
17//! that matches nothing becomes a (curried) application.
18//!
19//! Values entered at a REPL prompt follow 1–3; data read by the `read`
20//! primitive stops after 1 and is converted directly to a value with
21//! [`sexp_to_val`].
22
23use smol_str::SmolStr;
24
25use crate::core::{rc_exp, Exp};
26
27// ── S-expression type ─────────────────────────────────────────────────────────
28
29/// A read s-expression — the concrete syntax tree, before lowering.
30///
31/// Unlike [`Exp`] and [`crate::core::Val`], lists here are flat
32/// `Vec<Sexp>`, which is what the shape-matching in lowering wants.
33#[derive(Debug, Clone, PartialEq)]
34pub enum Sexp {
35 /// Integer atom: a maximal symbol-char run matching `-?[0-9]+`.
36 Num(i64),
37 /// Symbol atom.
38 /// SmolStr: inline storage for strings ≤22 bytes (all common symbol names).
39 Sym(SmolStr),
40 /// Parenthesized list.
41 List(Vec<Sexp>),
42 /// The atom `nil`, classified at read time.
43 Nil,
44}
45
46// ── Parser ────────────────────────────────────────────────────────────────────
47//
48// Hand-written explicit-stack parser: paren nesting lives in a heap `Vec`
49// of frames, never on the native call stack. The previous chumsky
50// combinator recursed one (large, debug-mode) frame cycle per nesting
51// level, which overflowed the 2 MiB default test-thread stack once
52// lib/pink-forms.naj pushed source depth past ~70 parens.
53//
54// Grammar (unchanged): atoms are maximal runs of symbol chars, classified
55// as Num iff `-?[0-9]+` (else Sym; `nil` → Nil); `'`/`` ` `` → quote /
56// quasiquote; `,` and `,@` both → unquote; `;` comments to end of line.
57
58/// Character set for atoms: alphanumerics plus Scheme-ish punctuation.
59fn is_sym_char(c: char) -> bool {
60 c.is_alphanumeric()
61 || matches!(
62 c,
63 '+' | '-' | '*' | '/' | '?' | '!' | '<' | '>' | '=' | '_' | '#' | '.'
64 )
65}
66
67/// Classify a completed atom token: `nil`, then number, else symbol.
68/// A numeric-looking token that overflows `i64` falls back to symbol.
69fn classify_atom(tok: &str) -> Sexp {
70 if tok == "nil" {
71 return Sexp::Nil;
72 }
73 let digits = tok.strip_prefix('-').unwrap_or(tok);
74 if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) {
75 if let Ok(n) = tok.parse::<i64>() {
76 return Sexp::Num(n);
77 }
78 }
79 Sexp::Sym(SmolStr::new(tok))
80}
81
82/// A finished value gets wrapped in any pending quote-family prefixes
83/// (innermost = most recently seen, so pop order is correct), then lands
84/// in the enclosing list frame or the top-level output.
85fn finish(
86 mut v: Sexp,
87 pending: &mut Vec<&'static str>,
88 stack: &mut Vec<(Vec<Sexp>, Vec<&'static str>)>,
89 out: &mut Vec<Sexp>,
90) {
91 while let Some(tag) = pending.pop() {
92 v = Sexp::List(vec![Sexp::Sym(tag.into()), v]);
93 }
94 match stack.last_mut() {
95 Some((items, _)) => items.push(v),
96 None => out.push(v),
97 }
98}
99
100/// Parse top-level forms iteratively. If `stop_after_one`, return as soon
101/// as the first complete form closes (trailing input ignored — matching
102/// the old `parse_one`, which ran the combinator without `end()`).
103fn parse_forms(src: &str, stop_after_one: bool) -> Result<Vec<Sexp>, String> {
104 let mut out: Vec<Sexp> = Vec::new();
105 // Each frame: (items so far, quote-prefixes that wrap the list on close).
106 let mut stack: Vec<(Vec<Sexp>, Vec<&'static str>)> = Vec::new();
107 // Quote-family prefixes awaiting the next value at the current point.
108 let mut pending: Vec<&'static str> = Vec::new();
109 let mut i = 0;
110 while i < src.len() {
111 let c = src[i..].chars().next().expect("i is on a char boundary");
112 if c.is_whitespace() {
113 i += c.len_utf8();
114 continue;
115 }
116 match c {
117 ';' => {
118 i += src[i..].find('\n').map_or(src.len() - i, |n| n + 1);
119 }
120 '(' => {
121 stack.push((Vec::new(), std::mem::take(&mut pending)));
122 i += 1;
123 }
124 ')' => {
125 if let Some(tag) = pending.last() {
126 return Err(format!("expected expression after {tag}, found ')'"));
127 }
128 let (items, wrappers) = stack
129 .pop()
130 .ok_or_else(|| "unexpected ')'".to_string())?;
131 pending = wrappers;
132 finish(Sexp::List(items), &mut pending, &mut stack, &mut out);
133 i += 1;
134 if stop_after_one && stack.is_empty() {
135 return Ok(out);
136 }
137 }
138 '\'' => {
139 pending.push("quote");
140 i += 1;
141 }
142 '`' => {
143 pending.push("quasiquote");
144 i += 1;
145 }
146 ',' => {
147 i += 1;
148 if src[i..].starts_with('@') {
149 i += 1;
150 }
151 pending.push("unquote");
152 }
153 c if is_sym_char(c) => {
154 let start = i;
155 while let Some(ch) = src[i..].chars().next() {
156 if !is_sym_char(ch) {
157 break;
158 }
159 i += ch.len_utf8();
160 }
161 finish(classify_atom(&src[start..i]), &mut pending, &mut stack, &mut out);
162 if stop_after_one && stack.is_empty() {
163 return Ok(out);
164 }
165 }
166 other => return Err(format!("unexpected character '{other}'")),
167 }
168 }
169 if !stack.is_empty() {
170 return Err(format!(
171 "unexpected end of input: {} unclosed '('",
172 stack.len()
173 ));
174 }
175 if let Some(tag) = pending.last() {
176 return Err(format!("unexpected end of input after {tag}"));
177 }
178 Ok(out)
179}
180
181// ── Surface desugaring ────────────────────────────────────────────────────────
182
183/// pink.scm's `sug` pre-pass (pink.scm:11): desugar `cadr`/`caddr`/`cadddr`
184/// into car/cdr chains over the whole surface tree — **including quoted
185/// data**, since Pink source is itself a quoted list. Oracle under Chez:
186/// `(sug '(quote (cadr x)))` => `(quote (car (cdr x)))`.
187///
188/// Applied exactly once, at the surface-source boundary
189/// (`TopLevel::eval_exp`) — not inside `lower`'s recursion, and not on the
190/// `read` primitive's runtime data (the reference sugs source only).
191///
192/// ```rust
193/// use narju::parse::{parse_one, sug};
194///
195/// let form = parse_one("(cadr xs)").unwrap();
196/// assert_eq!(sug(&form), parse_one("(car (cdr xs))").unwrap());
197/// ```
198pub fn sug(sexp: &Sexp) -> Sexp {
199 if let Sexp::List(items) = sexp {
200 if let [Sexp::Sym(op), arg] = items.as_slice() {
201 let cdrs = match op.as_str() {
202 "cadr" => Some(1),
203 "caddr" => Some(2),
204 "cadddr" => Some(3),
205 _ => None,
206 };
207 if let Some(cdrs) = cdrs {
208 let mut acc = sug(arg);
209 for _ in 0..cdrs {
210 acc = Sexp::List(vec![Sexp::Sym("cdr".into()), acc]);
211 }
212 return Sexp::List(vec![Sexp::Sym("car".into()), acc]);
213 }
214 }
215 return Sexp::List(items.iter().map(sug).collect());
216 }
217 sexp.clone()
218}
219
220// ── Name resolution: Sexp -> Exp ─────────────────────────────────────────────
221
222/// Lower a read s-expression to an [`Exp`], resolving names against
223/// `names` — the compile-time stack of binders in scope, which mirrors
224/// the runtime environment slot-for-slot. Innermost binding wins
225/// (`rposition`), so shadowing works; an unresolved name is an error at
226/// lowering time, not runtime.
227///
228/// `names` is borrowed mutably because binders push onto it during
229/// their body's lowering; it is restored before returning. Callers
230/// evaluating closed top-level forms pass a fresh `Vec`.
231pub fn lower(sexp: &Sexp, names: &mut Vec<SmolStr>) -> Result<Exp, String> {
232 match sexp {
233 Sexp::Num(n) => Ok(Exp::Lit(*n)),
234 Sexp::Nil => Ok(Exp::Nil),
235
236 Sexp::Sym(s) => {
237 if s == "#t" { return Ok(Exp::Lit(1)); }
238 if s == "#f" { return Ok(Exp::Lit(0)); }
239 names
240 .iter()
241 .rposition(|n| n == s)
242 .map(Exp::Var)
243 .ok_or_else(|| format!("unbound variable: {s}"))
244 }
245
246 Sexp::List(items) => lower_list(items, names).map_err(|e| {
247 if e.starts_with("unbound") {
248 let head = items.first().map(|s| format!("{s:?}")).unwrap_or_default();
249 format!("{e} [in list starting with {head}]")
250 } else {
251 e
252 }
253 }),
254 }
255}
256
257/// Lower a list form: special forms first (matched by head symbol and
258/// arity), then binary application, then the n-ary case, which curries
259/// left-to-right — `(f a b)` is `((f a) b)`.
260///
261/// A special-form head with the wrong arity falls through to the
262/// application arms, matching pink.scm's behavior — e.g. `(car)` is an
263/// application of the variable `car`, and errors as unbound. `let` is
264/// the one exception with an explicit arity error, since a malformed
265/// `let` otherwise fails with a confusing unbound-variable message.
266fn lower_list(items: &[Sexp], names: &mut Vec<SmolStr>) -> Result<Exp, String> {
267 match items {
268 [] => Err("empty list expression".into()),
269
270 [Sexp::Sym(q), rest @ ..] if q == "quote" => {
271 if rest.len() != 1 {
272 return Err("quote takes exactly one argument".into());
273 }
274 lower_quote(&rest[0])
275 }
276
277 [Sexp::Sym(q), rest @ ..] if q == "quasiquote" => {
278 if rest.len() != 1 {
279 return Err("quasiquote takes exactly one argument".into());
280 }
281 lower_quasiquote(&rest[0], names)
282 }
283
284 [Sexp::Sym(kw), Sexp::Sym(f), Sexp::Sym(x), body] if kw == "lambda" => {
285 let base = names.len();
286 names.push(f.clone());
287 names.push(x.clone());
288 // Truncate before `?`: an error mid-body must not leave the
289 // binders on the caller's name stack.
290 let body_exp = lower(body, names);
291 names.truncate(base);
292 Ok(Exp::Lam(rc_exp(body_exp?)))
293 }
294
295 [Sexp::Sym(kw), Sexp::Sym(x), e, body] if kw == "let" => {
296 let e_exp = lower(e, names)?;
297 let base = names.len();
298 names.push(x.clone());
299 let body_exp = lower(body, names);
300 names.truncate(base);
301 Ok(Exp::Let(rc_exp(e_exp), rc_exp(body_exp?)))
302 }
303
304 [Sexp::Sym(kw), rest @ ..] if kw == "let" => Err(format!(
305 "let requires (let name init body), got {} args after let",
306 rest.len()
307 )),
308
309 [Sexp::Sym(kw), c, t, e] if kw == "if" => Ok(Exp::If(
310 rc_exp(lower(c, names)?),
311 rc_exp(lower(t, names)?),
312 rc_exp(lower(e, names)?),
313 )),
314
315 [Sexp::Sym(op), a] if op == "number?" => Ok(Exp::IsNum(rc_exp(lower(a, names)?))),
316 [Sexp::Sym(op), a] if op == "symbol?" => Ok(Exp::IsSym(rc_exp(lower(a, names)?))),
317 [Sexp::Sym(op), a] if op == "null?" => {
318 Ok(Exp::IsNil(rc_exp(lower(a, names)?)))
319 }
320 [Sexp::Sym(op), a] if op == "pair?" => Ok(Exp::IsPair(rc_exp(lower(a, names)?))),
321 [Sexp::Sym(op), a, b] if op == "code?" => Ok(Exp::IsCode(
322 rc_exp(lower(a, names)?),
323 rc_exp(lower(b, names)?),
324 )),
325 [Sexp::Sym(op), a] if op == "car" => Ok(Exp::Car(rc_exp(lower(a, names)?))),
326 [Sexp::Sym(op), a] if op == "cdr" => Ok(Exp::Cdr(rc_exp(lower(a, names)?))),
327
328 [Sexp::Sym(op), a, b] if op == "cons" => Ok(Exp::Cons(
329 rc_exp(lower(a, names)?),
330 rc_exp(lower(b, names)?),
331 )),
332 [Sexp::Sym(op), a, b] if op == "+" => Ok(Exp::Plus(
333 rc_exp(lower(a, names)?),
334 rc_exp(lower(b, names)?),
335 )),
336 [Sexp::Sym(op), a, b] if op == "-" => Ok(Exp::Minus(
337 rc_exp(lower(a, names)?),
338 rc_exp(lower(b, names)?),
339 )),
340 [Sexp::Sym(op), a, b] if op == "*" => Ok(Exp::Times(
341 rc_exp(lower(a, names)?),
342 rc_exp(lower(b, names)?),
343 )),
344 [Sexp::Sym(op), a, b] if op == "eq?" => Ok(Exp::Eq(
345 rc_exp(lower(a, names)?),
346 rc_exp(lower(b, names)?),
347 )),
348
349 [Sexp::Sym(kw), e] if kw == "lift" => Ok(Exp::Lift(rc_exp(lower(e, names)?))),
350 [Sexp::Sym(kw), b, e] if kw == "run" => Ok(Exp::Run(
351 rc_exp(lower(b, names)?),
352 rc_exp(lower(e, names)?),
353 )),
354 [Sexp::Sym(kw), e1, e2] if kw == "lift-ref" => Ok(Exp::LiftRef(
355 rc_exp(lower(e1, names)?),
356 rc_exp(lower(e2, names)?),
357 )),
358 [Sexp::Sym(kw), b, v] if kw == "log" => Ok(Exp::Log(
359 rc_exp(lower(b, names)?),
360 rc_exp(lower(v, names)?),
361 )),
362 // catch is the single restricted floor primitive for error handling.
363 // It is written for the boot script (lib/purple.naj) — not intended
364 // for user code. It IS parseable here so the boot script can be
365 // loaded, but user programs should use the REPL's error handling via
366 // define/try at the Pink level rather than calling catch directly.
367 [Sexp::Sym(kw), e] if kw == "catch" => Ok(Exp::Catch(rc_exp(lower(e, names)?))),
368 [Sexp::Sym(kw), e] if kw == "throw" => Ok(Exp::Throw(rc_exp(lower(e, names)?))),
369 [Sexp::Sym(kw), v] if kw == "cell-new" => {
370 Ok(Exp::CellNew(rc_exp(lower(v, names)?)))
371 }
372 [Sexp::Sym(kw), c] if kw == "cell-read" => {
373 Ok(Exp::CellRead(rc_exp(lower(c, names)?)))
374 }
375 [Sexp::Sym(kw), c, v] if kw == "cell-set!" => Ok(Exp::CellSet(
376 rc_exp(lower(c, names)?),
377 rc_exp(lower(v, names)?),
378 )),
379 [Sexp::Sym(kw), p] if kw == "read" => Ok(Exp::Read(rc_exp(lower(p, names)?))),
380 [Sexp::Sym(kw), p] if kw == "read-file" => {
381 Ok(Exp::ReadFile(rc_exp(lower(p, names)?)))
382 }
383 [Sexp::Sym(kw), e] if kw == "print" => Ok(Exp::Print(rc_exp(lower(e, names)?))),
384 [Sexp::Sym(kw), el, ex] if kw == "evalms" => Ok(Exp::Evalms(
385 rc_exp(lower(el, names)?),
386 rc_exp(lower(ex, names)?),
387 )),
388 [Sexp::Sym(kw), e, env_list] if kw == "trans" => Ok(Exp::Trans(
389 rc_exp(lower(e, names)?),
390 rc_exp(lower(env_list, names)?),
391 )),
392
393 [f, arg] => Ok(Exp::App(
394 rc_exp(lower(f, names)?),
395 rc_exp(lower(arg, names)?),
396 )),
397
398 [f, args @ ..] => {
399 let mut acc = lower(f, names)?;
400 for arg in args {
401 acc = Exp::App(rc_exp(acc), rc_exp(lower(arg, names)?));
402 }
403 Ok(acc)
404 }
405 }
406}
407
408// ── Quasiquote expansion ──────────────────────────────────────────────────────
409
410/// Lower a quasiquoted datum: atoms become literals, `(unquote e)`
411/// splices `e` as a host expression, lists become cons-chains.
412fn lower_quasiquote(sexp: &Sexp, names: &mut Vec<SmolStr>) -> Result<Exp, String> {
413 match sexp {
414 Sexp::Num(n) => Ok(Exp::Lit(*n)),
415 Sexp::Nil => Ok(Exp::Nil),
416 Sexp::Sym(s) => Ok(Exp::Sym(s.clone())),
417
418 Sexp::List(items) => {
419 if let [Sexp::Sym(tag), inner] = items.as_slice() {
420 if tag == "unquote" {
421 return lower(inner, names);
422 }
423 }
424 lower_qq_list(items, names)
425 }
426 }
427}
428
429/// Build the cons-chain for a quasiquoted list by folding right-to-left.
430///
431/// Iterative on the list axis so deep quasiquoted lists (e.g. the spliced
432/// `pink-tie-src` in `lib/purple.naj`) can't overflow the native stack.
433/// Per-item dispatch into `lower` / `lower_quasiquote` is still recursive
434/// on Sexp *tree* depth, but real inputs are wide-and-shallow there so
435/// that recursion is not at risk in practice.
436fn lower_qq_list(items: &[Sexp], names: &mut Vec<SmolStr>) -> Result<Exp, String> {
437 let mut acc = Exp::Nil;
438 for item in items.iter().rev() {
439 // An `,unquote` head splices its argument as a host expression.
440 // Anything else recurses as a quasiquoted datum.
441 let head_exp = match item {
442 Sexp::List(inner) => match inner.as_slice() {
443 [Sexp::Sym(tag), splice_expr] if tag == "unquote" => {
444 lower(splice_expr, names)?
445 }
446 _ => lower_quasiquote(item, names)?,
447 },
448 _ => lower_quasiquote(item, names)?,
449 };
450 acc = Exp::Cons(rc_exp(head_exp), rc_exp(acc));
451 }
452 Ok(acc)
453}
454
455// ── Read-time quasiquote expansion (read-file only) ──────────────────────────
456
457/// Expand quasiquote in loaded `.naj` source into plain cons/quote *source*.
458///
459/// Like `sug`, this is a read-time source transform, applied by the
460/// `read-file` primitive only — the `read` primitive stays un-transformed.
461/// The reference gets quasiquote from Scheme (mk.scm is a Scheme-level
462/// template over Pink source); narju's floor is the Scheme analogue, so
463/// loaded libraries get the same affordance without touching pink-poly.
464///
465/// `(quote x)` is opaque outside quasiquote. Inside a template, a quoted
466/// form is ordinary list data — `'b` in a template builds the datum
467/// `(quote b)`, matching Scheme, where unquotes under it still splice.
468pub fn expand_qq(sexp: &Sexp) -> Result<Sexp, String> {
469 match sexp {
470 Sexp::List(items) => {
471 if let [Sexp::Sym(tag), inner] = items.as_slice() {
472 match tag.as_str() {
473 "quote" => return Ok(sexp.clone()),
474 "quasiquote" => return qq_datum(inner),
475 _ => {}
476 }
477 }
478 let expanded = items.iter().map(expand_qq).collect::<Result<Vec<_>, _>>()?;
479 Ok(Sexp::List(expanded))
480 }
481 _ => Ok(sexp.clone()),
482 }
483}
484
485/// Turn a quasiquoted datum into source that reconstructs it: atoms become
486/// `(quote atom)`, `(unquote e)` splices `e` (itself qq-expanded), lists
487/// become right-folded `(cons …)` chains ending in `(quote ())`.
488fn qq_datum(sexp: &Sexp) -> Result<Sexp, String> {
489 match sexp {
490 Sexp::Num(_) => Ok(sexp.clone()),
491 Sexp::Sym(_) | Sexp::Nil => Ok(quote_datum(sexp)),
492 Sexp::List(items) => {
493 if let [Sexp::Sym(tag), inner] = items.as_slice() {
494 match tag.as_str() {
495 "unquote" => return expand_qq(inner),
496 "quasiquote" => {
497 return Err("nested quasiquote unsupported in loaded source".into())
498 }
499 _ => {}
500 }
501 }
502 let mut acc = quote_datum(&Sexp::Nil);
503 for item in items.iter().rev() {
504 let head = qq_datum(item)?;
505 acc = Sexp::List(vec![Sexp::Sym("cons".into()), head, acc]);
506 }
507 Ok(acc)
508 }
509 }
510}
511
512/// Wrap a datum as `(quote datum)` source.
513fn quote_datum(sexp: &Sexp) -> Sexp {
514 Sexp::List(vec![Sexp::Sym("quote".into()), sexp.clone()])
515}
516
517// ── Quoted datum lowering ─────────────────────────────────────────────────────
518
519/// Lower a quoted datum: atoms become literal expressions, lists
520/// become cons-chains of quoted elements. No names are resolved —
521/// everything under `quote` is data.
522fn lower_quote(sexp: &Sexp) -> Result<Exp, String> {
523 match sexp {
524 Sexp::Sym(s) => Ok(Exp::Sym(s.clone())),
525 Sexp::Num(n) => Ok(Exp::Lit(*n)),
526 Sexp::Nil => Ok(Exp::Nil),
527 Sexp::List(items) => lower_quoted_list(items),
528 }
529}
530
531/// Build the cons-chain for a quoted list by folding right-to-left.
532///
533/// Same iteration shape as `lower_qq_list`, minus the unquote dispatch
534/// — quoted contexts treat everything as data. Per-item recursion into
535/// `lower_quote` is still tree-depth-bounded, which is fine for realistic
536/// inputs.
537fn lower_quoted_list(items: &[Sexp]) -> Result<Exp, String> {
538 let mut acc = Exp::Nil;
539 for item in items.iter().rev() {
540 let head = lower_quote(item)?;
541 acc = Exp::Cons(rc_exp(head), rc_exp(acc));
542 }
543 Ok(acc)
544}
545
546// ── Runtime helpers ───────────────────────────────────────────────────────────
547
548/// Convert a read s-expression directly into a runtime value — lists
549/// become `Tup` chains, no lowering, no name resolution. This is how
550/// the `read` and `read-file` primitives hand parsed input to the
551/// evaluator as data.
552pub fn sexp_to_val(sexp: &Sexp) -> crate::core::Val {
553 use crate::core::{tup, Val};
554 match sexp {
555 Sexp::Num(n) => Val::Cst(*n),
556 Sexp::Sym(s) => Val::Sym(s.clone()),
557 Sexp::Nil => Val::Nil,
558 Sexp::List(items) => items.iter().rev().fold(Val::Nil, |acc, item| {
559 tup(sexp_to_val(item), acc)
560 }),
561 }
562}
563
564/// Read the first complete form from `src`, ignoring anything after it.
565pub fn parse_one(src: &str) -> Result<Sexp, String> {
566 let mut forms = parse_forms(src.trim(), true)?;
567 forms
568 .pop()
569 .ok_or_else(|| "unexpected end of input: expected an s-expression".to_string())
570}
571
572/// Parse every top-level form in `src`. String-error twin of
573/// `repl::parse_all`, callable from the evaluator (`read-file`) without a
574/// layering inversion.
575pub fn parse_all(src: &str) -> Result<Vec<Sexp>, String> {
576 if src.trim().is_empty() {
577 return Ok(vec![]);
578 }
579 parse_forms(src, false)
580}