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 and warnings
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.
W3011 warns where a function changes a global a lane reads, at the function’s first change to it, when the lane neither runs that function nor changes the global itself, as it would a scratch buffer. Each lane starts from fresh globals, so it never sees a change the main lane makes later, such as a setter the main lane calls. It follows a change as E3004 does, and a global whose change E3004 already refuses does not warn again. Like E3004, only the compile of a lane module finds it.
warning W3011: this changes `unitsPerMeter`, which job code reads, but lanes start from fresh globals and never see the change; pass it to the job as an argument (read through scaled)
To fix it, pass the value to the lane as an argument.
Typing errors
These errors and W3007 to W3009 and W3013 to W3018 below come from
rasc’s typing rules, which are on by
default, so a compile with --no-typing-rules reports none of
them.
E3005 rejects a value no 64 bit type holds: a
u64 beside a signed value that may be negative in
|, ^ or a pick, or a literal past
i64’s range in arithmetic beside one, where no declared
integer type reads the result. For let m = l | q:
error E3005: no 64 bit type holds both a u64 and an i64 that may be negative; annotate the declaration or cast an operand
E3006 rejects an integer division or remainder
whose divisor is always zero, a literal, a constant or a folded constant,
which could only trap. Under a float context the division is real and gives
±Infinity or NaN instead, and code a constant condition skips is not
checked. For let r: i32 = x / 0:
error E3006: this divisor is always 0, so the division always traps; use a nonzero divisor, or make an operand a float for Infinity
E3008 rejects a shift whose constant count
would shift by another count than it spells: one at or past the 32 or 64
bits the shift computes in, after an 8 or 16 bit left widens to
i32 and a constant left takes the 64 bit type beside it, or one
below zero. x << 32, x << -1 and
let r = 1 << 40, where no 64 bit type is beside the
literal, are errors, while q | (1 << 40) with
q: u64 sets bit 40. It also rejects a <<
whose count shifts every bit out of a narrower type that reads it, as in
let r: u8 = b << 8, and a >>> whose
count is at or past the bits of the 8 or 16 bit signed value whose own bits
it shifts, as in a >>> 8 with a: i8, where
<i32>a >>> 8 shifts the sign extended value.
Generic code is checked where no type
parameter gives the width, and code a constant condition skips is not
checked:
error E3008: this shift count of 32 is at or past the 32 bits of the i32 it shifts; write <i64>x << 32
error E3008: 1 << 40 shifts an i32 here, since no 64 bit type is beside it; write <u64>1 << 40 or annotate the declaration u64
error E3008: this shift count of 8 is at or past the 8 bits of the u8 that reads it, so every bit it shifts is lost; shift right, or give the target a wider type
error E3008: this shift count of 8 is at or past the 8 bits of the i8 whose own bits >>> shifts, so every bit is lost; write <i32>a >>> 8 to shift its sign extended value
error E3008: this shift count of -1 is negative and wraps to 31; use a count from 0 to 31
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
W3007 to W3009 and W3013 to W3018 come from rasc’s typing rules, so
a compile with --no-typing-rules reports none of them.
W3007 flags an integer literal leading
+, - or * that took an unsigned or 8
or 16 bit type from the other operand, because no declared integer context
reaches it: an unannotated declaration at any scope, a mutable module
let or a hoisted var included, an array element or
generic argument whose type the compiler infers, a template interpolation, a
method’s receiver, a switch value, a float or bool context, or a bare
expression. It warns where the value can leave that type, below 0 for
an unsigned type or outside its range for a small one, and a consumer shows
the difference: a mask, a shift, >>>, a store or cast
to that width or an equality reads only bits the wrap keeps, a test of zero
only whether it is zero, and an ordering against a literal only which side
the value lands on, so those stay quiet where the wrap cannot change them.
rasc keeps the typing, which existing code relies on (see
integer typing), so annotate the
declaration : i32, or elsewhere cast the expression or an
operand, as the warning says. Ranges come from
literals, constants, operand types, casts the value fits, x %
c, x & c, shifts by a literal, min and
max, clz, ctz, popcnt and
both arms of a ternary, so 255 - b, 255 -
<u32>b, 100 - (e % 100) and 64 - (e &
63) stay quiet. It also flags an inferred type keeping a wrapping
literal at its top typed by its other operand: let m = -1 + u
stays a u32, since another inferred type could stop an array
literal or generic argument it reaches from compiling. And it flags an
unannotated declaration whose literal arithmetic wraps in the type it
infers:
warning W3007: 1024 * 1024 * 1024 * 4 is 4294967296, which wraps to 0 in the i32 this constant infers; annotate it ': i64'
W3008 flags an unsigned value ordered against
0 or a negative literal so that the answer never changes:
u < 0, u >= 0, 0 > u or
u <= -1. It catches an unsigned countdown however it is
spelled, for (let i: u32 = n; i >= 0; i--) included, while
u <= 0 and u > 0 stay quiet. It also flags an
equality whose constant the other side’s type cannot hold, a signed
value ordered against a literal past i64’s range, and a
switch case the discriminant’s type reads as another value, and the
complement of an unsigned 8 or 16 bit value, an i32 from
-256 or -65536 to -1, compared with
0 or tested, as in ~b == 0 or !~b,
which are always false; <u8>~b == 0 checks for a byte of all
ones. A side whose
sum or difference a comparison of mixed signs computes exactly stays quiet,
since (u - 1) == -1 holds at u = 0:
warning W3008: a u32 never equals -1, so this comparison is always false; compare with 0xFFFFFFFF or cast
warning W3008: an i64 is always below 9223372036854775808, so this comparison is always true
warning W3008: an i64 never equals 18446744073709551615, so this case matches -1, as i64 reads it; write -1
Neither warning looks at the std or at generic code, whose type arguments may be signed.
export function remaining(elapsed: u32): i32 {
let left = 60 - elapsed;
return left < 0 ? 0 : <i32>left;
}
warning W3007: the literal takes u32 from the other operand, so a negative value wraps; annotate the declaration ': i32' or cast an operand to keep it
warning W3008: a u32 is never negative, so this comparison is always false
W3009 flags a width sensitive operation that
reads a value whose 64 bit type came only from keeping both operands’
values: a >>> of such a value, a clz,
ctz, popcnt, rotl or rotr
whose inferred type argument it decided, and an | or
^ of an unsigned operand with a constant only a shift or
~ made negative. An unannotated local holding such a value
counts too, so a ported hash step let h = seed ^ len warns at its
h >>> 13. It also flags a comparison or test of such a
value, or of a shift, arithmetic or pick that carries it, either operand of
a tested && or || included, as in the sign test
((a ^ b) >> 31) != 0, against a constant or a 32 bit side,
which compares other numbers than the 32 bit code it was ported from, an 8 or 16 bit value shifted into bit 31
beside an unsigned operand, and a named constant like const SB = 1
<< 31 beside one. Cast an operand, annotate the local, or give
the type argument, for the 32 bit answer.
warning W3009: x ^ 0x9E3779B9 is an i64, since no 32 bit type holds both operands, so this >>> shifts 64 bits; cast an operand to u32 or i32 for a 32 bit shift
warning W3009: h is an i64, as its declaration's operands share no 32 bit type, so this >>> shifts 64 bits; annotate it ': u32' or ': i32' for a 32 bit shift
warning W3009: x | u is an i64, so clz counts 64 bits; give clz<i32> or cast the operand
warning W3009: (a ^ b) >> 31 is an i64, as an | or ^ in it holds operands no 32 bit type holds, so != compares that i64; cast an operand to u32 or i32 to compare 32 bits
warning W3009: 1 << 31 is the i32 -2147483648, so this | gives an i64; write 0x80000000 for bit 31
warning W3009: a << 24 is an i32, which bit 31 makes negative, so this | gives an i64; write <u32>a << 24 for an unsigned pack
W3013 flags an equality whose sides hold
values that compare apart while their 32 or 64 bit patterns match: a signed
value that may be negative against an unsigned value of at least 32 bits that
may reach its sign bit, or a pick of a negative constant and an unsigned value,
such as let idx = found ? -1 : u, against either. rasc compares
their values, so x == u is false at x = -1 and
u = 0xFFFFFFFF, and so is idx == NONE_U when nothing
was found, while code ported from C or JavaScript often means to compare bit
patterns. Value ranges keep it quiet where the two readings agree: a local
takes its initializer’s range where nothing stores to it later, and a
loop counter that starts at zero or more and only grows is never negative, so
let kind = u & 0xFF; kind == x and
for (let i = 0; i != u; i++) do not warn. A negative constant
such as a -1 sentinel means the same in both readings, and a
constant the other side cannot hold is W3008’s. It also flags
x >>> 0 on a signed x, which JavaScript
reads as unsigned and rasc leaves as it is:
warning W3013: x is an i32 and u a u32, so == compares their values, and a negative i32 never equals a u32; to compare 32 bit patterns, cast one side: <u32>x == u
warning W3013: idx is an i64 and NONE_U a u32, so == compares their values, and a negative i64 never equals a u32; to compare 32 bit patterns, cast one side: <u32>idx == NONE_U
warning W3013: x >>> 0 is x, an i32: >>> keeps its left's type, so this does not read x as unsigned the way JavaScript's >>> 0 does; write <u32>x
W3014 flags a difference of two unsigned 8 or
16 bit values that is directly the dividend of / or
% by an unsigned value of 32 bits or more or by a literal, as
in (h - b) % 16. The difference
computes in i32, so it is negative where the right operand is
the larger, and the remainder or quotient keeps that sign, where code
ported from a language that wraps the difference at its own width counted
around a ring. With tl and hd as u16
and CAP: u32 = 16, (tl - hd) % CAP is
-2 at tl = 3, hd = 5. Adding the
capacity first keeps the count in range and is quiet, and so does casting
the difference back to its width where the capacity is a power of two that
width holds, which the warning then suggests too:
warning W3014: tl - hd may be negative, as a difference of u16 values computes in i32, so this % keeps its sign; for a count that wraps write (<u16>(tl - hd)) % CAP, or add the capacity: (tl - hd + CAP) % CAP
W3015 flags an integer / that
truncates under a float context. A float context divides integers for
real, but where another operand of the value may wrap, as
col * cellW may, rasc keeps the integer code for the whole
value, since the float would read the wrapped product, so the quotient in
it truncates too, in either order and in generic code:
let cx: f64 = x * 2 + y / 2 is 9 at
x = 3, y = 7. Cast the left operand to the float
to divide for real, or an operand inside it where it is computed and may
wrap itself, or wrap the quotient in <i32> to keep the
integer. x + y / 2 has no
operand that wraps, so it is 6.5 and quiet:
warning W3015: cellW / 2 divides integers here, as this f64 keeps asc's code where an operand may wrap; write <f64>cellW / 2 to divide for real, or <i32>(cellW / 2) to keep the integer quotient
W3016 flags a u32 complement or
negation that a 64 bit operator, declaration or compound divisor widens,
but not an & beside an unsigned operand of 32 bits or less,
whose high half is zero anyway. ~ and
unary - compute in their operand’s own type, so the high 32
bits of the widened value are 0, and q & ~FLA with
FLA: u32 clears the high half of q as well as
FLA’s bit:
warning W3016: ~FLA is a u32, so this & also clears the high 32 bits of q; write ~<u64>FLA to clear only FLA's bits, or <u64>~FLA to keep this
warning W3016: ~crc is a u32, so its high 32 bits are 0 in this u64; write <u64>~crc to keep that, or ~<u64>crc for a 64 bit complement
W3017 flags a 32 bit rotate whose
<< a declared 64 bit type widens, a declaration,
compound target, argument, return or store: an |,
^ or + of
x << k and x >>> m of one 32 bit
x, or x >> m of an unsigned one, a load
included, with constant counts that sum to 32 or with
m written 32 - k. A 64 bit type reading a
<< shifts its left in 64 bits, so the bits that would
rotate round to the bottom move past bit 31 instead, and
let h: u64 = (k << 15) | (k >>> 17) with
k: u32 is 0x40000000c000 at
k = 0x80000001, where the rotate is 0xc000:
warning W3017: k << 15 shifts 64 bits in this u64, so (k << 15) | (k >>> 17) does not rotate k; write rotl<u32>(k, 15) or <u32>((k << 15) | (k >>> 17)) to rotate 32 bits, or <u64>k << 15 to keep the 64 bit shift
W3018 flags a declared 64 bit type, a
declaration, compound target, argument, return, cast or store, reading a
name bound once, with no type annotation, to a << by a
variable count, to a << of a never negative left that
reaches int32’s sign bit, or to arithmetic or bitwise operators
holding one, such as a word assembled from bytes. The name is the
i32 AssemblyScript types it, so the 64 bit type reads its
int32 value sign extended, where the same shift written inline widens
first: const mk = 1 << b; flags |= mk on a
u64 flags sets bit b & 31,
8 at b = 35, where flags |= 1 << b
sets bit 35. Annotate the name : u64 to shift in 64 bits, or
: u32 to read a word unsigned; a left that may be negative
shifts as int32 inline too, so its name is quiet:
warning W3018: mk is an i32, as its declaration shifts in 32 bits, so this u64 reads it sign extended; annotate it ': u64' to shift in 64 bits, or ': u32' to read the word unsigned
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