Operators & Arithmetic​

JaiScript has C++'s operators with C++'s precedence, and none of its undefined behavior. Five rules cover most of the chapter:

  • int op int is an int (7 / 2 is 3); a float operand makes the result a float.
  • Integer overflow and integer division by zero raise catchable errors. Float arithmetic is IEEE: it gives inf and nan.
  • + joins text with text. A number beside a string is an error, never a conversion.
  • A char is an unsigned integer 0–255 in arithmetic and text beside a string.
  • Evaluation order is fixed: the callee, then the arguments left to right; in an assignment, the right side first.

The table​

CategoryOperatorsNotes
Arithmetic+ - * / %int stays int; any float operand promotes
Unary- + ! ~ ++ --++/-- prefix and postfix; + is the number itself
Assignment= += -= *= /= %= &= |= ^= <<= >>=the target is evaluated once; a typed target converts the result C++-style (Type Ladder)
Comparison== != < > <= >= <=>1 == 1.0 is true; the builtin <=> is -1, 0 or 1; arrays, maps and class values compare memberwise (Equality)
Logical&& || !short-circuit
Bitwise& | ^ ~ << >>integer or char operands; a shift count is 0 to 63 (Shift amount must be between 0 and 63) and its result wraps
Null?. ?? ??=below
Ternary?:right-associative; throw is an expression, so x > 0 ? x : throw "negative" works
Member, index. -> [] ::-> is a synonym for . on values and handles alike; :: reaches statics and namespaces
Castcast<T>(e) cast<T&>(e) cast<shared_ptr<T>>(e)the explicit conversion: a copy, a reference, a handle (Type Ladder)
jaiscript
print(`${7 / 2} ${7.0 / 2} ${7 % 3} ${2 + 3 * 4}`);
auto x = 5;
print(`${x++} ${x} ${--x} ${-x}`);
print(`${6 & 3} ${6 | 3} ${6 ^ 3} ${~0} ${1 << 4} ${32 >> 2}`);
print(`${3 <=> 7} ${7 <=> 7} ${"a" < "b"} ${1 == 1.0}`);
auto calls = 0;
function tick() { calls += 1; return true; }
auto skipped = false && tick();       // short-circuit: tick never runs
print(calls);
try { print(1 / 0); } catch (e) { print(`caught: ${e}`); }
print(`${1.0 / 0.0} ${-1 / 0.0} ${0.0 % 0.0}`);
output
3 3.5 1 14
5 6 5 -5
2 7 5 -1 16 8
-1 0 true true
0
caught: Division by zero
inf -inf nan

Integer / and % by zero raise Division by zero; with a float operand the result is an infinity or a NaN. A NaN operand makes ==, <, <=, > and >= false and != true, wherever the comparison runs.

Text and +. "a" + "b" joins, and a char joins as its character ("ab" + 'c', 'c' + "ab"). An object beside a string joins its display text, unless its class declares an operator+ that takes the string. A number, bool or null beside a string is an error in either order: cannot apply '+' to 'string' and 'int': stringify it with to_string(x) or `${x}`, at load when the checker knows both types and at run time otherwise. += onto a string appends strings and chars and raises on anything else. Strings compare lexicographically.

Checked overflow: raises, never wraps silently​

Overflow in + - *, unary -, ++/--, a compound assignment, or INT64_MIN / -1 raises a catchable error and leaves the variable as it was:

jaiscript
auto big = 9223372036854775807;   // INT64_MAX
try { big = big + 1; } catch (e) { print("caught: ", e, ", big is still ", big); }
int h = -3750763034362895579;     // the FNV-1a offset basis, 0xcbf29ce484222325, as an int
for (auto c : "abc") { h = math::wrap_mul(h ^ c, 1099511628211); }
print("fnv1a = ", h);
output
caught: Integer overflow in '+', big is still 9223372036854775807
fnv1a = -1792535898324117685

When wraparound is the point (a hash, a checksum, a generator), say so: math::wrap_add(a, b), math::wrap_sub(a, b) and math::wrap_mul(a, b) give the two's-complement result modulo 264 in every build, and a shift's result always wraps.

Checking is the default build policy, JAISCRIPT_CHECKED_OVERFLOW. A library built with JAISCRIPT_WRAP_ON_OVERFLOW wraps instead, and engine::throw_on_overflow() reports which one a build uses. Integer arithmetic is undefined behavior under neither.

