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 —
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.
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. 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 rasc 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 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.
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.
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