Skip to content

PicoMLX/SwiftScript

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

75 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SwiftScript — a Swift interpreter written in Swift

SwiftScript

A tree-walking interpreter for Swift, written in Swift. Parses real Swift syntax via swift-syntax and walks the AST directly — no codegen, no compiler. Comes with a swift-script CLI for running .swift files or one-line expressions, and ships as a library you can embed in your own Mac, iOS, Linux, or Windows app.

This is the follow-up to SwiftBash: take the same idea — a real interpreter, no Process / fork / exec, sandbox-friendly — and apply it to Swift itself. Swift is much more elegant than bash at arithmetic, functions, and data structures (no shelling out to bc for math), and as a proof of concept this shows that Swift can be a very good scripting language.

The motivating use case: running Swift code generated by an LLM, inside an app, in a sandbox. You can't ship swiftc in an App Store binary — Apple won't allow downloaded code to be compiled and executed at runtime — but a tree-walking interpreter sidesteps that entirely. The same source can also run through stock swift via shebang for ahead-of-time use.

#!/usr/bin/env swift
import Foundation

let data = [4.0, 2.0, 5.0, 8.0, 1.0, 9.0, 3.0]

let sorted = data.sorted()
let median = sorted.count.isMultiple(of: 2)
    ? (sorted[sorted.count / 2 - 1] + sorted[sorted.count / 2]) / 2
    : sorted[sorted.count / 2]

let mean = data.reduce(0, +) / Double(data.count)
let variance = data.reduce(0) { $0 + ($1 - mean) * ($1 - mean) } / Double(data.count - 1)
let stdDev = sqrt(variance)

print("median:  ", median)
print("stdDev:  ", stdDev)
print("hypot:   ", hypot(3.0, 4.0))

Same source runs unchanged under stock swift (compiled by swiftc via the shebang) or under swift-script (interpreted, no compiler involved). The shebang #!/usr/bin/env swift works on any machine with a Swift toolchain installed; the interpreter is what lets you run the same code inside an app without a toolchain, without compilation, and without violating App Store rules.

Install

git clone https://github.com/Cocoanetics/SwiftScript.git
cd SwiftScript
swift build -c release
cp .build/release/swift-script /usr/local/bin/

Use

swift-script script.swift              # run a file
swift-script script.swift arg1 arg2    # forwards CLI args via CommandLine.arguments
swift-script -e '1 + 2 * 3'            # evaluate a single expression

Or with a shebang:

#!/usr/bin/env swift-script
print("hello from a script")

What works

