Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix: LISP Interpreter in Kleis

This appendix presents a LISP interpreter written entirely in Kleis. This is a demonstration of Kleis’s power as a meta-language — proving that Kleis can parse and execute programs written in other languages.

Note: This is a proof-of-concept, not a production LISP implementation. See Known Limitations for details.

The interpreter includes:

  • Recursive descent S-expression parser
  • Full evaluator with special forms, arithmetic, comparisons, and list operations
  • Lexical closures with lambda
  • Recursive functions with letrec

Running in the REPL

$ cargo run --bin repl
🧮 Kleis REPL v1.0.0

λ> :load examples/meta-programming/lisp_parser.kleis
✅ Loaded: 2 files, 60 functions, 15 structures, 5 data types

λ> :eval run("(+ 2 3)")
✅ VNum(5)

λ> :eval run("(* 4 5)")  
✅ VNum(20)

λ> :eval run("(if (< 3 5) 100 200)")
✅ VNum(100)

λ> :eval run("((lambda (x) (* x x)) 7)")
✅ VNum(49)

λ> :eval run("(let ((x 10)) (+ x 5))")
✅ VNum(15)

Factorial

λ> :eval run("(letrec ((fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))) (fact 5))")
✅ VNum(120)

Fibonacci

λ> :eval run("(letrec ((fib (lambda (n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))))) (fib 10))")
✅ VNum(55)

Complete Source Code

The complete LISP interpreter is in examples/meta-programming/lisp_parser.kleis. Below is the full implementation.

Part 1: S-Expression Data Types

import "stdlib/prelude.kleis"

// S-Expression: atoms and lists
data SExpr =
    SAtom(value: String)
  | SList(elements: List(SExpr))

// Parser result: success with remaining input, or error
data ParseResult =
    ParseOK(expr: SExpr, rest: String)
  | ParseErr(message: String)

Part 2: Parser Helper Functions

// Check if character is whitespace
define is_ws(c: String) : Bool =
    or(eq(c, " "), or(eq(c, "\n"), eq(c, "\t")))

// Check if character is a delimiter
define is_delim(c: String) : Bool =
    or(is_ws(c), or(eq(c, "("), eq(c, ")")))

// Skip leading whitespace
define skip_ws(s: String) : String =
    if le(strlen(s), 0) then s
    else if is_ws(charAt(s, 0)) then skip_ws(substr(s, 1, strlen(s) - 1))
    else s

// Read atom characters until delimiter
define read_atom(s: String, acc: String) : ParseResult =
    if le(strlen(s), 0) then ParseOK(SAtom(acc), "")
    else if is_delim(charAt(s, 0)) then ParseOK(SAtom(acc), s)
    else read_atom(substr(s, 1, strlen(s) - 1), concat(acc, charAt(s, 0)))

Part 3: Recursive Descent Parser

// Parse a single S-expression
define parse_sexpr(s: String) : ParseResult =
    let trimmed = skip_ws(s) in
    if le(strlen(trimmed), 0) then ParseErr("Unexpected end of input")
    else if eq(charAt(trimmed, 0), "(") then 
        parse_list(substr(trimmed, 1, strlen(trimmed) - 1), Nil)
    else read_atom(trimmed, "")

// Parse list elements until ")"
define parse_list(s: String, acc: List(SExpr)) : ParseResult =
    let trimmed = skip_ws(s) in
    if le(strlen(trimmed), 0) then ParseErr("Expected ')'")
    else if eq(charAt(trimmed, 0), ")") then 
        ParseOK(SList(rev(acc)), substr(trimmed, 1, strlen(trimmed) - 1))
    else 
        match parse_sexpr(trimmed) {
            ParseOK(expr, rest) => parse_list(rest, Cons(expr, acc))
          | ParseErr(msg) => ParseErr(msg)
        }

// Reverse a list
define rev(xs: List(SExpr)) : List(SExpr) =
    rev_acc(xs, Nil)

define rev_acc(xs: List(SExpr), acc: List(SExpr)) : List(SExpr) =
    match xs {
        Nil => acc
      | Cons(h, t) => rev_acc(t, Cons(h, acc))
    }

