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 = trueStrict 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 runry 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.
What it reports
Section titled “What it reports”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)#: fn(df: data.frame) -> doubletotal <- function(df) 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) -> DataFrameload_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.
Package files only define things
Section titled “Package files only define things”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` startsrates 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")}#: @pure#: fn(x: double[]) -> double[]scale <- function(x) x * 100
rates <- c(0.01, 0.05)scaled <- scale(rates)
#: fn(libname: character, pkgname: character) -> NULL.onLoad <- function(libname, pkgname) { cat("tally loaded\n")}$ ry load . -e 'print(scaled)'tally loadedLoaded tally: 4 definitions bound lazily, 0 top-level statements run, in 10 ms.[1] 1 5ry trusts @pure without checking the body. Scripts, test files and the console have no such
restriction. The reference lists every allowed
form.
Clearing a finding
Section titled “Clearing a finding”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.
Moving a file to strict mode
Section titled “Moving a file to strict mode”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 * heightunit <- 1Llabel <- function(shape) shape$namearea <- function<T: numeric>(width: T, height: T = 1) -> T width * heightunit: integer <- 1Llabel <- function(shape) shape$name#: <T: numeric> fn(width: T, [height]: T) -> Tarea <- function(width, height = 1) width * height#: integerunit <- 1Llabel <- 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 annotations2 problems in 1 fileThe 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.