A useful subset of Swift, large enough that most "small utility" scripts run unchanged:

  • Language core: let/var, control flow (if/else/guard/while/ repeat/for-in/switch with pattern matching), tuples, optionals (?/!/ if let/guard let/??), ranges, string interpolation, defer, throw/ try/catch/do.
  • Functions & closures: argument labels, default values, inout, variadics, trailing closures, @autoclosure, generic parameters with Comparable/Equatable constraints.
  • Types: struct, class (single inheritance, super, required init, willSet/didSet), enum (raw values, associated values, methods), protocol (declared and used in annotations — dynamic dispatch by runtime value, not statically checked), typealias, extension on builtins and on user types, key paths.
  • Stdlib: Int, Double, Bool, String, Array, Dictionary, Set, Optional, Range/ClosedRange, Result, Mirror, the usual operators and methods (map/filter/reduce/sorted/compactMap/etc.).
  • Foundation bridges (auto-generated from Apple's symbol graphs): URL, URLSession, URLComponents, Data, Date, DateFormatter, Calendar, JSONEncoder/JSONDecoder (with script-side Codable), FileManager, ProcessInfo, UUID, Regex, format styles, and several hundred more. ~220 generated bridge files cover most read-only Foundation APIs you reach for in scripts.
  • Async: async/await, Task, async bridged builtins (e.g. URLSession data tasks). Every step of evaluation is async, so script-side await actually suspends.
  • Extras: MathExtras (gcd/lcm/factorial/binomial, .clamped(to:), [Double] reductions: sum/product/average/median/variance/stdDev/ percentile), Concurrency, MirrorModule. MathExtras ships as a real Swift library too — see Custom modules below.

See Examples/ for runnable scripts, including ~40 "LLM probe" scripts in Examples/llm_probes/ that exercise the surface area end-to-end.

Parity with stock Swift

Scripts that use only standard Swift + Foundation APIs run unchanged under both swift (via shebang) and swift-script and produce identical output. The interpreter follows Swift semantics where it can — including numeric promotion in mixed Int/Double arithmetic — so common idioms like data.reduce(0, +) over [Double] work the same in both. Scripts in Examples/extras_demos/ deliberately use SwiftScript-only conveniences (MathExtras, Statistics) and won't run under stock swift; everything else is portable.

What does NOT work

This is an interpreter, not a Swift compiler. Things that intentionally don't work — and won't:

  • Static type checking. Type annotations are honored at coercion time (assignments, returns, arguments), but there is no type inference engine and no compile-time type errors. Mismatches surface as runtime errors.
  • Protocol witness checking. Protocols are accepted in annotations but conformance isn't verified — dispatch is dynamic. extension Foo: P { ... } works, but the compiler won't tell you if Foo is missing a requirement.
  • Generics beyond the basics. Generic functions with simple constraints work; full generic specialization, conditional conformances, opaque return types (some P), and primary associated types do not.
  • Property wrappers, result builders, macros, actors. Not implemented.
  • Objective-C interop / @objc / KVO / NotificationCenter selectors. The Foundation bridge is value-shaped: methods that return values, not ones that need a real Objective-C runtime.
  • Multi-file scripts. One file at a time. No import of other .swift files (only of bridged modules like Foundation).
  • SwiftPM packages at runtime. You can't import a third-party package from a script. The interpreter only sees the bridges it ships with.
  • Sendable / strict concurrency. Async works but the type system is not concurrency-checked.

If a script fails with cannot find 'X' in scope or an unimplemented-feature error, that's the boundary you've hit.

Project layout

  • Sources/SwiftScriptAST/ — parser facade over swift-syntax + diagnostics
  • Sources/SwiftScriptInterpreter/ — the tree-walking evaluator
    • API/Interpreter.swift — entry point (eval(_:) is async)
    • Execution/ — one file per language feature
    • Modules/ — built-in modules and the auto-generated Foundation bridge
    • Builtins/ — math, I/O, registry
  • Sources/swift-script/ — the CLI binary
  • Sources/BridgeGeneratorTool/ — reads swift-symbolgraph-extract JSON, filters by allowlist, emits bridge code (run via Tools/regen-foundation-bridge.sh)
  • Tests/ — 410+ unit tests, organized by feature area
  • Examples/ — runnable sample scripts

Custom modules

MathExtras is shipped both as an interpreter built-in (registered when the script writes import MathExtras) and as a real Swift library target. The same source therefore runs unchanged under both runtimes:

import Foundation
import MathExtras

let data = [4.0, 2.0, 5.0, 8.0, 1.0, 9.0, 3.0]
print("median:", data.median())
print("stdDev:", data.stdDev())
print("gcd:   ", gcd(48, 18))

Under swift-script the import MathExtras line activates the interpreter bridges. Under stock swift, install the module + dylib into the active toolchain once:

sudo bash Tools/install-mathextras.sh

After that, swift script.swift (or a #!/usr/bin/env swift shebang) finds MathExtras with no extra flags. The build embeds an autolink record into the .swiftmodule, so consumers never need -lMathExtras — just -I for the module and -L for the dylib:

BIN="$(swift build --show-bin-path)"
swift -I "$BIN/Modules" -L "$BIN" script.swift

…or shove the same flags into a shebang via env -S (absolute paths, so the script binds to your current build directory — fine for personal scripts, less portable than installing into the toolchain):

#!/usr/bin/env -S swift -I /abs/path/.build/.../debug/Modules -L /abs/path/.build/.../debug
import MathExtras

The same recipe works for any custom Swift module you want to share between scripts: SwiftScript resolves names the loaded interpreter knows about, and stock swift resolves them through the linker.

Embedding

The interpreter is also a library (SwiftScriptInterpreter):

import SwiftScriptInterpreter

let interp = Interpreter()
let value = try await interp.eval("1 + 2 * 3")
print(value)  // 7

eval(_:) is async — there is no sync wrapper. If you need to call it from sync code, hop through a Task.

Platforms

  • macOS 26+ — primary development target, all features.
  • iOS 26+ — library targets build; the swift-script CLI is macOS/Linux only.
  • Linux — library and CLI build against swift-corelibs-foundation. The Foundation bridge surface is narrower on Linux; some bridges (anything that depends on Apple-only Foundation types) are gated out.

License

MIT — see LICENSE.

About

A Swift Interpreter Library and CLI

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Swift 99.0%
  • Shell 1.0%