Skip to content
ry

Unknown values

R has constructs no static checker can follow, such as data frame columns and S4 and R6 objects. Their values are Unknown, which is compatible with everything. ry would rather skip a check than report something false, so one unmodeled construct never causes a cascade of errors.

The cost is that a clean run does not say how much was checked. Strict mode reports the places a value became Unknown.

An annotation gives an Unknown value a type, so every later use is checked against it:

#: double
amount <- df$amount

A plain annotation is checked against the value. Two forms give a type without that check, and inline they go between a binding’s : and its type, as in amount: @if-unknown double <- df$amount.

#: @if-unknown TYPE types an Unknown value, such as a data frame column, and becomes an error once ry can infer the type, so it never goes stale:

#: @if-unknown double
amount <- df$amount
#: @if-unknown double
n <- 1.5
type-mismatch
x `@if-unknown` applies only where the type is unknown, and this is already `double`. Drop the annotation, or use `#:` to check the type or `@trust` to override it

A value declared Any, such as the result of readRDS(), takes a plain annotation instead. #: @trust TYPE overrides a type ry did infer, for the cases where you know better.

Any is also compatible with everything, but it is a deliberate choice rather than a gap. A value declared Any, file reads such as readRDS(), eval(), and calls to S3 generics that dispatch through UseMethod() are Any, and strict mode does not report them.

data.frame() returns a data.frame, but a base data frame’s columns are not typed, so df$amount is Unknown. An annotation such as df: data.frame checks that a data frame arrives, but not which columns it has. ry’s built-in DataFrame is the typed alternative, with every column, query and join checked. For analysis code this is the gap that matters most, and a column read such as df$amount is usually the first place strict mode reports.

The checker never runs R, so it cannot ask a package what it exports. It reads stubs instead: declaration files that list a package’s names and their types, the way TypeScript reads .d.ts files.

Base R and its default packages are typed, and so are data.table, dplyr, ggplot2, and testthat. For the rest of R’s own packages, the tidyverse, and knitr, rlang, glue, magrittr, scales, jsonlite, and R6, ry ships export lists: the names without their types, which is enough to keep unresolved-name checks working.

Bare names from base R’s default packages always resolve. Every other package counts once your project uses it, through a library() call, a DESCRIPTION dependency, or a NAMESPACE import. Until then mutate is unresolved, as it would be in R.

Attaching a package with no stub means any bare name in the project might be one of its exports. ry cannot tell a typo from an export it has never heard of, so it stops reporting unresolved names across the whole project:

library(dbclient)
reslt <- 1
print(result) # not reported

A stub turns the check back on. Put it under stubs/, named after the package:

stubs/dbclient.Rtypes
@type Session
connect : fn(host: character) -> Session

Now the typo is reported, and with type checking on, so are calls that do not fit the declarations:

library(dbclient)
session <- connect(42) # expected `character`, found `double`
print(result) # I could not resolve `result` in this package, its imports, or builtins.
dbclient::conect("db") # `conect` is not exported by `dbclient`.

Declare only what you call: a name you leave out is reported wherever you use it, which tells you what to add next. Even an empty stub file turns the unresolved-name check back on.

A declaration under stubs/ replaces a shipped one with the same name, so you can fix a wrong return type, or add a function your version of the package has, without waiting for a release. ry check reports every line it could not load. Authoring stubs has the full format, including overloads, data-masking functions, and @pure for functions that strict mode lets a package constant call.

Most gaps are silent, because a value ry cannot type becomes Unknown and its checks are skipped. Strict mode shows most of the places they occur.

  • Data frame columns. Everything read out of a base data frame is Unknown, so df$whatever passes even if the column does not exist. A DataFrame types its columns.
  • Matrix dimensions. Matrix arithmetic is checked, but a non-conformable product is not. Indexing with several indices, as in m[i, j], or with drop = FALSE gives Unknown.
  • S4 and R6 objects are Unknown, and calls to an S3 generic that dispatches through UseMethod() return Any. Operator methods such as +.Date and methods called directly are checked. Custom types shows how to check who receives an object.
  • Packages without a stub. Their names are Unknown, and attaching one switches off unresolved-name reports for the whole project.
  • $ on an atomic vector in a package that defines a $ method. A classed vector may carry that method, so the read is Unknown instead of an error.
  • Some paths a test elsewhere in the body rules out. A list member beside a number in arithmetic, a list or NULL beside an operand of unknown type, the NULL of a computed list field used as a : endpoint, and the logical member in the false branch of isTRUE() or isFALSE() are not reported, because correct R usually guards them in ways ry cannot follow.
  • A placeholder local. A variable whose only visible value is NULL or a bare NA, filled later by assign() or <<-, is not checked where it is passed as an argument or tested as a condition.
  • A project’s own %op% operators, because one may quote its right operand instead of evaluating it, as magrittr’s %>% does.

These are known bugs, not design decisions:

  • A bare NA under a declared return type. A function declared -> character that returns NA on one path is reported, because NA is logical. Return NA_character_ instead.
  • A trailing comma for rlang’s dynamic dots. dplyr’s verbs accept mutate(d, y = 1, ), but ry reports the trailing comma as it would for c(1, 2, ).
  • Package-qualified calls to a package ry does not know. pkg::fun() reports an unknown namespace when ry has no stub or export list for pkg, even if a file calls library(pkg).
  • A generic function passed inline as an argument. lapply(xs, function<T>(v: T) -> T v) is reported, because type parameters on a function that is not bound to a name are not instantiated. Leave the function’s types off, or bind it to a name first.