// User-facing parse function
define parse(s: String) : SExpr =
    match parse_sexpr(s) {
        ParseOK(expr, rest) => expr
      | ParseErr(msg) => SAtom(concat("Error: ", msg))
    }

Part 4: LISP Value Types and Environment

// Values in our LISP
data LispVal =
    VNum(n: ℤ)                              // Integer
  | VSym(s: String)                         // Symbol (for errors/unbound)
  | VList(xs: List(LispVal))                // List value
  | VBool(b: Bool)                          // Boolean
  | VLambda(params: List(String), body: SExpr, env: Env)  // Closure

// Environment: list of (name, value) bindings
data Binding = Bind(name: String, val: LispVal)
data Env = Env(bindings: List(Binding))

// Empty environment
define empty_env : Env = Env(Nil)

// Look up a variable in the environment
define lookup(name: String, env: Env) : LispVal =
    match env {
        Env(bindings) => lookup_list(name, bindings)
    }

define lookup_list(name: String, bs: List(Binding)) : LispVal =
    match bs {
        Nil => VSym(concat("Unbound: ", name))
      | Cons(Bind(n, v), rest) => 
            if eq(n, name) then v else lookup_list(name, rest)
    }

// Extend environment with a new binding
define extend(name: String, val: LispVal, env: Env) : Env =
    match env {
        Env(bindings) => Env(Cons(Bind(name, val), bindings))
    }

// Extend with multiple bindings (for function application)
define extend_all(names: List(String), vals: List(LispVal), env: Env) : Env =
    match names {
        Nil => env
      | Cons(n, ns) => 
            match vals {
                Nil => env
              | Cons(v, vs) => extend_all(ns, vs, extend(n, v, env))
            }
    }

Part 5: Integer Parsing

define is_digit_char(c: String) : Bool =
    or(eq(c, "0"), or(eq(c, "1"), or(eq(c, "2"), or(eq(c, "3"), or(eq(c, "4"),
    or(eq(c, "5"), or(eq(c, "6"), or(eq(c, "7"), or(eq(c, "8"), eq(c, "9"))))))))))

define is_number_str(s: String) : Bool =
    if le(strlen(s), 0) then false
    else if eq(charAt(s, 0), "-") then 
        if le(strlen(s), 1) then false 
        else all_digits(substr(s, 1, strlen(s) - 1))
    else all_digits(s)

define all_digits(s: String) : Bool =
    if le(strlen(s), 0) then true
    else if is_digit_char(charAt(s, 0)) then all_digits(substr(s, 1, strlen(s) - 1))
    else false

define parse_int(s: String) : ℤ =
    if eq(charAt(s, 0), "-") then 0 - parse_int_pos(substr(s, 1, strlen(s) - 1))
    else parse_int_pos(s)

define parse_int_pos(s: String) : ℤ =
    parse_int_acc(s, 0)

define parse_int_acc(s: String, acc: ℤ) : ℤ =
    if le(strlen(s), 0) then acc
    else 
        let d = digit_val(charAt(s, 0)) in
        parse_int_acc(substr(s, 1, strlen(s) - 1), acc * 10 + d)

define digit_val(c: String) : ℤ =
    if eq(c, "0") then 0 else if eq(c, "1") then 1 else if eq(c, "2") then 2
    else if eq(c, "3") then 3 else if eq(c, "4") then 4 else if eq(c, "5") then 5
    else if eq(c, "6") then 6 else if eq(c, "7") then 7 else if eq(c, "8") then 8
    else 9

Part 6: Main Evaluator

define eval_lisp(expr: SExpr, env: Env) : LispVal =
    match expr {
        SAtom(s) => 
            if is_number_str(s) then VNum(parse_int(s))
            else if eq(s, "true") then VBool(true)
            else if eq(s, "false") then VBool(false)
            else lookup(s, env)
      | SList(elements) => eval_list(elements, env)
    }

define eval_list(elements: List(SExpr), env: Env) : LispVal =
    match elements {
        Nil => VList(Nil)  // Empty list is a value
      | Cons(head, rest) => eval_form(head, rest, env)
    }

