Parser

The parser is scannerless and top-down: it matches characters and builds the AST in the same pass. There is no token stream in between, and no token list to collect — a charseq action is the nearest thing to a token, and it is already a node in the tree.

The parser { } block is itself syntac: every run of it produces a root node, and those roots are the whole tree. result.nodes is the list of them, in the order they were parsed.

The parser block names the start grammar — the single grammar the parser begins from. Parsing runs it from the front of the input; when it returns a match and text remains, the parser runs it again from where it left off, and so on until the input is consumed or a run matches nothing. Each run is one top-level block. There is exactly one start grammar.

parser {
    main_grammar;   # the single start grammar (a syntac/charseq action, an option, a series, or any unit)
};

The start grammar can be a named action:

parser {
    my_action;
};

an option:

parser {
    "A" | "B" | "C";
};

any single unit:

parser {
    "A";
};

or a series:

parser {
    "A" "B" "C" "D";
};

Repetition is the loop; separators are .

Because the parser re-runs the start grammar over what remains, a grammar of adjacent items needs no counter — the loop walks them:

parser {
    item;   # the loop repeats it: item item item ...
};

The loop does not skip anything between runs. Inbetween is the . building block — it runs the configured skip only where . appears in the grammar. So when items are separated (e.g. by spaces), put . where the separator goes:

parser {
    item (. item)*;   # items separated by the inbetween
};
. { spc };