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 typeDefinition
TermRequirementDeclarationShared 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

parseruns a full parse over a named file; entry point
getGrammarName(pure virtual) returns a diagnostic label for the grammar
errorsreturns the error collector by const reference
hasErrorsreturns true if any error has been recorded
clearErrorsempties the error collector

Grammar entry point (protected, must override)

start(pure virtual) grammar entry point called once per parse

Whitespace and comment skipping (protected)

skipWhiteSpacesconsumes any run of std::isspace characters; overridable
skipCommentsno-op by default; override to skip your grammar's comment syntax
skipCommentsBlockalternates whitespace and comments until neither consumes anything

Token extraction (protected)

extractTokenconsumes a literal token if it matches; backtracks on failure
extractCharacterconsumes one character if it matches

Cursor management (protected)

getCursorreturns the active cursor by reference
pushCursorpushes the current cursor and installs a new one around a stream
storeCursorrecords the current cursor position for backtracking
restoreCursorseeks back to a saved cursor position

Error handling (protected)

exitsignals a fatal grammar error; throws internally, caught by parse()
recordErrorappends a ParseError to the collector without terminating

Open-scope tracking (protected)

enterScopeincrements the open-scope counter (call on opener consumption)
leaveScopedecrements the counter (call on closer consumption)
openScopeDepthreturns the current open-scope depth

Trace helper (protected)

writeTraceMessagewrites 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 of parse() may still be true if the grammar recovers.
  • Fatal errors — grammar reaches an unrecoverable state. Recorded via recordError() first, then exit() throws a private exception caught inside parse(), which returns false.
  • Infrastructure errors — file not found, permission denied, I/O failure. Logged to std::clog by parse(). Do not become ParseError entries. parse() returns false with 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.