From JavaScript​

JaiScript shares JavaScript's surface: var, let and function, arrow functions, template strings with ${...}, null, ?. and ??, array literals, and try/catch/throw of any value.

Underneath, the contract is C++'s, and five differences account for most surprises:

  • Assignment copies, arrays and objects included. Sharing is explicit: a handle (new) or a reference (auto&).
  • int and float are separate types, and nothing converts between text and numbers on its own.
  • A method changes its receiver. a.sort(), a.filter(f) and s.to_upper() work in place and return nothing; array::filter(a, f) and string::to_upper(s) return copies.
  • A class declares its fields, and a map is not an object: a key is read with m["key"], never m.key.
  • A static checker reads the whole program before its first statement runs, so many mistakes stop the load instead of surfacing later.

Ported side by side​

Five small JavaScript programs and their JaiScript ports. Each pair was run, with Node and on both JaiScript backends, and printed the output shown. The ports use the JavaScript spellings JaiScript shares (let, const, function, arrow functions, ??); Functions sets them beside the C++ ones.

An array pipeline​

javascript
const scores = [72, 95, 40, 88, 61];
const passed = scores.filter(s => s >= 60).map(s => s + 5);
passed.sort((a, b) => b - a);
const total = passed.reduce((sum, s) => sum + s, 0);
console.log(`passed: ${passed.join(", ")}`);
console.log(`average: ${(total / passed.length).toFixed(1)}`);
console.log(`best: ${passed[0]}, any perfect: ${passed.some(s => s === 100)}`);
jaiscript
const scores = [72, 95, 40, 88, 61];
let passed = array::map(array::filter(scores, s => s >= 60), s => s + 5);
passed.sort((a, b) => a > b);
const total = passed.reduce((sum, s) => sum + s, 0);
print(`passed: ${passed.join(", ")}`);
print(`average: ${1.0 * total / passed.size():.1f}`);
print(`best: ${passed[0]}, any perfect: ${passed.some(s => s == 100)}`);
output
passed: 100, 93, 77, 66
average: 84.0
best: 100, any perfect: true
  • filter and map as methods work in place, so a pipeline that builds new arrays calls array::filter and array::map.
  • A sort comparator answers "does a go first?" with a bool. / on two ints is integer division, hence 1.0 *, and a format spec replaces toFixed.

A class and a list of instances​

javascript
class Monster {
  constructor(name, hp) { this.name = name; this.hp = hp; }
  hit(damage) { this.hp = Math.max(0, this.hp - damage); }
}
const pack = [new Monster("orc", 30), new Monster("bat", 8)];
const boss = pack[0];
for (const m of pack) { m.hit(10); }
boss.hit(5);
for (const m of pack) { console.log(`${m.name}: ${m.hp}`); }
jaiscript
class Monster {
    string name;
    int hp = 0;
    Monster(string n, int h) { name = n; hp = h; }
    function hit(int damage) { hp = math::max(0, hp - damage); }
}
let pack = [new Monster("orc", 30), new Monster("bat", 8)];   // new: shared objects, as in JavaScript
let boss = pack[0];
for (let m : pack) { m.hit(10); }
boss.hit(5);
for (let m : pack) { print(`${m.name}: ${m.hp}`); }
output
orc: 15
bat: 0
  • Fields are declared with their types, the constructor is named after the class, and a method reads hp without this.
  • new Monster(...) makes a shared object, as every JavaScript object is, so pack and boss reach the same orc. Without new an array holds values and assignment copies:
jaiscript
class Monster {
    string name;
    int hp = 0;
    Monster(string n, int h) { name = n; hp = h; }
    function hit(int damage) { hp = math::max(0, hp - damage); }
}
let values = [Monster("orc", 30), Monster("bat", 8)];         // without new: the array holds values
let copy = values[0];                                         // a copy of the orc
for (auto& m : values) { m.hit(10); }                         // auto& reaches the elements
copy.hit(5);
print(`${values[0].hp} ${copy.hp}`);
output
20 25

