the first type checker for R · written in rust

Modern developer tooling for R.

Type checker, language server, and formatter in one binary. Catch errors before you run the code — without changing the R you already write.

three tools, one binary

Three tools, one binary.

Built for R users who have outgrown RStudio: editor smarts, problem-catching analysis, and one consistent style from a single binary. R has world-class statistics — Roughly brings the IDE-grade tooling other ecosystems already take for granted, fast enough to scale past 300k lines.

Language server

Completion, hover, go-to-definition, references, rename, and inlay hints — live in your editor.

Language server

Code analyzer

Lint diagnostics on by default — plus the first static type checker for R, opt-in.

Type checker

Formatter

roughly fmt rewrites your whole project to one consistent style, with a --diff mode for CI.

Formatter

write

Your editor, fluent in R.

One server behind every editor feature — complete a record's fields, hover a name for its inferred signature, jump to where it is bound, rename it everywhere at once. Extensions for VS Code and Zed, plain LSP for the rest.

sales.R report.R roughly
# one sale, and what is left of it after fees
sale: list{region: character, amount: double, closed: logical} <- list(region = "north", amount = 1240.5, closed = TRUE)
margin: <T: numeric, U: numeric> fn(gross: T, rate: U) -> double <- function(gross, rate) gross * (1 - rate)
net: double <- margin(sale$amount, 0.18)
label: character <- paste0(sale$region, ": ", round(net, 2))
message(label)
3 references
R/sales.R4:1margin <- function(gross, rate) gross * (1 - rate)
R/sales.R6:8net <- margin(sale$amount, 0.18)
R/report.R2:28paste0(sale$region, " ", margin(sale$amount, rate))
The fields of a record type, each with its inferred type. The inferred type, and where the name is bound. Jump to the binding — in this file, another file, or a stdlib stub. Every read and write of the name, across the whole workspace. One rename, applied everywhere the name is bound. Inferred types shown inline. Nothing was annotated here.

Language server docs Also: signature help, document and workspace symbols, folding ranges, semantic highlighting of #: annotations, and format-on-save.

check

Catch it before you run it.

roughly check lints out of the box. Switch the type checker on and it reads your code the way a compiler would — and since inference runs whether or not you annotate anything, it catches the mistake either way.

R/sales.R no configuration
read_sales <- function(path) {
read.csv(path, stringsAsFactors = FALSE)
}
top_regions <- function(sales, n) {
ranked = sales[order(-sales$amount), ]
head(unique(ranked$region), n)
}
report <- function(sales, regions) {
message(nrwo(sales), " rows in ", length(regions), " regions")
invisible(regions)
}
sales <- read_sales("sales.csv")
regions <- top_regions(sales, 5)
verbose <- T
if (verbose) report(sales, regions)
$ roughly check
warning[assignment-operator]: Use <-, not =, for assignment
--> R/sales.R:6:10
6 | ranked = sales[order(-sales$amount), ]
^
warning[unresolved]: I could not resolve `nrwo` in this package, its imports, or builtins. Did you mean `nrow`?
--> R/sales.R:11:11
11 | message(nrwo(sales), " rows in ", length(regions), " regions")
^^^^
warning[boolean-shorthand]: Use TRUE, not T, for Boolean values
--> R/sales.R:18:12
18 | verbose <- T
^
3 problems in 1 file
Lints and unresolved names need no setup. Each diagnostic names the code you would put in a # roughly: allow(...) comment. exit 1

Type checker guide Linter rules Type checking is opt-in; lints are not. roughly check reads .R plus the R chunks of .Rmd, .qmd and .Rnw files.

ship

One consistent style, in place.

roughly fmt is deliberately non-invasive. It fixes spacing, braces and indentation, and it never splits a line you chose to keep on one line. Run it across the project, or use --diff in CI.

totals.R operators, commas, quotes
as written
totals<-sapply(sales,function(s)s$amount)
top<-sales[totals>threshold ,]
message(paste0('kept: ',length(top),' of ',length(sales)))
roughly fmt
totals <- sapply(sales, function(s) s$amount)
top <- sales[totals > threshold, ]
message(paste0("kept: ", length(top), " of ", length(sales)))
$ roughly fmt --diff totals.R 1 file would be reformatted, 0 files already formatted

Formatter docs --check and --diff exit 1 when a file would change, so CI stays honest. # fmt: skip and # fmt: off opt code out.

adopt

Drop it onto your R project.

Nothing about your existing R has to change. Install the VS Code extension in one click — no toolchain required — or grab the CLI and point Roughly at your project.

# install the CLI
cargo install --git https://github.com/felix-andreas/roughly roughly
# format, lint, and type-check
roughly fmt .
roughly check .

Grab the VS Code extension from the Marketplace, then tune behavior in configuration.