JaiScript · scripting for C++ engines

Edit. Save.
It's already running.

An embedded scripting language for C++ engines. It reads like the C++ you already write, copies the way C++ copies, stops typed mistakes before the program runs, reloads classes without losing the world, and splits a loop it can prove safe across worker threads with the serial loop's exact result. Underneath, two independent backends (a tree-walking interpreter and a bytecode VM) must print the same output and the same error text, byte for byte.

every standalone script sample on this site runs on both backends, and the output shown is what they print

  • C++ value semantics
  • hot reload keeps state
  • strict checker at load
  • coroutines
  • parallel_for
  • guides from JS / C++ / C#
gator.jairunning
// raise spd: a faster gator
class Gator : Mob {
    int spd = 1;
    int slices = 0;
    void tick() {
        step_toward(cake, spd);
        if (beside(cake)) { slices++; say("Nom."); }
    }
}
Gator gator;  // kept by every reload
void hot_reload_migrate(map<string, var> previous) {
    gator.spd = Gator().spd;  // live gator takes the edit
}
gator at spd 1 · slices 0

Get JaiScript

Clone the repository, build the runner with one command, and play Gator Quest on it. The build needs a C++20 compiler, CMake 3.25+ and Ninja; on Windows, run it from a Visual Studio developer prompt.

shell
git clone https://github.com/Devacor/jaiscript.git
cd jaiscript
cmake --workflow --preset runner
build/bin/jaiscript examples/gatorquest/scripts/gatorquest.jai

To embed it, add the folder as a CMake subproject, fetch it with FetchContent, install it as a package, or drop the sources into a Visual Studio or Xcode project: Building JaiScript into a Host has each route in full.

Two lines to hello

C++
auto e = jai::engine::make();
e->execute("print(\"hello\");");

Yes, really two lines: a bare engine ships a basic print. One more, jai::stdlib::register_all(e);, adds the formatted {} print and the standard library (math, containers, JSON). It is a static lib plus headers, with no global interpreter state.

The embedding chapters take it from there: bind a C++ class in a fluent chain (one declaration makes a field reflected, script-visible and serialized), connect host signals to script, give a script a budget and a memory cap, and hot-reload it. Every sample on this site assumes the stdlib is registered. The standalone runner does that for you, and the two-line host needs the register_all line first. Here is what the language itself looks like:

jaiscript
var anything = 5;            // dynamic — the explicit, visible opt-out
anything = "now a string";   // fine; you asked for this

class Hero {
    int hp = 10;
    void heal(int amount) { hp += amount; }
}
function bump(int& x) { x += 1; }   // mutation is visible at both ends

auto h = Hero();
h.heal(5);
bump(h.hp);            // fields and elements bind by reference too
print("hp = {}", h.hp);
output
hp = 16

No host yet? The repository ships a standalone runner. Save the script above as hello.jai and run jaiscript hello.jai. Strict checking and the VM backend are the defaults, jaiscript alone is a REPL, and jaiscript --watch hello.jai re-runs on every save. Building the runner is one CMake target; its flags are on the reference page. A web application is a host away too: jai_spider app.jai spins the app over HTTP, with SQLite, cookie sessions, static files and the spider:: client, and applies each saved edit to the next request (jai_spider).

Coming from another language

The Language Bridges start from what you already write. Each sets JavaScript, C++ or C# beside JaiScript idiom by idiom, then shows, running, the habits that get a different answer. C++ code moving from Cereal, Boost.Serialization, Boost.Signals2, ChaiScript, sol2, Squirrel, pybind11, nlohmann::json, Qt properties, RTTR or coroutine schedulers starts in the Code Migrations, which map each library's calls to JaiScript's and convert real code.

The seven pillars

01The type ladder