// Evaluate a special form or function call
define eval_form(head: SExpr, args: List(SExpr), env: Env) : LispVal =
    match head {
        SAtom(op) => 
            // Special forms
            if eq(op, "if") then eval_if(args, env)
            else if eq(op, "quote") then eval_quote(args)
            else if eq(op, "lambda") then eval_lambda(args, env)
            else if eq(op, "let") then eval_let(args, env)
            else if eq(op, "letrec") then eval_letrec(args, env)
            // Arithmetic
            else if eq(op, "+") then eval_add(args, env)
            else if eq(op, "-") then eval_sub(args, env)
            else if eq(op, "*") then eval_mul(args, env)
            else if eq(op, "/") then eval_div(args, env)
            // Comparison
            else if eq(op, "<") then eval_lt(args, env)
            else if eq(op, ">") then eval_gt(args, env)
            else if eq(op, "=") then eval_eq(args, env)
            else if eq(op, "<=") then eval_le(args, env)
            else if eq(op, ">=") then eval_ge(args, env)
            // List operations
            else if eq(op, "list") then eval_list_op(args, env)
            else if eq(op, "car") then eval_car(args, env)
            else if eq(op, "cdr") then eval_cdr(args, env)
            else if eq(op, "cons") then eval_cons(args, env)
            else if eq(op, "null?") then eval_null(args, env)
            // Function call
            else eval_call(op, args, env)
      | SList(_) => 
            // First element is an expression (e.g., lambda)
            let fn_val = eval_lisp(head, env) in
            eval_apply(fn_val, args, env)
    }

Part 7: Special Forms

define eval_if(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(cond, Cons(then_br, Cons(else_br, Nil))) =>
            let cv = eval_lisp(cond, env) in
            if is_truthy(cv) then eval_lisp(then_br, env) else eval_lisp(else_br, env)
      | _ => VSym("Error: if requires 3 arguments")
    }

define is_truthy(v: LispVal) : Bool =
    match v {
        VBool(b) => b
      | VNum(n) => not(eq(n, 0))
      | VList(Nil) => false
      | VList(_) => true
      | VSym(_) => false
      | VLambda(_, _, _) => true
    }

define eval_quote(args: List(SExpr)) : LispVal =
    match args {
        Cons(expr, Nil) => sexpr_to_val(expr)
      | _ => VSym("Error: quote requires 1 argument")
    }

define sexpr_to_val(expr: SExpr) : LispVal =
    match expr {
        SAtom(s) => if is_number_str(s) then VNum(parse_int(s)) else VSym(s)
      | SList(elements) => VList(map_sexpr_to_val(elements))
    }

define map_sexpr_to_val(xs: List(SExpr)) : List(LispVal) =
    match xs {
        Nil => Nil
      | Cons(h, t) => Cons(sexpr_to_val(h), map_sexpr_to_val(t))
    }

define eval_lambda(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(SList(params), Cons(body, Nil)) =>
            VLambda(extract_param_names(params), body, env)
      | _ => VSym("Error: lambda requires (params) body")
    }

define extract_param_names(params: List(SExpr)) : List(String) =
    match params {
        Nil => Nil
      | Cons(SAtom(name), rest) => Cons(name, extract_param_names(rest))
      | _ => Nil
    }

define eval_let(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(SList(bindings), Cons(body, Nil)) =>
            let new_env = eval_let_bindings(bindings, env) in
            eval_lisp(body, new_env)
      | _ => VSym("Error: let requires ((bindings)) body")
    }

define eval_let_bindings(bindings: List(SExpr), env: Env) : Env =
    match bindings {
        Nil => env
      | Cons(SList(Cons(SAtom(name), Cons(val_expr, Nil))), rest) =>
            let val = eval_lisp(val_expr, env) in
            eval_let_bindings(rest, extend(name, val, env))
      | _ => env
    }

// letrec: evaluate lambda in an environment that already contains the binding
// This enables recursion: (letrec ((fact (lambda (n) ...))) (fact 5))
define eval_letrec(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(SList(bindings), Cons(body, Nil)) =>
            let rec_env = eval_letrec_bindings(bindings, env) in
            eval_lisp(body, rec_env)
      | _ => VSym("Error: letrec requires ((bindings)) body")
    }

