Defined in header <cparse/Parser.hh>
bool parse( std::string filename );
Runs a full parse over filename. Canonicalises the path, opens the file as a Cursor, calls the subclass's start() method, and translates every exception path into a return value plus (usually) an appended ParseError entry.
Callers read errors() after parse() returns to obtain the diagnostics.
Parameters
| Parameter | Description |
|---|---|
filename | Path to the file to parse. Passed to std::filesystem::canonical, so relative paths are resolved and symlinks followed. |
Return value
true on clean completion — the grammar returned true from start() and the input was fully consumed. false on any error path (see Lifecycle below).
Note: true does not mean "no errors recorded". A grammar may call recordError() with ParseError::Kind::Warning and still return true from start(); parse() will also return true. Callers who need "no diagnostics at all" combine the return value with a hasErrors() check.
Exceptions
Does not throw. Every exception path is translated internally, per the Lifecycle table below.
Lifecycle
parse() runs six phases. Each phase is small, but the interaction between them — especially the exception translations — is where the subtle behaviours live.
Phase 1 — Error-collector reset
if (!cursor) clearErrors();
The error collector is emptied only when this is the outermost parse. Nested parse() calls (invoked from within start() to handle an #include construct, for example) leave the parent context's diagnostics intact.
Phase 2 — Cursor push
if (cursor) {
cursor->store();
returnPoints.push(cursor);
}
If there is already an active cursor, its position is saved and it is pushed onto the return-point stack. The stack unwinds automatically when the nested parse returns.
If this is the outermost parse, the return-point stack starts empty and stays empty until Phase 6.
Also in Phase 2 (outermost only): the open-scope depth is reset to zero. A nested parse does not reset the depth — the including grammar legitimately has scopes open around the include point, and clearing the count would hide a truncated include behind its parent's still-open braces.
Phase 3 — File open
try {
absolutePath = filesystem::canonical(filename);
cursor = make_shared<Cursor>(absolutePath.string());
result = true;
} catch (filesystem::filesystem_error const&) {
clog << "[Parser::parse] filesystem error: ...";
} catch (ios_base::failure const&) {
clog << "[Parser::parse] I/O failure: ..." or "file not found";
} catch (exception const&) {
clog << "[Parser::parse] ...";
}
Infrastructure errors (file not found, permission denied, I/O failure) are logged to std::clog and translated to a false return with no ParseError recorded. Grammar errors are recorded; infrastructure errors are not, because they are not the grammar's concern. Callers who care about the distinction check the return value and the error-vector emptiness together.
If any exception fires in Phase 3, result stays false and the implementation falls through to Phase 6.
Phase 4 — Grammar invocation
try {
result = start();
if (!result) {
recordError(Kind::Internal, "unexpected file parsing result");
} else if (!cursor->isValid() || !returnPoints.empty()) {
if (!returnPoints.empty()) {
recordError(Kind::Warning, "parser returned with unclosed cursor stack");
result = false;
}
}
}
start() is called. Its return value is stored in result. Three paths matter:
start()returnedfalsewithout recording a diagnostic — a grammar bug.parse()records anInternal-kind"unexpected file parsing result"entry so the failure is not silent.start()returnedtrueand the cursor is at end-of-input with the return-point stack empty — the normal happy path.start()returnedtruebut the return-point stack is non-empty — apushCursorwas never popped by a nested parse completing. Recorded as aWarning,resultset tofalse.
Phase 5 — Exception translation
} catch (Exited const&) {
// exit() was called — clear return points, return false
} catch (ios_base::failure const& e) {
if (grammar-EOF)
result = (openScopes_ == 0) // true if clean EOF, false if truncated
else
recordError(Kind::Internal, "I/O failure: ...");
} catch (runtime_error& e) {
recordError(Kind::Internal, e.what());
} catch (int& code) {
recordError(Kind::Internal, "error code ...");
} catch (...) {
recordError(Kind::Internal, "undefined error");
}
Five exception paths, each handled distinctly:
Exited— a private exception thrown byexit(). The diagnostic was already recorded by the grammar before callingexit();parse()clears the return-point stack and setsresult = false.ios_base::failureat EOF — the cursor threw at end-of-input. IfopenScopeDepth()is zero, this is a clean end andresultbecomestrue. If it is non-zero, aSyntax-kind"unexpected end of input: N unterminated scope(s)"is recorded andresultbecomesfalse. This is the full "clean vs truncated EOF" discrimination in one branch.ios_base::failurenon-EOF — a real I/O failure mid-parse. Recorded asInternal.runtime_error— any other standard-library or grammar-thrown runtime error. Recorded asInternalwith the exception'swhat().int&andcatch (...)— legacy escape hatches from older grammars. Both recorded asInternal.
Phase 6 — Cursor pop
if (returnPoints.empty()) {
cursor.reset();
} else {
cursor = returnPoints.top();
returnPoints.pop();
cursor->restore();
result = true;
}
If the return-point stack is empty (this was the outermost parse), the active cursor is released. Otherwise, this is a nested parse completing: the parent cursor is popped from the stack, restored, and result is forced to true — the nested parse's success/failure is subsumed into the parent's, which will see the diagnostics through the shared error collector.
Notes
Filename provenance
The filename stored in the Cursor — and subsequently written into every ParseError by recordError() — is the canonicalised absolute path, not what the caller passed in. Error consumers displaying paths may want to shorten them relative to a project root; that is the consumer's job.
The forced result = true at nested-parse end
Phase 6's result = true for nested parses looks surprising at first — a failed nested parse still returns success? — but it is correct: the failure has already been captured in the shared error collector. Returning false would double-count the failure and make the calling grammar's control flow harder (every nested parse() call would have to be manually OR-combined). Callers that need per-nest granularity call hasErrors() before and after the nested parse and compare.
Example
#include <cparse/Parser.hh>
#include <iostream>
int main(int argc, char** argv)
{
MyGrammar p;
bool const ok = p.parse(argv[1]);
for (auto const& e : p.errors())
{
std::cerr << e.filename << ':' << e.line << ':' << e.col
<< ": " << e.message << '\n';
}
return ok ? 0 : 1;
}
See also
- Parser::start — the grammar entry point invoked in Phase 4.
- Parser::exit — signals the fatal path handled in Phase 5.
- Parser::pushCursor — the mechanism behind nested parses.
- Parser::errors — read the diagnostics after
parse()returns. - Cursor — the stream position
parse()constructs and owns.

