Preview of main at d0a501a, 2026-11-07

pratt is a header-only engine for Pratt parsing, top-down operator precedence, in C++20, one of the libraries of webcpp. It knows no token, no operator and no tree: a program gives it a stream of tokens and a grammar that says what each token means, and gets back the grammar’s node or an error, as a value. A calculator built on the engine comes with it, exact in decimal by default.

This page teaches pratt concept by concept: why each exists, its smallest example, its variations and its rules. Every program it shows was compiled and run, natively and for wasm32-wasip2 and wasm32-wasip3, and printed the output shown under it. The reference that closes it lists every name the headers declare.

Introduction

A parser turns text into structure: 1 + 2 * 3 into an addition whose right operand is a product. Pratt parsing, or top-down operator precedence, after Vaughan Pratt’s paper of 1973, does it with one loop and one question asked of every token: what it means at the start of an expression, what it means after one, and how tightly it holds what stands on its left. Precedence and associativity become numbers, and a new operator is a new answer to that question, not a new rule of a grammar.

pratt is that loop, and nothing else: an engine that knows no token, no operator and no tree. A program gives it a token stream and a grammar, and the engine calls the grammar for each token it reads. The grammar decides what a parse builds, a value computed on the way, a tree, or a string, and every failure comes back as a value. Beside the engine, pratt ships a calculator built on it, calc, which computes exactly in decimal by default (The calculator).

nud, led and lbp

Pratt names the three answers of a token:

  • nud, its null denotation: what it means with nothing on its left. A digit is a number, - is the negation of what follows it, ( opens a grouping.

  • led, its left denotation: what it means after a complete expression, which becomes its left operand. - is a subtraction there, and * a product.

  • lbp, its left binding power: how tightly it binds that left operand. A token of power 0, such as the end of the input or a closing parenthesis, takes no left operand, and ends an expression.

The engine’s whole algorithm is one function, expression(rbp). It reads a token and calls its nud; then, as long as the next token’s lbp is greater than rbp, it reads that token and calls its led with what it has parsed so far. A nud or a led that needs an operand calls expression again, with the binding power of its own operator, so the recursion stops at the first operator that binds less tightly. A * that asks for its right operand with its power, 20, stops before a + of power 10, which then takes the product as its left operand: precedence is that comparison of two numbers.

One token can have both denotations, as - has, and its nud and its led are two functions that never meet. That is what makes Pratt parsing fit the languages people write, where the same character is a sign, an operator, a grouping or a call, depending on where it stands.

A first grammar

This program parses integers of one digit with a unary minus and the binary +, - and *, and computes their values while it parses. It begins with the engine’s header and an alias for its namespace:

#include <webcpp/pratt.hpp>

#include <cstddef>
#include <iostream>
#include <string_view>

namespace pratt = webcpp::pratt;

The engine reads tokens from a token stream: here, the characters of a text, each one a token, with '\0' once the text is spent. peek shows the next token, advance consumes it, and at_end says whether anything is left:

/** The characters of `text`, one token each, and '\0' once the text is spent. */
class characters {
public:
    using token_type = char;

    explicit characters(std::string_view text) : text_(text) {}

    [[nodiscard]] char peek() const { return pos_ < text_.size() ? text_[pos_] : '\0'; }

    char advance() {
        const char token = peek();
        if (pos_ < text_.size()) {
            ++pos_;
        }
        return token;
    }

    [[nodiscard]] bool at_end() const { return pos_ == text_.size(); }

private:
    std::string_view text_;
    std::size_t pos_ = 0;
};

The grammar answers the three questions. Its node_type, what a parse builds, is int, since this grammar computes as it goes. lbp gives + and - the power 10 and * the power 20. The nud of a digit is its value, and the nud of - asks the parser for an operand with the power 30, above every binary operator, so that the minus takes only the next number. The led of a binary operator asks for the right operand with the operator’s own power, then combines the two:

/** Integers of one digit, a unary minus, and the binary `+`, `-` and `*`. */
struct arithmetic {
    using node_type = int;

    /** How tightly `token` binds the expression on its left: 0 ends an expression. */
    [[nodiscard]] static int lbp(char token) {
        switch (token) {
            case '+':
            case '-': return 10;
            case '*': return 20;
            default: return 0;
        }
    }

    /** What `token` means at the start of an expression: a digit, or a minus sign. */
    template <class Parser>
    pratt::result<int> nud(Parser& p, char token) const {
        if (token >= '0' && token <= '9') {
            return token - '0';
        }
        if (token == '-') {
            const pratt::result<int> operand = p.expression(30);
            if (operand.has_error()) {
                return operand;
            }
            return -*operand;
        }
        if (token == '\0') {
            return pratt::error::unexpected_end;
        }
        return pratt::error::unexpected_token;
    }

    /** What `token` means after the expression `left`: a binary operator. */
    template <class Parser>
    pratt::result<int> led(Parser& p, char token, int left) const {
        const pratt::result<int> right = p.expression(lbp(token));
        if (right.has_error()) {
            return right;
        }
        switch (token) {
            case '+': return left + *right;
            case '-': return left - *right;
            default: return left * *right;
        }
    }

    // NOLINTEND(misc-no-recursion)
};

pratt::parse parses a whole input with a grammar, and returns a pratt::result<int>, the value or the error:

int main() {
    const arithmetic grammar;
    for (const std::string_view text : {"1+2*3", "2*3+4", "8-2-1", "-2*3", "1+", "1+*2", "12"}) {
        const pratt::result<int> value = pratt::parse(grammar, characters(text));
        if (value.has_error()) {
            std::cout << text << " -> error: " << value.error().message() << '\n';
        } else {
            std::cout << text << " -> " << *value << '\n';
        }
    }
}

It prints:

1+2*3 -> 7
2*3+4 -> 10
8-2-1 -> 5
-2*3 -> -6
1+ -> error: unexpected end of input
1+*2 -> error: unexpected token
12 -> error: trailing input after expression

1+2*3 and 2*3+4 both multiply first, whatever their order, since * binds more tightly than +. 8-2-1 is 5, (8-2)-1, because the led of the first - asks for its right operand with the power 10, and the second -, of the same power, is not greater, so it ends that operand and takes 8-2 as its own left one: an operator that asks with its own power is left-associative. -2*3 negates only the 2, as the minus asks with 30. The last three fail, each with an error of pratt’s: the input ends where an operand was due, a * stands where no nud has a meaning for it, and the 2 of 12, a second number with no operator before it, is input left over after a whole expression.

Requirements

  • A C++20 compiler. pratt’s tests and examples are built and run on every native lane of webcpp’s continuous integration, on emscripten under node, and for wasm32-wasip2 and wasm32-wasip3 with the Clang of wasi-sdk, under wasmtime (Testing).

  • Boost’s headers, 1.92 or newer. The engine uses Boost.System, for result and the error codes, and the calculator Boost.Decimal, for its default number type. pratt is header-only, and nothing of Boost is compiled for it.

  • webcpp’s configuration, config, whose <webcpp/config.hpp> every header of pratt includes first. It is header-only too.

  • Exceptions are the program’s choice. pratt throws nothing of its own, and no header of it throws, tries or catches, so the same headers serve a build with exceptions and one without. webcpp builds pratt with exceptions on every target, and its lint compiles every public header, and every test and example, without them; whether a program uses them is up to whoever builds it. A program without exceptions defines boost::throw_exception, which Boost.System’s result names, as xactor’s page shows.

  • RTTI is never restricted. Every target is built with it, and pratt asks no program to turn it off.

  • Nothing else: no threads, no clock, no global or static mutable state, and no allocation in the engine (Allocation).

Reading this page

<webcpp/pratt.hpp> includes the engine, <webcpp/pratt/errors.hpp> and <webcpp/pratt/parser.hpp>, whose names are in the namespace webcpp::pratt. The calculator, <webcpp/pratt/calculator.hpp>, is included by its own name, so that a program of the engine alone does not read Boost.Decimal; its names are in webcpp::pratt::calc. The code of this page writes them pratt::parse and calc::evaluate, as after namespace pratt = webcpp::pratt; and namespace calc = webcpp::pratt::calc;.

Writing a grammar takes the parts a program writes: the token stream, the grammar, the node, and the helpers that write the usual kinds of operator. The calculator shows the calculator, its number types and its environments, and The openui-lang micro-subset and the REPLs a grammar with calls, the seed of a parser of openui-lang, with the two programs that read lines and answer them. Errors lists every error, Limits the depth limit and the stack it protects, and Allocation what allocates and through which allocator. Testing shows how a program tests its grammar, and how pratt itself is tested. The reference closes the page.

Writing a grammar

A parse takes two things of the program’s: a token stream, which gives the tokens, and a grammar, which gives them their meaning. Each is a type that models a concept of the engine, checked when the program compiles, with no base class, no virtual function and no type erasure between them and the engine. What a parse builds is the grammar’s node, which the engine never looks into.

The token stream

A type models pratt::token_stream when it names its tokens token_type and has three functions:

  • peek(), which returns the next token without consuming it;

  • advance(), which consumes the next token and returns it;

  • at_end(), which says whether the input is spent.

The parser reads peek to decide whether the next token extends an expression, calls advance once for each token it hands a nud or a led, and asks at_end once a whole expression is parsed. peek must stay valid at the end of the input, and return there a token whose lbp is 0 and whose nud fails: '\0' in the first grammar, an end token in the calculator.

A stream may hold every token in advance, or lex each one as it is asked for. The characters of the first grammar is the simplest of them, and the calculator’s lexer, calc::stream, is lazy: it holds the source, its position and one token of lookahead, and no vector of tokens exists. The parser holds the stream by value, and moves it in.

The grammar

A type G models pratt::grammar_for<G, S>, a grammar of the tokens of a stream S, when it names its node node_type and has three functions of a token t:

  • lbp(t), its left binding power, an int;

  • nud(p, t), its meaning at the start of an expression;

  • led(p, t, left), its meaning after the expression left, which the engine moves in.

nud and led receive the parser, p, through which they parse their operands, and return a pratt::result<node_type>: the node, or an error. They are usually templates of the parser’s type, which names the grammar and the stream. The engine calls a led only for a token whose lbp is greater than the binding power the expression is parsed with, so a token of lbp 0 never reaches its led when the whole input is parsed with 0.

The parser’s expression(rbp) is what a nud or a led calls for an operand, and two more functions serve one that reads past its own token: p.peek() and p.advance(), the stream’s. The grammar of the openui-lang micro-subset reads the arguments of a call with them.

The node

The node is whatever the grammar builds. The first grammar’s is an int, the value, computed while the parse goes; the grammar of the helpers below builds a string, the S-expression of the parse; the calculator’s node is a value and the name of a variable, so that = knows what it writes (The calculator). A program that keeps a tree makes the node a tree of its own. The engine moves nodes, and does nothing else with them.

The helpers

Most operators come in a few kinds, and the engine writes each kind once, as a function a nud or a led returns. Each takes the parser and a binding power, parses its operands through expression, and calls a function of the grammar’s, make, which builds the node from the operands’ nodes:

Helper Is a What it parses

literal(make, token)

nud

Nothing more: the token is the whole expression, a number or a name.

prefix(p, bp, make)

nud

One operand, after the token, parsed with bp: a sign, a negation.

infix_left(p, bp, left, make)

led

The right operand, parsed with bp, so an operator of the same power ends it: left-associative, 1-2-3 is (1-2)-3.

infix_right(p, bp, left, make)

led

The right operand, parsed with bp - 1, so an operator of the same power extends it: right-associative, 2^3^2 is 2^(3^2).

postfix(left, make)

led

Nothing more: the left expression is the whole operand, as of a factorial.

grouping(p, is_close)

nud

A whole expression, parsed with 0, then the closing token that is_close accepts, or the error expected_closing_token.

This grammar is written with all six, and its node is the S-expression of the parse, (+ 1 (* 2 3)), so that its output shows how each line grouped. Its tokens are characters, as in the first grammar. Its binding powers put the sign below ^, and the postfix ! above everything; the grammar is shown here without its nud and its led, which follow:

/** Digits, `+ - * / ^`, a unary minus, a postfix `!`, and parentheses, as S-expressions. */
struct sexpr {
    using node_type = std::string;

    static constexpr int additive = 10;
    static constexpr int multiplicative = 20;
    static constexpr int sign = 30;
    static constexpr int power = 40;
    static constexpr int factorial = 50;

    [[nodiscard]] static int lbp(char token) {
        switch (token) {
            case '+':
            case '-': return additive;
            case '*':
            case '/': return multiplicative;
            case '^': return power;
            case '!': return factorial;
            default: return 0;
        }
    }

};

Its nud makes a digit a literal, - a prefix operator and ( a grouping. unary and binary are two functions of the program that return the make of a node of one operand and of two:

template <class Parser>
pratt::result<std::string> nud(Parser& p, char token) const {
    if (token >= '0' && token <= '9') {
        return pratt::literal(
            [](char digit) -> pratt::result<std::string> { return std::string(1, digit); },
            token);
    }
    switch (token) {
        case '-': return pratt::prefix(p, sign, unary('-'));
        case '(': return pratt::grouping(p, [](char next) { return next == ')'; });
        case '\0': return pratt::error::unexpected_end;
        default: return pratt::error::unexpected_token;
    }
}

Its led makes + - * / left-associative, ^ right-associative, and ! postfix. Only those tokens have an lbp above 0, so no other token reaches the led:

template <class Parser>
pratt::result<std::string> led(Parser& p, char token, std::string left) const {
    switch (token) {
        case '+':
        case '-':
        case '*':
        case '/': return pratt::infix_left(p, lbp(token), std::move(left), binary(token));
        case '^': return pratt::infix_right(p, power, std::move(left), binary('^'));
        default: return pratt::postfix(std::move(left), unary('!'));
    }
}
int main() {
    const sexpr grammar;
    for (const std::string_view text :
         {"1-2-3", "2^3^2", "1+2*3", "-2^2", "-3!", "2*-3", "(1+2)*3", "(1+2", "1+)", "()"}) {
        const pratt::result<std::string> tree = pratt::parse(grammar, characters(text));
        if (tree.has_error()) {
            std::cout << text << " -> error: " << tree.error().message() << '\n';
        } else {
            std::cout << text << " -> " << *tree << '\n';
        }
    }
}

It prints:

1-2-3 -> (- (- 1 2) 3)
2^3^2 -> (^ 2 (^ 3 2))
1+2*3 -> (+ 1 (* 2 3))
-2^2 -> (- (^ 2 2))
-3! -> (- (! 3))
2*-3 -> (* 2 (- 3))
(1+2)*3 -> (* (+ 1 2) 3)
(1+2 -> error: expected closing token
1+) -> error: unexpected token
() -> error: unexpected token

-2^2 negates the power, since the sign asks for its operand with 30 and ^ binds with 40: the sign’s operand is 2^2. -3! takes the factorial first for the same reason, and 2*-3 shows the same - as a nud where a led stood before it. A grouping parses 1+2 with 0, so the + inside it binds whatever the power of what is outside, and a missing ) is expected_closing_token. A ) where an operand is due has no nud, and is an unexpected token.

