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:
int x = 5;
print("before");
x = "oops";
print("after");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:
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);caught: Cannot assign string to 'x' (declared int)
x is still 5This 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:
<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 toexecute(); a file, whether run directly or reached throughimportorinclude, is named by its resolved path instead (Modules). The listings in this chapter use<script>.errororwarning, 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:
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 loadjaiscript: 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.
| Site | Checked | Example 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 stores | against 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 constructors | arity (defaults counted) and each argument's assignability; with overloads, an error only when none fits | 'fib' expects 1 argument(s), got 2 |
| Returns | the 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 return | cannot return 'string' from 'label' declared to return 'int', 'sign' declared to return 'int' may reach the end of its body without returning |
| Reference bindings | a T& declaration or parameter binds only a T lvalue; a literal or temporary reaches a T& parameter only when the function never writes through it | argument 1 of 'bump': cannot bind 'float' to 'n' declared 'int&', 'bump' writes through parameter 'n' (line 1); pass a variable |
| Assignment targets | the left side of =, op=, ++ and -- must name storage; a read-only property and a map key seen through a range-for pair are not assignable | cannot 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 class | reads 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 value | no member 'range' in class 'Unit' |
| Identifiers | every 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 undeclared | use of undeclared identifier 'totl' |
| Methods and statics | a 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 it | cannot compare 'int' with 'string' using '==': the types have no comparison (it is always false); convert one operand |
| Enum values | membership | no value 'zzz' in enum 'Direction' |
| Host (C++) functions | arity 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 |
| Casts | T 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:
function fib(int n) -> int { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
print("{}", fib(10, 2));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:
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); }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:
class Unit { int hp = 10; }
class Archer : Unit { int range = 6; }
Unit u = Archer();
print("{}", u.range);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:
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);range = 6
range = 9
range = 9Through 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:
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; }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:
int lives = 3;
string typed = "3";
if (lives == typed) { print("match"); } // always false
if (lives == typed.to_int()) { print("match"); }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.
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 pathjaiscript: 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):
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 storagejaiscript: 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.autowithout an initializer, or with anullone (the type locks at the first real store, which the checker cannot see).- Stores into a reference itself (
r = vthrough aT&local, anauto&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. Avar&, anauto&over a value of unknown type and aparallel_forelement parameter check no members. - Elements of
varreceivers 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), untypedexternnames, a base class the program cannot find, and namespace paths at every depth. - Accessor-bearing members whose
_get_x/_set_xdeclaration is ambiguous (Classes). - Coroutines: yield and return types are not checked, and what
next()returns is unknown. Calling one yields a handle typed ascoroutine_handle, which answersnext(),advance(),done()andresult()(typed as the declared type);advance(v)needs a variable (advance() writes the yielded value through its argument; pass a variable, and aconstfails 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,
thisandsuperused as values, classes mid-hot-reload (receivers that still carry the old shape), and anything passed toextend(...).
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:
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);caught: Cannot return string from 'label' (declared -> int)
caught: Cannot pass string to parameter 'n' of 'twice' (declared int)
5! 2The 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 whatT tmp = e;would hold. For primitives that is the conversion table (cast<int>(2.7)is2,cast<string>(5)is"5"). For a class it is a copy, typedT, whene's class isTor derives from it.cast<T&>(e)is a reference to the same object, seen as aT. 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.
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);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: trueA 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:
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);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 6Take 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:
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");loaded; on_packet waits for the host to register net_relayEach 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:
extern int tick_rate;
tick_rate = "fast";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:
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);frags 1An 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; });acceptsspawn, butx = 5inside 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:
int x = 5;
[[warn_mode]] y = 5; // covers this one statement: 'y' is a warning
z = 5; // the mode reverted: this one is an errorjaiscript: 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:
[[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");loaded; on_debug_key waits for the hostAnd [[strict_mode]] re-arming inside that same warn-mode unit. The block gates the load even though the file around it would not:
[[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 loadjaiscript: 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.