The Ten-Minute Tour​

One small game, built a piece at a time. Every block of code on this page was run through two independent engines, a tree-walking interpreter and a bytecode VM, and the output under it is what both of them printed.

It reads like the C++ you already write​

Classes with typed fields, constructors, methods, operator overloading. Strings that splice values in and format them on the way. If you have written a game in C++, you can read this without a manual.

jaiscript
class Vec2 {
    float x = 0.0; float y = 0.0;
    Vec2(float px, float py) { x = px; y = py; }
    Vec2 operator+(Vec2 o) { return Vec2(x + o.x, y + o.y); }
    float length() { return math::sqrt(squared()); }
private:
    float squared() { return x * x + y * y; }
}

auto ship = Vec2(3.0, 4.0);
auto next = ship + Vec2(1.0, -1.0);
print(`ship is ${ship.length():.1f} from home, heading for (${next.x}, ${next.y})`);
output
ship is 5.0 from home, heading for (4.0, 3.0)

x inside a method is the field, and `${expr:.1f}` formats like you think it should. JaiScript has no struct keyword, just classes, and since it is meant to be a simple scripting language, members are public by default; private: marks what only the class and its subclasses can touch.

A copy is a copy​

Value semantics as in C++: b = a copies an object, T& names the same one, and shared_ptr<T> shares it.

This is the one idea that makes JaiScript feel different from Lua, Python or JavaScript: objects are values, exactly as in C++. Assigning a ship gives you a second ship. When you want two names for one ship, you ask for a reference.

jaiscript
class Vec2 { float x = 0.0; float y = 0.0; }

Vec2 a;           // a Vec2, default-constructed
Vec2 b = a;        // a copy, like C++: two ships, not one
b.x = 100.0;

Vec2& pilot = a;   // a reference, when you mean it
pilot.y = 7.0;

print("a = ({}, {})   b = ({}, {})", a.x, a.y, b.x, b.y);
output
a = (0.0, 7.0)   b = (100.0, 0.0)

That is the whole spooky-action-at-a-distance category of bug, gone. A function you hand an enemy to cannot quietly teleport yours; if it should, its parameter says Enemy& and everyone can see it.

Mistakes die before the program runs​

Types are not decoration. The checker walks the whole program before the first statement executes, and a typed mistake stops the load:

jaiscript
class Ship { int hull = 100; string name = "Wanderer"; }

Ship s = Ship();
s.hull = "full";   // a string into an int field: caught before anything runs
output
<script>:4:8: error: cannot assign 'string' to field 'hull' declared 'int' (declared at 1:18)
    s.hull = "full";   // a string into an int field: caught before anything runs
           ^

Nothing ran. No half-initialized level, no bug report three hours into a playtest. When you genuinely want a dynamic value, var is the escape hatch, and the checker lets it through. The types you do write also make the code faster: the runtime trusts what the checker proved.

A swarm in twenty lines​

Arrays of class instances, loops that edit them in place through auto&, methods, and nothing you have to free.

jaiscript
class Enemy {
    string kind = ""; float x = 0.0; int hp = 0;
    Enemy(string k, float px, int h) { kind = k; x = px; hp = h; }
    void hit(int dmg) { hp -= dmg; }
    bool alive() { return hp > 0; }
}

array<Enemy> swarm = [Enemy("drone", 0.0, 30), Enemy("brute", 5.0, 120), Enemy("drone", 9.0, 30)];

for (int wave = 1; wave <= 3; ++wave) {
    for (auto& e : swarm) { e.hit(45); e.x -= 1.5; }
    int standing = 0;
    for (auto& e : swarm) { if (e.alive()) { standing += 1; } }
    print("wave {}: {} standing", wave, standing);
}
output
wave 1: 1 standing
wave 2: 1 standing
wave 3: 0 standing

array<Enemy> holds enemies by value: copy the array and you have a second swarm. On the VM a typed loop like this compiles into a register block that runs without leaving it.

Behavior that waits​

Game logic is full of things that happen over time: patrols, cutscenes, attack wind-ups. A coroutine lets you write them top to bottom, and yield pauses right where you are until the next frame asks for more.

jaiscript
coroutine string patrol(int laps) {
    for (int lap = 1; lap <= laps; ++lap) {
        yield `lap ${lap}: walk east`;
        yield `lap ${lap}: scan`;
        yield `lap ${lap}: walk west`;
    }
}

