Map: core / syntax
core/src/syntax/ turns source text into the surface AST — the only place that
sees raw bytes and bare words.
lexer.rs—lex(source) -> Result<Vec<(Token, Span)>, LexError>; theTokenenum. An unterminated string names the exact closer it still wants,StringForm::closing()— a bumped literal wants its#run back after the', so the bare reflex never closes it. Folding fd 1 onto fd 2 (1>&2, and>&2, which is the same redirect spelled short) is refused at the lexer, pointing at thewarnbuiltin, and at2>&1for a program holding the direction backwards: a diagnostic is a verb here, not a second name for the byte channel.parser.rs—parse(source) -> Result<Vec<Stmt>, ParseError>;parse_withcarries aFileId. A trailing&is a stage terminator only so its refusal can namespawn { … }, whose handle youawait. A digit glued to a comparison operator inside$[…]($[2>3], lexed as the file-descriptor redirect2>) earns a diagnostic that names the shape and asks for spaces, not a bare “redirect” token.<<is the here-string redirect; the bash spellings (<<<, a glued heredoc<<EOF) earn targeted diagnostics naming ral’s form. A run of separators collapses to one token,Token::Semiwhen it contains a;and the softToken::Newlineotherwise: a;is hard, never crossed by a pipeline or chain continuation.parse_patternis the one place a duplicate binder is refused — a pattern binds all its names or none, solet [same, same] = [1, 2]is a parse error at the pattern’s span, while a repeat across curried parameters stays ordinary shadowing.ast.rs— the surface AST.Astis the expression node andStmtthe statement node; the enum is deliberately wide and flat (ast-stays-flat).Ast::Unitis the()literal — punctuation denoting the unit value, like[]and[:], so not a word.Ast::Casecarriesarms: Vec<CaseArm>, a finite list of tag-and-body alternatives the parser hands on whole (case-is-syntax-try-is-not).WordLiteral::classifyis the numeral grammar: purely lexical, never a round trip through printing, so007and1.50are numerals while1e5, which merely happens to f64-parse, is text (a-numeral-denotes-its-number).group.rs— pre-pass detecting mutually recursive binding groups, consumed by the elaborator.quote.rs— bare-word classification (is_bare_word,quote_word,quote_word_if_needed), shared with the REPL. It is the dual ofWordLiteral::classify: a string may go bare only where the numeral grammar declines it, since a bare007would read back as the number 7.tag.rs,free_refs.rs— variant-label tagging and free-reference scans.
Recursion is bounded by a single shared depth cap, so adversarial nesting
rejects cleanly instead of overflowing the stack. One NESTING_DEPTH_LIMIT
governs both stages.
- The lexer enforces it at
scan_token_group, the sole recursion through bracketed token groups. - The parser’s three mutually-recursive sub-grammars each descend through
exactly one guarded chokepoint, all counting against
Parser::nested’s shareddepth: values throughparse_primary, arithmetic throughparse_expr_atom(the unary prefixes-/notand parenthesised sub-expressions bottom out here), patterns throughparse_pattern(list and map patterns recurse back through it per element).
The five reserved control operators are recognised
at this layer; everything else is a library binding. See docs/SPEC.md §3,
§17.1 for the grammar.