Closures and callbacks​

javascript
function makeCounter(step = 1) {
  let count = 0;
  return () => { count += step; return count; };
}
const next = makeCounter();
const byTen = makeCounter(10);
next(); next();
const callbacks = [];
for (let i = 0; i < 3; i++) { callbacks.push(() => i * i); }
console.log(next(), byTen(), callbacks.map(cb => cb()).join(" "));
jaiscript
function make_counter(step = 1) {
    let count = 0;
    return () => { count += step; return count; };
}
const next = make_counter();
const by_ten = make_counter(10);
next(); next();
let callbacks = [];
for (let i = 0; i < 3; i++) { callbacks.push(() => i * i); }
print(next(), " ", by_ten(), " ", array::map(callbacks, cb => cb()).join(" "));
output
3 10 0 1 4
  • Nothing changes but the names and print: an arrow function shares the variables around it, and the loop's i is each iteration's own, as with let in JavaScript.

A dictionary​

javascript
const stock = new Map([["arrow", 20], ["potion", 3]]);
stock.set("rope", 1);
stock.set("arrow", stock.get("arrow") - 5);
for (const [item, count] of stock) { console.log(`${item} x${count}`); }
console.log(stock.get("sword") ?? 0, stock.has("rope"), stock.size);

const seen = {};
for (const word of "the cat and the hat".split(" ")) { seen[word] = (seen[word] ?? 0) + 1; }
console.log(Object.keys(seen).sort().map(w => `${w}=${seen[w]}`).join(" "));
jaiscript
let stock = {"arrow": 20, "potion": 3};
stock["rope"] = 1;
stock["arrow"] = stock["arrow"] - 5;
for (let [item, count] : stock) { print(`${item} x${count}`); }
print(stock.get("sword") ?? 0, " ", stock.has("rope"), " ", stock.size());

let seen = {};
for (let word : "the cat and the hat".split(" ")) { seen[word] = (seen.get(word) ?? 0) + 1; }
print(array::map(seen.keys(), w => `${w}=${seen[w]}`).join(" "));
output
arrow x15
potion x3
rope x1
0 true 3
and=1 cat=1 hat=1 the=2
  • A map literal with [] access replaces both Map and the object used as a dictionary. It iterates in key order, where a Map iterates in insertion order (here the two coincide).
  • stock["sword"] on a missing key raises Key "sword" not found in map where JavaScript gives undefined; get answers null, so get(k) ?? 0 reads a default.

A generator and a thrown class​

javascript
class ParseError extends Error {
  constructor(line, text) { super(`line ${line}: ${text}`); this.line = line; }
}
function* numbers(lines) {
  let n = 0;
  for (const text of lines) {
    n++;
    if (isNaN(Number(text))) { throw new ParseError(n, `"${text}" is not a number`); }
    yield Number(text);
  }
}
let sum = 0;
try {
  for (const value of numbers(["4", "8", "x", "15"])) { sum += value; }
} catch (e) {
  if (!(e instanceof ParseError)) { throw e; }
  console.log(`parse error, ${e.message}`);
}
console.log(`sum so far: ${sum}`);
jaiscript
class ParseError {
    int line;
    string message;
    ParseError(int l, string text) { line = l; message = `line ${l}: ${text}`; }
}
coroutine numbers(lines) {
    let n = 0;
    for (let text : lines) {
        n++;
        if (text.to_int() == null) { throw ParseError(n, `"${text}" is not a number`); }
        yield text.to_int();
    }
}
let sum = 0;
try {
    for (let value : numbers(["4", "8", "x", "15"])) { sum += value; }
} catch (ParseError e) {
    print(`parse error, ${e.message}`);
}
print(`sum so far: ${sum}`);
output
parse error, line 3: "x" is not a number
sum so far: 12
  • function* becomes coroutine, and a range-for drives it. Any value can be thrown, so the error class extends nothing.
  • catch (ParseError e) selects by type, which replaces the instanceof test and the rethrow. to_int() answers null for text that is not a number.
  • e instanceof ParseError is JaiScript too, for a class or any class derived from it.

