Code View

cparse

cparse

cparse is a small C++20 library for writing hand-written recursive-descent parsers — and the code generators that often come with them. It is not a parser generator: you write the grammar as ordinary C++, one method per rule, and cparse supplies the parts every such parser needs.

Class (fedem::parser)What it gives you
CursorA read position over a file or stream: get / peek (throwing and safe… variants), automatic line / column tracking, store / restore for backtracking.
ParserThe abstract driver. Override start() (your grammar's entry rule) and getGrammarName(); use the protected helpers — extractToken, extractCharacter, skipWhiteSpaces, skipComments, recordError, nested inputs, scope tracking for truncated input.
GeneratorBase for code generators: snake ↔ camel case conversion, header guards, namespace scopes, C++ string encoding.
IndentationA std::ostream manipulator for step-based indentation of generated code.

Documentation, a reference manual and walkthroughs: <https://cparse.fedem.eu>.

Example

A grammar for a comma-separated list of integers — 1, 2, 30, 400:

#include <cparse/Parser.hh>

#include <cctype>
#include <iostream>
#include <vector>

using fedem::parser::ParseError;

class IntList : public fedem::parser::Parser
{
  public:
    std::string getGrammarName( ) const noexcept override { return "int-list"; }
    std::vector<int> const &values( ) const noexcept { return values_; }

  protected:
    bool start( ) override
    {
        skipWhiteSpaces( );
        if( !readInt( ) ) return true;                 // empty input is valid

        while( true )
        {
            skipWhiteSpaces( );
            if( !extractCharacter( ',' ) ) return true; // end of the list
            skipWhiteSpaces( );
            if( !readInt( ) )
            {
                recordError( ParseError::Kind::Syntax, "expected an integer after ','" );
                return false;
            }
        }
    }

  private:
    bool readInt( )
    {
        std::string digits;
        while( std::isdigit( static_cast<unsigned char>( getCursor( ).safePeek( ) ) ) )
            digits += getCursor( ).safeGet( );
        if( digits.empty( ) ) return false;
        values_.push_back( std::stoi( digits ) );
        return true;
    }

    std::vector<int> values_;
};

int main( int argc, char **argv )
{
    if( argc < 2 ) { std::cerr << "usage: intlist <file>\n"; return 1; }

    IntList list;
    bool const ok = list.parse( argv[1] );

    for( auto const &e : list.errors( ) )
        std::cerr << e.filename << ':' << e.line << ':' << e.col << ": " << e.message << '\n';
    if( !ok ) return 1;

    for( int v : list.values( ) ) std::cout << v << '\n';
}
$ ./intlist list.txt              # "1, 2, x"
list.txt:1:7: expected an integer after ','
list.txt:1:7: unexpected file parsing result

parse() opens the file, runs start() and returns false if the grammar failed; errors() holds every diagnostic with its file:line:col (a failed start() also adds a generic "unexpected file parsing result" record after the grammar's own).

Without an explicit position, recordError() points at the character the grammar stopped at — here the x. To point at text already consumed (the start of a token, an opening bracket), save getCursor().getNextLineNumber() / getNextColumnNumber() before consuming it and pass them in.

ParseError lives in the namespace, not in Parser — qualify it, or bring it in with a using, as above.

The source tree has three complete examples, each with a smoke test:

examples/
toy-calcan arithmetic expression evaluator — precedence, parentheses, one expression per line of standard input
key-valuean INI-style [section] / key = value reader with # comments
csv-litea streaming CSV reader driven row by row with openStream()

Building

cparse builds with CMake (≥ 3.20) and a C++20 compiler; its only other dependency is GoogleTest, for the tests, when they are built.

With Conan:

conan create .                          # build, run the tests, package
conan create . -o with_tests=False
conan build . -o with_docs=True         # also the Doxygen tree (build/Release/docs/html)

For development:

conan install . --build=missing
cmake --preset conan-release
cmake --build --preset conan-release
ctest --test-dir build/Release --output-on-failure
CMake optionDefault
TESTINGON when top-levelGoogleTest suites in test/ (skipped if GTest is not found)
EXAMPLEON when top-levelexamples/ and their smoke tests
CPARSE_INSTALL_COMPONENTcparseinstall component of libcparse.so
CPARSE_DOCS_WARN_AS_ERROROFFfail the docs target on a Doxygen warning

cmake --build … --target docs writes the API reference with Doxygen (and Graphviz dot, if present).

Using cparse

Conan — the way fson, mstring and FFS take it:

def requirements(self):
    self.requires("cparse/[>=1.2 <2]")
find_package(cparse CONFIG REQUIRED)
target_link_libraries(myparser PRIVATE cparse::cparse)

Embedded — add_subdirectory(cparse) and link the cparse target. As a subproject cparse turns off its tests, examples, docs target and CPack section, and leaves the build type to the host.

Installed — cmake --install puts the headers under include/cparse/, libcparse.so under lib/ and a pkg-config file:

g++ -std=c++20 myparser.cpp $(pkg-config --cflags --libs cparse)

Notes

  • A Cursor built from a file name reads the whole file into memory; a parser can backtrack as often as it likes without touching the file again. Inputs are expected to be small (configuration, source files).
  • libcparse.so.1 keeps its ABI within 1.x.
  • The version is the literal on the project( … VERSION … ) line of CMakeLists.txt; the Conan recipe and the release tooling read it from there.

Layout

Path
src/cparse/the library
test/GoogleTest suites
examples/toy-calc, key-value, csv-lite
Doxyfile.inthe docs target's configuration
conanfile.py, cparse.pc.inpackaging

License

MIT — see LICENSE, CONTRIBUTING.md and TRADEMARKS.md.