Types That Fail Early​

You have met the checker on every page of this guide, usually as a sentence that began "under strict". This chapter is the whole story in one place: what it catches before a script runs, what it deliberately leaves to the runtime, how to read what it prints, and the spellings that let you talk back to it: cast<T>, extern, inline, [[warn_mode]] and [[strict_mode]].

The one-sentence version: the checker is always on, and strict. A typed mistake it can see fails the load, before a single statement runs. A mistake it cannot see raises at run time, where try/catch sees it. Nothing is opt-in; var is the opt-out.

Two kinds of mistake​

Start with the smallest possible program that gets a type wrong:

jaiscript
int x = 5;
print("before");
x = "oops";
print("after");
output
jaiscript: static check failed:
<script>:3:3: error: cannot assign 'string' to 'x' declared 'int' (declared at 1:5)
    x = "oops";
      ^

Nothing ran. before never printed. The checker saw a string literal flowing into a slot declared int, and under strict that is a load error: the load fails, the whole listing is the error text, and the engine is untouched. On the C++ side execute() throws jai::static_check_error; the standalone runner prints the listing and exits 1.

Now the same mistake, hidden behind var:

jaiscript
int x = 5;
var v = "oops";                    // var: the checker has no idea what this holds
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

This one loaded and ran. var holds anything, so the checker has no idea what v is and says nothing. The store still fails, at run time, through the same conversion table the checker was reasoning about, with a catchable error that names the slot. x keeps its old value.

That is the whole split. What the checker can see (literals, typed locals and parameters, typed returns, typed fields, calls to functions defined in the same program, members of a class it knows) fails the load. What it cannot see (anything var-sourced, the elements of an untyped container, callables held in variables, values that arrive from the host) raises at run time, catchable, and byte-identical on both backends. Every "raises" in the earlier chapters is the second kind.

Reading a diagnostic​

A listing is compiler-shaped, one entry per problem, and every entry has the same three lines:

output
<script>:3:4: error: cannot assign 'string' to 'x' declared 'int' (declared at 1:5)
    x = "oops";
       ^
  • <script>:3:4, the line and column of the offending expression. <script> is a source string handed to execute(); a file, whether run directly or reached through import or include, is named by its resolved path instead (Modules). The listings in this chapter use <script>.
  • error or warning, the severity. Errors gate the load; warnings never gate.
  • The message, in the checker's voice: lower-case, names quoted, types bare, and (declared at L:C) pointing at the declaration it is holding you to.
  • The source line and a caret under the column.

Several problems come out together, sorted by location and deduplicated, so you fix a file in one pass rather than one error per attempt. An erroring expression assumes its declared type from then on, so one bad line produces one diagnostic, not a cascade. The listing caps at 100. Here is one with both severities in it:

jaiscript
class Creature {
    int hp = 3;
    int poke() { int hp = 1; return hp; }   // shadows the field: a warning
}
Creature c = Creature();
c.hp = "full";                              // a type error: this one gates 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>:6:6: error: cannot assign 'string' to field 'hp' declared 'int' (declared at 2:9)
    c.hp = "full";                              // a type error: this one gates the load
         ^

The shadowing warning would not have stopped the load on its own; the field store did. (Shadowing warnings are covered in Classes; the runner prints warnings when a load fails or when you pass --warnings, and a host reads them from engine->last_check_diagnostics().)

What the checker sees​

The rules, in the order you are likely to trip over them. Each is a load error under strict.