Feature by feature​

Variables, types and functions

JavaScriptJaiScript
holds anythinglet x = 5; x = "five";the same, or var x = 5;: let and var are one word
inferred once, then fixed(TypeScript) let n = 5;auto n = 5; is an int from then on; n = "five" is a load error
spelled out(TypeScript) let hp: number = 10;int hp = 10; float speed = 2.5; string name = "Ada"; bool alive = true;
numbersone number typeint and float, both 64-bit: 7 / 2 is 3, 7 / 2.0 is 3.5
constantconst MAX = 10;const MAX = 10; or const int MAX = 10;; assigning to it is a load error
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; (extra elements ignored, missing ones null)
spread, rest[...a, 3], f(...args), function f(...xs)none: array::concat(a, [3]); a call passes its arguments one by one
functionfunction 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; }
closure() => count++an arrow function or function() { ... } shares the variables around it; a C++ lambda names its captures: [&] shares, [=] or [count] copies
function valueconst f = add; f(1, 2);the same

Arrays, maps and strings

JavaScriptJaiScript
array literal[1, 2, 3][1, 2, 3]; array<int> pins the element type
lengtha.length, s.lengtha.size(), s.size() (bytes; a utf8_string counts characters)
add, removea.push(x), a.pop(), a.splice(i, 1)a.push(x), a.pop(), a.remove(i), a.insert(i, x); there is no shift
searcha.indexOf(x), a.includes(x)a.index_of(x) (-1 when absent), a.contains(x)
slice, filter, map, concata.slice(1, 3), a.filter(f), a.map(f), a.concat(b)array::slice(a, 1, 3), array::filter(a, f), array::map(a, f), array::concat(a, b). The dot forms change a and return nothing.
sorta.sort((x, y) => x - y)a.sort() orders numbers by value; a comparator answers "does x go first?": a.sort((x, y) => x < y)
reduce, forEach, find, findIndex, some, everya.reduce(f, 0), a.forEach(f), ...a.reduce(f, 0), a.for_each(f), a.find(f) (null when none), a.find_index(f) (-1), a.some(f), a.every(f)
joina.join(", ")the same
object literal{hp: 10, name: "Ada"}a map: {"hp": 10, "name": "Ada"}; a bare name before the colon is the key's text
read, write a keyo.hp, o.hp = 5m["hp"], m["hp"] = 5; reading a missing key raises, m.get("hp") answers null
has, delete, keys"hp" in o, delete o.hp, Object.keys(o)m.contains("hp"), m.erase("hp"), m.keys() (in key order, as iteration is)
upper cases.toUpperCase() returns a new strings.to_upper() changes s; string::to_upper(s) returns a new one
to number, to stringNumber("42"), String(x)"42".to_int(), "2.5".to_float() (null when the text is not a number); to_string(x)
template string`${name} has ${n}`the same, with format specs: `${hp:.1f}`
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 all (no g flag), r.replace(s, "$2"). Matching is linear-time, so lookaround and backreferences are errors (Regular expressions).
JSONJSON.stringify(o), JSON.parse(s)to_json(v), from_json(s)
logconsole.log(a, b)print(a, b) joins with no separator and shows a container's contents; print("{} {}", a, b) formats. Only a string literal written first is a format string.
MathMath.floor(x), Math.PImath::floor(x), math::PI
jaiscript
auto nums = [5, 3, 8, 1];
nums.push(4);
auto big = array::filter(nums, x => x > 3);      // a copy; nums.filter(f) would change nums
big.sort();
print(nums.size(), " ", nums.index_of(8), " ", big, " ", array::map(big, x => x * 2), " ", big.reduce((sum, x) => sum + x, 0));
var hero = {hp: 10, name: "Ada"};                // var: the values are of two types
hero["hp"] = 15;
hero.erase("name");
print(hero, " ", hero.contains("hp"), " ", hero.get("mana"), " ", hero.keys());
string name = "ada";
name.to_upper();                                 // changes name, returns nothing
print(name, " ", string::to_lower(name), " ", "42".to_int() + 1, " ", `hp ${72.25:.1f}`);
output
5 2 [4, 5, 8] [8, 10, 16] 17
{"hp": 15} true null ["hp"]
ADA ada 43 hp 72.2

