JaiScript reads as C++ or as JavaScript. Each row's two columns are the same construct. One pair differs in behaviour: a [] lambda captures only what its list names, while an arrow or a function(...) expression captures its whole scope by reference (as [&]).
Construct
C++ spelling
JavaScript spelling
Named function
int f(int a) { ... }
function f(a) { ... }, function f(int a) -> int { ... }
Anonymous function
[x, &y](int a) { ... }: captures what it lists
(a) => expr, (a) => { ... }, a => expr, function(a) { ... }: capture by reference
Dynamic variable
none (auto takes its initializer's static type)
var x, let x
Null literal
nullptr
null
One shared object
make_shared<T>(args)
new T(args)
Floating-point type
double, float (one type)
none (a literal with a . is a float)
Non-public members
private:, protected: (one level: the class and its subclasses)
none
Textual inclusion
#include "x.jai"
include "x.jai"
Is it a T?
none (dynamic_cast has no spelling: cast<T&>(x) raises where it would fail)
x instanceof T
A constant
const auto x = e;, for (const auto x : xs), const auto [a, b] = e;
const x = e;, for (const x : xs), const [a, b] = e;
Words from other languages (virtual, delete, goto, extends, constexpr, const_cast, ===, function*, &x) are not reserved; where one fails to parse, the error names the JaiScript spelling (catalogue, Language Bridges). const is a keyword; C++'s other places for it (a const method, a const return type, array<const int>, shared_ptr<const T>) each raise their own text.
int op int stays int (7 / 2 → 3); any float operand promotes.
String +
+
Joins text; a char beside a string joins as its character. A number, bool or null beside a string is an error: write to_string(x) or `${x}`.
Unary
+ - ! ~
+: identity on a number, a char (promoted to its int) and a math value (a copy). - also negates a math value. ~: an int only.
Step
++ --
Prefix and postfix, on every lvalue (elements, map entries, members).
Assignment
= += -= *= /= %= &= |= ^= <<= >>=
Typed targets convert C++-style. x op= y is x = x op y with x evaluated once; the right side runs before any part of the target.
Assignment targets
Every C++ lvalue: a call returning T&, a conditional of lvalues, (a = b) = c, ++x = v, cast<T&>(e).
Comparison
== != < > <= >=
1 == 1.0 is true. A char beside a string compares as its one-character string (s[i] == "x"). NaN follows IEEE (false and unequal).
Three-way
<=>
Returns -1 / 0 / 1; groups all NaNs last; signed zeros are equivalent.
Class test
x instanceof T
True when x holds an object whose class is T or derives from it at any depth (script and host classes, ns::T). A handle answers for its object; a null or expired handle and a value that is no object are false. A relational operator: !(x instanceof T), a instanceof T && b. T is a class: an unknown name, an enum and a builtin type fail the load.
Logical
&&||!
Short-circuit. !v on a class instance is !to_bool(v); an instance whose class declares no bool to_bool() is true.
Bitwise
&|^~<<>>
Integers, on the 64-bit pattern. << discards high bits by contract, >> is arithmetic; the count must be 0..63.
Null
?? ??=
a ?? b: a unless null, else b (evaluated only then). a ??= b stores only into a null a.
Ternary
?:
Right-associative. An int arm and a float arm make a float.
Arrow function
(params) => expr(params) => { ... }x => expr
A function expression that closes over its scope by reference, like function(...).
+ - *, unary - and INT64_MIN / -1 raise a catchable error in the default build (JAISCRIPT_CHECKED_OVERFLOW). Integer arithmetic is never undefined behavior.
Division by zero
Integer / and % by zero raise a catchable error. With a float operand they are IEEE: an infinity or NaN.
Deliberate wraparound
math::wrap_add / wrap_sub / wrap_mul.
char operands
Promote to int (unsigned, 0..255) under arithmetic, bitwise and comparison operators: 'a' == 97 is true, 'z' - 'a' is 25, s[i] > 127 is an unsigned high-bit test (byte 0xC3 is 195, never -61).
int → char
Never implicit: type_of('a' + 1) is "int".
Making a char
cast<char>(n) (range-checked 0..255), cast<char>("a"), to_char(n), a \xNN literal. Out of one: cast<int>(c), cast<string>(c).
A class declares an operator as a method (Vec operator+(Vec o)). It runs only when the (left) operand is an instance whose class chain declares it; every other operand keeps the builtin meaning.
Operator
Rule
Declarable set
C++'s overloadable set less && || , -> new delete and !: binary + - * / % & | ^ << >> < > <= >= == != <=>, unary - + ~ (told apart by the parameter count), =, [] and ().
a op= b
a = a op b through operator op; there is no operator op=.
=
Into an existing object (a slot declared the class, a var): operator= when an overload takes the value, else the implicit memberwise copy. Initialization copy-constructs.
!=, relational
!= without an operator!= negates operator==. A relational operator the class does not declare uses <=>.
Truthiness
bool to_bool(): if (v) calls it and !v negates it. Declaring operator! is the parse error operator! cannot be declared: !v is !to_bool(v); declare bool to_bool().
operator()
Makes an instance callable (v(1, 2)). A function slot still takes only functions and null: wrap the instance in a lambda.
A reserved word names no variable, parameter, program-scope function or class. After ., ?., -> or ::, or in a member declaration, it is a name (keyword member names).
Word
Meaning
intfloatboolcharstringvoid
The scalar types and the no-result return type.
varlet
A dynamic variable; let is var.
auto
A variable that takes its initializer's static type (The Type Ladder).
arraymapshared_ptrweak_ptr
Container and handle types, written with arguments: array<int>, map<string, int>, shared_ptr<T>.
function
A function declaration or expression (spellings); the callable-or-null slot type (function cb = null;).
const
A variable, parameter or field set once and then only read (const).
classnocopy
A class; nocopy class C: instances cannot be copied.
publicprivateprotected
Access labels; protected is private, which admits the class and its subclasses.
static
A member of the class, not of an instance (C::s).
override
A method that replaces a base class's method.
thissuper
The receiver in a method; the base class (: super(args) in a constructor's initializer list, super::m() in a method).
new
new T(args): one shared object, a shared_ptr<T>; the same expression as make_shared<T>(args).
enum
A scoped enumeration (E::v); its name is a type (Kitchen Sink).
namespace
A named scope (ns::x).
ifelsewhileforbreakcontinuereturn
Control flow; for is the C-style loop and the range form for (auto x : xs).
switchcasedefaultfallthrough
A switch; fallthrough; continues into the next case (rule).
class Meters { float v = 0; Meters(float x) { v = x; } }
class Reading { int raw = 0; Reading(int r) { raw = r; } int to_int() { return raw * 10; } }
Meters m = 3; // one-step converting constructor
int n = Reading(4); // to_int() on the way into an int slot
float f = 7; // widens
int t = cast<int>(3.99); // a float enters an int only through cast
bool b = 5; // truthiness
string s = to_string(42); // text only: stringify explicitly
print("{} {} {} {} {} {}", m.v, n, f, t, b, s);
output
3.0 40 7.0 3 true 42
One table governs declarations (int x = e), assignment (x = e), by-value parameters, return values, instance and static field stores, and the elements of typed arrays and maps. A conversion is one step: a converting constructor or a to_X() method runs, never a chain (X.to_int() then C(int) is not implicit); one that throws shows its own error. Between typed sides a mismatch fails the load; out of a var the same rule raises at run time (Diagnostics).
Target
Accepts
Refuses
int
int; bool (true → 1); an enum value; an object with to_int()
A float (rule). string, char, null, containers: no implicit parse ("42".to_int()).
float
float; int (widens); bool; an object with to_float()
A string, an array or a map into a declared bool: a load error (!s.empty()). Conditions keep truthiness for all of them.
enum E
a value of E; an int out of a var that is one of its values
An int or another enum's value: cast<E>(n) converts an int that is one of its values.
string / utf8_string
either text kind (a utf8_string's bytes, a string's characters); a char (its one-character text); an object with to_string()
int, float, bool, null, containers: no implicit stringify. Write to_string(x) or `${x}` (Printing values).
char
char; a string of exactly one character; an object with to_char()
A string of another length: Cannot convert 'string' to 'char': a char takes a string of exactly one character (a literal fails the load). char c = 65: write cast<char>(65).
array<T> / map<K,V>
an array / a map
An element or key of the wrong type, on every store: at load where the checker sees both types (cannot push into 'array<int>': expected 'int', got 'string'), else at run time (Cannot store string in array<int>, Cannot push 'string' to array<int>).
an element
its exact type; an int into a float element; bool and int both ways; char and string both ways
Anything else: no truthiness and no to_X() at the element boundary.
a map key
the declared key type; a char key and its one-character string are one key (stored as the declared type)
Keys never coerce numerically.
math::vec2vec3vec4
the same type; the ivec of its dimensions (widens, as an int into a float)
Another dimension, a color, a number.
math::ivec2ivec3
the same type
The vec of its dimensions: cast<math::ivec2>(v) truncates each component toward zero (a load error where both types are visible, the slot's error through a var). A constructor converts what it is handed: math::ivec2(v).
class C
the same class; a derived class (it keeps its class in the slot: B x = D(); type_of(x) is "D"); a shared_ptr to C or a derived class (copies the pointee); a value exactly one single-parameter constructor C(src) accepts directly; on plain =, a user operator= of C that accepts the source
A parent, a sibling, an unrelated class of the same shape; arrays, maps, functions. null: a class value is never null (rule).
shared_ptr<T>
a handle to T or a subclass; a T value (construct-and-share); null
Another pointee: shared_ptr<Bar> p = Foo() is a load error when the checker sees both classes, else Cannot initialize shared_ptr<Bar> with Foo: type must match or be a subclass. A shared_ptr<Base> into a shared_ptr<C>: cast<shared_ptr<C>>(b).
weak_ptr<T>
a handle to T or a subclass; null
An instance: it has no shared owner.
function
a function, a lambda, a bound method; null
Anything else, an instance with operator() included: wrap it in a lambda.
T& (declaration or parameter)
an lvalue that holds a T (a class: T or a subclass); a var only while it holds one
Any conversion: float& r = v with v holding an int raises Cannot initialize 'r' (declared float&) with int.
T& parameter given a literal or temporary
when its body never writes through it; a string& parameter takes one as a writable temporary
A write-through parameter: the load error 'bump' writes through parameter 'x' (line 1); pass a variable.
a store through T&
what the reference's declared type takes (var v = 0; int& r = v; r = 3.9 and r = "s" are load errors)
What the storage's own declared type refuses: int x; var& w = x; w = 2.7 raises Cannot assign float to 'w' (declared int&).
var
anything, forever
Nothing. Everything computed from a var is unknown to the checker and can only fail at run time.
auto with an initializer
the initializer's type, then enforced as that type
A null initializer carries no type: the variable stays dynamic until its first non-null store.
auto without an initializer
dynamic until the first assignment, then locked
The checker cannot see branch order and stays silent.
Case
Rule
a op= b, typed or locked-auto target
Computes like C++ and converts back to the target's type: int i = 7; i /= 2 → 3; i += 0.5 and i += "s" are errors.
a op= b, var target
Stores whatever the operation produces.
a op= b, missing map key
A compound reads its target first: map<string, int> m; m["x"] += 5 raises Key "x" not found in map.
Value of an assignment
What the destination stored: float f; auto r = (f = 3) is float 3.0.
Printing a float
The shortest text that reads back to the same value, keeping .0 on a whole number; inf, -inf, nan (every NaN prints nan, under a format spec too).
Format specs
Template strings and {} placeholders take them: ${f:.1f} (format specs).
class Base { int b = 1; }
class Derived : Base { int t = 0; }
class Holder { Base p = Derived(); } // a Derived, held through a Base-typed field
print("{} {} {}", cast<int>('a'), cast<char>(97), cast<string>('z'));
char c = cast<char>(65); // `char c = 65;` would be a load error
auto h = Holder();
cast<Derived&>(h.p).t = 5; // alias the same object: the store lands
print("{} {}", c, cast<Derived&>(h.p).t);
shared_ptr<Derived> sp = new Derived();
shared_ptr<Base> sb = cast<shared_ptr<Base>>(sp); // share it, don't copy it
sb.b = 42;
print("{}", sp.b);
var v = Base();
try { Derived d = cast<Derived>(v); } catch (e) { print("caught: {}", e); }
output
97 a z
A 5
42
caught: Cannot cast 'Base' to 'Derived'
Form
Yields
Rule
cast<T>(e), class T
what T tmp = e; would hold
A value copy when e's class is T or derives from it; the copy keeps e's class. On a handle or a reference it copies the instance: the checker warns and suggests the two forms below.
Declares a name that exists outside the program (a host global bound after load, a function the running instance registers, a value another engine injects). The checker counts it as declared instead of reporting use of undeclared identifier.
extern at run time
Declares nothing; the name resolves when used. A use while nothing defines it raises 'name' is declared extern but never defined (an 'inline' declaration creates it) (extern function: without the parenthesis).
extern function log_to_host;
A callable of unknown signature: calls are unchecked.
extern int frame; / extern shared_ptr<Game> G;
Typed: member access, assignments and conversions are checked as for a declared variable of that type.
extern config;
Untyped: behaves like var.
extern function game::spawn; / extern int a::b::depth;
A namespace member, in any of the three strengths. The namespace declares it (game::spawn is not no member 'spawn' in namespace 'game'), and the path opens the namespace for its other members' checks.
Where it stands
At program scope or in a namespace, where its name is the namespace's: namespace game { extern int tick; } declares game::tick (read as tick by the namespace's own blocks); namespace game { extern ai::depth; } declares game::ai::depth.
A qualified extern nothing defines
Raises the same text with the qualified name: 'game::tick' is declared extern but never defined (an 'inline' declaration creates it). The definition is a namespace block of any unit (namespace game { int tick = 0; }) or the host's registration of that namespace.
Reach
An extern name travels with the program: every unit the root imports sees it. Where a program does import the defining module, the declaration is inert and the definition wins.
inline T name;
The C++17 inline variable: one variable shared by every unit that declares it. The first unit to run creates it with its initializer or its type's default; the rest adopt it and their initializers do not run.
A plain declaration of an inline variable
Raises 'x' is already defined inline; declare it 'inline int x;' to share it; at load, with this text, when the checker sees both units.
An inline declaration of another unit's plain variable
Raises 'x' is already defined (not inline) in <file>; declare every definition 'inline' to share it.
using ns::name;
The rest of the file reads name as ns::name, for any namespace; a comma list works (using game::spawn, game::cost;). A parse-time rewrite: file-local, not transitive through imports or includes, and the name cannot be declared again in that file. There is no using namespace.
extern function log_to_host; // the host registers this later: calls are unchecked
extern int frame; // typed: reads and stores are checked as an int
extern config; // untyped: behaves like var
int total = 0;
try {
[[warn_mode]] total = "oops"; // this one statement is only warned about...
} catch (e) {
print("run time: {}", e); // ...so the mistake surfaces here instead
}
print("{}", total);
output
run time: Cannot assign string to 'total' (declared int)
0
The two attributes set the checker's mode for a region of one file. They compose and the innermost wins; everywhere else a file is strict.
Position
Covers
First declaration of a file ([[warn_mode]]; at the top; a leading comment is fine)
The whole unit.
First statement of a block (a function, method or lambda body, an if/while/for body, a bare { })
The whole block, nested bodies included.
Before any other statement
That statement's own subtree; a nested function or lambda body inside it keeps the enclosing mode.
Mode
Rule
Warn region
Every error is reported as a warning (still collected, still visible through engine->check()). An undeclared identifier is accepted as unknown; nothing is proven from it.
Strict region
Errors gate the load, even inside a warn region.
Run time
Never changes: both backends treat the attribute as nothing, so a warn region's type mistakes raise at run time.
Imports
A unit's mode says nothing about the units it imports.
[[nodiscard]]
Marks a function or a method whose result a caller must use (Functions). Any other attribute name is a parse error.
A typed-to-typed mismatch or an undeclared identifier fails the load; a [[warn_mode]] region reports its errors as warnings. Only a var-sourced value can still fail at run time, where try/catch sees it.
A file whose import/include paths are literals is the root of a program: every unit they reach is gathered and checked before anything runs. Names, class models, function signatures and global types cross units; import order does not matter for names.
A builtin method and a math:: function take the shapes the API reference states (a number, text, an array, a map, a function, a vector); a host function or bound method takes what its C++ signature takes. A name declared extern function f; is unchecked.
A top-level statement sees the declarations above it. A function or method body may name a later declaration: it runs only when called, and what it names must exist by then.
Fails the load: 'f' is called before its declaration (line 1): top-level code runs in order, so declare it above the call or make the call after it.
A read before the declaration
A global read, a class constructed or a function taken as a value before its declaration raises Undefined variable 'G'.
Namespace top level
Runs in order the same way. Until its declaration runs, a bare name the namespace declares further down reaches an enclosing namespace's member or the global; with neither it is the same load error.
One declaration per scope
A block, a parameter list or one file's top level that declares a variable name twice is a load error (redeclaration of 'x' in the same scope).
if (a = b), while (a = b) draw assignment used as a condition: write '==' to compare, or '((a = b))' with parentheses of its own to assign; if ((a = b)) is silent.
A store into a by-value copy of a known value type (a range-for variable, a parameter, a local, a field or element of one) that no path reads again before the copy dies, and nothing else can observe, is a load error naming the reference spelling.
An expression statement whose value is a call to a [[nodiscard]] function, method or host callable, or to a library copy such as string::to_upper or array::sort. The check is static only.
A print or format literal's placeholders must pair with its arguments, a literal alone included (print("{}") has a placeholder and no argument); format(s, ...) takes only a literal (vformat formats a run-time text).
Compound assignment, insert, a range-for variable, a coroutine's yield or return and a store through an int& included: a load error between typed sides. cast<int>(x) and math::itrunc(x) truncate toward zero, math::ifloor(x) rounds down. An int widens to a float everywhere.
A slot declared as a class by value (a variable, a field, a static, a parameter, a return) refuses null, and a class-returning function returns on every path. shared_ptr<T>, weak_ptr<T>, var and function hold null.
An E slot holds E's values and starts at the first. A value converts to int; an int enters only through cast<E>(n); var v = E::a holds the int. The bare name is its value table, a map<string,int>.
On a receiver of declared type a builtin method has its catalog result type at load: int bad = xs.join(","); is a load error. s.to_int() is null for text that does not parse; s.to_int(0) is an int.
A declaration without an initializer takes its type's default: a class default-constructs, int 0, float 0.0, bool false, an enum its first value, string "", containers empty, a math vector zero; var stays null.
int& r; raises Reference variable must be initialized.
Aggregate initialization
A class that declares no constructor takes Pt p{1, 2}, Pt{1, 2}, Pt(1, 2): instance fields fill in declaration order (bases first); the rest keep their initializers.
Assignment, parameters and returns copy. & parameters, auto&, for (auto& x : ...) and shared_ptr<T> (new T(...) is sugar for it) are the spelled-out ways to alias.
Arrays elementwise, maps entrywise, instances of one class fieldwise (a user operator== first); handles by pointer; same_object(x, y) is identity. contains, index_of, has and switch cases use the same ==.
Key "k" not found in map, and the read does not insert. m[k] = v inserts. get(k, default) is the explicit fallback. A dot store on a map the checker knows is a load error: store the key as ["k"].
s.to_upper(), xs.filter(f), …: using the result, or chaining, is an error. The string::, array:: and map:: functions return copies, and + never mutates.
The callee is resolved before any argument; arguments go left to right, each taking its value when evaluated (pick()(arg("left", 1), arg("right", 2)) records callee left right).
t = e and t op= e evaluate e before every part of the target (a member's receiver, a subscript's container and index), as in C++17.
Captures are exhaustive
[] captures nothing and [x] only x. Using an unlisted enclosing local is the parse error Lambda capture list does not capture enclosing local 'n'; name it or use [=] or [&].
A function declared to return a primitive, a string, an enum, a class value or a reference must return on every path: 'f' declared to return 'int' may reach the end of its body without returning.
It fails inside the callee, at the return's line, with the callee as the top frame; a try around the return catches it there, and uncaught it surfaces in the caller.
coroutine prefixes every function form, at top level, in namespaces and as methods (static ones too): coroutine int g(), coroutine g() -> int, coroutine function g() -> auto, coroutine function g(), bare coroutine g().
A field initializer reads the fields above it, its bases' fields, the statics and the enclosing scope, never a constructor's parameters; reading its own field or a later one fails the load. Its value lands as the declaration T f = e; would, so a class value field refuses a null initializer.
obj.heal without a call is an error, at load when the checker knows the receiver: 'heal' is a method of Player; call it with player.heal() or bind it with reflect::bind(player, "heal"). reflect::bind(obj, "name") is the method as a callable that holds the object.
Cannot access private member 'x' of class 'C'; for a private constructor (checked on the overload resolution picks) Cannot access private constructor of class 'C'.
Loads ignore access
from_json and from_binary reconstitute the saved state: they run the default constructor whatever its access, or create an instance without one when no constructor takes zero arguments.
Statics
Enforce their declared type; spelled Class::name, or bare name inside the class's own methods. Inherited as in C++: a subclass sees its bases' statics bare, and Derived::name names them.
If a class (or an ancestor) declares _get_x / _set_x, obj.x reads and writes through them. A bare x inside the class's methods is the backing field, so an accessor reads its own field without recursing.
After ., ?., -> or :: every word is a member name (p.catch(f), obj.default, spider::catch(url), Mode::default, super::catch()). Every reserved word but this and super declares a field, a method, a namespace member or an enum value.
Bare, the word stays the keyword: a statement catch(x); and a program-scope void catch() {} are parse errors; a class reaches its own as this.catch(x). A host binds one the same way (.method("catch", ...)); reflection and archives use the word as written.
C++ property references bind the member
A bound property passed to a & parameter (f(this.value), f(obj.value)) reads and writes through the host getter and setter, like any field lvalue.
catch (Type e) takes a class (and its subclasses), an enum's value, a builtin kind or error; clauses run in order; catch (e) takes everything and is last; an error no clause takes goes on.
The execution budget cannot be caught by script; the memory cap gives one catchable raise, then goes terminal. Budget and second memory-cap errors pass every catch clause.
The version moves whenever the parser's output changes; a bite loads only in a build with the same version. Proofs are never serialized and are re-derived on load.
The automatic disk cache silently reparses a cache file of another version or of another source text. An explicit jaibite_load fails with jaibite: unsupported format version N (expected M), M being the build's version.
When first reached, then again when its file changed (checked in passes: import_recheck_interval, default 0.2 s).
Scope
Executes in the current scope.
Module declaration.
Expression?
Yes: var cfg = include "config.jai"; evaluates to the file's last expression.
The statement form never (auto x = import ... is a parse error). The call form import("f.jai") is a bool: true when a module ran.
Dynamic paths
include(expr)
import(expr)
Checked how?
A literal path joins the program closure: its names and classes are known to every unit and checked before anything runs. A dynamic path cannot be gathered: that unit's undeclared identifiers drop to warnings.
Idiom
Forcing a re-run: hot reload of rules or content mid-game.
Declaring the module graph at the top of each file.
Persistent state
Goes in a module that is only imported and never included, so a reload cannot wipe it.
Every file-based load keeps one in a .cache folder beside the source (foo.jai → .cache/foo.jai.jaibite), or below the host's cache directory.
Hit
A cache recording the exact source text (size + hash) and a matching registration fingerprint loads instead of parsing. Any other text reparses and refreshes the cache, whatever its mtime.
Effect
Parse-level and side-effect-free: it never changes whether a file runs. Silent on write failure.