Skip to content

MAA-98/Swisp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Swisp

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.

Status

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, and let
  • a built-in standard library for arithmetic, comparison, and JSON encoding/decoding

It does not aim to be a full Scheme/Common Lisp implementation.


Example

(do
  (let add (lambda (a b) (+ a b)))
  (let twice (\ (f x) (f (f x))))
  (twice add 10 5))

Result:

20

Another example:

(if (and true (> 10 3))
    "yes"
    "no")

Result:

"yes"

Installation

Swift Package Manager

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:

  • Swisp
  • SwispDomain
  • Tokenizer
  • Parser
  • IRepresenter
  • Interpreter

Quick start

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))
""")

Language overview

Literals

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]

Calls

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)

Special forms

if

(if condition then-expr else-expr)

Only false and nil are falsey. Everything else is truthy.

or

(or expr1 expr2 expr3)

Returns the first truthy value, or false if none is found.

and

(and expr1 expr2 expr3)

Returns the first falsey value, or the last value if all are truthy.

do

(do expr1 expr2 expr3)

Evaluates expressions in sequence and returns the value of the last one.

lambda / \

(lambda (x y) (+ x y))
(\ (x y) (+ x y))

Creates a function with lexical scope.

let

(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.


Standard library

Swisp currently ships with the following built-ins.

Arithmetic

  • + — 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.5

Numeric behavior:

  • arithmetic works with Int and Double
  • values are promoted to Double when needed
  • division preserves Int when exact, otherwise returns Double

Boolean

  • not
(not true) ; false
(not nil)  ; true

Comparison

  • =
  • >
  • >=
  • <
  • <=

Examples:

(= 1 1 1)        ; true
(< 1 2 3)        ; true
(>= 5 5 3)       ; true

Notes:

  • arrays and dictionaries are compared structurally for =
  • functions cannot be compared
  • numeric comparisons work across Int and Double

JSON

  • json-decode
  • json-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

Truthiness

In Swisp, only these values are falsey:

  • false
  • nil

Everything else is truthy, including:

  • 0
  • ""
  • []
  • [:]-style empty dictionaries
  • void / ()

Using the lower-level API

Swisp is split into layers, so you can use only the parts you need.

Pipeline

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)

Extending the environment

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)

Design notes

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 to void.
  • Top-level multiple expressions are not implicitly sequential; use (do ...).

Current limitations

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)

Project structure

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

Example values

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.


Why this project?

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.


License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.


Contributing

Issues and pull requests are welcome, especially for:

  • standard library additions
  • better error messages
  • comments / REPL support
  • tests
  • docs

About

A Swift-flavored LISP. In-process interpreted scripting language for Swift programs.

Resources

Stars

Watchers

Forks

Packages

Contributors

Languages