Types That Fail Early​

The static checker reads a whole program before its first statement runs, in every host and in the standalone runner. This chapter has three parts: what it catches at load, what it leaves to run time and why, and the spellings that opt a name or a region out.

Two kinds of mistake​

  • A mistake between types the checker can see is a load error. Nothing runs, the engine is untouched, execute() throws jai::static_check_error, and the runner prints the listing and exits 1.
  • A mistake it cannot see raises a run-time error at the statement that makes it. try/catch sees it, the failed store writes nothing, and both backends raise the same text.

The checker reports only what it is certain of: where a type is unknown it stays silent and the run time decides. The Type Ladder shows the same store failing both ways.

Reading a diagnostic​

jaiscript
class Creature {
    int hp = 3;
    int poke() { int hp = 1; return hp; }   // shadows the field: a warning
}
Creature c = Creature();
print("before");
c.hp = "full";                              // a type error: this one stops the load
output
jaiscript: static check failed:
<script>:3:22: warning: local 'hp' shadows field 'hp' of class 'Creature' (field declared at 2:9)
        int poke() { int hp = 1; return hp; }   // shadows the field: a warning
                         ^
<script>:7:6: error: cannot assign 'string' to field 'hp' declared 'int' (declared at 2:9)
    c.hp = "full";                              // a type error: this one stops the load
         ^
  • Location. Line and column of the expression. <script> is a source string; a file, run directly or reached through import or include, is named by its resolved path.
  • Severity. An error stops the load. A warning never does: the runner prints warnings when a load fails or under --warnings, and a host reads them from engine->last_check_diagnostics().
  • Message, with (declared at L:C) pointing at the declaration it holds the code to, then the source line and a caret.

Every problem in the program comes out in one listing, sorted by location, up to 100 entries. An expression that draws an error is taken as its declared type afterwards, so one bad line gives one entry.

Caught at load​

Each row is an error whenever the types involved are declared or inferred (auto counts: it holds the type it inferred).

SiteWhat is checkedExample message
Namesevery identifier is declared where the use can see it; top-level code sees only what is declared above it (top-level order), a function body also what is declared below; a qualified name exists in its namespaceuse of undeclared identifier 'totl'; no member 'sqroot' in namespace 'math'
Storesa declaration, =, op=, a field, a static, a return and a pushed or literal element against the slot's type, by the conversion tablecannot assign 'string' to field 'hp' declared 'int'
Class valuesnever null, in a variable, parameter, field, static, return or element; a class cannot hold itself by value, directly, through another class or through a basecannot initialize 'h' declared 'Hero' with 'null': a 'Hero' is never null; declare 'shared_ptr<Hero>' (or var) for a slot that can be empty
Callsa function, method or constructor the checker knows, another file's and a namespace's (ns::f) included: argument count (defaults counted), each argument and the use of the result; with overloads, an error only when none fits'fib' expects 1 argument(s), got 2
Builtin and host callsa builtin method and a math:: function: each argument's shape (a number, text, an array, a map, a function, a vector) as the API reference states it. A function the host registered and a method it bound: argument count and each argument against the C++ signatureargument 1 of 'math::sqrt': cannot pass 'string' where a number is expected
Qualified namesns::limit = v, a read of ns::limit and ns::f(x) as the bare names are checked, at any depth and through usingcannot assign 'string' to 'cfg::limit' declared 'int'
Stores through a referencean int&, string&, array<T>& or other scalar, text, container or math reference stores what its type takescannot assign 'string' to 'r' declared 'int&'
Membersa member the receiver's declared class lacks, a member only a subclass declares included; a method an array, map or string lacks; any member of an int, float, bool or char but same_asno member 'range' in class 'Unit'; no member 'pusj' on 'array<int>' (did you mean 'push'?)

Every site the checker covers, with its message: checked at load, by site.

