From C++​

JaiScript reads like C++ and keeps C++'s semantics as its contract: classes are value types that copy, references are declared with &, destructors run when a scope ends, operators overload, and auto, range-for, capture lists, switch and try/catch mean what they mean in C++, with C++17's left-to-right evaluation order. What it leaves out is the machinery a script does not need: raw pointers, delete, templates, headers and undefined behavior. const is the narrow C++ form: declarations, parameters and class constants, without const methods or const types. Integers are 64-bit, overflow and division by zero raise errors a catch sees, a switch never falls through by accident, and a static checker reads the whole program before its first statement runs. From JavaScript it borrows var for a dynamic variable and the function keyword. This page sets the two languages side by side, then lists the places where a C++ habit gets a different answer. Every JaiScript line on it was run on both backends, and the output under each sample is what they printed.

Side by side​

Types and values​

C++JaiScript
integerint, int64_t, unsigned, longint: always 64-bit and signed; no unsigned, long or sized integer types
floating pointdouble, floatfloat, 64-bit; double names the same type
characterchar, signed or unsigned by platformchar, unsigned: 0..255
stringstd::stringstring: bytes, a value; utf8_string counts characters
deducedauto x = 5;auto x = 5;
dynamicstd::anyvar x = 5; holds anything
constantconst int k = 3;, constexprconst int k = 3;; no constexpr (a const with a constant initializer is folded at compile time); a reference parameter that the body never writes binds like a const T&
nullnullptrnull (nullptr reads as null)
initializationint i{5};, T x;, T x(a, b);the same three; T x; default-constructs a class and zeroes a number, never garbage
containersstd::vector<int>, std::map<std::string, int>array<int>, map<string, int> (iterates in key order, hashed lookup)
container literals{1, 2, 3}[1, 2, 3] and {"key": 1}
jaiscript
int i{5};
double d = 2;                 // double is float
char c = 'A';
var p = nullptr;
array<int> scores = [90, 72];
map<string, int> ages = {"ana": 31};
array<int> none;              // T x; default-constructs: an empty array
int zero;                     // and a number starts at 0
print(i, " ", d, " ", type_of(d), " ", cast<int>(c), " ", p == null, " ", scores.size() + ages.size(), " ", none.size(), " ", zero);
output
5 2.0 float 65 true 3 0 0

Functions, references and lambdas​

C++JaiScript
functionint add(int a, int b)the same, or auto add(int a, int b) -> int, or function add(a, b) with types optional
default argumentint hit(int d = 5)the same
reference parametervoid heal(int& hp)the same; a field or an element binds too: heal(h.hp), heal(arr[i])
const T& parameterint total(const std::vector<int>& xs)int total(const array<int>& xs); a plain array<int>& xs the body never writes takes a literal or a temporary too
reference variableauto& r = x;the same; a reference cannot be a field or an element
overloadingfree functions overloadthe same, at program scope, in namespaces and in classes; a second definition of the same signature in one file is an error, and one from another unit replaces it; two overloads that differ only by reference, f(int) and f(int&), are an error at the second definition
templatetemplate<class T> T biggest(T a, T b)auto biggest(auto a, auto b); no templates
lambda[x, &y](int v) { ... }the same capture syntax: [], [x], [&x], [=], [&], [this], [n = expr]
mutable lambda[n]() mutable { n++; }[n]() { n++; }: no keyword; a copied capture is the lambda's own variable, writable and kept between calls
callable slotstd::function<int(int)> f;function f; (any callable, or null)
variadictemplate<class... A> void f(A... a)none in script; a host (C++) function can take any number of arguments
jaiscript
void heal(int& hp, int amount) { hp += amount; }
int total(array<int>& xs) { int s = 0; for (auto x : xs) { s += x; } return s; }
auto biggest(auto a, auto b) { return a > b ? a : b; }
class Hero { int hp = 10; }
Hero h;
heal(h.hp, 5);                          // a field binds to int&
auto& alias = h.hp;
alias += 1;
int bonus = 3;
auto add_bonus = [bonus](int v) { return v + bonus; };
function f = add_bonus;
print(h.hp, " ", total([1, 2, 3]), " ", biggest(3, 9), " ", biggest("ab", "b"), " ", f(10));
output
16 6 9 b 13

Classes​

