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 cell can have at most one filter; filters have no names.

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

Writing a cell stores exactly the supplied value without applying its filter. The filter applies when the sheet resolves.

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

The filter cannot cause a write to fail or report an error. A read returns the written value until the sheet next resolves.

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 writes only the derived value, preserving the original input across resolutions. If a dynamic bound loosens, the cell returns toward its last-written value rather than an intermediate clamped value:

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

Self-referencing and forced methods also preserve source values; see reverting to source.

Range filters

A CEL inclusive range over a built-in numeric type defines a range filter. Resolution clamps the candidate into [lo, hi]. The sheet can report the current bounds independently of a candidate value:

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

A range filter need not reference _; its endpoints are independent of the candidate. The tutorial’s live example displays bounds that track the range.

Derived cells: diagnosed, never corrected

When a relationship claims a filtered cell, the filter diagnoses violations without changing the derived value. A violation does not prevent resolution; reads still return the relationship’s result:

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

    relationship {
        bound := driver;
    }
}

A filter violation on a source or derived cell never causes resolution to fail. Hosts can query violations through the embedding API.

A filter on an output cell

An out declaration accepts a filter after its := initializer. An output cell is always derived, so its filter only diagnoses violations:

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

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.