Warnings & Errors
Warnings that catch suspicious and dead code early, without ever changing the compiled output.
AnimaScript diagnostics come in two severities. Errors stop a build;
warnings never do. The rive command line tool prints both, each
with the script, line and column it points at. rive <dir>
--verify checks a project and prints its problems. While the live
window runs, the terminal prints them again on every rebuild. Each build also
rewrites the problems log, the file logs.problems names in
rive.yaml, which is build/problems.log in a project
rive create made.
Lane errors
E3004 refuses code that a lane module cannot
run. A project’s jobs compile into one lane module, its workers into
another and its audio processors into a third, and each holds only the code
its functions or classes reach. That code may use its arguments, shared
memory, pure std code and console logs, and in a job or a worker also
timers and the clock. When it would reach the host, such as making a
Path or drawing a random number, or would wait in a way its
lane may not, the compiler refuses it and names the chain of calls that
reaches it. It also refuses a top level statement or an initializer, in any
of the project’s scripts, that changes a global the lane reads, itself
or in the functions, methods, constructors, default arguments, accessors and
operators it calls, counting every override a call through a base class or an
interface may reach, and following the global’s object through casts,
ternaries, cascades, locals and globals set from it, ref
parameters and views over its buffer: a lane module never runs top level
code, nor the initializers of globals and static fields it never reads, so
the lane would see the global without the change. A standard library method
counts as a change unless it only reads. The error names the function that
makes the change when it is not the refused code. A change made through an
object a function returns, through a function value, or inside code the
standard library calls on its own is not refused. Only the compile of a lane module finds these
errors, so they appear when the project builds rather than as you type,
after the lane module’s name, as in jobs module:.
Threads says what each lane can reach.
error E3004: job code reaches env.seed, which a job cannot use; it sees only its arguments, shared memory, pure std code and console logs, timers and the clock (through rolls -> random)
error E3004: this top level statement changes `table`, which job code reads, but lanes skip top level code; move the change into `table`'s initializer (read through reads)
error E3004: this initializer changes `table`, which job code reads, but lanes skip the initializers of globals they never read; move the change into `table`'s initializer (changed in fill; read through reads)
To fix a refused call, give the lane what it needs as arguments or through shared memory, and do the host’s part on the main lane: draw the random numbers there, pass them in, and use the lane’s result there. Where the lane has another way to wait, the error names it. To fix a refused change, give the global its whole value in its own initializer.
Warnings
The flagship is W3001, the dead-mutated-copy warning: a
field store into a value-class local that was copied out of a writable place
(an element access or field chain, seen through unchecked()), where
no forward path reads the local again before reassignment or scope end —
warning W3001: this writes to a copy of 'poly.vertices[i]'; the original
is never updated because value classes copy on assignment. write through
'poly.vertices[i]' or store the copy back
Idiomatic shapes stay silent: mutate-then-use, store-back, reads in a later loop iteration, and passing the copy to a call. See the alias-mutation hazard for why this exists.
A lint pass covers every user file in the import closure (never the
standard library or the library sources bundled with the compiler): unused locals and parameters, unreachable code,
assignment in if conditions, root-scope shadowing, use before
declaration — and W1007, every var declaration.
var compiles for upstream AssemblyScript compatibility, but it is
function-scoped and hoisted, a for..of binding too, and one
nested in module code belongs to the module, unless only a narrowing in its
block can tell its type and it has no annotation; AnimaScript code uses
let and const.
export function last(xs: Array<i32>): i32 {
for (var i = 0; i < xs.length; i++) {
}
return i; // legal only because var hoists: i outlives the loop
}
warning W1007: 'var' is function-scoped and hoisted; use 'let' or 'const'
Rewriting it with let makes the escape an error instead of a
surprise: i dies with the loop, and the function has to say what
it means.
A batch of suspicious-code rules rounds out the syntactic pass:
W1008 literal true/false conditions,
W1009 assigning a variable or field chain to itself,
W1010 comparisons whose two sides are the same expression,
W1011 duplicate switch case values, and
W1013 expression statements with no effect, like
a == b; where an assignment was meant.
export function tune(mode: i32, gain: f32): f32 {
let level = gain;
level = level;
if (level == level) {
level += 1.0;
}
if (false) {
level = -1.0;
}
level + 0.5;
switch (mode) {
case 1:
return level;
case 1:
return -level;
}
return level;
}
warning W1009: 'level' is assigned to itself
warning W1010: both sides of this comparison are the same
warning W1008: condition is always false
warning W1013: statement has no effect
warning W1011: duplicate case value; first on line 12
The loop idioms stay silent: while (true) and
do..while (false) never trip W1008. A deliberate NaN probe
does trip W1010 — write isNaN(x) instead of
x != x.
W1012 flags unused imports. A name used only as a type annotation, a generic constraint, or a re-export counts as used, so type-only imports never warn.
W1014 comes from the compiler: inside a method, a bare
name that resolves to a module scope global or function while the enclosing
class also has an instance member of that name. The global wins, as the
implicit this rule says, and
the warning points at the member that was probably meant.
warning W1014: 'count' is the global here, write this.count for the member
W1015 flags a closure writing to a local or parameter of
an enclosing function that the function then uses again.
Closures capture by value, so the write
changes only the closure's copy and the outer variable keeps its value. A
closure that owns its copy, like a counter, stays silent, as do writes to
module scope variables, fields through this, and a captured
array's elements, which are all shared. It also flags the mirror, once per
write: the function writing a local after a closure held in a local read
it, then using that closure on a path from the write, which still sees the
old value. Every closure a path may have left in the local counts, copies
into another local included, and a loop's next pass is such a path when the
body's end or a continue reaches it. Each pass binds its own
copy of a let or const declared in the loop, of its
for..of binding and of a for statement's
let, as JavaScript does, so a write to a later pass's copy never
reaches a closure an earlier pass made. A self-recursive closure stored into
the variable it calls through is that shape too: it captured the variable
while it was still null.
warning W1015: 'total' is captured by value; this write is not seen outside the closure
warning W1015: 'limit' is captured by value; the closure on line 3 does not see this write
Shadowing a member with a local or a parameter never warns; that is how
every constructor assigns its fields. Builtin names like load or
select are reserved rather than warned about: a member named
like one is only reachable through this..
W3004 is the null-safety check: a member access,
call, index, for..of or cascade on a T? value the
checker cannot prove non-null on that path, and a nullable value (or
null) stored into a non-nullable local, parameter, field, element
or return. Without it a read through a null reference is a silent load from
the bottom of memory. The proofs follow TypeScript's narrowing where
AnimaScript has the construct, over locals and over field chains like
this.context; the types page has the
rule.
class Context {
size: i32 = 1;
grow(): i32 { return this.size + 1; }
}
class Owner {
context: Context? = null;
reset(): void { this.context = null; }
tick(item: Context?): i32 {
let total = item.size; // unchecked parameter
if (item != null) {
total += item.grow(); // proven
}
if (this.context == null) {
return total;
}
total += this.context.size; // proven by the early return
this.reset();
return total + this.context.size; // a call on this unproved it
}
}
export function run(): i32 {
return new Owner().tick(new Context());
}
warning W3004: 'item' may be null; check for null, or assert with !
warning W3004: 'this.context' may be null; check for null, or assert with !
The analysis only decides whether to warn; no type changes and the emitted wasm is byte identical with or without it. A closure starts with no proofs, and a loop forgets every key it stores or calls through at entry, so re-check inside the loop or copy the handle into a fresh local.
W3006 flags new of an
@unmanaged class when a function binds the instance to a local,
uses it as a temporary, or makes it in a loop. A Rive project reclaims dead
objects at each frame boundary, but not an unmanaged instance, so code that
makes one every frame grows the heap every frame until __free
releases them. Make it once at module scope and reuse it, or free it: a local
the function passes to __free stays quiet. An instance the function returns,
yields, or pushes or stores as an element of an array that outlives the call
is kept on purpose and stays quiet, even when a loop builds a pool of them.
One stored into a field, global or static, or gathered into an array the
function made and drops, is kept outside a loop. Inside one, each pass would
leak the last pass's, unless the function also reads that field or global,
as a list linking the instance it replaces does. Module start code, which
runs once, stays quiet too. Locals match by name, and one handed to another
local shares its fate, as does a parameter's default when the body keeps or
frees the parameter. To keep one deliberately, name its local or parameter
with a leading _.
warning W3006: each @unmanaged 'Vec2' made here lives until __free releases it; make it once at module scope or free it
Two more warnings come from the compiler itself, because it already works out which code a program can reach: W3002, a file-local function no code that runs reaches, so functions that only call each other or are only called by unused ones warn too, and W3003, a global nothing reads or writes.
function fallback(): f32 {
return 1.0;
}
const seed: f32 = 3.0;
export function apply(v: f32): f32 {
return v * 2.0;
}
warning W3002: function 'fallback' is never used
warning W3003: global 'seed' is never used
API surface never warns: exported declarations, names in an
export { ... } list, and names starting with
_. Declarations named only inside a constant-folded feature
branch — the ASC_FEATURE_* gating pattern from
SIMD — are spared too, so dual-artifact
sources stay clean in both bakes. Both rules go quiet when the compile has
errors, since reference tracking is incomplete then.
W3005 also comes from the compiler: a
constructor or a field initializer calls a method on this that a
subclass overrides. The override then runs while the subclass is still being
built, so the fields it assigns in its own constructor, and every field of a
subclass without a constructor, are not set yet; see the
construction order. It warns at each such
override, so it shows in the subclass's script when the base lives in
another.
warning W3005: 'Base' calls 'setup' while it constructs, so this override can run before 'Derived' has set its fields