Express: A Language That Speaks Math
I have a habit of writing math on paper first and then fighting with LaTeX to reproduce it. You know how it goes: you want to write a simple fraction and you end up typing \frac{a + b}{2 \cdot c} while your brain is still thinking (a+b)/(2c). It is not the end of the world, but it is annoying enough that I decided to do something about it.
The result is Express, a small interpreted language that takes math written in a clean, R-like syntax and translates it directly into LaTeX. You write the math the way you think it. Express gives you the LaTeX.
What it looks like
Here are a few examples of what Express code produces:
area = (b * h) / 2
outputs:
area = \frac{b \cdot h}{2}
roots = sqrt(b^2 - 4*a*c) / (2*a)
outputs:
roots = \frac{\sqrt{b^{2} - 4 \cdot a \cdot c}}{2 \cdot a}
v = (0.4, 0.5, 0.6, 0.8)
c = vsum(v)
outputs:
\vec{v} = (0.4,0.5,0.6,0.8)
c = \sum{(0.4,0.5,0.6,0.8)}
The key insight is that every assignment knows whether its right-hand side is a constant expression (a "final" value) or a symbolic one. For constant values it shows the full step-by-step evaluation: 3 + 4 = 7. For symbolic expressions it keeps the form intact and renders it as LaTeX. Vectors get \vec{} automatically when a variable is first assigned a tuple.
Try it live
Type any Express expression below to see the LaTeX it would generate:
…
The grammar
Express is built with two tools that have been around forever and are still great: re2c for lexing and Bison (LALR(1)) for parsing. The lexer runs first and breaks the input into tokens. The parser then reads those tokens and builds an AST.
The full grammar fits in about 50 lines:
expression: expression-block | return-block | assignment
| function | lvalue | svalue | variable
| function-call | operation | vector
lvalue: NUMCONST { new Constant($1) }
variable: IDENTIFIER { new Variable($1) }
assignment: variable '=' expression
function: vector expression-block // (x,y) { ... }
function-call: variable vector // f(x, y)
operation: expression '+' expression
| expression '-' expression
| expression '*' expression
| expression '/' expression
| expression '^' expression
| expression '[' expression ']' // vector indexing
vector-item: expression
| vector-item ',' expression
vector: '(' vector-item ')'
| '(' ')'
A few things worth noting here. Functions are defined by putting a parameter vector immediately before a block: (x, y) { return x + y }. Function calls look identical but with a variable in front: f(x, y). The parser can tell the difference because at the grammar level, if you have IDENTIFIER '(' ...) it becomes a function-call, while a standalone '(' ...) that is followed by '{' becomes a function.
Vector indexing uses square brackets: v[0]. This maps to an Operation node with op_ref, and evaluates as a->evaluate()[b->evaluate()[0]].
The AST nodes and their LaTeX output
Every node in the AST inherits from a base Expression class that has two pure virtual methods:
virtual Value i_evaluate() = 0;
virtual void i_print(std::string& str) = 0;
i_evaluate computes the numeric result. i_print generates the LaTeX. The trick is that print() (not i_print) checks first whether the expression was already evaluated:
void print(std::string& str) {
if (wasEvaluated) str += lastEvaluatedValue;
else i_print(str);
}
So once you evaluate something, the next time you print it you get the number, not the symbolic form. This is what drives the step-by-step evaluation output for constant assignments.
Here is a table of how the key node types produce LaTeX:
| Node | Express | LaTeX |
|---|---|---|
Constant | 42 | 42 |
Variable (scalar) | x | x |
Variable (vector) | v | \vec{v} |
Operation (/) | a / b | \frac{a}{b} |
Operation (^) | a ^ b | a^{b} |
Operation (*) | a * b | a \cdot b |
InternalFunction (sqrt) | sqrt(x) | \sqrt{x} |
InternalFunction (abs) | abs(x) | \left|x\right| |
InternalFunction (exp) | exp(x) | e^{x} |
InternalFunction (log2) | log2(x) | \log_{2}{x} |
InternalFunction (vsum) | vsum(v) | \sum{v} |
The division case is the cleanest example. The Operation::i_print for op_div wraps the numerator in \frac{, prints }, then wraps the denominator in {...}:
if (op_type == op_div) str += "\\frac{";
a->print(str);
if (op_type == op_div) str += "}{";
// ... (for other operators their symbol goes here)
if (op_type == op_div) str += "}";
b->print(str);
Special functions like sqrt, abs, and log2 carry a prefix and suffix string. When Express registers them it calls set_prefixes("\\sqrt{", "}"). At print time the function call node just writes prefix + arguments + suffix instead of the usual name(args) form. This is how you get \sqrt{x} instead of \operatorname{sqrt}(x) without any special-casing in the printer.
The Value type
Value is a vector<double> with a sprinkle of broadcast semantics baked in. Scalar-vector arithmetic just works:
Value a = {1.0, 2.0, 3.0};
Value b = 2.0; // single element
Value c = a + b; // → {3.0, 4.0, 5.0}
When you add a scalar to a vector, the scalar is broadcast across all elements first. The ^ operator is overloaded to call pow element-wise. This is the same idea as NumPy broadcasting but implemented in about 30 lines of C++ using a single macro that generates all four arithmetic operators at once:
#define MASTER_OPERATOR(op, op2) \
inline Value& operator op2 (Value& v, const Value& other) \
{ \
if (v.is_numeric()) v.resize(other.size(), v[0]); \
for (size_t i = 0; i < v.size(); ++i) v[i] op2 other[i]; \
return v; \
}
MASTER_OPERATOR(+, +=)
MASTER_OPERATOR(-, -=)
MASTER_OPERATOR(/, /=)
MASTER_OPERATOR(*, *=)
Four operators, one macro. ^ needs its own version because C++ already has ^= as bitwise XOR and you cannot put a call to pow inside the same macro pattern.
Scope and functions
Variables live in a Scope object that is a stack of map<string, Expression*>. Each time you enter a block ({...}) the scope is incremented, and it pops when you leave:
void i_evaluate() override {
++(*Scope::scope); // push a new scope frame
if (expectsParameters)
initialize_body(); // bind parameter names to argument values
// ... evaluate expressions ...
--(*Scope::scope); // pop
}
User-defined functions work the same way as internal ones. A function definition like f = (x) { return x^2 + 1 } stores a Function node. When called as f(3), the FunctionCall node grabs the Function, passes a Vector of evaluated arguments, and the function's ExpressionBlock binds them into its local scope before running.
Internal functions (like sin, sqrt) are just special Function subclasses that call a raw C function pointer instead of evaluating an AST body. Some of them (solve, vsum, vprod) take the whole vector as an argument rather than element-wise scalars.
What is left to build
The language already parses, evaluates, and prints LaTeX for a solid subset of math. But there are some things still on the roadmap:
- Caching for sub-expressions that are called repeatedly
- Lazy evaluation so you can define recursive or conditional expressions without blowing the stack
- Memory management (right now every expression is heap-allocated and never freed)
- Full LaTeX translation pipeline that walks the full evaluation trace and formats it as aligned equations showing each step
The step-by-step evaluation thing (where x = 3 + 4 becomes x = 3 + 4 = 7 in the output) is already partially working through the latexize function. Making that robust for all expression types and getting the alignment right in the output LaTeX is the main remaining piece.
If you want to dig into the code, the grammar is the cleanest entry point (expr.y). The LaTeX output lives in i_print methods in expression_types.h. The whole thing compiles with make assuming you have Bison, re2c, and a C++17 compiler handy.