Every helper recurses through expression alone, so a grammar written with them routes every token through its nud and its led, and the parser’s depth limit bounds its whole recursion (Limits).

Parsing

pratt::parse(grammar, stream) parses the whole of a stream as one expression, with the power 0, and returns a pratt::result of the grammar’s node: the node, the first error a nud or a led returned, or trailing_input when tokens remain after a whole expression. A third argument, max_depth, bounds the recursion (Limits).

The class it uses is pratt::parser<Grammar, Stream>, which a program can construct itself, to call expression with another power, or to go on reading the stream after one expression. It holds a pointer to the grammar, so the grammar must outlive it, and a temporary grammar does not compile there; parse takes one, since its parser lives only for the call.

An error is whatever a nud or a led returned, passed on unchanged: one of pratt’s (Errors), or an error code of the program’s own category, which the engine never reads.

The calculator

<webcpp/pratt/calculator.hpp> is a grammar on the engine, ready to use: numbers and variables, the arithmetic and comparison operators of C, and assignment, increment and decrement. calc::evaluate(source, env) parses one line and computes its value as it parses, reading and writing its variables in an environment, env, which keeps them from one call to the next. Every operator goes through the engine’s helpers; the calculator adds no parsing of its own.

A program needs the header, and Boost.Decimal’s, whose default number type the calculator computes in:

#include <webcpp/pratt/calculator.hpp>

#include <boost/decimal.hpp>
#include <boost/decimal/charconv.hpp>

#include <array>
#include <iostream>
#include <string_view>

namespace pratt = webcpp::pratt;
namespace calc = webcpp::pratt::calc;

evaluate returns a pratt::result of the environment’s number type, here boost::decimal::decimal64_t. This program writes each line, then its value in Boost.Decimal’s shortest form, or its error:

/** Writes `line`, then its value in Boost.Decimal's shortest form, or its error. */
void print(std::string_view line, const pratt::result<boost::decimal::decimal64_t>& value) {
    std::cout << line << " -> ";
    if (value.has_error()) {
        std::cout << "error: " << value.error().message() << '\n';
        return;
    }
    std::array<char, 48> buffer{};
    const boost::decimal::to_chars_result written =
        boost::decimal::to_chars(buffer.data(), buffer.data() + buffer.size(), *value);
    std::cout << std::string_view(buffer.data(), written.ptr) << '\n';
}

It evaluates a few lines in one environment, calc::environment<>:

calc::environment<> env;
for (const std::string_view line : {
         "43 + 12 + 2 * (44 - 12) ^ 4",
         "0.1 + 0.2",
         "0.1 + 0.2 == 0.3",
         "2 ^ -3",
         "-2 ^ 2",
         "2 ^ 3 ^ 2",
         "7 % 3",
         "1 / 3",
     }) {
    print(line, calc::evaluate(line, env));
}

It prints:

43 + 12 + 2 * (44 - 12) ^ 4 -> 2097207
0.1 + 0.2 -> 0.3
0.1 + 0.2 == 0.3 -> 1
2 ^ -3 -> 0.125
-2 ^ 2 -> -4
2 ^ 3 ^ 2 -> 512
7 % 3 -> 1
1 / 3 -> 0.3333333333333333

The language

A number is digits, then optionally a point and more digits: 42, 0.5. A name is a letter or _, then letters, digits and _. Space, tab, carriage return and line feed separate tokens. The operators, from the loosest to the tightest, with the binding powers the grammar gives them:

Operators Power Groups What it gives

=

10

right

Writes the value on its right into the variable on its left, and gives that value; c = d = 5 sets both.

== != < <= > >=

20

left

1 when the comparison holds, 0 when it does not.

+ -

30

left

The sum and the difference.

* / %

40

left

The product, the quotient and the remainder, whose sign is the left operand’s.

+ - ++ -- before an operand

45

-

The value, its negation; the variable plus or minus one, written back, as its new value.

^

50

right

The left operand to the power of the right one.

++ -- after an operand

70

-

The variable plus or minus one, written back, as its old value.

A sign binds less tightly than ^, so -2 ^ 2 is -4, and ^ groups to the right, so 2 ^ 3 ^ 2 is 512, as the output above shows.

A variable keeps its value in the environment, from one line to the next:

for (const std::string_view line : {
         "price = 19.99",
         "quantity = 3",
         "price * quantity",
         "quantity++",
         "quantity",
         "--quantity",
         "a = b = 2",
         "a + b",
     }) {
    print(line, calc::evaluate(line, env));
}

It prints:

price = 19.99 -> 19.99
quantity = 3 -> 3
price * quantity -> 59.97
quantity++ -> 3
quantity -> 4
--quantity -> 3
a = b = 2 -> 2
a + b -> 4

++ and -- behave as in C, and only on a variable: before it, they give the new value, and after it, the old one. A node of the calculator holds a value and, for a bare variable, its name, which is what =, ++ and -- write. A grouping keeps the name of what it holds, so (a) = 1 writes a and (a)++ increments it, while (1) = 2 has no name to write, and is an error. A variable that no line has written is an error only where it is read: a = 5 writes a name the environment does not hold yet.

Exactness and the number type

The calculator computes in boost::decimal::decimal64_t by default, a decimal floating-point type of 16 significant digits, for exactness: a decimal fraction written in the input, 0.1 or 19.99, is exactly that number, so 0.1 + 0.2 == 0.3 holds and price * quantity is 59.97, not a binary approximation of it. The lexer builds each number in the number type’s own arithmetic, its digits as one integer divided once by ten to the number of its fraction digits, and never through a double.

A literal longer than the type’s precision keeps its first 17 significant digits in decimal64_t and in double (digits10 + 1, or max_digits10 where that is more): a later digit of the integer part multiplies the value by ten, and a later digit of the fraction is dropped. So 1. followed by any number of zeros is exactly 1, a literal up to the type’s largest value is finite, the largest double written out in full included, and 1 followed by 400 zeros is an infinity. A long literal is within one unit in the last place of the nearest decimal64_t, and within a few of the nearest double, whose digits round as they are read. A fraction keeps its value down to the type’s smallest subnormal, and is 0 below it.

An integral power is computed by repeated squaring, in the number type’s own multiplications, for exactness and determinism: every product is exact while it fits the type’s precision, so 43 + 12 + 2 * (44 - 12) ^ 4 is exactly 2097207, and the result is the same bits on every platform, where a library’s pow may round differently from one to the next. Past the precision, each product rounds, and the relative error grows with the exponent, where a correctly rounded pow would not: in double, 1.000000001 ^ 1000000000 differs from the result of std::pow in its ninth significant digit. That is the price of the same bits on every platform.

The same bits assume each compiler’s default for fusing a multiplication and an addition, which every operation of the calculator keeps apart in statements of their own. Built with -ffp-contract=fast or -ffast-math, a compiler may fuse them anyway, and a double result may then differ in its last place from one platform to another.

The power’s sign is the base’s when the exponent is odd. A negative integral exponent is one divided by the positive power, so 2 ^ -3 is 0.125, and 0 ^ 0 is 1. When the positive power overflows, or underflows to 0, pow computes the result instead, so 2 ^ -1074 is the smallest positive double and 0.1 ^ -400 is an infinity. Otherwise only an exponent that is not an integer calls pow.

Division by zero is an error, division_by_zero, for / and for %, and for 0 ^ -1, which divides by zero too. A floating-point type would answer an infinity or a NaN, and the line after would compute on with it; the calculator stops at the line that asked.

Any type that models calc::number serves. It is copyable and constructible from int, has the binary + - / *, the unary - and the six comparisons, and pow, fmod and trunc, found as using std::pow; pow(a, b) finds them, each returning the type itself. Boost.Decimal’s types model it, and so do float, double and long double. The number type is the environment’s: calc::environment<double> computes in double.

calc::environment<double> env;
for (const std::string_view line : {
         "43 + 12 + 2 * (44 - 12) ^ 4",
         "0.1 + 0.2",
         "0.1 + 0.2 == 0.3",
         "2 ^ -3",
         "2 ^ 60",
         "2.25 ^ 0.5",
         "1 / 0",
     }) {
    print(line, calc::evaluate(line, env));
}

