( ? [overview] [philosophy] [features] [design] [library & example] )
pbg, an initialism for Prefix Boolean Grammar, is a lightweight grammar for writing boolean expressions. This repository provides an efficient and minimial C library implementing a pbg runtime.
pbg (the grammar) is designed to be simple (simple syntax with a minimal set of features), readable (easy to read and write expressions), and expressive (expressions pack the full punch of propositional logic).
pbg (the C library) is designed to be fast (quick compilation and evaluation), robust (faithful implementation of the grammar and full error handling), and simple (few API functions with a flexible dictionary implementation).
Both are designed to be predictable. Neither is built to make assumptions. The library will never fail silently. For example, an undefined variable evaluates to NULL
, which, when passed to the <
operator, will generate an error: the operation will not simply evaluate to FALSE
. If it did, this would violate the defined behavior of the operator. If the variable may not be defined, the expression should explicitly test this before using the variable.
- simple, powerful grammar
- fast compilation and evaluation
- compatible with C89
- no dependencies except libc
- small memory footprint
A boolean-valued function takes a set of inputs and outputs a truth value, TRUE
or FALSE
. Each pbg expression can be thought of as a boolean-valued function built from a small set of core operations and literals. On this view, the inputs to a pbg expression are variables.
An operation is a built-in boolean-valued function. A literal is either a constant or a variable. Each literal has a data type, and each operation takes only certain literal types as inputs. The truth value of a pbg expression is determined by the operations and literals composing it.
Each literal has a type. pbg defines the following types: DATE
, BOOL
, NUMBER
, STRING
, and NULL
. The type of a literal is not explicitly declared; it is implicitly encoded via formatting. Literals can be either constants or variables.
A VAR
literal represents a variable and is formatted using square brackets. So [start]
defines a variable named start
. During evaluation, a variable will be replaced either by a constant or by NULL
, if it is undefined.
A DATE
literal represents a day in the Gregorian calendar and is formatted as YYYY-MM-DD
. So 2018-10-12
is October 12, 2018
.
A BOOL
literal represents a truth value. It can be either TRUE
or FALSE
.
A NUMBER
literal represents a floating point number and is formatted similarly to JSON numbers. So 3
, 3.14
, 314e-2
, 0.314
, and 0.0
are all valid NUMBER
literals, but .314
, 0.
, and 1e
are not.
A STRING
literal represents text, i.e. a string of characters, and is formatted using single quotes. So 'Hello pbg!'
is a STRING
, and ''
is the empty STRING
. To represent a single quote within a STRING
, it must be escaped: 'I said, \'hi\' before I left.'
.
A NULL
literal occurs only when a variable is undefined. So if an expression references a variable which is undefined when the expression is evaluated, then it will evaluate to NULL
during evaluation. This
Only two operations, EXST
and TYPE
, support NULL
as an input. All other operations will throw errors when given NULL
as an input. This is done to differentiate variable existence from the truth value of the operation predicate when a variable is defined.
The boolean not operator, abbreviated NOT
. Take a single input of type BOOL
. Return the inverse of its input.
(! TRUE)
isFALSE
(! FALSE)
isTRUE
(! (! FALSE))
isFALSE
The boolean and operator, abbreviated AND
. Take two or more inputs of type BOOL
. Return TRUE
only if every input is TRUE
.
(& TRUE FALSE)
isFALSE
(& TRUE TRUE TRUE)
isTRUE
The boolean or operator, abbreviated OR
. Take two or more inputs of type BOOL
. Return FALSE
only if every input is FALSE
.
(| FALSE TRUE FALSE)
isTRUE
(| FALSE FALSE)
isFALSE
The equality operator, abbreviated EQ
. Take two or more inputs of any type. Return TRUE
only if all arguments have equal values.
(= 3 'hi')
isFALSE
(= 3 '3')'
isFALSE
(= 3 3)
isTRUE
The not equal operator, abbreviated NEQ
. Take two inputs of any type. Return TRUE
only if its arguments are different.
(!= 3 'hi')
isTRUE
(!= 3 '3')'
isTRUE
(!= 3 3)
isFALSE
The less than operator, abbreviated LT
. Take two inputs of any type. Return TRUE
only if the first argument is less than the second.
(< 'aa' 'ab')
isTRUE
(< 2018-10-12 2018-10-11)
isTRUE
(< 5 2)
isFALSE
The greater than operator, abbreviated GT
. Take two inputs of any type. Return TRUE
only if the first argument is greater than the second.
(> 'aa' 'ab')
isFALSE
(> 2020-12-01 2019-12-01)
isTRUE
(> 5 2)
isTRUE
The at most operator, abbreviated LTE
. Take two inputs of any type. Return TRUE
only if the first argument is less than or equal to the second.
(<= 'aa' 'aa')
isTRUE
(<= 2020-12-01 2019-12-01)
isFALSE
(<= 5 6)
isTRUE
The at least operator, abbreviated GTE
. Take two inputs of any type. Return TRUE
only if the first argument is greater than or equal to the second.
(>= 'aa' 'zz')
isFALSE
(>= 2020-12-01 2019-12-01)
isTRUE
(>= 7 7)
isTRUE
The existence operator, abbreviated EXST
. Take one or more inputs of any type, including NULL
. Return TRUE
only if it its argument is not NULL
. EXST
is useful for checking if a variable is defined.
(? 5)
isTRUE
(? 2018-10-12)
isTRUE
(? NULL)
isFALSE
(? [a])
isTRUE
only if variablea
is defined.
The type check operator is written as @
. Take a TYPE
name followed by at least one input of any type, including NULL
. Return TRUE
only if every argument after the first has the type specified by the first.
- Notice that
(?[a])
and(@ NULL [a])
have the same truth table. (@ NUMBER 3.14 'hi' 17)
isFALSE
, because'hi'
is not aNUMBER
even though3.14
and17
are.(@ DATE 2018-10-12)
isTRUE
, because2018-10-12
is aDATE
literal.(@ DATE '2018-10-12')
isFALSE
, because'2018-10-12'
is aSTRING
literal.
The syntax for writing pbg expressions is succinctly captured by the following formal grammar:
EXPR
= BOOL
BOOL
= (! BOOL)
= (& BOOL BOOL ...)
= (| BOOL BOOL ...)
= (= ANY ANY ...)
= (!= ANY ANY)
= (< ANY ANY)
= (> ANY ANY)
= (<= ANY ANY)
= (>= ANY ANY)
= (? ALL ...)
= (@ TYPE ALL ...)
= TRUE
= FALSE
ANY
= DATE
= NUMBER
= STRING
= BOOL
ALL
= ANY
= NULL
This repository provides a lightweight implementation of a pbg compiler and evaluator. It can be incorporated into an existing project by including pbg.h
. Documentation of each API function is provided in pbg.h
but is partially reproduced in this section.
The library reserves the pbg_
and PBG_
prefixes. If these are used by another library you are using, you'll need to rename all library functions and constants. Good luck, and godspeed.
The below example is found in test/example.c
. It evaluates the expression (&(=[a][b])(?[d]))
with a=5.0
, b=5.0
, and c=6.0
.
#include "../pbg.h"
#include <stdio.h>
#include <string.h>
/* Our dictionary prototype! */
pbg_field dictionary(char* key, int n);
int main(void)
{
pbg_error err;
pbg_expr e;
int result;
/* This is the expression string we'll evaluate using our dictionary.
* It asks, "Are [a] and [b] the same, AND does [d] exist?" */
char* str = "(&(=[a][b])(?[d]))";
/* Parse the expression string and check if
* there were any compilation errors. */
pbg_parse(&e, &err, str);
if(pbg_iserror(&err)) {
pbg_error_print(&err);
return 1;
}
/* Evaluate the expression string and check if
* there were any runtime errors. */
result = pbg_evaluate(&e, &err, dictionary);
if(pbg_iserror(&err)) {
pbg_error_print(&err);
return 1;
}
/* Success! Print the output. */
printf("%s is %s\n", str, (result == PBG_TRUE) ? "TRUE" : "FALSE");
return 0;
}
/* This is a simple, handmade dictionary. A more general implementation would
* (hopefully) utilize a dictionary data structure. */
pbg_field dictionary(char* key, int n)
{
PBG_UNUSED(n); /* Ignore compiler warnings. */
if(key[0] == 'a' || key[0] == 'b')
return pbg_make_number(5.0);
if(key[0] == 'c')
return pbg_make_number(6.0);
return pbg_make_null();
}
The output is FALSE
because (?[d])
asks if the variable d
is defined, which it is not. The expression (|(=[a][b])(?[d]))
is TRUE
, however, because (=[a][b])
asks if a
and b
are equal, which they are.
/* Parse the string as a pbg expression. If a compilation error occurs, initialize
* the provided error argument accordingly. */
void pbg_parse(pbg_expr* e, pbg_error* err, char* str)
/* Parse the string with the given length as a pbg expression. If a compilation error
* occurs, initialize the provided error argument accordingly. */
void pbg_parse_n(pbg_expr* e, pbg_error* err, char* str, int n)
/* Evaluate the pbg expression with the provided dictionary. If a runtime error
* occurs, initialize the provided error accordingly. */
int pbg_evaluate(pbg_expr* e, pbg_error* err, pbg_field (*dict)(char*, int))
/* Destroy the pbg expression instance, and free all associated resources. If
*`pbg_parse` succeeds, this function must be called to free up internal resources. */
void pbg_free(pbg_expr* e)
/* Makes a field representing a DATE. */
pbg_field pbg_make_date(int year, int month, int day)
/* Makes a field representing a BOOL. */
pbg_field pbg_make_bool(int truth)
/** Makes a field representing a NUMBER. */
pbg_field pbg_make_number(double value)
/* Makes a field representing a STRING. */
pbg_field pbg_make_string(char* str)
/* Makes a field representing a NULL. */
pbg_field pbg_make_null(void)
/* Checks if the given error has been initialized with error data. */
int pbg_iserror(pbg_error* err)
/* Prints a human-readable representation of the given error. */
void pbg_error_print(pbg_error* err)
/* Frees resources being used by the given error, if any. */
void pbg_error_free(pbg_error* e)