define eval_letrec_bindings(bindings: List(SExpr), env: Env) : Env =
    match bindings {
        Nil => env
      | Cons(SList(Cons(SAtom(name), Cons(SList(Cons(SAtom(lambda_kw), 
            Cons(SList(params), Cons(body, Nil)))), Nil))), rest) =>
            let dummy_env = extend(name, VSym("placeholder"), env) in
            let lambda_val = VLambda(extract_param_names(params), body, dummy_env) in
            let new_env = extend(name, lambda_val, env) in
            let fixed_lambda = VLambda(extract_param_names(params), body, new_env) in
            eval_letrec_bindings(rest, extend(name, fixed_lambda, env))
      | _ => env
    }

Part 8: Arithmetic Operations

define eval_add(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VNum(x + y)
                  | _ => VSym("Error: + requires numbers")
                }
              | _ => VSym("Error: + requires numbers")
            }
      | _ => VSym("Error: + requires 2 arguments")
    }

define eval_sub(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VNum(x - y)
                  | _ => VSym("Error: - requires numbers")
                }
              | _ => VSym("Error: - requires numbers")
            }
      | _ => VSym("Error: - requires 2 arguments")
    }

define eval_mul(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VNum(x * y)
                  | _ => VSym("Error: * requires numbers")
                }
              | _ => VSym("Error: * requires numbers")
            }
      | _ => VSym("Error: * requires 2 arguments")
    }

define eval_div(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => if eq(y, 0) then VSym("Error: division by zero") 
                               else VNum(x / y)
                  | _ => VSym("Error: / requires numbers")
                }
              | _ => VSym("Error: / requires numbers")
            }
      | _ => VSym("Error: / requires 2 arguments")
    }

Part 9: Comparison Operations

define eval_lt(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VBool(lt(x, y))
                  | _ => VSym("Error: < requires numbers")
                }
              | _ => VSym("Error: < requires numbers")
            }
      | _ => VSym("Error: < requires 2 arguments")
    }

define eval_gt(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VBool(gt(x, y))
                  | _ => VSym("Error: > requires numbers")
                }
              | _ => VSym("Error: > requires numbers")
            }
      | _ => VSym("Error: > requires 2 arguments")
    }

define eval_eq(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VBool(eq(x, y))
                  | _ => VSym("Error: = requires numbers")
                }
              | _ => VSym("Error: = requires numbers")
            }
      | _ => VSym("Error: = requires 2 arguments")
    }

define eval_le(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VBool(le(x, y))
                  | _ => VSym("Error: <= requires numbers")
                }
              | _ => VSym("Error: <= requires numbers")
            }
      | _ => VSym("Error: <= requires 2 arguments")
    }

define eval_ge(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(a, Cons(b, Nil)) =>
            match eval_lisp(a, env) {
                VNum(x) => match eval_lisp(b, env) {
                    VNum(y) => VBool(ge(x, y))
                  | _ => VSym("Error: >= requires numbers")
                }
              | _ => VSym("Error: >= requires numbers")
            }
      | _ => VSym("Error: >= requires 2 arguments")
    }

Part 10: List Operations

define eval_list_op(args: List(SExpr), env: Env) : LispVal =
    VList(eval_all(args, env))

define eval_all(args: List(SExpr), env: Env) : List(LispVal) =
    match args {
        Nil => Nil
      | Cons(h, t) => Cons(eval_lisp(h, env), eval_all(t, env))
    }

define eval_car(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(lst, Nil) =>
            match eval_lisp(lst, env) {
                VList(Cons(h, _)) => h
              | _ => VSym("Error: car requires non-empty list")
            }
      | _ => VSym("Error: car requires 1 argument")
    }

define eval_cdr(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(lst, Nil) =>
            match eval_lisp(lst, env) {
                VList(Cons(_, t)) => VList(t)
              | _ => VSym("Error: cdr requires non-empty list")
            }
      | _ => VSym("Error: cdr requires 1 argument")
    }

