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&). intandfloatare separate types, and nothing converts between text and numbers on its own.- A method changes its receiver.
a.sort(),a.filter(f)ands.to_upper()work in place and return nothing;array::filter(a, f)andstring::to_upper(s)return copies. - A class declares its fields, and a map is not an object: a key is read with
m["key"], neverm.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
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)}`);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)}`);passed: 100, 93, 77, 66
average: 84.0
best: 100, any perfect: truefilterandmapas methods work in place, so a pipeline that builds new arrays callsarray::filterandarray::map.- A sort comparator answers "does a go first?" with a bool.
/on two ints is integer division, hence1.0 *, and a format spec replacestoFixed.
A class and a list of instances
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}`); }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}`); }orc: 15
bat: 0- Fields are declared with their types, the constructor is named after the class, and a method reads
hpwithoutthis. new Monster(...)makes a shared object, as every JavaScript object is, sopackandbossreach the same orc. Withoutnewan array holds values and assignment copies:
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}`);20 25Closures and callbacks
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(" "));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(" "));3 10 0 1 4- Nothing changes but the names and
print: an arrow function shares the variables around it, and the loop'siis each iteration's own, as withletin JavaScript.
A dictionary
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(" "));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(" "));arrow x15
potion x3
rope x1
0 true 3
and=1 cat=1 hat=1 the=2- A map literal with
[]access replaces bothMapand the object used as a dictionary. It iterates in key order, where aMapiterates in insertion order (here the two coincide). stock["sword"]on a missing key raisesKey "sword" not found in mapwhere JavaScript givesundefined;getanswers null, soget(k) ?? 0reads a default.
A generator and a thrown class
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}`);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}`);parse error, line 3: "x" is not a number
sum so far: 12function*becomescoroutine, and a range-fordrives it. Any value can be thrown, so the error class extends nothing.catch (ParseError e)selects by type, which replaces theinstanceoftest and the rethrow.to_int()answers null for text that is not a number.e instanceof ParseErroris JaiScript too, for a class or any class derived from it.
Feature by feature
Variables, types and functions
| JavaScript | JaiScript | |
|---|---|---|
| holds anything | let 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; |
| numbers | one number type | int and float, both 64-bit: 7 / 2 is 3, 7 / 2.0 is 3.5 |
| constant | const MAX = 10; | const MAX = 10; or const int MAX = 10;; assigning to it is a load error |
| what is it? | typeof x | type_of(x): "int", "float", "string", "array", "map", "null", or a class's name |
| nothing | null and undefined | null only |
| destructuring | const [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 |
| function | function 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 parameter | function hit(dmg = 5) | function hit(int dmg = 5) |
| arrow function | x => x * 2 | the 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 value | const f = add; f(1, 2); | the same |
Arrays, maps and strings
| JavaScript | JaiScript | |
|---|---|---|
| array literal | [1, 2, 3] | [1, 2, 3]; array<int> pins the element type |
| length | a.length, s.length | a.size(), s.size() (bytes; a utf8_string counts characters) |
| add, remove | a.push(x), a.pop(), a.splice(i, 1) | a.push(x), a.pop(), a.remove(i), a.insert(i, x); there is no shift |
| search | a.indexOf(x), a.includes(x) | a.index_of(x) (-1 when absent), a.contains(x) |
| slice, filter, map, concat | a.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. |
| sort | a.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, every | a.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) |
| join | a.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 key | o.hp, o.hp = 5 | m["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 case | s.toUpperCase() returns a new string | s.to_upper() changes s; string::to_upper(s) returns a new one |
| to number, to string | Number("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). |
| JSON | JSON.stringify(o), JSON.parse(s) | to_json(v), from_json(s) |
| log | console.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. |
| Math | Math.floor(x), Math.PI | math::floor(x), math::PI |
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}`);5 2 [4, 5, 8] [8, 10, 16] 17
{"hp": 15} true null ["hp"]
ADA ada 43 hp 72.2Control flow, errors and null
| JavaScript | JaiScript | |
|---|---|---|
| for-of | for (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 T | the same, for a class and the classes derived from it; false for a value that is no object |
| entries | for (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-while | the same | |
| switch | falls through without break | breaks by default; fallthrough; continues into the next case, and case 1: case 2: share a body |
| throw | throw new Error("bad") | throw "bad"; (any value: a string, a number, an instance) |
| catch | catch (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. |
| finally | finally { ... } | none: a destructor, ~Guard(), runs when its scope ends, a throw included |
| optional chaining | o?.f(), a?.b.c | the same; a null o skips the rest of the chain |
| nullish default | x ?? "none", x ??= v | the same |
| generator | function* countdown() { yield 3; } | coroutine countdown() { yield 3; }, driven by h.next() or a range-for |
| async, await | await load() | none: a coroutine that the host advances with next() |
| module | import { 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
| JavaScript | JaiScript | |
|---|---|---|
| fields | assigned in the constructor | declared with a type and an initializer: int hp = 10; |
| constructor | constructor(name) { this.name = name; } | a method named after the class: Hero(string n) { name = n; } |
| new instance | new Hero("Ada") | Hero("Ada") is a value, copied on assignment; new Hero("Ada") is a shared handle (shared_ptr<Hero>) |
| inheritance | class Mage extends Hero, super(name); | class Mage : Hero, Mage(string n) : super(n) {} |
| override | implicit; super.describe() | required: string describe() override { ... }; super::describe() |
| this | this.hp, always | this.hp, or just hp |
| private | #gold | a private: section, reachable from the class and its subclasses |
| getter, setter | get wealth() {}, set wealth(v) {} | _get_wealth() and _set_wealth(v); obj.wealth goes through them |
| static | static count = 0;, Hero.count | static int count = 0;, Hero::count |
| method as a value | hero.heal.bind(hero) | reflect::bind(hero, "heal"), or a lambda; hero.heal without a call is an error |
| operators | none | a class declares them: Vec operator+(Vec o), bool operator==(Vec o) |
| alias a variable, cast | none; Number(x) | auto& r = x;, int& and var& parameters; cast<int>(2.7), cast<Mage&>(h) |
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, ")"); }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.
| Habit | What happens | Write |
|---|---|---|
o.hp = 5 on a map | Cannot assign to member of non-object type | o["hp"] = 5 |
o.hp on a map | map has no member 'hp'; read the key as ["hp"] | o["hp"] |
o["mana"] for a missing key | Key "mana" not found in map | o.get("mana") (null) or o.get("mana", 0) |
for (var x of xs), for (var k in m) | parse error: expected ';' after for loop initializer | for (auto x : xs), for (auto [k, v] : m) |
typeof x | parse error: expected ')' after arguments or expected ';' after variable declaration | type_of(x) |
a.length, s.length | 'length' is a method of array; call it with a.length() or wrap the call in a lambda | a.size() |
try { } catch (e) { } finally { } | parse error: expected '}' after constructor arguments | a destructor |
var b = a.sort();, a.filter(f).map(g) | sort changes a in place and returns nothing; array::sort(a) returns a sorted copy | array::sort(a), array::map(array::filter(a, f), g) |
a.sort((x, y) => x - y) | sort() comparator must return bool | a.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: " + n | cannot apply '+' to 'string' and 'int': stringify it with to_string(x) or `${x}` | `hp: ${n}`, "hp: " + to_string(n) |
a === b | JaiScript has no '===': == never converts between types, so a == b already compares without coercion | a == 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, parseInt | use 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 it | declare first; a function body may call one declared further down |
Assignment copies; == compares contents
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); }3 4 10 99
true true false true
false 3 0.30000000000000004
caught: Division by zero
FFFFFFTT- Copies.
b = acopies an array, a map or a class instance, and so does passing one by value; avar¶meter reaches the caller's. Value Semantics & References is the chapter to read first. - Equality.
==compares arrays, maps and instances by contents, andsame_object(x, y)asks whether they are one object.==never converts: anintcompared with astringis a load error when the checker sees it (Types That Fail Early) and false through avar. - Numbers. Integer overflow and integer division by zero raise errors that
catchsees; float division by zero givesinf. - Truthiness.
0,0.0,"",nulland an empty array or map are false; JavaScript treats[]and{}as true. An instance is true unless its class declaresbool to_bool().
Closures
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);2 012 5- An arrow function or a
function(...)expression is JavaScript's closure: it shares the variables around it. - A C-style
forvariable is each iteration's own for the closures that share it, asletis 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.