// RLANG — R IN RUST

rlang v0.1.6 · R on fusevm · lex/parse → AST → bytecode → Cranelift JIT · a fusevm language host (with zshrs, stryke, awkrs, elisp, rubylang) · MIT · in active development

Report GitHub Issues
// Color scheme

>_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

FlagEffect
FILERun a .R script.
-e SRCRun a one-liner.
--replInteractive REPL on a persistent host — a function defined at one prompt completes at the next.
--dapDebug Adapter Protocol over stdio: handshake, launch, and run to completion with stdout forwarded as output events. Stepping is a later wave.
--lspLanguage Server Protocol over stdio (diagnostics, hover, completion).
--build FILEAhead-of-time compile the script's bytecode into the on-disk cache.
--aot FILEAhead-of-time compile the script to a standalone native .fvm executable (override the path with -o OUT).
-o OUTOutput path for --aot (default: the script's name with a .fvm extension).
--dump-tokens FILEPrint the lexer token stream.
--dump-ast FILEPrint the parsed AST.
--disasm FILEDisassemble 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.

ComponentStateNotes
Lexer / parser → ASTImplementedFull ?Syntax precedence ladder, %op%, native pipe (src/lexer.rs, src/parser.rs).
AST → fusevm bytecode loweringImplementedNo local VM; native jumps and integer loop counters (src/compiler.rs).
Vector heap & attributesImplementedLogical/integer/double/character/list with NA, recycling, promotion; names/dim/class/attr (src/host.rs).
All four index forms + [[, $, m[i, j]ImplementedPositive, negative, logical (recycled), character, and the m[cbind(i, j)] matrix subscript (read and assign, any rank).
Assignment & replacement functionsImplemented<-, =, ->, <<-, growing assignment, nested targets, user-defined `f<-`.
Functions, closures, ..., arg matchingImplementedDefaults referring to other arguments; exact/partial/positional matching; lexical closures.
Control flowImplementedif as an expression, for/while/repeat, break/next, short-circuit &&/||.
S3 (UseMethod, class, inherits)ImplementedClass-vector walk, implicit classes, .default fallback.
Primitive library (133)ImplementedApply family, string/regex, numeric summaries, sequence/set, matrix helpers (src/builtins.rs).
R's printing rulesImplemented[n] prefixes with 80-column wrapping, shared decimal widths, named-vector column pairs, matrix and list layout.
Standalone Rscript binary + REPLImplementedFiles, -e one-liners, persistent-host REPL (src/main.rs, src/repl.rs).
rkyv bytecode script cacheImplementedVersioned shard at ~/.rlang/scripts.rkyv (src/cache.rs).
Native .fvm AOT executable (--aot)ImplementedEmits 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 buildImplementedThe 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 RscriptImplemented182-snippet differential corpus plus 8 runnable examples, frozen and replayed in CI with no R installed.
DAP adapter (--dap)PartialHandshake, 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)ImplementedDiagnostics, hover, completion over stdio (src/lsp.rs).
AOP call interceptsPartialGlob-matched before/after/around registry is live and tested; the dispatcher does not consult it yet (src/intercepts.rs).
Promises / non-standard evaluationPlannedArguments evaluate eagerly, so substitute(), quote(), match.call(), and formulas are absent.
Condition system (tryCatch, restarts)DonetryCatch/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/eigenPlannedNo 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