using the compiler

Warnings & Lints

Warnings that catch suspicious and dead code early, without ever changing the compiled output.

rasc carries two diagnostic severities. Errors stop a build; warnings never do — they print from every CLI mode, and rasc check --strict opts into failing on them (with --json for tooling). --strict-null, on rasc and rasc check, promotes just the null-safety warning below to an error.

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 stdlib or vendored lib sources): 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; rasc code uses let and const, and the generated wasm is identical.

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. 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 rasc 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 under the frame runtime when a function binds the instance to a local, uses it as a temporary, or makes it in a loop. Such an instance takes memory no frame boundary reclaims, so code that runs 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, even in a loop. Outside a loop, an instance the function stores into a field, global, static or array, returns or yields, directly or through a local, is kept on purpose and stays quiet too, as does module start code, which runs once. Locals match by name, so one kept through a second local still warns.

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 lazy compilation is already a whole-program reachability analysis: W3002, a file-local function nothing references, 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