>_RLANG REFERENCE
A compiled R runtime written in Rust. Source is lexed and parsed to an AST, lowered to fusevm bytecode, and executed on the same language-agnostic VM + three-tier Cranelift JIT that hosts zshrs, stryke, awkrs, elisp, and rubylang. In active development.
What it is
GNU R evaluates R by walking a parse tree in C. rlang runs R programs as ordinary compiled bytecode: it lexes and parses the source, lowers it to fusevm instructions, and lets the shared engine execute and JIT-compile them. There is no bespoke VM or tree-walker — if, for, while, repeat, &&, and || lower to native fusevm jumps over native integer counters so the tracing JIT sees ordinary loops, while R-specific behaviour (vectors, attributes, recycling, environments, S3 dispatch) is served by the runtime host.
It is another language hosted on fusevm, the shared bytecode VM and Cranelift JIT behind zshrs (the shell), stryke (the language), awkrs (AWK), elisp (Emacs Lisp), and rubylang (Ruby). rlang carries no VM or JIT of its own. The binary is named Rscript.
Architecture
The pipeline mirrors how zshrs hosts zsh and rubylang hosts Ruby:
R source → lexer → parser (AST) → lower to fusevm bytecode → fusevm VM + Cranelift JIT
│
RHost heap (vectors, attributes, environments, closures)
fusevm-hosted
No local vm.rs / jit.rs. R is lowered to fusevm bytecode and executed on the shared three-tier Cranelift JIT; jit-disk-cache persists native code across runs.
Native control flow
Loops and branches lower to native fusevm jumps over native integer counters (src/compiler.rs), so hot loops trace-compile instead of re-entering a builtin per iteration.
Everything is a vector
There are no scalars: 1 is a length-one double vector. Every value is a Value::Obj handle into the RHost heap because any value can carry attributes.
Environments by reference
Frames are Rc<RefCell<..>> environments chained to their enclosure — R's lexical scoping, and what lets <<- reach the defining frame.
The value model
R's semantics are not object semantics; they are vector semantics. rlang implements them directly rather than approximating them.
Recycling & NA
Every operator recycles the shorter operand and propagates NA through all four atomic types. Three-valued logic is honoured: NA & FALSE is FALSE, NA | TRUE is TRUE.
Copy-on-modify
z <- y; z[1] <- 99 leaves y untouched. Assignment into an index rebuilds the container and re-binds the name, which is what R's semantics actually specify.
Attributes
names, dim, class, and arbitrary attr() ride along on every value in an insertion-ordered map, preserved through arithmetic and subsetting.
R truthiness
A condition must be a single non-NA logical, so conditions normalize through a TRUTHY host op before a native branch rather than using the VM's numeric truthiness.
y <- c(1, 2, 3) z <- y z[1] <- 99 print(y) # [1] 1 2 3 print(z) # [1] 99 2 3 print(c(1, 2, 3) + c(10, 20, 30, 40, 50, 60)) # [1] 11 22 33 41 52 63 print(c(1, NA, 3) * 2) # [1] 2 NA 6 print(NA & FALSE) # [1] FALSE print(NA | TRUE) # [1] TRUE v <- c(10, 20, 30) v[5] <- 50 # growing assignment past the end print(v) # [1] 10 20 30 NA 50
Indexing — all four forms
Positive, negative (exclusion), logical (recycled), and character (by name), plus [[, $, 2-D matrix indexing m[i, j], and the matrix subscript m[cbind(i, j)], whose rows are read as whole coordinates.
x <- 1:10 print(x[x > 5]) # [1] 6 7 8 9 10 print(x[-(1:5)]) # [1] 6 7 8 9 10 print(x[c(TRUE, FALSE)]) # [1] 1 3 5 7 9 v <- c(a = 1, b = 2, c = 3) print(v[v > 1]) # b c # 2 3 m <- matrix(1:6, nrow = 2) print(m) # [,1] [,2] [,3] # [1,] 1 3 5 # [2,] 2 4 6 print(m[, 2]) # [1] 3 4
Functions, closures, and complex assignment
Defaults may refer to other arguments, ... forwards, and argument matching follows R's exact/partial/positional rules. Complex assignment targets compile the way R defines them: f(x) <- v becomes x <- `f<-`(x, v), and nested targets unwind through the same rule.
fib <- function(n) if (n < 2) n else fib(n - 1) + fib(n - 2)
print(sapply(0:10, fib))
# [1] 0 1 1 2 3 5 8 13 21 34 55
counter <- function() {
n <- 0
function() {
n <<- n + 1
n
}
}
tick <- counter()
tick(); tick()
print(tick()) # [1] 3
l <- list(v = c(1, 2, 3))
l$v[2] <- 99 # nested complex target
print(l$v) # [1] 1 99 3
x <- c(1, 2, 3)
names(x) <- c("a", "b", "c") # replacement function
print(x)
# a b c
# 1 2 3
S3 dispatch
UseMethod walks the class vector and falls back to .default, with implicit classes for the builtin types. class(), inherits(), structure(), and unclass() are present.
area <- function(s) UseMethod("area")
area.square <- function(s) s$side^2
area.default <- function(s) stop("unknown shape")
sq <- structure(list(side = 3), class = "square")
print(area(sq)) # [1] 9
print(class(sq)) # [1] "square"
print(inherits(sq, "square")) # [1] TRUE
Operators, pipes, and the primitive library
The full precedence ladder from ?Syntax, %%/%/% taking the sign of the divisor, %in%, user-defined %op%, and the native pipe |>. 288 primitives are registered in builtins::PRIMITIVES — the apply family, string and regex functions, numeric summaries, sequence and set functions, and matrix helpers.
print(-5 %% 3) # [1] 1
print(7 %/% 2) # [1] 3
print(1:3 %in% c(2, 3, 9)) # [1] FALSE TRUE TRUE
`%+%` <- function(a, b) paste0(a, b)
print("fu" %+% "sevm") # [1] "fusevm"
print(c(3, 1, 2) |> sort() |> rev()) # [1] 3 2 1
print(Reduce(function(a, b) a + b, 1:5)) # [1] 15
print(Filter(function(x) x %% 2 == 0, 1:10)) # [1] 2 4 6 8 10
print(do.call(sum, list(1, 2, 3))) # [1] 6
print(mean(c(1, 2, 3, 4))) # [1] 2.5
print(sd(c(1, 2, 3, 4))) # [1] 1.290994
print(cumsum(1:5)) # [1] 1 3 6 10 15
print(gsub("[aeiou]", "-", "compiled r")) # [1] "c-mp-l-d r"
print(sprintf("%.3f", 2 / 3)) # [1] "0.667"
print(substr("bytecode", 1, 4)) # [1] "byte"
Command-line flags
| Flag | Effect |
|---|---|
FILE | Run a .R script. |
-e SRC | Run a one-liner. |
--repl | Interactive REPL on a persistent host — a function defined at one prompt completes at the next. |
--dap | Debug Adapter Protocol over stdio: handshake, launch, and run to completion with stdout forwarded as output events. Stepping is a later wave. |
--lsp | Language Server Protocol over stdio (diagnostics, hover, completion). |
--build FILE | Ahead-of-time compile the script's bytecode into the on-disk cache. |
--aot FILE | Ahead-of-time compile the script to a standalone native .fvm executable (override the path with -o OUT). |
-o OUT | Output path for --aot (default: the script's name with a .fvm extension). |
--dump-tokens FILE | Print the lexer token stream. |
--dump-ast FILE | Print the parsed AST. |
--disasm FILE | Disassemble the lowered fusevm chunk. |
Status & roadmap
The table below reflects the current state of the tree. The honest known-gaps list lives in BUGS.md; nothing is faked as working, and calling an unimplemented primitive raises could not find function.
| Component | State | Notes |
|---|---|---|
| Lexer / parser → AST | Implemented | Full ?Syntax precedence ladder, %op%, native pipe (src/lexer.rs, src/parser.rs). |
| AST → fusevm bytecode lowering | Implemented | No local VM; native jumps and integer loop counters (src/compiler.rs). |
| Vector heap & attributes | Implemented | Logical/integer/double/character/list with NA, recycling, promotion; names/dim/class/attr (src/host.rs). |
All four index forms + [[, $, m[i, j] | Implemented | Positive, negative, logical (recycled), character, and the m[cbind(i, j)] matrix subscript (read and assign, any rank). |
| Assignment & replacement functions | Implemented | <-, =, ->, <<-, growing assignment, nested targets, user-defined `f<-`. |
Functions, closures, ..., arg matching | Implemented | Defaults referring to other arguments; exact/partial/positional matching; lexical closures. |
| Control flow | Implemented | if as an expression, for/while/repeat, break/next, short-circuit &&/||. |
S3 (UseMethod, class, inherits) | Implemented | Class-vector walk, implicit classes, .default fallback. |
| Primitive library (133) | Implemented | Apply family, string/regex, numeric summaries, sequence/set, matrix helpers (src/builtins.rs). |
| R's printing rules | Implemented | [n] prefixes with 80-column wrapping, shared decimal widths, named-vector column pairs, matrix and list layout. |
Standalone Rscript binary + REPL | Implemented | Files, -e one-liners, persistent-host REPL (src/main.rs, src/repl.rs). |
| rkyv bytecode script cache | Implemented | Versioned shard at ~/.rlang/scripts.rkyv (src/cache.rs). |
Native .fvm AOT executable (--aot) | Implemented | Emits a fusevm object and links it against the rlang runtime staticlib into a standalone binary; user closures embedded in the chunk name table (src/aot.rs, src/aot_runtime.rs). |
Inline-Rust FFI (.rust / .Call) | Implemented | .rust(code) compiles a self-contained Rust block to a cached cdylib; .Call(name, …) invokes its exports, marshalling length-1 vectors to i64/f64/string and back (src/ffi.rs). |
| wasm32 build | Implemented | The same crate builds for wasm32-unknown-unknown on the bare fusevm interpreter and exports rlang_eval / rlang_alloc / rlang_free for a web-worker host (src/wasm.rs). |
Parity harness vs reference Rscript | Implemented | 182-snippet differential corpus plus 8 runnable examples, frozen and replayed in CI with no R installed. |
DAP adapter (--dap) | Partial | Handshake, launch, and run to completion with program stdout forwarded as output events; stepping and breakpoints are a later wave (src/dap.rs). |
LSP server (--lsp) | Implemented | Diagnostics, hover, completion over stdio (src/lsp.rs). |
| AOP call intercepts | Partial | Glob-matched before/after/around registry is live and tested; the dispatcher does not consult it yet (src/intercepts.rs). |
| Promises / non-standard evaluation | Planned | Arguments evaluate eagerly, so substitute(), quote(), match.call(), and formulas are absent. |
Condition system (tryCatch, restarts) | Done | tryCatch/try/on.exit/finally with handlers selected by condition class, plus restarts: withRestarts/invokeRestart/computeRestarts and the built-in muffleWarning/muffleMessage, so withCallingHandlers resumes at the signalling point. A warning, an error, and the condition object itself all carry the call they were raised in, under R's own context rules. |
Data frames, complex numbers, solve/det/eigen | Planned | No native data.frame (delegated to the embedded GNU R) and no complex numbers. Factors (ordered included), apply over margins, %*%, outer and cbind/rbind do work. |
Parity harness
Behaviour is checked against the reference Rscript by a differential harness rather than a self-recorded baseline. cargo run --bin parity diffs the 182-snippet corpus (tests/data/parity_corpus.R) live against the system R; tests/parity.rs replays the frozen outputs in CI with no R installed. The 8 programs in examples/ double as tests — they embed stopifnot assertions and tests/examples.rs runs each through the binary asserting a clean exit and stdout matching the frozen reference output. Corpus and examples are byte-identical to GNU R 4.6.1.
Why rlang
Compiled, not tree-walked
GNU R walks a node tree in C; rlang lowers R to bytecode and JITs hot paths through Cranelift — the same architecture that makes zshrs and stryke fast.
One shared engine
Bug fixes and JIT improvements in fusevm benefit zshrs, stryke, awkrs, elisp, rubylang, and rlang at once.
Differential, not self-graded
Every corpus snippet is diffed against the reference Rscript, and an unimplemented primitive raises rather than silently returning something plausible.
Editor-ready
Ships an LSP server over stdio and a REPL on a persistent host, where a function defined at one prompt completes at the next.
Deploys three ways
--aot links a standalone native .fvm executable, .rust / .Call bridge into cached inline-Rust cdylibs, and the same crate builds for wasm32 to run R in a web worker.
Building from source
rlang builds as a standalone Rust crate (it is not a workspace member of the meta repo):
# clone git clone https://github.com/MenkeTechnologies/rlang cd rlang # build (debug) cargo build # run a script, a one-liner, or the REPL ./target/debug/Rscript script.R ./target/debug/Rscript -e 'print(sum(1:100))' ./target/debug/Rscript --repl # run tests cargo test
fusevm 0.26.0 is pulled from crates.io with the jit, jit-disk-cache, aot, and ffi features. A zsh completion for Rscript ships in completions/_Rscript.
License
rlang is MIT licensed — free and open source. See LICENSE.
Repository & links
- Engineering report — report.html (architecture, fusevm hosting, roadmap, dependency posture)
- Primitive reference — reference.html (the primitive library)
- Known gaps — BUGS.md
- Source — github.com/MenkeTechnologies/rlang
- Issues — github.com/MenkeTechnologies/rlang/issues
- The shared VM — fusevm (also behind
zshrs,stryke,awkrs,elisp,rubylang)