Null: ?., ?? and ??=​

FormMeaning
x?.membernull when x is null, and the rest of the chain is skipped: x?.a.b, x?.items[0] and x?.f().g() are all null. Only the value before the ?. is tested: a null x.a still raises at .b, so each nullable link needs its own ?..
a ?? ba unless it is null, else b; b runs only then. 0, false and "" are values, not null.
a ??= bassigns b only when a holds null; its value is what a holds afterwards. The target is a variable, a member, or an element whose index runs no code (a[i++] ??= v is a load error that asks for a local).

?? binds more loosely than || and more tightly than ?:, and groups to the right: a ?? b || c is a ?? (b || c), and a ?? b ? x : y is (a ?? b) ? x : y.

jaiscript
class Weapon { string name = "axe"; }
class Hero { var weapon = null; }
Hero bare;
Hero armed;
armed.weapon = Weapon();
var nobody = null;
print(bare.weapon?.name ?? "fists", " ", armed.weapon?.name ?? "fists", " ", nobody?.weapon.name ?? "no hero");
var settings = {"volume": 7};
print(settings["volume"] ?? 5, " ", settings.get("music") ?? 5, " ", 0 ?? 5);
var cache = null;
cache ??= [1, 2];
cache ??= [9];
print(cache);
output
fists axe no hero
7 5 0
[1, 2]

Reading a missing map key raises (Maps), so a default is m.get(k) ?? fallback or m.get(k, fallback), and m[k] ??= v needs the key to be there.

Evaluation order is a contract​

C++17's order: the callee before its arguments, the arguments left to right, a binary operator's left operand first, and in an assignment the right side before every part of the target.

Errors surface in the same order: when the callee and the first argument both fail, the callee's error is the one raised.

StepRule
A by-value argumenttakes its value when it is evaluated. A later argument that writes the variable, element or object it read does not change what the callee receives.
A reference argumentbinds its storage when it is evaluated: the object a field argument reads from and the index an element argument names are the ones evaluation saw. A later argument can change what that storage holds, never which storage the parameter names.
obj.f op= e, C::s op= e, ns::x op= ee, then the receiver, then the member's read (a getter included), the arithmetic and the store.
a[j] = e, a[j] op= ee, then a, then j, then the element. An error in e leaves the target untouched.
jaiscript
function a() { print("argument a"); return 1; }
function b() { print("argument b"); return 2; }
function add(x, y) { return x + y; }
function pick() { print("callee"); return add; }
print("sum = ", pick()(a(), b()));

var scores = [10, 20, 30];
function bump() { scores[1] = 99; return 1; }
function add_ref(int& x, y) { return x + y; }
print("by value: ", add(scores[1], bump()));        // 20 was taken before bump ran
scores[1] = 20;
print("by reference: ", add_ref(scores[1], bump()));

var i = 0;
var hits = [0, 0];
function inc(int& x, y) { x += 1; return y; }
inc(hits[i], i++);                                  // binds hits[0], then i moves
print(hits);

string log = "";
class Box { int v = 1; }
shared_ptr<Box> box = new Box();
function target() -> shared_ptr<Box> { log += "target "; return box; }
function value() -> int { log += "value "; return 5; }
target().v += value();                              // the right side runs first
print(log, "-> ", box.v);
output
callee
argument a
argument b
sum = 3
by value: 21
by reference: 100
[1, 0]
value target -> 6

Functions adds what is particular to constructors, coroutines and host functions.

Chars: numbers in arithmetic, text beside a string​

A char operand enters arithmetic, bitwise operators, comparisons with numbers, and the unary -, + and ~ as an int in 0–255, always unsigned. s[i] reads a char from a string, so byte parsing is ordinary arithmetic:

