Skip to content

Why ry

R has good tools, but they are separate tools. Linting, formatting, style, analysis and running the code are five programs that each parse your source and each build their own partial picture of it. None of them shares what it learned with the others, so each one starts over.

R’s language servers know what your values are because they ask a live R session. That is what makes completion on a fitted model work in RStudio.

It is also the limit: a session knows only code that has already run, in the state it happens to be in. It cannot describe the branch you have not taken, the function nobody called, or the file you just opened. ry answers from the source alone, so the answers exist in a pull request, in CI, and in a file you have never run:

  • a typo in a variable name
  • an argument in the wrong position, or a call missing a required one
  • a value that is sometimes NULL, used as though it never is
  • a name you deleted in another file

All of these come from one understanding: formatting, analysis, editor features and type checking are views onto the same knowledge.

Latency matters most on large R projects, where a check slow enough to interrupt your work stops being run at all.

ry is written in Rust, and analysis is incremental: an edit re-checks only what that edit could have affected, not the project. It is tested against roughly 970,000 lines of real R — 69 CRAN packages plus R’s own base library — and the check that an edit does not trigger more work than it should runs on every change.

check and fmt never load R and never execute your code, which is what makes them safe in CI and fast in an editor. The one exception is the R console, which runs R by definition.

Python has type hints, JavaScript got TypeScript, Ruby has RBS, Elixir is adding set-theoretic types. Each stayed dynamic, kept types optional, and adopted them because finding type errors by running the program stops scaling long before the codebase does.

R code is full of implicit type expectations, and nothing checks them until the code runs.

ry’s approach rests on inference: the checker works out types from how values are used, instead of requiring you to declare them.

scale <- function(x, factor) x * factor

* is arithmetic, so both parameters are numbers. Nothing was declared, and scale("a", 2) is already an error. This is why most R needs no annotations at all. The ones you do write live in #: comments, so the file stays ordinary R that every other tool reads, and type checking is opt-in, so you can adopt it one file at a time.

The inference is Hindley–Milner, which is sound and close to linear on real code. That choice excludes two features R programmers might expect — class hierarchies and overloading in your own functions — because a type system that admits them can spend an unbounded amount of time on a single expression, which an editor cannot afford. R is dynamic enough that some constructs cannot be described statically at all; those become Unknown, which is compatible with everything, so a gap means a check was skipped rather than a wrong answer produced.

The project has alpha level quality.

The interfaces are stable. The diagnostics, the ry.toml keys, the diagnostic codes, and the JSON output are covered by tests that fail when they change, so CI built on them will not break silently.

The type system is still gaining capability. A new release may report findings an older one did not, so pin a version if you gate a build on a clean run.

Where it runs. ry check reads .R files and the R chunks of .Rmd, .qmd, and .Rnw documents. The editor integration does not cover literate documents yet — you get them in check and in CI, but not as you type. The formatter skips them, since most of an .Rmd is prose the formatter should not rewrite.

What it does not cover yet. The largest gaps are data frames, S4, and R6 — see limitations for the full account before deciding how far to trust a clean run.

  • Features — what you get, before you turn anything on
  • Tutorial — the type checker on real code