// SPDX-License-Identifier: MIT
#pragma once
#include <memory>
#include <stack>
#include <string>
#include <vector>
#include "Cursor.hh"
/// \file Parser.hh
/// \brief Abstract recursive-descent parser driver and its diagnostic record.
namespace fedem
{
namespace parser
{
/// \brief A single diagnostic produced during parsing.
///
/// Records are appended to the collector by `Parser::recordError()` and
/// read back through `Parser::errors()` after `parse()` returns. A record
/// carries its severity (#Kind), a message, and the source location it was
/// raised at (filename / line / column, filled from the cursor when the
/// caller passes 0).
///
/// \see https://cparse.fedem.eu/reference/parse-error — reference manual page.
/// \see Parser::errors
struct ParseError
{
/// \brief Severity / category of a #ParseError.
enum class Kind
{
Syntax, ///< Malformed token sequence.
Type, ///< Type mismatch / undefined identifier.
Faulty, ///< Ill-formed expression (e.g. nested comment terminator).
Internal, ///< Parser-internal assertion / unexpected state.
Warning, ///< Non-fatal diagnostic; parsing continues.
};
Kind kind { Kind::Internal }; ///< Severity / category.
std::string message; ///< Human-readable description.
std::string filename; ///< Source file the error is in.
unsigned long int line { 0 }; ///< 1-based line, or 0 if unknown.
unsigned long int col { 0 }; ///< 1-based column, or 0 if unknown.
};
/// \brief Abstract driver for a hand-written recursive-descent grammar.
///
/// Subclass `Parser`, implement `start()` (the grammar's entry rule) and
/// `getGrammarName()`, and call `parse()` with a filename. The base handles
/// the parts every grammar shares:
///
/// - **Cursor management** — opens the file, exposes it through
/// `getCursor()`, and supports nested inputs (includes) with a cursor
/// stack (`pushCursor()`, `storeCursor()` / `restoreCursor()` for
/// backtracking).
/// - **End-of-input classification** — `parse()` cannot tell "the grammar
/// finished" from "the input was truncated"; both arrive as the same
/// `std::ios_base::failure`. A grammar with nesting calls `enterScope()`
/// on an opener and `leaveScope()` on a closer so `parse()` can report a
/// non-zero `openScopeDepth()` at EOF as a syntax error.
/// - **Error collection** — `recordError()` appends; `errors()`,
/// `hasErrors()`, `clearErrors()` read and reset. `parse()` clears the
/// collector on entry.
/// - **Lexical helpers** — `skipWhiteSpaces()`, `skipComments()` (a no-op
/// base a grammar overrides), `skipCommentsBlock()`, `extractToken()`,
/// `extractCharacter()`.
/// - **Fatal exit** — `exit()` throws an internal exception caught by
/// `parse()`; record the reason with `recordError()` first.
///
/// Copy and move are deleted.
///
/// \see https://cparse.fedem.eu/reference/parser — reference manual page.
/// \see https://cparse.fedem.eu/docs/getting-started — build a first grammar.
/// \see Cursor
class Parser
{
public:
/// \brief Whether a grammar rule requires, permits, or forbids a
/// terminator. Passed to grammar-specific term-matching helpers.
/// \see https://cparse.fedem.eu/reference/parser-term-requirement-declaration
enum class TermRequirementDeclaration
{
DISALLOWED, ///< A terminator must not appear.
OPTIONAL, ///< A terminator may appear.
MANDATORY ///< A terminator must appear.
};
/// \brief Default-construct. No file is open until `parse()` is called.
/// \see https://cparse.fedem.eu/reference/parser-ctor
Parser( );
/// \brief Destructor.
/// \see https://cparse.fedem.eu/reference/parser-dtor
virtual ~Parser( );
/// \brief Parse \p filename: open it, run `start()`, and classify the
/// ending.
/// \param filename Path to parse. Resolved with
/// `std::filesystem::canonical`; a missing file is reported on
/// `std::clog` and yields `false`.
/// \return `true` on a clean parse (grammar succeeded and any scopes it
/// opened were closed); `false` if `start()` failed, `exit()`
/// was called, the input ended with scopes still open, or an
/// I/O / internal error occurred. A nested `parse()` (an
/// include) restores the parent cursor and returns `true`.
/// \note On failure, inspect `errors()` for the reason(s). Only the
/// outermost `parse()` resets the open-scope depth.
/// \see https://cparse.fedem.eu/reference/parser-parse
bool parse( std::string filename );
/// \brief Open \p filename and set up the cursor, but do not run
/// `start()` — for a grammar driven from outside one unit at
/// a time instead of "parse everything, then read the
/// result" (the canonical example: a CSV-style reader whose
/// public interface is `nextRow()`, called until it returns
/// nothing).
///
/// `parse()` does not fit that shape: it calls `start()` itself and,
/// the instant `start()` returns, tears the cursor down — right for
/// a grammar where `start()` **is** the whole parse, fatal for a
/// driver that still needs `getCursor()` valid afterwards (the
/// external caller has not finished reading yet). `openStream()` does
/// only the setup half `parse()` does — same
/// `std::filesystem::canonical` resolution, same missing-file
/// diagnostic on `std::clog` — and stops there. Drive the grammar
/// afterwards through whatever public methods it exposes for that
/// (e.g. repeated calls into helpers built on `getCursor()`); the
/// cursor stays valid until the next `parse()`/`openStream()` call
/// or destruction.
///
/// \param filename Path to open. Resolved with
/// `std::filesystem::canonical`; a missing file is reported on
/// `std::clog` and yields `false`.
/// \return `true` if the file was opened and the cursor is ready.
/// \see https://cparse.fedem.eu/reference/parser-open-stream
/// \see https://cparse.fedem.eu/walkthrough/csv-lite
bool openStream( std::string filename );
/// \brief The grammar's name, for diagnostics. Implemented by the subclass.
/// \return The grammar name.
/// \see https://cparse.fedem.eu/reference/parser-get-grammar-name
virtual std::string getGrammarName( ) const noexcept = 0;
/// \brief The collected diagnostics, in the order they were recorded.
/// Valid after `parse()` returns; check when it returns `false`.
/// \return The diagnostics collector, oldest first.
/// \see https://cparse.fedem.eu/reference/parser-errors
std::vector< ParseError > const& errors ( ) const noexcept;
/// \brief Whether any diagnostic has been collected.
/// \return `true` if the collector is non-empty.
/// \see https://cparse.fedem.eu/reference/parser-has-errors
bool hasErrors ( ) const noexcept;
/// \brief Discard all collected diagnostics. Called by `parse()` on entry.
/// \see https://cparse.fedem.eu/reference/parser-clear-errors
void clearErrors( ) noexcept;
protected:
/// \brief The grammar's entry rule. Implemented by the subclass; called
/// by `parse()`.
/// \return `true` if the grammar accepted the input.
/// \see https://cparse.fedem.eu/reference/parser-start
virtual bool start( ) = 0;
/// \brief Consume a run of whitespace at the cursor.
/// \return `true` if at least one whitespace character was consumed.
/// \see https://cparse.fedem.eu/reference/parser-skip-white-spaces
virtual bool skipWhiteSpaces( );
/// \brief Consume a comment at the cursor. The base returns `false`
/// (no comment syntax); a grammar overrides it.
/// \return `true` if a comment was consumed.
/// \see https://cparse.fedem.eu/reference/parser-skip-comments
virtual bool skipComments( );
/// \brief Repeatedly `skipWhiteSpaces()` and `skipComments()` until
/// neither consumes anything.
/// \return `true` if anything at all was skipped.
/// \see https://cparse.fedem.eu/reference/parser-skip-comments-block
bool skipCommentsBlock( );
/// \brief Match \p token literally at the cursor. On a partial match the
/// cursor is restored to where it started.
/// \param token The literal to match.
/// \return `true` if the whole token matched and was consumed.
/// \see https://cparse.fedem.eu/reference/parser-extract-token
bool extractToken( std::string const& token );
/// \brief Match a single character at the cursor.
/// \param token The character to match.
/// \return `true` if it matched and was consumed.
/// \see https://cparse.fedem.eu/reference/parser-extract-character
bool extractCharacter( std::string::value_type const token );
/// \brief Write a short trace (location plus the next few lines of
/// input) to `std::clog`. A debugging aid.
/// \see https://cparse.fedem.eu/reference/parser-write-trace-message
void writeTraceMessage( );
/// \brief The active cursor.
/// \return A reference to the cursor `parse()` is currently reading.
/// \see https://cparse.fedem.eu/reference/parser-get-cursor
Cursor& getCursor( );
/// \brief The active cursor (const overload).
/// \return A const reference to the cursor `parse()` is currently reading.
/// \see https://cparse.fedem.eu/reference/parser-get-cursor
Cursor const& getCursor( ) const;
/// \brief Push a new input onto the cursor stack (an include). The
/// current cursor is saved and restored when the nested input ends.
/// \param sourceName Name for diagnostics.
/// \param stream The nested input stream.
/// \see https://cparse.fedem.eu/reference/parser-push-cursor
void pushCursor( std::string const& sourceName,
std::shared_ptr< Cursor::stream_type > stream );
/// \brief Save the cursor position and return a copy to hold as a
/// backtrack point.
/// \return A reference to the active cursor, now with its position saved.
/// \see https://cparse.fedem.eu/reference/parser-store-cursor
Cursor const& storeCursor( );
/// \brief Restore the cursor from a copy taken by `storeCursor()`.
/// \param crs A cursor copy previously returned by `storeCursor()`.
/// \see https://cparse.fedem.eu/reference/parser-restore-cursor
void restoreCursor( Cursor const& crs );
/// \brief Abort the parse. Throws an internal exception caught by
/// `parse()`, which then returns `false`.
/// \param status Exit code carried on the exception.
/// \note Record the reason with `recordError()` before calling this.
/// \see https://cparse.fedem.eu/reference/parser-exit
void exit( int status );
/// \brief Append a diagnostic to the collector.
/// \param kind Severity / category.
/// \param message Description.
/// \param line 1-based line, or 0 for the line of the next unread
/// character (`Cursor::getNextLineNumber()`).
/// \param col 1-based column, or 0 for the column of the next unread
/// character (`Cursor::getNextColumnNumber()`).
/// \note The default names the character the grammar stopped at: after
/// a failed `extractToken()` / `extractCharacter()`, the first
/// character that did not match. To point at text already
/// consumed, save the cursor's position before consuming it and
/// pass it explicitly.
/// \note Does **not** call `exit()`; call `exit()` separately when fatal.
/// \see https://cparse.fedem.eu/reference/parser-record-error
void recordError( ParseError::Kind kind,
std::string const& message,
unsigned long int line = 0,
unsigned long int col = 0 );
/// \brief Note that the grammar has consumed a scope opener (brace,
/// bracket, `begin`, …).
///
/// A grammar with nesting calls this on an opener and `leaveScope()` on
/// the matching closer. `parse()` reads `openScopeDepth()` after
/// end-of-input: a non-zero depth means the input was truncated
/// mid-construct and `parse()` records a syntax error instead of
/// treating the EOF as clean.
///
/// This is deliberately **not** an RAII guard: the depth must stay
/// non-zero while the end-of-input exception unwinds, because that is
/// the condition being detected. Grammars that never call these keep a
/// depth of zero and behave exactly as before.
///
/// \see https://cparse.fedem.eu/reference/parser-enter-scope
void enterScope( ) noexcept;
/// \brief Note that the grammar has consumed the matching scope closer.
/// Called only on the path that actually consumed a closer.
/// \see https://cparse.fedem.eu/reference/parser-leave-scope
void leaveScope( ) noexcept;
/// \brief The number of scope openers not yet balanced by a closer.
/// \return The current open-scope depth (0 for a grammar that does not
/// call `enterScope()`).
/// \see https://cparse.fedem.eu/reference/parser-open-scope-depth
unsigned int openScopeDepth( ) const noexcept;
private:
/// \brief Resolve \p filename to a canonical path and point `cursor`
/// at a freshly opened Cursor for it. Shared by `parse()` and
/// `openStream()` — what happens after this succeeds (run
/// `start()`, or just stop) is where they diverge.
/// \param caller Public entry point name for the `std::clog` tag
/// ("parse" / "openStream").
bool openCursorFor( std::string const& filename, std::string const& caller );
std::shared_ptr< Cursor > cursor;
std::stack< std::shared_ptr< Cursor > > returnPoints;
std::vector< ParseError > errors_;
unsigned int openScopes_ = 0U;
Parser( Parser const& ) = delete;
Parser( Parser&& ) = delete;
Parser& operator=( Parser const& ) = delete;
Parser& operator=( Parser&& ) = delete;
/// \brief Internal exception thrown by `exit()` and caught by `parse()`.
class Exited
{
public:
explicit Exited( int code );
virtual ~Exited( ) = default;
int getExitCode( ) const;
protected:
int exitCode;
};
};
inline Cursor& Parser::getCursor( )
{
return *cursor;
}
inline Cursor const& Parser::getCursor( ) const
{
return *cursor;
}
inline std::vector< ParseError > const& Parser::errors( ) const noexcept
{
return errors_;
}
inline bool Parser::hasErrors( ) const noexcept
{
return !errors_.empty( );
}
inline void Parser::clearErrors( ) noexcept
{
errors_.clear( );
}
} // end namespace parser
} // end namespace fedem