> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canary.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Language reference

> The complete RSL 0.4.1 language on one page, in the order of a file: each form with its grammar, its rules and a checked example.

This page states what `rslc` reads as RSL 0.4.1, form by form. Each excerpt quotes a checked example rater.
[Names](https://docs.canary.effectiveai.app/rsl/reference/names.md) lists every section, type and annotation key, and
[Functions](https://docs.canary.effectiveai.app/rsl/reference/functions.md) explains every function.
The [language tour](https://docs.canary.effectiveai.app/rsl/language-tour.md) teaches the same forms in reading order.

Two synthetic raters from the carrier "Example Mutual" supply the excerpts: `auto` and `auto-extras`.
[Examples](https://docs.canary.effectiveai.app/rsl/reference/examples.md) shows both whole. Save and check them with these commands:

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc examples get auto
rslc examples get auto-extras
rslc check auto/rater.rsl
rslc check auto-extras/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
WROTE auto/enums/Territory.csv
...
PASS auto/rater.rsl (strict)
cost premium: 27 + 7*|Policy.drivers| + 25*|Policy.vehicles| (each list of size 1: 59, 3: 123, 10: 347)
PASS auto-extras/rater.rsl (strict)
cost premium: 21 (each list of size 1: 21, 3: 21, 10: 21)
```

## Find a form

Each row names something a rater or a guideline states, its shortest form and the section that defines it.

| To write | Form | Section |
| - | - | - |
| An input | `name: Type` | [Inputs](#inputs) |
| A follow-up input | `name: Type when condition` | [Follow-up inputs](#follow-up-inputs) |
| A list of records | `drivers: List(Driver) keyed by number` | [Lists and keys](#lists-and-keys) |
| A rule that refuses a request | `where condition [code: "CODE", message: "…"]` | [Rules](#rules) |
| A rate table | `Name: table dims (key: Type) -> { column: Type }` | [Declaration](#declaration) |
| A lookup | `lookup!(Table, key).column` | [`lookup!`](#lookup) |
| An interpolation | `interpolate!(Table, key, ratio: …, step: …, result: …).column` | [`interpolate!`](#interpolate) |
| A rounded premium | `round(x, 2, half_up)` | [Rounding](#rounding) |
| A minimum or maximum | `max(a, b)`, `min(a, b)` | [Functions](#functions) |
| A sum over a list | `sum(self.vehicles as v, v.premium)` | [Functions](#functions) |
| A date difference | `diff_years(later, earlier)` | [Dates](#dates) |
| A question | `[ask: "…"]` | [Questions and choices](#questions-and-choices) |
| A choice | `[choice, api_default: "…"]` | [Questions and choices](#questions-and-choices) |
| A guideline role | `Roles = { assistant < uw < manager }` | [Guidelines](#guidelines) |
| A term that each quote sets | `name: Type per quote` | [Terms of a quote](#terms-of-a-quote) |
| A referral | `refer name when condition to @Role.r because "…"` | [Guidelines](#guidelines) |

## Notation

Each grammar block uses this notation:

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rule = definition     a rule and what it stands for
"text"                the characters between the quotes, as written
A | B                 A or B
[ A ]                 A, or empty
{ A }                 A repeated: zero, one or more times
( A B )               a group
name                  an identifier that starts with a lower-case letter or "_"
Name                  an identifier that starts with an upper-case letter
string, number, date  a literal, as "Lexical elements" defines it
```

A comma separates the items of every list. Between two items of a section, a record, an `inputs` block or a step, the comma is optional.
`rslc fmt` writes one comma between items and leaves the last item bare.

## Files and versions

An RSL program is one entry file plus the files it imports. Three kinds of entry file exist:

| File | Holds | Read by |
| - | - | - |
| A rater, `rater.rsl` | Inputs, tables, calculations and outcomes | `rslc check`, `rslc export` and every rating host |
| A guideline, `guideline.rsl` | A rater it uses, roles, question owners and referrals | `rslc check` and `rslc submission` |
| A scenario | One set of inputs with expected values | `rslc eval` |

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
file    = { comment | item }
item    = spec | import | package | use
        | "Sources"        "=" "{" { source } "}"
        | "Types"          "=" "{" { type_decl } "}"
        | "Constants"      "=" "{" { constant } "}"
        | "Tables"         "=" "{" { table } "}"
        | "Geographies"    "=" "{" { geography } "}"
        | "Stages"         "=" "{" { stage } "}"
        | "Roles"          "=" "{" chain { "," chain } "}"
        | "Acts"           "=" "{" { act } "}"
        | "Records"        "=" "{" { record } "}"
        | "RatingOutcomes" "=" "{" outcome { outcome } "}"
        | "Scenario"       "=" "{" { scenario_part } "}"
comment = "#" { any character up to the end of the line }
```

* Sections stand in any order, and a name resolves wherever its declaration stands in the program. `rslc fmt` keeps the order you write.
* A file may repeat a section. The compiler reads the repeats as one section.
* Spaces and line ends only separate tokens.
* A comment starts with `#` and runs to the end of its line.
* `use`, `Roles` and `Acts` belong to a guideline. See [Guidelines](#guidelines).

### The `spec` block

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
spec   = "spec" "{" field { "," field } [ "," ] "}"
field  = name ":" value
value  = string | number | date | "true" | "false" | name | Name | "[" [ value { "," value } ] "]"
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
spec {
    rsl: "0.4.1",
    carrier: "Example Mutual",
    state: AZ,
    product: "Personal Auto (reference example)",
    version: "1.0.0",
    effective_date: 2026-01-01
}
```

| Field | Rule |
| - | - |
| `rsl` | Required. The RSL version of the file: `"0.4"` or `"0.4.1"`. `rslc fmt` writes it first. |
| `carrier`, `state`, `product`, `effective_date` | Required and filled, or the check reports [`E-RATER-METADATA`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/header#e-rater-metadata). |
| `vocabulary` | Optional. `"rating@1"` or `"rating@2"`, the list of concepts the file uses. An RSL 0.4.1 file reads `rating@2` by default. |
| Any other name | Free metadata, such as `version`, carried to the exported program. |

A value is a literal or a bare word, so `state: AZ` and `state: "AZ"` both pass.
Write `product` for the name a customer reads. The older field `name` gets
[`E-RATER-METADATA-INVALID`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/header#e-rater-metadata-invalid).

### Versions

`rslc` reads the RSL 0.4 line: `rsl: "0.4"` and its patch `rsl: "0.4.1"`.
Any other version, or a `spec` that lacks `rsl`, gets one
[`E-RSL-VERSION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/header#e-rsl-version) message and stops the check.

| Declared | Adds |
| - | - |
| `"0.4"` | The whole language below, except the two rows that follow |
| `"0.4.1"` | Rule clauses and `select`; a 0.4 file that uses them gets [`E-RSL-PATCH`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/header#e-rsl-patch) |
| `"0.4.1"` | The readability checks [`E-CHAIN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-chain), [`E-ROUND-SPLIT`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-round-split) and [`E-MAX-BRANCH`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-max-branch) |

[Versions](https://docs.canary.effectiveai.app/rsl/explanation/versions.md) explains how a patch relates to its line.

### `import`

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
import = "import" string
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
import "rater.rsl"
```

* The string is a path relative to the importing file, or a standard-library path that starts with `std/`.
* An import reads every section of the named file into the program. Sections with the same name merge.
* A scenario imports the rater it tests. A large rater imports files of types, constants or tables.
  See [Split a rater into files](https://docs.canary.effectiveai.app/rsl/how-to/split-a-rater-into-files.md).

The standard library ships inside the compiler:

| Path | Holds |
| - | - |
| `std/ppa.rsl`, `std/ho.rsl`, `std/ca.rsl`, `std/gl.rsl`, `std/wc.rsl` | Enumerations for personal auto, homeowners, commercial auto, general liability and workers' compensation |
| `std/ppa_facts.rsl` | The shared-facts package `ppa.facts` for personal auto |
| `std/law/us_ca/vehicle_code.rsl` | Types and tables of California Vehicle Code violation points |

### `package`

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
package = "package" name { "." name } "version" number
```

A file whose first line is a `package` line is a shared-facts package: questions that several raters ask in the same words.
It holds `Types`, `Tables` and records made of `inputs` only. See [Shared facts](#shared-facts).

## Lexical elements

| Element | Form | Examples |
| - | - | - |
| Comment | `#` to the end of the line | `# The filed base rates.` |
| Lower-case identifier | `[a-z_][a-zA-Z0-9_]*` | `premium`, `base_rate`, `half_up` |
| Upper-case identifier | `[A-Z][a-zA-Z0-9_]*` | `Policy`, `BaseRate`, `Num` |
| Number | `-?[0-9]+(\.[0-9]+)?` | `35`, `0.015`, `-3` |
| Date | `[0-9]{4}-[0-9]{2}-[0-9]{2}` | `2026-01-01` |
| String | `"…"`, with `\"` and `\\` as escapes | `"Example Mutual"` |
| Sheet name | `'…'`, inside an `xlsx` citation only | `'Base rates'` |

* Case is syntax. Fields, constants, enumeration members, functions and steps start in lower case. Types, records, tables and sections start in upper case.
* Keywords such as `if`, `match`, `step` and `where` are identifiers that the grammar reads by position.
* A number is an exact decimal. Write the digits on both sides of the point: `0.5`. Exponents and digit separators are outside the language.
* A minus sign directly before digits belongs to the number. Write `x - 1` with spaces: the compiler reads `x -1` as two values and reports a parse error.
* A date is a calendar day from `0001-01-01` to `9999-12-31`.
* A string appears in annotations, in a `Code` comparison, in `otherwise` cells, in `import` and `use`, and after `nl`.

The operators and punctuation:

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
+  -  *  /  %        arithmetic; "%" only in "N% of x"
=  !=  <  <=  >  >=  comparison; "=" is equality
->  =>  ..  |  !  @  table arrow, match arm, range, union, table read, member or role
( )  [ ]  { }  ,  ;  :  .  _
```

## Sources and citations

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
source   = name ":" "Source" "=" "{" "artifact" ":" "artifact" "(" string ")" "," "kind" ":" kind "}"
kind     = "pdf" | "xlsx" | "csv" | "html" | "zip" | "png" | "parquet"
citation = "cite" ":" name ":" region
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Sources = {
    manual: Source = {
        artifact: artifact("00000000-0000-4000-8000-000000000001"),
        kind: pdf
    },
    rates: Source = {
        artifact: artifact("00000000-0000-4000-8000-000000000002"),
        kind: xlsx
    }
}
```

A source has exactly the two fields `artifact` and `kind`; anything else gets
[`E-SOURCE-SCHEMA`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/citations#e-source-schema).
The string in `artifact(…)` is the identifier of the stored document. The compiler checks each region's form and leaves the document closed.

A citation is the annotation `[cite: source:region]`. The region follows the kind of the source:

| Kind | Region | Example from the two raters |
| - | - | - |
| `pdf` | `p33` or `pp20-24` | `[cite: manual:pp14-15]` |
| `xlsx` | `'Sheet name'!A2` or `'Sheet name'!A2:AQH2` | `[cite: rates:'Base rates'!A2:D4]` |
| `csv` | `row 12` or `rows 12-20` | `[cite: rates:rows 3-6]` |
| `html` | `anchor "fragment-id"` | `[cite: site:anchor "schedule-rating"]` |
| `zip` | `member "path/in/archive"` | `[cite: filing:member "rules/new-business.pdf"]` |
| `png` | `image` | `[cite: scan:image]` |
| `parquet` | `row 12` or `rows 12-20`, counted from 1 | `[cite: cells:rows 1-4]` |

| Declaration | Citation |
| - | - |
| A table, a constant | Required, or `[reason: "…"]` in its place, else [`E-PROVENANCE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/citations#e-provenance) |
| A calculation that rounds | Required on the calculation or on its step |
| The calculation that rounds a rating outcome | Required on the calculation itself, else [`E-OUTCOME-ROUND`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/header#e-outcome-round) |
| An `otherwise` row, a `then` tie-breaker | Required |
| A step, an input, a rule, a stage | Optional |

`reason` states why a number that the filing leaves unstated is in the rater:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
winter_factor [reason: "Example Mutual applies its seasonal load by underwriting bulletin."] = 1.03
```

A citation that names an undeclared source, or a region in the wrong form, gets
[`E-CITATION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/citations#e-citation).
Evidence belongs in citations: a comment that carries a page reference gets
[`E-PROVENANCE-COMMENT`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/citations#e-provenance-comment).
[Citations](https://docs.canary.effectiveai.app/rsl/explanation/citations.md) explains what a citation establishes.

## Types

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
type_decl = Name [ annotations ] "=" type_rhs { refinement }
type_rhs  = type [ meta ]                         an alias
          | "{" member { "," member } "}"         an enumeration
          | "Enum"                                an enumeration read from a CSV file
          | Name "-" ( "{" name { "," name } "}" | Name )   a narrowed enumeration
          | dimension
type      = "Num" | "Bool" | "Date" | Name
          | "Option" "(" type ")" | "List" "(" type ")" | "Ref" "(" Name ")" | "Key" "(" Name ")"
          | Name "|" Name { "|" Name }            a union of records
          | Name "(" type ")"                     a concept around a type
meta      = "{" name ":" value { "," name ":" value } "}"
```

A field states its type after its name. `Types` gives a type a name. The dimension forms and enumerations exist only as named types in `Types`.

### Scalars

| Type | Value |
| - | - |
| `Num` | An exact number |
| `Premium` | An exact number of dollars charged. One rating outcome or more has this type. |
| `Rate` | An exact number of dollars for each unit of exposure |
| `Factor` | An exact multiplier |
| `Pct` | An exact ratio: `0.05` is five percent |
| `Bool` | `true` or `false` |
| `Date` | A calendar day |
| `Source` | A cited document, declared in `Sources` only |

`Premium`, `Rate`, `Factor` and `Pct` are numbers with a stated meaning.
Words are enumerations or `Code` types: RSL has left `Text` behind.

### Numbers and precision

Every number is an exact rational: `1 / 3` stays one third until a `round` narrows it.
[Exact numbers](https://docs.canary.effectiveai.app/rsl/explanation/exact-numbers.md) gives the reasons.

A numeric input states its precision in metadata after the type, and its range in a refinement:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Money = Num {precision: 0.01},
...
value: Money where self in [500, 250000],
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
prior_claims: Option(Num) {precision: int} where self in [0, 5],
```

| Precision | Admits |
| - | - |
| `int` | Whole numbers |
| A power of ten, such as `0.01`, `0.000001` or `1` | Multiples of that step |
| `exact` | Any decimal inside the range, for a value whose scale the filing leaves open |

* Metadata follows the whole type: `Option(Num) {precision: int}`.
* A numeric input needs both a precision and a range `where self in [a, b]`, else the check reports
  [`E-INPUT-DOMAIN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-input-domain).
* Either end of a range may be `_` for an open side: `[0, _]`.
* A request value off the precision grid or outside the range is refused before rating. See [Refusals](https://docs.canary.effectiveai.app/rsl/reference/refusals.md).

### Enumerations

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
member = ( name | Name ) [ "=" string ] [ annotations ]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Coverage = {
    liability [label: "Liability"],
    collision [label: "Collision"],
    comprehensive [label: "Comprehensive"]
},
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Construction = {
    frame = "F" [label: "Frame"],
    masonry = "M" [label: "Masonry"],
    fire_resistive = "FR" [label: "Fire resistive"]
},
Combustible = Construction - { fire_resistive },
```

* `@Coverage.collision` names a member in an expression.
* A member written `frame = "F"` has the value `F`: a request and a table cell hold the value. A plain member's value is its name.
* A member takes a `label`, the words a person reads.
* `Combustible = Construction - { fire_resistive }` declares a narrowed enumeration: the members of `Construction` minus the listed ones. Its members read `@Combustible.frame`.
* An enumeration of more than 256 members belongs in a CSV file:
  [`E-DATA-AS-CODE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-data-as-code).

`Name = Enum` reads the members from `enums/Name.csv` beside the file:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Territory = Enum,
```

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
ident,value,label,aliases
phoenix,phoenix,Phoenix metro,
tucson,tucson,Tucson metro,
remainder,remainder,Remainder of state,rest;other
```

| Column | Holds |
| - | - |
| `ident` | The member's name in RSL: `@Territory.phoenix` |
| `value` | The value in requests and table cells |
| `label` | The words a person reads |
| `aliases` | Other spellings, separated by `;` |

A CSV-backed enumeration may key a table. A map or a fold needs an enumeration declared in the file:
[`E-MAP-DATA-SIZED`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables#e-map-data-sized).
The annotation `[listed]` on the type lists its members in the input contract.

### Bands and value lists

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
dimension = ( "IntBands" | "DecimalBands" | "IntValues" | "DecimalValues" | "Code" )
            "(" [ param { "," param } ";" ] [ item { "," item } ] ")"
param     = name ":" value
item      = "..<" number | ".." number         an open bottom band
          | number | ">" number                a cut
          | number ".." | ">" number ".."      an open top band
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Zip = Code(pattern: "[0-9]{5}"),
License = Code(pattern: "[A-Z][0-9]{8}"),
DriverAge = IntBands(..<21, 21, 25, 65..),
VehicleValue = DecimalBands(scale: 2; ..<10000.00, 10000.00, 25000.00, 50000.00..),
LiabilityLimit = IntValues(50000, 100000, 300000),
Deductible = IntValues(250, 500, 1000),
MileagePoint = IntValues(0, 5000, 10000, 15000, 20000),
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
WindShare = DecimalValues(scale: 2; 0.01, 0.02, 0.05),
```

A band type splits the number line into bands. A value-list type holds exactly the listed numbers. Both type a table dimension. A value-list type also types an input, and a request with an unlisted value is refused.

| Item | Meaning | Table cell |
| - | - | - |
| `..<21` then `21` | The open bottom band holds every value below 21. The cut `21` starts the next band. | `..` and `21` |
| `..10.00` then `>10.00` | The open bottom band holds every value up to and including 10.00. | `..` and `>10.00` |
| `25` | A cut that starts a band at 25. The band below ends before 25. | `25` |
| `>64` | A cut that starts a band above 64. The band below includes 64. | `>64` |
| `65..` | The open top band holds 65 and every value above. | `65` |
| `>64..` | The open top band holds every value above 64. | `>64` |

* A band runs from its cut to the next cut. With a plain last cut, as in `IntBands(0, 10, 20)`, the bands are 0 to 9 and 10 to 19, and 20 is outside.
* An open bottom is the first item and repeats its number in the next item. An open top is the last item. Cuts ascend.
* A sentinel bound such as 999999 gets [`E-BAND-SENTINEL`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables#e-band-sentinel): write an open band.
* A dimension lists at most 65,536 items.

| Type | Parameters |
| - | - |
| `IntBands` | `key: unrounded`, for bands that the filing defines over an unrounded value |
| `DecimalBands` | `scale` (required: the count of decimal places), `unit`, `key` |
| `IntValues` | Items only |
| `DecimalValues` | `scale` (required), `unit` |
| `Code` | `pattern` (required) |

A computed key of a band lookup must sit on the band's grid: round it first, or declare `key: unrounded`.
The check reports [`E-KEY-HOLE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-key-hole) otherwise.

### Codes

A `Code` type holds text that matches a pattern, such as a postal code or a license number:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Zip = Code(pattern: "[0-9]{5}"),
License = Code(pattern: "[A-Z][0-9]{8}"),
```

* The pattern is a regular expression over printable ASCII that the whole value must match.
* A pattern holds literals, classes such as `[A-Z0-9]`, groups, `|`, `?` and counts such as `{5}` or `{1,3}`. `*` and `+` follow one literal or one class.
* A pattern outside this set gets [`E-CODE-PATTERN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-code-pattern).
* Two codes compare with `=` and `!=` only. A code compares with a string literal: `self.zip = "85001"`.
* A `Code` types an input and a table dimension. A column that holds codes of another table has the type `Key(Table)`.

### `Option`

`Option(T)` is a value that is present, `Some(value)`, or absent, `None`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
telematics: Option(Telematics),
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
prior_claims: Option(Num) {precision: int} where self in [0, 5],
```

* A request leaves an optional field out, or sends `null`, for `None`.
* A calculation reads an `Option` through `match`, through a rule clause, or with `= None`. Arithmetic on an `Option` is
  [`E-TYPE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-type).
* `Some(x)` makes a present value in a calculation of type `Option(T)`.
* A refinement on an optional field tests the present value.
* A field with a filed default is an `Option` whose `None` arm holds the default.

### `List`, `Ref` and `Key`

| Form | Meaning |
| - | - |
| `List(Record)` | A list of child records. See [Lists and keys](#lists-and-keys). |
| `List(Enumeration)` | A list of members, read with `len` in a refinement |
| `Ref(Record)` | A reference to one record of a keyed list. A request sends the key of that record. |
| `Key(Table)` | A value that is a filed key of a table with one dimension. A request with any other value is refused with `INPUT-KEY-UNFILED`. |

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
principal: Option(Ref(Driver))
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
zip: Key(ZipFactor),
```

`self.principal` above is an `Option` of a driver: a `match` binds the driver and reads its fields.
A plain `Ref(Driver)` field reads through the reference: `self.principal.age`.
A lookup keyed by a `Key(Table)` value always finds its row, so the table works with its filed rows alone.

### Unions

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
union = Name "|" Name { "|" Name }
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
payment: PaidInFull | Installments,
...
PaidInFull = {
},
Installments = {
    inputs {
        count: Num {precision: int} where self in [2, 12]
    }
},
```

* Each arm is a record that holds inputs and rules only.
* A request sends the arm's name as `tag`, beside the arm's fields: `{"tag": "Installments", "count": 6}`.
* A calculation reads a union with a `match` that names every arm. `_` is unavailable here:
  [`E-UNION-USE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-union-use).

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
installment_fee: Fee(Premium) [label: "Installment fee", cite: manual:p11] =
    match self.payment {
        PaidInFull => 0,
        Installments(i) => installment_charge * i.count
    }
```

### Maps

A map holds one value for each member of an enumeration. A calculation declares one with an index, and a table column declares one with `Type[Enumeration]`:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
base_rate[c: Coverage]: BaseRate(Rate) [label: "Base rate", cite: manual:p4] =
    lookup!(BaseRate, self.territory).rate[c],
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rate: Rate[Coverage]
```

* `c` is the binder: inside the body it holds one member of `Coverage`.
* A read names its key: `self.base_rate[c]` or `self.base_rate[@Coverage.collision]`.
* The key type is an enumeration declared in the file, or a refined one such as `PhysicalDamage`.
* A read whose key may fall outside the map's key type gets
  [`E-MAP-DOMAIN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-map-domain).
* A fold visits every member: `sum(Coverage as c, self.coverage_premium[c])`.

### Refinements

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
refinement = "where" expression [ annotations ]
```

A refinement narrows a type to the values that pass a `Bool` expression. `self` is the value.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
PhysicalDamage = Coverage where self in {@Coverage.collision, @Coverage.comprehensive},
...
Miles = Num {precision: int}
    where self in [0, 20000] [code: "MILEAGE", message: "The program writes up to 20,000 miles a year.", cite: manual:p4]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
ProtectionClass = Num {precision: int, values: Csv} where self in [1, 10],
```

| Form | Narrows |
| - | - |
| `where self in [a, b]` | A number or a date to a range. `(a, b]` leaves `a` out, and `_` opens a side. |
| `where self in {1, 2, 5}` | A number to a set |
| `where self in {@E.a, @E.b}` | An enumeration to some members: a refined enumeration |
| `Num {precision: int, values: Csv}` | A number to the `value` column of `enums/Name.csv` |

* A field of a refined type inherits the rule. `annual_miles: Miles` refuses 30,000 with the code `MILEAGE`.
* A value states one range, on its type or on its field:
  [`E-REFINEMENT`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-refinement).
* A bound has one spelling, `self in [a, b]`. `self >= a` gets
  [`E-REFINE-CANONICAL`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-refine-canonical).
* `c in PhysicalDamage` tests membership of a refined enumeration in an expression.
* [Rules](#rules) covers refinements on fields, lists and records, and their codes.

## Constants

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
constant = name [ annotations ] "=" ( number | date | "true" | "false" )
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
policy_fee [cite: manual:p12] = 25,
paid_in_full_factor [cite: manual:p11] = 0.95,
installment_charge [cite: manual:p11] = 6,
multi_car_factor [cite: manual:p10] = 0.90,
```

* A constant is a fixed value with a citation, or with `[reason: "…"]`.
* A calculation reads a constant by its bare name. `Constants.policy_fee` is the long form.

## Tables and lookups

### Declaration

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
table     = Name [ annotations ] ":" "table" "dims" "(" dim { "," dim } ")" "->" "{" column { "," column } "}"
            [ "columns" ":" "{" header { "," header } "}" ]
            [ "otherwise" [ annotations ] ":" "{" cell { "," cell } "}" ]
dim       = name ":" Name
column    = name ":" ( type [ meta ] | Name "[" Name "]" | Name "(" Name "[" Name "]" ")" )
header    = name [ "[" name "]" ] ":" string
cell      = name [ "[" name "]" ] ":" ( number | "true" | "false" | string )
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
TerritoryByZip [cite: manual:pp14-15] :
    table dims (zip: Zip) -> {
        territory: Territory
    }
    otherwise [cite: manual:p15]: { territory: "remainder" },
BaseRate [cite: rates:'Base rates'!A2:D4] :
    table dims (territory: Territory) -> {
        rate: Rate[Coverage]
    }
    columns: {
        rate[liability]: "liability",
        rate[collision]: "collision",
        rate[comprehensive]: "comprehensive"
    },
...
DriverClass [cite: manual:p6] :
    table dims (age: DriverAge, use: Use) -> {
        factor: Factor
    },
```

| Part | Rule |
| - | - |
| Name | An upper-case name. The rows come from `tables/Name.csv` beside the file, with exactly this spelling. |
| Dimension | A key of the table. Its type is an enumeration, `Bool`, a band type, a value-list type or a `Code`. |
| Column | A value of the row: a number type, `Bool`, `Date`, an enumeration, a `Key(Table)`, or a map `Type[Enumeration]`. |
| `columns` | Maps a dimension, a column or one member of a map column to its CSV header. The default header is the name, and `name[member]` for a map column. |
| `otherwise` | The filed all-other row: the values of every key that lacks a row. It carries its own citation, or a `reason`. |
| Annotations | `cite` or `reason` is required. `label` and `note` are optional. |

* Every column and every dimension is read by a calculation, or the check reports
  [`E-TABLE-UNUSED`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tidy#e-table-unused) and its relatives in the same topic.
* An `otherwise` row that each lookup already covers gets
  [`E-OTHERWISE-DEAD`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables#e-otherwise-dead).
* An `otherwise` cell holds the text of a CSV cell: a number, `true`, `false`, or a string such as `"remainder"` for an enumeration value.

### CSV files

The file `tables/DriverClass.csv` of the `auto` rater starts:

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
age,use,factor
..,pleasure,1.60
..,commute,1.75
..,business,1.90
21,pleasure,1.20
```

| Rule | Detail |
| - | - |
| Header row | One header for each dimension and each column, and one for each member of a map column |
| Key cell | Exact: an enumeration value, `true` or `false`, a band's cut or `..`, one listed number, or one code |
| Rows | Each key appears once, or the check reports [`E-LOOKUP-OVERLAP`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables#e-lookup-overlap). Rows stand in any order. |
| Number cell | A decimal on the precision grid of the column's type |
| `unfiled` | Marks a cell that the filing leaves blank. A row whose values are all `unfiled` is absent. |
| `cite` column | Optional. Names one citation for each row. Rating ignores it. |

[Read a filed table](https://docs.canary.effectiveai.app/rsl/how-to/read-a-filed-table.md) works through the layouts a filing uses.

### `lookup!`

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
lookup = "lookup!" "(" Name { "," expression } ")" "." name [ "[" expression "]" ]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
class_factor: Relativity(Factor) [label: "Driver class factor", cite: manual:p6] =
    lookup!(DriverClass, self.rated_age, self.use).factor,
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
base_rate[c: Coverage]: BaseRate(Rate) [label: "Base rate", cite: manual:p4] =
    lookup!(BaseRate, self.territory).rate[c],
```

* The arguments are the table and one key for each dimension, in the declared order. The suffix names one column, and `[member]` names one value of a map column.
* The subscript of a map column is a binder or one member written `@Coverage.collision`.
  The binder belongs to the map calculation that holds the lookup, as `c` does above, or to a fold such as `sum(Coverage as c, …)`.
* Any other subscript, such as an input in `.rate[self.coverage]`, gets
  [`E-MAP-DOMAIN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-map-domain).
* To read the member that an input names, read the column into a map calculation as above, then index that map: `self.base_rate[self.coverage]`.
  [Use a filed rate table](https://docs.canary.effectiveai.app/rsl/how-to/read-a-filed-table.md#read-the-rate-an-input-selects) shows both steps.
* A number key selects its band or its listed value. An enumeration, `Bool` or `Code` key selects its exact row.
* The check proves that every call finds a row. A key that may leave the dimension gets
  [`E-KEY-RANGE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-key-range), and a reachable key that lacks a row gets
  [`E-LOOKUP-GAP`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-lookup-gap).
* Close a gap with a narrower key type, a filed row, an open band or an `otherwise` row.
* The `!` is part of the name. `lookup(…)` gets
  [`E-LOOKUP-SYNTAX`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables#e-lookup-syntax).

### `interpolate!`

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
interpolate = "interpolate!" "(" Name { "," expression } ","
              "ratio" ":" rounding "," "step" ":" rounding "," "result" ":" "(" number "," mode ")" ")" "." name
rounding    = "(" number "," mode ")" | "exact"
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
MileagePoint = IntValues(0, 5000, 10000, 15000, 20000),
...
MileageFactor [cite: manual:p5] :
    table dims (miles: MileagePoint) -> {
        factor: Factor
    },
...
mileage_factor: Relativity(Factor) [label: "Mileage factor", cite: manual:p5] =
    interpolate!(MileageFactor, self.annual_miles, ratio: (4, half_up), step: (4, half_up), result: (3, half_up)).factor,
```

* The last dimension is a value-list type that holds the filed points. The last key is the value to read at.
* At a filed point the result is that row's value. Between two points `p1` and `p2` with values `v1` and `v2`, the result is
  `round(v1 + round(ratio * (v2 - v1)))`, where `ratio = round((x - p1) / (p2 - p1))`.
* `ratio`, `step` and `result` state the three roundings in that order, each as `(scale, mode)`. `ratio` and `step` may be `exact`.
* With 12,000 miles the example reads 1.048: two fifths of the way from 1.000 to 1.120.
* The key must lie inside the filed points, else the check reports
  [`E-INTERP-RANGE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-interp-range).
  A band dimension or an `otherwise` row gets
  [`E-INTERP-KEY`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables#e-interp-key).

[Interpolate between rows](https://docs.canary.effectiveai.app/rsl/how-to/interpolate-between-rows.md) shows a clamp around the read.

### Geographies and `locate`

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
geography = Name [ annotations ] "=" ( "projected_grid" | "degree_grid" ) "{" name ":" value { "," name ":" value } "}"
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Geographies = {
    WildfireGrid [cite: manual:p9] = degree_grid {
        cell: 0.01,
        origin_latitude: 0,
        origin_longitude: 0,
        cells: Cell,
        latitude: latitude,
        longitude: longitude,
        on_line: least_key
    }
}
...
wildfire_factor: Factor =
    match locate(WildfireGrid, self.latitude, self.longitude) {
        Some(c) => lookup!(Cell, c).factor,
        None => 1
    },
```

A geography maps a point to one cell of a grid. Each row of the `cells` table is one cell, with its key and one point inside it.

| Field | `degree_grid` | `projected_grid` |
| - | - | - |
| `cell` | The side of a cell in decimal degrees | The side of a cell in metres |
| Origin | `origin_latitude`, `origin_longitude` | `origin_x`, `origin_y`, and `crs: esri_102008` |
| `cells` | The table of cells | The same |
| `latitude`, `longitude` | The columns of that table that hold each cell's point | The same |
| `on_line` | `least_key`: a point on a line between cells takes the least key | The same |

* Every field of the chosen method is required.
* `locate(Geography, latitude, longitude)` gives `Some(key)` for a point inside a filed cell and `None` elsewhere.
* A `Some` key is a filed key, so the lookup that follows always finds its row.

## Records

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
record = Name [ annotations ] "=" "{" { record_item } "}" { refinement | referral }
record_item = "inputs" "{" { input } "}"
            | "step" name [ "in" name ] [ annotations ] "{" { calculation } "}"
            | calculation
```

A record is one thing a request describes: a policy, a driver, a vehicle. It holds inputs, steps of calculations, and rules.

* The records of a program form one tree with one root. The request is the root record.
  [`E-RECORD-ROOT`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-record-root),
  [`E-RECORD-POSITION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-record-position) and
  [`E-RECORD-RECURSIVE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-record-recursive) report a broken tree.
* A record sits at one field of one parent. A record that holds itself is outside the language.
* [The input contract](https://docs.canary.effectiveai.app/rsl/reference/input-contract.md) describes the request that the records define.

### Inputs

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
input = name ":" type [ "keyed" "by" name ] [ meta ] [ "by" role | "per" "quote" ] [ "refines" name ]
        [ "from" expression ] [ "when" expression ] [ annotations ] { refinement }
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
inputs {
    effective_date: Date [ask: "When does the policy start?", group: "Policy"]
        where self in [2026-01-01, 2026-12-31],
    expiration_date: Date [ask: "When does the policy end?", group: "Policy"]
        where self in [2026-07-01, 2027-12-31],
    garage: Garage,
    liability_limit: LiabilityLimit [choice, shape: "limit", api_default: "100000", group: "Coverage"],
    payment: PaidInFull | Installments,
    telematics: Option(Telematics),
    prior_insurance: Bool [ask: "Are the cars insured today?", group: "Policy"],
    prior_lapse_days: Num {precision: int} when self.prior_insurance [ask: "How many days was the household uninsured in the last year?", group: "Policy"]
        where self in [0, 365],
```

| Field type | The request holds |
| - | - |
| A number, `Bool`, `Date` | A JSON number, `true` or `false`, or a date string such as `"2026-03-01"` |
| An enumeration, a `Code` | A string: the member's value, or the code |
| A record | A nested object: exactly one child |
| `Option(T)` | The value, or `null`, or the field left out |
| `List(Record)` | An array of objects |
| A union | An object with a `tag` |
| `Ref(Record)` | The key of the record it names |

* The clauses follow the order of the grammar. `keyed by` stands before the metadata and the annotations.
* An input takes a type and rules, and a calculation takes `=` and a value.
* Every input is read by a rating outcome or by a rule, or the check reports
  [`E-UNREAD-INPUT`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tidy#e-unread-input).

### Follow-up inputs

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
prior_insurance: Bool [ask: "Are the cars insured today?", group: "Policy"],
prior_lapse_days: Num {precision: int} when self.prior_insurance [ask: "How many days was the household uninsured in the last year?", group: "Policy"]
    where self in [0, 365],
```

* `when` makes an input a follow-up: the request gives it where the condition holds. Left out under a true condition, it is refused with `INPUT-FOLLOW-UP`.
* Where the condition fails, the rater ignores a given value.
* A read of a follow-up sits under its condition, as in the rule `not self.prior_insurance or self.prior_lapse_days <= 30`.
  Any other read gets [`E-FOLLOW-UP-READ`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-follow-up-read).
* A `Bool` follow-up follows the same rule: `condition and answer` counts an unasked question as false, and `not condition or answer` counts it as true.
* The condition reads inputs of the same record. A list follow-up gets
  [`E-FOLLOW-UP-TYPE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-follow-up-type).

### Lists and keys

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
drivers: List(Driver) keyed by number [ask: "Who drives the cars?", group: "Drivers", complete: "Any more drivers?"]
    where len(self) in [1, 4] [code: "DRIVER-COUNT", message: "A policy rates one to four drivers.", cite: manual:p3]
    where unique(self as d, d.license) [code: "DUPLICATE-LICENSE", message: "Each driver has a distinct license.", cite: manual:p3]
    where (count(self as d, d.named_insured) = 1) [code: "NAMED-INSURED", message: "A policy has one named insured.", cite: manual:p3],
vehicles: List(Vehicle) keyed by number [ask: "Which cars do you want to insure?", group: "Cars"]
    where len(self) in [1, 4] [code: "VEHICLE-COUNT", message: "A policy rates one to four vehicles.", cite: manual:p3]
```

* `keyed by number` names an input of the item record. The key is distinct across the list, and a repeat is refused with `INPUT-KEY`.
* A keyed list is a map from key to record, so one request rates the same way however its list is ordered.
  `find`, `argmax`, `argmin`, `topn` and `rank` break ties by the key,
  and they need a keyed list: [`E-SELECT-TIE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-select-tie).
* `index` reads a position, so it takes a plain list. On a keyed list it gets
  [`E-KEYED-ORDER`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-keyed-order), and so does `=` between two lists of keyed records.
* A `Ref(Record)` field names one item of a keyed list by its key. An unknown key is refused with `INPUT-REF`.
* `len(self)` bounds the length and `unique(self as d, d.license)` states a second distinct field.
* [Rate a list](https://docs.canary.effectiveai.app/rsl/how-to/rate-a-list.md) works through folds over a list.

### Steps and calculations

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
calculation = name [ "[" name ":" Name "]" ] ":" type [ meta ] [ annotations ] { refinement } "=" body
body        = expression [ "with" expression "as" name "otherwise" expression ]
              { "exception" "when" expression "then" expression }
              [ "overridden" "by" expression ]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
step class in classify [label: "Driver class"] {
    age: Num = diff_years(Policy.effective_date, self.birth_date),
    years_licensed: Num = diff_years(Policy.effective_date, self.licensed_date),
    experience_factor: Surcharge(Factor) [label: "New driver factor", cite: manual:p7] =
        1 exception when self.years_licensed < new_driver_years then new_driver_factor
}
...
step premium in vehicle [label: "Vehicle premium"] {
    coverage_premium[c: Coverage]: Subtotal(Premium) [label: "Coverage premium", cite: manual:p9] =
        round(
            Policy.base_rate[c] * self.class_factor * self.experience_factor * self.mileage_factor * Policy.points_factor * self.coverage_factor[c],
            2,
            half_up
        ),
    premium: Subtotal(Premium) [label: "Vehicle premium"] = sum(Coverage as c, self.coverage_premium[c])
}
```

* A step groups calculations under a name. A step takes `in stage`, `cite`, `label`, `note` and `skip`.
* A calculation has a name, a type and one expression.
* `name[c: Enumeration]` declares a map: one value for each member.
* Calculations stand in any order. A cycle gets [`E-CYCLE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-cycle).
* Every calculation reaches a rating outcome, or the check reports
  [`E-ORPHAN-COMPUTED`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tidy#e-orphan-computed).
* A child's calculation that reads only its parent belongs on the parent:
  [`E-HOIST`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tidy#e-hoist).
* A refinement on a calculation's type, `where self in [a, b]`, is a claim that the check proves:
  [`E-COMPUTED-RANGE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/numbers#e-computed-range).
* `[skip: "id":"reason"]` on a step marks a step of the manual that the rater leaves out.

The three rule clauses end a calculation's body. They need `rsl: "0.4.1"`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
multi_car_discount: Credit(Factor) [label: "Multi-car discount", cite: manual:p10] =
    1 exception when len(self.vehicles) >= 2 then multi_car_factor,
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
claims_factor: Factor = 1 + claim_load * c with self.prior_claims as c otherwise new_business_factor,
schedule_factor: Factor =
    select(schedule_low..schedule_high, self.schedule_position)
    overridden by self.underwriter_factor
```

| Clause | Meaning |
| - | - |
| `default with option as v otherwise z` | With `option` present, the default, which reads `v`. With `option` absent, `z`. |
| `exception when c then v` | `v` where `c` holds. Of several exceptions, the first that holds applies. |
| `overridden by option` | The present value of `option`, which beats every exception and the default |

A clause belongs at the end of a calculation. To use one inside a larger expression, name the value as its own calculation.

A calculation whose value is still prose holds `nl "text"`. The strict check fails while a hole remains, and `rslc check --allow-incomplete` reports the rest.

### Rules

A rule is a refinement on an input, on a list or on a record. `self` is the value that the rule tests.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
points: Num {precision: int}
    where self in [0, 6] [code: "POINTS", message: "The program writes drivers with six points or fewer.", cite: manual:p8]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
drivers: List(Driver) keyed by number [ask: "Who drives the cars?", group: "Drivers", complete: "Any more drivers?"]
    where len(self) in [1, 4] [code: "DRIVER-COUNT", message: "A policy rates one to four drivers.", cite: manual:p3]
    where unique(self as d, d.license) [code: "DUPLICATE-LICENSE", message: "Each driver has a distinct license.", cite: manual:p3]
    where (count(self as d, d.named_insured) = 1) [code: "NAMED-INSURED", message: "A policy has one named insured.", cite: manual:p3],
...
} where not self.prior_insurance or self.prior_lapse_days <= 30 [code: "LAPSE", message: "The program accepts a lapse of 30 days or fewer.", cite: manual:p3]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
} where self.coverage_c <= 70% of self.coverage_a [code: "CONTENTS-LIMIT", message: "Contents coverage is at most 70 percent of the dwelling limit.", cite: manual:p3]
```

| Rule on | Written | `self` is |
| - | - | - |
| A field | After the input's annotations | The field's value, or the present value of an `Option` |
| A list | After the list input | The list: `len(self)`, `unique(self as x, x.key)`, `count(self as x, …)` |
| A record | After the record's closing brace | The record: `self.field` reads its inputs and its children's |
| A type | In `Types` | The value; every field of the type inherits the rule |

| Annotation | Effect |
| - | - |
| `code: "CODE"` | Makes the rule a business rule: a request that breaks it is refused as `BusinessRejection` with this code. The code matches `[A-Z][A-Z0-9_-]*`. |
| `message: "…"` | The words a person reads with the refusal |
| `cite: source:region` | The page that states the rule. Optional. It travels in each refusal of the rule. |

* A rule that lacks a code is an input rule: the refusal is `InvalidInputs` with a code such as `INPUT-RANGE`.
* `cite` is optional on every rule. A coded business rule and a plain range such as `where self in [0, 6]` both pass the check uncited.
* A refusal of an uncited rule holds `"cite": null`.
* A `cite` on a rule names a declared source and a region of that source's kind, or the check reports
  [`E-CITATION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/citations#e-citation).
* A rule takes `code`, `message` and `cite` only. `reason` on a rule gets
  [`E-ANNOTATION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-annotation).
* `code` and `message` follow the rule's expression. On the input's own brackets they get
  [`E-ANNOTATION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-annotation).
* On an input, write an equality inside parentheses: `where (count(self as d, d.named_insured) = 1)`.
  A bare `=` there ends the declaration.
* A rule reads its own value and what lies below it. A rule that relates a child to its parent stands on the parent:
  [`E-REFINE-UPWARD`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/inputs#e-refine-upward).
* `N% of x` is an exact share, for rules only: `self.coverage_c <= 70% of self.coverage_a`.
* A rule may use dates, arithmetic, `if`, `match`, folds over its own lists, constants and `lookup!`.
* Rating starts after every rule passes. A calculation always gives a value.
  [Refusals](https://docs.canary.effectiveai.app/rsl/reference/refusals.md) lists each refusal code and the order of the checks.

## Expressions

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
expression = "if" expression "then" expression "else" expression
           | "match" expression "{" arm { "," arm } "}"
           | or
or         = and { "or" and }
and        = compare { "and" compare }
compare    = sum { ( "=" | "!=" | "<" | "<=" | ">" | ">=" ) sum }
           | sum "in" ( interval | "{" sum { "," sum } "}" | Name )
interval   = ( "[" | "(" ) bound "," bound ( "]" | ")" )
bound      = number | date | "_"
sum        = product { ( "+" | "-" ) product }
product    = unary { ( "*" | "/" ) unary }
unary      = ( "-" | "not" ) unary | number "%" "of" unary | postfix
postfix    = atom { "." name | "(" [ argument { "," argument } ] ")" | "[" expression "]" } [ "as" name [ "by" expression ] ]
atom       = number | date | string | "true" | "false" | "self" | name | Name
           | "@" Name "." name | "(" expression ")" | "nl" string
arm        = pattern "=>" expression
pattern    = "@" Name "." name | "true" | "false" | "Some" "(" name ")" | "None" | Name [ "(" name ")" ] | "_"
```

### Precedence

From the loosest binding to the tightest:

| Level | Forms | Notes |
| - | - | - |
| 1 | `if … then … else …`, `match … { … }` | Each takes the longest expression to its right. `else` is required. |
| 2 | `or` | |
| 3 | `and` | |
| 4 | `=`, `!=`, `<`, `<=`, `>`, `>=`, `in` | A chain such as `1 < x < 5` holds when each step holds. |
| 5 | `+`, `-` | Left to right |
| 6 | `*`, `/` | Left to right |
| 7 | Unary `-`, `not`, `N% of x` | |
| 8 | `.field`, a call, `[key]` | |

* `=` tests equality and `!=` tests the opposite. `==` is a parse error.
* `not` binds tighter than a comparison: `not a = b` reads as `(not a) = b`, a type error. Write `not (a = b)` or `a != b`.
* `not self.prior_insurance or self.prior_lapse_days <= 30` reads as `(not …) or (… <= 30)`.
* Both sides of a comparison have the same type. `<` and its relatives order numbers and dates.
* Every division proves a divisor other than zero, or the check reports
  [`E-DIV-ZERO`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-div-zero).

### `if` and `match`

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
mature_discount: Credit(Factor) [label: "Mature driver discount", cite: manual:p10] =
    match find(self.drivers as d, d.named_insured) {
        Some(d) => if d.age >= mature_age then mature_factor else 1,
        None => 1
    },
...
coverage_factor[c: Coverage]: Composite(Factor) [label: "Coverage factor"] =
    match c {
        @Coverage.liability => Policy.limit_factor,
        @Coverage.collision => self.value_factor * self.age_factor * lookup!(DeductibleFactor, self.deductible, c).factor,
        @Coverage.comprehensive => self.value_factor * Policy.garage_factor * lookup!(DeductibleFactor, self.deductible, c).factor
    }
```

| `match` on | Arms |
| - | - |
| An enumeration | `@Enum.member => …` for each member, or `_ => …` for the rest |
| A `Bool` | `true => …` and `false => …` |
| An `Option` | `Some(v) => …` and `None => …`; `v` holds the present value |
| A union | `Arm(v) => …` or `Arm => …` for every arm |

* Both branches of an `if`, and every arm of a `match`, have the type of the calculation.
* A `match` covers every case, or the check reports
  [`E-MATCH`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-match) or
  [`E-OPTION-MATCH`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-option-match).
* Inside an arm, the check knows the matched case: `lookup!(DeductibleFactor, self.deductible, c)` above passes
  because `c` is a `PhysicalDamage` member in those two arms.
* An `if` that sorts a number into classes belongs in a band table:
  [`E-IF-AS-TABLE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tidy#e-if-as-table).

### Field access, `self` and binders

| Form | Reads |
| - | - |
| `self.value` | A field of the record that holds the calculation |
| `self.garage.zip` | A field of a child record |
| `Policy.effective_date` | A field of the enclosing record of that type, from any record below it |
| `d.age` | A field of the item that the binder `d` holds |
| `policy_fee` | A constant |
| `@Use.commute` | An enumeration member |

* `list as d` binds each item of a list to `d` for the rest of a call. `Enumeration as c` binds each member.
* `list as d by d.key` adds the order of a stepwise fold.
* A read of a record that stands outside the path to the root gets
  [`E-ANCESTOR-READ`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/names#e-ancestor-read).

### Membership

| Form | True when |
| - | - |
| `x in [a, b]` | `a <= x <= b` |
| `x in (a, b]`, `x in [a, b)` | The round bracket leaves that end out |
| `x in [a, _]` | `x >= a`; `_` opens a side. The ends are number or date literals. |
| `x in {1, 2, 5}`, `m in {@E.a, @E.b}` | `x` equals one listed value |
| `c in PhysicalDamage` | `c` is a member of the refined enumeration |

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
season_factor: Factor = 1 exception when month(self.effective_date) in {12, 1, 2} then winter_factor
```

### Dates

Dates compare with `=`, `!=`, `<`, `<=`, `>`, `>=` and `in [a, b]`. Arithmetic on dates goes through functions:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
age: Num = diff_years(Policy.effective_date, self.birth_date),
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
term_months: Num = diff_months(self.expiration_date, self.effective_date),
...
where self.expiration_date = add_months(self.effective_date, 6) or self.expiration_date = add_years(self.effective_date, 1) [code: "TERM", message: "The program writes six-month and annual terms.", cite: manual:p4],
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
term_days: Num = diff_days(self.expiration_date, self.effective_date),
```

| Function | Result | Example |
| - | - | - |
| `diff_days(later, earlier)` | Days between two dates | `diff_days(2025-03-01, 2024-02-29)` is 366. A reversed pair gives a negative count. |
| `diff_months(later, earlier)` | Calendar months; the day of the month is ignored | From `2024-01-31` to `2024-02-01` is 1. |
| `diff_years(later, earlier)` | Full years, as an age is counted | Born `2024-02-29`: 0 on `2025-02-28`, 1 on `2025-03-01`. |
| `add_days(date, n)` | The date `n` days later | `n` is a whole-number literal; a negative `n` moves back. |
| `add_months(date, n)` | The date `n` months later | `add_months(2024-01-31, 1)` is `2024-02-29`: a day past the month's end becomes its last day. |
| `add_years(date, n)` | The date `n` years later | `add_years(2024-02-29, 1)` is `2025-02-28`. |
| `year(date)`, `month(date)` | The year, and the month from 1 to 12 | |

[Work with dates](https://docs.canary.effectiveai.app/rsl/how-to/work-with-dates.md) derives ages and terms.

### Functions

A call is `name(arguments)`. Only `lookup!` and `interpolate!` take `!`. [Functions](https://docs.canary.effectiveai.app/rsl/reference/functions.md) gives the rules of each one.

| Group | Functions |
| - | - |
| Tables | [`lookup!`](https://docs.canary.effectiveai.app/rsl/reference/functions#lookup), [`interpolate!`](https://docs.canary.effectiveai.app/rsl/reference/functions#interpolate), [`locate`](https://docs.canary.effectiveai.app/rsl/reference/functions#locate) |
| Numbers | [`round`](https://docs.canary.effectiveai.app/rsl/reference/functions#round), [`root`](https://docs.canary.effectiveai.app/rsl/reference/functions#root), [`pow`](https://docs.canary.effectiveai.app/rsl/reference/functions#pow), [`abs`](https://docs.canary.effectiveai.app/rsl/reference/functions#abs), [`floor`](https://docs.canary.effectiveai.app/rsl/reference/functions#floor), [`ceil`](https://docs.canary.effectiveai.app/rsl/reference/functions#ceil), [`min`](https://docs.canary.effectiveai.app/rsl/reference/functions#min), [`max`](https://docs.canary.effectiveai.app/rsl/reference/functions#max) |
| Folds | [`sum`](https://docs.canary.effectiveai.app/rsl/reference/functions#sum), [`product`](https://docs.canary.effectiveai.app/rsl/reference/functions#product), [`avg`](https://docs.canary.effectiveai.app/rsl/reference/functions#avg), `min`, `max` |
| Lists | [`len`](https://docs.canary.effectiveai.app/rsl/reference/functions#len), [`count`](https://docs.canary.effectiveai.app/rsl/reference/functions#count), [`exists`](https://docs.canary.effectiveai.app/rsl/reference/functions#exists), [`all`](https://docs.canary.effectiveai.app/rsl/reference/functions#all), [`find`](https://docs.canary.effectiveai.app/rsl/reference/functions#find), [`filter`](https://docs.canary.effectiveai.app/rsl/reference/functions#filter), [`argmax`](https://docs.canary.effectiveai.app/rsl/reference/functions#argmax), [`argmin`](https://docs.canary.effectiveai.app/rsl/reference/functions#argmin), [`topn`](https://docs.canary.effectiveai.app/rsl/reference/functions#topn), [`rank`](https://docs.canary.effectiveai.app/rsl/reference/functions#rank), [`index`](https://docs.canary.effectiveai.app/rsl/reference/functions#index), [`unique`](https://docs.canary.effectiveai.app/rsl/reference/functions#unique) |
| Dates | [`diff_days`](https://docs.canary.effectiveai.app/rsl/reference/functions#diff_days), [`diff_months`](https://docs.canary.effectiveai.app/rsl/reference/functions#diff_months), [`diff_years`](https://docs.canary.effectiveai.app/rsl/reference/functions#diff_years), [`add_days`](https://docs.canary.effectiveai.app/rsl/reference/functions#add_days), [`add_months`](https://docs.canary.effectiveai.app/rsl/reference/functions#add_months), [`add_years`](https://docs.canary.effectiveai.app/rsl/reference/functions#add_years), [`year`](https://docs.canary.effectiveai.app/rsl/reference/functions#year), [`month`](https://docs.canary.effectiveai.app/rsl/reference/functions#month) |
| Options and others | [`Some`](https://docs.canary.effectiveai.app/rsl/reference/functions#some), [`select`](https://docs.canary.effectiveai.app/rsl/reference/functions#select), [`nl`](https://docs.canary.effectiveai.app/rsl/reference/functions#nl) |

Numbers:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
units: Num = ceil(self.coverage_a / 1000),
size_factor: Factor [cite: manual:p2] = root(self.coverage_a / base_amount, 2, 4, half_up),
...
age_decades: Num = floor(self.dwelling_age / 10),
age_factor: Factor = pow(age_load, self.age_decades),
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Some(t) => max(telematics_floor, 1 - t.score / 500),
```

| Form | Result |
| - | - |
| `abs(x)`, `floor(x)`, `ceil(x)` | Exact. `floor(-1.5)` is -2 and `ceil(-1.5)` is -1. |
| `min(a, b, …)`, `max(a, b, …)` | The least or greatest of two or more values |
| `pow(x, n)` | Exact, for a whole `n` |
| `pow(x, e, scale, mode)`, `root(x, n, scale, mode)` | Rounded to `scale` decimal places; the calculation or its step carries a citation |
| `select(low..high, position)` | `low + position * (high - low)`: a value inside a filed range |

Folds and lists:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
youngest_age: Num = min(self.drivers as d, d.age),
highest_points: Num =
    match argmax(self.drivers as d, d.points, then [cite: manual:p8]: d.age) {
        Some(d) => d.points,
        None => 0
    },
...
young_drivers: Num = count(self.drivers as d, d.age < 25),
...
vehicle_premium: Subtotal(Premium) [label: "Vehicle premium"] = sum(self.vehicles as v, v.premium),
...
premium: Subtotal(Premium) [label: "Vehicle premium"] = sum(Coverage as c, self.coverage_premium[c])
```

| Form | Result |
| - | - |
| `sum(list as v, value)`, `product(list as v, value)` | The total or the product. An empty list gives 0 or 1. |
| `sum(Enum as m, value)`, `product`, `min`, `max`, `count`, `exists`, `all` | The same folds over the members of an enumeration |
| `min(list as v, value)`, `max(…)`, `avg(…)` | Need a list that holds an item, or the check reports [`E-EMPTY-FOLD`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-empty-fold) |
| `product(list as v by v.key, value, round_each: (scale, mode))` | A stepwise fold in key order that rounds after each step. `sum` takes the same form. |
| `len(list)`, `count(list as v, condition)` | A whole number |
| `exists(list as v, condition)`, `all(list as v, condition)` | A `Bool`. An empty list gives `false` and `true`. |
| `find(list as v, condition)` | `Option` of the first match in key order |
| `argmax(list as v, score, then [cite: …]: priority)`, `argmin(…)` | `Option` of the item with the greatest or least score. A tie goes to the greater `priority`, then to the least key. |
| `filter(list as v, condition)`, `topn(list as v, score, n)` | A list, for another fold to read |
| `rank(list as v, score, target, asc \| desc, then [cite: …]: priority)` | The position of the item whose key is `target`, counted from 0 |
| `index(self)`, `index(v)` | The position of a record in a plain list as the request sends it, counted from 0 |
| `unique(self as v, v.key)` | A list rule: the key is distinct |

A fold that walks a list once for each item of the same list gets
[`E-QUADRATIC`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-quadratic) when the walk reads the same value each time.

## Rounding

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
round = "round" "(" expression "," scale "," mode ")"
mode  = "half_up" | "half_down" | "half_even" | "up" | "down" | "ceiling" | "floor"
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
coverage_premium[c: Coverage]: Subtotal(Premium) [label: "Coverage premium", cite: manual:p9] =
    round(
        Policy.base_rate[c] * self.class_factor * self.experience_factor * self.mileage_factor * Policy.points_factor * self.coverage_factor[c],
        2,
        half_up
    ),
```

* Arithmetic is exact, and a value changes precision only where the file says `round`, `root`, `pow` with four arguments, `round_each` or `interpolate!`.
* `scale` is a whole-number literal: the count of decimal places. A negative scale rounds to tens, hundreds and beyond: `round(x, -3, half_up)`.
* The mode is required.
* Every numeric rating outcome passes through a `round` in a calculation that cites the rounding rule.

| Mode | Rounds |
| - | - |
| `half_up` | To the nearest step; a tie goes away from zero |
| `half_down` | To the nearest step; a tie goes toward zero |
| `half_even` | To the nearest step; a tie goes to the even step |
| `up` | Away from zero |
| `down` | Toward zero |
| `ceiling` | Toward positive infinity |
| `floor` | Toward negative infinity |

`round(1.25, 1, half_up)` is 1.3 and `round(1.25, 1, half_even)` is 1.2. [Round a premium](https://docs.canary.effectiveai.app/rsl/how-to/round-a-premium.md) compares the modes.

## Questions and choices

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
annotations = "[" annotation { "," annotation } "]"
annotation  = name                                a flag, such as "choice"
            | name ":" ( string | number )        a value, such as "ask" or "look_back"
            | "cite" ":" name ":" region
            | "collect" ":" member { "+" member }
            | "skip" ":" string ":" string
```

An input's annotations say how a person is asked for it. They change the input contract and leave every premium as it is.
[Names](https://docs.canary.effectiveai.app/rsl/reference/names.md#annotations) lists each key with the contract field it writes, and
[Plan the next question](https://docs.canary.effectiveai.app/rsl/how-to/plan-the-next-question.md) shows a host that asks them.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
zip: Zip [ask: "Where are the cars parked overnight?", help: "A five-digit ZIP code.", group: "Policy", format: "postal-code"],
enclosed: Bool [ask: "Are the cars parked in an enclosed garage?", group: "Policy"]
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
liability_limit: LiabilityLimit [choice, shape: "limit", api_default: "100000", group: "Coverage"],
```

| Key | On | Says |
| - | - | - |
| `ask: "…"` | An input | The question. `{name}` stands for another input of the record and `{#}` for the item's number in its list. |
| `help: "…"` | An input | Help text shown with the question |
| `group: "Policy"` | An input | The section of the questionnaire |
| `format: "postal-code"` | An input | The answer's format: `vin`, `postal-code`, `money`, `year`, `country` or `subdivision` |
| `description: "…"`, `unit: "USD"` | An input | Words and a unit for a reader of the input contract |
| `api_default: "100000"` | An input | The value used when a request leaves the field out |
| `choice` | An input | Something the person buys, such as a limit. It requires `api_default`: [`E-CHOICE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-choice). |
| `shape: "limit"` | A `choice` | The kind of choice: `limit`, `limit_pair`, `deductible`, `per_day`, `included`, `tier` or `term` |
| `labels: "0=No one"` | An enumeration input, or a number input with `refers` | The words a person reads for a value |
| `complete: "Any more drivers?"` | A list | The question that closes the list |

A wrong placeholder, format or shape gets [`E-QUESTION`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-question).

### `from` and `filled_by`

These two inputs come from the package of the next section:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
decoded_year: Num {precision: int} [filled_by: "vin_decode(vin).year"]
    where self in [1981, 2100],
model_year: Num {precision: int} from self.decoded_year [ask: "What year is car {#}?", format: "year"]
    where self in [1981, 2100],
```

* `x: T from expression` suggests an answer. The expression reads inputs and tables of the same record, and the person accepts the suggestion or answers over it.
* `[filled_by: "function(input).field"]` names a host function that fills the input, so the question is skipped.
* Rating reads the answer and ignores the `from` expression. An input that only a `from` reads stays optional, and a calculation that reads it gets
  [`E-FROM`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-from).
* `x: T refines base` places a question directly after `base`, an input of the same record.

### Shared facts

A shared-facts package declares questions once for many raters. This package and the rater that binds it check together:

> [accordion] household/facts.rsl and household/rater.rsl
>
> ```rsl household/facts.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> package example.household version 1
>
> Types = {
>     Relationship = {
>         applicant [label: "you"],
>         spouse [label: "your spouse"],
>         other [label: "another driver"]
>     },
>     Zip = Code(pattern: "[0-9]{5}"),
>     Vin = Code(pattern: "[0-9A-Z]{8}0[0-9A-Z]"),
>     Coverage [choices] = {
>         collision [label: "Collision", shape: "deductible"],
>         bodily_injury [label: "Bodily injury liability", shape: "limit_pair"]
>     }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             insured_now: Bool [gives: "declarations_page", ask: "Are your cars insured now?", group: "Policy"],
>             cars: List(Car) keyed by car_id [inline, found_on: "declarations_page", ask: "Which cars do you want to insure?", group: "Cars", called: "the {model_year} car|car {#}", complete: "Any other cars?"]
>                 where len(self) in [1, _],
>             drivers: List(Driver) keyed by driver_id [ask: "Who drives these cars?", group: "Drivers", called: "{relationship}|driver {#}", you: "relationship = applicant", complete: "Anyone else?"]
>                 where len(self) in [1, _]
>         }
>     },
>     Car = {
>         inputs {
>             car_id: Num {precision: int} where self in [1, 9],
>             decoded_year: Num {precision: int} [filled_by: "vin_decode(vin).year"]
>                 where self in [1981, 2100],
>             model_year: Num {precision: int} from self.decoded_year [ask: "What year is car {#}?", format: "year"]
>                 where self in [1981, 2100],
>             vin_known: Bool [ask: "Do you have the VIN of {this}?"],
>             vin: Vin when self.vin_known [none_answer: "I don't have it", ask: "What is the VIN of {this}?", format: "vin", reduce: "1-8,'0',10"],
>             zip: Zip [ask: "Where is {this} parked overnight?", format: "postal-code"],
>             rideshare: Bool [checklist: "Cars", statement: "Used for rideshare or delivery", ask: "Is {this} used for rideshare?"],
>             main_driver: Num {precision: int} [ask: "Who drives {this} most?", refers: "Policy.drivers", labels: "0=No one more than the others"]
>                 where self in [0, 9]
>         }
>     },
>     Driver = {
>         inputs {
>             driver_id: Num {precision: int} where self in [1, 9],
>             relationship: Relationship [ask: "How is driver {#} related to you?"],
>             birth_date: Date [ask: "When was {this} born?", ask_you: "When were you born?"]
>                 where self in [1900-01-01, 2100-12-31],
>             tickets: List(Ticket) keyed by ticket_id [rows, ask: "Any tickets for {this} since {look_back}?", ask_you: "Any tickets for you since {look_back}?", called: "ticket {#}", look_back: 5]
>         }
>     },
>     Ticket = {
>         inputs {
>             ticket_id: Num {precision: int} where self in [1, 9],
>             date: Date [ask: "When was {this}?"] where self in [1900-01-01, 2100-12-31]
>         }
>     }
> }
> ```
>
> ```rsl household/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Household Auto (shared facts)",
>     effective_date: 2026-01-01
> }
>
> import "facts.rsl"
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000021"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Deductible = IntValues(250, 500, 1000),
>     RatedYear = Num {precision: int} where self in [1990, 2100]
> }
>
> Constants = {
>     car_rate [cite: manual:p2] = 480,
>     ticket_load [cite: manual:p3] = 0.10,
>     rideshare_load [cite: manual:p4] = 0.30
> }
>
> Tables = {
>     ZipFactor [cite: manual:p5] :
>         table dims (zip: Zip) -> {
>             factor: Factor
>         }
>         otherwise [cite: manual:p5]: { factor: 1 },
>     DeductibleFactor [cite: manual:p6] :
>         table dims (deductible: Deductible) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             insured_now
>                 where self [code: "UNINSURED", message: "The program writes households that are insured today.", cite: manual:p1],
>             garage_count: Num {precision: int} [ask: "How many of the cars are parked in a garage?", why: "Example Mutual rates garaged cars for less."]
>                 where self in [0, 9]
>         },
>         step premium [cite: manual:p7] {
>             tickets: Num = sum(self.drivers as d, len(d.tickets)),
>             premium: Premium [cite: manual:p7] =
>                 round(sum(self.cars as c, c.premium) * (1 + ticket_load * self.tickets) - 10 * self.garage_count, 0, half_up)
>         }
>     },
>     Car = {
>         inputs {
>             model_year as RatedYear [code: "CAR-AGE", message: "The program writes cars from model year 1990.", cite: manual:p1],
>             collision_deductible: Deductible [choice, shape: "deductible", coverage: "collision", api_default: "500"]
>         },
>         step premium [cite: manual:p5] {
>             rideshare_factor: Factor = if self.rideshare then 1 + rideshare_load else 1,
>             zip_factor: Factor = lookup!(ZipFactor, self.zip).factor,
>             deductible_factor: Factor = lookup!(DeductibleFactor, self.collision_deductible).factor,
>             premium: Premium = car_rate * self.zip_factor * self.deductible_factor * self.rideshare_factor
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv household/tables/ZipFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> zip,factor
> 85001,1.10
> 85004,1.05
> ```
>
> ```csv household/tables/DeductibleFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> deductible,factor
> 250,1.15
> 500,1.00
> 1000,0.85
> ```

A rater imports the package and gets each package record's inputs in its own record of the same name:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
import "facts.rsl"
...
    garage_count: Num {precision: int} [ask: "How many of the cars are parked in a garage?", why: "Example Mutual rates garaged cars for less."]
        where self in [0, 9]
},
step premium [cite: manual:p7] {
    tickets: Num = sum(self.drivers as d, len(d.tickets)),
...
},
step premium [cite: manual:p5] {
    rideshare_factor: Factor = if self.rideshare then 1 + rideshare_load else 1,
    zip_factor: Factor = lookup!(ZipFactor, self.zip).factor,
```

| Binding | Meaning |
| - | - |
| `name where rule [code: …]` | The rater declines some answers. The code is required: [`E-NARROW`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-narrow). |
| `name as Type [code: …]` | The rater accepts a narrower type and declines the rest |
| `name when condition` | The rater asks the fact under its own condition |
| `[why: "…"]` | Why this rater asks. An input that the package lacks, other than a choice, requires it: [`E-QUESTION-WHY`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-question-why). |

* A rater binds a shared fact by its name alone. A second declaration gets
  [`E-FACT-REDECLARED`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-fact-redeclared), and an unknown name gets
  [`E-FACT-BINDING`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-fact-binding).
* A shared fact that the rater leaves unread is optional in its requests.
* A package holds `Types`, `Tables` and records of `inputs`:
  [`E-FACT-PACKAGE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-fact-package).

The package above uses the keys that serve a questionnaire:

| Key | On | Says |
| - | - | - |
| `called: "the {model_year} car\|car {#}"` | A list | How a question names an item. `\|` separates alternatives, and `{this}` in a question below stands for the name. |
| `ask_you: "…"` | An input of a list item | The question when the item is the person who answers |
| `you: "relationship = applicant"` | A list | Which item is the person who answers |
| `look_back: 5` | A list | The longest window in years that the rater reads; `{look_back}` in the list's question names it |
| `inline`, `rows` | A list | Items are added on the list's own screen; the question is asked of every item on one screen |
| `refers: "Policy.drivers"` | An input | The list whose item the value picks |
| `none_answer: "I don't have it"` | A follow-up input | An answer that sets the `Bool` question before it to `false` |
| `reduce: "1-8,'0',10"`, `check: "…"` | An input | Cuts a longer answer to the field's form, after its check digit holds |
| `found_on: "declarations_page"` | An input | The documents that print the answer |
| `gives: "declarations_page"` | A `Bool` input | The document a person has when the answer is true |
| `checklist: "Cars"`, `statement: "…"` | A `Bool` input | A row of a named checklist, and its words |
| `closed` | An input | Only the narrowed values are answers |
| `choices` | An enumeration in `Types` | The one list of shared coverage codes, each with its `shape` |
| `coverage: "collision"` | A `choice` | The shared coverage code the choice sells: [`E-CHOICE-CODE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-choice-code) |
| `steps: "30000/60000, 50000/100000"` | A `limit_pair` choice | The pairs of limits that are sold |

A host that shows the questions reads these keys as well.
[The input contract](https://docs.canary.effectiveai.app/rsl/reference/input-contract.md) shows each one in a checked rater.

| Key | On | Says |
| - | - | - |
| `short: "Annual miles"` | An input | A label of 40 characters at most, for a summary of the answers |
| `picker: "logo"` | An input | One lowercase word that names how a host shows a long list of answers |
| `earliest: "today"`, `latest: "today + 1 year"` | A `Date` input | The first and the last date an answer takes: `today`, a date input or a root path, plus or minus years, months or days |
| `date_precision: "month"` | A `Date` input | The person picks a month, and the answer is its first day |
| `priced_on` | A `Date` input | The date that a price is for |
| `add: "Add a roommate"` | A list of records | The words of the button that adds an item |
| `item_name: "First name"` | A list of records | A label under which a host asks each item for a name; the name stays with the host |
| `statement_you: "You smoke"` | A checklist row | The row's words for the person who answers |
| `attestation` | A `Bool` input | A statement that the person confirms, asked as a question of its own |

In a rater that imports a package, each choice states its `shape`:
[`E-CHOICE-SHAPE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/questions#e-choice-shape).

## Outcomes

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
outcome = name ":" Name "." name
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
RatingOutcomes = {
    premium: Policy.premium
}
```

* `RatingOutcomes` lists the calculations a program returns, each under a public name. The block holds one outcome or more.
* A target is a calculation of type `Premium`, `Num`, `Rate`, `Factor`, `Pct`, `Bool` or an enumeration. One outcome or more is a `Premium`.
* Each numeric outcome is rounded in a calculation that cites the rule.
* A rater evaluates what its outcomes read. [Results](https://docs.canary.effectiveai.app/rsl/reference/results.md) describes what a host returns.
* A wrong target gets [`E-RATING-OUTCOME`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/header#e-rating-outcome).

## Stages, labels, notes and concepts

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
stage  = name ":" string [ annotations ]
member = [ Name "." ] name [ ".." [ Name "." ] name ]
```

These forms describe the rating manual that the rater implements. They leave every premium as it is, and the check holds them to the program.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Stages = {
    classify: "Classification" [cite: manual:p4, note: "The garaging ZIP sets the territory and each driver gets a class."],
    vehicle: "Vehicle premium" [cite: manual:p9, note: "Each vehicle is rated for each coverage."],
    policy: "Policy premium" [cite: manual:p11, note: "The vehicle premiums are summed and the discounts, payment plan and fee apply."]
}
...
step territory in classify [label: "Territory and base rates"] {
    territory: Classification(Territory) [label: "Territory", cite: manual:p14] =
        lookup!(TerritoryByZip, self.garage.zip).territory,
...
telematics_discount: Credit(Factor) [label: "Telematics discount", note: "A safe-driving score lowers the premium, to the filed floor.", cite: manual:p10, collect: multi_car_discount..telematics_discount, title: "Policy discounts"] =
```

| Form | Rule |
| - | - |
| `Stages` | The stages of the manual, in reading order. Each has a title, and takes `cite` and `note`. |
| `step name in stage` | Places a step in a declared stage. The checks are [`E-STAGE-UNDECLARED`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-stage-undeclared), [`E-STAGE-MISSING`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-stage-missing) and [`E-STAGE-ORDER`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-stage-order). |
| `label: "…"` | A short display name for a record, step, table, value, stage or enumeration member |
| `note: "…"` | One sentence of 200 characters or fewer that explains a value: [`E-NOTE-SENTENCE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-note-sentence) |
| `Concept(Type)` | Names what a value is in the manual, such as `Credit(Factor)` |
| `collect: a + b..c` with `title: "…"` | Heads a group: the listed values, where `b..c` is every value from `b` to `c` in file order |

A concept wraps the type of a calculation, an input or a table column. The `rating@2` vocabulary holds these concepts:

| Role | Concepts |
| - | - |
| Rate | `BaseRate` |
| Factor | `Relativity`, `Credit`, `Surcharge`, `IncreasedLimits`, `Deductible`, `Composite`, `ExternalFactor`, `Adjustment`, `ScheduleRating` |
| Premium | `BasePremium`, `Subtotal`, `OptionPremium`, `ExpenseConstant`, `Rounding`, `PolicyPremium`, `Tax`, `Fee`, `Charge` |
| Fact | `Exposure`, `Classification`, `Default`, `Assignment`, `Limit`, `Attachment`, `Retention`, `Election`, `OptionParameter` |
| Constraint | `Bound`, `Eligibility`, `Validation`, `RateCap`, `MinimumPremium`, `MaximumPremium` |
| Choice | `Override`, `Selection` |

* A concept must fit the value's type and formula, or the check reports a message of the
  [rating manual topic](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual.md).
* Once a rater uses labels or concepts, the check prints hints for values that lack them: `H-LABEL-MISSING` and `H-CONCEPT-MISSING`. A hint leaves the check passing.
* In an RSL 0.4.1 file, arithmetic reads named values, constants and inputs. An inline `lookup!`, `if`, `match` or `round` inside arithmetic gets
  [`E-CHAIN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/manual#e-chain): name it as its own calculation.

## Scenarios

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
scenario_part = "instances" "=" "{" { Name name "=" "{" { setting } "}" } "}"
              | "evaluate" "=" "{" "targets" ":" "[" target { "," target } "]" "}"
              | "assert" "=" "{" { expression } "}"
setting       = name "=" expression | "__parent" ":" name | name ":" name
target        = name "." name [ "[" name "]" ]
```

A scenario file imports a rater, builds one request from named instances and states what to evaluate:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
import "rater.rsl"

# One household: two drivers, one car, paid in full.

Scenario = {
    instances = {
        Policy p = {
            effective_date = 2026-03-01,
            expiration_date = 2027-03-01,
            liability_limit = 100000,
            prior_insurance = true,
            prior_lapse_days = Some(0),
            payment = PaidInFull
        },
        Garage g = {
            __parent: p,
            zip = "85004",
            enclosed = true
        },
...
        Vehicle v1 = {
            __parent: p,
            number = 1,
            model_year = 2021,
            value = 27500.00,
            use = @Use.commute,
            annual_miles = 12000,
            deductible = 500,
            principal: d2
        }
    }
    evaluate = {
        targets: [
            v1.rated_age,
            v1.mileage_factor,
            p.premium
        ]
    }
    assert = {
        p.territory = @Territory.phoenix,
        v1.rated_age = 19,
        p.premium = 2240
    }
}
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc eval auto/scenario.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
PASS auto/scenario.rsl (passed)
targets: {"p.premium": {"Num": {"exact": "2240", "value": 2240.0}}, "v1.mileage_factor": {"Num": {"exact": "1.048", "value": 1.048}}, "v1.rated_age": {"Num": {"exact": "19", "value": 19.0}}}
PASS assertion: assert@p
PASS assertion: assert@v1
PASS assertion: assert@p
```

| Part | Rule |
| - | - |
| `instances` | `Record name = { … }` declares one record. `field = value` sets an input. |
| `__parent: p` | Places a record under the instance `p`, in the field of its record type |
| `principal: d2` | Sets a `Ref` field to another instance |
| `payment = PaidInFull` | Sets a union field to an arm |
| `prior_lapse_days = Some(0)` | Sets an `Option` field or a follow-up input |
| `evaluate` | Lists the values to print: `instance.calculation`, or `instance.calculation[member]` for a map |
| `assert` | Each line is a `Bool` expression over the instances. A false line fails the run with exit code 1. |

The scenario's request passes the same rules as a rating request. The file stands beside the rater that it imports.
[Test a rater](https://docs.canary.effectiveai.app/rsl/how-to/test-a-rater.md) covers expected premiums and refusals.

## Guidelines

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
use      = "use" name "=" string
chain    = name { "<" name }
act      = ( "quote" | "bind" ) ":" role
role     = "@" "Role" "." name
by       = "by" role
binding  = name "." name { "." name } "by" role
term     = "per" "quote"
referral = "refer" name "when" expression "to" role "because" string [ annotations ]
```

A guideline is an RSL file that uses a rater and adds who answers and who signs off. This excerpt comes from the shared example `staffing`:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
use ts = "rater.rsl"

Roles = { assistant < uw < manager }

Acts = { quote: @Role.uw, bind: @Role.uw }
...
Submission = {
    inputs {
        operation: Operation [cite: guideline:p1],
        years_in_operation: Num {precision: int} where self in [0, 300] [cite: guideline:p1],
        financially_sound: Bool by @Role.uw when self.operation = @Operation.peo [cite: guideline:p1],
        policy: ts.Policy,
        policy.experience by @Role.uw
    },
    step premium [cite: guideline:p2] {
        premium: Premium = ts(self.policy).premium
    }
} where self.operation != @Operation.peo or (self.years_in_operation >= established_years and self.financially_sound) [code: "PEO_NOT_ESTABLISHED", message: "A PEO must be in operation for at least 3 years and be financially sound.", cite: guideline:p1]
    refer new_venture when self.years_in_operation < established_years
        to @Role.manager because "The firm is in operation for less than 3 years." [cite: guideline:p1]
    refer large_account when self.premium >= large_premium
        to @Role.manager because "The premium is $10,000 or more." [cite: guideline:p2]
```

| Form | Rule |
| - | - |
| `use ts = "rater.rsl"` | Links a rater in the same folder under an alias. Its records read `ts.Policy`. |
| `policy: ts.Policy` | An input that holds one request of the used rater |
| `ts(self.policy).premium` | Calls the rater on that request and reads one of its rating outcomes |
| `match ts(self.policy) { Rated(r) => r.premium, Declined => 0 }` | Reads the call as a value: `Rated` binds the outcomes, and `Declined` covers a request that a coded rule of the rater declines |
| `Roles` | Chains of roles, each lowest first. A role may do what the roles below it do. |
| `Acts` | The lowest role for each act: `quote` and `bind`. The role of `quote` also declines a submission. |
| `x: T by @Role.uw` | The role that answers an input |
| `policy.experience by @Role.uw` | The role that answers an input of a record this record holds |
| `x: T per quote` | A term: each quote of a submission may hold its own answer to this input |
| `refer name when c to @Role.r because "…"` | A referral: where `c` holds, the submission needs that role's sign-off before a quote |

* A guideline reads a used rater through its inputs and its outcomes only:
  [`E-USE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/guidelines#e-use).
* Every role that `Acts`, `by` or `refer` names is declared in `Roles`:
  [`E-ROLE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/guidelines#e-role).
* A referral's name is distinct: [`E-REFER`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/guidelines#e-refer).
* In an input, `by` or `per quote` follows the type and its metadata, before `refines`, `from` and `when`.

### Terms of a quote

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
limit: Num {precision: int} per quote where self in [1000000, 2000000] [cite: rules:p3]
```

This line comes from the rater of `staffing`. `per quote` marks an input as a **term**. Each quote
of one submission may hold its own answer to a term. Every other input is a **fact**, and all
quotes of the submission share one answer to it.

* Only the submission log reads the mark. A rater rates the same with the mark and with the mark removed.
* A rater and a guideline each mark their own inputs. A term of a used rater is a term in each guideline that uses it.
* The mark covers its own input. On a record or an `Option`, it covers whether the record is present.
  A field of that record is a term only when the field carries the mark too.
* An input takes `by` or `per quote`, one of the two. An input with both gets
  [`E-ROLE`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/guidelines#e-role), because a role's answer is a judgment that every quote shares.

`rslc submission open` holds the marks to three more rules, and each breach stops it with `E-ROLE`:

| Rule | Reason |
| - | - |
| A term stands outside each path that a role answers, such as `policy by @Role.uw` | A role's answer covers every input under its path |
| A union or a list with the mark holds only inputs with the mark | A quote option answers a union or a list whole |
| Two arms of one union that share a field name mark that field alike | The log reads a path under a union before it knows the arm |

[Quote options](https://docs.canary.effectiveai.app/rsl/reference/submission-log.md#quote-options) describes how the log answers and quotes a term.

[Raters and guidelines](https://docs.canary.effectiveai.app/rsl/explanation/raters-and-guidelines.md) explains the split, and
[The submission log](https://docs.canary.effectiveai.app/rsl/reference/submission-log.md) records answers, sign-offs, quotes and binds.

## What the check proves

`rslc check` accepts a file only when every rule on this page holds, every lookup finds a row, every division has a divisor other than zero
and every request that passes the input rules gets a value for each outcome.
Each message has a code, and [Check messages](https://docs.canary.effectiveai.app/rsl/reference/check-messages.md) explains each code with an example and its fix.
[What rslc checks and guarantees](https://docs.canary.effectiveai.app/rsl/explanation/checks-and-guarantees.md) states what a passing check establishes.

## Removed forms

`rslc` still reads these older spellings so that it reports the current one. Each row gets an error;
[Messages for older forms](https://docs.canary.effectiveai.app/rsl/reference/check-messages/older.md) explains the codes.

| Removed | Write |
| - | - |
| `--` and `//` comments | `#` |
| `×`, `÷` | `*`, `/` |
| `Int`, `Integer`, `Decimal`, `Percent` | `Num`, `Pct` |
| `Text`, `Str`, `String` | An enumeration or a `Code` type: `E-TEXT-REMOVED` |
| `Axes`, `sum(x over axis)` | An enumeration in `Types` and a fold `sum(Enum as m, …)`: `E-AXES-REMOVED` |
| `Entities`, `[children]`, `[parent]`, `EntityId(R)` | `Records` with fields of type `R`, `Option(R)` or `List(R)`: `E-RELATION-REMOVED` |
| `requires { … }` | A `where` rule: `E-REQUIRES-REMOVED` |
| `{range: [a, b]}` metadata | `where self in [a, b]`: `E-RANGE-REMOVED` |
| `{default: …}` metadata | `Option(T)` with the default in the `None` arm: `E-DEFAULT` |
| `reject("CODE", "message")`, `checked(…)` | A `where` rule with a `code`: `E-REJECT-REMOVED`, `E-CHECKED-REMOVED` |
| `otherwise: reject(…)` | A narrower key type, or a cited `otherwise` row: `E-OTHERWISE-REJECT-REMOVED` |
| `round(x, increment)` | `round(x, scale, mode)`: `E-ROUND` |
| `match:` lines, `NumRange`, `_` cells in tables | Band and value-list dimensions: `E-TABLE-MODE`, `E-TABLE-WILDCARD` |
| `[extern: …]` tables | A table with its rows in `tables/Name.csv`: `E-EXTERN-REMOVED` |
| Statement slots such as `[by: …]` and `[chain: …]` | `collect` groups and named calculations: `E-SLOT-REMOVED` |
| `spec.name` | `spec.product` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.