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

# Check messages: tables

> A table declares its key dimensions and its columns. These messages concern table declarations, CSV files and lookups.

*Generated from the `rslc` compiler.*

## E-TABLE-COL

**Each table cell fits its declared column.**

A table declares its key columns in `dims (…)` and its output columns in the record after `->`. The rows live in `tables/<Name>.csv`. The check reads every cell with the type of its column and reports a cell that the type rejects.

A rate column holds plain decimals, and an enum key holds the enum's values. Currency signs, thousands separators, percent signs and labels such as `n/a` belong to the exhibit's layout. Reading the whole file at check time means a mistyped cell stops the build before a quote reaches it.

The message names the table, the CSV row and the column; the header is row 1. The same code covers a table declaration with a wrong shape, as the other causes list.

**Example**

The `rate` cell for `rural` carries a dollar sign, so the cell is text where the column expects a number.

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
territory,rate
urban,2.40
rural,$1.85
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-TABLE-COL bad/rater.rsl:24:5: table `TerritoryRate` (Table:0), CSV row 3, column `rate` (Column:1) (header `rate`): invalid numeric cell: `$1.85`
cost premium: 3 (each list of size 1: 3, 3: 3, 10: 3)
```

**Fix**

Write the cell as a plain decimal.

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
territory,rate
urban,2.40
rural,1.85
```

**Other causes**

* A key cell holds a value outside its dimension type, such as `Rural` for the enum value `rural`. Write the cell as the type spells it.
* A key cell of a `Code` dimension fails the pattern. The message text then carries `E-CODE-PATTERN:` after the cell.
* A row marks some outputs `unfiled` and files others. Mark every output of an absent row `unfiled`, or split the table by output.
* A dim has a plain type such as `Num`. Key a number with a bands or values type, an identifier with `Code`, and a label with an enum or `Bool`.
* An output has a bands or values type. Declare the output as a number, `Bool`, `Date`, an enum or a `Code`.
* A table names one dim twice. Give each dim its own name.
* A `columns:` entry declares a type. Keep only the CSV header there, as in `rate: "base_rate"`.
* An `otherwise` row omits an output or sets one twice. Set every output once.
* An output typed `Key(T)` holds a cell that names an absent row of table `T`. Add the row to `T` or correct the cell.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,$1.85
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```

## E-LOOKUP-SYNTAX

**A table read uses its checked spelling.**

A calculation reads a table with `lookup!` or `interpolate!`. The `!` states a promise the compiler proves: every key the read can receive has a filed row. A read that lacks a row for some key gets its own message at check time, so a quote always finds its cell.

The check asks for the two spellings exactly. `lookup!` takes the table and one key per dim. `interpolate!` takes the same and three roundings, `ratio:`, `step:` and `result:`, each written once.

**Example**

The `rate` calculation calls `lookup` and leaves out the `!`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        },
        step rating {
            rate: Rate = lookup(TerritoryRate, self.territory).rate,
            premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
        }
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-LOOKUP-SYNTAX bad/rater.rsl:37:26: lookup must use the proven-total `lookup!(...)` spelling
```

**Fix**

Write the read as `lookup!`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        },
        step rating {
            rate: Rate = lookup!(TerritoryRate, self.territory).rate,
            premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
        }
```

**Other causes**

* `interpolate` is written bare. Write `interpolate!`.
* `!` follows another function, as in `round!(…)`. Keep `!` for `lookup!` and `interpolate!`.
* `lookup!` carries `ratio:`, `step:` or `result:`. Those roundings belong to `interpolate!`.
* `interpolate!` omits one of its three roundings. Write `ratio:`, `step:` and `result:` with the scale and mode the filing states.
* `interpolate!` writes one rounding twice. Write each rounding once.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```

## E-DIMENSION

**Dimension types are well formed and key tables.**

A dimension type describes a table key. `IntBands` and `DecimalBands` cut a number into bands. `IntValues` and `DecimalValues` list exact values, and `Code` names an identifier by its pattern. The check asks that each declaration is well formed and that a band type appears only as a table dim.

An input or a calculation holds the number itself, and the lookup places that number in its band. Keeping the number apart from its bands lets one input feed several tables, each with its own cuts.

**Example**

