From C++
JaiScript reads like C++ and keeps C++'s semantics as its contract: classes are values 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++.
It leaves out what a script does not need (raw pointers, delete, templates, headers, undefined behavior), checks the whole program before the first statement runs, and borrows var, function and arrow lambdas from JavaScript. Five ported programs come first, then the tables; Watch out lists where a C++ habit gets a different answer.
Ported side by side
Five small C++20 programs and their JaiScript ports. Each pair was run, built with MSVC and on both JaiScript backends, and printed the output shown. Top-level statements stand in for main, array<T> for std::vector<T> and float for double.
A value type and its operators
#include <format>
#include <iostream>
#include <vector>
struct Vec2 {
double x = 0.0;
double y = 0.0;
Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
Vec2 operator*(double s) const { return {x * s, y * s}; }
Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
bool operator==(const Vec2& o) const = default;
};
int main() {
std::vector<Vec2> points = {{1.0, 2.0}, {3.0, 4.0}, {-2.0, 0.5}};
const Vec2 velocity{0.5, -1.0};
for (auto& p : points) { p += velocity * 2.0; }
Vec2 first = points[0]; // a copy
first.x = 100.0;
Vec2 sum;
for (const auto& p : points) { sum = sum + p; }
std::cout << std::format("{:.1f} {:.1f}\n", points[0].x, first.x);
std::cout << std::format("{:.1f} {:.1f} {}\n", sum.x, sum.y, points[0] == Vec2{2.0, 0.0});
}class Vec2 {
float x = 0.0;
float y = 0.0;
Vec2 operator+(const Vec2& o) { return Vec2{x + o.x, y + o.y}; }
Vec2 operator*(float s) { return Vec2{x * s, y * s}; }
}
array<Vec2> points = [Vec2{1.0, 2.0}, Vec2{3.0, 4.0}, Vec2{-2.0, 0.5}];
const Vec2 velocity{0.5, -1.0};
for (auto& p : points) { p += velocity * 2.0; }
Vec2 first = points[0]; // a copy
first.x = 100.0;
Vec2 sum;
for (const auto& p : points) { sum = sum + p; }
print("{:.1f} {:.1f}", points[0].x, first.x);
print("{:.1f} {:.1f} {}", sum.x, sum.y, points[0] == Vec2{2.0, 0.0});2.0 100.0
5.0 0.5 true- The rest is C++ as written: a class is a value,
auto&names the element, andVec2 first = points[0];copies. +=runsoperator+and stores the result; a compound operator is not declared on its own.==compares fields without a declaration. Braces name their class (Vec2{1.0, 2.0}), and there are no const methods.
Shared ownership
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Node {
std::string name;
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr<Node> parent;
explicit Node(std::string n) : name(std::move(n)) {}
~Node() { std::cout << "~" << name << "\n"; }
};
std::shared_ptr<Node> add_child(std::shared_ptr<Node> parent, std::string name) {
auto child = std::make_shared<Node>(std::move(name));
child->parent = parent;
parent->children.push_back(child);
return child;
}
int main() {
std::weak_ptr<Node> watch;
{
auto root = std::make_shared<Node>("root");
auto leaf = add_child(add_child(root, "branch"), "leaf");
watch = leaf;
std::shared_ptr<Node> up = leaf->parent.lock();
std::cout << up->name << " " << leaf.use_count() << " " << root->children.size() << "\n";
}
std::cout << "expired: " << std::boolalpha << watch.expired() << "\n";
}class Node {
string name;
array<shared_ptr<Node>> children;
weak_ptr<Node> parent;
Node(string n) { name = n; }
~Node() { print("~", name); }
}
shared_ptr<Node> add_child(shared_ptr<Node> parent, string name) {
auto child = make_shared<Node>(name);
child->parent = parent;
parent->children.push(child);
return child;
}
weak_ptr<Node> watch;
{
auto root = new Node("root"); // new is make_shared
auto leaf = add_child(add_child(root, "branch"), "leaf");
watch = leaf;
shared_ptr<Node> up = leaf->parent.lock();
print(up->name, " ", leaf.use_count(), " ", root->children.size());
}
print("expired: ", watch.expired());branch 2 1
~root
~branch
~leaf
expired: truenew Node("root")ismake_shared<Node>("root"): it returns ashared_ptr<Node>, and nothing is ever deleted by hand. The last handle frees the object, so the destructors run in C++'s order.weak_ptr,lock(),expired()anduse_count()keep their meaning. There is no member initializer list: a constructor assigns its fields in its body.
A class hierarchy
#include <format>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
virtual std::string name() const { return "shape"; }
virtual double area() const { return 0.0; }
std::string describe() const { return std::format("{} {:.2f}", name(), area()); }
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius(r) {}
std::string name() const override { return "circle"; }
double area() const override { return 3.14159 * radius * radius; }
private:
double radius;
};
class Rect : public Shape {
public:
Rect(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
private:
double width;
double height;
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(1.0));
shapes.push_back(std::make_unique<Rect>(2.0, 3.5));
double total = 0.0;
for (const auto& s : shapes) {
std::cout << s->describe() << "\n";
total += s->area();
}
std::cout << std::format("total {:.2f}\n", total);
}class Shape {
public:
string name() { return "shape"; }
float area() { return 0.0; }
string describe() { return format("{} {:.2f}", name(), area()); }
}
class Circle : Shape {
public:
Circle(float r) { radius = r; }
string name() override { return "circle"; }
float area() override { return 3.14159 * radius * radius; }
private:
float radius;
}
class Rect : Shape {
public:
Rect(float w, float h) { width = w; height = h; }
float area() override { return width * height; }
private:
float width;
float height;
}
array<shared_ptr<Shape>> shapes;
shapes.push(make_shared<Circle>(1.0));
shapes.push(make_shared<Rect>(2.0, 3.5));
float total = 0.0;
for (const auto& s : shapes) {
print(s->describe());
total += s->area();
}
print("total {:.2f}", total);circle 3.14
shape 7.00
total 10.14- No
virtual: every method is virtual, and a redefinition must sayoverride.private:admits subclasses. - No
unique_ptr: the container holdsshared_ptr<Shape>. Anarray<Shape>of values works too, because a base-typed element is never sliced.
Lambdas and a callback
#include <functional>
#include <iostream>
#include <string>
#include <vector>
struct Button {
std::string label;
std::function<void(const std::string&)> on_click;
void click() const { if (on_click) { on_click(label); } }
};
int main() {
int clicks = 0;
std::vector<std::string> log;
Button ok{"ok"};
ok.click(); // no handler yet
ok.on_click = [&clicks, &log](const std::string& label) { clicks += 1; log.push_back(label); };
ok.click();
ok.click();
int base = 10;
auto add_base = [base](int v) { return v + base; };
base = 99; // the lambda keeps its copy
auto counter = [n = 0]() mutable { n += 1; return n; };
counter();
auto copy = counter; // copies the lambda and its n
counter();
std::cout << clicks << " " << log.size() << " " << add_base(1) << " " << counter() << " " << copy() << "\n";
}2 2 11 3 2class Button {
string label;
function on_click;
void click() { if (on_click != nullptr) { on_click(label); } }
}
int clicks = 0;
array<string> log;
Button ok{"ok"};
ok.click(); // no handler yet
ok.on_click = [&clicks, &log](const string& label) { clicks += 1; log.push(label); };
ok.click();
ok.click();
int base = 10;
auto add_base = [base](int v) { return v + base; };
base = 99; // the lambda keeps its copy
auto counter = [n = 0]() { n += 1; return n; };
counter();
auto copy = counter; // both names count on one n
counter();
print(clicks, " ", log.size(), " ", add_base(1), " ", counter(), " ", copy());2 2 11 3 4- Capture lists mean what they mean in C++, and a by-value capture is writable without
mutable. Afunctionslot holds any callable or null and declares no signature. - The last number differs. C++ copies a lambda together with its
n; in JaiScriptcopyandcounterare two names for one lambda and count on onen.
A thrown class, a destructor and a map
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
struct OutOfStock {
std::string item;
int wanted;
int have;
};
struct Audit {
std::string what;
explicit Audit(std::string w) : what(std::move(w)) {}
~Audit() { std::cout << "audited " << what << "\n"; }
};
void take(std::map<std::string, int>& stock, const std::string& item, int wanted) {
Audit audit{"take " + item};
int& have = stock.at(item); // a missing key throws std::out_of_range
if (have < wanted) { throw OutOfStock{item, wanted, have}; }
have -= wanted;
}
int main() {
std::map<std::string, int> stock = {{"rope", 2}, {"arrow", 20}};
for (std::string item : {"arrow", "rope", "sword"}) {
try {
take(stock, item, 5);
} catch (const OutOfStock& e) {
std::cout << "wanted " << e.wanted << " " << e.item << ", have " << e.have << "\n";
} catch (const std::out_of_range&) {
std::cout << "no " << item << " at all\n";
}
}
for (const auto& [item, count] : stock) { std::cout << item << "=" << count << "\n"; }
}class OutOfStock {
string item;
int wanted;
int have;
}
class Audit {
string what;
Audit(string w) { what = w; }
~Audit() { print("audited ", what); }
}
void take(map<string, int>& stock, const string& item, int wanted) {
Audit audit{"take " + item};
int& have = stock[item]; // a missing key raises a run-time error
if (have < wanted) { throw OutOfStock{item, wanted, have}; }
have -= wanted;
}
map<string, int> stock = {"rope": 2, "arrow": 20};
for (string item : ["arrow", "rope", "sword"]) {
try {
take(stock, item, 5);
} catch (const OutOfStock& e) {
print("wanted ", e.wanted, " ", e.item, ", have ", e.have);
} catch (error e) {
print("no ", item, " at all");
}
}
for (const auto& [item, count] : stock) { print(item, "=", count); }audited take arrow
audited take rope
wanted 5 rope, have 2
audited take sword
no sword at all
arrow=15
rope=2- Any value can be thrown,
catchselects by type, andAudit's destructor runs during the unwind. - Reading
stock[item]for a missing key raises a run-time error, caught aserror; C++'soperator[]would insert it, andatthrows.stock.get(item, 0)reads with a default.
Feature by feature
Types and values
| C++ | JaiScript | |
|---|---|---|
| integer | int, int64_t, unsigned | int: always 64-bit and signed, the only integer type |
| floating point | double, float | float and double both name the 64-bit type |
| character, string | char, std::string | char, unsigned 0..255; string, a value of bytes (utf8_string counts characters) |
| deduced, dynamic | auto x = 5;, std::any | auto x = 5;; var x = 5; holds anything |
| constant | const, constexpr | const int k = 3; on declarations, parameters and class constants; no const methods, const types or constexpr |
| null | nullptr | nullptr or null, held by a shared_ptr<T>, a weak_ptr<T>, a function or a var |
| initialization | int i{5};, T x;, T x(a, b); | the same three; T x; default-constructs a class and zeroes a number |
| containers | std::vector<int>, std::map<std::string, int>, {1, 2, 3} | array<int>, map<string, int> (iterates in key order), [1, 2, 3], {"key": 1} |
| casts | static_cast<int>(2.7), dynamic_cast<Square&>(s), std::static_pointer_cast<Square>(p) | cast<int>(2.7), cast<Square&>(s), cast<shared_ptr<Square>>(p); a wrong class raises the catchable Cannot cast 'X' to 'T' (cast) |
| is it a Square? | if (dynamic_cast<Square*>(p)) | if (s instanceof Square), then cast<Square&>(s), which cannot fail after the test (instanceof) |
| identity | &a == &b | same_object(a, b) |
Functions, references and lambdas
| C++ | JaiScript | |
|---|---|---|
| function | int add(int a, int b), default arguments, overloads | the same, at program scope, in namespaces and in classes; a default cannot name an earlier parameter |
| reference | void heal(int& hp), auto& r = x; | the same; a field or an element binds too (heal(h.hp), heal(arr[i])); a reference cannot be a field or an element |
| template | template<class T> T biggest(T a, T b) | auto biggest(auto a, auto b); no templates, no variadics in script |
| lambda | [x, &y](int v) { ... } | the same captures: [], [x], [&x], [=], [&], [this], [n = expr]; a by-value capture is writable without mutable |
| callable slot | std::function<int(int)> f; | function f;: any callable or null, with no signature |
Classes
| C++ | JaiScript | |
|---|---|---|
| class | class Hero { public: int hp = 10; };, struct | class Hero { int hp = 10; }: members start public; no struct keyword |
| inheritance | class Mage : public Hero, Mage() : Hero(5) {}, Hero::describe() | class Mage : Hero, Mage() : super(5) {} (: this(...) delegates), super::describe() |
| virtual | virtual in the base, override optional | every method is virtual; a redefinition requires override |
| destructor | ~Hero() | ~Hero(), run when the value's scope ends, a throw included |
| heap object | std::make_shared<Hero>(), Hero*, delete | make_shared<Hero>() or new Hero(): a shared_ptr<Hero>. weak_ptr<T> with lock() and expired(). No raw pointers; the last handle frees the object |
| statics | Hero::count | Hero::count |
| 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 the comparisons as in C++20 |
| bool conversion | explicit operator bool() const | bool to_bool() |
Control flow and the rest
| C++ | JaiScript | |
|---|---|---|
| range-for, bindings | for (auto& x : v), for (auto& [k, v] : m), auto [a, b] = p; | the same; auto [a, b] = arr; unpacks an array |
| loops | do { } while (c);, goto | do/while the same; no goto |
| throw, catch | throw std::runtime_error("x");, catch (const std::exception& e) | throw "x"; (any value); catch (Err e) by type, catch (error e) for a run-time error, catch (e) for anything |
| enum | enum class Dir { N, S = 5 }; | enum Dir { n, s = 5 }: a type, scoped as Dir::n, converting to int implicitly and back only through cast<Dir>(i) |
| namespace | namespace util { }, using util::f; | the same; no using namespace |
| include | #include "x.h" | #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}`; only a literal is a format string, and vformat is std::vformat |
#include <format>
#include <iostream>
#include <memory>
#include <string>
struct Shape {
std::string name;
explicit Shape(std::string n) : name(std::move(n)) {}
virtual ~Shape() = default;
virtual double area() const { return 0.0; }
};
struct Square : Shape {
double side;
explicit Square(double s) : Shape("square"), side(s) {}
double area() const override { return side * side; }
Square operator+(const Square& o) const { return Square(side + o.side); }
};
enum class Dir { north, south = 5 };
void grow(Square& sq, double by) { sq.side += by; }
auto biggest(auto a, auto b) { return a > b ? a : b; }
int main() {
Square sq{3.0};
Shape& s = sq; // area() dispatches to Square
std::shared_ptr<Shape> heap = std::make_shared<Square>(2.0);
auto again = heap; // a second handle to one object
grow(dynamic_cast<Square&>(*again), 3.0);
try {
throw (sq + sq).side;
} catch (double e) {
std::cout << std::format("{} {:.1f} {:.1f} {:.1f} {}\n", s.name, s.area(), heap->area(), e,
biggest(std::string("ab"), std::string("b")));
}
Dir d = Dir::south;
std::cout << std::format("{} {}\n", static_cast<int>(d), d == static_cast<Dir>(5));
}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; }
Square operator+(const Square& o) { return Square(side + o.side); }
}
enum Dir { north, south = 5 }
void grow(Square& sq, float by) { sq.side += by; }
auto biggest(auto a, auto b) { return a > b ? a : b; }
Square sq{3.0};
Shape& s = sq; // area() dispatches to Square
shared_ptr<Shape> heap = make_shared<Square>(2.0);
auto again = heap; // a second handle to one object
grow(cast<Square&>(again), 3.0);
try {
throw (sq + sq).side;
} catch (float e) {
print("{} {:.1f} {:.1f} {:.1f} {}", s.name, s.area(), heap->area(), e,
biggest("ab", "b"));
}
Dir d = Dir::south;
print("{} {}", cast<int>(d), d == cast<Dir>(5));square 9.0 25.0 6.0 b
5 trueTwo spellings, one meaning
The language blends C++ and JavaScript on purpose, so some things have a spelling from each. Both columns mean exactly the same thing:
| C++ spelling | JavaScript spelling |
|---|---|
nullptr | null |
make_shared<Hero>(5) | new Hero(5) |
int add(int a, int b) { ... } | function add(a, b) { ... } (every form) |
[](int x) { return x * 2; } | x => x * 2 |
p->hp | p.hp |
#include "x.jai" | include "x.jai"; |
Within the C++ column, double and float are one type and protected: and private: one access level; within the JavaScript column, let and var are one keyword.
Watch out
Where a C++ habit gets a different answer:
| C++ habit | JaiScript |
|---|---|
a Derived copied into a Base is sliced | No slicing. A base-typed variable, parameter or element keeps the whole object and its overrides; the declared type decides what the checker lets you name, and cast<Derived&> reaches the rest |
int i = 3.9; compiles | a float into an int slot is a load error: cast<int>(3.9) truncates, math::ifloor rounds down. int to float stays implicit |
private: is the class only | private: admits the class and its subclasses (C++'s protected); there is no class-only level |
| a class may hold a pointer to itself; a pointer may be null | a class value is never null: Hero h = null; is a load error, and a class that holds itself by value (directly, through a cycle or a base) is one too, as in C++. The link is a shared_ptr<Node> |
| copying a lambda copies its captures | a copy of a lambda shares its by-value capture state: both copies count on one n |
| a non-const reference never binds a literal | const T& binds one, and so does a plain T& the body never writes through. A string& takes a literal even when written (a writable temporary); a written int& needs a variable: 'bump' writes through parameter 'x' (line 1); pass a variable |
a returned [&] lambda dangles | a reference capture keeps its variable alive, and a C-style for variable captured by reference is fresh per iteration |
switch falls through | a case with statements ends at the next case; empty stacked labels (case 1: case 2:) share a body, and fallthrough; continues on purpose |
catch (const E& e) avoids a copy | catch (E e) names the thrown value itself; no copy is made either way |
obj.count reads a static | a static through an instance is a load error naming Counter::count; so is Counter.count |
signed overflow and x / 0 are undefined | integer overflow and integer division by zero raise catchable errors; float division by zero is IEEE (inf); shifts wrap modulo 264; math::wrap_add, wrap_sub, wrap_mul wrap on purpose |
| argument order is unspecified | C++17's sequencing made total: arguments left to right, an assignment's right side before its target |
&Player::heal | no pointer to member, and obj.method without a call is an error: reflect::bind(obj, "heal") or a lambda |
char may be signed | char is 0..255 and promotes to int in arithmetic; s.size() and s[i] count bytes |
| prototypes and headers | none: a function body may call a function declared further down. Top-level statements run in order, so top-level code calls only what is declared above it |
| two globals of one name is a link error | a second top-level declaration of a variable in one file is a load error; a function of the same signature from another file replaces the earlier one (hot reload) |
f(int) and f(int&) overload | overloads must differ in parameter types or count, not only by reference; an ambiguous call raises Ambiguous call to ... |
#include <iostream>
#include <string>
struct Shape {
virtual ~Shape() = default;
virtual std::string kind() const { return "shape"; }
};
struct Circle : Shape {
double r = 1.5;
std::string kind() const override { return "circle"; }
};
void shout(std::string& text) { for (char& c : text) { c = static_cast<char>(std::toupper(c)); } std::cout << text << "\n"; }
int main() {
Shape s = Circle(); // sliced to a Shape
int n = 0;
auto count = [n]() mutable { n += 1; return n; };
auto copy = count; // its own n
count();
std::string text = "a variable binds string&"; // shout("a literal") does not compile
shout(text);
int hits = 0;
switch (1) {
case 0:
case 1: hits += 1; // falls through
case 2: hits += 10;
}
double zero = 0.0; // 10 / 0 is undefined behavior
std::cout << s.kind() << " " << copy() << " " << hits << " " << static_cast<int>(3.9) << " " << 1.0 / zero << "\n";
}A VARIABLE BINDS STRING&
shape 1 11 3 infclass Shape { string kind() { return "shape"; } }
class Circle : Shape { float r = 1.5; string kind() override { return "circle"; } }
void shout(string& text) { text.to_upper(); print(text); }
Shape s = Circle(); // no slicing: s is a Circle
int n = 0;
auto count = [n]() { n += 1; return n; };
auto copy = count; // shares n
count();
shout("a literal binds string&");
int hits = 0;
switch (1) {
case 0:
case 1: hits += 1; // ends here
case 2: hits += 10;
}
float zero = 0.0;
print(s.kind(), " ", copy(), " ", hits, " ", cast<int>(3.9), " ", 1.0 / zero);
try { print(10 / 0); } catch (error e) { print("caught: ", e.message); }A LITERAL BINDS STRING&
circle 2 1 3 inf
caught: Division by zeroThe same lines, different answers: the C++ s is sliced, its lambda copy counts alone, and its switch falls through.
#include <iostream>
struct Node { int v = 0; Node* next = nullptr; };
int main() {
Node* head = nullptr;
double f = 3.9;
int i = f; // compiles; /W4 warns of a possible loss of data
std::cout << (head == nullptr) << " " << i << "\n";
}1 3class Node { int v = 0; Node next; }
Node head = null;
float f = 3.9;
int i = f;jaiscript: static check failed:
<script>:1:30: error: class 'Node' holds itself by value (Node.next): a class value is never null; make one link a 'shared_ptr<Node>' (an array or a map of them starts empty)
class Node { int v = 0; Node next; }
^
<script>:2:6: error: cannot initialize 'head' declared 'Node' with 'null': a 'Node' is never null; declare 'shared_ptr<Node>' (or var) for a slot that can be empty
Node head = null;
^
<script>:4:5: error: cannot initialize 'i' declared 'int' with 'float': cast<int>(x) truncates toward zero, math::ifloor(x) rounds down
int i = f;
^The C++ side links through a pointer and compiles with one conversion warning; the same intent written with values stops the JaiScript load.
What is not there
Raw pointers (T*, &x, delete), templates, const methods and types, const_cast, constexpr, unsigned and sized integers, struct, virtual, mutable, enum class, goto, static_cast (spelled cast) and sizeof. &x, delete, a const method or type, const_cast, constexpr, a mutable field, virtual and goto are parse errors that name the JaiScript spelling; the others fail with a message that does not name the missing feature, so a puzzling error on a C++-shaped line is worth checking against this list.
Where to next
Value Semantics & References has the copy and reference rules, Classes constructors, access and hot reload, and Types That Fail Early what the checker stops at load. Embedding starts at the Embedding Quickstart.