From JavaScript​

JaiScript takes two words from JavaScript: var for a variable that holds anything, and function for declaring one. Template strings with ${...}, null, ?., array literals and try/catch/throw of any value read the same as well. Underneath, the contract is C++'s: assignment copies (arrays and objects included), int and float are separate types, a class declares its fields, a lambda names what it captures, and a static checker reads the whole program before its first statement runs. This page sets the two languages side by side, then lists the places where a JavaScript 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​

Variables and types​

JavaScriptJaiScript
holds anythinglet x = 5; x = "five";var x = 5; x = "five";, or let x = 5;: let is the same word as var
inferred once, then fixed(TypeScript) let n = 5;auto n = 5; is an int from then on
spelled out(TypeScript) let hp: number = 10;int hp = 10; float speed = 2.5; string name = "Ada"; bool alive = true;
numbersone number typeint (64-bit) and float (64-bit) are separate types: 7 / 2 is 3
constantconst MAX = 10;const MAX = 10; or const int MAX = 10;; assigning to it is a load error, cannot assign to const 'MAX' (declared at 1:7)
what is it?typeof xtype_of(x): "int", "float", "string", "array", "map", "null", or a class's name
nothingnull and undefinednull only
destructuringconst [a, b] = arr;auto [a, b] = arr; or const auto [a, b] = arr; (extra elements ignored, missing ones null); for (auto [k, v] : m) over a map
spread[...a, 3], f(...args)none: array::concat(a, [3]) builds the array, and a call takes its arguments one by one
jaiscript
var x = 5;
x = "five";                  // var holds anything
auto n = 5;                  // inferred once: an int from here on
int hp = 10;
float speed = 2.5;
string name = "Ada";
print(type_of(x), " ", type_of(n), " ", type_of(speed), " ", type_of(null));
print(7 / 2, " ", 7.0 / 2, " ", 7 / 2.0);
output
string int float null
3 3.5 3.5

auto n = 5; n = "five"; is a load error, the checker's answer to a type change it can see. The ladder from var to int is the subject of The Type Ladder.

Functions and closures​

JavaScriptJaiScript
declarationfunction add(a, b) { return a + b; }the same; types are optional: function add(int a, int b) -> int, or C++'s int add(int a, int b)
default parameterfunction hit(dmg = 5)function hit(int dmg = 5)
arrow functionx => x * 2the same: x => x * 2, (int a, int b) => a + b, (x) => { return x * 2; }; it shares the variables around it, as function(...) does
closure over a local() => count++() => { count++; }, function() { count++; } or [&]() { count++; } shares the variable; [=] or [count] copies it
top-level variable in a functionread when the function runsthe same: functions and lambdas read program-scope variables when they run
function valueconst f = add; f(1, 2);const f = add; f(1, 2); or var f = add;
rest parametersfunction f(...xs)none in script; a host (C++) function can take any number of arguments
jaiscript
function add(a, b) { return a + b; }
int add_ints(int a, int b) { return a + b; }
function hit(int dmg = 5) { return dmg; }
var f = add;
auto twice = x => x * 2;

int total = 0;                                    // a program-scope variable
function bank(int n) { total += n; }              // read and written when bank runs

function make_counter() {
    int count = 0;
    return [&]() { count += 1; return count; };   // [&] shares count with the closure
}
auto next = make_counter();
next();
bank(5);
bank(2);
print(f(1, 2), " ", add_ints(2, 3), " ", hit(), " ", hit(9), " ", twice(21), " ", next(), " ", total);
output
3 5 5 9 42 2 7

A shared ([&]) capture outlives the function that made it, so the counter is safe to return. Capture lists are exhaustive: Functions & Lambdas has the whole set.

Arrays and maps​