The input `payroll` is typed with the band type `PayrollBand`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
    Policy = {
        inputs {
            payroll: PayrollBand
        },
        step rating {
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-DIMENSION bad/rater.rsl:34:13: attribute `payroll` has band type `PayrollBand`; bands key tables. Declare the attribute as a number, for example `payroll: Num {precision: int}`, and key the table column with `[type: PayrollBand]`
```

**Fix**

Declare the input as a number with a range. The dim in `table dims (…)` already carries the band type, so leave the table as written. The `[type: …]` column annotation that the message names is a removed form.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
    Policy = {
        inputs {
            payroll: Num {precision: int} where self in [0, 100000000]
        },
        step rating {
```

**Other causes**

* A calculation is typed with a band or values type. Type it as a number.
* `Option` or `List` wraps a band or values type. Wrap a number.
* Band cuts or listed values descend. List them in ascending order, each once.
* An open top such as `1000000..` sits before the last item. Put it last, and put an open bottom such as `..0` first, followed by its cut.
* A bands type lists a single cut or stays empty. Write two cuts or more, or an open tail such as `IntBands(0..)`.
* A values type holds a band item such as `1000000..`. Use a bands type for bands.
* `DecimalBands` or `DecimalValues` lacks its scale. Declare it first, as in `DecimalBands(scale: 2; 0, 0.5, 1..)`, with a whole number from 0 to 15.
* A declaration passes an unknown parameter, such as `scale` on `IntBands`. The message lists the parameters the keyword accepts.
* A bands type declares `key:` with a value other than `unrounded`. Round the key at the lookup with `round(…)`.
* A `Code` type lists items. Give it one `pattern` parameter.

When a broken type keys a table, the dim gets `E-TABLE-COL` as a second message. Correct the type and both messages clear.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollBand = IntBands(0, 250000, 1000000..)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     SizeFactor [cite: manual:p5] :
>         table dims (payroll: PayrollBand) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: PayrollBand
>         },
>         step rating {
>             size_factor: Factor = lookup!(SizeFactor, self.payroll).factor,
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.size_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/SizeFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> payroll,factor
> 0,1.00
> 250000,0.95
> 1000000,0.90
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollBand = IntBands(0, 250000, 1000000..)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     SizeFactor [cite: manual:p5] :
>         table dims (payroll: PayrollBand) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000]
>         },
>         step rating {
>             size_factor: Factor = lookup!(SizeFactor, self.payroll).factor,
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.size_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/SizeFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> payroll,factor
> 0,1.00
> 250000,0.95
> 1000000,0.90
> ```

## E-BAND-SENTINEL

**An outer band is open at its end.**

A bands type cuts a number into contiguous bands. An exhibit often prints the last band as `1,000,000 – 999,999,999`, where the nines stand for "and above". The check asks you to write what the filing means: an open tail, `1000000..`.

Written as a cut, the placeholder closes the top band at that number, and a larger value falls outside the table. An open tail keeps every large value in the last band. The input's own range then states the largest value the rater accepts.

The check reads a bound as a placeholder when its digits are all nines, three or more, as in `999` or `99999.99`. A last cut one step above such a number, like `1000000`, counts too, since it closes the top band at `999999`.

**Example**

`PayrollBand` ends with the cut `999999999`, a placeholder for the top of the last band.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Types = {
    PayrollBand = IntBands(0, 250000, 1000000, 999999999)
}
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-BAND-SENTINEL bad/rater.rsl:17:5: bound 999999999 in `PayrollBand` is a sentinel placeholder. Write an open tail: `1000000..`
```

**Fix**

End the bands with an open tail on the last real cut.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Types = {
    PayrollBand = IntBands(0, 250000, 1000000..)
}
```

**Other causes**

* The first cut is a placeholder such as `-999999`. Start the bands with an open bottom, as in `IntBands(..<250000, 250000, 1000000..)`. The CSV row for the open bottom has the key cell `..`.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollBand = IntBands(0, 250000, 1000000, 999999999)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     SizeFactor [cite: manual:p5] :
>         table dims (payroll: PayrollBand) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000]
>         },
>         step rating {
>             size_factor: Factor = lookup!(SizeFactor, self.payroll).factor,
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.size_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/SizeFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> payroll,factor
> 0,1.00
> 250000,0.95
> 1000000,0.90
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollBand = IntBands(0, 250000, 1000000..)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     SizeFactor [cite: manual:p5] :
>         table dims (payroll: PayrollBand) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000]
>         },
>         step rating {
>             size_factor: Factor = lookup!(SizeFactor, self.payroll).factor,
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.size_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/SizeFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> payroll,factor
> 0,1.00
> 250000,0.95
> 1000000,0.90
> ```

## E-BAND-DEGENERATE

**Each band cut appears once.**

Bands are contiguous: each cut starts a band that runs up to the next cut. The check asks that each cut appears once. A cut written twice makes a band from a number to itself, and that band is empty.

A repeated cut usually comes from an exhibit that prints both ends of each band, such as `0 – 250,000` and `250,000 – 1,000,000`. One cut serves as the end of the first band and the start of the second.

When the exhibit lists exact values in place of ranges, declare the type with `IntValues` or `DecimalValues`.

**Example**

`PayrollBand` lists the cut `250000` twice.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Types = {
    PayrollBand = IntBands(0, 250000, 250000, 1000000..)
}
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-BAND-DEGENERATE bad/rater.rsl:17:5: `PayrollBand` repeats cut 250000; the band [250000, 250000) is empty. Remove the repeated cut, or use `IntValues` when the values are exact
```

**Fix**

List the cut once. When the type keys a table, the dim also gets `E-TABLE-COL`, and both messages clear together.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Types = {
    PayrollBand = IntBands(0, 250000, 1000000..)
}
```

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollBand = IntBands(0, 250000, 250000, 1000000..)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000]
>         },
>         step rating {
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * rate_per_1000, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollBand = IntBands(0, 250000, 1000000..)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000]
>         },
>         step rating {
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * rate_per_1000, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```

## E-DECIMAL-SCALE

**Each number lies on its declared precision.**

Every number in a rater sits on a declared grid. An input's `precision`, the `scale` of a decimal bands or values type, and an output column's precision each fix the decimal places a value has. The check asks that every number written against a grid lies on it.

A value off the grid is unreachable, because admission accepts inputs at the declared precision only. A range endpoint, band cut or table cell with extra decimal places therefore points to a typing slip or a wrong precision. Finding it at check time keeps the rater's numbers identical to the filing's.

**Example**

The input `payroll` has precision `0.01`, and the upper end of its range has three decimal places.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
    Policy = {
        inputs {
            payroll: Num {precision: 0.01} where self in [0, 99999999.999]
        },
        step rating {
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-DECIMAL-SCALE bad/rater.rsl:23:13: range endpoint 99999999.999 of input `Policy.payroll` is not a multiple of its precision 10^-2; write an endpoint on the declared grid
cost premium: 1 (each list of size 1: 1, 3: 1, 10: 1)
```

**Fix**

Write the endpoint at the input's precision. When the filing states three decimal places, declare `precision: 0.001` and keep the endpoint.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
    Policy = {
        inputs {
            payroll: Num {precision: 0.01} where self in [0, 99999999.99]
        },
        step rating {
```

**Other causes**

* A cut or value of a decimal type has a different count of fraction digits from its `scale`. With `scale: 2`, write `0.50` and `1.00`.
* A cut or value of `IntBands` or `IntValues` has a fraction. Write whole numbers, or declare a decimal type with a scale.
* An `otherwise` cell has more decimal places than its output's precision. Write the cell at that precision.
* A quoted `api_default`, such as `[api_default: "0.5"]` on a whole-number input, lies off the input's grid. Write a default on the grid.
* A CSV output cell has more decimal places than its column's precision. That message starts with `error: invariant runner:` and carries `E-DECIMAL-SCALE:` in its text.
* A key cell differs from the declared spelling of its cut or value, such as `0` for `0.00`. That message has the code `E-TABLE-COL` and carries `E-DECIMAL-SCALE:` in its text.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: 0.01} where self in [0, 99999999.999]
>         },
>         step rating {
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * rate_per_1000, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: 0.01} where self in [0, 99999999.99]
>         },
>         step rating {
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * rate_per_1000, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```

## E-TABLE-WILDCARD

**Each key cell names one exact value.**

Every key cell of a table CSV names one value of its dim. The check reports a key cell that holds `_`, a wildcard that stands for any value.

Exact cells give each key one row. The rows read in any order, and a reviewer finds the rate for a territory in a single row. A wildcard row would overlap the rows beside it and make the answer depend on row order.

The all-other case has its own form: `otherwise` on the table declaration, with a citation to the filed all-other row.

**Example**

Row 3 of the CSV uses `_` to give every remaining territory the rate 1.85.

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
territory,rate
urban,2.40
_,1.85
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-TABLE-WILDCARD bad/rater.rsl:25:5: CSV row 3 of `TerritoryRate` has `_` in key `territory`. Cells are exact. Write one row for each value of `territory`, or end the table with `otherwise` if this row is the all-other case. Rewrite an older rater in RSL 0.4 with this rslc
cost premium: 3 (each list of size 1: 3, 3: 3, 10: 3)
```

**Fix**

Write one row for each value the wildcard stood for.

When the filing states an all-other row, delete the `_` row and declare it on the table: `otherwise [cite: manual:p4]: { rate: 1.85 }`. That form suits a `Code` key, whose values are too many to list.

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
territory,rate
urban,2.40
suburban,1.85
rural,1.85
```

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         suburban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> _,1.85
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         suburban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> suburban,1.85
> rural,1.85
> ```

## E-LOOKUP-OVERLAP

**Each table key appears on one row.**

Each row of a table CSV names one key tuple: one value for each dim. The check asks that every tuple appears on a single row.

Two rows with the same key leave the read ambiguous, since either rate could apply. With distinct keys the table reads the same in any row order. A reviewer matches each row to one line of the exhibit.

Repeated keys usually have one of two origins. A row was pasted twice, or the exhibit varies by a key that the table leaves out, such as a class or a coverage.

**Example**

The CSV gives `urban` two rows with two rates.

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
territory,rate
urban,2.40
urban,2.55
rural,1.85
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-LOOKUP-OVERLAP bad/rater.rsl:24:5: CSV rows 2 and 3 of `TerritoryRate` all match (territory: urban). Rows must be disjoint: remove the repeated rows, or split the dimension so each row names one key tuple
cost premium: 3 (each list of size 1: 3, 3: 3, 10: 3)
```

**Fix**

Keep the row the filing states and remove the other.

When the exhibit prints both rates, it varies by a second key. Add that key as a dim, as in `table dims (territory: Territory, class: Class)`, so each row names one tuple.

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
territory,rate
urban,2.40
rural,1.85
```

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> urban,2.55
> rural,1.85
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```

## E-OTHERWISE-DEAD

**An `otherwise` row covers a key.**

`otherwise` on a table states the filed all-other row: the outputs for every key tuple that the CSV rows leave out. The check asks that at least one tuple reaches it.

When the rows already name every value of every dim, `otherwise` holds a number that every quote skips. A reviewer would still compare that number with the filing. Deleting it leaves the table as the exhibit prints it.

Rows can cover every value of an enum, `Bool` or bands dim. A `Code` dim has an open set of values, so `otherwise` on its table stays live.

**Example**

`TerritoryRate` has a row for each of the two territories and also declares `otherwise`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            rate: Rate
        }
        otherwise [cite: manual:p4]: { rate: 2.00 }
}
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-OTHERWISE-DEAD bad/rater.rsl:28:9: the rows of `TerritoryRate` cover all 2 key tuples, so `otherwise` never applies. Delete the `otherwise` row
cost premium: 3 (each list of size 1: 3, 3: 3, 10: 3)
```

**Fix**

Delete the `otherwise` row.

When the filing does state an all-other rate, some CSV row is surplus. Remove the rows that the all-other rate covers.

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

**Other causes**

* An `otherwise: reject(…)` clause sits on a table whose lookups all find a row. The message comes beside `E-OTHERWISE-REJECT-REMOVED`; delete the clause.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
>         otherwise [cite: manual:p4]: { rate: 2.00 }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = {
>         urban,
>         rural
>     }
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```

## E-INTERP-KEY

**An interpolated table is keyed by filed points.**

`interpolate!` reads a table between two filed points. The check asks that the table's last dim lists those points as a values type, `IntValues` or `DecimalValues`, with two points or more.

A band row gives one factor to a whole interval, so the factor stays flat between cuts. A values row gives the factor at one exact point. For a key between two neighbouring points, `interpolate!` computes the factor with the roundings you write.

Use `lookup!` on bands when the filing states one factor for each interval. Use `interpolate!` on values when the filing says to interpolate.

**Example**

`SizeFactor` is read with `interpolate!`, and its dim `payroll` has a band type.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Types = {
    PayrollPoint = IntBands(0, 500000, 1000000..)
}
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-INTERP-KEY bad/rater.rsl:38:17: `interpolate!` reads filed points, but dim `payroll` of `SizeFactor` is the band type `PayrollPoint`: a band row is one factor for a whole interval, and `lookup!` on it would take the lower point's factor. Declare the filed points as a values type, for example `payrollPoint = IntValues(p1, p2, …, pn)` (or `DecimalValues`), and key `SizeFactor` by it. A filed interval with one factor stays a band read by `lookup!`
```

**Fix**

Declare the filed points as a values type. The CSV keeps one row for each point.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Types = {
    PayrollPoint = IntValues(0, 500000, 1000000)
}
```

**Other causes**

* The values type lists a single point. Read that table with `lookup!`.
* The points dim sits before another dim. Move it to the last place in `dims (…)`, and pass its key last.
* The table declares `otherwise`. Delete it, and write a filed clamp around the key, as in `min(self.payroll, 1000000)`.
* The column that `interpolate!` reads holds an enum, a `Bool` or a date. Interpolate a numeric column.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollPoint = IntBands(0, 500000, 1000000..)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     SizeFactor [cite: manual:p5] :
>         table dims (payroll: PayrollPoint) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 1000000]
>         },
>         step rating {
>             size_factor: Factor [cite: manual:p5] =
>                 interpolate!(SizeFactor, self.payroll, ratio: (4, half_up), step: (4, half_up), result: (3, half_up)).factor,
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.size_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/SizeFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> payroll,factor
> 0,1.000
> 500000,0.900
> 1000000,0.850
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     PayrollPoint = IntValues(0, 500000, 1000000)
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     SizeFactor [cite: manual:p5] :
>         table dims (payroll: PayrollPoint) -> {
>             factor: Factor
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 1000000]
>         },
>         step rating {
>             size_factor: Factor [cite: manual:p5] =
>                 interpolate!(SizeFactor, self.payroll, ratio: (4, half_up), step: (4, half_up), result: (3, half_up)).factor,
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.size_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/SizeFactor.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> payroll,factor
> 0,1.000
> 500000,0.900
> 1000000,0.850
> ```

## E-MAP-DATA-SIZED

**Read one member of a CSV-backed enum.**

An enum declared as `Enum` takes its members from `enums/<Name>.csv`. The check asks that a rater reads one member of such an enum at a time. A map keyed by the enum, a fold over it and an `in` test against it each visit every row of the file.

An inline enum such as `{ fire, wind, theft }` is part of the program, and its three members are three calculations in every trace. A CSV-backed enum can hold thousands of territories, and a data update adds more. A fold over it would make the work and the trace of each quote grow with the file.

A table keyed by the enum gives the same answer in one read: `lookup!` finds the row for the policy's own territory.

**Example**

`Territory` is a CSV-backed enum, and the calculation `rate` folds over all of its members to pick the policy's own rate.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        },
        step rating {
            rate: Rate = sum(Territory as t, if t = self.territory then lookup!(TerritoryRate, t).rate else 0),
            premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
        }
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-MAP-DATA-SIZED bad/rater.rsl:34:26: `Territory` is a CSV-backed enum: its size is data, not program, so a map or an enum fold keyed by it would make the work, the trace and the catalog data-sized. Key a table by `Territory` and read one cell, such as `lookup!(Factors, self.territory).factor[c]`
```

**Fix**

Read the one cell the policy needs: key the lookup by the `territory` input.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        },
        step rating {
            rate: Rate = lookup!(TerritoryRate, self.territory).rate,
            premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
        }
