A minimal Lisp interpreter written in Swift.
Swisp is a compact, modular Lisp with:
- a tokenizer
- a parser
- an IR layer
- an interpreter
- a small standard library
- a simple top-level
eval(_:)API for embedding in Swift apps
It is designed to be easy to read, easy to extend, and pleasant to use in Swift apps.
Swisp is currently small and intentionally minimal; embedding in Swift apps is just a matter of adding it as a dependency. It only has SwiftLintPlugins as a dependency.
It already supports:
- numbers, strings, booleans,
nil - array and dictionary literals
- function calls
- lexical closures
- special forms like
if,and,or,do,lambda, andlet - a built-in standard library for arithmetic, comparison, and JSON encoding/decoding
It does not aim to be a full Scheme/Common Lisp implementation.
(do
(let add (lambda (a b) (+ a b)))
(let twice (\ (f x) (f (f x))))
(twice add 10 5))Result:
20Another example:
(if (and true (> 10 3))
"yes"
"no")Result:
"yes"Add Swisp to your Package.swift:
.package(url: "https://github.com/<your-username>/Swisp.git", from: "0.1.0")Then add the product to your target dependencies:
.product(name: "Swisp", package: "Swisp")If you want lower-level access, the package is split into modules such as:
SwispSwispDomainTokenizerParserIRepresenterInterpreter
The simplest API is the top-level eval(_:) function.
import Swisp
import SwispDomain
let value = try eval("(+ 1 2 3)")
switch value {
case .int(let n):
print(n) // 6
default:
print(value)
}If you want multiple expressions, wrap them in do:
let value = try eval("""
(do
(let x 10)
(let y 20)
(+ x y))
""")Swisp supports these literal types:
nil- integers:
1,-42 - doubles:
3.14,-0.5 - strings:
"hello" - booleans:
true,false - arrays:
[1, 2, 3] - dictionaries:
["name": "Marek", "age": 30]
Function application uses normal Lisp syntax:
(+ 1 2 3)
(* 2 5)
(json-decode "{\"a\":1}")Swisp is a LISP-1: functions and values live in the same namespace.
The first element of a list is evaluated and then applied as a function:
((lambda (x) (+ x 1)) 41)(if condition then-expr else-expr)Only false and nil are falsey. Everything else is truthy.
(or expr1 expr2 expr3)Returns the first truthy value, or false if none is found.
(and expr1 expr2 expr3)Returns the first falsey value, or the last value if all are truthy.
(do expr1 expr2 expr3)Evaluates expressions in sequence and returns the value of the last one.
(lambda (x y) (+ x y))
(\ (x y) (+ x y))Creates a function with lexical scope.
(let name value)Binds a value in the current environment.
Example:
(do
(let x 10)
(let y 20)
(+ x y))let does not allow rebinding the same name in the same environment.
Swisp currently ships with the following built-ins.
+— variadic addition*— variadic multiplication-— variadic subtraction, unary negation supported/— variadic division, unary reciprocal supported
Examples:
(+ 1 2 3) ; 6
(* 2 3 4) ; 24
(- 10 3 2) ; 5
(- 5) ; -5
(/ 20 5) ; 4
(/ 2) ; 0.5Numeric behavior:
- arithmetic works with
IntandDouble - values are promoted to
Doublewhen needed - division preserves
Intwhen exact, otherwise returnsDouble
not
(not true) ; false
(not nil) ; true=>>=<<=
Examples:
(= 1 1 1) ; true
(< 1 2 3) ; true
(>= 5 5 3) ; trueNotes:
- arrays and dictionaries are compared structurally for
= - functions cannot be compared
- numeric comparisons work across
IntandDouble
json-decodejson-encode
Examples:
(json-decode "{\"name\":\"Swisp\",\"version\":1}")
(json-encode ["name": "Swisp", "version": 1])Notes:
- decoded JSON objects become Swisp dictionaries
- JSON object keys must be strings when encoding
In Swisp, only these values are falsey:
falsenil
Everything else is truthy, including:
0""[][:]-style empty dictionariesvoid/()
Swisp is split into layers, so you can use only the parts you need.
source string
-> Tokenizer
-> SyntaxParser
-> SyntaxExpr
-> IRForm
-> Interpreter
-> Value
Example:
import SwispDomain
import Tokenizer
import Parser
import IRepresenter
import Interpreter
var tokenizer = Tokenizer("(+ 1 2)")
let tokens = try tokenizer.tokenize()
var parser = SyntaxParser(tokens)
let syntax = try parser.parse()
let ir = try syntax.asIRForm()
var interpreter = Interpreter(env: Environment.stdLib())
let result = try interpreter.evalIRForm(ir)You can inject your own values and built-ins from Swift.
import SwispDomain
import Interpreter
import Tokenizer
import Parser
import IRepresenter
let env = Environment.stdLib()
env.values["square"] = .function(.builtin { args in
guard args.count == 1 else {
throw Interpreter.RuntimeError.argumentCountMismatch(args.count)
}
guard case .int(let n) = args[0] else {
throw Interpreter.RuntimeError.typeError("square expects an Int")
}
return .int(n * n)
})
var tokenizer = Tokenizer("(square 9)")
let tokens = try tokenizer.tokenize()
var parser = SyntaxParser(tokens)
let syntax = try parser.parse()
let ir = try syntax.asIRForm()
var interpreter = Interpreter(env: env)
let result = try interpreter.evalIRForm(ir)A few semantics are worth calling out:
- Swisp evaluates function arguments eagerly.
- Special forms (
if,and,or,do,lambda,let) control evaluation. - User-defined functions capture their defining environment.
- Empty list
()evaluates tovoid. - Top-level multiple expressions are not implicitly sequential; use
(do ...).
Swisp is intentionally small. Some things it does not currently include:
- macros
- quoting / quasiquoting
- pattern matching
- mutation forms like
set! - comments in source
- tail-call optimization
- a REPL
- a full standard library
Also note:
- array/dictionary syntax is literal syntax, not general expression syntax
- dictionary keys in Swisp values must be hashable literal types (
Int,Double,String,Bool)
Sources/
├── Swisp/ # top-level eval API
├── SwispDomain/ # shared domain types
├── Tokenizer/ # source -> tokens
├── Parser/ # tokens -> syntax tree
├── IRepresenter/ # syntax tree -> IR
└── Interpreter/ # IR evaluator + stdlib
From Swift, evaluation returns a Value:
public enum Value {
case void
case nil
case int(Int)
case double(Double)
case string(String)
case bool(Bool)
case array([Value])
case dict([Hashables: Value])
case function(FunctionValue)
}That makes it straightforward to embed Swisp in Swift code and pattern-match on results.
Swisp exists to make embedded scripting in Swift apps simple.
A key motivation is providing a small, controllable scripting language for Swift apps that want programmable behavior while staying within App Store constraints. I have personally released macOS apps on the App Store that used Swisp.
The code is organized to make the implementation easy to understand:
- tokenize
- parse
- lower to IR
- interpret
If you want a tiny Lisp implementation in Swift to study, extend, or embed, this project is for you.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Issues and pull requests are welcome, especially for:
- standard library additions
- better error messages
- comments / REPL support
- tests
- docs