JavaScriptJaiScript
array literal[1, 2, 3][1, 2, 3]; array<int> pins the element type
lengtha.lengtha.size() (a method; a.length() is the same)
add, removea.push(x), a.pop()a.push(x), a.pop(), a.remove(i) by index
searcha.indexOf(x), a.includes(x)a.index_of(x) (-1 when absent), a.contains(x)
copy a rangea.slice(1, 3)array::slice(a, 1, 3); end defaults to size. a.slice changes the input
filtera.filter(x => x > 2)array::filter(a, x => x > 2) returns a copy; a.filter changes the input
sorta.sort((x, y) => x - y)a.sort() orders numbers by value; a.sort((x, y) => x < y) takes a comparator that answers "does x go first?"
map, reduce, forEacha.map(f), a.reduce(f, 0), a.forEach(f)array::map(a, f) returns a copy; a.map(f) changes the input. a.reduce(f, 0) and a.for_each(f) match their JS roles
find, findIndexa.find(f), a.findIndex(f)a.find(f) (null when none matches), a.find_index(f) (-1 when none does)
some, everya.some(f), a.every(f)the same
concat, splicea.concat(b), a.splice(i, 0, x)array::concat(a, b) returns a new array; a.concat(b) appends in place; a.insert(i, x) inserts in place
joina.join(", ")a.join(", ")
object literal{hp: 10, name: "Ada"}a map: {"hp": 10, "name": "Ada"}
read, write a keyo.hp, o.hp = 5m["hp"], m["hp"] = 5
has, delete"hp" in o, delete o.hpm.contains("hp"), m.erase("hp")
keysObject.keys(o)m.keys(), in key order
jaiscript
auto nums = [5, 3, 8, 1];
nums.push(4);
print(nums.size(), " ", nums.index_of(8), " ", nums.contains(7), " ", to_json(array::slice(nums, 1, 3)));
auto big = array::filter(nums, x => x > 3);
big.sort();
print(to_json(big), " ", big.join("-"));
int dbl(int v) { return v * 2; }
print(to_json(array::map(nums, dbl)));

var hero = {"hp": 10, "name": "Ada"};
hero["hp"] = 15;
hero["level"] = 2;
hero.erase("name");
print(to_json(hero), " ", hero.contains("hp"), " ", to_json(hero.keys()));
output
5 2 false [3,8]
[4,5,8] 4-5-8
[10,6,16,2,8]
{"hp":15,"level":2} true ["hp","level"]

hero is a var: an auto map literal must hold one value type, and {"hp": 10, "name": "Ada"} holds two. Containers & Strings lists every array, map and string method.

Control flow​

JavaScriptJaiScript
for-offor (const x of xs)for (const auto x : xs) or for (auto x : xs); for (auto& x : xs) changes the elements
entriesfor (const [k, v] of Object.entries(o))for (auto [k, v] : m); for (auto& [k, v] : m) makes v the entry's value
if, while, classic forthe samethe same
do ... whiledo { ... } while (c);the same: do { ... } while (c);
switchfalls through without breakbreaks by default; fallthrough; continues into the next case
jaiscript
var inventory = {"potion": 3, "arrow": 20};
for (auto [item, count] : inventory) { print(item, " x", count); }
var names = ["ada", "bo"];
for (auto& n : names) { n = n + "!"; }
print(names.join(" "));
output
arrow x20
potion x3
ada! bo!

Classes​

JavaScriptJaiScript
fieldsassigned in the constructordeclared with a type and an initializer: int hp = 10;
constructorconstructor(name) { this.name = name; }a method named after the class: Hero(string n) { name = n; }
new instancenew Hero("Ada")Hero("Ada") is a value; new Hero("Ada") is a shared handle
inheritanceclass Mage extends Heroclass Mage : Hero
base constructorsuper(name);Mage(string n) : super(n) {}
overrideimplicitrequired: string describe() override { ... }
call the basesuper.describe()super::describe()
thisthis.hp, alwaysthis.hp, or just hp
private#golda private: section, reachable from the class and its subclasses
getter, setterget wealth() {}, set wealth(v) {}_get_wealth() and _set_wealth(v); obj.wealth goes through them
staticstatic count = 0; and Hero.countstatic int count = 0; and Hero::count
method as a valueconst f = hero.heal.bind(hero);var f = reflect::bind(hero, "heal");, or a lambda; hero.heal without a call is an error
operatorsnonea class declares them: Vec operator+(Vec o), bool operator==(Vec o)
jaiscript
class Hero {
private:
    int gold = 0;
public:
    static int count = 0;
    string name;
    int hp = 10;
    Hero(string n) { name = n; Hero::count += 1; }
    string describe() { return name + " (" + hp + " hp)"; }
    int _get_wealth() { return gold; }
    void _set_wealth(int v) { gold = v < 0 ? 0 : v; }
}
class Mage : Hero {
    int mana = 30;
    Mage(string n) : super(n) {}
    string describe() override { return super::describe() + " with " + mana + " mana"; }
}
auto a = Hero("Ada");
auto m = Mage("Merlin");
a.wealth = 120;
m.wealth = -5;
print(a.describe());
print(m.describe());
print(a.wealth, " ", m.wealth, " ", Hero::count);
output
Ada (10 hp)
Merlin (10 hp) with 30 mana
120 0 2

