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.

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:

  1. 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.
  2. {@@Eval}: This directive evaluates a captured string as an expression. We'll use it to perform the arithmetic for each sub-expression.
  3. 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:

  1. (* 2 3 4) becomes (* 6 4)
  2. The transformer rewinds and finds (* 6 4), which is then evaluated by the base case rule to 24.

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:

  1. 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) into 6.
  2. The expression becomes (+ 1 6).
  3. Rewind: The transformer re-scans the new string (+ 1 6).
  4. Pass 2: The base case rule ({op:1} {a:1} {b:1}) matches (+ 1 6) and evaluates it to 7.

⚖️ Why uCalc? (Comparative Analysis)

Building a LISP interpreter from scratch is a classic computer science exercise that involves:

  1. Lexical Analysis: Writing a tokenizer to handle parentheses, symbols, and numbers.
  2. Syntactic Analysis: Building a parser to construct an Abstract Syntax Tree (AST) from the token stream.
  3. 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.

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
				
					#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;
}
				
			
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
				
			
LISP: (+ 10 20)
uCalc: (10 + 20)
Result: 30
A practical LISP interpreter that handles variadic (multiple arguments) and nested expressions.

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
				
					#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;
}
				
			
--- 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
				
			
--- 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