start

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.

diagnostics
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.

diagnostic
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:

diagnostic
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:

diagnostic
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:

diagnostics
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 —

diagnostic
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.

hoisted.as
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
}
diagnostic
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.

suspicious.as
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;
}
diagnostics
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.

diagnostic
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.

diagnostics
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.

handles.as
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());
}
diagnostics
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 _.

diagnostic
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:

diagnostic
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:

diagnostics
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.

countdown.as
export function remaining(elapsed: u32): i32 {
    let left = 60 - elapsed;
    return left < 0 ? 0 : <i32>left;
}
diagnostics
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.

diagnostics
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:

diagnostics
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:

diagnostic
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:

diagnostic
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:

diagnostics
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:

diagnostic
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:

diagnostic
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.

orphaned.as
function fallback(): f32 {
    return 1.0;
}

const seed: f32 = 3.0;

export function apply(v: f32): f32 {
    return v * 2.0;
}
diagnostics
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.

diagnostic
warning W3005: 'Base' calls 'setup' while it constructs, so this override can run before 'Derived' has set its fields