Classes covers constructors, accessors, statics and hot reload.

Errors, strings and null​

JavaScriptJaiScript
throwthrow new Error("bad")throw "bad"; (any value: a string, a number, an instance)
catchcatch (e) { e.message; e.stack; }catch (e) { e.message; e.stack; e.value; }; e itself reads as the message
finallyfinally { ... }none: a destructor, ~Guard(), runs when its scope ends, a throw included
template string`${name} has ${n}`the same, with format specs: `${hp:.1f}`
lengths.lengths.size() counts bytes; a utf8_string counts characters
upper cases.toUpperCase() returns a new strings.to_upper() changes s; string::to_upper(s) returns a new one
to numberNumber("42")"42".to_int(), "2.5".to_float(): null when the text is not a number
to stringString(x)to_string(x)
regular expression/(\d+)-(\d+)/g, s.match(re), s.replace(re, "$2")regex(R"((\d+)-(\d+))"); r.search(s) for one match, r.find_all(s) for every one (there is no g flag), r.replace(s, "$2"); matching is linear-time, so lookaround and backreferences are errors (Regular expressions)
optional chainingo?.f(), a?.b.co?.f(); a null o skips the rest of the chain, so o?.a.b is null too
nullish defaultx ?? "none"the same: x ?? "none"; x ??= v assigns only when x is null
jaiscript
class Guard {
    string what;
    Guard(string w) { what = w; }
    ~Guard() { print("released ", what); }
}
function load(string path) {
    Guard g = Guard(path);
    throw "missing " + path;
}
try { load("level.dat"); } catch (e) {
    print("caught: ", e, " (line ", e.line, ", value ", e.value, ")");
}
var hp = 72.25;
var who = null;
print(`hp ${hp:.1f}`, " ", "42".to_int() + 1, " ", "ten".to_int() == null, " ", who?.size() == null);
print(who ?? "none", " ", string::to_upper("quiet"));
output
released level.dat
caught: missing level.dat (line 8, value missing level.dat)
hp 72.2 43 true true
none QUIET

Modules, coroutines, references and JSON​

JavaScriptJaiScript
moduleimport { area } from "./geometry.js";import "geometry.jai";: its functions and classes join the program, nothing is exported
reloada bundler's hot reloadan import reached again re-runs a module whose file changed
paste a filenoneinclude "part.jai"; runs the file's text where it stands
generatorfunction* countdown() { yield 3; }coroutine countdown() { yield 3; }, driven by h.next() or range-for
async, awaitawait load()none: a coroutine that the host advances with next() every frame
shared objectevery object is a referencenew Hero(...), typed shared_ptr<Hero>: copies of the handle share one object
alias a variablenoneauto& r = x;, and int& or var& parameters
castNumber(x), String(x)cast<int>(2.7), cast<string>('c'), cast<Mage&>(h)
JSONJSON.stringify(o), JSON.parse(s)to_json(v), from_json(s)
logconsole.log(a, b)print(a, b) joins with no separator; print("{} {}", a, b) formats
MathMath.floor(x), Math.PImath::floor(x), math::PI
jaiscript
import "geometry.jai";

coroutine countdown(int from) {
    for (int i = from; i > 0; i--) { yield i; }
}
for (auto n : countdown(3)) { print("T-", n); }

var data = from_json("{\"name\": \"Ada\", \"tags\": [\"fast\", \"kind\"]}");
print(data["tags"][1], " ", area(3, 4), " ", math::floor(2.7), " ", cast<int>(2.7));
print(to_json({"w": 3, "h": 4}));

with geometry.jai:

jaiscript — geometry.jai
int area(int w, int h) { return w * h; }
output
T-3
T-2
T-1
kind 12 2.0 2
{"h":4,"w":3}

Modules covers reloading and the include/import split, Coroutines the handles.

Watch out​

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

Assignment copies arrays and objects​

In JavaScript, b = a makes two names for one array. In JaiScript it makes a second array, and the same holds for class instances and for by-value parameters. Share on purpose with a handle (new, shared_ptr<T>), or alias with a reference (auto&, a var& parameter):

jaiscript
var a = [1, 2, 3];
var b = a;                     // a copy
b.push(4);

class Hero { int hp = 10; }
Hero h = Hero();
Hero other = h;                // a copy too
other.hp = 1;

shared_ptr<Hero> s = new Hero();
auto alias = s;                // a handle: both names reach one object
alias.hp = 99;

