Status & Gotchas
What doesn’t compile, what behaves differently from upstream, and the sharp edges worth knowing before they find you.
Not supported
Compile errors unless noted:
- Exceptions —
throwcompiles to a trap,try/catchis rejected. (Upstream aborts with a message; rasc just traps.) - Destructuring and rest parameters. Spread arguments landed for
static-length sources — array literals and
FixedArray— so arity and element types check entirely at compile time; spreading a dynamic array, a string, or into a constructor is a clean error, and spreads never supplyrefparameters. - Closures capturing
this— copy fields to a local first. yield*, expression-positionyield,yieldinsideswitch/for..of.- Nullable basic types (
i32 | null). DataView, threads/shared memory, asc’s--transformplugins.- Struct scope pins that error explicitly — nullability,
generic arguments beyond
Map/Setpositions, closures and generators holding struct locals, float-keyed maps without a declaredoperator==/hash()pair. Methods, operators, constructors, refs, structMapkeys and values,Setelements, the fullArray<V>surface, and single inheritance all landed — see Structs & Refs.
Behavioral deviations from upstream asc
- Closure capture is by value at creation — a deliberate semantic, not JS’s by-reference capture. Internal mutation persists per closure; the outer scope never observes it.
- Default parameter values cannot reference earlier
parameters —
f(a: i32, b: i32 = a)is a clean compile error where upstream evaluates the default in the callee. Defaults compile at the call site, and before the fence they silently captured a same-named caller local. Defaults over globals and constants are unaffected. - Value-array out-of-bounds traps via
unreachablewith no message; stdlib arrays throw RangeError. arr.map(...)needs an explicit type argument when the element type only appears in the callback’s return type.- The
~lib/import prefix is not recognized — use bare specifiers against--lib. - Lambda parameters need type annotations unless contextually typed; generator locals need them when not inferable.
Gotchas from the ports
Collected while porting Draco, Box2D, and the Fields app — violate at your own cost:
- Silent traps fold to zero. A trapped host call can read as a zero and corrupt downstream state; when numbers go inexplicably wrong, check for “wasm call trapped” and audit the import table before chasing phantom stack overflows.
- The stub runtime never frees. That is its design — a
long session leaks unless the module rewinds its own bump position with
__mark/__resetTo; debug builds poison the rewound region with0xDDand trap any module-static root still pointing past the mark. The host warns when the heap keeps growing, again every 8 MB, and points atwasmRuntime: frame, the default for Rive projects: there the collector scavenges at every boundary and releases the host handles of wrappers that died — see Resource Lifetime for the walkthrough. - Closures under stub: the closure suite runs under the GC runtimes; under stub prefer class-based listeners.
Array<T | null>literals need an explicitly annotated local before assignment.- No
Math.randomwithout a host seed import — use a deterministic LCG. NoDate.noweither; time arrives throughadvance. - Static init ordering is three-tier: runtime files, then libraries, then the entry. Library static initializers must not assume entry state.
- Value-semantics porting: mutating a copy where the original was meant is legal code; W3001 catches the provably dead case, the rest is on you — see the alias-mutation hazard.
- AOT falls back silently on a stale hash. Verify
wasm aot: loaded; module timing and size are the tells. - Memory growth under AOT needs
RIVE_WASM_PREGROW_PAGES— growth moves malloc-backed memory under live AOT frames.
Fixed during bring-up
Caught while building the compiler and its ports; each carries a regression fixture:
- Cascades — Dart’s
..operator:new Paint()..color = c..width = 2evaluates the receiver once, runs each section on it and yields the receiver, in any expression position. Nullable receivers need!and structs are fenced in v1; the formatter puts one section per line and completion works after... See Classes & Generics. - Struct single inheritance landed — the base’s leaves become the
derived prefix, methods inherit with static shadowing (no vtable),
derived values slice to the base by value or view it in place through
refparams, constructors inherit TS-style, andsuperworks in constructors and methods. Operators and the==/hash()key pair deliberately do not inherit (the base’s fns have the base’s leaf shape); redeclare them on the derived struct. - A relative
--libpath crashed the compiler resolvingFixedArray— the loaded-file probe keyed on the raw path while loads record absolutized paths, so the load-and-retry loop never terminated and overflowed the stack. - Direct calls of function-typed fields and accessors
(
obj.fn(x)) landed — the closure loads off the receiver and dispatches indirectly, so the copy-to-a-local workaround is no longer required. for..ofover strings, typed arrays, and any class with alengthgetter andoperator[]landed. Strings iterate JS-faithfully: one code point per step, surrogate pairs kept whole, each yielded as aString. Thevalues()/next()protocol still wins when a class declares both.instanceofagainst an interface — the runtime-id walk only followed base chains, so the test silently folded tofalsefor every class. Implements clauses now match, including through a base, with a static fast path when the receiver’s declared class already implements the interface.- Compound assignment through an accessor (
b.x += 5) — the getter’s body was never scheduled for compilation, so if nothing else read the property the program trapped at the+=. - Unmanaged
newunder the GC runtimes —__allocresolution missed tlsf and fell back to a synthetic bump allocator based at the end of static data, underneath the spill zone: two allocators handing out overlapping memory. tlsf now serves whenever a runtime is linked, and the synthetic allocator (runtimenone) starts above the spill zone and shadow stack. - Imports from tiny files — a source small enough for the small-string buffer moved when later files loaded, dangling every identifier into it (“unknown identifier” from a 18-byte module).
- Deeply nested expressions — operand type peeks recompiled subtrees once per enclosing level, exponential in depth (30 levels compiled for 72 s). Peeks are memoized per node; any depth is instant.
- A compile-time pass over the profile — pooled parser lists, reused body and encoder buffers, one merged lib-export lookup — cut project-scale compiles roughly 3× and trimmed peak memory.
- Struct key identity —
MapandSetaccept struct keys: integer-leaf structs derive==and an FNV leaf hash automatically (and compare with==/!=anywhere); float-bearing structs declare theoperator==+readonly hash(): u32pair, with declare-both-or-neither enforced. Operators are native C++-style members everywhere — structs and reference classes alike (operator+(a, b),operator[](i),unchecked operator[]for theunchecked()fast path, unary by arity, struct!=always derived); the@operatordecorator is removed and the whole std is converted. Classes gained prefixoperator-dispatch; prefix!/~on classes keep built-in truthiness and stay undispatched. - AssemblyScript diagnostics reach the problems panel — rasc errors and W-family warnings report per script with spans through the workspace; the bake compile doubles as the diagnostics pass.
- Native value syntax —
struct,FixedArray<T, N>,refplaces with address-taken spilling and GC root pairing, struct methods (implicit refthis,readonlycopies), and@operatorstatics. The@value/@inlineArraydecorators are removed. Verified across all six runtime/optimization legs plus the Box2D differential harness. - Growable
Array<V>— sized zero-filled construction, element places with leaf chains,lengthreads and assignment, literals,for..of, andpush/popgrowth with flat strides, the backing buffer surviving GC churn.fill/slice/sortgrow from a stride-generic lib layer; W3001 covers array-element origins. - Struct ergonomics — overloadable by-value constructors
(
Vec2(3, 4);newon a struct is an error), value globals in static memory, value ternaries, structtoStringinterpolation, prefix and compound operators, andMap<i32, V>with flat struct values. - The
unsafegate — raw-memory builtins (load/store,changetype,memory.*, v128 load/store) now require anunsafe-marked function, with--unsafeas the whole-build waiver. Lib and runtime sources are exempt. - The dead-mutated-copy warning (W3001) — rasc’s first warning
severity. A field store into a value-class local copied from a writable
place, never read again on any path, now warns instead of silently mutating
a dead copy; idiomatic mutate-then-use, store-back, and loop-carried reads
stay silent. Warnings never fail the build (
rasc check --strictopts in) and codegen stays byte-identical. - Template literal interpolation (
`x is ${x}`) — each expression stringifies through its type’stoString(wrapper classes for primitives, virtual dispatch for classes and interfaces), pieces chain throughString.__concat, and intermediates stay rooted under both GC runtimes. What can’t stringify errors explicitly inside${}. Tested across stub, frame, and-O, including a GC-survival loop. - Value classes: the v1 POD shape is now enforced at every usage site —
generic arguments,
switch, conditions,for..of, generator params and yields,changetype/select/assert, plus declaration-side constructors, accessors, static methods, and index signatures — where multi-slot values previously could silently corrupt the wasm stack. isNullable<T>()folding per generic instance — nullable array elements no longer trap.- Generic
TypedArray#setand#slice(instantiate<T>builtin). - Ternaries unify sibling classes to the annotated base.
- Compound indexed assignment (
arr[i] += v). - String
s += x(previously miscompiled;s = s + xwas always fine). - Abstract methods — now unreachable stubs with stock-asc enforcement.
unchecked()— was a silent no-op, now honored under-O; debug builds stay fully checked.@externaland ambient namespace imports — declares now bind to real import modules, not justenv.--exportRuntime— the stock-asc runtime export surface.- Value-class holes closed with diagnostics: rvalue member stores, value
setters,
nullinto value targets, generic-path element stores.
On the roadmap
- By-reference closure capture (boxed slots).
- Debug tier: source maps, statement-boundary hooks, and a locals spill map — engine-independent, so a script debugs identically on the browser, on WAMR, and on device.
- Workspace mode: per-file re-analysis over a cached module graph for editor-speed diagnostics on large projects.
- Zero-copy struct-buffer handoff to the host across the web and native
bindings, with a Luau polyfill over its native
buffertype.