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.
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})`);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.
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);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:
class Ship { int hull = 100; string name = "Wanderer"; }
Ship s = Ship();
s.hull = "full"; // a string into an int field: caught before anything runs<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.
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);
}wave 1: 1 standing
wave 2: 1 standing
wave 3: 0 standingarray<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.
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); }lap 1: walk east
lap 1: scan
lap 1: walk west
lap 2: walk east
lap 2: scan
lap 2: walk westNo 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:
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:
class Turret {
int shots = 0;
string fire() { shots += 3; return `BRRRT x3 (shot ${shots})`; }
}The game keeps running across the change:
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());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:
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.
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}`);100000 sparks stepped, drift -0.080The 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.
#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 ownFrom 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
- The Language Guide: eighteen chapters, starting from hello world.
- Value Semantics & References: the chapter that saves your first hour.
- Embedding Quickstart: JaiScript inside your engine in one sitting.
- The example games: a raycast shooter, a dungeon crawler, a demoscene reel, a networked terminal RPG and DOOM, written entirely in JaiScript.