A callable slot takes only a callable (function f = 5; fails the load), a weak_ptr only a handle, and a namespace's bare name is no value ('math' is a namespace, not a value; name one of its members (math::name)). Three shapes stop earlier, at the parser:

  • a lambda that names an enclosing local its capture list does not admit (lambda capture list does not capture enclosing local 'hp'; name it or use [=] or [&]);
  • a named function inside a function body that uses the body's locals (nested function 'helper' cannot use enclosing local 'y': a named function captures nothing; ...);
  • a class declared inside a function body (a class cannot be declared inside a function body (a class is a program-scope type); ...).

Members on a base type and through references​

A slot's declared class decides what the checker lets you name, as in C++. A Unit slot that holds an Archer has no range; cast<Archer&>(u) is the same object seen as an Archer, and a var receiver leaves the question to run time. A reference names its type as a value does, so member reads, field stores and method arguments are checked alike through a T& or const T&, an auto& of a known class and a range-for variable over class elements. A var& checks nothing.

jaiscript
class Unit { int hp = 10; void hurt(int n) { hp -= n; } }
class Archer : Unit { int range = 6; }
Unit u = Archer();
print(cast<Archer&>(u).range);          // loads: the same object, as an Archer
print(u.range);
void drill(Unit& who) { who.hurt(); }
array<Unit> squad = [Unit()];
for (auto& s : squad) { s.hp = "full"; }
output
jaiscript: static check failed:
<script>:5:9: error: no member 'range' in class 'Unit' (declared at 1:7)
    print(u.range);
            ^
<script>:6:34: error: 'hurt' expects 1 argument(s), got 0 (declared at 1:32)
    void drill(Unit& who) { who.hurt(); }
                                     ^
<script>:8:30: error: cannot assign 'string' to field 'hp' declared 'int' (declared at 1:18)
    for (auto& s : squad) { s.hp = "full"; }
                                 ^

Operators​

Two declared types that no operator row relates are an error, with the text the run time would raise:

  • Comparisons. int, float and char compare as numbers, string and utf8_string as text, and a class with a comparison operator decides for itself, either way round. Anything else is cannot compare 'int' with 'string' using '==': the types have no comparison (it is always false); convert one operand.
  • null. It compares only with what can hold it: a handle, a function, a var. Against a typed number, string, enum, container or class value: cannot compare 'int' with null using '==': 'int' is never null (it is always false); ....
  • Arithmetic and bitwise. + - * / % take numbers (a char promotes to its int), + joins when a string stands on either side, & | ^ << >> take ints and chars; compounds and ++/-- follow the same rows, and ++ on a declared char is refused. Past the rows only an operator of the left operand's class answers: cannot apply '*' to 'string' and 'int' (it raises 'Invalid operands for * operator'); convert one operand.
  • Unary. -, + and ~ over a type with no row: Unary minus requires numeric operand.

Two class values, math values, bound C++ classes and anything var are judged at run time (Operators).

Every path returns​

A function, method or lambda declared to return a primitive, a string, an enum, a class value or a reference returns a value on every path.

  • A path ends when it returns, throws, or loops forever (while (true) with no break).
  • An if needs its else and a switch its default; a loop body that returns does not count, since the loop may run zero times.
  • void, var and untyped functions are exempt. A handle or container return that can fall off the end is a warning, and the call yields null.