jaiscript
var s = "abc";
print('z' - 'a', " ", s[0] + 0, " ", s[0] == 97, " ", 'a' & 0x7F, " ", -'a', " ", ~'a', " ", type_of('a' + 1));
string word = "banana";
int count = 0;
for (auto ch : word) { if (ch == "a") { count++; } }
char first = "b";                  // a one-character string fills a char
print(count, " ", word[0] == first, " ", word + '!', " ", 'a' < "ab", " ", 'a' == "ab");
switch (word[2]) {
    case "n": print("n at 2");
    default: print("something else");
}
char c = cast<char>(65);           // int -> char, range-checked 0..255
print(c, " ", cast<int>('a'), " ", cast<string>('z') + "!", " ", cast<char>("q"), " ", to_char(66));
try { cast<char>(256); } catch (e) { print("caught: ", e); }
try { char two = word.substr(0, 2); } catch (e) { print("caught: ", e); }
output
25 97 true 97 -97 -98 int
3 true banana! true false
n at 2
A 97 z! q B
caught: Cannot cast 'int' to 'char'
caught: Cannot convert 'string' to 'char': a char takes a string of exactly one character

Beside a string a char is text, never a number.

  • A comparison with a string compares the char as its one-character string, in either order: 'a' == "ab" is false, 'a' < "ab" is true, and the empty string sorts before every char.
  • s + 'c' and 'c' + s join, and s += 'c' appends.
  • A char variable, parameter, field, element or return takes a string of exactly one character, and a string one takes a char (the conversion table). Any other length fails: char c = "ab"; is the load error cannot initialize 'c' declared 'char' with 'string': a char takes a string of exactly one character, and a run-time value raises Cannot convert 'string' to 'char': a char takes a string of exactly one character.
  • A switch over a char matches one-character string cases, and the reverse. In a map the two are one key (Key types).

Between chars and numbers nothing converts implicitly.

  • 'a' + 1 is the int 98. char c = 65; and int n = 'A'; are type errors.
  • Compound assignment and ++/-- promote too: a var holding 'b' holds the int 97 after -= 1. A variable declared char refuses the int (Cannot assign int to 'c' (declared char), a load error when the checker sees it).
  • The explicit conversion is cast: cast<int>(c) is the code, and cast<char>(n) takes an int in 0..255 or a one-character string (else Cannot cast 'int' to 'char'). to_char(n) is the function form of the same range-checked rule.
  • An operand of a class's own operator arrives as written: a char is not promoted on the way in.

Equality and ordering​

Operands== and !=<, <=, >, >=, <=>
numbers, text, boolby value; 1 == 1.0by value; false before true
floatsIEEE: a NaN equals nothing< is IEEE; <=> is a total order in which 0.0 and -0.0 are equivalent and every NaN sorts after the numbers. The default sort() uses it.
arrayselement by elementlexicographic: [1, 5] < [2]
mapsentry by entry in key order, keys by the map's key rules: {1: "a"} and {1.0: "a"} differ (Key types)lexicographic over the ordered entries
class valuesfield by field, unless the class declares operator== or operator<=>Invalid operands for < operator, unless the class declares operator< or operator<=>
shared_ptr, weak_ptr, function valuesidentity, as std::shared_ptr compares: two new objects with equal fields are not equalidentity
referencestheir referentstheir referents

Memberwise comparison stops at the first difference, answers at once on a size or class mismatch, and walks values of any depth. A class's own operator also applies where its instances are elements or fields.

same_object(x, y) asks about identity: references resolve to their referents, handles to the object they share, and two containers or instances are the same object only when they are one allocation. Numbers and strings have no identity, so it compares them by value.

jaiscript
class Stats { int hp = 10; array<int> buffs = [1, 2]; }
Stats a = Stats();
Stats b = Stats();                 // a second value with the same fields
Stats c = Stats(); c.buffs.push(3);
print("values:  ", a == b, " ", a == c);
print("arrays:  ", [1, [2, 3]] == [1, [2, 3]], " ", [1, 2] == [1, 2, 3], " ", [1, 5] < [2]);
print("maps:    ", {"x": 1, "y": 2} == {"y": 2, "x": 1}, " ", {1: "a"} == {1.0: "a"});
var h1 = new Stats();
var h2 = h1;                       // a copy of a handle shares its object
var h3 = new Stats();              // a different object with equal fields
print("handles: ", h1 == h2, " ", h1 == h3);
print("same:    ", same_object(a, b), " ", same_object(h1, h2), " ", same_object(a, a));
output
values:  true false
arrays:  true false true
maps:    true false
handles: true false
same:    false true true

Operator overloading​

C++'s overloadable set and C++20's comparison rewriting: declare operator<=> and <, <=, >, >= follow, and == too when no operator== takes the operand. There is no operator!: one conversion, to_bool(), answers both !v and if (v).