define eval_cons(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(h, Cons(t, Nil)) =>
            let hv = eval_lisp(h, env) in
            match eval_lisp(t, env) {
                VList(lst) => VList(Cons(hv, lst))
              | _ => VSym("Error: cons requires list as second arg")
            }
      | _ => VSym("Error: cons requires 2 arguments")
    }

define eval_null(args: List(SExpr), env: Env) : LispVal =
    match args {
        Cons(lst, Nil) =>
            match eval_lisp(lst, env) {
                VList(Nil) => VBool(true)
              | VList(_) => VBool(false)
              | _ => VSym("Error: null? requires list")
            }
      | _ => VSym("Error: null? requires 1 argument")
    }

Part 11: Function Application

define eval_call(name: String, args: List(SExpr), env: Env) : LispVal =
    let fn_val = lookup(name, env) in
    eval_apply(fn_val, args, env)

define eval_apply(fn_val: LispVal, args: List(SExpr), env: Env) : LispVal =
    match fn_val {
        VLambda(params, body, closure_env) =>
            let arg_vals = eval_all(args, env) in
            // Merge current env into closure env for recursive calls
            let merged_env = merge_envs(env, closure_env) in
            let new_env = extend_all(params, arg_vals, merged_env) in
            eval_lisp(body, new_env)
      | VSym(msg) => VSym(msg)  // Error propagation
      | _ => VSym("Error: not a function")
    }

// Merge two environments: first takes precedence
// This allows letrec functions to see their own definitions
define merge_envs(e1: Env, e2: Env) : Env =
    match e1 {
        Env(b1) => match e2 {
            Env(b2) => Env(append_bindings(b1, b2))
        }
    }

define append_bindings(b1: List(Binding), b2: List(Binding)) : List(Binding) =
    match b1 {
        Nil => b2
      | Cons(h, t) => Cons(h, append_bindings(t, b2))
    }

Part 12: User-Facing Run Function

// Run a LISP program from string
define run(code: String) : LispVal =
    eval_lisp(parse(code), empty_env)

// Run with an environment (for multiple expressions)
define run_with_env(code: String, env: Env) : LispVal =
    eval_lisp(parse(code), env)

Known Limitations

This LISP interpreter is a demonstration, not a production-level implementation. It proves that Kleis can parse and execute programs, but has known limitations:

IssueDescriptionStatus
Trailing junk ignoredparse("(+ 1 2) garbage") parses successfully, ignoring “garbage”Known
Errors as atomsParse errors return SAtom("Error: ...") instead of a proper error typeKnown
No quote syntaxStandard LISP '(1 2 3) is not supported; use (list 1 2 3) insteadKnown
Limited special formsOnly if, lambda, let, letrec, define are implementedBy design
No macrosLISP macros are not supportedBy design
Integer-only arithmeticNo floating-point numbersBy design

Why These Limitations Exist

This interpreter demonstrates Kleis’s meta-language capabilities, not LISP completeness. The goal is to show:

  1. Kleis can parse arbitrary grammars using recursive descent
  2. Kleis can build and traverse ASTs (S-expressions as Kleis data types)
  3. Kleis can execute recursive programs via :eval
  4. Kleis is Turing-complete

For production LISP, use Racket, SBCL, or Clojure.


Summary

This LISP interpreter demonstrates that Kleis is Turing-complete and can serve as a host language for other programming languages. The implementation uses:

FeatureKleis Construct
Data typesdata SExpr, data LispVal, data Env
Pattern matchingmatch expr { ... }
RecursionRecursive function definitions
Higher-order functionslambda, closures with captured environments
String operationscharAt, substr, concat, strlen
List operationsCons, Nil, pattern matching on lists

Key Insights

  1. :eval enables execution — The :eval REPL command executes Kleis functions directly, without going through Z3’s symbolic unrolling.

  2. Environment merging for recursion — letrec works by merging the current environment (which contains the function binding) into the closure’s environment.

  3. 60 pure functions — The entire interpreter is implemented in ~560 lines of pure functional Kleis code.

  4. Meta-circular potential — With minor extensions, this could interpret a subset of Kleis itself, demonstrating meta-circularity.