uCalc API Version: 2.1.3-preview.2 Released: 6/17/2026
Warning
uCalc API Preview Release Notice:The documentation describes the intended behavior of the API. The current preview build contains incomplete features, unoptimized performance, and is subject to breaking changes.
Project: Developing a Simple LISP Interpreter
Product:
Class:
A step-by-step tutorial on building a simple LISP interpreter using uCalc's ExpressionTransformer.
Remarks
💡 Project: Building a Simple LISP Interpreter
This project is a masterclass in uCalc's dynamic syntax capabilities. We will build a simple, functional interpreter for a LISP-like language. LISP (LISt Processing) is famous for its distinctive parenthesized prefix notation, also known as S-expressions (Symbolic Expressions).
This project showcases how uCalc's Transformer can transpile one language's syntax into another on the fly using a set of declarative, recursive rules.
The Goal: Our LISP Syntax
We want our uCalc engine to understand expressions like this:
(+ 1 2)(- 10 5)(* 2 3 4)(variadic)(/ 100 5 2)(variadic)(+ 1 (* 2 3))(nested)
These will be transformed into standard uCalc expressions (1 + 2, 10 - 5, etc.) and evaluated.
The Strategy: A Declarative, Multi-Rule Approach
A simple find-and-replace rule isn't enough because we need to handle nested and variadic expressions. The solution is to create a set of transformation rules that work together to recursively simplify the LISP expression from the inside out until only a single value remains.
This requires three key uCalc features:
RewindOnChange(true): After a rule makes a replacement, the transformer re-scans the text, allowing other rules (or the same rule) to be applied to the result. This is the engine for our recursion.{@@Eval}: This directive evaluates a captured string as an expression. We'll use it to perform the arithmetic for each sub-expression.- Immediate Transform (
%): This variable modifier forces the transformer to evaluate nested patterns first, which is essential for our inside-out evaluation strategy.
Step 1: The Base Case - Binary Operations
The simplest rule handles a binary operation. It finds an operator followed by two operands, evaluates them, and replaces the entire S-expression with the result.
t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}");
This rule transforms (+ 10 20) into the result 30.
Step 2: The Recursive Step - Variadic Operations
To handle more than two operands, we define a rule that reduces the list one step at a time. It finds an operator and at least three operands, evaluates the first two, and puts the result back at the front of the list.
t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})");
With RewindOnChange(true), this rule will be applied repeatedly:
(* 2 3 4)becomes(* 6 4)- The transformer rewinds and finds
(* 6 4), which is then evaluated by the base case rule to24.
Step 3: Inside-Out Evaluation - Nested Expressions
To handle nested expressions like (/ 100 (+ 5 5)), we need to evaluate the inner part first. The immediate transform modifier (%) on a variable forces this behavior.
t.FromTo("({op:1} {a:1} {expr%: ({exp})})", "({op} {a} {expr}");
This rule looks for an expression where the second operand is itself a parenthesized expression. The % on {expr%} tells the transformer: "Stop and fully evaluate the inner expression ({exp}) first, then substitute its result back here." RewindOnChange then allows the simplified outer expression to be re-evaluated.
Step 4: Putting It All Together
When an expression like (+ 1 (* 2 3)) is processed:
- Pass 1: The nesting rule matches. The
%modifier forces the inner part(* 2 3)to be evaluated first. The base case rule transforms(* 2 3)into6. - The expression becomes
(+ 1 6). - Rewind: The transformer re-scans the new string
(+ 1 6). - Pass 2: The base case rule
({op:1} {a:1} {b:1})matches(+ 1 6)and evaluates it to7.
⚖️ Why uCalc? (Comparative Analysis)
Building a LISP interpreter from scratch is a classic computer science exercise that involves:
- Lexical Analysis: Writing a tokenizer to handle parentheses, symbols, and numbers.
- Syntactic Analysis: Building a parser to construct an Abstract Syntax Tree (AST) from the token stream.
- Evaluation: Writing an evaluator that walks the AST to compute the result.
uCalc allows us to skip almost all of this work. We leverage:
- uCalc's built-in, high-performance tokenizer.
- uCalc's powerful Transformer to handle the syntactic analysis (parsing) declaratively.
- uCalc's mature evaluation engine to handle the final computation.
We are simply building a lightweight "syntactic frontend" that translates one syntax into another that the engine already understands. This dramatically reduces the complexity and amount of code required.
Examples
A simple LISP transpiler that handles only binary operators.
using uCalcSoftware;
var uc = new uCalc();
var t = uc.ExpressionTransformer;
t.FromTo("({op:1} {a:1} {b:1})", "({a} {op} {b})");
Console.WriteLine("LISP: (+ 10 20)");
Console.WriteLine($"uCalc: {t.Transform("(+ 10 20)")}");
Console.WriteLine($"Result: {uc.Eval("(+ 10 20)")}");
LISP: (+ 10 20)
uCalc: (10 + 20)
Result: 30 using uCalcSoftware; var uc = new uCalc(); var t = uc.ExpressionTransformer; t.FromTo("({op:1} {a:1} {b:1})", "({a} {op} {b})"); Console.WriteLine("LISP: (+ 10 20)"); Console.WriteLine($"uCalc: {t.Transform("(+ 10 20)")}"); Console.WriteLine($"Result: {uc.Eval("(+ 10 20)")}");
#include
#include "uCalc.h"
using namespace std;
using namespace uCalcSoftware;
int main() {
uCalc uc;
auto t = uc.ExpressionTransformer();
t.FromTo("({op:1} {a:1} {b:1})", "({a} {op} {b})");
cout << "LISP: (+ 10 20)" << endl;
cout << "uCalc: " << t.Transform("(+ 10 20)") << endl;
cout << "Result: " << uc.Eval("(+ 10 20)") << endl;
}
LISP: (+ 10 20)
uCalc: (10 + 20)
Result: 30 #include <iostream> #include "uCalc.h" using namespace std; using namespace uCalcSoftware; int main() { uCalc uc; auto t = uc.ExpressionTransformer(); t.FromTo("({op:1} {a:1} {b:1})", "({a} {op} {b})"); cout << "LISP: (+ 10 20)" << endl; cout << "uCalc: " << t.Transform("(+ 10 20)") << endl; cout << "Result: " << uc.Eval("(+ 10 20)") << endl; }
Imports System
Imports uCalcSoftware
Public Module Program
Public Sub Main()
Dim uc As New uCalc()
Dim t = uc.ExpressionTransformer
t.FromTo("({op:1} {a:1} {b:1})", "({a} {op} {b})")
Console.WriteLine("LISP: (+ 10 20)")
Console.WriteLine($"uCalc: {t.Transform("(+ 10 20)")}")
Console.WriteLine($"Result: {uc.Eval("(+ 10 20)")}")
End Sub
End Module
LISP: (+ 10 20)
uCalc: (10 + 20)
Result: 30 Imports System Imports uCalcSoftware Public Module Program Public Sub Main() Dim uc As New uCalc() Dim t = uc.ExpressionTransformer t.FromTo("({op:1} {a:1} {b:1})", "({a} {op} {b})") Console.WriteLine("LISP: (+ 10 20)") Console.WriteLine($"uCalc: {t.Transform("(+ 10 20)")}") Console.WriteLine($"Result: {uc.Eval("(+ 10 20)")}") End Sub End Module
A practical LISP interpreter that handles variadic (multiple arguments) and nested expressions.
using uCalcSoftware;
var uc = new uCalc();
var t = uc.NewTransformer();
t.DefaultRuleSet.RewindOnChange = true;
// {@@Eval} evaluates the expression obtained by concatinating the captured
// elements {op} for operator, and {a} and {b} for the two numbers.
// These elements are strings and do not need curly braces within {@@Eval}
// :1 tels it to capture one token at a time.
t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}");
// If there are more than to numbers, as indicated by {more}, then the first
// two numbers are evaluated and put back into the list. The operator remains.
t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})");
// If a nested expression is present, it is evaluated immediately, by using
// % in {expr%} and the section is reprocessed again with the resulting value
// since .@RewindOnChange(true) was set.
t.FromTo("({op:1} {expr%: ({exp})}", "({op} {expr}");
t.FromTo("({op:1} {a:1} {expr%: ({exp})}", "({op} {a} {expr}");
// --- Test Cases ---
Console.WriteLine("--- Simple Binary ---");
var expr = "(- 100 25)";
Console.WriteLine($"LISP: {expr}");
Console.WriteLine($"Result: {t.Transform(expr)}");
Console.WriteLine("");
Console.WriteLine("--- Variadic (Multiple Args) ---");
expr = "(* 2 3 4)";
Console.WriteLine($"LISP: {expr}");
Console.WriteLine($"Result: {t.Transform(expr)}");
Console.WriteLine("");
Console.WriteLine("--- Nested Expressions ---");
expr = "(/ 100 (+ 5 5))";
Console.WriteLine($"LISP: {expr}");
Console.WriteLine($"Result: {t.Transform(expr)}");
expr = "(+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))";
Console.WriteLine($"LISP: {expr}");
Console.WriteLine($"Result: {t.Transform(expr)}");
Console.WriteLine("");
--- Simple Binary ---
LISP: (- 100 25)
Result: 75
--- Variadic (Multiple Args) ---
LISP: (* 2 3 4)
Result: 24
--- Nested Expressions ---
LISP: (/ 100 (+ 5 5))
Result: 10
LISP: (+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))
Result: 270 using uCalcSoftware; var uc = new uCalc(); var t = uc.NewTransformer(); t.DefaultRuleSet.RewindOnChange = true; // {@@Eval} evaluates the expression obtained by concatinating the captured // elements {op} for operator, and {a} and {b} for the two numbers. // These elements are strings and do not need curly braces within {@@Eval} // :1 tels it to capture one token at a time. t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}"); // If there are more than to numbers, as indicated by {more}, then the first // two numbers are evaluated and put back into the list. The operator remains. t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})"); // If a nested expression is present, it is evaluated immediately, by using // % in {expr%} and the section is reprocessed again with the resulting value // since .@RewindOnChange(true) was set. t.FromTo("({op:1} {expr%: ({exp})}", "({op} {expr}"); t.FromTo("({op:1} {a:1} {expr%: ({exp})}", "({op} {a} {expr}"); // --- Test Cases --- Console.WriteLine("--- Simple Binary ---"); var expr = "(- 100 25)"; Console.WriteLine($"LISP: {expr}"); Console.WriteLine($"Result: {t.Transform(expr)}"); Console.WriteLine(""); Console.WriteLine("--- Variadic (Multiple Args) ---"); expr = "(* 2 3 4)"; Console.WriteLine($"LISP: {expr}"); Console.WriteLine($"Result: {t.Transform(expr)}"); Console.WriteLine(""); Console.WriteLine("--- Nested Expressions ---"); expr = "(/ 100 (+ 5 5))"; Console.WriteLine($"LISP: {expr}"); Console.WriteLine($"Result: {t.Transform(expr)}"); expr = "(+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))"; Console.WriteLine($"LISP: {expr}"); Console.WriteLine($"Result: {t.Transform(expr)}"); Console.WriteLine("");
#include
#include "uCalc.h"
using namespace std;
using namespace uCalcSoftware;
int main() {
uCalc uc;
auto t = uc.NewTransformer();
t.DefaultRuleSet().RewindOnChange(true);
// {@@Eval} evaluates the expression obtained by concatinating the captured
// elements {op} for operator, and {a} and {b} for the two numbers.
// These elements are strings and do not need curly braces within {@@Eval}
// :1 tels it to capture one token at a time.
t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}");
// If there are more than to numbers, as indicated by {more}, then the first
// two numbers are evaluated and put back into the list. The operator remains.
t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})");
// If a nested expression is present, it is evaluated immediately, by using
// % in {expr%} and the section is reprocessed again with the resulting value
// since .@RewindOnChange(true) was set.
t.FromTo("({op:1} {expr%: ({exp})}", "({op} {expr}");
t.FromTo("({op:1} {a:1} {expr%: ({exp})}", "({op} {a} {expr}");
// --- Test Cases ---
cout << "--- Simple Binary ---" << endl;
auto expr = "(- 100 25)";
cout << "LISP: " << expr << endl;
cout << "Result: " << t.Transform(expr) << endl;
cout << "" << endl;
cout << "--- Variadic (Multiple Args) ---" << endl;
expr = "(* 2 3 4)";
cout << "LISP: " << expr << endl;
cout << "Result: " << t.Transform(expr) << endl;
cout << "" << endl;
cout << "--- Nested Expressions ---" << endl;
expr = "(/ 100 (+ 5 5))";
cout << "LISP: " << expr << endl;
cout << "Result: " << t.Transform(expr) << endl;
expr = "(+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))";
cout << "LISP: " << expr << endl;
cout << "Result: " << t.Transform(expr) << endl;
cout << "" << endl;
}
--- Simple Binary ---
LISP: (- 100 25)
Result: 75
--- Variadic (Multiple Args) ---
LISP: (* 2 3 4)
Result: 24
--- Nested Expressions ---
LISP: (/ 100 (+ 5 5))
Result: 10
LISP: (+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))
Result: 270 #include <iostream> #include "uCalc.h" using namespace std; using namespace uCalcSoftware; int main() { uCalc uc; auto t = uc.NewTransformer(); t.DefaultRuleSet().RewindOnChange(true); // {@@Eval} evaluates the expression obtained by concatinating the captured // elements {op} for operator, and {a} and {b} for the two numbers. // These elements are strings and do not need curly braces within {@@Eval} // :1 tels it to capture one token at a time. t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}"); // If there are more than to numbers, as indicated by {more}, then the first // two numbers are evaluated and put back into the list. The operator remains. t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})"); // If a nested expression is present, it is evaluated immediately, by using // % in {expr%} and the section is reprocessed again with the resulting value // since .@RewindOnChange(true) was set. t.FromTo("({op:1} {expr%: ({exp})}", "({op} {expr}"); t.FromTo("({op:1} {a:1} {expr%: ({exp})}", "({op} {a} {expr}"); // --- Test Cases --- cout << "--- Simple Binary ---" << endl; auto expr = "(- 100 25)"; cout << "LISP: " << expr << endl; cout << "Result: " << t.Transform(expr) << endl; cout << "" << endl; cout << "--- Variadic (Multiple Args) ---" << endl; expr = "(* 2 3 4)"; cout << "LISP: " << expr << endl; cout << "Result: " << t.Transform(expr) << endl; cout << "" << endl; cout << "--- Nested Expressions ---" << endl; expr = "(/ 100 (+ 5 5))"; cout << "LISP: " << expr << endl; cout << "Result: " << t.Transform(expr) << endl; expr = "(+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))"; cout << "LISP: " << expr << endl; cout << "Result: " << t.Transform(expr) << endl; cout << "" << endl; }
Imports System
Imports uCalcSoftware
Public Module Program
Public Sub Main()
Dim uc As New uCalc()
Dim t = uc.NewTransformer()
t.DefaultRuleSet.RewindOnChange = true
'// {@@Eval} evaluates the expression obtained by concatinating the captured
'// elements {op} for operator, and {a} and {b} for the two numbers.
'// These elements are strings and do not need curly braces within {@@Eval}
'// :1 tels it to capture one token at a time.
t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}")
'// If there are more than to numbers, as indicated by {more}, then the first
'// two numbers are evaluated and put back into the list. The operator remains.
t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})")
'// If a nested expression is present, it is evaluated immediately, by using
'// % in {expr%} and the section is reprocessed again with the resulting value
'// since .@RewindOnChange(true) was set.
t.FromTo("({op:1} {expr%: ({exp})}", "({op} {expr}")
t.FromTo("({op:1} {a:1} {expr%: ({exp})}", "({op} {a} {expr}")
'// --- Test Cases ---
Console.WriteLine("--- Simple Binary ---")
Dim expr = "(- 100 25)"
Console.WriteLine($"LISP: {expr}")
Console.WriteLine($"Result: {t.Transform(expr)}")
Console.WriteLine("")
Console.WriteLine("--- Variadic (Multiple Args) ---")
expr = "(* 2 3 4)"
Console.WriteLine($"LISP: {expr}")
Console.WriteLine($"Result: {t.Transform(expr)}")
Console.WriteLine("")
Console.WriteLine("--- Nested Expressions ---")
expr = "(/ 100 (+ 5 5))"
Console.WriteLine($"LISP: {expr}")
Console.WriteLine($"Result: {t.Transform(expr)}")
expr = "(+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))"
Console.WriteLine($"LISP: {expr}")
Console.WriteLine($"Result: {t.Transform(expr)}")
Console.WriteLine("")
End Sub
End Module
--- Simple Binary ---
LISP: (- 100 25)
Result: 75
--- Variadic (Multiple Args) ---
LISP: (* 2 3 4)
Result: 24
--- Nested Expressions ---
LISP: (/ 100 (+ 5 5))
Result: 10
LISP: (+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))
Result: 270 Imports System Imports uCalcSoftware Public Module Program Public Sub Main() Dim uc As New uCalc() Dim t = uc.NewTransformer() t.DefaultRuleSet.RewindOnChange = true '// {@@Eval} evaluates the expression obtained by concatinating the captured '// elements {op} for operator, and {a} and {b} for the two numbers. '// These elements are strings and do not need curly braces within {@@Eval} '// :1 tels it to capture one token at a time. t.FromTo("({op:1} {a:1} {b:1})", "{@@Eval: a + op + b}") '// If there are more than to numbers, as indicated by {more}, then the first '// two numbers are evaluated and put back into the list. The operator remains. t.FromTo("({op:1} {a:1} {b:1} {more})", "({op} {@@Eval: a + op + b} {more})") '// If a nested expression is present, it is evaluated immediately, by using '// % in {expr%} and the section is reprocessed again with the resulting value '// since .@RewindOnChange(true) was set. t.FromTo("({op:1} {expr%: ({exp})}", "({op} {expr}") t.FromTo("({op:1} {a:1} {expr%: ({exp})}", "({op} {a} {expr}") '// --- Test Cases --- Console.WriteLine("--- Simple Binary ---") Dim expr = "(- 100 25)" Console.WriteLine($"LISP: {expr}") Console.WriteLine($"Result: {t.Transform(expr)}") Console.WriteLine("") Console.WriteLine("--- Variadic (Multiple Args) ---") expr = "(* 2 3 4)" Console.WriteLine($"LISP: {expr}") Console.WriteLine($"Result: {t.Transform(expr)}") Console.WriteLine("") Console.WriteLine("--- Nested Expressions ---") expr = "(/ 100 (+ 5 5))" Console.WriteLine($"LISP: {expr}") Console.WriteLine($"Result: {t.Transform(expr)}") expr = "(+ (* (- 100 (/ 80 4)) 3) (- (* (+ 5 3) (- 12 7)) (+ (* 2 3) 4)))" Console.WriteLine($"LISP: {expr}") Console.WriteLine($"Result: {t.Transform(expr)}") Console.WriteLine("") End Sub End Module