SiteCheckedExample message
Declarations and =the value against the declared type, through the one conversion table; compound assigns as x = T(x op rhs)cannot assign 'string' to 'x' declared 'int'
Field and static storesagainst the field's declared type, from outside (c.hp = v) and inside (bare hp = v)cannot assign 'string' to field 'hp' declared 'int'
Calls to same-program functions, methods and constructorsarity (defaults counted) and each argument's assignability; with overloads, an error only when none fits'fib' expects 1 argument(s), got 2
Returnsthe value against the declared return type; a value from a void function; a function declared to return a primitive, a string or a reference that can reach the end of its body without a returncannot return 'string' from 'label' declared to return 'int', 'sign' declared to return 'int' may reach the end of its body without returning
Reference bindingsa T& declaration or parameter binds only a T lvalue; a literal or temporary reaches a T& parameter only when the function never writes through itargument 1 of 'bump': cannot bind 'float' to 'n' declared 'int&', 'bump' writes through parameter 'n' (line 1); pass a variable
Assignment targetsthe left side of =, op=, ++ and -- must name storage; a read-only property and a map key seen through a range-for pair are not assignablecannot assign to a call result: 'f' returns by value, cannot assign to read-only property 'weekday', cannot mutate 'kv.first': map keys are read-only
Members on a known classreads and writes of a member the class does not declare, including a member only a subtype declares, through a value, a handle or a reference (T&, const T&, an auto& of a known class, a range-for variable over class elements) and the fields and elements reached through one; through a reference, field stores, method arity and arguments are checked as on a valueno member 'range' in class 'Unit'
Identifiersevery name must be declared where the use can see it: an enclosing scope, the class (in a method, also a class derived from it), the file's top level (a function body may name a later declaration, while top-level code sees only what is declared above it: top-level order), the program, extern, or the host's registrations; another function's locals and another class's members are undeclareduse of undeclared identifier 'totl'
Methods and staticsa method read without a call; a static reached with a dot'heal' is a method of P; call it with p.heal() or bind it with reflect::bind(p, "heal"), 'P' is a class; use P::n
Comparisons== != < <= > >= <=> between two declared types with no comparison: no conversion relates them (int, float and char compare as numbers; string and utf8_string as text) and neither side's class declares a comparison operator, either way round; null against a typed slot that never holds itcannot compare 'int' with 'string' using '==': the types have no comparison (it is always false); convert one operand
Enum valuesmembershipno value 'zzz' in enum 'Direction'
Host (C++) functionsarity across overloads, and an argument only when the mismatch is certain (a string into a numeric parameter); a bound static method's arity'to_char' expects 1 argument(s), got 2, no overload of 'to_char' accepts these argument types, 'Gate::twice' expects 1 argument(s), got 0
CastsT must name a type; the operand is never checked against it (that is the runtime's verdict)unknown type in cast: 'Nope'

Two of these deserve a closer look, because the fix is not obvious the first time. A call with the wrong shape:

jaiscript
function fib(int n) -> int { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
print("{}", fib(10, 2));
output
jaiscript: static check failed:
<script>:2:22: error: 'fib' expects 1 argument(s), got 2 (declared at 1:10)
    print("{}", fib(10, 2));
                         ^

The checker knows fib's signature because fib is defined in the same program. Put the function in a variable and the knowledge is gone. A callable held in a variable is unchecked, and the same two mistakes become run-time errors:

jaiscript
function fib(int n) -> int { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
var f = fib;                                   // a callable in a variable: unchecked
try { f(10, 2); } catch (e) { print("caught: {}", e); }
try { f("ten"); } catch (e) { print("caught: {}", e); }
output
caught: 'fib' expects 1 argument, got 2
caught: Cannot pass string to parameter 'n' of 'fib' (declared int)

And a member that only a subtype declares, read through a base-typed receiver:

jaiscript
class Unit { int hp = 10; }
class Archer : Unit { int range = 6; }
Unit u = Archer();
print("{}", u.range);
output
jaiscript: static check failed:
<script>:4:15: error: no member 'range' in class 'Unit' (declared at 1:7)
    print("{}", u.range);
                  ^

This is C++'s rule too: u is declared Unit, and a Unit has no range, whatever it holds tonight. There are two fixes. cast<Archer&>(u) is a reference to the same object, seen as an Archer, and reads and writes go through it; a var receiver drops the question to run time:

jaiscript
class Unit { int hp = 10; }
class Archer : Unit { int range = 6; }
Unit u = Archer();
print("range = {}", cast<Archer&>(u).range);   // a reference to the same object, as an Archer
cast<Archer&>(u).range = 9;                    // writes go through
print("range = {}", cast<Archer&>(u).range);
var loose = u;                                 // or drop to var: checked at run time instead
print("range = {}", loose.range);
output
range = 6
range = 9
range = 9

Through references​

A reference names its type as surely as a value does, so every member check above runs through one: a T& or const T& parameter or local, an auto& bound to a known class, a range-for variable over class elements, and the fields and elements reached through any of them. The texts are the ones a value receiver draws:

jaiscript
class Wheel { int spokes = 32; void true_up(int tension) {} }
void service(Wheel& w) {
    w.spokes = "many";
    w.true_up();
}
array<Wheel> rack = [Wheel()];
for (auto& w : rack) { w.spoke = 36; }
output
jaiscript: static check failed:
<script>:3:14: error: cannot assign 'string' to field 'spokes' declared 'int' (declared at 1:19)
        w.spokes = "many";
                 ^
<script>:4:15: error: 'true_up' expects 1 argument(s), got 0 (declared at 1:37)
        w.true_up();
                  ^
<script>:7:26: error: no member 'spoke' in class 'Wheel' (declared at 1:7)
    for (auto& w : rack) { w.spoke = 36; }
                             ^

A base-class reference follows the base-typed slot's rule: a member only a subtype declares is an error through Unit& as through Unit, and cast<Archer&> is the fix. A var& checks nothing.

Comparisons​

Two declared types that no comparison relates are a mistake the checker can see: == between them is always false, != always true, and the orderings raise Invalid operands at run time. int, float and char compare as numbers, string and utf8_string as text, and a class with a comparison operator, either way round, decides for itself. Two class values and anything var are left to the run time (Operators). null compares the way nullptr does in C++: with what can hold it (a class value, a handle, a function, a var, a call whose class or container result can fall off its body), and never with a typed int, float, char, bool, string, array, map or math value, which a store of null raises or converts on (cannot compare 'int' with null using '==': 'int' is never null (it is always false); ...). An auto holds the type it inferred, so it counts as declared:

jaiscript
int lives = 3;
string typed = "3";
if (lives == typed) { print("match"); }        // always false
if (lives == typed.to_int()) { print("match"); }
output
jaiscript: static check failed:
<script>:3:11: error: cannot compare 'int' with 'string' using '==': the types have no comparison (it is always false); convert one operand
    if (lives == typed) { print("match"); }        // always false
              ^

Every path returns​

A function, method or lambda declared to return a primitive (int, float, bool, char), a string or a reference must return a value on every path, as a C++ compiler warns and this checker requires. A path ends when it returns, throws, or never ends (while (true) with no break); an if needs its else, and a switch needs its default. A loop body that returns does not count, since the loop may run zero times. void, var and untyped functions are exempt, and a class, handle or container return that can fall off the end draws a warning instead ('pick' declared to return 'array<int>' may reach the end of its body without returning (returns null)). The check is a load verdict only: in a [[warn_mode]] region a call that falls off the end yields null.

jaiscript
int sign(int n) {
    if (n > 0) { return 1; }
    if (n < 0) { return -1; }
}                                            // n == 0 falls off the end
string name(int k) {
    switch (k) { case 1: return "one"; default: return "many"; }   // every path returns
}
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​

Every C++ lvalue is assignable: a variable, a field, a static, an element, a map value, a call that returns T&, a conditional whose arms are both lvalues, (a = b), cast<T&>(e). Anything else names no storage, and the checker says what it is instead, with the text the runtime raises for the same mistake (lower-cased):

jaiscript
int f() { return 1; }
int x = 1;
bool c = true;
f() = 3;                      // a call that returns by value
cast<float>(x) = 3.0;         // a value cast is a temporary
x++ = 4;                      // postfix yields the old value
(c ? x : 5) = 6;              // one arm names no storage
output
jaiscript: static check failed:
<script>:4:5: error: cannot assign to a call result: 'f' returns by value
    f() = 3;                      // a call that returns by value
        ^
<script>:5:16: error: cannot assign to the result of cast<float>(x): it is a temporary value (cast<float&>(x) names the variable)
    cast<float>(x) = 3.0;         // a value cast is a temporary
                   ^
<script>:6:5: error: cannot assign to x++: it yields the old value
    x++ = 4;                      // postfix yields the old value
        ^
<script>:7:13: error: cannot assign to a conditional: its arm '5' names no storage
    (c ? x : 5) = 6;              // one arm names no storage
                ^

A lambda that names an enclosing local its capture list does not admit is rejected even earlier, by the parser: Lambda capture list does not capture enclosing local 'hp'; name it or use [=] or [&] (Lambdas and captures). Two more shapes stop at the parser: a named function declared inside a function body that uses the body's locals (Nested function 'helper' cannot use enclosing local 'y': a named function captures nothing; ...), and a class declared inside a function, method or lambda body (a class cannot be declared inside a function body (a class is a program-scope type); declare it at program scope or import its file).

The promise: a clean program is never rejected​

A checker that guesses is worse than no checker, because you stop reading it. So this one is held to a contract: a program that runs clean and does not genuinely violate a declared type produces zero errors. Wherever it is not certain, it is silent, and the runtime's own checks take over. A regression gate runs two hundred generated programs through both paths and fails if anything that executes cleanly draws an error.

The other side of that promise is a list of places where a mistake waits for run time rather than failing the load. Knowing it saves you from expecting a diagnostic that will never come:

  • var, and everything computed from it. auto without an initializer, or with a null one (the type locks at the first real store, which the checker cannot see).
  • Stores into a reference itself (r = v through a T& local, an auto& loop variable or a by-reference parameter) and the values read out of one: a reference's type checks its members (above), not what is stored into it or read from it. A reference declaration checks its known initializer type; compatible handle declarations and known handle stores are checked too. A var&, an auto& over a value of unknown type and a parallel_for element parameter check no members.
  • Elements of var receivers and container types whose elements remain unknown, plus builtin container/string method results. Declared container reads, literal elements, typed pushes and stores are checked (Containers). Bounds, missing map keys and computed numeric range failures remain runtime checks.
  • Calls through callable variables and function-typed parameters; storing a known non-callable in a function variable or field is a load error. Host functions beyond arity and the certain primitive mismatches; C++-bound methods and properties reached through a script chain; engine globals (typed off the value, not a declaration).
  • Files reached through a dynamic-path import(expr) / include(expr) (undeclared identifiers drop to warnings there), untyped extern names, a base class the program cannot find, and namespace paths at every depth.
  • Accessor-bearing members whose _get_x / _set_x declaration is ambiguous (Classes).
  • Coroutines: yield and return types are not checked, and what next() returns is unknown. Calling one yields a handle typed as coroutine_handle, which answers next(), advance(), done() and result() (typed as the declared type); advance(v) needs a variable (advance() writes the yielded value through its argument; pass a variable, and a const fails as for a writing reference parameter) and, when both types are declared, one the yielded type can be assigned to (advance() cannot store the yielded 'string' into 'int'); any other member is a load error (no member 'resume' on 'coroutine_handle' (did you mean 'next'?)).
  • Mixed-branch ternaries, this and super used as values, classes mid-hot-reload (receivers that still carry the old shape), and anything passed to extend(...).

Everything else (a concrete int, float, bool, char, string, or a class the checker knows, on both sides of a store) is certain, and certain mistakes fail the load.

One conversion table, at load and at run time​

The checker applies the scalar conversion rules in The Type Ladder when it knows the source and destination types: declarations, = and compound assignments, by-value parameters, returns, fields and static fields. Known scalar conversions follow that table; unknown values and user conversion bodies can still fail at run time. Container elements use their boundary rules. The checker additionally enforces the declared weak-handle, reference and callable constraints even where an unchecked store would succeed. Known container types follow the runtime element boundary; var keeps the contents check at run time. A chained conversion (obj.to_int() and then C(int)) is never implicit in either place.

When the mistake does reach run time, the error names the slot it failed at. Two shapes worth recognizing: a return that fails at the return statement, and an argument that fails at the parameter:

jaiscript
function label(int n) -> int {
    var v = "n=" + to_string(n);               // var: the checker cannot see this string
    return v;                                  // so the conversion runs at the return statement
}
try { label(3); } catch (e) { print("caught: {}", e); }
function twice(int n) -> int { return n * 2; }
var f = twice;
try { f("four"); } catch (e) { print("caught: {}", e); }
string s = 5;                                  // string accepts anything, via to_string
int truncated = 2.9;                           // float to int truncates
print("{} {}", s + "!", truncated);
output
caught: Cannot return string from 'label' (declared -> int)
caught: Cannot pass string to parameter 'n' of 'twice' (declared int)
5! 2

The return conversion is part of the return statement, as in C++. A try around the return inside label would catch it there; uncaught, label is the top frame of the stack trace and the error surfaces in the caller, which is what happened above. The other texts in the family follow the same pattern: Cannot initialize 'x' (declared int) with string for a declaration, Cannot assign string to field 'hp' of Stats (declared int) and ... to static 's' of C ... for fields, Cannot store string in array<int> at the element boundary.

cast<T>(e): the explicit boundary​

cast is a keyword, and it is how you say "I know" to the type system. It has three forms, and the difference between them is the difference between a copy, an alias and a share, the same three relationships Value Semantics is about.

  • cast<T>(e) yields what T tmp = e; would hold. For primitives that is the conversion table (cast<int>(2.7) is 2, cast<string>(5) is "5"). For a class it is a copy, typed T, when e's class is T or derives from it.
  • cast<T&>(e) is a reference to the same object, seen as a T. Member reads and writes go through. This is the strict-mode fix for a subtype member on a base-typed receiver.
  • cast<shared_ptr<T>>(e) is a handle sharing the instance.

Char converts only here: the assignment table has no char row, so char c = 65 is a load error and cast<char>(65) is the spelling. cast<int>(c) and cast<float>(c) are the code point (0–255), cast<string>(c) the one-character string, and cast<char>(e) takes an int in range or a one-character string.

jaiscript
print("{} {} {}", cast<int>(2.7), cast<string>(5) + "!", cast<float>(3));
print("{} {} {}", cast<int>('a'), cast<char>(97), cast<string>('a') + "b");
class Unit { int hp = 10; }
class Archer : Unit { int range = 6; }
class Rock { int mass = 1; }
var thing = Archer();
Archer copy = cast<Archer>(thing);            // value form: a copy, typed Archer
copy.range = 1;
print("copy {} original {}", copy.range, thing.range);
cast<Archer&>(thing).range = 2;               // reference form: writes reach the original
print("original now {}", thing.range);
shared_ptr<Unit> shared = cast<shared_ptr<Unit>>(thing);   // handle form: shares the instance
shared.hp = 99;
print("through the handle: {}", thing.hp);
try { cast<Rock>(thing); } catch (e) { print("caught: {}", e); }
try { cast<Archer&>(null); } catch (e) { print("caught: {}", e); }
print("null passes the value form: {}", cast<Archer>(null) == null);
output
2 5! 3.0
97 a ab
copy 1 original 6
original now 2
through the handle: 99
caught: Cannot cast 'Archer' to 'Rock'
caught: Cannot cast 'null' to 'Archer&'
null passes the value form: true

A class mismatch raises the catchable Cannot cast 'X' to 'T', where X is the object's actual class. Null passes the value and handle forms (you get a typed null) and fails the reference form (there is nothing to refer to). Upcasts always pass. The operand is never checked statically against T. The cast is the boundary, and the verdict is the runtime's.

One warning to know about. The value form on a handle is legal, but it copies, and that is rarely what you meant when you had a shared_ptr in your hand. The checker says so: a warning, never a gate. The file simply loads; this is the runner with --warnings, which prints what the checker collected:

jaiscript
class Archer { int range = 6; }
shared_ptr<Archer> h = new Archer();
Archer c = cast<Archer>(h);                   // legal, but it copies: the checker warns
c.range = 1;
print("copy {} handle {}", c.range, h.range);
output · --warnings
jaiscript: static check warnings:
try.jai:3:12: warning: cast<Archer>(h) copies the instance - use cast<Archer&>(h) to alias it or cast<shared_ptr<Archer>>(h) to share it (declared at 2:20)
    Archer c = cast<Archer>(h);                   // legal, but it copies: the checker warns
               ^
copy 1 handle 6

Take its advice: cast<Archer&>(h) to alias, cast<shared_ptr<Archer>>(h) to share.

Escape hatches: extern and the mode attributes​

Two spellings tell the checker about what it cannot see, or should not gate on, without loosening anything else.

extern: declare a name you do not define​

Some names are real but not in your program: a global the host binds after load, a function the running instance registers, a value injected at run time. Without help the checker calls them undeclared. extern declares them, in three strengths:

jaiscript
extern function net_relay;          // a callable of unknown signature: calls are unchecked
extern int tick_rate;               // typed: every use is checked as an int
extern config;                      // untyped: behaves like var
function on_packet(var p) { return net_relay(p, tick_rate, config); }
print("loaded; on_packet waits for the host to register net_relay");
output
loaded; on_packet waits for the host to register net_relay

Each form counts as a declaration, and the program's pool carries it to every file. The typed form gives the name that type for real: member checks, stores and everything downstream are checked as for a declared variable, so this is a load error:

jaiscript
extern int tick_rate;
tick_rate = "fast";
output
jaiscript: static check failed:
<script>:2:11: error: cannot assign 'string' to 'tick_rate' declared 'int' (declared at 1:1)
    tick_rate = "fast";
              ^

At run time an extern declares nothing. The name resolves when it is used, so the extern may run before the file or the host that defines it; a declaration in any file, a host add_global or a host function is the definition. A use while nothing defines the name raises 'tick_rate' is declared extern but never defined (an 'inline' declaration creates it) (for extern function: 'net_relay' is declared extern but never defined), a catchable error. If the same program does define the name, the definition wins, which makes extern the right spelling for a shared file that calls into a module only some programs import (Modules has that shape).

inline: one variable every file shares​

inline on a variable means what it means in C++17: one variable shared by every file that declares it. The first declaration to run creates it, from its initializer or its type's default (int 0, string "", a class default-constructed, var null); every later one, in any file or a re-run of the same file, adopts the existing variable and runs no initializer. It is allowed at program scope and in a namespace, and the checker sees an ordinary declaration of that type:

jaiscript
inline int frags;                   // hud.jai: exists whether or not netplay.jai runs
inline int frags = 10;              // netplay.jai: adopts it, the initializer does not run
frags += 1;
print("frags {}", frags);
output
frags 1

An inline declaration also adopts a global the host bound. Inline and plain declarations of one name never mix across files, whichever runs first: a plain declaration of a name an inline declaration created raises 'frags' is already defined inline; declare it 'inline int frags;' to share it, and an inline declaration of a name another file declared plainly raises 'frags' is already defined (not inline) in stats.jai; declare every definition 'inline' to share it. The checker reports the mix at load, at the plain declaration. Editing one file's declaration between the two and reloading it converts the variable in place: to inline it keeps its value, to plain it follows the ordinary re-run rule (an initializer runs again; without one the value stays unless the declared type changed).

[[warn_mode]] and [[strict_mode]]: a region, not a switch​

The attributes set the checker's mode for a region of one file. In a warn region every error is reported as a warning (still collected, still visible to a host), and an undeclared identifier is accepted as unknown. In a strict region errors gate the load, even inside a warn region. They compose and innermost wins; outside every region a file is strict. An attribute leads one of three positions:

  • One statement. Before any statement, it covers that statement's own subtree, then the mode reverts. A nested function or lambda body is a new scope that keeps the enclosing mode: [[warn_mode]] spawn([]() { x = 5; }); accepts spawn, but x = 5 inside the lambda is still an error.
  • A block. As the first statement of a block (a function, method or lambda body, a loop body, a bare { }), it covers the whole block, nested bodies included.
  • A unit. As the first declaration of a file (a leading comment is fine, the trailing ; optional) it covers the whole file.

Statement position, with the revert made visible:

jaiscript
int x = 5;
[[warn_mode]] y = 5;               // covers this one statement: 'y' is a warning
z = 5;                             // the mode reverted: this one is an error
output
jaiscript: static check failed:
<script>:2:15: warning: use of undeclared identifier 'y'
    [[warn_mode]] y = 5;               // covers this one statement: 'y' is a warning
                  ^
<script>:3:1: error: use of undeclared identifier 'z'
    z = 5;                             // the mode reverted: this one is an error
    ^

Unit position, for a file whose hooks reach host names the checker cannot see. The load succeeds, the function waits for the host:

jaiscript
[[warn_mode]];                     // first declaration: the whole file reports warnings
function on_debug_key() { return debug_overlay(1); }    // a host name the checker cannot see
print("loaded; on_debug_key waits for the host");
output
loaded; on_debug_key waits for the host

And [[strict_mode]] re-arming inside that same warn-mode unit. The block gates the load even though the file around it would not:

jaiscript
[[warn_mode]];
function on_debug_key() { return debug_overlay(1); }
[[strict_mode]] { int n = 1; n = "one"; }   // re-armed strict inside a warn unit: gates the load
output
jaiscript: static check failed:
<script>:2:34: warning: use of undeclared identifier 'debug_overlay'
    function on_debug_key() { return debug_overlay(1); }
                                     ^
<script>:3:32: error: cannot assign 'string' to 'n' declared 'int' (declared at 3:23)
    [[strict_mode]] { int n = 1; n = "one"; }   // re-armed strict inside a warn unit: gates the load
                                   ^

The runtime never changes. Both backends treat an attribute as nothing, and a warn region's type mistakes raise at run time exactly as they would with no checker at all (the debug_overlay call above would raise Undefined variable if it ran before the host registered it). A file's mode says nothing about the files it imports, and a [[warn_mode]] leaf never fails its program. warn_mode and strict_mode are the only attribute names; anything else in [[ ]] is a parse error.

Prefer extern when you know the name; it is precise and it keeps the rest of the file checked. Reach for [[warn_mode]] when a region is deliberately dynamic.

Across files​

Checking does not stop at the file boundary. 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, so a function in one file may use a global another file declares whatever the import order, and a mistake in any file fails the root's load with a listing named by that file's path. Modules walks through it with a two-file program.

One mode, everywhere​

The embedded engine and the standalone runner check alike: errors fail the load before anything runs, warnings are collected and never gate, and [[warn_mode]] is the one way to let a region's errors through as warnings. A host that wants the listing without running anything calls engine->check(source), and reads the last executed unit's diagnostics from engine->last_check_diagnostics(). The C++ side of all this (the exception type, jaibite behavior, hot-reload re-checks) is in Static Checking in the Embedding guide.

In the editor​

The same checker runs as you type. The language server jaiscript_lsp, launched by the VS Code extension, checks each file against its whole program and underlines every diagnostic this chapter describes at its own file and line, imported files included, before anything runs; it also answers go to definition, find references, hover (declared and inferred types, signatures), completion, signature help and rename from the same parse. Editor Support covers installing it and what it can and cannot see.

Where to next​

Two chapters remain: Time covers the time:: namespace (clocks, stopwatches, calendar dates, and the time source a host can replace), and Math covers the vector and color types.