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 | |
|---|---|---|
| integer | int, int64_t, unsigned, long | int: always 64-bit and signed; no unsigned, long or sized integer types |
| floating point | double, float | float, 64-bit; double names the same type |
| character | char, signed or unsigned by platform | char, unsigned: 0..255 |
| string | std::string | string: bytes, a value; utf8_string counts characters |
| deduced | auto x = 5; | auto x = 5; |
| dynamic | std::any | var x = 5; holds anything |
| constant | const int k = 3;, constexpr | const 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& |
| null | nullptr | null (nullptr reads as null) |
| initialization | int i{5};, T x;, T x(a, b); | the same three; T x; default-constructs a class and zeroes a number, never garbage |
| containers | std::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} |
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);5 2.0 float 65 true 3 0 0Functions, references and lambdas
| C++ | JaiScript | |
|---|---|---|
| function | int add(int a, int b) | the same, or auto add(int a, int b) -> int, or function add(a, b) with types optional |
| default argument | int hit(int d = 5) | the same |
| reference parameter | void heal(int& hp) | the same; a field or an element binds too: heal(h.hp), heal(arr[i]) |
const T& parameter | int 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 variable | auto& r = x; | the same; a reference cannot be a field or an element |
| overloading | free functions overload | the 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 |
| template | template<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 slot | std::function<int(int)> f; | function f; (any callable, or null) |
| variadic | template<class... A> void f(A... a) | none in script; a host (C++) function can take any number of arguments |
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));16 6 9 b 13Classes
| C++ | JaiScript | |
|---|---|---|
| class | class Hero { public: int hp = 10; }; | class Hero { int hp = 10; }: members start public |
| struct | struct P { int x; }; | no struct: every class is a value type |
| access | public:, protected:, private: | public: and private:; private members reach subclasses too, and protected: means the same as private: |
| inheritance | class Mage : public Hero | class Mage : Hero, with no access specifier |
| base constructor | Mage() : Hero(5) {} | Mage() : super(5) {}; : this(...) delegates |
| virtual | virtual in the base, override optional | every method is virtual; a redefinition requires override |
| base call | Hero::describe() | super::describe() |
| destructor | ~Hero() | ~Hero(), run when the value's scope ends, a throw included |
| heap object | std::make_shared<Hero>() | new Hero() returns a shared_ptr<Hero> |
-> | member access through a pointer | a synonym for . on values and handles |
delete, raw pointers | delete p;, Hero* p | none: shared_ptr<T>, weak_ptr<T> and references; the last handle frees the object |
| statics | Hero::count | Hero::count; Hero.count is an error that names the :: spelling |
| no copies | Hero(const Hero&) = delete; | nocopy class Hero { ... } |
| operators | Vec operator+(const Vec& o) const | Vec 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 conversion | explicit operator bool() const | bool to_bool(); !v is !to_bool(v) |
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);square 9.0 25.0 4,6Control flow and errors
| C++ | JaiScript | |
|---|---|---|
| range-for | for (auto& x : v) | the same; over a map, kv.first and kv.second |
| structured binding | auto [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 |
| switch | falls through without break | breaks by default; fallthrough; continues into the next case |
| do, goto | do { } while (c);, goto end; | do { } while (c); is the same; no goto (writing one is an error that says so) |
| throw | throw std::runtime_error("x"); | throw "x";, any value |
| catch | catch (const std::exception& e) | catch (e), untyped: e reads as the message and e.value holds what was thrown |
| enum | enum class Dir { N, S }; and Dir::N | enum 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) |
| namespace | namespace 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 |
| coroutine | C++20 co_yield | coroutine int gen() { yield 1; }, driven by next() or range-for |
| formatting | std::format("{} {:.2f}", a, b) | format("{} {:.2f}", a, b), print("{}", a), `${b:.2f}` |
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));gem=1
ore=4
caught 16 (int) 1 3
pi 3.14Casts and handles
| C++ | JaiScript | |
|---|---|---|
| value cast | static_cast<int>(2.7) | cast<int>(2.7) |
| downcast to a reference | dynamic_cast<Square&>(s) | cast<Square&>(s); a wrong class raises the catchable Cannot cast 'X' to 'T' |
| handle cast | std::static_pointer_cast<Square>(p) | cast<shared_ptr<Square>>(p) |
| weak handle | std::weak_ptr<T> | weak_ptr<T>, with lock() and expired() |
| identity | &a == &b | same_object(a, b) |
| char from an int | static_cast<char>(65) | cast<char>(65), range-checked to 0..255 |
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); }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:
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);1101Overloads 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:
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));16 6At 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:
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));6 16 6.75Overloads 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:
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);circle Circle circle 1.5Arithmetic 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:
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);caught: Integer overflow in '+'
caught: Division by zero
caught: Division by zero
-9223372036854775808 -9223372036854775808 2147483648Evaluation 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):
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]);first second value index | 3 7Reference 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:
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");6
QUIETA parameter the body does write keeps needing a variable, and the checker names the line that writes:
void bump(int& x) { x += 1; }
bump(5);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:
function make_counter() {
int count = 0;
return [&count]() { count += 1; return count; };
}
auto next = make_counter();
next();
print(next());2Methods 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:
class Player { int hp = 5; void heal() { hp += 1; } }
Player p;
var heal = reflect::bind(p, "heal");
heal();
heal();
print(p.hp);7Strings 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:
string s = "\xC3\xA9"; // é in UTF-8: two bytes
utf8_string u = s;
print(s.size(), " ", s[0] + 0, " ", s.utf8_length(), " ", u.size());2 195 1 1No 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):
int first(int x) { return second(x) + 1; } // second is declared below: fine
int second(int x) { return x * 10; }
print(first(2));21What 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.
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})`);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.