Skip to content
ry

Strict mode

By default, ry checks what it can and lets the rest through as Unknown, so a clean ry check does not tell you how much was actually checked. Strict mode turns that around: every value ry cannot type, and every parameter nothing constrains, is an error. A clean run then means every value in the code has a known type, which is what you want before you let an agent change it. Strict mode is a check, so it needs nothing from ry’s runtime: it works on plain R with #: comments, run on GNU R.

[check]
strict = true

Strict mode reports type errors too, so it needs no typing = true beside it. A # typing: strict comment turns it on for a single file instead.

Strict mode also guards running code. ry run and ry R print the errors and exit with status 1 instead of running a script that has one, ry load does the same for a package whose R/ files have one, and the console refuses each command that has one:

x strict mode found 1 error, so this code does not run

ry load checks only the package’s own files, so an error in a test or a script fails ry check but does not stop the load.

A parameter with no type, whose use in the body does not determine one, accepts any value unchecked:

describe <- function(x) cat("a value\n")
strict
x strict mode: parameter `x` has no type. How the function uses it does not determine one, so any value is accepted unchecked. Annotate it, as in `x: TYPE`

A function such as function(x) x is not reported, because it is generic: it works for any x and returns the same type.

An expression whose type ry could not determine is reported where it appears:

total <- function(df: data.frame) -> double sum(df$amount)
strict
x strict mode: this expression has an undetermined type (`Unknown`)

ry reports it on df$amount, and again on the sum() call that receives it.

An unresolved name, normally a warning, becomes an error.

A DataFrame query that names a column of a frame whose columns are unknown is reported, because nothing checks those names and types. A chain of verbs is reported once, at its first step that names a column:

#: fn(path: character) -> DataFrame
load_sales <- function(path) {
sales <- DataFrame.read_csv(path)
sales |> DataFrame.filter(col("units") > 1L)
}
strict
x strict mode: the columns of `sales` are unknown, so the column names and types this query uses are not checked. Give `sales` a type that lists its columns, as in `sales: DataFrame{units: TYPE}`

Annotating sales with its columns, or reading a file the project has with a literal path, fixes it.

In strict mode, the top level of a file in R/ may only define things: functions, types, imports, classes, and constants. Loading the package then binds names and registers classes and has no other effect, so ry load can bind constants lazily too. Any other statement is an error that says where the code belongs:

#: fn(x: double[]) -> double[]
scale <- function(x) x * 100
rates <- c(0.01, 0.05)
scaled <- scale(rates)
cat("tally loaded\n")
strict
x strict mode: this calls `scale`, which is not declared pure, so it runs every time the package loads. A file in `R/` may only define things at its top level: move the call into a function,
| `.onLoad`, or the script `ry run` starts, or mark `scale` `@pure` if calling it has no effect
strict
x strict mode: this calls `cat`, which is not declared pure, so it runs every time the package loads. A file in `R/` may only define things at its top level: move the call into a function,
| `.onLoad`, or the script `ry run` starts

rates is fine, because c() is pure: calling it has no effect beyond returning a value. A constant may call only pure functions. ry never guesses that a function is pure, so you declare it with @pure, and move the rest into .onLoad:

scale <- @pure function(x: double[]) -> double[] x * 100
rates <- c(0.01, 0.05)
scaled <- scale(rates)
.onLoad <- function(libname: character, pkgname: character) -> NULL {
cat("tally loaded\n")
}
$ ry load . -e 'print(scaled)'
tally loaded
Loaded tally: 4 definitions bound lazily, 0 top-level statements run, in 10 ms.
[1] 1 5

ry trusts @pure without checking the body. Scripts, test files and the console have no such restriction. The reference lists every allowed form.

An annotation clears the finding, because the value now has a type. A value declared Any, or a call that returns Any (readRDS(), eval(), and S3 generics that dispatch through UseMethod()), is a deliberate opt-out, so strict mode does not report it. A call to stop() never returns, so it types as Any too, and a guard such as if (x < 0) stop("negative") is not reported.

Most of what strict mode asks for is already known to inference: it only needs writing down. ry check --annotate writes the inferred type of each unannotated top-level function and binding, and then reports what is left, which is the code a person has to type. Take this file:

area <- function(width, height = 1) width * height
unit <- 1L
label <- function(shape) shape$name
area <- function<T: numeric>(width: T, height: T = 1) -> T width * height
unit: integer <- 1L
label <- function(shape) shape$name
x strict mode: parameter `shape` has no type. How the function uses it does not determine one, so any value is accepted unchecked. Annotate it, as in `shape: TYPE`
...
added 2 inferred annotations
2 problems in 1 file

The annotations are inline unless the project sets inline-annotations = false, in which case they are #: comments. Nothing constrains shape, so inference has no type to write and the finding stays for you. A binding that already has an annotation is left alone, so running the command again changes nothing. The editor offers the same edit for one binding, including a local inside a function, and for the whole file.