Using the Compiler
One self-contained binary compiles rasc straight to WebAssembly — no Node, no external toolchain, about ten milliseconds for a real module.
rasc <entry.as> [options]
-o <file> output file (default out.wasm)
--lib <dir> stdlib directory (default std/assembly)
--path <dir> search path for bare imports; repeatable
--runtime <name> none | stub | frame (default none)
-D <name>[=<n>] i32 build constant (default 1); repeatable
-O rasc's own optimization passes
-O2 -O plus a binaryen wasm-opt post-pass
--enable-simd v128 + f32x4/i32x4 subset
--enable-relaxed-simd relaxed fused multiply-add (implies simd)
--exportRuntime export __new/__pin/__unpin/__collect
--no-alloc-sink disable allocation sinking (bisect aid)
--max-pages <n> memory ceiling in 64KB pages, emitted as the
wasm memory max; errors when static data
alone exceeds it
--dangerously-fast compile out the execution-budget fuel checks
--unsafe lift the unsafe gate for the whole module
--wat also print the module as wat
--tokens / --ast dump token stream / parsed AST
rasc check <file> [--json] [--strict] [--runtime <rt>] [--lib <dir>]
[--path <dir>] [-D ...]
diagnostics only; --strict fails on warnings,
--json emits machine-readable diagnostics
The fast-default principle
The default compile is deliberately unoptimized: it is the edit-time iteration mode, about 10 ms for a 375 KB module where upstream asc takes 0.5–1 s. Every optimization is opt-in, default output stays byte-identical build to build, the name section is always emitted, and debug info survives every pass — the debugger never has a reason to distrust an artifact.
Production performance does not depend on -O either: compiled
ahead of time, unoptimized rasc output already lands within 3–16% of
asc -O3, ahead on i64 arithmetic. Optimize when module size or
interp-tier speed matters.
Build defines
-D <name>[=<n>] sets an i32 build constant, 1 when no
value is given; repeat it for more. In a Rive project the same set comes from
--define=NAME[=n] on rive_cli (bake, live window, and
push) or a defines: map in rive.yaml holding integers
or booleans, with the command line overriding the file. A define is not a
global. It is a name the compiler answers for in two places:
isDefined(NAME)folds to true when the define is set or the source binds the name as a local, global, function, class, function template, or class template, false otherwise. Enums, namespaces, and type aliases do not count.- A use of
NAMEthat the source never binds compiles to the constant and folds inside conditions like a literal, short circuits included, soisDefined(X) && X > 1never compiles the reference whenXis absent. A bare use of an unset name is still an unknown identifier,ASC_names aside (below).
A constant if never compiles its dead branch, so a define drops
whole features from the module rather than just skipping them:
rasc main.as -D PIN_TIER=2 -D NO_FOG
rive_cli . --define=PIN_TIER=2 --define=NO_FOG
export function tier(): i32 {
if (isDefined(PIN_TIER)) {
return PIN_TIER; // -D PIN_TIER=2 folds this to 2
}
return adaptiveTier();
}
export function draw(): void {
if (!isDefined(NO_FOG)) { // -D NO_FOG compiles the pass out
drawFog();
}
}
A local (captured ones included), global, or function of the same name
shadows the define, so a define only stands in for value names the source
never binds; a class or template of that name leaves the define in force. Arithmetic on a define wraps at i32 like the code it
replaces, and as u32/as u64 fold with unsigned
compares. rasc check takes -D too, so lint folds
the same branches the bake does.
Compiler constants
Alongside your own defines, a fixed handful of ASC_ identifiers
are derived from other flags. These four fold at compile time, and a constant
if on one never compiles its dead branch — the mechanism behind one
source serving several artifacts:
| Constant | Driven by | Value |
|---|---|---|
ASC_FEATURE_SIMD | --enable-simd |
1 under the flag, 0 without it |
ASC_FEATURE_RELAXED_SIMD | --enable-relaxed-simd |
1 under the flag, 0 without it |
ASC_DEBUG | -O / -O2 |
1 in the default build, 0 once optimized |
ASC_RUNTIME | --runtime |
3 (Runtime.Frame) under frame,
0 (Runtime.Stub) under stub and none
alike |
export function tick(): void {
if (ASC_DEBUG) {
// Only compiled into the default build; -O drops the branch.
checkInvariants();
}
if (ASC_RUNTIME == 3) {
// Only compiled under --runtime frame.
}
}
Compare ASC_RUNTIME against the literal: the folder evaluates
literals, identifiers, and operators, not member access, so
ASC_RUNTIME == Runtime.Frame still emits a runtime compare and keeps
both branches.
The stdlib declares the rest of upstream asc’s set
(ASC_TARGET, ASC_SHRINK_LEVEL,
ASC_OPTIMIZE_LEVEL, …) and the compiler accepts any
ASC_-prefixed identifier, declared or not. None of them fold:
each compiles to an i32.const 0, so an if on one
keeps both branches in the module. Only -O2’s wasm-opt
post-pass removes them. A define wins over the prefix, so
-D ASC_TARGET=7 folds to 7 like any other define. Gate feature
code on the four constants above or on your own defines.
The deeper material has its own chapters:
Optimization covers -O and
the binaryen post-pass, SIMD the
one-source-two-artifacts pattern in full, Warnings &
Lints the analysis-only diagnostics, The
Unsafe Gate the raw-memory review marker, and
Runtimes & Environment the runtime
flavors, environment knobs, and verification culture.