This project is an implementation of the Lox programming language, based on Robert Nystrom's excellent book Crafting Interpreters.
Written in modern C++, this interpreter aims to provide a solid and extended execution environment for Lox. Alongside the standard language features, it includes custom data structures, memory-efficient parsing techniques, and advanced language constructs like closures and named parameters.
To maintain precise control over fractional arithmetic, this interpreter implements a specialized RealNumber class.
- Design Philosophy: This custom representation is used instead of standard IEEE 754 floating-point types (like
double) specifically to explore, study, and implement custom arithmetic algorithms (e.g., Newton-Raphson method, Goldschmidt division) with a higher degree of control over precision and noise reduction. - Internal Representation: Numbers are split into
integer,fractional, andn_fractional_digitsto help preserve exact decimal precision. - Noise Reduction: Includes
.reduce_noise()and.clean_trailing_zeroes()methods to help mitigate common floating-point artifacts.
The parser uses an iterative approach to evaluate expressions and build the Abstract Syntax Tree (AST). By maintaining operator precedence dynamically and avoiding recursion, it safely handles deeply nested expressions while keeping the call stack light.
To improve memory efficiency, the scanner makes heavy use of std::string_view through a custom BasicTokenView template. This provides a lightweight, non-owning view of token data, which helps reduce heap allocations during the tokenization and parsing phases.
The interpreter features a fully functional Read-Eval-Print Loop (REPL). It natively maintains the state of your variables and functions across inputs. By executing single statements line-by-line, it immediately evaluates the expression and prints out the returned underlying type, making it a powerful tool for interactive experimentation.
Here is the exhaustive documentation of the Lox language as supported by this interpreter.
The interpreter strictly supports the following data types:
- Number: Managed by the custom
RealNumberclass. Example:42,3.1415. - String: Text wrapped in double quotes. Example:
"Hello, World!". - Boolean:
trueorfalse. - Nil: Represents the absence of a value. Example:
nil.
Variables are dynamically typed and lexically scoped. They are declared using the var keyword.
If not explicitly initialized, variables default to nil.
var a = 10;
var b = "texto";
var emptyVar; // Evaluates to nil
a = 20; // Reassignment
The interpreter includes advanced implicit type coercion and operator overloading for extreme flexibility, far beyond standard Lox.
-
Addition (
+):- Number + Number: Standard mathematical addition.
- String + String: String concatenation.
- Boolean + Boolean: Booleans are implicitly cast to
1(true) and0(false), returning a Number. (true + true=2). - Number + Boolean / Boolean + Number: The boolean is cast to an integer (
1or0) and added to the number. - String + Boolean: Converts the boolean to its string representation and concatenates. (
"Value: " + true="Value: true"). - String + Nil: Converts
nilto"nil"and concatenates. ("Value: " + nil="Value: nil").
-
Subtraction (
-):- Unary
-Number: Returns negated Number. - Number - Number: Standard mathematical subtraction.
- Boolean - Boolean: Booleans cast to
0/1and subtracted. - Number - Boolean / Boolean - Number: Boolean cast to
0/1and subtracted against the number.
- Unary
-
Multiplication (
*):- Number * Number: Standard mathematical multiplication.
- String * Number or Number * String (String Repetition): Multiplies a string, repeating it the specified number of times! The number must be a positive integer.
print "ha" * 3; // Output: "hahaha" print 4 * "lox"; // Output: "loxloxloxlox" -
Division (
/):- Defined strictly for
Number / Number(returns Number).
- Defined strictly for
- Relative (
>,>=,<,<=):- Strictly defined for comparing
NumberagainstNumber.
- Strictly defined for comparing
- Equality (
==,!=):- Can compare any two values of the same type (String, Number, Boolean, Nil).
- If you compare values of different types, it safely evaluates to
false(for==) ortrue(for!=).
print 5 > 3; // true
print "lox" == "lox"; // true
print 5 == "5"; // false
- AND (
and) / OR (or):- They perform short-circuit evaluation.
- They return the evaluated actual underlying type, not just a boolean.
print "hello" or 2; // "hello" (short-circuits)
print nil and "lox"; // nil
Evaluates a condition and branches execution. The condition relies on "truthiness" (everything is true except nil and false).
var age = 20;
if age >= 18 {
print "Adult";
} else {
print "Minor";
}
Executes a block of code repeatedly while the condition evaluates to truthy.
var count = 0;
while count < 5 {
print count;
count = count + 1;
}
A standard for (init; condition; increment) loop.
Important: All three components (init, condition, increment) are strictly optional.
- If
conditionis omitted, it defaults totrue(infinite loop). - If
initorincrementare omitted, they default to ano-op(do nothing). - The loop establishes a new isolated stackframe scope.
// Standard usage
for (var i = 0; i < 5; i = i + 1) {
print i;
}
// Optional parameters usage (acts like a while loop)
var j = 0;
for (; j < 3; ) {
print j;
j = j + 1;
}
// Infinite loop (omitted condition evaluates to true)
// for (;;) { ... }
Due to the custom iterative Pratt parser implementation, there are a few interesting edge cases where the language allows syntax that differs from traditional C-style languages:
-
Optional Parentheses for
ifandwhileConditions: Becauseifandwhileare parsed as operators that naturally expect an expression operand, you are not required to wrap the condition in parentheses. This makes the language slightly more fluid if you prefer less punctuation.// Completely valid in this interpreter: if true print "Always executes!"; var x = 0; while x < 3 { print x; x = x + 1; }Note: This does not apply to
forloops, which strictly expect parentheses()for their 3-part grouping. -
Block Braces
{}are Optional for Single Statements: Any control flow block naturally accepts a single expression. You only need{}when you want to group multiple expressions.if 10 > 5 print "Greater"; else print "Smaller"; -
Multi-Type Return in
and/or: As mentioned above,and/ordo not coerce their return types to booleans; they return the actual last evaluated operand.
Functions in this implementation are incredibly versatile, acting as first-class citizens.
Declared with either the fun or func keyword and returns a value (or nil by default) with return.
// Both declarations are valid!
fun multiply(a, b) {
return a * b;
}
func multiply(a, b) {
return a * b;
}
print multiply(3, 4); // 12
Functions support default arguments. Default argument expressions are evaluated at call time, not at definition time. This means if you use a mutable variable or a function call as a default parameter, it will always compute the most up-to-date value when the function is invoked!
var counter = 0;
fun incrementCounter() {
counter = counter + 1;
return counter;
}
fun printScore(score = incrementCounter()) {
print "Score: " + score;
}
printScore(); // Score: 1
printScore(); // Score: 2
printScore(100); // Score: 100
Functions capture their surrounding lexical environment (closures). You can return functions from functions.
- Function Chaining: The interpreter supports chained function calls natively. If a function returns another
- function, you can execute it immediately by appending another set of parentheses.
fun createGreeter(greeting) {
fun greeter(name) {
print greeting + ", " + name + "!";
}
return greeter;
}
// Normal usage
var sayHi = createGreeter("Hi");
sayHi("Alice"); // Output: Hi, Alice!
// Direct Chaining!
createGreeter("Hello")("Bob"); // Output: Hello, Bob!
You can invoke functions using named parameters via the : operator. This allows you to pass arguments in any order, which greatly improves code readability.
fun computeStats(health, mana, stamina) {
print "Health: " + health;
print "Mana: " + mana;
}
// Order doesn't matter when using named parameters!
computeStats(stamina: 100, health: 50, mana: 200);
The runtime environment comes with built-in utility native functions pre-loaded in the global scope:
clock(): Returns the exact current time in seconds since the Epoch.delete(varName): Manually removes a variable from the current stack frame execution environment, allowing for explicit memory/state control.
var start = clock();
// do some heavy work...
var end = clock();
print "Time taken: ";
print end - start;
var temp = "data";
delete(temp); // temp no longer exists in scope
While this interpreter is highly capable, there are still a few areas planned for future improvements:
- Tail Call Optimization (TCO): Currently, deep recursion can still grow the host C++ stack when executing function bodies. Implementing tail recursion optimization would allow for infinite recursive loops without stack overflows.
- Multi-Return Support: The AST and internal token structures have groundwork laid for
return a, b;andvar a, b = f(), but full interpreter support needs to be finalized. - Enhanced Standard Library: Expanding the global native functions to include standard file I/O operations, random number generation, and math libraries.