C++JaiScript
classclass Hero { public: int hp = 10; };class Hero { int hp = 10; }: members start public
structstruct P { int x; };no struct: every class is a value type
accesspublic:, protected:, private:public: and private:; private members reach subclasses too, and protected: means the same as private:
inheritanceclass Mage : public Heroclass Mage : Hero, with no access specifier
base constructorMage() : Hero(5) {}Mage() : super(5) {}; : this(...) delegates
virtualvirtual in the base, override optionalevery method is virtual; a redefinition requires override
base callHero::describe()super::describe()
destructor~Hero()~Hero(), run when the value's scope ends, a throw included
heap objectstd::make_shared<Hero>()new Hero() returns a shared_ptr<Hero>
->member access through a pointera synonym for . on values and handles
delete, raw pointersdelete p;, Hero* pnone: shared_ptr<T>, weak_ptr<T> and references; the last handle frees the object
staticsHero::countHero::count; Hero.count is an error that names the :: spelling
no copiesHero(const Hero&) = delete;nocopy class Hero { ... }
operatorsVec operator+(const Vec& o) constVec operator+(Vec o); C++'s set less &&, ||, ,, ->, new, delete and !; operator<=> supplies <, <=, >, >= and, when no operator== takes the operand, == and !=, in both operand orders (C++20 rewriting)
bool conversionexplicit operator bool() constbool to_bool(); !v is !to_bool(v)
jaiscript
class Shape {
    string name;
    Shape(string n) { name = n; }
    float area() { return 0.0; }
}
class Square : Shape {
    float side;
    Square(float s) : super("square") { side = s; }
    float area() override { return side * side; }
}
class Vec {
    int x = 0; int y = 0;
    Vec(int px, int py) { x = px; y = py; }
    Vec operator+(Vec o) { return Vec(x + o.x, y + o.y); }
}
Square sq = Square(3.0);
Shape& s = sq;                          // a base reference: area() dispatches to Square
shared_ptr<Square> heap = new Square(2.0);
auto again = heap;                      // a second handle to one object
again.side = 5.0;
Vec v = Vec(1, 2) + Vec(3, 4);
print(s.name, " ", s.area(), " ", heap->area(), " ", v.x, ",", v.y);
output
square 9.0 25.0 4,6

Control flow and errors​

C++JaiScript
range-forfor (auto& x : v)the same; over a map, kv.first and kv.second
structured bindingauto [a, b] = p;, for (auto& [k, v] : m)auto [a, b] = arr; unpacks an array; for (auto& [k, v] : m) binds a map entry's key and value
switchfalls through without breakbreaks by default; fallthrough; continues into the next case
do, gotodo { } while (c);, goto end;do { } while (c); is the same; no goto (writing one is an error that says so)
throwthrow std::runtime_error("x");throw "x";, any value
catchcatch (const std::exception& e)catch (e), untyped: e reads as the message and e.value holds what was thrown
enumenum class Dir { N, S }; and Dir::Nenum Dir { n, s }, then Dir::n or Dir.n: plain ints counted from 0; no enum class and no explicit values (n = 5 is a parse error)
namespacenamespace util { } and util::f()the same
include#include "x.h"#include "x.jai" or include "x.jai"; runs the file's text in place; import "x.jai"; loads a module once and reloads it when it changes
coroutineC++20 co_yieldcoroutine int gen() { yield 1; }, driven by next() or range-for
formattingstd::format("{} {:.2f}", a, b)format("{} {:.2f}", a, b), print("{}", a), `${b:.2f}`
jaiscript
enum Dir { north, south }
namespace util { int twice(int v) { return v * 2; } }
auto [lo, hi] = [3, 8];
map<string, int> stock = {"ore": 4, "gem": 1};
for (auto [name, count] : stock) { print(name, "=", count); }
try {
    throw util::twice(hi);
} catch (e) {
    print("caught ", e.value, " (", type_of(e.value), ") ", Dir.south, " ", lo);
}
print(format("{} {:.2f}", "pi", math::PI));
output
gem=1
ore=4
caught 16 (int) 1 3
pi 3.14

Casts and handles​

C++JaiScript
value caststatic_cast<int>(2.7)cast<int>(2.7)
downcast to a referencedynamic_cast<Square&>(s)cast<Square&>(s); a wrong class raises the catchable Cannot cast 'X' to 'T'
handle caststd::static_pointer_cast<Square>(p)cast<shared_ptr<Square>>(p)
weak handlestd::weak_ptr<T>weak_ptr<T>, with lock() and expired()
identity&a == &bsame_object(a, b)
char from an intstatic_cast<char>(65)cast<char>(65), range-checked to 0..255
jaiscript
class Shape { int sides = 0; }
class Square : Shape { float side = 2.0; }
Shape s = Square();                          // a base-typed slot holding a whole Square
print(cast<Square&>(s).side, " ", cast<int>(2.7), " ", cast<char>(65));
shared_ptr<Shape> p = new Square();
shared_ptr<Square> q = cast<shared_ptr<Square>>(p);
weak_ptr<Shape> w = p;
print(same_object(p, q), " ", w.expired());
try { cast<Square&>(Shape()).side; } catch (e) { print("caught: ", e); }
output
2.0 2 A
true false
caught: Cannot cast 'Shape' to 'Square&'

