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 intis anint(7 / 2is3); a float operand makes the result a float.- Integer overflow and integer division by zero raise catchable errors. Float arithmetic is IEEE: it gives
infandnan. +joins text with text. A number beside a string is an error, never a conversion.- A
charis 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
| Category | Operators | Notes |
|---|---|---|
| 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 |
| Cast | cast<T>(e) cast<T&>(e) cast<shared_ptr<T>>(e) | the explicit conversion: a copy, a reference, a handle (Type Ladder) |
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}`);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 nanInteger / 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:
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);caught: Integer overflow in '+', big is still 9223372036854775807
fnv1a = -1792535898324117685When 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 ??=
| Form | Meaning |
|---|---|
x?.member | null 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 ?? b | a unless it is null, else b; b runs only then. 0, false and "" are values, not null. |
a ??= b | assigns 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.
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);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.
| Step | Rule |
|---|---|
| A by-value argument | takes 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 argument | binds 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= e | e, then the receiver, then the member's read (a getter included), the arithmetic and the store. |
a[j] = e, a[j] op= e | e, then a, then j, then the element. An error in e leaves the target untouched. |
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);callee
argument a
argument b
sum = 3
by value: 21
by reference: 100
[1, 0]
value target -> 6Functions 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:
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); }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 characterBeside 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' + sjoin, ands += 'c'appends.- A
charvariable, parameter, field, element or return takes a string of exactly one character, and astringone takes a char (the conversion table). Any other length fails:char c = "ab";is the load errorcannot initialize 'c' declared 'char' with 'string': a char takes a string of exactly one character, and a run-time value raisesCannot convert 'string' to 'char': a char takes a string of exactly one character. - A
switchover 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' + 1is theint98.char c = 65;andint n = 'A';are type errors.- Compound assignment and
++/--promote too: avarholding'b'holds the int97after-= 1. A variable declaredcharrefuses 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, andcast<char>(n)takes an int in 0..255 or a one-character string (elseCannot 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
charis not promoted on the way in.
Equality and ordering
| Operands | == and != | <, <=, >, >=, <=> |
|---|---|---|
| numbers, text, bool | by value; 1 == 1.0 | by value; false before true |
| floats | IEEE: 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. |
| arrays | element by element | lexicographic: [1, 5] < [2] |
| maps | entry 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 values | field 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 values | identity, as std::shared_ptr compares: two new objects with equal fields are not equal | identity |
| references | their referents | their 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.
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));values: true false
arrays: true false true
maps: true false
handles: true false
same: false true trueOperator 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).
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));(8, 12) (-8, -12) true true
true false true true true true 1
0.9
[0, 0, 8] 5| Operator | Rule |
|---|---|
| any | Runs 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 operands | When 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 <=> declared | operator== 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++ classes | Compare through the operators the binding registers. One that binds neither == nor <=> compares by identity. |