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
| JavaScript | JaiScript | |
|---|---|---|
| holds anything | let 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; |
| numbers | one number type | int (64-bit) and float (64-bit) are separate types: 7 / 2 is 3 |
| constant | const 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 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; 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 |
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);string int float null
3 3.5 3.5auto 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
| JavaScript | JaiScript | |
|---|---|---|
| declaration | 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; }; 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 function | read when the function runs | the same: functions and lambdas read program-scope variables when they run |
| function value | const f = add; f(1, 2); | const f = add; f(1, 2); or var f = add; |
| rest parameters | function f(...xs) | none in script; a host (C++) function can take any number of arguments |
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);3 5 5 9 42 2 7A 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
| JavaScript | JaiScript | |
|---|---|---|
| array literal | [1, 2, 3] | [1, 2, 3]; array<int> pins the element type |
| length | a.length | a.size() (a method; a.length() is the same) |
| add, remove | a.push(x), a.pop() | a.push(x), a.pop(), a.remove(i) by index |
| search | a.indexOf(x), a.includes(x) | a.index_of(x) (-1 when absent), a.contains(x) |
| copy a range | a.slice(1, 3) | array::slice(a, 1, 3); end defaults to size. a.slice changes the input |
| filter | a.filter(x => x > 2) | array::filter(a, x => x > 2) returns a copy; a.filter changes the input |
| sort | a.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, forEach | a.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, findIndex | a.find(f), a.findIndex(f) | a.find(f) (null when none matches), a.find_index(f) (-1 when none does) |
| some, every | a.some(f), a.every(f) | the same |
| concat, splice | a.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 |
| join | a.join(", ") | a.join(", ") |
| object literal | {hp: 10, name: "Ada"} | a map: {"hp": 10, "name": "Ada"} |
| read, write a key | o.hp, o.hp = 5 | m["hp"], m["hp"] = 5 |
| has, delete | "hp" in o, delete o.hp | m.contains("hp"), m.erase("hp") |
| keys | Object.keys(o) | m.keys(), in key order |
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()));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
| JavaScript | JaiScript | |
|---|---|---|
| for-of | for (const x of xs) | for (const auto x : xs) or for (auto x : xs); for (auto& x : xs) changes the elements |
| entries | for (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 for | the same | the same |
| do ... while | do { ... } while (c); | the same: do { ... } while (c); |
| switch | falls through without break | breaks by default; fallthrough; continues into the next case |
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(" "));arrow x20
potion x3
ada! bo!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; new Hero("Ada") is a shared handle |
| inheritance | class Mage extends Hero | class Mage : Hero |
| base constructor | super(name); | Mage(string n) : super(n) {} |
| override | implicit | required: string describe() override { ... } |
| call the base | super.describe() | 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; and Hero.count | static int count = 0; and Hero::count |
| method as a value | const f = hero.heal.bind(hero); | var f = 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) |
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);Ada (10 hp)
Merlin (10 hp) with 30 mana
120 0 2Classes covers constructors, accessors, statics and hot reload.
Errors, strings and null
| JavaScript | JaiScript | |
|---|---|---|
| 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.message; e.stack; e.value; }; e itself reads as the message |
| finally | finally { ... } | none: a destructor, ~Guard(), runs when its scope ends, a throw included |
| template string | `${name} has ${n}` | the same, with format specs: `${hp:.1f}` |
| length | s.length | s.size() counts bytes; a utf8_string counts characters |
| upper case | s.toUpperCase() returns a new string | s.to_upper() changes s; string::to_upper(s) returns a new one |
| to number | Number("42") | "42".to_int(), "2.5".to_float(): null when the text is not a number |
| to string | String(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 chaining | o?.f(), a?.b.c | o?.f(); a null o skips the rest of the chain, so o?.a.b is null too |
| nullish default | x ?? "none" | the same: x ?? "none"; x ??= v assigns only when x is null |
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"));released level.dat
caught: missing level.dat (line 8, value missing level.dat)
hp 72.2 43 true true
none QUIETModules, coroutines, references and JSON
| JavaScript | JaiScript | |
|---|---|---|
| module | import { area } from "./geometry.js"; | import "geometry.jai";: its functions and classes join the program, nothing is exported |
| reload | a bundler's hot reload | an import reached again re-runs a module whose file changed |
| paste a file | none | include "part.jai"; runs the file's text where it stands |
| generator | function* countdown() { yield 3; } | coroutine countdown() { yield 3; }, driven by h.next() or range-for |
| async, await | await load() | none: a coroutine that the host advances with next() every frame |
| shared object | every object is a reference | new Hero(...), typed shared_ptr<Hero>: copies of the handle share one object |
| alias a variable | none | auto& r = x;, and int& or var& parameters |
| cast | Number(x), String(x) | cast<int>(2.7), cast<string>('c'), cast<Mage&>(h) |
| 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; print("{} {}", a, b) formats |
| Math | Math.floor(x), Math.PI | math::floor(x), math::PI |
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:
int area(int w, int h) { return w * h; }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):
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());3 4 10 99 1Value 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.
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 conversiontrue true
false
false trueIntegers 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:
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); }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:
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);hp: 10 / 10 / v12
caught: Cannot apply '+=' to string and int
hp: 10Written 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:
var values = [0, 0.0, "", [], {}, null, "0", [0]];
for (var v : values) { print("{} {}", to_json(v), v ? "true" : "false"); }0 false
0.0 false
"" false
[] false
{} false
null false
"0" true
[0] trueswitch breaks by default
A case ends at the next case. fallthrough; is how a case continues into the one below:
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), "]");[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:
string name = "ada";
string shout = name.to_upper(); // name changes too
print(name, " ", shout);
string quiet = "BOB";
print(string::to_lower(quiet), " ", quiet);ADA ADA
bob BOBMap 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 []:
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); }{"hp":10,"speed":4} true 2
10
caught: Cannot assign to member of non-object typeThe 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:
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));[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:
function make_counter() {
int count = 0;
return function() { count += 1; return count; };
}
var next = make_counter();
next();
print(next());2A 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:
function broken() {
int count = 0;
return []() { return count; };
}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:
var callbacks = [];
for (int i = 0; i < 3; i++) { callbacks.push([i]() { return i; }); }
for (auto cb : callbacks) { print(cb()); }0
1
2Declare 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:
print(double_it(4));
function double_it(int x) { return x * 2; }== 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:
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());[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.
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));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.