# 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.

```toml
[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:

```text
  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.

## What it reports

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

```r
describe <- function(x) cat("a value\n")
```

```text
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:

### Inline

```r
total <- function(df: data.frame) -> double sum(df$amount)
```

### Comments

```r
#: fn(df: data.frame) -> double
total <- function(df) sum(df$amount)
```

```text
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:

```r
#: fn(path: character) -> DataFrame
load_sales <- function(path) {
  sales <- DataFrame.read_csv(path)
  sales |> DataFrame.filter(col("units") > 1L)
}
```

```text
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

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:

```r
#: fn(x: double[]) -> double[]
scale <- function(x) x * 100

rates <- c(0.01, 0.05)
scaled <- scale(rates)
cat("tally loaded\n")
```

```text
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`:

### Inline

```r
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")
}
```

### Comments

```r
#: @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")
}
```

```text
$ 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](/reference/type-system.md#a-package-files-top-level) lists every allowed
form.

## 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

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:

```r
area <- function(width, height = 1) width * height
unit <- 1L
label <- function(shape) shape$name
```

### Inline

```r
area <- function<T: numeric>(width: T, height: T = 1) -> T width * height
unit: integer <- 1L
label <- function(shape) shape$name
```

### Comments

```r
#: <T: numeric> fn(width: T, [height]: T) -> T
area <- function(width, height = 1) width * height
#: integer
unit <- 1L
label <- function(shape) shape$name
```

```text
  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.