```

**Other causes**

* A map such as `rate[t: Territory]` is keyed by a CSV-backed enum. Each read of that map then gets `E-UNRESOLVED` as a second message.
* An `in` test names a refined type of a CSV-backed enum. Add a `Bool` column to a table keyed by the enum and read that cell.
* Nested folds over inline enums evaluate their body more than 4096 times. Read a table keyed by the enums, or fold over a refined enum with fewer members.

> [accordion] Complete files
>
> ```csv bad/enums/Territory.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> value
> urban
> rural
> ```
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = Enum
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = sum(Territory as t, if t = self.territory then lookup!(TerritoryRate, t).rate else 0),
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```
>
> ```csv good/enums/Territory.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> value
> urban
> rural
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = Enum
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Territory
>         },
>         step rating {
>             rate: Rate = lookup!(TerritoryRate, self.territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> urban,2.40
> rural,1.85
> ```

## E-KEY-TYPE

**Use `Key(T)` on an input or table column.**

`Key(T)` is the type of a value that names a filed row of table `T`. On an input it tells admission to accept only the keys that the CSV of `T` files. On a table output it says each cell names a row of `T`. Either way, a lookup of `T` with that value always finds its row.

The check asks that `Key(T)` appears in those two places only. It also asks that `T` has one dim, keyed by a `Code` or an enum.