function add_item(var& list, string item) { list.push(item); }
var bag = [];
add_item(bag, "rope");         // a reference parameter reaches the caller's array
print(a.size(), " ", b.size(), " ", h.hp, " ", s.hp, " ", bag.size());
output
3 4 10 99 1

Value Semantics & References is the chapter to read first.

== compares contents, and never converts​

Two arrays, maps or instances are equal when their contents are; same_object(x, y) asks whether they are one object. There is no === (writing one is an error that points to ==) and nothing to need it: == never turns a string into a number. Comparing an int with a string the checker can see is a load error (Types That Fail Early); through var it is simply false.

jaiscript
print([1, 2] == [1, 2], " ", {"a": 1} == {"a": 1});
var a = [1, 2];
var b = a;
print(same_object(a, b));      // b is a separate array with equal contents
var one = "1";
print(1 == one, " ", 1 == 1.0);   // a var operand: false, never a conversion
output
true true
false
false true

Integers and floats are different numbers​

An int divided by an int is an int. Integer overflow and division by zero raise errors that catch sees, instead of producing Infinity:

jaiscript
print(7 / 2, " ", 7 / 2.0, " ", 0.1 + 0.2);
int big = 9223372036854775807;
try { big += 1; } catch (e) { print("caught: ", e); }
try { print(1 / 0); } catch (e) { print("caught: ", e); }
output
3 3.5 0.30000000000000004
caught: Integer overflow in '+='
caught: Division by zero

+ converts a number to text; += does not​

+ with a string on either side turns the other side into text, left to right as in JavaScript. += onto a string accepts only strings and chars, so a number is spelled to_string(n) there:

jaiscript
print("hp: " + 10, " / ", 1 + "0", " / ", "v" + 1 + 2);
string label = "hp: ";
var n = 10;
try { label += n; } catch (e) { print("caught: ", e); }
label += to_string(n);
print(label);
output
hp: 10 / 10 / v12
caught: Cannot apply '+=' to string and int
hp: 10

Written with a literal (label += 10;), the same mistake is a load error.

Empty containers are false​

0, 0.0, "", null and an empty array or map are false in a condition; everything else is true. JavaScript treats [] and {} as true. A class instance is true unless its class declares bool to_bool(), which if (v) calls and !v negates:

jaiscript
var values = [0, 0.0, "", [], {}, null, "0", [0]];
for (var v : values) { print("{} {}", to_json(v), v ? "true" : "false"); }
output
0 false
0.0 false
"" false
[] false
{} false
null false
"0" true
[0] true

switch breaks by default​

A case ends at the next case. fallthrough; is how a case continues into the one below:

jaiscript
function ranks(int level) {
    string t = "";
    switch (level) {
        case 1: t += "novice ";
        case 2:
            t += "adept ";
            fallthrough;
        case 3: t += "master";
    }
    return t;
}
print("[", ranks(1), "] [", ranks(2), "]");
output
[novice ] [adept master]

Some string methods change the string​

to_upper, to_lower, trim, replace, pad_left and the other mutators change the string they are called on and return it. The string:: functions are the versions that return a new string:

jaiscript
string name = "ada";
string shout = name.to_upper();          // name changes too
print(name, " ", shout);
string quiet = "BOB";
print(string::to_lower(quiet), " ", quiet);
output
ADA ADA
bob BOB

Map keys​

A bare name before a colon is the key's text, as in JavaScript; a computed key goes through []. A missing key reads null and adds nothing. A map walks its keys in key order, whatever order they were added in. A dot reads a key that exists (a missing one raises map has no member or key 'mana', where [] reads null), and a store needs []:

jaiscript
var key = "speed";
var stats = {speed: 3, hp: 10};          // the keys "speed" and "hp"
stats[key] = 4;
print(to_json(stats), " ", stats["mana"] == null, " ", stats.size());
print(stats.hp);
try { stats.hp = 1; } catch (e) { print("caught: ", e); }
output
{"hp":10,"speed":4} true 2
10
caught: Cannot assign to member of non-object type

The array helpers​

Arrays have JavaScript's helpers, spelled in snake_case: map, reduce, for_each, find, find_index, some, every, concat and filter, with index_of and contains for indexOf and includes, and insert(i, x) for an insert by splice. A callback takes the element, or the element and its index; a predicate answers with true. There is no shift or splice:

