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

ParameterDescription
filenamePath 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() returned false without recording a diagnostic — a grammar bug. parse() records an Internal-kind "unexpected file parsing result" entry so the failure is not silent.
  • start() returned true and the cursor is at end-of-input with the return-point stack empty — the normal happy path.
  • start() returned true but the return-point stack is non-empty — a pushCursor was never popped by a nested parse completing. Recorded as a Warning, result set to false.

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 by exit(). The diagnostic was already recorded by the grammar before calling exit(); parse() clears the return-point stack and sets result = false.
  • ios_base::failure at EOF — the cursor threw at end-of-input. If openScopeDepth() is zero, this is a clean end and result becomes true. If it is non-zero, a Syntax-kind "unexpected end of input: N unterminated scope(s)" is recorded and result becomes false. This is the full "clean vs truncated EOF" discrimination in one branch.
  • ios_base::failure non-EOF — a real I/O failure mid-parse. Recorded as Internal.
  • runtime_error — any other standard-library or grammar-thrown runtime error. Recorded as Internal with the exception's what().
  • int& and catch (...) — legacy escape hatches from older grammars. Both recorded as Internal.

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