It prints, in the shortest form of std::to_chars:

43 + 12 + 2 * (44 - 12) ^ 4 -> 2097207
0.1 + 0.2 -> 0.30000000000000004
0.1 + 0.2 == 0.3 -> 0
2 ^ -3 -> 0.125
2 ^ 60 -> 1152921504606846976
2.25 ^ 0.5 -> 1.5
1 / 0 -> error: division by zero

The integral powers are still exact, 2 ^ 60 included, since repeated squaring multiplies powers of two, which double holds exactly. The decimal fractions are not: 0.1 and 0.2 are the nearest binary fractions, whose sum is not the nearest one to 0.3. 2.25 ^ 0.5 is not an integral power, and is the result of std::pow. Its exact value, 1.5, is a double, so any pow that errs by less than an ulp returns it exactly, on every standard library.

The environment

The environment is where the variables live, and its type is the program’s choice. A type E models calc::environment_for when its number_type models calc::number and it has two functions:

  • lookup(name), which returns a pointer to the value of name, or nullptr when the name is not set;

  • assign(name, value), which sets it and returns a pratt::result<void>, so that it can refuse, a storage that is full for instance, with an error of its own.

The name is a std::string_view into the line, so an environment that keeps a name copies it. calc::environment<Number, Allocator> is the calculator’s own, a std::map whose nodes and keys allocate through the program’s allocator (Allocation). This one is a program’s own, two variables in fixed storage, in double:

/** Two variables of names up to 8 characters, in fixed storage, computed in double. */
class two_variables {
public:
    using number_type = double;

    [[nodiscard]] const double* lookup(std::string_view name) const {
        for (std::size_t i = 0; i < used_; ++i) {
            if (slots_[i].name() == name) {
                return &slots_[i].value;
            }
        }
        return nullptr;
    }

    pratt::result<void> assign(std::string_view name, double value) {
        for (std::size_t i = 0; i < used_; ++i) {
            if (slots_[i].name() == name) {
                slots_[i].value = value;
                return {};
            }
        }
        if (used_ == slots_.size() || name.size() > slot::capacity) {
            return boost::system::errc::make_error_code(boost::system::errc::not_enough_memory);
        }
        slot& fresh = slots_[used_++];
        std::ranges::copy(name, fresh.characters.begin());
        fresh.length = name.size();
        fresh.value = value;
        return {};
    }

private:
    struct slot {
        static constexpr std::size_t capacity = 8;

        std::array<char, capacity> characters{};
        std::size_t length = 0;
        double value = 0;

        [[nodiscard]] std::string_view name() const { return {characters.data(), length}; }
    };

    std::array<slot, 2> slots_{};
    std::size_t used_ = 0;
};

static_assert(calc::environment_for<two_variables>);

It refuses a third name with errc::not_enough_memory, an error of Boost.System’s generic category, which the calculator returns unchanged:

two_variables env;
for (const std::string_view line : {
         "width = 3",
         "height = 4",
         "width * height",
         "width = 5",
         "width * height",
         "depth = 2",
         "depth",
     }) {
    const pratt::result<double> value = calc::evaluate(line, env);
    std::cout << line << " -> ";
    if (value.has_value()) {
        std::cout << *value << '\n';
    } else if (value.error() == boost::system::errc::not_enough_memory) {
        std::cout << "refused: the environment is full\n";
    } else {
        std::cout << "error: " << value.error().message() << '\n';
    }
}

It prints:

width = 3 -> 3
height = 4 -> 4
width * height -> 12
width = 5 -> 5
width * height -> 20
depth = 2 -> refused: the environment is full
depth -> error: undefined variable

Replacing width is no new name, so it succeeds when the storage is full; depth is refused, and stays unset, so reading it is undefined_variable. Nothing in this environment allocates, and so nothing in these evaluations does.

Errors, in reading order

Every error of the calculator is a value of pratt::result, one of pratt’s errors (Errors) or the environment’s own:

for (const std::string_view line : {
         "1 / 0",
         "5 % 0",
         "total + 1",
         "5 = 3",
         "3++",
         "(1 + 2",
         "1 2",
         "1 +",
         "1 $ 2",
     }) {
    print(line, calc::evaluate(line, env));
}

It prints:

