A pure-Swift JavaScript engine. A 1:1 port of QuickJS into ~70k lines of Swift, with a built-in DOM, fetch, IndexedDB, WebSockets, GPU-accelerated GC and regex via Metal, and an experimental quantum encoder.
The whole engine is open source — clone it, build it, ship it.
Want to support the project? Grab the official app on the App Store:
JeffJS Console
— iPhone · iPad · Mac · Vision Pro
·
JeffJS
— Apple Watch
Yes — that means you can run Shor's algorithm (and a dozen other quantum primitives) on your wrist.
import JeffJS
let env = JeffJSEnvironment()
env.onConsoleMessage = { level, msg in print("[\(level)] \(msg)") }
env.eval("""
const xs = [1, 2, 3, 4, 5];
console.log(xs.map(x => x * x).reduce((a, b) => a + b));
""")
// [log] 55JeffJS runs everywhere Swift runs — iOS, macOS, watchOS, tvOS, and visionOS — with no JavaScriptCore, no V8, no C dependencies. The whole engine is Swift source you can step through in Xcode.
Apple already ships JavaScriptCore. JeffJS exists because that's not enough:
- Pure Swift, end-to-end. No
JSC.framework, no FFI, no opaque blobs. Every opcode, every GC phase, every regex instruction is Swift you can read, breakpoint, and modify. The whole engine is inSources/JeffJS/. - Embeddable everywhere — including watchOS. JavaScriptCore isn't available on watchOS. JeffJS is. The
JeffJSConsolesample app ships an Apple Watch target that runs full JavaScript on your wrist. - Browser-shaped runtime out of the box.
JeffJSEnvironmentboots awindow/document/fetch/localStorage/IndexedDB/WebSocketworld without you wiring anything up. Drop in a script and it runs the way scripts expect to run. - GPU acceleration where it matters. Bacon–Rajan cycle collection runs as Metal compute kernels above ~5,000 live objects. Regex matching can dispatch an NFA to the GPU and test every starting position in parallel.
- A teaching artifact in the source tree. The
Quantum/module is an honest, working playground for procedural fields and lazy evaluation — read its README and push on the math. - 1:1 with QuickJS. Almost every file references the QuickJS source it ports. If you know QuickJS internals, you already know your way around JeffJS.
| Language coverage | ES2023: classes, modules, generators, async/await, destructuring, spread, template literals, optional chaining, nullish coalescing, BigInt, Symbol, Proxy, WeakRef, TypedArrays, RegExp with named capture groups |
| Runtime | Bytecode compiler + interpreter, shape-based inline caches, atom table, bytecode cache (.jfbc), Bacon–Rajan cycle GC |
| Standard library | Full Object / Array / String / Number / Math / Date / JSON / Map / Set / Promise / Error / Atomics / Iterator builtins |
| Browser surface | window, document, console, fetch, localStorage / sessionStorage, IndexedDB, WebSocket, setTimeout / setInterval, performance, crypto, TextEncoder / TextDecoder, URL, MessageChannel, structuredClone, queueMicrotask, atob / btoa, dynamic import() |
| DOM | HTML tokenizer + parser, full DOM tree, CSS selectors, events, mutation callbacks, computed styles |
| GPU | Metal-backed garbage collector, Metal-backed regex NFA, Metal-backed quantum search |
| Platforms | iOS 16+, macOS 13+, watchOS 9+, tvOS 16+, visionOS 1+ |
JeffJS is a Swift package. Add it in Xcode (File → Add Package Dependencies…) or in your Package.swift:
dependencies: [
.package(url: "https://github.com/jbachand/JeffJS.git", from: "1.0.0"),
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "JeffJS", package: "JeffJS"),
]
),
]Then import JeffJS and you're done. There are no system dependencies — the package is self-contained Swift plus a handful of Metal compute shaders.
import JeffJS
let env = JeffJSEnvironment()
let result = env.eval("Math.sqrt(2) ** 2")
switch result {
case .success(let str): print(str ?? "undefined") // "2.0000000000000004"
case .exception(let err): print("error:", err)
}env.onConsoleMessage = { level, message in
print("[\(level)] \(message)")
}
env.eval("""
console.log('hello from JS');
console.warn('watch out');
console.error(new Error('oh no'));
""")let env = JeffJSEnvironment()
env.eval("""
async function go() {
const r = await fetch('https://api.example.com/items');
const data = await r.json();
console.log(JSON.stringify(data).slice(0, 100));
}
go();
""")fetch, dynamic import(), WebSocket, IndexedDB, and timers all work the way the spec says they should — they run their work off the main thread and resolve their Promises through the JS event loop.
let env = JeffJSEnvironment()
env.eval("""
const div = document.createElement('div');
div.innerHTML = '<h1>Hello JeffJS</h1>';
document.body.appendChild(div);
document.querySelector('h1').textContent;
""")
// .success("Hello JeffJS")You also get the live DOMNode tree on the Swift side via env.document, plus an onDOMMutation callback when JS mutates it.
let config = JeffJSEnvironment.Configuration(
baseURL: URL(string: "https://example.com")!,
userAgent: "MyApp/1.0",
viewportWidth: 1024,
viewportHeight: 768,
storageScope: "my-app",
startupScripts: [
"globalThis.greet = (name) => `hello, ${name}`;"
]
)
let env = JeffJSEnvironment(configuration: config)
env.eval("greet('world')") // .success("hello, world")Sources/JeffJS/
├── Core/ Runtime, Context, GC, Object/Shape/Value/Atom/String — the engine heart
├── Parser/ Tokenizer + recursive-descent parser → AST
├── Bytecode/ AST → bytecode compiler, interpreter, opcode table, .jfbc serializer
├── Builtins/ Object, Array, String, Number, Date, JSON, Map, Set, Promise, …
├── StdLib/ console, timers, performance, URL, atob/btoa, structuredClone, …
├── Environment/ JeffJSEnvironment — turnkey browser-shaped runtime
├── DOM/ HTML parser, DOM tree, CSS selectors, events, fetch, WebSocket, IndexedDB, video, dynamic import bridges
├── RegExp/ Bytecode regex engine + Metal GPU NFA matcher
├── Unicode/ ES-spec Unicode tables
├── Quantum/ Experimental qubit-field encoder (see Sources/JeffJS/Quantum/README.md)
├── Resources/ Metal shaders + JeffJSConfig.plist
└── Utils/ dtoa, list utilities, C interop helpers
The pipeline is the QuickJS pipeline: source → tokens → AST → bytecode → interpreter, with shape-based property caches and a Bacon–Rajan cycle collector. Read any file in Core/ and you'll find a comment pointing at its QuickJS counterpart.
JeffJS opportunistically lifts hot work onto the GPU on Apple Silicon:
| Subsystem | Threshold | What it does |
|---|---|---|
GC (JeffJSMetalGC) |
> 5,000 live objects | Three Metal kernels run trial-decref → scan-rescue → collect-dead. Object graph is uploaded zero-copy on Apple Silicon. |
RegExp (JeffJSMetalRegex) |
Long inputs | Compiles the pattern to a flat NFA instruction set and tests every starting position in parallel. |
Quantum (QuantumGPU) |
Always when available | Three kernels — slice search, exact search, search-and-trace — parallelize the chain encoder across the thread grid. |
All Metal paths are wrapped in #if canImport(Metal) so the engine still builds and runs (CPU fallback) on platforms without it.
Compiled functions can be serialized to a portable .jfbc format with an atom remapping table, so a JeffJSRuntime can pre-load bytecode without re-parsing source. See Bytecode/JeffJSBytecodeCache.swift. Toggle via cache.bytecodeEnabled in the config plist.
Two distinct things live under window.jeffjs.quantum. Both are usable from any JeffJSEnvironment — including the watchOS app.
.simulator— a real state-vector simulator (≤ ~12 qubits), a stabilizer simulator that scales past a thousand qubits, Bell / CHSH experiments, and a handful of working textbook algorithms..sliceand.chain— the experimental "qubit-field" encoders. Honest teaching artifact about locality, not crypto, not compression. Read the paper and the Quantum README.
| Algorithm | JavaScript |
|---|---|
|
Shor's algorithm — factor an integer using QFT period-finding. |
const r = await jeffjs.quantum.simulator.shorFactor(15);
// { factors: [3, 5], N: 15, numQubits: 8, numTrials: 128 } |
|
Deutsch–Jozsa — distinguish constant from balanced functions in one query. |
const r = await jeffjs.quantum.simulator.deutschJozsa(4, 'balanced');
// { isConstant: false, measurements: [1, 0, 1, 1] } |
|
Bernstein–Vazirani — recover an N-bit secret string in a single oracle call. |
const r = await jeffjs.quantum.simulator.bernsteinVazirani('10110');
// { recovered: '10110' } |
|
Quantum teleportation — send a qubit state via a Bell pair + 2 classical bits. |
const r = await jeffjs.quantum.simulator.teleport();
// { success: true, bobOutcome: 1 } |
|
Superdense coding — send 2 classical bits with 1 qubit and a shared Bell pair. |
const r = await jeffjs.quantum.simulator.superdenseCoding(1, 0);
// { success: true, sent: [1,0], received: [1,0] } |
|
3-qubit error correction — flip a bit, detect the syndrome, undo the flip. |
const r = await jeffjs.quantum.simulator.errorCorrection(1);
// { corrected: true, errorQubit: 1, syndrome: [1,0] } |
| Operation | JavaScript |
|---|---|
|
State-vector circuit (≤ ~12 qubits). Gates: |
const sim = jeffjs.quantum.simulator;
const id = sim.createCircuit(2);
sim.gate(id, 'h', { qubit: 0 });
sim.gate(id, 'cnot', { control: 0, target: 1 });
sim.measureAll(id); // [0,0] or [1,1] — Bell pair
sim.destroyCircuit(id); |
|
Stabilizer simulator — Clifford-only, polynomial in qubit count, scales past 1000 qubits. |
const id = sim.createStabilizer(1000);
sim.stabBellPair(id, 0, 1);
sim.stabGHZ(id, [0, 1, 2, 3, 4]);
sim.stabMeasure(id, 0);
sim.destroyStabilizer(id); |
|
CHSH / Bell tests — compare classical bound (S=2) to Tsirelson's bound (S≈2.83). |
sim.chsh('boundQubitPair', 10000);
// { S: 2.823, noSignaling: true, variant: 'boundQubitPair', numTrials: 10000 }
sim.ghzCorrelation(3, [Math.PI/2, Math.PI/2, Math.PI/2], 10000);
// 0.998 |
The gap above the classical line is the quantum advantage, in numbers — measured live by the simulator.
| Encoder | JavaScript |
|---|---|
|
Slice encoder — multi-key envelope, no length cap. Each "slice" is a 25-bit address into a deterministic qubit field. |
const env = jeffjs.quantum.slice.encode('Hello, world!');
jeffjs.quantum.slice.decode(env); // 'Hello, world!'
// Inspect the raw addresses
jeffjs.quantum.slice.encodeRaw('hi');
// { keys: ['0x1A2B3C4', ...], length: 2, seedOffset: 0 } |
|
Chain encoder — resolution-deepening octree walk, one 64-hex master key for ≤ 6 chars. Async because it can backtrack. |
const k = await jeffjs.quantum.chain.encodeAsync('Hi');
await jeffjs.quantum.chain.decodeAsync(k); // 'Hi' |
The chain encoder walks an octree of resolutions — coarse to fine and back — collapsing the entire path into a single key. The shape is the hourglass.
The whole subsystem is gated on quantum.enabled in JeffJSConfig.plist and disabled cleanly in environments that don't want it.
JeffJSConsole/ is an Xcode project containing four targets that all link JeffJS:
- JeffJSConsole — iOS, iPad, macOS, and visionOS REPL with command history, dot-completion, an inline ghost-text suggestion buffer, persisted user globals, and a JS keyboard accessory bar.
- JeffJS Watch App — the same JavaScript engine, on your wrist. (JavaScriptCore won't run on watchOS. JeffJS does.)
- JeffJS Watch Widget — a WidgetKit extension that shows the most recent eval result on your watch face.
Open JeffJSConsole/JeffJSConsole.xcodeproj in Xcode and run any target.
All engine flags live in Sources/JeffJS/Resources/JeffJSConfig.plist. Highlights:
| Key | Default | Purpose |
|---|---|---|
optimize.enabled |
true |
Master switch for the bytecode optimizer. |
optimize.shortOpcodes |
true |
Use short-form opcodes where possible. |
cache.bytecodeEnabled |
false |
Persist compiled bytecode to disk. |
cache.bytecodeMaxSize |
1000000 |
Maximum cached bytecode size (bytes). |
stack.maxCallDepth |
200 |
JS call stack depth limit. |
stack.defaultSize |
1048576 |
Per-context stack buffer size. |
gc.mallocThreshold |
262144 |
Bytes allocated before a GC pass triggers. |
gc.metalThreshold |
5000 |
Object count above which the GPU GC takes over. |
regex.metalThreadGroupSize |
256 |
Metal regex thread group width. |
security.blockRemoteImports |
false |
Refuse import() of remote URLs. |
quantum.enabled |
true |
Install window.jeffjs.quantum in JeffJSEnvironment. |
quantum.preferGPU |
true |
Use Metal for quantum search when available. |
Read them from Swift via JeffJSConfig.optimizeEnabled, JeffJSConfig.gcMetalThreshold, etc.
The test target lives in Tests/JeffJSTests/:
JeffJSTestRunner.swift— a self-contained runner with 90+ test groups covering every major language feature: arithmetic, strings, arrays, classes, generators, async/await, modules, regex, Map/Set/WeakRef, Proxy, Symbol, TypedArrays, BigInt, the entire opcode surface, plus regression tests for every spec-compliance fix and a critical subset of test262.JeffJSPerfTests.swift— micro-benchmarks for hot paths.QuantumTests.swift— round-trip tests for the quantum encoder/decoder, both slice and chain variants.
Run them via SwiftPM:
swift testOr run the same test runner inside the JeffJSConsole app target — it streams results into the in-app console.
.
├── Package.swift SwiftPM manifest (iOS 16, macOS 13, watchOS 9, tvOS 16, visionOS 1)
├── Sources/JeffJS/ The engine
├── Tests/JeffJSTests/ Test runner, perf tests, quantum tests
├── JeffJSConsole/ Xcode project: REPL app + Watch app + Watch widget
├── Quantum/ Visualizations (qubit field GIF + still)
└── Screenshots/ Console screenshots
This is, fundamentally, one person's port. PRs are welcome but the bar is fidelity to QuickJS: if you're touching Core/, Parser/, Bytecode/, or any builtin, the change should be defensible against the QuickJS source it ports. New surface (DOM bridges, Web APIs, Metal kernels, the Quantum module) has more latitude — open an issue first if it's a big one.
Ground rules:
- No new external dependencies. The package is intentionally
import Foundationand (optionally)import Metal. Keep it that way. - Tests for everything that touches the language. Add a case to
JeffJSTestRunneror to one of the focused test files.swift testmust stay green. - Comment what's non-obvious. The QuickJS port is dense; future readers (including future-you) will thank you.
- QuickJS by Fabrice Bellard — the engine JeffJS is a port of. Every clever idea here belongs to that source tree; the bugs are mine.
- test262 — the conformance suite the spec compliance tests draw from.
- Apple Metal team — for making it possible to write a regex matcher and a garbage collector as compute kernels in two afternoons.
Copyright © 2026 Jeff Bachand. License TBD — see LICENSE once published.
JeffJS is a derivative work of QuickJS (MIT) and ports significant portions of its design and source. Any release will preserve the QuickJS attribution.
my name is jeff.