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
resultand 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’sresultnames, 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, anint; -
nud(p, t), its meaning at the start of an expression; -
led(p, t, left), its meaning after the expressionleft, 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 |
|---|---|---|
|
nud |
Nothing more: the token is the whole expression, a number or a name. |
|
nud |
One operand, after the token, parsed with |
|
led |
The right operand, parsed with |
|
led |
The right operand, parsed with |
|
led |
Nothing more: the left expression is the whole operand, as of a factorial. |
|
nud |
A whole expression, parsed with 0, then the closing token that
|
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; |
|
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. |
|
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. |
|
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 ofname, ornullptrwhen the name is not set; -
assign(name, value), which sets it and returns apratt::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, |
90 |
|
a pair |
20 |
|
|
|
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 |
|---|---|---|---|
|
1 |
unexpected token |
A token stands where the grammar has no nud or led for it: |
|
2 |
unexpected end of input |
The input ends where a token was still due: |
|
3 |
expected closing token |
A grouping, or a call of the micro-subset, misses its |
|
4 |
trailing input after expression |
Tokens remain after a whole expression: |
|
5 |
invalid assignment target |
|
|
6 |
undefined variable |
The calculator reads a variable that the environment does not hold. |
|
7 |
invalid character |
A character that starts no token: |
|
8 |
expression nested too deeply |
A parse would nest deeper than its |
|
9 |
division by zero |
The calculator’s |
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 |
|---|---|---|---|
|
232 (282 B) |
450 (146 B) |
16854 (497 B) |
|
263 (249 B) |
450 (146 B) |
18016 (465 B) |
|
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
expressioncalls. 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: inassign, 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
Numberthat allocates does so through its own allocator.decimal64_tanddoubleallocate nothing. A type whose arithmetic allocates, Boost.Multiprecision’smpfr_floatwithet_offfor 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 |
|---|---|
|
The values of |
|
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 |
|
The engine through a grammar whose tokens are characters and whose nodes are strings, with nothing of the calculator. |
|
The calculator’s |
|
The calculator over |
|
The global |
|
The default depth limit with the calculator’s own frames: a grouping, a
chain of signs and a chain of |
|
The openui-lang micro-subset, through the grammar it shares with its
REPL: every line of its session, |
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 |
emscripten |
b2’s toolset |
wasip2 |
The toolset |
wasip3 |
The toolset |
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-wasip3does 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 |
|---|---|
Defined when pratt is built without exceptions: when webcpp has none (the compiler has none, or the developer defined |
webcpp::pratt
Namespaces
| Name |
|---|
Types
| Name | Description |
|---|---|
A Pratt parser of the tokens of a |
Type Aliases
| Name | Description |
|---|---|
What a parse that can fail returns: a |
Enums
| Name | Description |
|---|---|
The errors pratt reports, as |
Functions
| Name | Description |
|---|---|
The category of every |
|
The nud of an opening token: a whole inner expression, then the closing token that |
|
The led of a left-associative infix operator of power |
|
The led of a right-associative infix operator of power |
|
The nud of an atom: the token is the whole expression. |
|
Makes the |
|
Parses the whole of |
|
The led of a postfix operator: its operand is the whole left expression. |
|
The nud of a prefix operator, whose operand binds with power |
Variables
| Name | Description |
|---|---|
The deepest nesting of |
Concepts
| Name | Description |
|---|---|
A grammar of the tokens of |
|
A source of tokens: the next one is read by |
webcpp::pratt::calc
Types
| Name | Description |
|---|---|
The calculator’s own environment: a |
|
The calculator’s grammar over an environment: numbers, variables, the arithmetic operators |
|
A node of a calculator parse: a value, and the name of a bare variable. |
|
The calculator’s lexer, a lazy |
|
A token of the calculator’s input. |
Enums
| Name | Description |
|---|---|
What a token is. |
Functions
| Name | Description |
|---|---|
Evaluates |
Concepts
| Name | Description |
|---|---|
An environment the calculator reads and writes its variables in. |
|
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
Declared in <webcpp/pratt/calculator.hpp>
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 |
|---|---|
The allocator the environment was given, which its map and its keys rebind. |
|
The type of the values. |
Member Functions
| Name | Description |
|---|---|
|
Constructors |
Sets |
|
The allocator the environment allocates through. |
|
Finds the value of |
Template Parameters
| Name | Description |
|---|---|
Number |
The type of the values, |
Allocator |
The allocator of the map and of its keys. The default is |
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.
webcpp::pratt::calc::environment::number_type
The type of the values.
webcpp::pratt::calc::environment::environment
Constructors
Synopses
Declared in <webcpp/pratt/calculator.hpp>
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.
webcpp::pratt::calc::environment::environment
Constructs an empty environment that allocates through allocator.
Synopsis
Declared in <webcpp/pratt/calculator.hpp>
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
Declared in <webcpp/pratt/calculator.hpp>
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
Declared in <webcpp/pratt/calculator.hpp>
[[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
Declared in <webcpp/pratt/calculator.hpp>
[[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
Declared in <webcpp/pratt/calculator.hpp>
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 |
|---|---|
What a parse builds. |
|
The type the calculator computes in, the environment’s. |
|
The tokens the grammar reads. |
Member Functions
Static Data Members
| Name | Description |
|---|---|
The power of binary |
|
The power of |
|
The power of the six comparisons. |
|
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 |
|
The power of |
|
The power of a postfix |
|
The power of |
|
The power a unary sign and a prefix |
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::grammar
Constructs the grammar over env.
Parameters
| Name | Description |
|---|---|
env |
The environment. |
Preconditions
-
envoutlives the grammar.
webcpp::pratt::calc::grammar::lbp
How tightly the token t binds the expression on its left.
Synopsis
Declared in <webcpp/pratt/calculator.hpp>
[[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
Declared in <webcpp/pratt/calculator.hpp>
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 |
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
Declared in <webcpp/pratt/calculator.hpp>
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 |
Parameters
| Name | Description |
|---|---|
p |
The parser. |
t |
The token. |
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
Declared in <webcpp/pratt/calculator.hpp>
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
Declared in <webcpp/pratt/calculator.hpp>
inline constexpr static
int multiplicative_bp = 40;
webcpp::pratt::calc::grammar::unary_bp
The power a unary sign and a prefix ++ or -- parse their operand with, below ^.
webcpp::pratt::calc::node
A node of a calculator parse: a value, and the name of a bare variable.
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
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.
webcpp::pratt::calc::node::name
The name of a bare variable, a view into the source; empty for every other node.
webcpp::pratt::calc::stream
The calculator’s lexer, a lazy webcpp::pratt::token_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 |
|---|---|
The tokens the stream yields. |
Member Functions
| Name | Description |
|---|---|
|
Constructs the stream of |
Consumes the next token and returns it; at the end, returns the end token again. |
|
Whether the input is spent: the next token is the end token. |
|
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::stream
Constructs the stream of source, and lexes the first token.
Parameters
| Name | Description |
|---|---|
source |
The text to lex. |
Preconditions
-
sourceoutlives 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.
Return Value
The token consumed.
webcpp::pratt::calc::stream::peek
The next token, not consumed; the end token once the input is spent.
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.
Data Members
Template Parameters
| Name | Description |
|---|---|
Number |
The type a number token’s value is. |
webcpp::pratt::calc::kind
What a token is.
Members
| Name | Description |
|---|---|
A number literal, such as |
|
A variable’s name, such as |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A character the lexer reads as no token, one character long. |
|
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::invalid
A character the lexer reads as no token, one character long.
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.
webcpp::pratt::calc::evaluate
Evaluates source as one expression of the calculator, reading and writing its variables in env.
Synopsis
Declared in <webcpp/pratt/calculator.hpp>
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 |
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 |
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
Declared in <webcpp/pratt/calculator.hpp>
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 |
See Also
"The calculator", in the guide.
"Allocation", in the guide.
webcpp::pratt::calc::number
A type the calculator computes in.
Synopsis
Declared in <webcpp/pratt/calculator.hpp>
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 |
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 |
Template Parameters
| Name | Description |
|---|---|
T |
The type of the value; |
See Also
"Errors", in the guide.
webcpp::pratt::parser
A Pratt parser of the tokens of a Stream, by a Grammar.
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 |
|---|---|
What the grammar builds. |
|
The tokens the stream yields. |
Member Functions
| Name | Description |
|---|---|
|
Constructors |
Consumes the next token and returns it: for a nud or a led that reads past its own token. |
|
Parses the longest expression whose operators bind tighter than |
|
Parses the whole input as one expression. |
|
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 |
Stream |
A type that models |
See Also
"Writing a grammar", in the guide.
"Limits", in the guide.
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 |
Preconditions
-
grammaroutlives 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 |
Preconditions
-
grammaroutlives 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.
Return Value
The token consumed.
webcpp::pratt::parser::expression
Parses the longest expression whose operators bind tighter than 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.
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.
webcpp::pratt::error
The errors pratt reports, as boost::system::error_code values of the category error_category.
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 |
|---|---|
A token appears where the grammar’s |
|
The input ends where a token was still expected. |
|
A closing token the grammar requires is missing. |
|
Input remains after a complete expression. |
|
The left side of an assignment or an increment is not a name. |
|
A name is read before anything has written it. |
|
A character the lexer reads as no token, such as |
|
A parse recursed past the parser’s configured depth limit. |
|
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.
webcpp::pratt::error::invalid_assignment_target
The left side of an assignment or an increment is not a name.
webcpp::pratt::error::invalid_character
A character the lexer reads as no token, such as $, or ! without =.
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 |
IsClose |
A predicate of the parser’s |
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 |
Node |
The node’s type, the parser’s |
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 |
Builds the node of the operator from its operands’, as a |
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 |
Node |
The node’s type, the parser’s |
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 |
Builds the node of the operator from its operands’, as a |
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 |
Token |
The token’s type. |
Parameters
| Name | Description |
|---|---|
make |
Builds the node of |
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 |
Stream |
A type that models |
Parameters
| Name | Description |
|---|---|
grammar |
The grammar. |
stream |
The tokens. |
max_depth |
The deepest nesting of |
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 |
Make |
A callable of the operand’s node. |
Parameters
| Name | Description |
|---|---|
left |
The operand’s node, which |
make |
Builds the node of the operator from its operand’s, as a |
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 |
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 |
webcpp::pratt::default_max_depth
The deepest nesting of parser::expression calls a parse allows unless told otherwise.
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 |
S |
The type of the token stream, which models |
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
|
|
Template Parameters
| Name | Description |
|---|---|
S |
The stream’s type, which names its tokens |
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.
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::environmentfailing to compile with a stateful allocator that has no default constructor. -
Added the
WEBCPP_PRATT_NO_EXCEPTIONSconfiguration macro, defined automatically or by the developer. -
Added a changelog, the page’s last appendix, with each version’s changes.
-
Changed
config.hppto 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.