uCalc API Version: 5.7.0-preview.1 Released: 7/30/2026
Warning
uCalc API Preview Release Notice:This preview documentation describes the intended behavior of the API. It is not fully accurate or complete.The current preview build contains incomplete features, unoptimized performance, and is subject to breaking changes.Use of the preview version in your production code is not recommended.
A step-by-step tutorial on building a simple LISP interpreter using uCalc's ExpressionTransformer.
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.
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.
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.%): This variable modifier forces the transformer to evaluate nested patterns first, which is essential for our inside-out evaluation strategy.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.
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)(* 6 4), which is then evaluated by the base case rule to 24.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.
When an expression like (+ 1 (* 2 3)) is processed:
% modifier forces the inner part (* 2 3) to be evaluated first. The base case rule transforms (* 2 3) into 6.(+ 1 6).(+ 1 6).({op:1} {a:1} {b:1}) matches (+ 1 6) and evaluates it to 7.Building a LISP interpreter from scratch is a classic computer science exercise that involves:
uCalc allows us to skip almost all of this work. We leverage:
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.
ID: 1416
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
ID: 1417
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