Code View

cparse / cparse-1.3.0.2 / src / cparse / Parser.hh
// 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