A calculation gets its key fact from what it reads. When every branch yields a key of `T`, the compiler already knows the result names a row. A calculation therefore carries the key's own type, and the proof travels with it.

**Example**

The calculation `rating_territory` picks one of two `Key(TerritoryRate)` inputs and is itself typed `Key(TerritoryRate)`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        },
        step rating {
            rating_territory: Key(TerritoryRate) =
                if self.rate_at_worksite then self.worksite_territory else self.territory,
            rate: Rate = lookup!(TerritoryRate, self.rating_territory).rate,
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-KEY-TYPE bad/rater.rsl:36:31: `rating_territory` is a calculation typed `Key(TerritoryRate)`. `Key(T)` types an input or a table column; a calculation that reads a key carries its key fact already. Type `rating_territory` with the key's type
```

**Fix**

Type the calculation with the key's own type, here the `Code` type `Territory`. The lookup stays total, because both branches read a key of `TerritoryRate`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        },
        step rating {
            rating_territory: Territory =
                if self.rate_at_worksite then self.worksite_territory else self.territory,
            rate: Rate = lookup!(TerritoryRate, self.rating_territory).rate,
```

**Other causes**

* `Key(…)` names a type or an unknown name, as in `Key(Territory)`. Name a table declared in `Tables`.
* The table has two dims or more. Key it by one `Code` or enum whose values each name a filed row.
* The table is keyed by a bands or values type. Type the input as a number with a range.
* A table dim is typed `Key(…)`. Type the dim with the `Code` or enum itself.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = Code(pattern: "[0-9]{3}")
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Key(TerritoryRate),
>             worksite_territory: Key(TerritoryRate),
>             rate_at_worksite: Bool
>         },
>         step rating {
>             rating_territory: Key(TerritoryRate) =
>                 if self.rate_at_worksite then self.worksite_territory else self.territory,
>             rate: Rate = lookup!(TerritoryRate, self.rating_territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> 101,2.40
> 102,1.85
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Territory = Code(pattern: "[0-9]{3}")
> }
>
> Tables = {
>     TerritoryRate [cite: manual:p4] :
>         table dims (territory: Territory) -> {
>             rate: Rate
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             territory: Key(TerritoryRate),
>             worksite_territory: Key(TerritoryRate),
>             rate_at_worksite: Bool
>         },
>         step rating {
>             rating_territory: Territory =
>                 if self.rate_at_worksite then self.worksite_territory else self.territory,
>             rate: Rate = lookup!(TerritoryRate, self.rating_territory).rate,
>             premium: Premium [cite: manual:p3] = round(self.payroll / 1000 * self.rate, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/TerritoryRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,rate
> 101,2.40
> 102,1.85
> ```

## E-GEOGRAPHY

**A geography declares every field of its grid.**

A geography assigns a rating cell from the latitude and longitude of a risk. Its declaration in `Geographies` names a grid method, the size and origin of a cell, and the table that lists the filed cells. `locate(G, latitude, longitude)` then returns the key of the filed cell that holds a point.

The check asks that each declaration has exactly the fields of its method, each with a valid value. It also asks that each `locate` call names a declared geography and passes two numbers. A grid with a wrong field would place risks in the wrong cells, so the check settles the declaration first.

`degree_grid` takes `cell`, `origin_latitude`, `origin_longitude`, `cells`, `latitude`, `longitude` and `on_line`. `projected_grid` takes `crs`, `cell`, `origin_x`, `origin_y`, `cells`, `latitude`, `longitude` and `on_line`.

**Example**

The geography `Zone` is a `degree_grid` and sets `crs`, a field of `projected_grid`.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Geographies = {
    Zone [cite: manual:p6] = degree_grid {
        crs: esri_102008,
        cell: 1,
        origin_latitude: 0,
```

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc check bad/rater.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
FAIL bad/rater.rsl (strict)
error: E-GEOGRAPHY bad/rater.rsl:35:9: geography `Zone` has no field `crs`; degree_grid takes cell, origin_latitude, origin_longitude, cells, latitude, longitude, on_line
```

**Fix**

Delete the `crs` line. A degree grid works in latitude and longitude directly.

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Geographies = {
    Zone [cite: manual:p6] = degree_grid {
        cell: 1,
        origin_latitude: 0,
```

**Other causes**

* A field of the method is missing, or one field is set twice. Set each field once.
* The method is a name other than `projected_grid` or `degree_grid`.
* `on_line` has a value other than `least_key`, or `crs` a value other than `esri_102008`.
* A number is out of range, such as `cell: 0` or `origin_latitude: 100`.
* `cells` names something other than a table, or a table whose key is other than one `Code` dim.
* `latitude` or `longitude` names a column that the cell table lacks, or a column that holds an enum.
* `locate` names an undeclared geography or passes fewer than three arguments.
* A `locate` coordinate is a `Bool` or an enum. Pass numbers in decimal degrees.

A declaration that fails leaves its geography unresolved. Each `locate` that reads it then gets a second `E-GEOGRAPHY` message, and often `E-PATTERN` and `E-TYPE` on the `match` around it. Correct the declaration and all of them clear.

> [accordion] Complete files
>
> ```rsl bad/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     CellId = Code(pattern: "c[0-9]")
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     Cell [cite: manual:p6] :
>         table dims (cell_id: CellId) -> {
>             factor: Factor,
>             latitude: Num {precision: 0.1},
>             longitude: Num {precision: 0.1}
>         }
> }
>
> Geographies = {
>     Zone [cite: manual:p6] = degree_grid {
>         crs: esri_102008,
>         cell: 1,
>         origin_latitude: 0,
>         origin_longitude: 0,
>         cells: Cell,
>         latitude: latitude,
>         longitude: longitude,
>         on_line: least_key
>     }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             latitude: Num {precision: 0.000001} where self in [-90, 90],
>             longitude: Num {precision: 0.000001} where self in [-180, 180]
>         },
>         step rating {
>             zone_factor: Factor =
>                 match locate(Zone, self.latitude, self.longitude) {
>                                Some(c) => lookup!(Cell, c).factor,
>                                None => 1
>                            },
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.zone_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv bad/tables/Cell.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> cell_id,factor,latitude,longitude
> c1,1.10,32.5,-111.5
> c2,1.00,32.5,-110.5
> c3,1.25,33.5,-111.5
> c4,0.95,33.5,-110.5
> ```
>
> ```rsl good/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Staffing Liability",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     CellId = Code(pattern: "c[0-9]")
> }
>
> Constants = {
>     rate_per_1000 [cite: manual:p3] = 2
> }
>
> Tables = {
>     Cell [cite: manual:p6] :
>         table dims (cell_id: CellId) -> {
>             factor: Factor,
>             latitude: Num {precision: 0.1},
>             longitude: Num {precision: 0.1}
>         }
> }
>
> Geographies = {
>     Zone [cite: manual:p6] = degree_grid {
>         cell: 1,
>         origin_latitude: 0,
>         origin_longitude: 0,
>         cells: Cell,
>         latitude: latitude,
>         longitude: longitude,
>         on_line: least_key
>     }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000],
>             latitude: Num {precision: 0.000001} where self in [-90, 90],
>             longitude: Num {precision: 0.000001} where self in [-180, 180]
>         },
>         step rating {
>             zone_factor: Factor =
>                 match locate(Zone, self.latitude, self.longitude) {
>                                Some(c) => lookup!(Cell, c).factor,
>                                None => 1
>                            },
>             premium: Premium [cite: manual:p3] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.zone_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv good/tables/Cell.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> cell_id,factor,latitude,longitude
> c1,1.10,32.5,-111.5
> c2,1.00,32.5,-110.5
> c3,1.25,33.5,-111.5
> c4,0.95,33.5,-110.5
> ```


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