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.
git clone https://github.com/Cocoanetics/SwiftScript.git
cd SwiftScript
swift build -c release
cp .build/release/swift-script /usr/local/bin/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 expressionOr with a shebang:
#!/usr/bin/env swift-script
print("hello from a script")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/switchwith 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 withComparable/Equatableconstraints. - 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,extensionon 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-sideCodable),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-sideawaitactually suspends. - Extras:
MathExtras(gcd/lcm/factorial/binomial,.clamped(to:),[Double]reductions: sum/product/average/median/variance/stdDev/ percentile),Concurrency,MirrorModule.MathExtrasships 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.
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.
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 ifFoois 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 /NotificationCenterselectors. 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
importof other.swiftfiles (only of bridged modules likeFoundation). - SwiftPM packages at runtime. You can't
importa 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.
Sources/SwiftScriptAST/— parser facade over swift-syntax + diagnosticsSources/SwiftScriptInterpreter/— the tree-walking evaluatorAPI/Interpreter.swift— entry point (eval(_:)is async)Execution/— one file per language featureModules/— built-in modules and the auto-generated Foundation bridgeBuiltins/— math, I/O, registry
Sources/swift-script/— the CLI binarySources/BridgeGeneratorTool/— readsswift-symbolgraph-extractJSON, filters by allowlist, emits bridge code (run viaTools/regen-foundation-bridge.sh)Tests/— 410+ unit tests, organized by feature areaExamples/— runnable sample scripts
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.shAfter 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 MathExtrasThe 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.
The interpreter is also a library (SwiftScriptInterpreter):
import SwiftScriptInterpreter
let interp = Interpreter()
let value = try await interp.eval("1 + 2 * 3")
print(value) // 7eval(_:) is async — there is no sync wrapper. If you need to call it from
sync code, hop through a Task.
- macOS 26+ — primary development target, all features.
- iOS 26+ — library targets build; the
swift-scriptCLI 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.
MIT — see LICENSE.