for (auto step : patrol(2)) { print(step); }
output
lap 1: walk east
lap 1: scan
lap 1: walk west
lap 2: walk east
lap 2: scan
lap 2: walk west

No state machine, no enum of phases, no switch (state) spread across a file. The loop counter is the state.

Change the code, keep the world​

Hot reload is not a file watcher bolted on the side. Running a class declaration again is the reload: every live instance keeps its data and picks up the new methods. Here is the turret as it ships,

with turret_v1.jai:

jaiscript · turret_v1.jai
class Turret {
    int shots = 0;
    string fire() { shots += 1; return `pew (shot ${shots})`; }
}

and the edit a designer makes mid-playtest,

with turret_v2.jai:

jaiscript · turret_v2.jai
class Turret {
    int shots = 0;
    string fire() { shots += 3; return `BRRRT x3 (shot ${shots})`; }
}

The game keeps running across the change:

jaiscript
include "turret_v1.jai";
var t = Turret();
print(t.fire());
print(t.fire());

include "turret_v2.jai";   // the edit lands: same turret, new trigger
print(t.fire());
output
pew (shot 1)
pew (shot 2)
BRRRT x3 (shot 5)

Same turret, same shot count, new trigger. A running game does the same by reaching its imports every frame: an import re-runs its module only when the file changed. Write that loop with its guard from the start. A reload the checker rejects raises a catchable error at the import, and the program keeps its old code:

jaiscript
array<string> MODULES = ["turret.jai", "rules.jai"];
while (running) {
    try {
        import(MODULES);   // re-runs a module whose file changed; an unchanged one does nothing
    } catch (e) {
        print("reload rejected: {}", e);   // the file and line of the mistake; the old code keeps running
    }
    step_world();
}

Fix the file and save again, and the loop picks it up within about 0.2 seconds. The world never restarts. Reloading from a running program has the whole story.

Every core, one loop​

Most scripting languages give you one thread and a lecture. parallel_for spreads a loop over every core, and the result is byte-identical to running it serially, at any thread count.

jaiscript
class Particle { float x = 0.0; float vx = 0.0; float life = 1.0; }

array<Particle> sparks = [];
for (int i = 0; i < 100000; ++i) { Particle p = Particle(); p.vx = (i % 7) - 3.0; sparks.push(p); }

parallel_for (auto& p : sparks) {   // every core, one loop, the same answer as serial
    p.x += p.vx * 0.016;
    p.life -= 0.016;
}

float sum = 0.0;
for (auto& p : sparks) { sum += p.x; }
print(`${sparks.size()} sparks stepped, drift ${sum:.3f}`);
output
100000 sparks stepped, drift -0.080

The runtime proves the body only touches its own element before it lets the workers loose; a loop that would race is rejected with an explanation instead of a heisenbug.

Hand it your engine​

Embedding is a header and a few lines. Functions and whole C++ classes become script-callable, with conversions derived from their signatures.

C++
#include <jaiscript/core/engine.hpp>
#include <jaiscript/stdlib/stdlib.hpp>

auto eng = jai::engine::make();
jai::stdlib::register_all(eng);
eng->add_function("screen_shake", [](jai::script_float amount) { camera.shake(amount); });

jai::dynamic_binder<Turret>(*eng, "Turret")      // your C++ class, callable from script
    .constructor<>()
    .method("fire", &Turret::fire)
    .property("shots", &Turret::shots)
    .build();

eng->execute_file("scripts/level1.jai");         // scripts see screen_shake and Turret like their own

From there: step through scripts in VS Code while the game runs, cap a script's time and memory so a bad mod cannot hang a frame, precompile scripts (.jaibite) so shipped builds skip parsing, and serialize script objects alongside your own. The Embedding guide walks each one.

Fast where it counts​

Typed code is where JaiScript is fastest: hot loops run as register blocks, and small functions, methods and lambdas are compiled straight into their callers. Integer arithmetic is overflow-checked and every index is bounds-checked, and the checker's proofs remove most of that cost where the types are known. Against Lua 5.4 the VM wins or ties statements, containers and range-for, and trails on hot numeric loops (about 3×), call-dense recursion (about 2.8×), object-heavy programs (about 2.4× on Richards and DeltaBlue) and fully dynamic code; Performance Notes has the measurements and the patterns that avoid them.

Where to go next​