Skip to content

Working in the R console

ry repl gives you an R prompt whose Tab completion comes from ry’s type checker, and ry run executes a script through the same session.

ry does not reimplement R. It finds the R already installed on your machine, loads it into its own process, and hands control to R’s real main loop. Everything R does, R still does: parsing, evaluation, autoprinting, error messages, browser(), readline(), S4 dispatch. Your .Rprofile is sourced. Packages install and attach normally.

What ry replaces is the line editor in front of R. That is where the difference shows up:

$ ry repl
... R's own startup banner ...
ry R console — R at /usr/lib/R (q() or Ctrl-D quits)
> account <- list(holder = "ada", balance = 120.5)
> account$
balance double
holder character

R’s own console can complete account$balance because by the time you press Tab the object exists in memory. ry reaches the same answer by type-checking what you have typed so far, which also lets it show that balance is a double, complete full signatures, and complete names inside #: annotations. The trade-off: completion never inspects the live R session.

The console is also the only part of ry that needs R at all — see What needs R.

Terminal window
ry repl # start a session
ry repl --keybindings vi # vi editing instead of emacs
ry repl -f setup.R # run a script first, then hand you the prompt

ry locates R in two steps:

  1. R_HOME, if it is set and non-empty.
  2. Otherwise it runs R RHOME using your PATH and takes the answer.

Whatever it settles on is exported back as R_HOME before R starts, so R.home() inside the session agrees with the banner line — which is how you check at a glance which installation you got. On Unix, R_SHARE_DIR, R_INCLUDE_DIR and R_DOC_DIR are recovered from R’s own bin/R script too, so R.home("share") stays correct on distributions that relocate those directories.

To pin a specific R, set R_HOME for the one command:

Terminal window
R_HOME=/opt/R/4.5.1/lib/R ry repl

Startup problems fail immediately and say what to do, with exit status 2:

$ ry run analysis.R
error: no R found: R_HOME is not set and running `R RHOME` failed (No such file or directory (os error 2)). Install R or set R_HOME to an R installation.
$ R_HOME=/nonexistent ry run analysis.R
error: R_HOME is set to /nonexistent, but that directory does not exist

One hard requirement beyond “R is installed”: it must have been built as a shared library (--enable-R-shlib). Every CRAN binary distribution is. If yours is not, ry says so and names the directory it looked in.

R 4.2 or newer is what this is developed and tested against, and it is required on Windows. Nothing checks the version, so an older R will simply fail at load time with a missing-symbol error rather than a clear message.

Sessions start clean and leave nothing behind: no .RData is restored on the way in, and nothing is saved on the way out. ry repl also needs a terminal — piping into it does nothing useful, since the editor cannot run and the session immediately sees end of input. Use ry run for anything non-interactive.

Press Tab and ry type-checks the session so far — every line you have accepted, plus the line you are editing — then offers what fits at the cursor. The right-hand column is the type it inferred.

> nchar
nchar fn(x: Any, [type]: character, [allowNA]: logical, [keepNA]: logical) -> integer
nzchar fn(x: Any, [keepNA]: logical) -> logical
getDLLRegisteredRoutines.character From the `base` package.

The first two have typed stubs shipped with ry, so you get the full signature and which arguments are optional ([type]). The third is a real base export with no stub yet, so you get the name and its package. Tab again to cycle, Enter to accept.

Six kinds of completion are available:

