start

Using the Compiler

One self-contained binary compiles rasc straight to WebAssembly — no Node, no external toolchain, about ten milliseconds for a real module.

shell
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:

A constant if never compiles its dead branch, so a define drops whole features from the module rather than just skipping them:

shell
rasc main.as -D PIN_TIER=2 -D NO_FOG
rive_cli . --define=PIN_TIER=2 --define=NO_FOG
tiers.as
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();
    }
}
source names win

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:

ConstantDriven byValue
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
gated.as
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.

every other ASC_ name is a runtime zero

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.