1 / 0 -> error: division by zero
5 % 0 -> error: division by zero
total + 1 -> error: undefined variable
5 = 3 -> error: invalid assignment target
3++ -> error: invalid assignment target
(1 + 2 -> error: expected closing token
1 2 -> error: trailing input after expression
1 + -> error: unexpected end of input
1 $ 2 -> error: invalid character

The lexer is lazy: it reads one token ahead, and the parse reads the tokens in order, so the first error in reading order is the one returned. A token’s error is found when the token is read, and an operator’s own error, an undefined operand or a zero divisor, once the operator has its operands. A character that is no token, such as $, is a token of its own, of the highest binding power, whose nud and led are invalid_character, so the engine reaches it wherever it stands, even after a whole expression, and 1 $ 2 is invalid_character rather than trailing_input.

A write made before the error stays made: nothing is undone.

print("i = 1", calc::evaluate("i = 1", env));
print("i++ $", calc::evaluate("i++ $", env));
print("i", calc::evaluate("i", env));
print("x + 1 $", calc::evaluate("x + 1 $", env));

It prints:

i = 1 -> 1
i++ $ -> error: invalid character
i -> 2
x + 1 $ -> error: invalid character

i++ $ increments i before it reads the $, so the line fails and i is 2. x + 1 $ is invalid_character although x is undefined: + finds that its left operand has no value only once it has its right one, and reading the right one reads the $ first. 5 = 3 $ is invalid_assignment_target, since = checks its target before it reads its right operand. A program that needs a line to write all or nothing evaluates it in a copy of the environment, and keeps the copy when the line succeeds.

The openui-lang micro-subset and the REPLs

openui-lang is a language that describes a view as assignments of calls, such as root = Stack(). A micro-subset of it, among pratt’s examples, is the seed of a future parser of openui-lang built on the engine: it proves that the engine parses its shape, calls with positional and named arguments, and assignments, with no recursive descent around it. Its grammar is example/openui/grammar.hpp, shared by a REPL and by a test.

The grammar

The micro-subset has identifiers, integers, strings in double quotes without escapes, and ( ) , : =. Its node is the S-expression of the parse, a std::string: root = Stack(gap: 4) is (= root (call Stack (: gap 4))).

Token nud led lbp

identifier, integer

itself

0

string

itself, quoted

0

(

a grouping

a call, (call f a ...)

90

:

a pair (: name value), left-associative

20

=

(= target value), right-associative

10

( holds both of Pratt’s denotations. At the start of an expression it is a grouping, the helper’s; after an expression, it calls what stands on its left, with the highest power, so Stack(1, 2)(3) calls the result of the first call. , and ) have the power 0, so each ends the expression of an argument, and the led reads them itself:

template <class Parser>
pratt::result<node_type> led(Parser& p, const token& t, node_type left) const {
    switch (t.type) {
        case kind::assign:
            return pratt::infix_right(p, assign_bp, std::move(left), detail::binary("="));
        case kind::colon:
            return pratt::infix_left(p, pair_bp, std::move(left), detail::binary(":"));
        case kind::lparen: return call(p, std::move(left));
        case kind::invalid: return pratt::error::invalid_character;
        case kind::unterminated: return pratt::error::unexpected_end;
        default: return pratt::error::unexpected_token;
    }
}

The call’s led reads its arguments in a loop, each one a whole expression of the engine’s, until the ):

template <class Parser>
static pratt::result<node_type> call(Parser& p, node_type callee) {
    node_type node = "(call " + std::move(callee);
    if (p.peek().type == kind::rparen) {
        p.advance();
        return std::move(node) + ")";
    }
    while (true) {
        pratt::result<node_type> argument = p.expression(0);
        if (argument.has_error()) {
            return argument;
        }
        node += ' ';
        node += *argument;
        const kind next = p.peek().type;
        if (next == kind::rparen) {
            p.advance();
            return std::move(node) + ")";
        }
        if (next != kind::comma) {
            return pratt::error::expected_closing_token;
        }
        p.advance();
    }
}

A character that is no token, and a " that the line does not close, are tokens of their own with the highest power, as in the calculator, so the engine reaches them wherever they stand: the first is invalid_character, the second unexpected_end. The node allocates, since it is a string; it is the examples’ output format, and the parser of openui-lang to come defines a tree of its own, under the rule of Allocation.

The REPLs

Two programs read lines from their standard input and answer each one: openui_repl parses it with the micro-subset’s grammar and writes its S-expression, and calculator_repl evaluates it with the calculator, in one environment for the whole session. Both share one loop, in example/repl.hpp, which drops the carriage return of a line that ends in one, skips a line that is empty or holds only spaces or tabs, and ends at the line exit, exactly, or at the end of the input, with the status 0:

template <class ProcessLine>
int repl(std::istream& in, std::ostream& out, ProcessLine process_line) {
    std::string line;
    while (std::getline(in, line)) {
        if (!line.empty() && line.back() == '\r') {
            line.pop_back();
        }
        if (line == "exit") {
            return 0;
        }
        if (line.find_first_not_of(" \t") == std::string::npos) {
            continue;
        }
        process_line(std::string_view(line), out);
    }
    return 0;
}

openui_repl hands the loop its grammar’s parse, and writes error:, a space and the error’s message when a line fails:

int main() {
    return webcpp::example::openui::repl(std::cin, std::cout);
}

Its build feeds it openui_repl.input, a session whose tenth line ends in a carriage return and a line feed, whose eleventh holds only four spaces, whose twelfth only a tab, a space and a tab, and whose thirteenth is exit and a space, a name to parse rather than the end:

root = Stack()
root = Stack(spacing: 8, Button(label: "Pay"), Text("Total"))
view = Row(Button(label: "ok"), gap: 4)
Stack(1, 2)(3)
(x)
Stack(1
= 3
root = Stack(label: "unterminated)
root @ 3
Text("crlf")
    
	 	
exit 
exit
ignored

It prints, one line for each line it read, and stops at exit, before ignored:

(= root (call Stack))
(= root (call Stack (: spacing 8) (call Button (: label "Pay")) (call Text "Total")))
(= view (call Row (call Button (: label "ok")) (: gap 4)))
(call (call Stack 1 2) 3)
x
error: expected closing token
error: unexpected token
error: unexpected end of input
error: invalid character
(call Text "crlf")
exit

openui_repl_eof is the same program on another input, whose lines end in a carriage return and a line feed, whose second line is empty, and whose last line, Stack(a), has no line feed after it and no exit:

a = 1

Stack(a)

It reads that last line, then ends at the end of its input:

(= a 1)
(call Stack a)

calculator_repl keeps one calc::environment<> for the session, and writes each value in Boost.Decimal’s shortest form:

int main() {
    calc::environment<> env;
    return webcpp::example::repl(
        std::cin, std::cout, [&env](std::string_view line, std::ostream& out) {
            const webcpp::pratt::result<calc::environment<>::number_type> value =
                calc::evaluate(line, env);
            if (value.has_error()) {
                out << "error: " << value.error().message() << '\n';
                return;
            }
            print(*value, out);
            out << '\n';
        });
}

Its session, calculator_repl.input, holds the values and the errors of The calculator, a division by zero, and, just before exit, a line of ten thousand ( and a 1, left out here:

a = 43 + 12 + 2 * ( 44 - 12 ) ^ 4
a
2 ^ 3 ^ 2
-2 ^ 2
b = a / 2 - 100
b > 1000000
b == a / 2 - 100
10 % 3
i = 0
i++
i
++i
--i
i--
i
c = d = 5
c + d
1.5 * 2
x + 1
5 = 3
3++
( 1 + 2
1 2
1 +
1 $ 2

a = 2
a * 3
1 / 0
exit 
exit
ignored

It prints, the line of ten thousand ( answering error: expression nested too deeply instead of overflowing the stack (Limits):

2097207
2097207
512
-4
1048503.5
1
1
1
0
0
1
2
1
1
0
5
10
3
error: undefined variable
error: invalid assignment target
error: invalid assignment target
error: expected closing token
error: trailing input after expression
error: unexpected end of input
error: invalid character
2
6
error: division by zero
error: expression nested too deeply
error: undefined variable

Errors

pratt reports every failure as a value, and throws nothing of its own. Every operation that can fail, every nud and every led returns a pratt::result<T>, which is Boost.System’s result: a T, or the boost::system::error_code of the failure. pratt’s own errors are the enumerators of pratt::error, in a category of their own, pratt::error_category(), whose name is webcpp.pratt. An error converts to an error_code and compares with one, so a program checks a failure as parsed.error() == pratt::error::trailing_input, and writes it with parsed.error().message().

Enumerator Value Message When

unexpected_token

1

unexpected token

A token stands where the grammar has no nud or led for it: ) where an operand is due, = 3 in the micro-subset.

unexpected_end

2

unexpected end of input

The input ends where a token was still due: 1 +; an unclosed string of the micro-subset.

expected_closing_token

3

expected closing token

A grouping, or a call of the micro-subset, misses its ): (1 + 2.

trailing_input

4

trailing input after expression

Tokens remain after a whole expression: 1 2. parse returns it.

invalid_assignment_target

5

invalid assignment target

=, ++ or -- of the calculator on what is not a variable: 5 = 3, 3++, (1) = 2.

undefined_variable

6

undefined variable

The calculator reads a variable that the environment does not hold.

invalid_character

7

invalid character

A character that starts no token: 1 $ 2, or ! without = in the calculator.

nesting_too_deep

8

expression nested too deeply

A parse would nest deeper than its max_depth (Limits).

division_by_zero

9

division by zero

The calculator’s / or % by zero, or 0 ^ -1.

The values are fixed: a value is never reused for another meaning, so a value stored or sent keeps its meaning from one version of pratt to the next, and 0 is no error.

The engine and its helpers produce nesting_too_deep, trailing_input and expected_closing_token themselves; every other error is a grammar’s choice, and the enumerators are there for a grammar to use, as the calculator and the micro-subset do. A grammar, or an environment of the calculator, may return an error of its own category instead, and pratt passes it on unchanged, as The environment shows.

A grammar returns an error into its result as return pratt::error::unexpected_token;. When its node type converts from an error too, as a Boost.Multiprecision number does, result refuses that conversion as ambiguous, and the grammar builds the error in place, as the engine and the calculator always do: return {boost::system::in_place_error, pratt::error::unexpected_token};.

Limits

Pratt parsing recurses: each operand that a nud or a led asks for is a call of expression, below the calls of the nud and the led that asked, so every level of nesting in the input costs stack. A line of ten thousand ( would nest ten thousand calls, and overflow the stack: a crash, not an error.

The depth limit

The parser counts its active expression calls, and refuses the call that would make them more than its limit, max_depth, with nesting_too_deep, before it reads a token. So the stack never grows past what the limit allows, and the input that would have overflowed it is an error like any other. max_depth is an argument of the parser’s constructor, of parse and of calc::evaluate, and its default is pratt::default_max_depth, which is 64. A negative max_depth behaves as 0, which refuses every parse.

A 1 inside a number of parentheses is that number plus one calls deep:

calc::environment<> env;
std::cout << "default_max_depth: " << pratt::default_max_depth << '\n';

std::cout << "63 pairs: ";
print(calc::evaluate(nested(63), env));

std::cout << "64 pairs: ";
print(calc::evaluate(nested(64), env));

std::cout << "64 pairs, max_depth 128: ";
print(calc::evaluate(nested(64), env, 128));

std::cout << "10000 opening parentheses: ";
print(calc::evaluate(std::string(10000, '('), env));

It prints:

default_max_depth: 64
63 pairs: 1
64 pairs: error: expression nested too deeply
64 pairs, max_depth 128: 1
10000 opening parentheses: error: expression nested too deeply

The limit bounds the recursion through expression alone. That is the whole recursion of a grammar whose nuds and leds parse every operand through expression or the helpers, as the calculator’s and the micro-subset’s do, the micro-subset’s argument loop included. A nud or a led that recurses through functions of its own, without calling expression, is not bounded by it, and bounds that recursion itself.

The stack each level costs

The default is measured, not chosen. The measurement is the deepest nesting that the calculator evaluated without a trap, over calc::environment<>, with wasi-sdk 34 (clang 23.1.0) and wasmtime 47.0.3, and the stack divided by that depth, for three shapes of N operators or (. It was taken on wasm’s default stack of 64 KiB, in a debug build (-O0 -fno-inline) on wasip2 and wasip3, and in a build with -O2 on wasip2; natively in a debug build, on a stack of 8 MiB:

Shape, N operators or ( wasip2, wasip3 -O0 wasip2 -O2 native -O0, 8 MiB

((...(1)...))

232 (282 B)

450 (146 B)

16854 (497 B)

- - ... - 1

263 (249 B)

450 (146 B)

18016 (465 B)

2 ^ 2 ^ ... ^ 2

240 (273 B)

405 (162 B)

12741 (657 B)

Of these three, the shape that costs the most is a grouping, by a few bytes a level. Over every shape of the calculator’s grammar and of openui-lang’s, the deepest debug parse that wasm’s default stack holds is at least 203 calls: openui-lang’s call shape, on wasip2. The calculator’s least is 214, a grouping with a pow inside. The default is the largest power of two at most half of that: half of 203 is 101, hence 64. A parse at the default depth uses at most half the stack, in the worst shape and the slowest build, on every target pratt is tested on; a build with -O2 and a native stack hold far more.

In a debug build on wasm, a level of grouping costs 272 bytes, a level of ^ 256 and a level of sign 240, plus at most about 7.5 KiB that a parse needs at any depth, its heaviest operand, a pow, included.

Raising the limit

A program that raises the limit raises its stack with it, by the cost of a level times the levels it adds. On wasm, the stack is set when the program is linked, with the linker’s flag -Wl,-z,stack-size= and the size in bytes. A program that evaluates groupings 1024 deep in a debug build needs about 1024 times 272 bytes and 7.5 KiB, about 280 KiB, so it links with a stack of 1 MiB, which keeps the margin the default keeps:

clang++ --target=wasm32-wasip2 -std=c++20 \
    -Wl,-z,stack-size=1048576 \
    program.cpp -o program.wasm

and passes max_depth to every call: calc::evaluate(line, env, 1024). A b2 project adds the flag to its target’s requirements:

<linkflags>-Wl,-z,stack-size=1048576

Natively, the stack is the thread’s. The main thread’s is the system’s, commonly 8 MiB on Linux and macOS (ulimit -s shows it), and a thread a program creates takes the size it is created with, through pthread_attr_setstacksize or Boost.Thread’s boost::thread::attributes::set_stack_size. The measurement above holds 12741 levels of ^ on 8 MiB in a debug build, so a native program has room for a limit far above the default without changing its stack.

Allocation

A webcpp library avoids dynamic allocation as far as its job allows, and lets its user choose the allocator of what it does allocate. In pratt:

  • The engine allocates nothing. A parser holds a pointer to its grammar, its stream and a count of its active expression calls. What a parse allocates is what the grammar’s nodes and stream allocate: nothing for the calculator’s, and a string for each node of the S-expression grammars of this page’s examples, their output format.

  • The calculator’s parse allocates nothing. Its lexer is lazy and holds a view of the line; its tokens and its nodes are views and numbers.

  • The environment allocates through its Allocator. calc::environment<Number, Allocator> allocates through it alone: in assign, when a name is new, the map’s node and the name’s characters when the name is longer than the string’s own small buffer; and, with a standard library whose map allocates when it is built, as MSVC’s does, then. Replacing the value of a name it holds and reading one allocate nothing. An environment of the program’s own allocates what it chooses, and the one of The environment nothing at all.

  • A Number that allocates does so through its own allocator. decimal64_t and double allocate nothing. A type whose arithmetic allocates, Boost.Multiprecision’s mpfr_float with et_off for instance, allocates through the allocator it was built with, which the calculator never sees.

So, for a Number that allocates nothing, the environment’s allocator is the calculator’s only source of memory. calc::environment takes any allocator: the map’s allocator is Allocator rebound to the map’s value type, and the key’s is Allocator rebound to char, so an allocator written for any type serves, a polymorphic one included. This one counts what it allocates, and takes the memory from std::allocator:

/** What a counting allocator has done, in an object its program owns. */
struct tally {
    std::size_t allocations = 0;
    std::size_t deallocations = 0;
};

/** An allocator that counts into a tally, and takes its memory from std::allocator. */
template <class T>
class counting {
public:
    using value_type = T;

    explicit counting(tally* count) noexcept : count_(count) {}

    template <class U>
    explicit counting(const counting<U>& other) noexcept : count_(other.count()) {}

    T* allocate(std::size_t n) {
        ++count_->allocations;
        return std::allocator<T>{}.allocate(n);
    }

    void deallocate(T* pointer, std::size_t n) noexcept {
        ++count_->deallocations;
        std::allocator<T>{}.deallocate(pointer, n);
    }

    [[nodiscard]] tally* count() const noexcept { return count_; }

    template <class U>
    bool operator==(const counting<U>& other) const noexcept {
        return count_ == other.count();
    }

private:
    tally* count_;
};

An environment that allocates through it evaluates five lines:

tally count;
{
    calc::environment<boost::decimal::decimal64_t, counting<char>> env{counting<char>(&count)};
    for (const std::string_view line : {"x = 6", "y = 7", "x * y + 1", "x = 8", "x * y"}) {
        const std::size_t before = count.allocations;
        const bool evaluated = calc::evaluate(line, env).has_value();
        std::cout << line << (evaluated ? "" : " failed") << " -> "
                  << (count.allocations > before ? "allocated" : "allocated nothing") << '\n';
    }
}
std::cout << "environment destroyed -> deallocations == allocations: "
          << (count.deallocations == count.allocations ? "yes" : "no") << '\n';

It prints:

x = 6 -> allocated
y = 7 -> allocated
x * y + 1 -> allocated nothing
x = 8 -> allocated nothing
x * y -> allocated nothing
environment destroyed -> deallocations == allocations: yes

Each line prints what it added: x and y are new names, which allocate through the allocator, while evaluating x * y + 1 and assigning x again allocate nothing, and the environment gives back all it allocated when it is destroyed. How many allocations a new name takes is the standard library’s: a node of the map, the name’s characters when they do not fit the string’s small buffer, and, in a debug build of MSVC’s library, the proxies it keeps for its checked iterators.

pratt’s own test of allocation replaces the global operator new for its program alone, and counts every call: none across a session of the calculator over an environment in fixed storage, and over calc::environment only what its allocator allocated (Testing).

Testing

A test of a grammar checks that each input gives the node it should, and each wrong input the error it should. In pratt both are values, a pratt::result of the node or of the error, and nothing in a parse depends on the world outside the test: no clock, no thread, no global state, and no exception to catch. A parse of the same input by the same grammar gives the same result every time.

Testing a grammar

The test below checks the grammar of the openui-lang micro-subset (The openui-lang micro-subset and the REPLs), with Boost.Core’s lightweight_test, the framework of one header that pratt’s own tests use. It includes the engine, the framework and the grammar:

#include <webcpp/pratt.hpp>

#include <boost/core/lightweight_test.hpp>

#include <string>
#include <string_view>

#include "openui/grammar.hpp"

namespace pratt = webcpp::pratt;
namespace openui = webcpp::example::openui;

A helper parses one line, with the default depth limit unless the test names another:

/** The parse of `line` by the micro-subset's grammar, as deep as `max_depth` allows. */
pratt::result<std::string> parsed(std::string_view line, int max_depth = pratt::default_max_depth) {
    return pratt::parse(openui::grammar{}, openui::stream(line), max_depth);
}

A test of trees compares the node of each line with the S-expression it should be: how = and calls group.

void assignment_is_right_associative_and_a_call_binds_tightest() {
    BOOST_TEST_EQ(parsed("a = b = 1").value(), "(= a (= b 1))");
    BOOST_TEST_EQ(parsed("Stack(1, 2)(3)").value(), "(call (call Stack 1 2) 3)");
    BOOST_TEST_EQ(parsed("a = Text(gap: 4)").value(), "(= a (call Text (: gap 4)))");
}

A test of errors compares the error of each wrong line with an enumerator of pratt::error, and checks the depth limit on both of its sides, with a limit small enough to write the input by hand:

void every_error_is_a_value() {
    BOOST_TEST(parsed("Stack(1").error() == pratt::error::expected_closing_token);
    BOOST_TEST(parsed("= 3").error() == pratt::error::unexpected_token);
    BOOST_TEST(parsed("root @ 3").error() == pratt::error::invalid_character);
    BOOST_TEST(parsed("a b").error() == pratt::error::trailing_input);
}

void the_depth_is_bounded() {
    BOOST_TEST(parsed("((x))", 3).has_value());
    BOOST_TEST(parsed("(((x)))", 3).error() == pratt::error::nesting_too_deep);
}

main calls the cases and returns boost::report_errors(), the program’s status:

int main() {
    assignment_is_right_associative_and_a_call_binds_tightest();
    every_error_is_a_value();
    the_depth_is_bounded();
    return boost::report_errors();
}

When every check holds, the program prints No errors detected. on its standard error, nothing on its standard output, and returns 0. A check that fails prints its expression, its values, its file and its line on the standard error, and main returns 1. .value() of a result that holds an error throws, or, built without exceptions, calls boost::throw_exception, which ends the program: the test stops there rather than compare a node that is not there.

How pratt is tested

pratt’s tests are lightweight_test programs, one per subject, under test/, each declared in test/Jamfile with webcpp’s webcpp.run:

Test What it checks

errors

The values of pratt::error, which are fixed, the category’s name and messages, and the conversion and the comparison with an error_code.

engine

The engine with a grammar of the test’s own, whose binding powers come from a table: precedence, associativity, prefix and postfix positions, the errors the engine and its helpers report, the order of the nud and led calls, the depth limit at its default, one level beyond and at a custom value, and what models token_stream and grammar_for.

sexpr

The engine through a grammar whose tokens are characters and whose nodes are strings, with nothing of the calculator.

lexer

The calculator’s number concept and its lazy lexer, over decimal64_t and double: the kinds, texts and values of the tokens, numbers built in the number type’s own arithmetic and past its range, the invalid token, whitespace, and the end token that stays.

calculator

The calculator over decimal64_t and double: values and errors, exact integral powers, division by zero, assignment and increment through parentheses, an invalid character wherever it stands, the first error in reading order, and the environments a user brings, one in fixed storage whose assign fails and one whose allocator counts.

allocation

The global operator new, replaced for this program alone: no call across a session over an environment in fixed storage, and over calc::environment only those of its allocator.

depth

The default depth limit with the calculator’s own frames: a grouping, a chain of signs and a chain of ^ exactly default_max_depth deep are evaluated and one level more is refused, twice the default still fits the stack, and a line of ten thousand ( is refused. On wasip2 and wasip3, in a debug build with wasm’s default stack, it is the check of the measurement of Limits.

openui

The openui-lang micro-subset, through the grammar it shares with its REPL: every line of its session, ( as a grouping and as a call, the right-associative =, and the errors of its lazy lexer.

Each case is a function named after the rule it checks, such as a_decimal_fraction_is_exact_in_decimal, and main calls them all and returns boost::report_errors(). Beside them, test/Jamfile compiles each public header alone, as the only include of its translation unit, so that a header that leans on an include it does not make fails, naming it. The lint checks the boundary that meta/include-boundaries.json declares: the engine’s headers include neither the calculator nor Boost.Decimal, since the engine knows no grammar and no number type. And example/Jamfile builds every program this page shows, runs it with the .input beside it as its standard input when there is one, and compares what it printed with the .expected beside it; a program that returns anything but 0 fails too.

Targets

test/Jamfile and example/Jamfile declare the targets they are built for, webcpp.targets native emscripten wasip2 wasip3, and b2 builds each program for those alone:

Target How it is built and run

native

Any toolset that is not one of the three below: GCC, Clang and MSVC among them. With exceptions, unless its user asks for exception-handling=off, which links webcpp’s tools/throw_exception.cpp.

emscripten

b2’s toolset emscripten, wasm32, run by node: 16 tests and 11 examples.

wasip2

The toolset clang-wasip2, the Clang of wasi-sdk for wasm32-wasip2, run by wasmtime. With exceptions, as WebAssembly’s exception handling, as on wasip3.

wasip3

The toolset clang-wasip3, for wasm32-wasip3, run by wasmtime. With exceptions, as WebAssembly’s exception handling.

That pratt works without exceptions is checked by the lint, which compiles every public header, and every test and example, which instantiate its templates, with exception-handling=off; a template that none of them instantiates is not checked.

<webcpp/pratt/config.hpp>, which every public header of pratt includes first, defines WEBCPP_PRATT_NO_EXCEPTIONS when webcpp has no exceptions (WEBCPP_HAS_EXCEPTIONS is not defined: the compiler has none, or the program defined WEBCPP_NO_EXCEPTIONS), and a program may define it for pratt alone, to turn pratt’s exceptions off in a build that has them, as Boost.Asio’s BOOST_ASIO_NO_EXCEPTIONS does. pratt raises no exception of its own, so the macro changes nothing in it: it exists so that a program can set it for every webcpp library alike. The tests config, config_developer_defined and config_all_off check the three ways it is defined, and the lint refuses a throw in a public header, even in a template nothing instantiates: an exception goes through boost::throw_exception.

From the root of a webcpp checkout, each of these builds and runs, natively:

b2 libs/pratt/test

The 16 tests: the lightweight_test programs, and each of the 5 public headers compiled alone.

b2 libs/pratt/example

The 11 examples, each built, run, and its output compared with the one beside it.

b2 libs/pratt/test libs/pratt/example toolset=emscripten

The same on emscripten: 16 tests and 11 examples, each run by node, b2’s toolset itself.

b2 libs/pratt/test libs/pratt/example toolset=clang-wasip2 testing.launcher=wasmtime

The same for wasm32-wasip2: 16 tests and 11 examples, each run by wasmtime. toolset=clang-wasip3 does the same for wasm32-wasip3: 16 tests and 11 examples.

The WASI toolsets are b2’s clang, which a user-config.jam registers against wasi-sdk, as xactor’s page shows. The emscripten toolset is registered against emsdk, as trystero’s page shows, and runs a program with node itself, never with a testing.launcher. b2 compares timestamps, so a result that counts is built from scratch, with b2 -a. b2 libs/pratt/test//calculator builds and runs one test alone, while working on it.

The test matrix

The continuous integration runs pratt’s tests and examples in lanes, one toolset each: the native compilers, and wasip2 and wasip3. Each lane builds from scratch and records what b2 built and ran:

b2 -a --dump-tests --out-xml=<lane>.xml toolset=<toolset> libs/pratt/test libs/pratt/example

webcpp’s tools/report/report.py merges the files of every lane into a matrix: a page of the libraries by lanes, and a page of each library’s tests and examples by lanes, where each cell says whether that program passed in that lane, or how it failed, at compile time, at link time or when it ran. The matrix is published with the documentation, at https://webcpporg.github.io/report/.

Reference

Every name pratt declares, in the namespaces webcpp::pratt and webcpp::pratt::calc, from the Doc Comments of its headers: each concept, class, function, type and enumerator with its declaration, what it does, what it asks of its caller and how it fails. The other sections teach the library; this one is where a name is looked up.

Macros

Name Description

WEBCPP_PRATT_NO_EXCEPTIONS

Defined when pratt is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone, to disable pratt’s in a build that has them.

webcpp::pratt

Namespaces

Name

calc

Types

Name Description

parser

A Pratt parser of the tokens of a Stream, by a Grammar.

Type Aliases

Name Description

result

What a parse that can fail returns: a T, or the error_code of the failure.

Enums

Name Description

error

The errors pratt reports, as boost::system::error_code values of the category error_category.

Functions

Name Description

error_category

The category of every error.

grouping

The nud of an opening token: a whole inner expression, then the closing token that is_close accepts.

infix_left

The led of a left-associative infix operator of power bp.

infix_right

The led of a right-associative infix operator of power bp.

literal

The nud of an atom: the token is the whole expression.

make_error_code

Makes the error_code of an error, as its implicit conversion does.

parse

Parses the whole of stream as one expression of grammar.

postfix

The led of a postfix operator: its operand is the whole left expression.

prefix

The nud of a prefix operator, whose operand binds with power bp.

Variables

Name Description

default_max_depth

The deepest nesting of parser::expression calls a parse allows unless told otherwise.

Concepts

Name Description

grammar_for

A grammar of the tokens of S: for every token, its nud (its meaning at the start of an expression), its led (its meaning after a left operand) and its lbp (how tightly it binds that left operand).

token_stream

A source of tokens: the next one is read by peek and consumed by advance, and at_end says whether the input is spent.

webcpp::pratt::calc

Types

Name Description

environment

The calculator’s own environment: a std::map from names to values, whose map and keys allocate through the user’s allocator.

grammar

The calculator’s grammar over an environment: numbers, variables, the arithmetic operators + - * / % ^, the comparisons, =, and ++ and -- before and after a variable.

node

A node of a calculator parse: a value, and the name of a bare variable.

stream

The calculator’s lexer, a lazy webcpp::pratt::token_stream.

token

A token of the calculator’s input.

Enums

Name Description

kind

What a token is.

Functions

Name Description

evaluate

Evaluates source as one expression of the calculator, reading and writing its variables in env.

Concepts

Name Description

environment_for

An environment the calculator reads and writes its variables in.

number

A type the calculator computes in.

webcpp::pratt::calc::environment

The calculator’s own environment: a std::map from names to values, whose map and keys allocate through the user’s allocator.

Synopsis

template<
    number Number = boost::decimal::decimal64_t,
    class Allocator = std::allocator<std::pair<std::string const, Number>>>
class environment;

Description

It allocates through Allocator alone: in assign, when a name is new, the map’s node, and the key’s characters when the name is longer than the string’s small buffer; and when it is constructed, with a standard library whose map allocates then, as MSVC’s does. Assigning a name it holds replaces the value in place, and lookup allocates nothing. For a Number that allocates nothing, such as decimal64_t or double, nothing else that evaluate does allocates, so this environment’s allocator is the calculator’s only source of memory. A Number whose arithmetic allocates, Boost.Multiprecision’s mpfr_float with et_off for instance, allocates through its own allocator.

The key holds a std::basic_string whose allocator is Allocator rebound to char, and the map’s allocator is Allocator rebound to the map’s value type, so any allocator serves, whatever type it is written for: environment<Number, A<char>> and the default are both well formed. The map compares with std::less<>, so a name is looked up as a std::string_view, without building a key.

Type Aliases

Name Description

allocator_type

The allocator the environment was given, which its map and its keys rebind.

number_type

The type of the values.

Member Functions

Name Description

environment [constructor]

Constructors

assign

Sets name to value, inserting the name or replacing its value.

get_allocator

The allocator the environment allocates through.

lookup

Finds the value of name.

Template Parameters

Name Description

Number

The type of the values, boost::decimal::decimal64_t by default.

Allocator

The allocator of the map and of its keys. The default is std::allocator<std::pair<const std::string, Number>>, the map’s own value type: std::allocator rebinds to std::allocator<char>, whose key is std::string, so the default names its own map, and depends only on Number.

See Also

"Allocation", in the guide.

webcpp::pratt::calc::environment::allocator_type

The allocator the environment was given, which its map and its keys rebind.

Synopsis

using allocator_type = Allocator;

webcpp::pratt::calc::environment::number_type

The type of the values.

Synopsis

using number_type = Number;

webcpp::pratt::calc::environment::environment

Constructors

Synopses

Constructs an empty environment with a default-constructed allocator.

environment() = default;

Constructs an empty environment that allocates through allocator.

explicit
environment(Allocator const& allocator);

Parameters

Name Description

allocator

The allocator, rebound for the map and for its keys.

webcpp::pratt::calc::environment::environment

Constructs an empty environment with a default-constructed allocator.

Synopsis

environment() = default;

webcpp::pratt::calc::environment::environment

Constructs an empty environment that allocates through allocator.

Synopsis

explicit
environment(Allocator const& allocator);

Parameters

Name Description

allocator

The allocator, rebound for the map and for its keys.

webcpp::pratt::calc::environment::assign

Sets name to value, inserting the name or replacing its value.

Synopsis

result<void>
assign(
    std::string_view name,
    Number value);

Return Value

Success. A new name allocates its node and its key through the allocator; a failure to allocate is the allocator’s, as in any standard container.

Parameters

Name Description

name

The variable’s name, which the environment copies.

value

Its new value.

webcpp::pratt::calc::environment::get_allocator

The allocator the environment allocates through.

Synopsis

[[nodiscard]]
allocator_type
get_allocator() const noexcept;

Return Value

A copy of it, rebound back to Allocator, as an allocator-aware standard container returns its own.

Note

The return value should not be discarded.

webcpp::pratt::calc::environment::lookup

Finds the value of name.

Synopsis

[[nodiscard]]
Number const*
lookup(std::string_view name) const;

Return Value

A pointer to its value, valid until the name is assigned again or the environment is destroyed, or nullptr when the name is not set.

Note

The return value should not be discarded.

Parameters

Name Description

name

The variable’s name.

webcpp::pratt::calc::grammar

The calculator’s grammar over an environment: numbers, variables, the arithmetic operators + - * / % ^, the comparisons, =, and ++ and -- before and after a variable.

Synopsis

template<environment_for Environment>
class grammar;

Description

^ is right-associative and binds tighter than a unary sign, so -2 ^ 2 == -4; = is right-associative, so c = d = 5 sets both. A comparison gives 1 or 0. ++x gives the new value and x++ the old, as in C. An integral exponent is computed by repeated squaring in Number, so an integer power is exact while it fits the type’s precision; past that, the relative error grows with the exponent, where a correctly rounded pow would not, which is the price of the same bits on every platform. x ^ -n is 1 / x ^ n, or pow’s result when x ^ n overflows or underflows to 0; otherwise only a non-integral exponent calls pow.

The tokens are read one at a time, and the first error found in reading order is the one returned. A token’s error is found when the token is read, and an operator’s when the operator has its operands: = checks its target before it reads its right operand, so 5 = 3 $ gives error::invalid_assignment_target, while + finds an undefined operand, or / a zero divisor, only after its right operand is read, so x + 1 $ gives error::invalid_character even when x is undefined, and so does 1 / 0 $.

A write made before the error stays made: nothing is undone. i++ $ increments i, then gives error::invalid_character, and i++ ) increments it, then gives error::trailing_input.

Type Aliases

Name Description

node_type

What a parse builds.

number_type

The type the calculator computes in, the environment’s.

token_type

The tokens the grammar reads.

Member Functions

Name Description

grammar [constructor]

Constructs the grammar over env.

lbp

How tightly the token t binds the expression on its left.

led

The meaning of the token t after the expression left.

nud

The meaning of the token t at the start of an expression.

Static Data Members

Name Description

additive_bp

The power of binary + and -.

assign_bp

The power of =, the weakest operator.

compare_bp

The power of the six comparisons.

invalid_bp

The power of an invalid character, higher than every other, so that one after a complete operand is read as an operator, whose led gives error::invalid_character, rather than end the expression with error::trailing_input or error::unexpected_end.

multiplicative_bp

The power of *, / and %.

postfix_bp

The power of a postfix ++ or --, the tightest operator.

power_bp

The power of ^.

unary_bp

The power a unary sign and a prefix ++ or -- parse their operand with, below ^.

Template Parameters

Name Description

Environment

The environment the grammar reads and writes, which must outlive the grammar.

See Also

"The calculator", in the guide.

webcpp::pratt::calc::grammar::node_type

What a parse builds.

Synopsis

using node_type = node<number_type>;

webcpp::pratt::calc::grammar::number_type

The type the calculator computes in, the environment’s.

Synopsis

using number_type = Environment::number_type;

webcpp::pratt::calc::grammar::token_type

The tokens the grammar reads.

Synopsis

using token_type = token<number_type>;

webcpp::pratt::calc::grammar::grammar

Constructs the grammar over env.

Synopsis

explicit
grammar(Environment& env);

Parameters

Name Description

env

The environment.

Preconditions

  • env outlives the grammar.

webcpp::pratt::calc::grammar::lbp

How tightly the token t binds the expression on its left.

Synopsis

[[nodiscard]]
int
lbp(token_type const& t) const;

Return Value

Its power, as the constants above give it; 0 for a token that takes no left operand.

Note

The return value should not be discarded.

Parameters

Name Description

t

The token.

webcpp::pratt::calc::grammar::led

The meaning of the token t after the expression left.

Synopsis

template<class Parser>
result<node_type>
led(
    Parser& p,
    token_type const& t,
    node_type const& left) const;

Return Value

The node of the binary operator, of the assignment or of the postfix ++ or --; or the error of the token or of its operands.

Template Parameters

Name Description

Parser

The parser’s type, a webcpp::pratt::parser of this grammar.

Parameters

Name Description

p

The parser.

t

The token.

left

The expression on its left.

webcpp::pratt::calc::grammar::nud

The meaning of the token t at the start of an expression.

Synopsis

template<class Parser>
result<node_type>
nud(
    Parser& p,
    token_type const& t) const;

Return Value

The node: a number, a variable, a grouping, a unary sign, or a prefix ++ or --; or the error of the token or of its operand.

Template Parameters

Name Description

Parser

The parser’s type, a webcpp::pratt::parser of this grammar.

Parameters

Name Description

p

The parser.

t

The token.

webcpp::pratt::calc::grammar::additive_bp

The power of binary + and -.

Synopsis

inline constexpr static
int additive_bp = 30;

webcpp::pratt::calc::grammar::assign_bp

The power of =, the weakest operator.

Synopsis

inline constexpr static
int assign_bp = 10;

webcpp::pratt::calc::grammar::compare_bp

The power of the six comparisons.

Synopsis

inline constexpr static
int compare_bp = 20;

webcpp::pratt::calc::grammar::invalid_bp

The power of an invalid character, higher than every other, so that one after a complete operand is read as an operator, whose led gives error::invalid_character, rather than end the expression with error::trailing_input or error::unexpected_end.

Synopsis

inline constexpr static
int invalid_bp = std::numeric_limits<int>::max();

Description

An error found earlier in reading order is still returned first, and a write made before the character stays made.

webcpp::pratt::calc::grammar::multiplicative_bp

The power of *, / and %.

Synopsis

inline constexpr static
int multiplicative_bp = 40;

webcpp::pratt::calc::grammar::postfix_bp

The power of a postfix ++ or --, the tightest operator.

Synopsis

inline constexpr static
int postfix_bp = 70;

webcpp::pratt::calc::grammar::power_bp

The power of ^.

Synopsis

inline constexpr static
int power_bp = 50;

webcpp::pratt::calc::grammar::unary_bp

The power a unary sign and a prefix ++ or -- parse their operand with, below ^.

Synopsis

inline constexpr static
int unary_bp = 45;

webcpp::pratt::calc::node

A node of a calculator parse: a value, and the name of a bare variable.

Synopsis

template<number Number>
struct node;

Description

A bare name keeps its name, so that =, ++ and -- can write it, and a grouping keeps it too, so (a) = 1 writes a. The name has a value only once the environment holds one: a = 5 can define a, and an undefined name is an error only where it is read.

Data Members

Name Description

has_value

Whether the node has a value: false for a name the environment does not hold.

name

The name of a bare variable, a view into the source; empty for every other node.

value

The value, when has_value says there is one.

Template Parameters

Name Description

Number

The type the calculator computes in.

See Also

"The calculator", in the guide.

webcpp::pratt::calc::node::has_value

Whether the node has a value: false for a name the environment does not hold.

Synopsis

bool has_value = true;

webcpp::pratt::calc::node::name

The name of a bare variable, a view into the source; empty for every other node.

Synopsis

std::string_view name = {};

webcpp::pratt::calc::node::value

The value, when has_value says there is one.

Synopsis

Number value = {0};

webcpp::pratt::calc::stream

The calculator’s lexer, a lazy webcpp::pratt::token_stream.

Synopsis

template<number Number>
class stream;

Description

It holds the source, its position and one token of lookahead, and lexes the next token only when advance consumes the current one.

A number is digits, then optionally . and more digits; it is computed in Number arithmetic alone, never through double, so 0.1 is exactly one tenth in a decimal type. A name is a letter or _, then letters, digits and _. Space, tab, carriage return and line feed separate tokens. Any other character is a token of kind kind::invalid, one character long, which the grammar rejects where it stands.

A number keeps its first digits10 + 1 significant digits, or max_digits10 where that is more, 17 in both decimal64_t and double, as one integer, divided once by ten to the number of its fraction digits. A later digit of the integer part multiplies the value by ten, and a later digit of the fraction is dropped. So a literal of at most digits10 significant digits, with at most 22 fraction digits in double, is the nearest value; a longer one is within one unit in the last place of it in decimal64_t, and may be a few units away in double, whose digits round as they are read. 1. followed by any number of zeros is exactly 1; a literal up to the type’s largest value is finite, the largest double written out in full included, and 1 followed by 400 zeros is infinite; and a fraction keeps its value down to the type’s smallest subnormal, and is 0 below it. A type without std::numeric_limits keeps every digit.

A non-ASCII character is a token of kind kind::invalid per byte, so the parse stops at its first byte with error::invalid_character.

Type Aliases

Name Description

token_type

The tokens the stream yields.

Member Functions

Name Description

stream [constructor]

Constructs the stream of source, and lexes the first token.

advance

Consumes the next token and returns it; at the end, returns the end token again.

at_end

Whether the input is spent: the next token is the end token.

peek

The next token, not consumed; the end token once the input is spent.

Template Parameters

Name Description

Number

The type a number token’s value is.

See Also

"The calculator", in the guide.

webcpp::pratt::calc::stream::token_type

The tokens the stream yields.

Synopsis

using token_type = token<Number>;

webcpp::pratt::calc::stream::stream

Constructs the stream of source, and lexes the first token.

Synopsis

explicit
stream(std::string_view source);

Parameters

Name Description

source

The text to lex.

Preconditions

  • source outlives the stream and its tokens, whose text views it.

webcpp::pratt::calc::stream::advance

Consumes the next token and returns it; at the end, returns the end token again.

Synopsis

token_type
advance();

Return Value

The token consumed.

webcpp::pratt::calc::stream::at_end

Whether the input is spent: the next token is the end token.

Synopsis

[[nodiscard]]
bool
at_end() const;

Return Value

true once only the end token remains.

Note

The return value should not be discarded.

webcpp::pratt::calc::stream::peek

The next token, not consumed; the end token once the input is spent.

Synopsis

[[nodiscard]]
token_type const&
peek() const;

Return Value

The token, valid until the next advance.

Note

The return value should not be discarded.

webcpp::pratt::calc::token

A token of the calculator’s input.

Synopsis

template<number Number>
struct token;

Data Members

Name Description

text

The token’s characters, a view into the source.

type

What the token is.

value

The value of a number token; 0 for every other kind.

Template Parameters

Name Description

Number

The type a number token’s value is.

webcpp::pratt::calc::token::text

The token’s characters, a view into the source.

Synopsis

std::string_view text = {};

webcpp::pratt::calc::token::type

What the token is.

Synopsis

kind type = kind::end;

webcpp::pratt::calc::token::value

The value of a number token; 0 for every other kind.

Synopsis

Number value = {0};

webcpp::pratt::calc::kind

What a token is.

Synopsis

enum class kind : int;

Members

Name Description

number

A number literal, such as 42 or 0.5, whose value the token holds.

name

A variable’s name, such as x or _total2.

plus

+, binary addition or a unary plus.

minus

-, binary subtraction or a unary minus.

star

*, multiplication.

slash

/, division.

percent

%, the remainder of a division.

caret

^, exponentiation.

lparen

(, which opens a grouping.

rparen

), which closes a grouping.

assign

=, assignment.

eq

==, equal.

ne

!=, not equal.

lt

<, less than.

le

<=, less than or equal.

gt

>, greater than.

ge

>=, greater than or equal.

increment

++, a prefix or a postfix increment.

decrement

--, a prefix or a postfix decrement.

invalid

A character the lexer reads as no token, one character long.

end

The end of the input, which an empty text at the end of the source spells.

See Also

"The calculator", in the guide.

webcpp::pratt::calc::kind::number

A number literal, such as 42 or 0.5, whose value the token holds.

Synopsis

number

webcpp::pratt::calc::kind::name

A variable’s name, such as x or _total2.

Synopsis

name

webcpp::pratt::calc::kind::plus

+, binary addition or a unary plus.

Synopsis

plus

webcpp::pratt::calc::kind::minus

-, binary subtraction or a unary minus.

Synopsis

minus

webcpp::pratt::calc::kind::star

*, multiplication.

Synopsis

star

webcpp::pratt::calc::kind::slash

/, division.

Synopsis

slash

webcpp::pratt::calc::kind::percent

%, the remainder of a division.

Synopsis

percent

webcpp::pratt::calc::kind::caret

^, exponentiation.

Synopsis

caret

webcpp::pratt::calc::kind::lparen

(, which opens a grouping.

Synopsis

lparen

webcpp::pratt::calc::kind::rparen

), which closes a grouping.

Synopsis

rparen

webcpp::pratt::calc::kind::assign

=, assignment.

Synopsis

assign

webcpp::pratt::calc::kind::eq

==, equal.

Synopsis

eq

webcpp::pratt::calc::kind::ne

!=, not equal.

Synopsis

ne

webcpp::pratt::calc::kind::lt

<, less than.

Synopsis

lt

webcpp::pratt::calc::kind::le

<=, less than or equal.

Synopsis

le

webcpp::pratt::calc::kind::gt

>, greater than.

Synopsis

gt

webcpp::pratt::calc::kind::ge

>=, greater than or equal.

Synopsis

ge

webcpp::pratt::calc::kind::increment

++, a prefix or a postfix increment.

Synopsis

increment

webcpp::pratt::calc::kind::decrement

--, a prefix or a postfix decrement.

Synopsis

decrement

webcpp::pratt::calc::kind::invalid

A character the lexer reads as no token, one character long.

Synopsis

invalid

Description

A non-ASCII character is several bytes of UTF-8, each a token of this kind, so a parse stops at its first byte.

webcpp::pratt::calc::kind::end

The end of the input, which an empty text at the end of the source spells.

Synopsis

end

webcpp::pratt::calc::evaluate

Evaluates source as one expression of the calculator, reading and writing its variables in env.

Synopsis

template<environment_for Environment>
result<Environment::number_type>
evaluate(
    std::string_view source,
    Environment& env,
    int max_depth = default_max_depth);

Description

For a Number that allocates nothing, such as decimal64_t or double, nothing is allocated but what env allocates to store a name: the lexer, the tokens and the nodes are views and numbers. A Number whose arithmetic allocates, Boost.Multiprecision’s mpfr_float with et_off for instance, allocates through its own allocator.

The first error found in reading order is the one returned, and the writes made before it stay made, as grammar says: a failed evaluation may have assigned variables in env. A caller that needs all or nothing evaluates in a copy of the environment and keeps the copy on success.

Return Value

The expression’s value; the first error of the parse, an error of error, or the error env.assign returned; or error::undefined_variable when the whole expression is a name the environment does not hold.

Template Parameters

Name Description

Environment

The environment’s type, which models environment_for.

Parameters

Name Description

source

The expression, which needs to live only for the call.

env

The environment.

max_depth

The deepest nesting of the engine’s expression calls allowed, as webcpp::pratt::parser takes it: a negative value behaves as 0.

See Also

"The calculator", in the guide.

"Errors", in the guide.

webcpp::pratt::calc::environment_for

An environment the calculator reads and writes its variables in.

Synopsis

template<class E>
concept environment_for = requires(E e, const E ce, std::string_view n, E::number_type v) {
    requires number<typename E::number_type>;
    { ce.lookup(n) } -> std::same_as<const typename E::number_type*>;
    { e.assign(n, v) } -> std::same_as<result<void>>;
};

Description

lookup(name) returns a pointer to the value of name, or nullptr when the name is not set. assign(name, value) sets it, and may fail, a fixed storage that is full for instance, with an error of the environment’s own, which the calculator returns unchanged. The name is a view into the source, so an environment that keeps it copies it.

Template Parameters

Name Description

E

The environment’s type, whose number_type models number.

See Also

"The calculator", in the guide.

"Allocation", in the guide.

webcpp::pratt::calc::number

A type the calculator computes in.

Synopsis

template<class N>
concept number = std::copyable<N> && std::constructible_from<N, int> && requires(const N a, const N b) {
        { a + b } -> std::convertible_to<N>;
        { a - b } -> std::convertible_to<N>;
        { a * b } -> std::convertible_to<N>;
        { a / b } -> std::convertible_to<N>;
        { -a } -> std::convertible_to<N>;
        { a == b } -> std::convertible_to<bool>;
        { a != b } -> std::convertible_to<bool>;
        { a < b } -> std::convertible_to<bool>;
        { a <= b } -> std::convertible_to<bool>;
        { a > b } -> std::convertible_to<bool>;
        { a >= b } -> std::convertible_to<bool>;
        { detail::math::adl_pow(a, b) } -> std::same_as<N>;
        { detail::math::adl_fmod(a, b) } -> std::same_as<N>;
        { detail::math::adl_trunc(a) } -> std::same_as<N>;
    };

Description

It is copyable and constructible from int; it has the binary + - * /, the unary - and the six comparisons; and pow(a, b), fmod(a, b) and trunc(a), found by ADL after using std::pow;, using std::fmod; and using std::trunc;, so boost::decimal’s overloads serve its types and std’s serve the builtin floating-point types.

Note

The three functions must return exactly N, so that the calculator never computes through another type. int does not model it: std’s pow, fmod and trunc of an int return a double. Nor does a type whose functions return a proxy, an expression template such as Boost.Multiprecision’s with et_on; the same type with et_off does.

Template Parameters

Name Description

N

The type the calculator would compute in.

See Also

"The calculator", in the guide.

webcpp::pratt::result

What a parse that can fail returns: a T, or the error_code of the failure.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

template<class T>
using result = boost::system::result<T>;

Description

It is Boost.System’s result. pratt throws nothing of its own: every operation of it that can fail returns one, and so does every nud and led of a grammar. Its error is an error, or an error of any category that a grammar or an environment of the user’s returns, which pratt passes on unchanged.

Note

When T converts from an error, as a Boost.Multiprecision number does, result takes neither implicitly: build the error in place, {boost::system::in_place_error, error::unexpected_token}, as pratt does.

Template Parameters

Name Description

T

The type of the value; void for an operation that returns none.

See Also

"Errors", in the guide.

webcpp::pratt::parser

A Pratt parser of the tokens of a Stream, by a Grammar.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Grammar,
    class Stream>
class parser;

Description

It holds a pointer to the grammar, the stream and the depth of its active expression calls, and allocates nothing.

Its depth limit, max_depth, bounds the recursion through expression alone. That is the whole recursion of a grammar whose nud and led parse every operand through expression or the helpers, as the calculator’s do. A nud or a led that recurses through functions of its own grammar, without calling expression, is not bounded by it, and must bound that recursion itself.

Type Aliases

Name Description

node_type

What the grammar builds.

token_type

The tokens the stream yields.

Member Functions

Name Description

parser [constructor]

Constructors

advance

Consumes the next token and returns it: for a nud or a led that reads past its own token.

expression

Parses the longest expression whose operators bind tighter than rbp.

parse

Parses the whole input as one expression.

peek

The next token, not consumed: for a nud or a led that reads past its own token.

Template Parameters

Name Description

Grammar

A type that models grammar_for of Stream.

Stream

A type that models token_stream.

See Also

"Writing a grammar", in the guide.

"Limits", in the guide.

webcpp::pratt::parser::node_type

What the grammar builds.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

using node_type = Grammar::node_type;

webcpp::pratt::parser::token_type

The tokens the stream yields.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

using token_type = Stream::token_type;

webcpp::pratt::parser::parser

Constructors

Synopses

Declared in <webcpp/pratt/parser.hpp>

Constructs a parser of stream by grammar.

parser(
    Grammar const& grammar,
    Stream stream,
    int max_depth = default_max_depth);

Refused: the parser keeps a pointer to its grammar, which a temporary would leave dangling.

parser(
    Grammar const&& grammar,
    Stream stream,
    int max_depth = default_max_depth) = delete;

Parameters

Name Description

grammar

The grammar.

stream

The tokens.

max_depth

The deepest nesting of expression calls allowed. A negative value behaves as 0, which refuses every parse with error::nesting_too_deep.

Preconditions

  • grammar outlives the parser.

webcpp::pratt::parser::parser

Constructs a parser of stream by grammar.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

parser(
    Grammar const& grammar,
    Stream stream,
    int max_depth = default_max_depth);

Parameters

Name Description

grammar

The grammar.

stream

The tokens.

max_depth

The deepest nesting of expression calls allowed. A negative value behaves as 0, which refuses every parse with error::nesting_too_deep.

Preconditions

  • grammar outlives the parser.

webcpp::pratt::parser::parser

Refused: the parser keeps a pointer to its grammar, which a temporary would leave dangling.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

parser(
    Grammar const&& grammar,
    Stream stream,
    int max_depth = default_max_depth) = delete;

webcpp::pratt::parser::advance

Consumes the next token and returns it: for a nud or a led that reads past its own token.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

token_type
advance();

Return Value

The token consumed.

webcpp::pratt::parser::expression

Parses the longest expression whose operators bind tighter than rbp.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

result<node_type>
expression(int rbp);

Description

The first token’s nud parses the start, and while the next token’s lbp is greater than rbp, that token’s led extends the expression.

Return Value

The expression’s node, the first error of a nud or a led, or error::nesting_too_deep, before any token is read, from the call that would make more than max_depth calls active.

Parameters

Name Description

rbp

The right binding power of the operator that asks for the expression, 0 for a whole one.

webcpp::pratt::parser::parse

Parses the whole input as one expression.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

Return Value

The expression’s node, its error, or error::trailing_input when tokens remain after it.

webcpp::pratt::parser::peek

The next token, not consumed: for a nud or a led that reads past its own token.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

[[nodiscard]]
decltype(auto)
peek() const;

Return Value

What the stream’s peek returns.

Note

The return value should not be discarded.

webcpp::pratt::error

The errors pratt reports, as boost::system::error_code values of the category error_category.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

enum class error : int;

Description

The values start at 1, so an error_code of value 0 is no error. An error converts to an error_code and compares with one: parsed.error() == pratt::error::trailing_input.

Note

The values are fixed: they are never reused for a different meaning, so a stored value keeps its meaning across versions.

Members

Name Description

unexpected_token

A token appears where the grammar’s nud or led has none for it.

unexpected_end

The input ends where a token was still expected.

expected_closing_token

A closing token the grammar requires is missing.

trailing_input

Input remains after a complete expression.

invalid_assignment_target

The left side of an assignment or an increment is not a name.

undefined_variable

A name is read before anything has written it.

invalid_character

A character the lexer reads as no token, such as $, or ! without =.

nesting_too_deep

A parse recursed past the parser’s configured depth limit.

division_by_zero

A division or a remainder was asked by zero.

See Also

"Errors", in the guide.

webcpp::pratt::error::unexpected_token

A token appears where the grammar’s nud or led has none for it.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

unexpected_token = 1

webcpp::pratt::error::unexpected_end

The input ends where a token was still expected.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

unexpected_end = 2

webcpp::pratt::error::expected_closing_token

A closing token the grammar requires is missing.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

expected_closing_token = 3

webcpp::pratt::error::trailing_input

Input remains after a complete expression.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

trailing_input = 4

webcpp::pratt::error::invalid_assignment_target

The left side of an assignment or an increment is not a name.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

invalid_assignment_target = 5

webcpp::pratt::error::undefined_variable

A name is read before anything has written it.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

undefined_variable = 6

webcpp::pratt::error::invalid_character

A character the lexer reads as no token, such as $, or ! without =.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

invalid_character = 7

webcpp::pratt::error::nesting_too_deep

A parse recursed past the parser’s configured depth limit.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

nesting_too_deep = 8

webcpp::pratt::error::division_by_zero

A division or a remainder was asked by zero.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

division_by_zero = 9

webcpp::pratt::error_category

The category of every error.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

boost::system::error_category const&
error_category() noexcept;

Description

Its name() is "webcpp.pratt", and its message(value) the human message of the error, "division by zero" for error::division_by_zero, or "unknown webcpp.pratt error" for a value error does not define.

Return Value

The category, a constant with a fixed identity.

See Also

"Errors", in the guide.

webcpp::pratt::grouping

The nud of an opening token: a whole inner expression, then the closing token that is_close accepts.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Parser,
    class IsClose>
result<Parser::node_type>
grouping(
    Parser& p,
    IsClose is_close);

Return Value

The inner expression’s node, its error, or error::expected_closing_token.

Template Parameters

Name Description

Parser

The parser’s type, a parser.

IsClose

A predicate of the parser’s token_type.

Parameters

Name Description

p

The parser.

is_close

Whether a token is the closing one.

webcpp::pratt::infix_left

The led of a left-associative infix operator of power bp.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Parser,
    class Node,
    class Make>
result<Parser::node_type>
infix_left(
    Parser& p,
    int bp,
    Node&& left,
    Make make);

Return Value

The node, or the right operand’s error.

Template Parameters

Name Description

Parser

The parser’s type, a parser.

Node

The node’s type, the parser’s node_type, as the led passes it.

Make

A callable of the two operands’ nodes.

Parameters

Name Description

p

The parser.

bp

The operator’s power.

left

The left operand’s node, which make receives as it is passed: a led that owns it moves it in.

make

Builds the node of the operator from its operands’, as a result.

webcpp::pratt::infix_right

The led of a right-associative infix operator of power bp.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Parser,
    class Node,
    class Make>
result<Parser::node_type>
infix_right(
    Parser& p,
    int bp,
    Node&& left,
    Make make);

Description

The right operand is parsed one step weaker, bp - 1, so that an equal operator on the right takes it.

Return Value

The node, or the right operand’s error.

Template Parameters

Name Description

Parser

The parser’s type, a parser.

Node

The node’s type, the parser’s node_type, as the led passes it.

Make

A callable of the two operands’ nodes.

Parameters

Name Description

p

The parser.

bp

The operator’s power.

left

The left operand’s node, which make receives as it is passed: a led that owns it moves it in.

make

Builds the node of the operator from its operands’, as a result.

webcpp::pratt::literal

The nud of an atom: the token is the whole expression.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Make,
    class Token>
decltype(make(token))
literal(
    Make make,
    Token const& token);

Return Value

What make returns.

Template Parameters

Name Description

Make

A callable of const Token&.

Token

The token’s type.

Parameters

Name Description

make

Builds the node of token, as a result.

token

The atom.

webcpp::pratt::make_error_code

Makes the error_code of an error, as its implicit conversion does.

Synopsis

Declared in <webcpp/pratt/errors.hpp>

boost::system::error_code
make_error_code(error e) noexcept;

Return Value

e as an error_code of the category error_category.

Parameters

Name Description

e

The error.

webcpp::pratt::parse

Parses the whole of stream as one expression of grammar.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Grammar,
    token_stream Stream>
requires grammar_for<Grammar, Stream>
result<Grammar::node_type>
parse(
    Grammar const& grammar,
    Stream stream,
    int max_depth = default_max_depth);

Return Value

As parser::parse returns.

Template Parameters

Name Description

Grammar

A type that models grammar_for of Stream.

Stream

A type that models token_stream.

Parameters

Name Description

grammar

The grammar.

stream

The tokens.

max_depth

The deepest nesting of parser::expression calls allowed, as parser takes it: a negative value behaves as 0.

See Also

"Writing a grammar", in the guide.

webcpp::pratt::postfix

The led of a postfix operator: its operand is the whole left expression.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Node,
    class Make>
decltype(make(std::forward<Node>(left)))
postfix(
    Node&& left,
    Make make);

Return Value

What make returns.

Template Parameters

Name Description

Node

The node’s type, the parser’s node_type, as the led passes it.

Make

A callable of the operand’s node.

Parameters

Name Description

left

The operand’s node, which make receives as it is passed.

make

Builds the node of the operator from its operand’s, as a result.

webcpp::pratt::prefix

The nud of a prefix operator, whose operand binds with power bp.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class Parser,
    class Make>
result<Parser::node_type>
prefix(
    Parser& p,
    int bp,
    Make make);

Return Value

The node, or the operand’s error.

Template Parameters

Name Description

Parser

The parser’s type, a parser.

Make

A callable of the operand’s node.

Parameters

Name Description

p

The parser.

bp

The power the operand binds with.

make

Builds the node of the operator from its operand’s, as a result.

webcpp::pratt::default_max_depth

The deepest nesting of parser::expression calls a parse allows unless told otherwise.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

inline constexpr int default_max_depth = 64;

Description

It is measured, not chosen: the largest power of two at most half the deepest parse that wasm’s default stack of 64 KiB holds in a debug build, in the shape that costs the most of the calculator’s grammar and of openui-lang’s. A program that raises the limit raises its stack with it, with -Wl,-z,stack-size= on wasm and the thread’s stack natively.

See Also

"Limits", in the guide.

webcpp::pratt::grammar_for

A grammar of the tokens of S: for every token, its nud (its meaning at the start of an expression), its led (its meaning after a left operand) and its lbp (how tightly it binds that left operand).

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<
    class G,
    class S>
concept grammar_for = token_stream<S> &&
                      requires(const G g, parser<G, S>& p, const S::token_type& t, G::node_type n) {
                          typename G::node_type;
                          { g.lbp(t) } -> std::convertible_to<int>;
                          { g.nud(p, t) } -> std::same_as<result<typename G::node_type>>;
                          {
                              g.led(p, t, std::move(n))
                          } -> std::same_as<result<typename G::node_type>>;
                      };

Description

The node a parse builds is the grammar’s node_type, which the library never looks into. A nud and a led receive the parser, so that they parse their operands through parser::expression or the helpers, and return the node or an error. A led is called only for a token whose lbp is greater than the binding power the expression is parsed with, so a token of lbp 0, such as an end token or a closing parenthesis, ends every expression parsed with a binding power of 0 or more.

Template Parameters

Name Description

G

The grammar’s type, which names its node node_type.

S

The type of the token stream, which models token_stream.

See Also

"Writing a grammar", in the guide.

webcpp::pratt::token_stream

A source of tokens: the next one is read by peek and consumed by advance, and at_end says whether the input is spent.

Synopsis

Declared in <webcpp/pratt/parser.hpp>

template<class S>
concept token_stream = requires(S s, const S cs) {
    typename S::token_type;
    { cs.peek() } -> std::convertible_to<typename S::token_type>;
    { s.advance() } -> std::same_as<typename S::token_type>;
    { cs.at_end() } -> std::convertible_to<bool>;
};

Description

The parser reads peek to decide whether the next token extends an expression, calls advance once for each token it hands a nud or a led, and asks at_end once the whole expression is parsed. A stream may lex lazily, one token ahead, as the calculator’s lexer, calc::stream, does.

Note

peek must stay valid at the end of the input, returning a token whose lbp is 0 and whose nud fails, such as an end token.

Template Parameters

Name Description

S

The stream’s type, which names its tokens token_type.

See Also

"Writing a grammar", in the guide.

WEBCPP_PRATT_NO_EXCEPTIONS

Defined when pratt is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone, to disable pratt’s in a build that has them.

Synopsis

Declared in <webcpp/pratt/config.hpp>

#define WEBCPP_PRATT_NO_EXCEPTIONS

Description

pratt raises no exception of its own: a parse that fails returns its error, so the macro changes nothing in it. It is provided so that a program can set it for every webcpp library alike. An exception pratt raised would go through boost::throw_exception, and an API that throws would be absent while this is defined.

See Also

"Testing", in the guide.

Appendix A: Changelog

What changed in pratt in each version of webcpp, newest first, one line per change. "Unreleased" is what the next version brings. The release notes of every version are on webcpp’s index page.

Unreleased

webcpp 0.1.0

  • Added emscripten as a target: the tests and examples run under Node.

  • Fixed calc::environment failing to compile with a stateful allocator that has no default constructor.

  • Added the WEBCPP_PRATT_NO_EXCEPTIONS configuration macro, defined automatically or by the developer.

  • Added a changelog, the page’s last appendix, with each version’s changes.

  • Changed config.hpp to include <webcpp/config.hpp>, webcpp’s configuration, which the build adds.

  • Added WEBCPP_NO_EXCEPTIONS, which disables pratt’s exceptions with every webcpp library’s.