The Type Ladder
Every variable, field and parameter picks one of three rungs of strictness, and the rungs mix freely in one file:
| Declaration | Meaning |
|---|---|
int x = 5; (any spelled type) | Enforced. The slot holds that type for good. A store converts when the conversion table allows it and is an error when not. |
auto x = 5; | Inferred, then enforced. The slot takes the initializer's static type and from then on behaves as if that type were spelled. |
var x = 5; | Dynamic. Any value at any time. An untyped parameter is a var. |
Three of the names have a C++ spelling and a JavaScript spelling that mean the same thing:
| C++ spelling | JavaScript spelling | |
|---|---|---|
| dynamic variable | var x, let x | |
| 64-bit float | double | float (the name type_of reports) |
| the empty handle | nullptr | null |
let stands wherever var may (let x = 5;, for (let i = 0; ...), let& r = x;). Every declaration, under any keyword, is scoped to its block, and a C-style for variable that a lambda captures by reference is a fresh variable on each iteration (Functions & Lambdas).
When a mismatch is caught
The checker reads the whole program before its first statement runs. A mismatch between two types it can see is a load error, and nothing executes:
int x = 5;
print("before");
x = "oops";jaiscript: static check failed:
<script>:3:3: error: cannot assign 'string' to 'x' declared 'int' (declared at 1:5)
x = "oops";
^A var has no static type, so a mismatch that arrives through one is found when the store runs. It is an ordinary error that catch sees, and the slot keeps its old value:
int x = 5;
var v = "oops";
try { x = v; } catch (e) { print("caught: ", e); }
print("x is still ", x);caught: Cannot assign string to 'x' (declared int)
x is still 5The same split holds at every typed slot: a variable, a parameter, a return, a field, a container element. Types That Fail Early lists what the checker sees, what waits for run time, and the opt-outs ([[warn_mode]], extern).
Declarations without an initializer
A typed declaration always holds a value of its type. Without an initializer it is value-initialized, as in C++, and nothing is ever left uninitialized:
| Declared type | Starts as |
|---|---|
int / float / bool / string | 0 / 0.0 / false / "" |
array<T> / map<K, V> | empty, of the declared type |
a class T | default-constructed, as T() |
an enum E | its first declared value |
var, shared_ptr<T>, weak_ptr<T>, function | null |
T x(args); and T x{args}; call a constructor, T x(); and T x{}; default-construct, and a primitive takes one value or its zero. A class that declares no constructor takes its fields in declaration order (Point p{1, 2};, in Classes).
class Vec2 { float x = 1.5; float y = 2.5; Vec2() {} Vec2(float a, float b) { x = a; y = b; } }
int hits; float speed; string name; array<int> scores; var anything; Vec2 home;
Vec2 a(3.0, 4.0);
int lives{3};
print("{} {} [{}] {} {} {} {} {}", hits, speed, name, scores.size(), type_of(anything), home.x, a.y, lives);0 0.0 [] 0 null 1.5 4.0 3A class value is never null. Hero h = null; is a load error that names shared_ptr<Hero> and var, the slots that can be empty, and a class cannot hold itself by value: class Node { Node next; } fails the load, and the link is a shared_ptr<Node>.
auto takes the static type
auto locks to the type the checker sees for the initializer, never to whatever the value happens to hold when the line runs:
- An initializer of a known type locks to it:
auto n = 5;is anint,auto d = Dir::north;is the enum,auto r = hit ? 1 : 0.5;is afloat(C++'s common type). - An initializer whose static type is
varor unknown (avarlocal, anarray<var>element, a call returningvar) makes thatautoavar. The same holds for anautofield and anautorange-for variable. auto x;with no initializer locks at its first assignment, when the program runs, so a mistake on it is a run-time error. Once locked, anintrefusesnullas anyintdoes.
auto y = 1.5; // float
y = 2; // an int widens
var row = [1, 2.5, "three"];
auto a = row[0]; // an array<var> element: a is a var
a = "now a string";
auto late;
late = 5; // locks to int here
try { late = "s"; } catch (e) { print("caught: ", e); }
print("{} ({}), {} ({}), {} ({})", y, type_of(y), a, type_of(a), late, type_of(late));caught: Cannot assign string to 'late' (declared int)
2.0 (float), now a string (string), 5 (int)auto y = x; copies what C++ auto copies: a value (a class instance, an array, a map) becomes a new value, a handle (new T(), a shared_ptr<T>) becomes a second handle to the same object, and a reference result is copied because auto drops the reference (auto& keeps it). T y = handle; copies the object the handle points at. Value Semantics shows each case.
var is dynamic
A var takes any value at any time, and a var container may mix element types. A typed container, an auto locked to one included, holds one element type:
var y = 5;
y = "now a string";
let mixed = [1, "two", 3.14, true];
mixed = {"even": "a map now"};
print("{} {}", type_of(y), type_of(mixed));string mapThe one conversion table
Every typed slot runs the same table: a declaration, =, a by-value parameter, a return, a field, a static. A refused store is a load error where the checker sees both types and a catchable run-time error where one arrives through a var.
| Slot | Converts implicitly | Refused, and the spelling that converts |
|---|---|---|
int | bool (true is 1), an enum value, an object with to_int() | a float: cast<int>(x) truncates toward zero, math::ifloor(x) rounds down. A char: cast<int>(c). A string: s.to_int() |
bool | int, float, null, an object, by truthiness (0 is false) | a string, an array or a map: !s.empty() or a comparison |
string | utf8_string (its bytes), char, an object with to_string() | an int, float, bool, null or container: to_string(x) or `${x}` |
class C | C or a subclass, as a copy that keeps its own class and overrides; a shared_ptr to one (copies the pointee); a value that exactly one one-argument constructor C(src) takes; on =, a value an operator= of C takes | null; a parent, sibling or unrelated class |
shared_ptr<C> | a handle or an instance of C or a subclass (an instance becomes a new shared object), null | a shared_ptr<Base>: cast<shared_ptr<C>>(b) |
Every slot (float, enums, char, containers, weak_ptr, T&, function, auto): the conversion table.
- One step. Conversions never chain: a class with
to_int()enters anintslot, and does not go on through another class'sC(int)constructor into aCslot. A converting constructor orto_X()that throws shows its own error. - Conditions keep truthiness.
if (s),while (xs),!xand&&test any value: an empty string or container is false there. Only a declaredboolslot refuses text and containers. - Typed container elements are stricter. An element takes its own type, an int into a float element, bool and int both ways, and char and string both ways, with no truthiness and no
to_X()(Containers & Strings). - Text never parses itself.
"42".to_int()is42and"abc".to_int()isnull, so give the miss a value:s.to_int() ?? 0, ors.to_int(0), which is anintfor the checker.
class Money {
int cents = 0;
Money(int c) { cents = c; }
string to_string() { return format("${}", cents); }
int to_int() { return cents; }
}
float f = 3; // int widens
bool b = 5; // a number is true unless it is zero
int i = cast<int>(-3.99); // a float enters an int only by name
string s = Money(1234); // through to_string()
int n = Money(1234); // through to_int()
Money m = 250; // through the constructor Money(int)
print("{} {} {} {} {} {}", f, b, i, s, n, m.cents);
print("{} {} {}", "abc".to_int() == null, "abc".to_int() ?? 0, "42".to_int(0));3.0 true -3 $1234 1234 250
true 0 42cast<T>(e): the explicit boundary
cast is a keyword. cast<T>(e) yields what T tmp = e; would hold, plus the conversions the table makes you name:
| Form | Result |
|---|---|
cast<int>(x) | a float truncated toward zero; a char's code, 0..255 (cast<float>(c) too) |
cast<char>(e) | an int in 0..255 or a one-character string; Cannot cast 'int' to 'char' out of range |
cast<E>(n) | the enum value that equals n; Cannot cast 3 to 'E': not one of its values otherwise |
cast<string>(e) | any value as text, as to_string(e) |
cast<T>(obj) | a copy of an object that is a T or a subclass; the copy keeps its own class |
cast<T&>(obj) | a reference to the same object: how a base-typed slot reaches a member only the subclass declares |
cast<shared_ptr<T>>(obj) | a handle sharing the same object |
Every form and its error texts: cast.
class Base { int a = 1; }
class Derived : Base { int t = 9; }
print("{} {} {} {}", cast<int>(2.7), cast<int>('a'), cast<char>(97), cast<string>(5) + "!");
try { var bad = cast<char>(256); } catch (e) { print("caught: ", e); }
Base slot = Derived(); // a Base slot holding a whole Derived
cast<Derived&>(slot).t = 5; // the same object, seen as a Derived
shared_ptr<Derived> h = cast<shared_ptr<Derived>>(slot);
h.t += 1;
Derived copy = cast<Derived>(slot); // a separate object
copy.t = 0;
print("{} {}", cast<Derived&>(slot).t, copy.t);
try { var oops = cast<Base>(5); } catch (e) { print("caught: ", e); }2 97 a 5!
caught: Cannot cast 'int' to 'char'
6 0
caught: Cannot cast 'int' to 'Base'- The checker requires only that
Tnames a type (unknown type in cast: 'Nope'). Whether the operand fits is decided when the cast runs: a mismatch raises the catchableCannot cast 'X' to 'T', whereXis the object's own class. An upcast always passes. nullpasses the handle form and fails the value and reference forms (Cannot cast 'null' to 'T&').- The value form on a handle copies the object. The checker warns (
cast<Derived>(h) copies the instance - use cast<Derived&>(h) to alias it or cast<shared_ptr<Derived>>(h) to share it); a warning never stops a load, and the runner prints it under--warnings.
Compound assignment
a op= b is a = a op b stored under the target's rung. A typed target (a variable, a field, a typed element, a reference to one) keeps its type, so i += 0.5 on an int is a float into an int: a load error. A var target takes whatever the operation produced:
int i = -7;
i /= 2; // integer division truncates toward zero
float f = 7.0;
f /= 2;
var v = 7;
v /= 2; // int / int: still an int
print("{} {} {} ({})", i, f, v, type_of(v));
v += 0.5; // a float result changes what the var holds
print("{} ({})", v, type_of(v));-3 3.5 3 (int)
3.5 (float)