The Getting Started guide shows the minimum you need to build a working grammar. This page catalogues the patterns you reach for once your grammar grows beyond a single production: how to backtrack out of a failed alternative, how to track open braces so a truncated file is detected as such, how to parse an embedded snippet mid-file, and how to teach the base class about your grammar's whitespace and comment syntax.
Each pattern is short — cparse is a small library — and each one maps directly to a handful of methods on Parser and Cursor.
Backtracking with cursor snapshots
Parser::extractToken already handles the common case: it saves the cursor, tries to match, and restores on failure. You do not need to think about it — the sequence
skipCommentsBlock();
if (extractToken("return")) parseReturn();
else if (extractToken("if")) parseIf();
works because each unsuccessful extractToken leaves the cursor exactly where it was, so the next extractToken starts from the same position.
For alternatives that consume more than a single literal, you save and restore explicitly. The idiom uses cursor copy assignment:
bool parseNumberOrIdentifier(std::string& out)
{
Cursor snapshot = getCursor(); // real copy — see Cursor::Cursor(Cursor const&)
if (tryParseNumber(out)) // may consume characters, may fail
return true;
// number failed — roll back and try identifier
restoreCursor(snapshot);
return tryParseIdentifier(out);
}
getCursor() returns a reference to the parser's own cursor; Cursor snapshot = getCursor(); invokes the copy constructor. The two cursors now share the same underlying stream but carry their own position record. restoreCursor(snapshot) assigns the snapshot back and calls restore(), seeking the stream to the saved position.
Do not try to use storeCursor() / restoreCursor() alone for this pattern — storeCursor() returns a Cursor const& aliasing the parser's own cursor, so a further advance would invalidate the "snapshot" you thought you had. See Parser::storeCursor Notes for the distinction.
Nested scopes with enterScope/leaveScope
Any grammar with paired openers and closers should track them:
bool parseBlock()
{
if (!extractCharacter('{')) return false;
enterScope();
while (getCursor().isValid() && getCursor().safePeek() != '}')
{
skipCommentsBlock();
if (!parseStatement())
return false; // scope stays open — intentional
}
if (!extractCharacter('}'))
return false; // scope stays open — intentional
leaveScope();
return true;
}
The two return false paths deliberately leave openScopeDepth non-zero. If EOF fires on either path, Parser::parse reads the non-zero depth in its Phase 5 handler, records "unexpected end of input: N unterminated scope(s)", and returns false. A truncated file that ends mid-block is caught.
Reading the Parser::enterScope page in full is worth it — it explains why the pattern deliberately avoids RAII (a scope guard would decrement during EOF unwinding and defeat the mechanism). If you find yourself thinking "surely a ScopeGuard class would tidy this up", that page is for you.
Parsing an embedded stream (pushCursor)
When your grammar hits a construct that expands to more source text — a macro, an #include, a here-doc — you can switch the active cursor to a new stream and let the same grammar helpers consume it:
void expandMacro(std::string const& body)
{
auto ss = std::make_shared<std::istringstream>(body);
pushCursor("<macro-expansion>", ss);
// Continue calling your grammar's helpers — they now read from
// the embedded stream. When the stream hits EOF and the current
// production returns, Parser::parse Phase 6 pops the return
// stack and restores the outer cursor automatically.
parseMacroBody();
}
Two things worth knowing:
- There is no
popCursor(). The stack unwinds automatically when the pushed cursor hits EOF and the grammar returns from whatever function pushed it. Do not attempt manual unwinding. - Diagnostics are correctly labelled. Errors recorded while the pushed cursor is active carry its
sourceName(here"<macro-expansion>") inParseError::filename, so consumers can distinguish outer-file errors from embedded-source errors.
pushCursor also cooperates with nested parse() calls — if your grammar handles #include by calling parse(includedFile) from inside start(), that call implicitly pushes and pops without you writing any of it.
Custom whitespace
The base skipWhiteSpaces consumes any run of std::isspace characters. For most grammars this is exactly right. For grammars where newlines are significant (Python-style, INI-style), override to consume space and tab but not newline:
bool skipWhiteSpaces() override
{
bool consumed = false;
while (getCursor().safePeek() == ' ' || getCursor().safePeek() == '\t')
{
getCursor().get();
consumed = true;
}
return consumed;
}
The virtual dispatch means every base-class caller — including skipCommentsBlock() — automatically uses your override.
Custom comments
The base skipComments is a no-op. Override to consume your grammar's comment syntax. A grammar with both // line comments and /* … */ block comments:
bool skipComments() override
{
Cursor& c = getCursor();
if (extractToken("//"))
{
while (c.isValid() && c.safePeek() != '\n')
c.get();
return true;
}
if (extractToken("/*"))
{
while (c.isValid() && !extractToken("*/"))
c.get();
if (!c.isValid())
{
recordError(ParseError::Kind::Faulty, "unterminated block comment");
}
return true;
}
return false;
}
Two nuances to notice:
skipCommentsis called byskipCommentsBlockin a loop that alternates whitespace and comments. Grammars callskipCommentsBlock(notskipCommentsdirectly) so multiple consecutive comments and whitespace are handled uniformly.- The unterminated-block-comment path uses
isValid()in the loop condition — omitting this check would loop forever on an unterminated/* …at end of input. Every "consume until sentinel" loop needs an EOF guard.
Reporting errors at earlier positions
recordError fills line/column from the current cursor when you pass zeros for those arguments. For errors that refer to a construct that opened earlier — an unterminated brace, an undefined identifier used before its declaration — capture the coordinates on the way in and pass them explicitly:
if (extractCharacter('{'))
{
unsigned long const braceLine = getCursor().getLineNumber();
unsigned long const braceCol = getCursor().getColumnNumber();
enterScope();
while (getCursor().isValid() && !extractCharacter('}'))
{
if (!parseStatement())
return false;
}
if (!getCursor().isValid())
{
recordError(ParseError::Kind::Syntax,
"unterminated block starting here",
braceLine, braceCol);
return false;
}
leaveScope();
}
The filename field cannot be overridden — it is always taken from the active cursor. This is what you want in the common case; diagnostics inside a pushCursor context automatically carry the pushed cursor's name.
Fatal versus non-fatal errors
Two distinct paths:
recordErroralone. The parse continues. Use for anything recoverable — "expected)", "unknown identifier". The finalparse()return may still betrueif the grammar recovers and the input parses through.recordErrorfollowed byexit(status).exitthrows a private exception caught inparse()Phase 5 and translates it tofalse. Use when the grammar has reached a state from which no further diagnostic would be meaningful — a corrupted header, a required field missing at the top level.
Returning false from start() without calling exit() is also possible but different: parse() will append its own Internal-kind "unexpected file parsing result" entry on top of whatever you recorded. Prefer exit() when your grammar already recorded a precise diagnostic and does not want the driver's boilerplate on top.
What not to do
- Do not wrap
enterScope/leaveScopein an RAII guard. The depth must stay non-zero during EOF unwinding — seeParser::enterScopeNotes. - Do not use
Cursor::get()(throwing) in your own consuming loops. UsesafeGet()andsafePeek(); reserveget()for situations where you already know a character is present. - Do not modify the error vector directly.
errors()returns byconstreference for a reason. UserecordErrorto append. - Do not attempt to catch the private
Exitedexception yourself. It has no accessible type —parse()is the only legitimate catcher.
See also
- Getting Started — the introductory walkthrough this page builds on.
- Code Generation — Generator patterns (the emitter side).
- Reference › Parser — the class synopsis that ties everything on this page together.
- Reference › Cursor — the stream-position methods you drive.

