The Type Ladder​

Every variable, field and parameter picks one of three rungs of strictness, and the rungs mix freely in one file:

DeclarationMeaning
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++ spellingJavaScript spelling
dynamic variablevar x, let x
64-bit floatdoublefloat (the name type_of reports)
the empty handlenullptrnull

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:

jaiscript
int x = 5;
print("before");
x = "oops";
output
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:

jaiscript
int x = 5;
var v = "oops";
try { x = v; } catch (e) { print("caught: ", e); }
print("x is still ", x);
output
caught: Cannot assign string to 'x' (declared int)
x is still 5

The 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 typeStarts as
int / float / bool / string0 / 0.0 / false / ""
array<T> / map<K, V>empty, of the declared type
a class Tdefault-constructed, as T()
an enum Eits first declared value
var, shared_ptr<T>, weak_ptr<T>, functionnull

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).

jaiscript
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);
output
0 0.0 [] 0 null 1.5 4.0 3

A 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 an int, auto d = Dir::north; is the enum, auto r = hit ? 1 : 0.5; is a float (C++'s common type).
  • An initializer whose static type is var or unknown (a var local, an array<var> element, a call returning var) makes that auto a var. The same holds for an auto field and an auto range-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, an int refuses null as any int does.
jaiscript
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));
output
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:

jaiscript
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));
output
string map

The 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.

SlotConverts implicitlyRefused, and the spelling that converts
intbool (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()
boolint, float, null, an object, by truthiness (0 is false)a string, an array or a map: !s.empty() or a comparison
stringutf8_string (its bytes), char, an object with to_string()an int, float, bool, null or container: to_string(x) or `${x}`
class CC 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 takesnull; 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), nulla 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 an int slot, and does not go on through another class's C(int) constructor into a C slot. A converting constructor or to_X() that throws shows its own error.
  • Conditions keep truthiness. if (s), while (xs), !x and && test any value: an empty string or container is false there. Only a declared bool slot 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() is 42 and "abc".to_int() is null, so give the miss a value: s.to_int() ?? 0, or s.to_int(0), which is an int for the checker.
jaiscript
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));
output
3.0 true -3 $1234 1234 250
true 0 42

cast<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:

FormResult
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.

jaiscript
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); }
output
2 97 a 5!
caught: Cannot cast 'int' to 'char'
6 0
caught: Cannot cast 'int' to 'Base'
  • The checker requires only that T names a type (unknown type in cast: 'Nope'). Whether the operand fits is decided when the cast runs: a mismatch raises the catchable Cannot cast 'X' to 'T', where X is the object's own class. An upcast always passes.
  • null passes 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:

jaiscript
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));
output
-3 3.5 3 (int)
3.5 (float)