The Type Ladder has the full conversion table behind cast.

Watch out​

The differences below are the ones a C++ habit runs into first. Each sample shows what JaiScript does.

switch does not fall through​

A case ends at the next case, with no break. fallthrough; (C++'s [[fallthrough]]) continues into the case below:

jaiscript
int hits = 0;
switch (1) {
    case 1: hits += 1;          // ends here
    case 2: hits += 10;
}
switch (1) {
    case 1:
        hits += 100;
        fallthrough;            // continue into case 2
    case 2: hits += 1000;
}
print(hits);
output
1101

Overloads work as in C++​

Functions of one name with different signatures coexist at program scope, in a namespace and in a class, and a call takes the best match; a tie raises Ambiguous call to ..., as C++ rejects an ambiguous call:

jaiscript
namespace geo {
    int area(int side) { return side * side; }
    int area(int w, int h) { return w * h; }
}
print(geo::area(4), " ", geo::area(2, 3));
output
16 6

At program scope the same holds, and a definition of the same signature from another unit replaces the earlier one, which is what hot reload needs:

jaiscript
int area(int side) { return side * side; }
int area(int w, int h) { return w * h; }
float area(float r) { return 3.0 * r * r; }
print(area(2, 3), " ", area(4), " ", area(1.5));
output
6 16 6.75

Overloads must differ in parameter types or count. Two that differ only by reference, void f(int a) and void f(int& a), are a load error at the second definition: 'f(int& a)' differs from 'f(int a)' (line 1) only by reference; overloads must differ in parameter types or count.

A base-typed variable keeps the whole object​

This is where JaiScript departs from C++: a Base slot holding a Derived keeps the whole object and its overrides, where C++ would slice it.

Copying a derived value into a base-typed variable, parameter or element keeps all of it: no slicing. Assigning one into a base-typed slot that already holds an object keeps that object and turns it into the derived class in place. The variable's type decides what the checker lets you name, and cast<T&> reaches the rest:

jaiscript
class Shape { string kind() { return "shape"; } }
class Circle : Shape { float r = 1.5; string kind() override { return "circle"; } }
Shape s = Circle();
array<Shape> all = [Shape(), Circle()];
print(s.kind(), " ", type_of(s), " ", all[1].kind(), " ", cast<Circle&>(s).r);
output
circle Circle circle 1.5

Arithmetic has no undefined behavior​

Signed overflow and division by zero, integer or float, raise an error that catch sees and leave the target untouched. Shifts wrap modulo 264, and math::wrap_add, wrap_sub and wrap_mul wrap on purpose. An int is 64 bits everywhere:

jaiscript
int big = 9223372036854775807;
try { big = big + 1; } catch (e) { print("caught: ", e); }
try { print(10 / 0); } catch (e) { print("caught: ", e); }
try { print(1.0 / 0.0); } catch (e) { print("caught: ", e); }
print(1 << 63, " ", math::wrap_add(big, 1), " ", 2147483647 + 1);
output
caught: Integer overflow in '+'
caught: Division by zero
caught: Division by zero
-9223372036854775808 -9223372036854775808 2147483648

Evaluation order is defined​

C++17's sequencing, made total: what C++17 leaves unspecified between arguments is left to right here, on both backends.

The C++17 rule, everywhere: the callee, then its arguments left to right; for an assignment, the right side first, then the target (its container, then its index):

jaiscript
string log = "";
int note(string s, int v) { log += s + " "; return v; }
int add(int a, int b) { return a + b; }
array<int> slots = [0, 0];
int r = add(note("first", 1), note("second", 2));
slots[note("index", 0)] = note("value", 7);
print(log, "| ", r, " ", slots[0]);
output
first second value index | 3 7

Reference parameters and literals​

const T& means what it means in C++, and a plain T& that the body never writes through binds the same way.

A const T& parameter takes a literal or a temporary, and a variable still binds with no copy; a write through it is an error. A plain reference parameter that the body never writes through takes a literal or a temporary as well, decided from the body. A string& takes a literal too, as a writable temporary:

jaiscript
int total(array<int>& xs) { int s = 0; for (auto x : xs) { s += x; } return s; }
void upper(string& s) { s.to_upper(); print(s); }
print(total([1, 2, 3]));
upper("quiet");
output
6
QUIET

A parameter the body does write keeps needing a variable, and the checker names the line that writes:

jaiscript
void bump(int& x) { x += 1; }
bump(5);
output
jaiscript: static check failed:
<script>:2:6: error: 'bump' writes through parameter 'x' (line 1); pass a variable
    bump(5);
         ^

A reference capture can outlive its frame​

[&] and [&x] move the captured local into a shared cell, so returning such a lambda is safe where C++ would leave it dangling:

jaiscript
function make_counter() {
    int count = 0;
    return [&count]() { count += 1; return count; };
}
auto next = make_counter();
next();
print(next());
output
2

Methods are called, not taken​

obj.method without a call is an error, and there is no pointer to member. reflect::bind(obj, "name") makes a callable that keeps the object, or a lambda wraps the call:

jaiscript
class Player { int hp = 5; void heal() { hp += 1; } }
Player p;
var heal = reflect::bind(p, "heal");
heal();
heal();
print(p.hp);
output
7

Strings are bytes, and char is unsigned​

s.size() and s[i] count bytes, a char promotes to an int in 0..255 under arithmetic, and the utf8_ methods (or a utf8_string) count characters:

jaiscript
string s = "\xC3\xA9";              // é in UTF-8: two bytes
utf8_string u = s;
print(s.size(), " ", s[0] + 0, " ", s.utf8_length(), " ", u.size());
output
2 195 1 1

No forward declarations, and the top level runs in order​

There are no prototypes or headers: a function body may call a function declared further down, because the call happens when the body runs. Top-level statements, though, run in order like a script, and a function or class exists once its declaration has run, so call into the file from below the declarations (or from a function the host calls later):

jaiscript
int first(int x) { return second(x) + 1; }   // second is declared below: fine
int second(int x) { return x * 10; }
print(first(2));
output
21

What is not there​

Raw pointers (T*, &x, delete), templates, const methods, const types and const_cast, constexpr, unsigned and sized integer types, struct, virtual, mutable, enum class, goto, typed catch clauses, static_cast (spelled cast) and sizeof. &x, delete, a const method or type, const_cast, constexpr, a mutable field, virtual, goto and a typed catch are parse errors that name the JaiScript spelling; the others, a mutable lambda among them, fail as a parse error, a load error or an error when the line runs, and the message does not name the missing feature, so a puzzling message on a C++-shaped line is worth checking against this list.

A complete program​

A small simulation step in the shape a C++ programmer would write it: value types with operators, a reference parameter for the hot loop, a scope guard whose destructor marks the end of each step, and a snapshot that is a real copy.

jaiscript
class Vec2 {
    float x = 0.0;
    float y = 0.0;
    Vec2() {}
    Vec2(float px, float py) { x = px; y = py; }
    Vec2 operator+(Vec2 o) { return Vec2(x + o.x, y + o.y); }
    Vec2 operator*(float k) { return Vec2(x * k, y * k); }
}
class Body {
    string name;
    Vec2 pos;
    Vec2 vel;
    Body() {}
    Body(string n, Vec2 p, Vec2 v) { name = n; pos = p; vel = v; }
}
class Scope {
    string label;
    Scope(string l) { label = l; print("begin ", label); }
    ~Scope() { print("end ", label); }
}
void step(array<Body>& bodies, float dt) {
    Scope guard = Scope("step");
    for (auto& b : bodies) { b.pos = b.pos + b.vel * dt; }
}

array<Body> world = [Body("ship", Vec2(0.0, 0.0), Vec2(2.0, 1.0)),
                     Body("rock", Vec2(10.0, 0.0), Vec2(-1.0, 0.0))];
array<Body> snapshot = world;            // a deep copy, like std::vector<Body>
step(world, 0.5);
step(world, 0.5);
for (auto& b : world) { print(`${b.name}: (${b.pos.x:.1f}, ${b.pos.y:.1f})`); }
print(`snapshot ship: (${snapshot[0].pos.x:.1f}, ${snapshot[0].pos.y:.1f})`);
output
begin step
end step
begin step
end step
ship: (2.0, 1.0)
rock: (9.0, 0.0)
snapshot ship: (0.0, 0.0)

Where to next​

Value Semantics & References covers the reference escape hatches and what each one costs. Operators & Arithmetic has the overloading rules and the evaluation order, Classes constructors, access and hot reload, and Types That Fail Early what the checker stops at load. Embedding JaiScript in a C++ program starts at the Embedding Quickstart.