jaiscript
var a = [1, 2, 3, 4];
print(to_json(array::map(a, x => x * 2)), " ", a.reduce((sum, x) => sum + x, 0), " ", a.find(x => x > 2));
print(a.find_index(x => x > 9), " ", a.some(x => x > 3), " ", a.every(x => x > 0));
a.for_each((x, i) => print(i, "=", x));
var b = array::concat(a, [5]);
b.insert(0, 0);
print(to_json(b), " ", to_json(a));
output
[2,4,6,8] 10 3
-1 true true
0=1
1=2
2=3
3=4
[0,1,2,3,4,5] [1,2,3,4]

Dot map, filter, concat and slice change the array; the matching array:: functions return new arrays. A map's filter pairs the same way: m.filter(f) keeps the entries in m, map::filter(m, f) returns a new map. insert, sort and remove_if also mutate. Dot map retains the input element type; array::map returns an array<T> when its callback declares return type T, and an array of anything otherwise. A callback whose element parameter is T& gets the live element, so a.for_each([](int& x) { x *= 2; }) doubles every element, as std::for_each does in C++.

A function expression closes over its scope; a lambda names what it captures​

A function(...) expression is JavaScript's closure: it shares the variables of the function around it, and they outlive that function, so the counter is safe to return. In lambda terms it is [&]. An arrow function, () => { ... }, closes over its scope the same way:

jaiscript
function make_counter() {
    int count = 0;
    return function() { count += 1; return count; };
}
var next = make_counter();
next();
print(next());
output
2

A lambda names what it captures, as C++ does: [] captures nothing, and using a local of the function around it is a load error that names it; [&] shares the variable, [=] or [count] copies it:

jaiscript
function broken() {
    int count = 0;
    return []() { return count; };
}
output
jaiscript: parse failed:
<script>:3:26: error: lambda capture list does not capture enclosing local 'count'; name it or use [=] or [&]
        return []() { return count; };
                             ^

A copy is taken when the lambda is made, so a loop that collects callbacks sees each iteration's value, as let does in JavaScript:

jaiscript
var callbacks = [];
for (int i = 0; i < 3; i++) { callbacks.push([i]() { return i; }); }
for (auto cb : callbacks) { print(cb()); }
output
0
1
2

Declare before use​

Top-level code runs from the top down, and nothing is hoisted: a function is defined when execution reaches its declaration. A function body may call one declared further down, because the body runs later:

jaiscript
print(double_it(4));
function double_it(int x) { return x * 2; }
output
== error: Undefined variable 'double_it'

print shows a container's contents​

print(a, b) joins its arguments with no separator, and a container prints its contents with the strings inside quoted, much as console.log does; to_json writes the JSON text. An instance prints its class's to_string(), as "text" + x does, or else its class name and fields. A first argument that is a string holding {} is a format string, so data that might contain braces goes in a placeholder:

jaiscript
var a = [1, "two"];
print(a);
print(to_json(a));
print("{} and {}", 1, "two");
print(1, 2, 3);
var empty = {};
print(to_json(empty), " entries");         // "{}" is read as a placeholder
print("{} entries", to_json(empty));
class Tag { string to_string() { return "<tag>"; } }
print(Tag());
output
[1, "two"]
[1,"two"]
1 and two
123
 entries
{} entries
<tag>

A complete program​

A small quest log in the shape a JavaScript program would take: a shared object holding a map, listeners registered as closures, a generator feeding events, and JSON out.

jaiscript
class QuestLog {
    var quests = {};                          // quest name -> progress
    array<function> listeners = [];

    void on_change(function f) { listeners.push(f); }

    void advance(string name, int by) {
        int now = quests.contains(name) ? quests[name] + by : by;
        quests[name] = now;
        for (auto& f : listeners) { f(name, now); }
    }
}

shared_ptr<QuestLog> journal = new QuestLog();      // one shared object, like a JavaScript object
array<string> messages = [];
journal.on_change([](string name, int progress) {
    messages.push(`${name}: ${progress}%`);         // messages is program scope: read when the listener runs
});

coroutine events() {
    yield ["rats", 50];
    yield ["relic", 25];
    yield ["rats", 50];
}
for (auto ev : events()) { journal.advance(ev[0], ev[1]); }

var done = [];
for (auto kv : journal.quests) {
    if (kv.second >= 100) { done.push(kv.first); }
}
print(messages.join(" | "));
print("done: ", done.join(", "));
print(to_json(journal.quests));
output
rats: 50% | relic: 25% | rats: 100%
done: rats
{"rats":100,"relic":25}

Where to next​

Value Semantics & References is the one chapter to read before writing anything large. The Type Ladder explains var, auto and typed slots, Functions & Lambdas the capture lists, and Containers & Strings every method. The ten-minute tour builds a small game with all of it.