You typeYou get
a bare nameSession bindings you defined, standard-library functions, R reserved words
stats::rnorThat namespace’s exports, with signatures where a stub exists
account$Fields of the record type ry inferred, each with its own type
obj@S4 slot names already spelled after @ earlier in the session — not read from setClass
#: charaType names — inside an annotation comment the completer switches from values to types
x[["Record fields, when the string subscripts a record

Anything you have defined is available on the next line, and anything reachable through :: is available whether or not the package is attached:

> monthly_rate <- function(annual) annual / 12
> monthly
monthly_rate
> stats::rnor
rnorm fn(n: Any, [mean]: Any, [sd]: Any) -> double[]
rlnorm fn(n: Any, [meanlog]: Any, [sdlog]: Any) -> double[]
order.dendrogram

Matching is fuzzy and smart-case: exact beats prefix, prefix beats substring, substring beats subsequence (which needs three characters). Names you defined outrank standard-library names.

Completion is static analysis over the session transcript, and only that transcript. Each of these looks like a bug the first time:

  • Objects R knows about but ry never saw you type. Anything created by source() or defined in a file you passed to -f is invisible to Tab. ry repl -f setup.R feeds the script straight to R, so compound() from that file runs fine but does not complete. Paste the definition into the prompt if you want it completed.
  • Your project’s files and ry.toml. The console analyzes one document — the session. It does not read your working directory, your project sources, or your stubs/*.Rtypes overrides. Use the language server for typed work inside project files.
  • Third-party packages. Only the standard library is available. library(dplyr) attaches dplyr in R as usual, but dplyr::muta + Tab still offers nothing typed — activating those stubs needs project metadata that a console session does not have.

The editor is ry’s, and it behaves the way a modern shell does.

KeyEffect
TabOpen the completion menu; Tab again cycles candidates
Ctrl-RReverse search through history
Up / DownWalk history
Ctrl-CAt the prompt, clear the line. During evaluation, interrupt it
Ctrl-DEnd the session (same as q())

History is persistent and shared across sessions — the last 1000 entries, in ~/.local/share/ry/history.txt on Linux, ~/Library/Application Support/ry/ on macOS. It is ry’s own file, separate from R’s .Rhistory. As you type, the greyed-out text ahead of the cursor is a suggestion from that history; Right arrow accepts it. Input is syntax-highlighted by the same lexer ry check uses — keywords blue, constants like TRUE purple, strings green, numbers cyan, comments grey.

Multi-line input is one editable buffer, not a sequence of lines you can no longer reach:

> compound <- function(principal, rate) {
+ principal * (1 + rate)
+ }
> compound(100, 0.05)
[1] 105

ry decides a line is incomplete from the syntax alone — an open bracket, a trailing operator, a dangling comma, an unclosed string. It is deliberately conservative: if it guesses “complete” and R disagrees, R asks for the rest with its own + prompt and nothing is lost.

Every prompt R produces passes through unchanged, because ry does not render prompts of its own. So debugging works exactly as you expect:

> f <- function(x) { browser(); x * 2 }
> f(21)
Called from: f(21)
Browse[1]> x
[1] 21
Browse[1]> c
[1] 42

Interrupts are cooperative, the same as in stock R: Ctrl-C sets a pending flag that R honors at its next check point. A long R loop stops promptly and drops you back at the prompt:

> for (i in 1:100000000) x <- sqrt(i)
^C
> 1 + 1
[1] 2

Compiled code that never checks for interrupts will not respond — again, exactly as in stock R.

ry run FILE is the same embedded session with the editor removed: the file is fed to R, R evaluates it, and the process exits.

$ cat report.R
rates <- c(0.02, 0.035, 0.05)
cat("mean rate:", mean(rates), "\n")
$ ry run report.R
... R startup banner ...
mean rate: 0.035
$ echo $?
0

An error at top level halts the script and fails the run — the same halt-on-error contract Rscript gives you:

$ ry run report.R # cat("before\n"); stop("rate table is empty"); cat("after\n")
before
Error: rate table is empty
$ echo $?
1

after never printed. Parse errors behave the same way: output before the bad line is kept, the run stops there, and the status is 1. An explicit q(status = 7) in your script passes straight through. For the full status table, see the CLI reference.

Four things to know before you reach for it as an Rscript replacement:

  • ry run does no analysis. No type checking, no findings, no diagnostic codes — the bytes go to R. Run ry check if you want the script checked.
  • interactive() returns TRUE. The embedded session is always an interactive one, which is a real divergence from Rscript. Code that branches on interactive() takes the other path.
  • R’s startup banner is printed, and your .Rprofile is sourced. There is no quiet or vanilla mode.
  • If your script sets its own options(error = ...), it replaces the handler that produces the failing exit status, and a failing script may then exit 0.

ry repl -f FILE uses the same feeding mechanism but keeps you at the prompt afterwards, which is the fastest way to load a scratch setup and start poking at it.

Only the console does. Everything else in ry is self-contained: type information for the standard library is compiled into the binary, not read from an R installation.

CommandNeeds R installed?
ry checkNo
ry fmtNo
ry server (the language server)No
ry replYes — at runtime
ry runYes — at runtime

This is why continuous integration for a ry project needs no R at all; see Continuous integration.