jaiscript
int sign(int n) {
    if (n > 0) { return 1; }
    if (n < 0) { return -1; }
}                                            // n == 0 falls off the end
int fail(int k) { throw "no"; }             // a throw ends its path
output
jaiscript: static check failed:
<script>:1:5: error: 'sign' declared to return 'int' may reach the end of its body without returning
    int sign(int n) {
        ^

Assignment targets​

The left side of =, op=, ++ and -- names storage: a variable, a field, an element, a call returning T&, a conditional of two lvalues, cast<T&>(e). Anything else is an error that says what was written (cannot assign to a call result: 'f' returns by value, cannot assign to x++: it yields the old value). So are a read-only property (cannot assign to read-only property 'weekday'), a map key seen through a range-for pair (cannot mutate 'kv.first': map keys are read-only), a const, and a write into a copy nothing reads afterwards (lost writes).

Caught only at run time​

A program that runs clean draws no errors, so wherever the checker lacks a type it says nothing and the mistake waits for the statement to run. These are the places:

WhereWhy the checker is silent
A value that came through a var: a var variable, an untyped parameter, an array<var> element, an auto made from one, an auto with no initializer or a null oneits type is whatever it holds when the line runs; everything computed from it is unknown too
A store through an auto&, a var& or a reference to a classthe reference's members are checked; what it names may hold a subclass or define operator=, so the store converts when it runs
A call through a callable: a function slot, a var or auto holding a function, a callback parametera function slot carries no signature, so the argument count, the argument types and the result's type are unknown
A function or global that two files declare, a class-typed global of another file, a namespace variable of a class typemore than one declaration could answer, or the slot may hold a subclass: the checker confirms the name exists and the run decides
Host values: a bound property reached through a chain, an engine global, a bound method a script subclass declares again, a parameter the host takes as any value, a name declared extern function f;the host's signature does not decide them
A builtin parameter that takes several shapes (math::lerp's operands, index_of's value)the catalog states no single shape
A file reached through a computed path (import(expr), include(expr))it cannot be read at load; a unit that has one reports undeclared identifiers as warnings
Coroutinesof yielded and returned values only a float into an int is checked, and next()'s result is unknown. The handle is checked: it answers next(), advance(v), done() and result(), and any other member is a load error
Values, not types: an index out of range, a missing map key, integer overflow, a cast whose operand does not fit, "abc".to_int() returning nullno declaration states them

Also left to run time: a member with an ambiguous _get_x / _set_x accessor, an untyped extern, a class whose base the program cannot find, and anything passed to extend(...).

jaiscript
int twice(int n) { return n * 2; }
function cb = twice;                           // a function slot carries no signature
try { cb(1, 2); } catch (e) { print("caught: {}", e); }
try { string s = cb(1); } catch (e) { print("caught: {}", e); }

int hp = 3;
auto& r = hp;                                  // an auto& states no type
try { r = "full"; } catch (e) { print("caught: {}", e); }

namespace cfg { int limit = 3; }
var none = "none";                             // typed text would fail the load
try { cfg::limit = none; } catch (e) { print("caught: {}", e); }

int label(int n) {
    var v = "n=" + to_string(n);               // a var hides the string
    return v;                                  // so the conversion runs at the return
}
try { label(3); } catch (e) { print("caught: {}", e); }
output
caught: 'twice' expects 1 argument, got 2
caught: Cannot initialize 's' (declared string) with int
caught: Cannot assign string to 'r' (declared int&)
caught: Cannot assign string to 'limit' (declared int)
caught: Cannot return string from 'label' (declared -> int)

Load and run time apply one conversion table, so a mistake means the same thing wherever it is found, and the run-time text names the slot it failed at: Cannot initialize 'x' (declared int) with string, Cannot assign string to field 'hp' of Stats (declared int), Cannot pass string to parameter 'n' of 'twice' (declared int), Cannot store string in array<int>. A return converts at its return statement, as in C++, so a try around that statement catches it inside the function.

The opt-outs​

SpellingWhat it tells the checker
varthis slot is dynamic: nothing stored in it or computed from it is checked at load
cast<T>(e), cast<T&>(e), cast<shared_ptr<T>>(e)treat e as a T: a copy, a reference or a handle. The operand is judged when the cast runs (The Type Ladder)
externthis name exists though the program does not define it
inlineseveral files declare this one variable
[[warn_mode]]report this region's errors as warnings

Prefer the narrowest: cast for one expression, extern for one name, [[warn_mode]] for a region that is dynamic on purpose.

extern: a name defined elsewhere​

A global the host binds after load or a function another program part registers is undeclared to the checker until an extern names it, in three strengths:

jaiscript
extern function net_relay;          // a callable: calls are unchecked
extern int tick_rate;               // typed: every use is checked as an int
extern config;                      // untyped: a var
extern function physics::raycast;   // a namespace member, at any depth
function on_packet(var p) { return net_relay(p, tick_rate, config); }
try { on_packet(1); } catch (e) { print(e); }
try { print(tick_rate); } catch (e) { print(e); }
try { physics::raycast(0, 0); } catch (e) { print(e); }
output
'net_relay' is declared extern but never defined
'tick_rate' is declared extern but never defined (an 'inline' declaration creates it)
'physics::raycast' is declared extern but never defined
  • The typed form is checked as a declared variable: tick_rate = "fast"; is cannot assign 'string' to 'tick_rate' declared 'int'.
  • An extern is visible to every file of the program, and inside a namespace it names that namespace's member.
  • At run time it declares nothing. The name resolves when it is used, so the definition (a declaration in any file, a host add_global, a host function) may arrive later; a use before then raises the catchable errors above.
  • A definition in the same program wins, so a shared file can extern a module that only some programs import (Modules).

inline: one variable every file shares​

As in C++17, the first inline declaration to run creates the variable, from its initializer or its type's default, and every later one, in any file or a re-run of the same file, adopts it without running an initializer. It also adopts a global the host bound. It stands at program scope or in a namespace.

jaiscript
inline int frags;                   // as hud.jai declares it
inline int frags = 10;              // as netplay.jai declares it: adopts, runs no initializer
frags += 1;
print("frags {}", frags);
output
frags 1

Inline and plain declarations of one name do not mix across files, and the checker reports the mix at the plain one: 'frags' is already defined inline; declare it 'inline int frags;' to share it, or 'frags' is already defined (not inline) in stats.jai; declare every definition 'inline' to share it. Editing a declaration and reloading its file converts the variable in place: to inline it keeps its value, to plain it follows the ordinary re-run rule.

[[warn_mode]] and [[strict_mode]]​

In a warn region every error is reported as a warning and an undeclared identifier is accepted. [[strict_mode]] restores errors inside one. The innermost attribute wins, and outside every region a file is strict. An attribute covers one of three things:

  • A statement, when it stands before one. A function or lambda body inside that statement keeps the enclosing mode.
  • A block, as its first statement (a function, method or lambda body, a loop body, a bare { }), nested bodies included.
  • A file, as its first declaration.
jaiscript
[[warn_mode]];                                         // the whole file
function on_debug_key() { return debug_overlay(1); }   // a host name the checker cannot see
[[strict_mode]] { int n = 1; n = "one"; }              // strict again inside this block
output
jaiscript: static check failed:
<script>:2:34: warning: use of undeclared identifier 'debug_overlay'
    function on_debug_key() { return debug_overlay(1); }   // a host name the checker cannot see
                                     ^
<script>:3:32: error: cannot assign 'string' to 'n' declared 'int' (declared at 3:23)
    [[strict_mode]] { int n = 1; n = "one"; }              // strict again inside this block
                                   ^

The attributes change only the verdict at load. The code runs the same, so a warn region's mistakes raise when they run (debug_overlay raises Undefined variable if the host has not registered it). A file's mode does not reach the files it imports. warn_mode and strict_mode are the only attribute names; any other is a parse error.

Across files, hosts and the editor​

A root file's literal imports and includes are gathered before anything runs, the top-level names and class shapes of every file are pooled, and every file is checked against the pool. A function may use a global another file declares whatever the import order, and a mistake in any file fails the root's load under that file's path (Modules).

The embedded engine and the runner check alike. A host gets the listing without running anything from engine->check(source); the exception type, jaibite behavior and hot-reload re-checks are in Static Checking. The language server jaiscript_lsp runs the same checker as you type and underlines each diagnostic at its file and line (Editor Support).