Defined in header <cparse/Parser.hh>
class Parser;
Parser is the abstract base for every cparse grammar. You subclass it and override two virtual methods; the rest is machinery the base class provides and you call from inside your grammar.
The class is deliberately small — around 170 lines including the comments that explain the design decisions — and it does one thing: drive a parse from start to finish while collecting errors and tracking open scopes. Everything else (tokenising, AST construction, symbol tables) is your subclass's responsibility.
Copy and move are deleted. Every Parser instance owns its own cursor stack and error collector; sharing does not make sense.
Member types
| Member type | Definition |
|---|---|
TermRequirementDeclaration | Shared enum vocabulary for grammars that classify a construct's terminator as DISALLOWED, OPTIONAL or MANDATORY. Declared in Parser but not used by the base class itself. |
Member functions
Construction / destruction
| (constructor) | constructs the parser (default-only) |
| (destructor) | destroys the parser (virtual) |
Public interface
| parse | runs a full parse over a named file; entry point |
| getGrammarName | (pure virtual) returns a diagnostic label for the grammar |
| errors | returns the error collector by const reference |
| hasErrors | returns true if any error has been recorded |
| clearErrors | empties the error collector |
Grammar entry point (protected, must override)
| start | (pure virtual) grammar entry point called once per parse |
Whitespace and comment skipping (protected)
| skipWhiteSpaces | consumes any run of std::isspace characters; overridable |
| skipComments | no-op by default; override to skip your grammar's comment syntax |
| skipCommentsBlock | alternates whitespace and comments until neither consumes anything |
Token extraction (protected)
| extractToken | consumes a literal token if it matches; backtracks on failure |
| extractCharacter | consumes one character if it matches |
Cursor management (protected)
| getCursor | returns the active cursor by reference |
| pushCursor | pushes the current cursor and installs a new one around a stream |
| storeCursor | records the current cursor position for backtracking |
| restoreCursor | seeks back to a saved cursor position |
Error handling (protected)
| exit | signals a fatal grammar error; throws internally, caught by parse() |
| recordError | appends a ParseError to the collector without terminating |
Open-scope tracking (protected)
| enterScope | increments the open-scope counter (call on opener consumption) |
| leaveScope | decrements the counter (call on closer consumption) |
| openScopeDepth | returns the current open-scope depth |
Trace helper (protected)
| writeTraceMessage | writes cursor location + up to three surrounding lines to std::clog |
Notes
Two pure virtuals
The only two = 0 methods are getGrammarName() (public) and start() (protected). Everything else is optional to override. A minimal grammar is:
class MyGrammar : public fedem::parser::Parser
{
public:
std::string getGrammarName() const noexcept override { return "my-grammar"; }
protected:
bool start() override { /* ... */ return true; }
};
Error philosophy
Three distinct failure modes surface differently:
- Non-fatal errors — grammar reports "expected X" but keeps going. Recorded via
recordError(). The parse continues; return ofparse()may still betrueif the grammar recovers. - Fatal errors — grammar reaches an unrecoverable state. Recorded via
recordError()first, thenexit()throws a private exception caught insideparse(), which returnsfalse. - Infrastructure errors — file not found, permission denied, I/O failure. Logged to
std::clogbyparse(). Do not becomeParseErrorentries.parse()returnsfalsewith an empty error vector.
End-of-input detection
End-of-input surfaces via an std::ios_base::failure exception from the cursor. parse() catches it. Whether reaching EOF is success depends on open scopes — see the next note.
The enterScope/leaveScope invariant
parse() cannot distinguish "the input ended because the grammar finished" from "the input ended in the middle of a construct". Both arrive as the same EOF exception, and a truncated file would parse as valid with the missing half silently defaulted.
The fix is enterScope() / leaveScope(). Grammars with nesting call enterScope() when they consume an opener and leaveScope() when they consume the matching closer. If EOF fires while openScopeDepth() is non-zero, parse() records a Syntax error naming the unterminated scope count and returns false.
This is not scope-guard/RAII on purpose. Do not wrap these in scope guards — see enterScope for the detailed rationale.
Nested parses
Calling parse() again from inside start() (typically to handle an #include construct) is supported. The outer cursor is pushed onto a return-point stack; when the nested parse finishes, the outer cursor is restored. The error collector is not cleared for nested parses — diagnostics from the parent context are preserved.
Example
Minimal derivation, wired to a file:
#include <cparse/Parser.hh>
#include <iostream>
class HelloParser : public fedem::parser::Parser
{
public:
std::string getGrammarName() const noexcept override { return "hello"; }
protected:
bool start() override
{
skipWhiteSpaces();
if (!extractToken("hello"))
{
recordError(ParseError::Kind::Syntax, "expected 'hello'");
return false;
}
return true;
}
};
int main(int argc, char** argv)
{
HelloParser p;
if (!p.parse(argv[1]))
{
for (auto const& e : p.errors())
std::cerr << e.filename << ':' << e.line << ':' << e.col
<< ": " << e.message << '\n';
return 1;
}
std::cout << "ok\n";
}
See also
- Cursor — the stream position that Parser owns and drives.
- ParseError — the diagnostic record produced by
recordError(). - Getting Started — a working comma-separated-integer parser in under 50 lines.

