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

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.

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

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