Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Filters — Self-Correcting Cells

Grammar

cell_filter = "filter" expression.

A filter clause is optional and trails a cell, source, or out declaration’s type/initializer. Its expression is deduced exactly like a relationship binding’s, plus one reserved identifier: _ always refers to the candidate value being conformed (of the filtered cell’s own declared type), never a cell. _ is reserved inside a filter expression only; outside one it’s an ordinary identifier (or the conditional default-branch token). A filter carries no name — a cell can have at most one, so nothing ever needs to distinguish “which filter.”

cell level: i32 = 50 filter 0..=100;             // a fixed range
cell level: i32 = 50 filter 0..=max;             // upper bound is another cell
cell level: i32 = 50 filter clamp(_, 0, max);    // an arbitrary expression over `_`

A filter expression must reference _ at least once (unless it’s a range expression, see range filters) and must produce a value of exactly the filtered cell’s own type; violating either is a parse-time error, not a runtime one.

Filters, including range-bound evaluators, must be purely functional: equal candidate and argument values produce equivalent values or errors, without externally observable side effects or reads of mutable external state. A conforming filter must also be idempotent: filtering its result again with the same arguments leaves that result unchanged. There is no guarantee that a filter executes, or when or how often it executes. The sheet may reuse a result while preserving conformance and diagnostics.

Writing never filters

This is the single most important rule in this chapter: writing a cell always stores exactly the value it was given, filter or no filter. A filter is applied live, when the sheet resolves, against the cell’s own current value, never synchronously at the moment of the write.

sheet s {
    source level: i32 = 50 filter 0..=100;
}

Writing a filtered cell never fails because of the filter, and never reports an error on the filter’s account. Whatever you write is exactly what a read shows until the sheet next resolves: the same “a read reflects the last full resolution, not a per-write side effect” rule every other cell in a sheet already follows.

The raw value is never lost

A filtered cell keeps two values: its raw last-written value, the source, and, when something currently claims it, a computed override, the derived value. Reading the cell always returns the derived value if one is present, the source value otherwise. A filter’s live output always lands in the derived value, never in the source, so a filtered cell’s original input is never destroyed, even after many rounds of clamping. If a dynamic bound loosens back up, the cell springs back to exactly what was last written, not to some intermediate clamped value:

sheet spring_back {
    source max: i32 = 100 filter 0..=200;
    source level: i32 = 50 filter 0..=max;
}

This is the same rule the default branch and reverting to source section already showed for a relationship’s method: a method’s output (and a filter’s output) always lands in the derived value, so nothing a computation produces can ever permanently overwrite what was actually written.

Range filters

A filter expression whose type is CEL’s lo..=hi range (over any type this book’s built-in numeric types supports) is recognized structurally as a range filter: resolving the sheet clamps into [lo, hi] instead of running the expression as an arbitrary function of _, and the sheet can report the range’s current live bounds without needing a candidate value at all:

sheet s {
    source level: i32 = 50 filter 0..=100;
}

A range filter’s body is exempt from the “must reference _” rule stated in the Grammar above: a genuine range expression like 0..=max has no reason to mention _ at all, since both endpoints are independent of the value being conformed. This book’s own live examples mount an editable widget bound to the filtered cell whose displayed min/max track the range’s current live bounds — try the example in the Filters section of the tutorial.

Derived cells: diagnosed, never corrected

A filter attaches to one cell, but that cell isn’t always a source: a relationship may claim it instead (see the relationships chapter). When that happens, the filter no longer has any authority to change the value: it only observes. The sheet still resolves successfully, and the out-of-range value is still what a read returns; the sheet simply records that the filter is violated:

sheet diagnose_only {
    cell bound: i32 = 100 filter 0..=100;
    source driver: i32 = 500;

    relationship {
        bound := driver;
    }
}

Resolving the sheet never fails because of a filter violation, on either side (source or derived): a filter is a diagnostic and a self-correction mechanism, never a gate. A host UI can query which cells currently have a violated filter; see the host embedding API that exposes this.

A filter on an output cell

A filter isn’t limited to a plain cell: an out declaration’s grammar carries the same optional cell_filter clause (see Grammar), trailing its := initializer instead of a cell declaration’s own initializer. Everything above applies unchanged — an out cell is always derived (derived cells: diagnosed, never corrected is the only case that ever actually applies to one), so a filter attached to an out cell is a pure diagnostic, exactly like a filter on any other cell a relationship currently claims:

sheet s {
    source width: i32 = 4;
    out area := width filter 0..=100;
}

A source declaration’s grammar carries the same optional cell_filter clause too — see a source cell can be filtered too. A filter attaches to any cell kind (cell, source, or out) exactly the same way; the grammar has no per-kind restriction.

Errors

The parser catches filter-declaration mistakes before resolving the sheet: a non-range filter body that never references _ fails with “filter must reference _”, and a body with the wrong complete type fails with “filter must produce T”. Filters accept tuples and arrays as well as concrete leaves; compound results must match every nested field and element type. At most one filter may be attached per cell.