int x is enforced. auto x is inferred, then enforced. var x is dynamic, a choice you can see in a diff, not a default you fall into. The static checker (always on, always strict) maps onto it exactly: typed code gets load-time errors before a statement runs, var opts out (like TypeScript's any, but designed-in), and [[warn_mode]] lets a region's errors through as warnings.

The Type Ladder →

02Value semantics, references by declaration

Assignment, parameters, and returns copy. Nothing aliases at a distance; a function cannot mutate your data unless its signature says &. Deterministic saves, no spooky mutation, and to_json of any value means what you think it means.

Value Semantics →

03Two implementations, one executable spec

A tree-walking interpreter and a bytecode VM, required to agree byte-for-byte (including error text), enforced by more than 4,700 tests run against both, plus a differential fuzzer that files a bug whenever they disagree. Semantics cannot rot silently. The one documented exception is recursion depth: the backends cap it differently, and the VM reuses a bare self tail call's frame.

Backends →

04Zero global state

No statics, no singletons; every value carries its engine. Two engines on two threads need no locks, tests cannot contaminate each other, and in-language parallelism (parallel_for, parallel_transform, deterministic at any worker count) falls out of this property instead of fighting it.

Quickstart →

05Hot reload that never bricks

Redefine a class mid-session and live instances migrate: fields keep values, changed types convert permissively, new fields take their initializers. Migration never fails. A reload never bricks an instance; a source that does not parse or check simply does not load, and the old class keeps running. Code already running when the reload lands, a suspended coroutine included, finishes on the version it started with. The dungeon crawler reloads its own scripts mid-game on a keypress.

Hot Reload →

06Safe at the boundary, by construction

Beyond the core builtins, a script can only touch what the host registered. The binding surface is the allowlist. Checked integer overflow, catchable recursion limits and an execution budget are on by default; a memory cap is one call, and so is switching off the core's file loading (include, import), which is on by default. A hostile script cannot catch-and-swallow its way past the budget or the cap. Untrusted scripts are a configuration, not a rewrite, though it is not an OS sandbox.

Safety Rails →

07Parse once, run anywhere

engine->jaibite(source) parses once, executes many, and save("level.jaibite") ships precompiled: symbols relocate on load, so one file serves any engine. Even naive execute(str) in a loop pays the parse once via the source cache.

jaibite →

The receipts

4,700+tests green on both backends, every commit, plus a differential fuzzer that fails on any divergence between them
16 bytesscript_value size, static-assert-gated (four per cache line)
5 programsship in the repository: a raycast shooter, a dungeon crawler, a 17-scene demoreel, a 16-player terminal RPG, and over 11,000 lines of pure JaiScript reimplementing DOOM, WAD parser to BSP renderer, 16-player netplay with the wire protocol in script, inside the original's 35 Hz budget (on first launch it downloads the freely distributable Freedoom and DOOM shareware data from their public releases)
5 languagesthe raycast shooter reimplemented in Squirrel, Lua, ChaiScript and Python to one spec and one seeded RNG, each held to the JaiScript state hashes bit-exactly, so the comparison measures the runtimes and nothing else; the ports live in a separate comparison harness, and the measured rows are on the Performance page
always strictcompiler-style diagnostics with line/column before a single statement runs; [[warn_mode]] lets one file, block or statement through as warnings
measuredhot-loop, call, boot, and per-feature costs are quantified on one page, under one protocol: Performance

The honest tradeoffs

  • Copies cost. Value semantics means passing a big structure by value deep-copies it. The idiom is & parameters for anything heavy, cheap by design. If you write Python-shaped code expecting reference semantics, your first hour will surprise you; read Value Semantics first and it won't.
  • Lua is faster. On microbenchmarks the VM wins or ties Lua 5.4 on statements, containers, range-for and method calls; Lua runs a hot arithmetic loop about 3× faster and call-dense recursion about 2.8× faster. On whole programs Lua 5.4 is ahead by 1.2 to 1.6× on the games (Gator Quest, GLOOM) and by about 2.4× on the object-heavy benchmark apps (Richards, DeltaBlue), the VM is ahead on the raymarched OSSUARY scene and the bytecode-interpreter app, and LuaJIT is far ahead of both. JaiScript's VM does reuse the frame for a bare self tail call, so tail recursion runs at loop speed to any depth. The head-to-head numbers, one machine, one protocol: Performance.
  • Two backends cost maintenance. That's the price of the executable spec, and shared semantic kernels keep the duplicated surface small.

Who it's for

Anyone embedding scripting in a C++ engine who wants iteration speed (hot reload, script classes), safety rails a host can tighten in a few calls, types that scale from prototype (var) to production (int, checked strictly) within the same file, and a language whose behavior is enforced by two implementations arguing with each other. It was built alongside a real 2D game engine, hardened by adversarial review and differential fuzzing, and dogfooded by building real, playable things in it. The source is MIT-licensed, and Stability lists what is settled before 1.0 and what may still change. If you ship a game or a tool with JaiScript inside, a small line in its credits, such as “Scripted with JaiScript”, would be lovely to see. It is a request, not a condition: the MIT license is the only term that applies.