Control flow, errors and null

JavaScriptJaiScript
for-offor (const x of xs)for (const x : xs), or for (auto x : xs); for (auto& x : xs) changes the elements. Over an array of new objects for (const m : pack) { m.hit(10); } behaves as JavaScript's does: m cannot be rebound and its object changes. Over an array of values m is a read-only copy
is it a T?x instanceof Tthe same, for a class and the classes derived from it; false for a value that is no object
entriesfor (const [k, v] of Object.entries(o))for (const [k, v] : m) or for (auto [k, v] : m); with auto&, v is the entry's value
if, while, classic for, do-whilethe same
switchfalls through without breakbreaks by default; fallthrough; continues into the next case, and case 1: case 2: share a body
throwthrow new Error("bad")throw "bad"; (any value: a string, a number, an instance)
catchcatch (e) { e.message; e.stack; }catch (e): e is the thrown value, with e.message, e.line and e.stack. catch (Type e) takes one type, and catch (error e) the engine's own errors, with e.kind.
finallyfinally { ... }none: a destructor, ~Guard(), runs when its scope ends, a throw included
optional chainingo?.f(), a?.b.cthe same; a null o skips the rest of the chain
nullish defaultx ?? "none", x ??= vthe same
generatorfunction* countdown() { yield 3; }coroutine countdown() { yield 3; }, driven by h.next() or a range-for
async, awaitawait load()none: a coroutine that the host advances with next()
moduleimport { area } from "./geometry.js";import "geometry.jai";: its functions and classes join the program, and nothing is exported. An import reached again re-runs a module whose file changed. include "part.jai"; runs a file's text where it stands.

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, copied on assignment; new Hero("Ada") is a shared handle (shared_ptr<Hero>)
inheritanceclass Mage extends Hero, super(name);class Mage : Hero, Mage(string n) : super(n) {}
overrideimplicit; super.describe()required: string describe() override { ... }; 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;, Hero.countstatic int count = 0;, Hero::count
method as a valuehero.heal.bind(hero)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)
alias a variable, castnone; Number(x)auto& r = x;, int& and var& parameters; cast<int>(2.7), cast<Mage&>(h)
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; }
    ~Hero() { print(name, " leaves"); }
    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`; }
}
function quest() {
    auto m = Mage("Merlin");
    m.wealth = -5;
    print(m.describe(), ", wealth ", m.wealth, ", heroes ", Hero::count);
    throw "ambush";                      // m's destructor runs on the way out: the finally
}
try { quest(); } catch (e) { print("caught: ", e, " (line ", e.line, ")"); }
output
Merlin (10 hp) with 30 mana, wealth 0, heroes 1
Merlin leaves
caught: ambush (line 23)

Watch out​

JavaScript habits that fail​

Some of these messages name the fix, and some are a parse error far from the cause.

HabitWhat happensWrite
o.hp = 5 on a mapCannot assign to member of non-object typeo["hp"] = 5
o.hp on a mapmap has no member 'hp'; read the key as ["hp"]o["hp"]
o["mana"] for a missing keyKey "mana" not found in mapo.get("mana") (null) or o.get("mana", 0)
for (var x of xs), for (var k in m)parse error: expected ';' after for loop initializerfor (auto x : xs), for (auto [k, v] : m)
typeof xparse error: expected ')' after arguments or expected ';' after variable declarationtype_of(x)
a.length, s.length'length' is a method of array; call it with a.length() or wrap the call in a lambdaa.size()
try { } catch (e) { } finally { }parse error: expected '}' after constructor argumentsa destructor
var b = a.sort();, a.filter(f).map(g)sort changes a in place and returns nothing; array::sort(a) returns a sorted copyarray::sort(a), array::map(array::filter(a, f), g)
a.sort((x, y) => x - y)sort() comparator must return boola.sort((x, y) => x < y)
a.forEach(f), a.includes(x), s.toUpperCase()no member 'forEach' on 'array' (did you mean 'for_each'?)snake_case: for_each, contains, to_upper
"hp: " + ncannot apply '+' to 'string' and 'int': stringify it with to_string(x) or `${x}``hp: ${n}`, "hp: " + to_string(n)
a === bJaiScript has no '===': == never converts between types, so a == b already compares without coerciona == b
throw new Error("bad")No constructor found for class 'Error'throw "bad", or an instance of your own class
undefined, console.log, Math.floor, parseIntuse of undeclared identifier 'console'null, print, math::floor, s.to_int()
calling a function above its declaration at top level'double_it' is called before its declaration (line 2): top-level code runs in order, so declare it above the call or make the call after itdeclare first; a function body may call one declared further down

Assignment copies; == compares contents​

jaiscript
var a = [1, 2, 3];
var b = a;                     // a copy: b = a makes a second array
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;
print(a.size(), " ", b.size(), " ", h.hp, " ", s.hp);
print([1, 2] == [1, 2], " ", {"a": 1} == {"a": 1}, " ", same_object(a, b), " ", 1 == 1.0);
var one = "1";
print(1 == one, " ", 7 / 2, " ", 0.1 + 0.2);
try { print(1 / 0); } catch (e) { print("caught: ", e); }
var values = [0, 0.0, "", [], {}, null, "0", [0]];
for (var v : values) { print(v ? "T" : "F", skip_newline); }
output
3 4 10 99
true true false true
false 3 0.30000000000000004
caught: Division by zero
FFFFFFTT
  • Copies. b = a copies an array, a map or a class instance, and so does passing one by value; a var& parameter reaches the caller's. Value Semantics & References is the chapter to read first.
  • Equality. == compares arrays, maps and instances by contents, and same_object(x, y) asks whether they are one object. == never converts: an int compared with a string is a load error when the checker sees it (Types That Fail Early) and false through a var.
  • Numbers. Integer overflow and integer division by zero raise errors that catch sees; float division by zero gives inf.
  • Truthiness. 0, 0.0, "", null and an empty array or map are false; JavaScript treats [] and {} as true. An instance is true unless its class declares bool to_bool().

Closures​

jaiscript
function make_counter() {
    int count = 0;
    return () => { count += 1; return count; };     // shares count, which outlives make_counter
}
auto next = make_counter();
next();
var callbacks = [];
for (int i = 0; i < 3; i++) { callbacks.push(() => i); }
int total = 0;                                      // program scope
auto bank = [](int n) { total += n; };              // read and written when the lambda runs
bank(5);
print(next(), " ", callbacks[0](), callbacks[1](), callbacks[2](), " ", total);
output
2 012 5
  • An arrow function or a function(...) expression is JavaScript's closure: it shares the variables around it.
  • A C-style for variable is each iteration's own for the closures that share it, as let is in JavaScript: the loop above gives 0 1 2.
  • A C++-style lambda names what it captures. [] captures nothing, and using an enclosing function's local in one is a parse error: lambda capture list does not capture enclosing local 'count'; name it or use [=] or [&]. Program-scope variables need no capture (Lambdas and captures).

Where to next​

Read Value Semantics & References before writing anything large, then The Type Ladder for var, auto and typed variables. The ten-minute tour builds a small game.