A class declares operators as methods. The set is binary + - * / % & | ^ << >> < > <= >= == != <=>, unary - + ~, and =, [], (); the parameter count tells unary from binary (operator-() negates, operator-(Vec o) subtracts). &&, ||, the comma, -> and new/delete are not overloadable. A bound C++ class gets the same treatment through its binding (the dynamic binder).

jaiscript
class Vec {
    int x = 0; int y = 0;
    Vec(int px, int py) { x = px; y = py; }
    Vec operator+(Vec o) { return Vec(x + o.x, y + o.y); }
    Vec operator*(int k) { return Vec(x * k, y * k); }
    Vec operator-() { return Vec(-x, -y); }
    bool operator==(Vec o) { return x == o.x && y == o.y; }
    bool to_bool() { return x != 0 || y != 0; }
    string to_string() { return `(${x}, ${y})`; }
}
auto v = Vec(1, 2) + Vec(3, 4);
v *= 2;                                    // v = v * 2, through operator*
print(v, " ", -v, " ", v == Vec(8, 12), " ", !Vec(0, 0));

class Version {
    int major = 0; int minor = 0;
    Version(int a, int b) { major = a; minor = b; }
    int operator<=>(Version o) { return major != o.major ? major - o.major : minor - o.minor; }
    int operator<=>(int m) { return major - m; }   // against a bare major number
}
Version v1 = Version(1, 4);
Version v2 = Version(2, 0);
print(v1 < v2, " ", v1 >= v2, " ", v1 == Version(1, 4), " ", v1 != v2, " ", v2 == 2, " ", 1 < v2, " ", 3 <=> v1);
array<var> versions = [v2, Version(0, 9), v1];
versions.sort();                           // sort() orders through the same <
print(versions[0].major, ".", versions[0].minor);

class Grid {
    array<int> cells = [0, 0, 0];
    int& operator[](int i) { return cells[i]; }      // a reference return: g[i] is an lvalue
    int operator()(int a, int b) { return a + b; }
}
Grid g;
g[2] = 7;
g[2] += 1;
print(g.cells, " ", g(2, 3));
output
(8, 12) (-8, -12) true true
true false true true true true 1
0.9
[0, 0, 8] 5
OperatorRule
anyRuns when its left operand (a unary operator's only operand) is an instance of the class or a subclass and one of its overloads takes the right operand; it then wins over a builtin meaning, as in C++ (with string operator+(string s), v + "!" runs it). Otherwise the builtin meaning stands: with only operator+(Vec), v + "!" concatenates v's text, and -v without operator-() raises Unary minus requires numeric operand.
=operator= runs on every = into an existing object of the class whose overloads take the right-hand value: a variable, field, static, element or map value, typed or var. A declaration, a parameter and a return copy-construct instead (Classes).
op=a %= b is a = a % b through operator%; a script class declares no separate operator%=. A C++ class can bind one, which then runs in place.
!v, if (v)Both go through bool to_bool(). An instance of a class without one is truthy. Declaring operator! is a parse error: operator! cannot be declared: !v is !to_bool(v); declare bool to_bool().
<, <=, >, >=A declared operator wins for an operand it takes. Otherwise operator<=> answers: a < b is (a <=> b) < 0, the result (an int or a float) compared with zero.
reversed operandsWhen the left operand has no operator for it, the right one's <=> answers: 5 < v is 0 < (v <=> 5), and 5 <=> v is 0 <=> (v <=> 5).
== with <=> declaredoperator== if one takes the other operand, else (a <=> b) == 0, trying the left operand then the right. Two values of the class compare through it in place of the memberwise default.
== without <=>Against a value of the class: its operator==, else memberwise. Against another operand: an overload with a typed parameter that takes it, from either side (v == 3 and 3 == v both run operator==(int)). Against anything else == is false.
!=!(a == b), unless the class declares operator!=. x == null stays a null test.
[]Returning T& makes g[i] = v and g[i] op= v store through it. Returning by value makes g[i] a read: a store raises Cannot assign to a call result: 'operator[]' returns by value (a load error when the checker knows the class).
()Makes instances callable, at any arity and with overloads chosen as for any method, wherever the instance is held (Callable objects).
bound C++ classesCompare through the operators the binding registers. One that binds neither == nor <=> compares by identity.