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​

C++
#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});
}
jaiscript
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});
output
2.0 100.0
5.0 0.5 true
  • The rest is C++ as written: a class is a value, auto& names the element, and Vec2 first = points[0]; copies.
  • += runs operator+ 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​

C++
#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";
}
jaiscript
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());
output
branch 2 1
~root
~branch
~leaf
expired: true
  • new Node("root") is make_shared<Node>("root"): it returns a shared_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() and use_count() keep their meaning. There is no member initializer list: a constructor assigns its fields in its body.

A class hierarchy​

C++
#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);
}
jaiscript
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);
output
circle 3.14
shape 7.00
total 10.14
  • No virtual: every method is virtual, and a redefinition must say override. private: admits subclasses.
  • No unique_ptr: the container holds shared_ptr<Shape>. An array<Shape> of values works too, because a base-typed element is never sliced.

Lambdas and a callback​

C++
#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";
}
output
2 2 11 3 2
jaiscript
class 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());
output
2 2 11 3 4
  • Capture lists mean what they mean in C++, and a by-value capture is writable without mutable. A function slot holds any callable or null and declares no signature.
  • The last number differs. C++ copies a lambda together with its n; in JaiScript copy and counter are two names for one lambda and count on one n.

A thrown class, a destructor and a map​

C++
#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"; }
}
jaiscript
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); }
output
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, catch selects by type, and Audit's destructor runs during the unwind.
  • Reading stock[item] for a missing key raises a run-time error, caught as error; C++'s operator[] would insert it, and at throws. stock.get(item, 0) reads with a default.

Feature by feature​

Types and values​

C++JaiScript
integerint, int64_t, unsignedint: always 64-bit and signed, the only integer type
floating pointdouble, floatfloat and double both name the 64-bit type
character, stringchar, std::stringchar, unsigned 0..255; string, a value of bytes (utf8_string counts characters)
deduced, dynamicauto x = 5;, std::anyauto x = 5;; var x = 5; holds anything
constantconst, constexprconst int k = 3; on declarations, parameters and class constants; no const methods, const types or constexpr
nullnullptrnullptr or null, held by a shared_ptr<T>, a weak_ptr<T>, a function or a var
initializationint i{5};, T x;, T x(a, b);the same three; T x; default-constructs a class and zeroes a number
containersstd::vector<int>, std::map<std::string, int>, {1, 2, 3}array<int>, map<string, int> (iterates in key order), [1, 2, 3], {"key": 1}
castsstatic_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 == &bsame_object(a, b)

Functions, references and lambdas​

C++JaiScript
functionint add(int a, int b), default arguments, overloadsthe same, at program scope, in namespaces and in classes; a default cannot name an earlier parameter
referencevoid 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
templatetemplate<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 slotstd::function<int(int)> f;function f;: any callable or null, with no signature

Classes​

C++JaiScript
classclass Hero { public: int hp = 10; };, structclass Hero { int hp = 10; }: members start public; no struct keyword
inheritanceclass Mage : public Hero, Mage() : Hero(5) {}, Hero::describe()class Mage : Hero, Mage() : super(5) {} (: this(...) delegates), super::describe()
virtualvirtual in the base, override optionalevery method is virtual; a redefinition requires override
destructor~Hero()~Hero(), run when the value's scope ends, a throw included
heap objectstd::make_shared<Hero>(), Hero*, deletemake_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
staticsHero::countHero::count
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 the comparisons as in C++20
bool conversionexplicit operator bool() constbool to_bool()

Control flow and the rest​

C++JaiScript
range-for, bindingsfor (auto& x : v), for (auto& [k, v] : m), auto [a, b] = p;the same; auto [a, b] = arr; unpacks an array
loopsdo { } while (c);, gotodo/while the same; no goto
throw, catchthrow 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
enumenum 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)
namespacenamespace 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
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}`; only a literal is a format string, and vformat is std::vformat
C++
#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));
}
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; }
    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));
output
square 9.0 25.0 6.0 b
5 true

Two 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++ spellingJavaScript spelling
nullptrnull
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->hpp.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++ habitJaiScript
a Derived copied into a Base is slicedNo 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; compilesa 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 onlyprivate: 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 nulla 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 capturesa copy of a lambda shares its by-value capture state: both copies count on one n
a non-const reference never binds a literalconst 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 danglesa reference capture keeps its variable alive, and a C-style for variable captured by reference is fresh per iteration
switch falls througha 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 copycatch (E e) names the thrown value itself; no copy is made either way
obj.count reads a statica static through an instance is a load error naming Counter::count; so is Counter.count
signed overflow and x / 0 are undefinedinteger 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 unspecifiedC++17's sequencing made total: arguments left to right, an assignment's right side before its target
&Player::healno pointer to member, and obj.method without a call is an error: reflect::bind(obj, "heal") or a lambda
char may be signedchar is 0..255 and promotes to int in arithmetic; s.size() and s[i] count bytes
prototypes and headersnone: 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 errora 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&) overloadoverloads must differ in parameter types or count, not only by reference; an ambiguous call raises Ambiguous call to ...
C++
#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";
}
output
A VARIABLE BINDS STRING&
shape 1 11 3 inf
jaiscript
class 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); }
output
A LITERAL BINDS STRING&
circle 2 1 3 inf
caught: Division by zero

The same lines, different answers: the C++ s is sliced, its lambda copy counts alone, and its switch falls through.

C++
#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";
}
output
1 3
jaiscript
class Node { int v = 0; Node next; }
Node head = null;
float f = 3.9;
int i = f;
output
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.