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

The Adam Programming Language

A tutorial and reference manual for Adam, the declarative language for describing property models (sheets of cells linked by multi-way constraints), implemented by the adam_lang crate and executed by adam_rs.

Why Adam

Many of an application’s properties are related to each other: a document’s own structure holds invariants among them, a command’s arguments and result constrain one another, and some relationships exist purely to construct a new argument or document value, such as pixels == inches * resolution so that entering any two of the three determines the third.

An application usually hand-codes each of these relationships as UI and scripting logic: one event handler recomputes pixels when resolution changes, another recomputes inches when pixels changes, and so on for every direction the relationship can be driven, repeated at every control that touches the value. Adam models the relationship once, as a declarative constraint; its solver derives whichever value the current interaction requires, so behavior that would otherwise be hand-written event logic happens automatically.

Adam borrows its sheet and cell terms from spreadsheets: a sheet holds named, typed cells, much like a spreadsheet holds named, typed values. A relationship plays the role of a spreadsheet’s equation cell, but unlike a spreadsheet formula, which computes in one direction only, a relationship is multi-way: a == b means that changing a updates b to match, and changing b updates a to match. Which cell is the source and which is derived is decided based on the cell strength. Typically, cells modified most recently have greater strength.

About this book

This book has three parts: a tutorial introduction you can read start to finish, chapters that go back over the same ground in more detail, and a terse reference manual for looking things up. Every Adam source fragment shown in a fenced adam block is exactly what you’d type into a .adm2 file; a fenced text block instead shows grammar notation (EBNF), not Adam source itself.

Expressions and the standard library

Adam adds a declarative shell (sheet, cell, source, relationship, conditional, out, require, and filter) around expressions written in the Common Expression Language (CEL), turning a set of CEL expressions into a live, bidirectional constraint graph. See the CEL language book for CEL’s own grammar, operators, literals, casts, and control-flow expressions.

Functions callable from inside an expression, such as min, max, clamp, and round, come from a function library installed into the parser’s OpLookup; this book’s own examples install cel-std.

A Tutorial Introduction

Adam programs are called sheets, a term borrowed from spreadsheets. This chapter is an informal tour of Adam; later chapters go over the same ground in more detail, and the reference manual provides a full specification.

An Adam sheet declares the relationships among a set of properties called cells. These relationships are maintained when edits occur, providing correct behaviors for user interfaces and scripting without having to code complex event handling logic.

Every source fragment below is the complete description used to generate the UI, UI behavior, and graph visualization. The UI is live, so you can change the values and explore the behavior. Within an application, a sheet instance is typically bound to a UI construct or scripting system with human- readable text labels, instead of cell identifiers.

A first sheet

An Adam program is a single sheet, named, with a body of declarations between braces. A simple sheet declares a couple of source cells:

sheet hello {
    source width = 1920;
    source height = 1080;
}

A cell is a named, typed storage location: the basic unit of state in a property model. width and height are i32-typed cells, each given an initial value (the types are deduced from the initial value). A source cell is like a spreadsheet’s value cell: it holds a value written into it.

A sheet’s body is a sequence of declarations. A sheet describes a graph of cells and the constraints between them. The graph for hello is just the two unconnected cells.

Filters

A filter clause attaches a domain constraint to a cell, most commonly a range:

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

0..=100 is an inclusive range. Try writing a value outside [0, 100] into level above and watch it snap back into range.

A filter’s bounds don’t have to be constants: 0..=max references another cell. The filters chapter covers filters in full.

Out Cells

An out declaration is like a spreadsheet’s equation cell. Its value is computed from the provided method.

sheet area_demo {
    source width = 10 filter 0..=100;
    source height = 20 filter 0..=100;

    out area := width * height;
}

The method on the out cell can reference other cells in the sheet, and the calculation is reapplied when those values change. In the graph representation, the method is a relationship and drawn as a circle between the cells. The heavy arrows and border around the out cell indicate that the value is forced by the relationship. A forced cell’s value is not editable.

See the outputs chapter for the full treatment.

Cells and Relationships

A plain cell declaration acts as a source or out cell. Cells are connected by one or more relationships, each a bundle of methods that satisfy the relationship but solve for a different term (or set of terms).

For example, if we have two values \(a\) and \(b\) where \(a = 2b\), that can be represented as:

sheet basic_relationship {
    cell a: f64 filter 0.0..=200.0;
    cell b = 2.0 filter 0.0..=100.0;

    relationship {
        a := b * 2.0;
        b := a / 2.0;
    }
}

For any active relationship, exactly one method is selected to execute. The method chosen is based on the strength of the cells. Cells that have been written more recently have a higher strength. The declaration order determines the cells’ initial strength. Cells declared later have a higher strength.

In the graph, you can see the flow change as you write a or b.

The methods in a relationship must be consistent. If the result of the selected method is used to recalculate the non-selected methods, the result should not change the value of the assigned cells within an error epsilon.

Relationships can be chained together. We can express the relationship a <= b <= c like this:

sheet inequality {
    cell a = 10 filter 0..=100;
    cell b = 20 filter 0..=100;
    cell c = 30 filter 0..=100;

    relationship {
        a := min(a, b);
        b := max(a, b);
    }

    relationship {
        b := min(b, c);
        c := max(b, c);
    }
}

This example also demonstrates two additional features.

  • A method can be self-referential, naming a cell as both a dependency and a result. In such a case, the method must be idempotent.
  • When a cell value is derived in terms of itself via a self-referential method (or filter). The last written value is preserved.

You can see the effect of the second behavior by sliding a to 100 which will pull b and c to 100 and then slide a back to 0. b and c will return to their prior values.

In the relationships chapter, you will see relationships are not limited in their arity (you can have n-way relationships with each method solving for 1 or more cells).

Conditionals

A conditional groups relationships that are only active under a matching condition. It evaluates a match subject, then activates whichever branch’s literal equals the current match value:

sheet constrain {
    source constrain = false;
    cell a = 5 filter 1..=100;
    cell b = 10 filter 1..=100;

    conditional constrain {
        true => {
            relationship {
                a := b;
                b := a;
            }
        }
    }
}

Only the active branch participates. The _ branch, if present, catches any value not in the named branches list, and must be written last. If no branch is matched, the conditional has no effect.

A conditional relationship may force a cell value, in this case any source cell value is preserved and restored when the conditional is removed. The UI for a forced cell value will typically disable the control.

sheet conditional_forced {
    source constrain = false;
    cell a = 5 filter 1..=100;

    conditional constrain {
        true => {
            relationship {
                a := 42;
            }
        }
    }
}

Requirements

Filters on source cells were introduced in the above [[Filters]] section. For an out cell or a cell in an out role, violating a filter will trigger a diagnostic.

sheet filter_diagnostic {
    cell width: i32 filter 0..=100;
    cell height = 150 filter 0..=200;

    relationship {
        width := height;
        height := width;
    }
}

A cell may also have one or more requirements. For source cell or when a cell is writable, the requirements act are a boolean expression that act as a filter rejecting any input that doesn’t satisfy the requirements.

For an out cell, violating a requirement will trigger a diagnostic. Requirements have an optional name that can be associated with a message explaining the issue.

sheet area_demo {
    source width = 10 filter 1..=100;
    source height = 10 filter 1..=100;

    out area := width * height require {
        @not_too_big area <= 1000;
    };
}

Where to go next

That’s the whole language. The remaining chapters, starting with sheets, cells, and types, cover each construct in the depth this chapter skipped past, and the reference manual gives you the full grammar and every built-in type in one place.

Sheets, Cells, and Types

Sheets

Every Adam source file is one sheet:

sheet name {
    /* cell, relationship, conditional, and out declarations, in any order,
       except that each identifier must be declared before it is referenced —
       see "Names and declaration order" below. */
}

The sheet’s own name (name above) is consumed by the parser and is not otherwise meaningful to adam-rs; it exists for readability and for host tooling, such as a language server or an example picker, to have something to display.

Cell declarations

cell_decl      = "cell" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
cell_type_init = (":" type_expr ["=" expression]) | ("=" expression).

A cell needs a type, an initial value, or both:

cell width: i32;             // type only — needs a registered default
cell height: i32 = 1080;     // type and initializer
cell area = 0;               // initializer only — type is inferred

At least one of : type_expr / = expression must be present: cell width; alone is a syntax error (“expected : or = in cell declaration”). A cell may also carry an optional trailing filter clause and/or require block — a standing domain constraint and named boolean diagnostics, respectively, both covered in full in the filters chapter and requirements: diagnostics, not gates (those two chapters introduce the mechanisms via out, but both apply to a plain cell — and, require only, to a source cell — exactly the same way).

A cell’s initializer may reference literals and CEL operators, but not other cells: it’s evaluated before the sheet has any state to read. To compute one cell from others, use a relationship or an out declaration instead.

Built-in types and inference

Adam ships with the following types pre-registered, each with a Default value used when a cell declares a type but no initializer:

Type nameRust typeDefault
i8, i16, i32, i64, i128, isizethe matching signed integer0
u8, u16, u32, u64, u128, usizethe matching unsigned integer0
f32, f64the matching float0.0
boolboolfalse
StringString""

When a cell has an initializer but no : type_expr annotation, its type is inferred from the initializer expression’s own result type (an ordinary CEL literal-defaulting rule: an unsuffixed integer literal like 0 is i32, an unsuffixed float literal like 0.0 is f64; see cel-parser’s documentation for the full literal grammar). When both are present, they must agree exactly, or the sheet fails to parse with a “type mismatch: expected T, got U” error (see error messages).

A host application can also register additional Rust types under their own Adam type names; that’s a Rust-level embedding concern, not something a sheet author does; see the type registry.

Cells with no default

A type registered without a Default (via the embedding API’s register_no_default) can still be used for a cell, but only with an initializer; declaring one with a bare : T and no = ... fails to parse (“type T has no default; provide = ...”). Every built-in type in the table above has a default, so this only matters for a host-registered custom type.

Tuple types

type_expr = identifier [ "(" type_expr { "," type_expr } ")" ]
          | "[" type_expr "]"
          | "(" [ type_expr ["," [ type_expr { "," type_expr } ]] ] ")".

A parenthesized type list is a tuple type. () is the empty tuple (an inert, zero-element value; see the relationships chapter for where it’s useful); (T) with no comma is plain grouping, identical to T (types have no precedence to disambiguate, but the parentheses are accepted for symmetry with expression grammar); (T,) (trailing comma mandatory) is a genuine one-element tuple; (T, U, ...) is the general case:

sheet s {
    cell point: (f64, f64) = (0.0, 0.0);
}

Tuples nest: (i32, (f64, String)) is a 2-tuple whose second element is itself a 2-tuple. Compound cells own canonical CEL values. Host code uses ParsedSheet::read_compound and write_compound with complete representation witnesses. For example, Cons<Leaf<i32>, Cons<Leaf<f64>, Nil>> witnesses (i32, f64). Reads return independent owned clones; writes validate the complete recursive shape before changing the cell. source_compound reads the original aspiration independently of any filter result. Concrete registered leaves retain the ordinary Rust read::<T> / write APIs.

Arrays, generic leaves, and callables

Arrays and tuples compose recursively: [(i32, [f64])] is an array of pairs whose second field is another array. An array defaults to empty without requiring an element default; a tuple defaults each child recursively. A missing leaf default names that leaf. Register a generic foreign leaf under its full source name, such as RangeInclusive(i32); containing array and tuple shapes need no additional registration.

Fn((A, B), R) describes a callable with two parameters; (A,) and () describe one and zero parameters. A callable has no default and requires an initializer. Every Adam cell also requires cloning and value equality: CEL can store callable values, but Adam rejects a callable cell when its descriptor lacks equality. Adam does not invent function identity or suppress propagation. Registered host function markers retain the concrete capabilities their type registration supplies.

Executable host boundary

CellValue is Adam’s private whole-value compound owner, not a wrapper around each array element. A host supplies ArrayOf, Cons, Nil, and Leaf witnesses to the checked ParsedSheet boundary. A wrong nested shape fails before mutation. Cloning and equality are required for persistence/propagation; debug formatting is an independent capability. Containers themselves do not impose those bounds. Even an empty array of callable elements lacks equality when its element descriptor lacks equality; emptiness does not manufacture that capability.

This example defaults recursive compounds, writes an array of concrete tuple lists, propagates it to an output, and reads the source aspiration:

#![allow(unused)]
fn main() {
/// Reads, writes, and propagates canonical arrays while retaining source aspirations.
#[test]
fn compound_cell_interoperability() {
    use adam_lang::{AdamParser, TypeRegistry};
    use cel_parser::OpLookup;
    use cel_runtime::{Array, ArrayOf, Cons, IntoCStackList, IntoRustTuple, Leaf, Nil};

    type Row = Cons<Leaf<i32>, Cons<ArrayOf<Leaf<f64>>, Nil>>;
    type Rows = ArrayOf<Row>;
    let mut sheet = AdamParser::new(TypeRegistry::new(), OpLookup::new())
        .parse_str(
            "sheet data {
                source rows: [(i32, [f64])];
                cell defaults: (i32, [f64]);
                cell unit: ();
                out copy := rows;
            }",
        )
        .unwrap();
    let rows = sheet.cell_names["rows"].0;
    let defaults = sheet
        .read_compound::<Row>(sheet.cell_names["defaults"].0)
        .unwrap()
        .into_rust_tuple();
    assert_eq!(defaults.0, 0);
    assert!(defaults.1.is_empty());
    assert!(sheet.read_compound::<Rows>(rows).unwrap().is_empty());
    sheet
        .read_compound::<Nil>(sheet.cell_names["unit"].0)
        .unwrap();
    sheet
        .write_compound::<Rows>(
            rows,
            Array::from_vec(vec![(7, Array::from_vec(vec![1.5])).into_c_stack_list()]),
        )
        .unwrap();
    sheet.propagate().unwrap();
    let copied = sheet
        .read_compound::<Rows>(sheet.output_names["copy"])
        .unwrap()
        .into_vec()
        .pop()
        .unwrap()
        .into_rust_tuple();
    assert_eq!(copied.0, 7);
    assert_eq!(copied.1.as_slice(), [1.5]);
    assert_eq!(
        sheet.source_compound::<Rows>(rows).unwrap().as_slice()[0].1,
        7
    );
    assert!(
        sheet
            .write_compound::<ArrayOf<Leaf<u32>>>(rows, Array::from_vec(vec![99]))
            .is_err()
    );
    assert_eq!(sheet.read_compound::<Rows>(rows).unwrap().len(), 1);
    assert_eq!(
        sheet.source_compound::<Rows>(rows).unwrap().as_slice()[0].1,
        7
    );
}
}

The corresponding capability error is also executable:

#![allow(unused)]
fn main() {
/// Rejects persistent callable cells without inventing function equality.
#[test]
fn callable_cells_require_equality() {
    let error =
        adam_lang::AdamParser::new(adam_lang::TypeRegistry::new(), cel_parser::OpLookup::new())
            .parse_str("sheet data { cell functions = []: [Fn((i32,), [i32])]; }")
            .err()
            .unwrap();
    assert!(
        error
            .to_string()
            .contains("equality capability unavailable")
    );
}
}

The Inspector formats arrays and tuples recursively when debug capabilities exist, including typed empty arrays and unit. Compound edits are explicitly unsupported and leave the cell unchanged; primitive editors retain their ordinary behavior. Formatting does not imply an editable source round trip.

Names and declaration order

A cell’s name must be unique across the whole sheet: cells and out declarations share one namespace, so declaring cell result: i32 = 0; and later out result := ...; in the same sheet is a duplicate-name error, exactly as two cell result declarations would be.

Referencing a name before its declaration — as a relationship binding’s output, a dependency inside any expression, a conditional’s match subject, or a filter’s dependency — is an “undeclared cell name” error (see error messages).

Source Cells

Grammar

source_decl = "source" identifier cell_type_init [ cell_filter ] [ require_block ] ";".

A source declaration looks exactly like a cell declaration: the same cell_type_init (a type, an initializer, or both), evaluated the same way, with no cell scope; the same optional trailing filter clause and require block. source and cell even share one namespace with out, so declaring source width and cell width in the same sheet is the same “duplicate cell” error two cell width declarations would be.

sheet resize {
    source width: i32 = 1920;
    source height: i32 = 1080;

    out area := width * height;
}

Both width and height above are declared source: ordinary inputs to the out that multiplies them, with nothing in this sheet ever claiming either as a relationship or conditional output.

Always a source, never derived

A source cell is the mirror image of an out cell. Where an out cell is always derived by its own fixed writer, a source cell is always left alone by the solver: never claimable as a relationship binding’s output, a conditional branch’s output, or an out’s own writer target. Naming a source cell on a binding’s left-hand side is an error.

A plain cell’s source/derived status is a per-round decision the solver makes from strength — the same cell might be a source in one round and derived in the next, depending on what’s been written recently. A source cell opts out of that entirely: whatever a host last wrote to it (or its own declared initializer, before any write) is always its value, unconditionally.

A source cell can be filtered too

A source cell is ordinary in every other respect: it can be read anywhere a plain cell can, written directly at any time (that’s the whole point — a source cell always reflects whatever was last supplied from outside the sheet), can carry a require block for domain diagnostics, exactly like a cell or out:

sheet s {
    source width: i32 = 1920 require {
        @positive width > 0;
    };
}

and can carry a filter clause, exactly like a cell or out: whatever a host writes to a filtered source cell is conformed the same way a filtered plain cell’s value is, live, whenever the sheet resolves:

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

See the filters chapter for filter’s full rules — everything there applies to a source cell unchanged.

Expressions and Dependency Deduction

Expressions are CEL

Everywhere Adam’s grammar calls for expression (a cell initializer, a relationship binding’s right-hand side, a conditional’s match subject or branch literal, an out declaration’s body, a requirement, a filter’s body), the expression itself is parsed and evaluated by cel-parser/cel-runtime, not by Adam. The standalone CEL reference manual and its book root here document literals, arithmetic and comparison operators, if/else, as casts, ranges (lo..=hi), function calls, and closures. This chapter covers only what Adam does around an expression: deciding which cells it may read, and what it does with the value it produces.

No standard library of its own

A parser built with a bare OpLookup::new() and no library installed can still parse and run every construct in this book except a function call — any attempt to call an undefined function fails to parse with an error naming the missing function. This book’s own examples always install cel-std (see support::parser in adam-lang-book’s own source).

Host-registered records and members additionally require their own schemas and operations; neither the bare language nor cel-std supplies application types.

Cell initializers see no cells

A cell’s = expression initializer is evaluated with no cell scope at all. Referencing any identifier that would otherwise name a cell is an “undeclared cell name” error, exactly as if the cell had never been declared; see error messages.

To compute one cell’s value from another’s, write a relationship or an out declaration; both of those do get a live cell scope, per the next section.

Deduced dependencies

A relationship binding, a conditional’s match subject, an out declaration’s body, and a requirement body all share one mechanism for deciding which cells they read: every identifier the expression references that names an already-declared cell becomes an input, automatically; there is no explicit parameter list to write. Referencing the same cell more than once (a && a) still counts as one input, not two. This is why Adam has no forward references (see names and declaration order): an identifier can only be recognized as a cell dependency if that cell was declared earlier in the same sheet.

A filter clause uses the same deduction, plus one reserved identifier: _ always denotes the candidate value being conformed, never a cell; see the filters chapter.

Constructor names, record field labels, and member names are not value references. The only dependencies of TestPoint { x: a, y: b }.x are a and b, even if cells named TestPoint, x, and y exist. A closure parameter shadows only value references in its body, not those labels or constructor names.

Host records and members

Write Sample { value: 3.0, } when the host registers a Sample schema. Supply every field exactly once; unknown constructors and unknown, duplicate, missing, or incorrectly typed fields are errors. Field expressions evaluate in written order. Empty schemas accept {}, and non-empty field lists accept a trailing comma. An f64 field requires 3.0, not 3: integers do not implicitly convert to floats.

reading.value and reading.value() both invoke the host’s unary value operation. reading.operation(a) passes reading first, then a. A same-named cell cannot hide a dot operation. (reading.value)() instead calls the operation’s result and fails when that value is not callable.

Bare record constructors are forbidden in if conditions and Adam conditional subjects so { can begin the following body without backtracking. Parenthesize the constructor-containing expression, for example conditional (Sample { value: 3.0 }.value) { ... }. Parentheses, call arguments, tuple elements, and array elements permit record expressions within such headers. Unparenthesized operators, ranges, and closure bodies inherit the header restriction.

Member assignment is not part of the language: update the whole cell from the host or construct a new record as a relationship’s output.

This executable host example registers the type, constructor, and accessor, then checks the sheet with the same metadata used to run it. The Sample and value cells do not shadow either constructor or member resolution:

#![allow(unused)]
fn main() {
/// Exercises host record construction and member reads in a live Adam sheet.
#[test]
fn registered_record_and_member_example() {
    use adam_lang::{AdamParser, TypeRegistry};
    use cel_parser::{OpLookup, OperationSignature, RecordField, RecordSchema, ResolvedLeafType};
    use cel_runtime::ValueType;

    /// A host-defined type with a named field.
    #[derive(Clone, Debug, Default, PartialEq)]
    struct Sample {
        value: f64,
    }
    let sample = ResolvedLeafType::new("Sample", ValueType::leaf::<Sample>());
    let scalar = ResolvedLeafType::new("f64", ValueType::leaf::<f64>());
    let mut registry = TypeRegistry::new();
    registry.register::<Sample>("Sample");
    let mut lookup = OpLookup::new();
    lookup
        .register_record(RecordSchema {
            ty: sample.clone(),
            fields: vec![RecordField {
                name: "value",
                ty: scalar.clone().into(),
            }],
            emit: Box::new(|s, _, _, _| s.op1(|value: f64| Sample { value })),
        })
        .unwrap();
    lookup
        .register_operation(OperationSignature {
            name: "value",
            operands: vec![sample.into()],
            result: scalar.into(),
            emit: Box::new(|s, _| s.op1(|sample: Sample| sample.value)),
        })
        .unwrap();
    let source = "sheet samples {
        cell Sample: bool = true;
        cell value: bool = false;
        cell reading: Sample = Sample { value: 3.0, };
        out measured: f64 := reading.value();
    }";
    let ast = adam_lang::AdamAstParser::new().parse_str(source).unwrap();
    assert!(adam_lang::check_sheet(&ast, &registry, &lookup).is_empty());
    let mut sheet = AdamParser::new(registry, lookup).parse_str(source).unwrap();
    let reading = sheet.cell_names["reading"].0;
    let measured = sheet.output_names["measured"];
    sheet.propagate().unwrap();
    assert_eq!(*sheet.read::<f64>(measured).unwrap(), 3.0);
    sheet.write(reading, Sample { value: 7.0 }).unwrap();
    sheet.propagate().unwrap();
    assert_eq!(*sheet.read::<f64>(measured).unwrap(), 7.0);
}
}

“Expression produced no value”

Every place an expression is required must produce a value one of Adam’s registered types recognizes, at the end of parsing: a bare CEL statement with no trailing value, or an expression whose result type isn’t registered with the TypeRegistry in use, is a parse error naming the construct it came from: expression produced no value, or cannot infer a type: unregistered type `T` . Register the result’s leaf types with the TypeRegistry; a type annotation alone does not register an unknown host type.

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.

Outputs and Requirements

Grammar

out_decl    = "out" identifier [ ":" type_expr ] ":=" expression
               [ cell_filter ] [ "require" "{" { requirement } "}" ] ";".
requirement = [ "@" identifier ] expression ";".

An out declares a new cell computed by exactly one expression: there’s no alternative binding to choose between, unlike a relationship. Its dependencies are deduced the same way. The : type_expr annotation is optional; when absent, the output’s type is inferred from the initializer, the same rule built-in types and inference gives for a plain cell. An out may also carry a filter clause, exactly like a plain cell; see the filters chapter.

sheet area_demo {
    source width = 10 filter 0..=100;
    source height = 20 filter 0..=100;

    out area := width * height;
}

An output cell can be read anywhere, written nowhere

out shares one namespace with cell: declaring out result := ...; after (or before) a cell result in the same sheet is a duplicate-name error, exactly like two cell declarations would be. An output cell is an ordinary cell for reading: a later declaration in the same sheet can reference an earlier out by name in its own expression — as a relationship input, a conditional’s match subject, a filter argument, or another out’s own initializer — exactly like referencing any other already-declared cell. What stays restricted is writing: an output’s cell can never be produced by more than one method, and can never be written directly, not by a host write, not by a relationship binding, not by a second out. Its value comes from its own initializer and nothing else. The initializer is purely functional; resolving the sheet may reuse its result rather than evaluate it again:

sheet s {
    source width: i32 = 10;
    out area := width * 2;
    out doubled_area := area * 2;
}

Requirements: diagnostics, not gates

require is not an outputs-only mechanism: a require { ... } block can trail a plain cell or a source declaration’s initializer too, with the same meaning described here — see cell declarations and source cells for those forms. A filter clause is just as unrestricted — see a filter on an output cell for a filter attached to an out declaration. Trailing an out’s initializer, require { ... } names zero or more boolean checks. Each requirement’s own dependencies are deduced separately from the output’s initializer; a requirement commonly reads the output’s own value by name, alongside whatever other cells it needs:

sheet area_demo {
    source width: i32 = 10;
    source height: i32 = 20;

    out area: i32 := width * height require {
        @not_too_big area <= 300;
    };
}

Requirements are purely functional predicates of their input values and immutable constants, with no externally observable side effects or mutable external dependencies. Equal inputs produce equivalent results or errors. There is no guarantee that a requirement executes, or when or how often it executes; current diagnostics may use a previously computed result.

A failed requirement never stops the sheet from resolving, and never stops area from being computed and readable; a host can query which requirements are currently failing precisely because nothing else in the sheet notices a requirement failing on its own (see the host embedding API). A requirement’s @name is optional — present only when an application wants to report which requirement failed; it happens to read naturally when it echoes a cell name (@not_too_big, @width_max), but it isn’t a cell reference and doesn’t have to match one.

Multiple requirements

An output can list any number of requirements; a host can query exactly the ones currently failing, identified individually (see the host embedding API):

sheet bounds_demo {
    source x: i32 = 50;

    out clamped: i32 := x require {
        @not_negative clamped >= 0;
        @not_too_big clamped <= 100;
    };
}

Relationships and the Solver

Bindings are alternative methods

relationship_decl = "relationship" "{" { binding } "}".
binding            = binding_target ":=" expression ";".
binding_target     = identifier | "(" identifier { "," identifier } [ "," ] ")".

Each binding inside a relationship block is a candidate method: an expression whose dependencies are deduced from whichever already-declared cells it references, paired with the cell(s) named on its left of :=. A relationship’s bindings are alternatives, not a sequence: at any moment, exactly one of them is selected, and only the selected one’s output cell(s) are actually written when the sheet resolves. Other bindings may contribute to self-referencing seed construction. Selection does not guarantee that an expression is evaluated; the runtime may reuse its result. A source cell can never be named on a binding’s left-hand side — it’s always a source, by construction, never a method’s output — and an out cell can be a binding’s input but never its output either, since an out already has its own fixed writer.

Strength: who gets to stay a source

Every cell carries a strength, a write-recency counter: resolving the sheet re-derives the stalest cells it safely can and leaves the freshest cells alone. Two things bump a cell’s strength: an explicit write, and, once only, the cell’s own declaration. Before any explicit write has happened, declaration order alone orders every cell’s freshness, earliest declared being stalest. The tutorial’s Cells and Relationships section walks through the simplest case of this rule. A write never touches strength itself except to promote the written cell to “freshest of all”; reading a cell never changes it.

Automatic plan reuse

Resolving the sheet does not always rerun method selection. The runtime retains one prepared unconditional plan for conditional guards and one prepared main plan for the current active relationships. A write to a released source can preserve a phase’s plan; a write to a claimed cell, a changed active set, or a structural mutation can require planning again. A filtered source remains released, but a self-referencing method’s output is claimed. The host calls the same propagate API either way.

Reuse preserves assignments and structural seed choices, not old values. Resolution still uses current inputs, strength-sensitive seed gates and sibling ordering, and current diagnostics. Released-source membership bookkeeping takes expected constant time; comparing active sets takes time proportional to their size, and checking a pending source-priority certificate takes time proportional to the cell count. Local sibling sorting and fresh value evaluation retain their costs. Discarding a prepared plan can also take time proportional to its artifact size. This optimization does not add cross-resolution value memoization or incremental assignment repair.

The rules a relationship’s methods must satisfy

Methods are purely functional: their results depend only on input values and immutable constants. Equal inputs produce equivalent values or errors. They have no externally observable side effects and do not read mutable external state. There is no guarantee that a method executes, or when or how often it executes. The same contract applies to filters, conditional expressions, and requirements, including callbacks supplied through the Rust host API. Represent changing dependencies as cells, not hidden callback state.

Every method in the same relationship must reference exactly the same set of cells — the union of that method’s own inputs and outputs — as every other method in that relationship; violating this fails to parse with methods in a relationship must reference the same set of cells. A relationship models one fixed group of related cells; its methods differ only in which subset of that group they treat as the output, using an “ignore an input” pattern — not in which cells they touch at all. The multiplication triangle from the tutorial’s Cells and Relationships section satisfies this: all three of c := a * b, a := c / b, and b := c / a reference the same {a, b, c}.

A method’s own outputs list must be duplicate-free, and no two methods in the same relationship may claim an identical outputs set; violating either fails to parse with a method's outputs must be duplicate-free, and no two methods in a relationship may share an outputs set. The planner treats a method’s output set as one indivisible claim, so two methods claiming the same set would make that claim ambiguous. The triangle’s three output sets — {c}, {a}, {b} — are pairwise distinct, as required.

A cell may appear in both a method’s inputs and its own outputs — a self-referencing method — which is explicitly allowed and has its own rules; see the relationships-continued chapter.

A shared-cell example

Cells can be shared across more than one relationship, letting the solver’s strength preference cross relationship boundaries. Four cells, two relationships, with b and c shared between them:

sheet diamond {
    cell a = 0.0;
    cell b = 0.0;
    cell c = 2.0;
    cell d = 3.0;

    relationship {
        c := a * b;
        b := c / a;
        a := c / b;
    }

    relationship {
        d := b * c;
        c := d / b;
        b := d / c;
    }
}

Declaration order makes d freshest, then c, then b, then a stalest. The solver tries, strongest first, to leave each cell a source:

  • d: the second relationship has a binding that doesn’t write d (b := d/c or c := d/b), so d can stay a source.
  • c: with d already pinned as a source, the second relationship’s only way to avoid writing d is b := d/c, which doesn’t touch c either, so c can also stay a source, as long as the first relationship also avoids writing it (a := c/b does).
  • b: now both relationships are already spoken for in a way that avoids writing b only by the first relationship (a := c/b), but the second relationship’s only binding that doesn’t write c or d is b := d/c, which does write b. There is no way left to leave b a source, so the attempt fails and b stays claimed by the second relationship.
  • a: stalest, and the first relationship’s remaining choice is a := c/b; a is derived.
sheet diamond {
    cell a = 0.0;
    cell b = 0.0;
    cell c = 2.0;
    cell d = 3.0;

    relationship {
        c := a * b;
        b := c / a;
        a := c / b;
    }

    relationship {
        d := b * c;
        c := d / b;
        b := d / c;
    }
}

Whether a cell came out of the last resolution as a source, left alone rather than derived, is useful for a host UI deciding whether a field should be editable.

When no assignment exists

Every relationship in a sheet must end up with exactly one selected binding once the sheet resolves; if that’s not possible, resolution fails instead of silently picking something inconsistent. Two relationships that both, unconditionally, insist on writing the same cell can never both be satisfied, and resolving fails with no valid method assignment (overconstrained) (Error::Conflict).

A subtler failure is a cycle: an assignment exists, but every valid choice of bindings forms a closed loop with no cell left as a source anywhere in the loop, and resolving fails with selected methods form a cycle (Error::Cycle). This happens when every relationship in the loop has only one binding, leaving the solver no alternative to try; giving even one relationship in the loop a second, cycle-breaking binding lets the solver route around it instead.

Destructuring a binding’s output across more than one cell, and a binding that references its own output cell, are covered next, in the relationships-continued chapter.

Relationships Continued: Destructuring and Self-Referencing Methods

Destructuring bindings

A binding’s left-hand side can name more than one output cell by parenthesizing it, in which case the right-hand side must be a tuple expression of matching arity, split element-wise:

sheet rect_demo {
    source width: f64 = 10.0;
    source height: f64 = 4.0;
    cell area: f64;
    cell perimeter: f64;

    relationship {
        (area, perimeter) := (width * height, 2.0 * (width + height));
    }
}

(a, b) := ... and the one-element (a,) := ... (trailing comma mandatory, matching Rust’s own 1-tuple pattern) both destructure; a bare a := ... or the equivalent single parenthesized (a) := ... (mere grouping, no comma) instead binds the right-hand side’s whole result (including a tuple-typed one) directly to the one named cell. Destructuring and direct-bind are otherwise governed by the same type-matching rules as any other binding: each output’s declared type must structurally match what the expression actually produces.

Self-referencing methods

A method’s expression may reference the very cell it writes — a self-referencing method — which the rules a relationship’s methods must satisfy section already noted is explicitly allowed: a cell may appear in both a method’s inputs and its own outputs. Each time the sheet resolves, a self-referencing method reads its own cell’s source value — never a previous round’s derived value — the same source/derived split the raw value is never lost section already introduced for filters:

sheet clamp_via_relationship {
    cell level: i32 = 0;

    relationship {
        level := min(level, 0);
    }
}

Writing level above never applies the clamp itself — exactly like a filter, the correction happens live, the next time the sheet resolves, against level’s own raw value, and that raw value survives underneath the clamp forever.

Self-referencing methods must be idempotent

A self-referencing method exists to correct its own cell into whatever set of values the relationship enforces. That only makes sense if reapplying the method to its own already-corrected output leaves it unchanged — f(f(x)) == f(x). min(level, 0) above satisfies this: once level is at most 0, computing min of that value and 0 again produces the same value.

The solver never checks this — nothing about add_relationship inspects a method’s function for idempotence, and nothing about propagate would refuse to resolve a sheet whose self-referencing method isn’t idempotent. It is purely a correctness obligation on the sheet author. A self-referencing binding built from a genuinely non-idempotent operation — a literal swap between two cells’ current values, for instance, rather than a one-sided correction like min/max/ clamp — has no single well-defined corrected value for the solver to settle on.

Conditionals

Grammar

conditional_decl   = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (expression | "_") "=>" "{" { relationship_decl } "}" [ "," ].

A conditional evaluates its match subject (the expression right after the conditional keyword) and activates the one branch, if any, whose literal equals the current match value. Only that branch’s relationship blocks participate in the round’s solve; every other branch’s relationships are invisible to the planner, exactly as if they weren’t declared. A branch’s own body holds nothing but relationship declarations: a conditional cannot declare cells, and cannot nest another conditional directly inside a branch.

The match subject

A bare cell name (conditional mode { ... }) is the common case: the match value is just that cell’s current value. The match subject can also be a general expression over several already-declared cells, deduced exactly like a relationship binding’s body:

sheet resample_demo {
    source resample: bool = true;
    source constrain: bool = true;
    cell locked: bool = false;

    conditional resample && constrain {
        true => {
            relationship {
                locked := true;
            }
        }
        false => {
            relationship {
                locked := false;
            }
        }
    }
}

Every branch literal’s type must match the match subject’s own inferred type exactly; a sheet mixing an i32 match subject with a bool branch literal fails to parse, not to propagate. The match subject can also be tuple-valued, in which case each branch literal is a tuple expression of the same shape.

Forced cells

A relationship with exactly one method has no alternative binding to choose: its output cell is claimed every time the sheet resolves, regardless of strength. Such a cell is forced — Sheet::is_forced reports this, and it’s false for a cell whose relationship has two or more methods, even if strength happens to pick the same direction every round.

A common convention in a host UI is to disable the editable widget for a forced cell: writing it would have no lasting effect once the sheet next resolves, so there’s nothing useful for the user to type into. This is a UI convention, not a language rule — adam-lang and adam-rs never disable anything themselves; a host is always free to accept the write anyway (see shadow state: forced and self-referencing cells below for what happens if it does).

Shadow state: forced and self-referencing cells

The raw value is never lost section’s filters and the self-referencing methods section’s self-referencing methods both keep a cell’s own raw source value forever, underneath whatever derived value a live correction currently computes. A forced cell works the same way: forcing it shadows its own source, never overwrites it.

sheet range_bounds {
    source mode: i32 = 0;
    cell low: i32 = 4;
    cell high: i32 = 9;

    conditional mode {
        0i32 => {
            relationship {
                low := min(low, high);
                high := max(low, high);
            }
        }
        1i32 => {
            relationship {
                low := high;
            }
        }
    }
}

With mode == 0 (the declared default), the self-referencing branch is active — two methods, low := min(low, high) and high := max(low, high), exactly like the triangle from the relationships chapter: only one is ever selected, and which one is decided by strength. low, declared first, is stalest, so the solver picks low := min(low, high); high is left alone as an ordinary source, its own max method never invoked. low and high start at 4 and 9, already satisfying low <= high, so nothing visibly changes.

Writing high to 42 and switching to mode == 1 activates the single-method branch, forcing low from high: low reads 42, but its own source is still 4 — the write that actually changed something (high) never touched low‘s source at all. Switching back to mode == 0 reselects low := min(low, high) (still the stalest cell) and recomputes it fresh from both cells’ own sources — 4 and 42 — not from the stale forced 42: low reads back down to 4; high, never written since the first round, is still the one left as a source, at 42.

Writing low to 100 promotes it to freshest, flipping the solver’s choice: now high := max(low, high) is the one selected, deriving high and leaving low as the source instead — high reads 100, pulled up to match. Either binding can fire; which one does is strength’s call, exactly as in the relationships chapter, never both at once.

The default branch and reverting to source

_ => { ... } matches whatever value none of the named branches list and (because it’s defined that way) must be the last branch textually; a named branch written after _ is a syntax error. Leaving off _ entirely is legal: if the match value doesn’t equal any branch’s literal, none of the conditional’s relationships are active that round, and every cell that would otherwise be one of their outputs stays free, reverting to its own last externally-written value (or its declared default), not to whatever the branch last computed for it. This is the same shadow-state mechanism the previous section just showed for a forced cell, triggered here by no branch matching at all rather than by switching branches: a relationship that stops being active can never have written a cell’s source, so the cell has nothing to revert to except that untouched source.

sheet no_default {
    source mode: i32 = 0;
    cell x: f64 = 1.0;

    conditional mode {
        0i32 => {
            relationship {
                x := 100.0;
            }
        }
    }
}

Nested and chained conditionals

Two conditionals in the same sheet compose freely at the sheet level: one conditional’s output cells can be another’s match subject or a relationship input, exactly like any other cell, even though a branch body itself can only hold relationships, not another conditional. A resize dialog, for example, might have two sibling conditionals this way: one gated on resample, the other on resample && constrain.

Optional geometry and self-referencing constraints

The book, adam-web-ui, and adam-lsp explicitly install cel-std operations, cel-euclid operations, and the seven geometry cell types. A standalone adam-lang host still chooses its own libraries; Adam has no mandatory geometry dependency. Enable cel-euclid’s adam Cargo feature, call cel_euclid::register_adam_types(&mut types) and cel_euclid::install(&mut lookup), and construct AdamParser::new(types, lookup). Pass the same registry and lookup to adam_lang::check_sheet for editor checking. The checker uses exact record/member metadata where types are known, visits closure bodies, and defers unresolved call-result and tuple shapes to runtime. It does not claim complete inference.

Geometry uses concrete f64 / UnknownUnit aliases, not generic Adam types. Write 1.0 or 1 as f64 rather than passing i32 fields implicitly. Field order is arbitrary; every declared field must occur once. Consult the complete signature table for fields, supported operand types, checked errors, and containment semantics.

Crop and straighten

This is the actual library example, included rather than copied. Run cargo run -p cel-euclid --features adam --example adam_geometry to execute it. cargo test -p adam-lang-book --test geometry verifies these same included sheets through the book’s parser. These cross-library demonstrations are executable Rust-driven examples, not live inspector mounts or a crop UI.

#![allow(unused)]
fn main() {
// Image coordinates -> crop coordinates. The caller rotates about the image
// point under the requested crop center. No separate requested-crop cell:
// Adam preserves the source aspiration of the self-referencing crop.
// A valid result preserves the crop center, image anchor, and all overlay
// corners within 64 epsilon times the requested maximum extent. Insufficient
// precision fails an explicit requirement, even with finite coefficients.
sheet crop_straighten {
    source image: Rect = Rect {
        origin: Point2D { x: 0.0, y: 0.0 },
        size: Size2D { width: 100.0, height: 100.0 }
    } require {
        @valid_image image.is_finite() && image.width > 0.0 && image.height > 0.0
            && image.max_x > image.min_x
            && image.max_y > image.min_y
            && Point2D { x: image.max_x, y: image.max_y }.is_finite();
    };
    source transform: Transform2D = identity_transform();
    cell crop: Rect = Rect {
        origin: Point2D { x: 0.0, y: 0.0 },
        size: Size2D { width: 100.0, height: 100.0 }
    } require {
        @positive_effective_crop crop.is_finite() && crop.width > 0.0 && crop.height > 0.0
            && crop.max_x > crop.min_x
            && crop.max_y > crop.min_y
            && Point2D { x: crop.max_x, y: crop.max_y }.is_finite();
    };
    cell shrink: f64 = 1.0 require {
        @representable_shrink shrink > 0.0 && shrink <= 1.0
            && 1.0 / shrink <= 1.7976931348623157e308;
    };
    cell precise: bool = true require {
        @sufficient_fit_and_presentation_precision precise;
    };
    // Map the effective crop back onto the unchanged source overlay.
    cell presentation: Transform2D = identity_transform() require {
        @finite_presentation presentation.is_finite();
    };

    relationship {
        (crop, shrink, precise, presentation) := (|
            r: Rect,
            i: Rect,
            inv: Transform2D,
            t: Transform2D
        |
            (|r: Rect, i: Rect, inv: Transform2D, t: Transform2D, a: Point2D, b: Rect|
                (|
                    r: Rect,
                    inv: Transform2D,
                    t: Transform2D,
                    s: f64,
                    accurate: bool
                | // Eight epsilon inward guard, only when shrinking.
                    (|r: Rect, inv: Transform2D, t: Transform2D, s: f64, accurate: bool|
                        (|
                            r: Rect,
                            inv: Transform2D,
                            t: Transform2D,
                            s: f64,
                            accurate: bool,
                            q: Rect
                        |
                            (|
                                r: Rect,
                                inv: Transform2D,
                                s: f64,
                                accurate: bool,
                                q: Rect,
                                p: Transform2D
                            | // Finite coefficients alone do not guarantee a fixed
                                // anchor or overlay after reciprocal-scale cancellation.
                                (|
                                    r: Rect,
                                    s: f64,
                                    accurate: bool,
                                    q: Rect,
                                    p: Transform2D,
                                    a: Point2D,
                                    lo: Point2D,
                                    lr: Point2D,
                                    ul: Point2D,
                                    hi: Point2D,
                                    e: f64
                                |
                                    (
                                        q,
                                        s,
                                        accurate
                                            && abs(q.width / r.width - s)
                                                <= 0.000000000000014210854715202004 * s
                                            && abs(q.height / r.height - s)
                                                <= 0.000000000000014210854715202004 * s
                                            && abs(q.center().x - r.center().x) <= e
                                            && abs(q.center().y - r.center().y) <= e
                                            && abs(a.x - r.center().x) <= e
                                            && abs(a.y - r.center().y) <= e
                                            && abs(lo.x - r.min_x) <= e
                                            && abs(lo.y - r.min_y) <= e
                                            && abs(lr.x - r.max_x) <= e
                                            && abs(lr.y - r.min_y) <= e
                                            && abs(ul.x - r.min_x) <= e
                                            && abs(ul.y - r.max_y) <= e
                                            && abs(hi.x - r.max_x) <= e
                                            && abs(hi.y - r.max_y) <= e,
                                        p
                                    ))(
                                    r,
                                    s,
                                    accurate,
                                    q,
                                    p,
                                    p.transform_point(inv.transform_point(r.center())),
                                    p.transform_point(inv.transform_point(q.origin)),
                                    p.transform_point(
                                        inv.transform_point(Point2D { x: q.max_x, y: q.min_y })
                                    ),
                                    p.transform_point(
                                        inv.transform_point(Point2D { x: q.min_x, y: q.max_y })
                                    ),
                                    p.transform_point(
                                        inv.transform_point(Point2D { x: q.max_x, y: q.max_y })
                                    ),
                                    0.000000000000014210854715202004 * max(r.width, r.height)
                                ))(
                                r,
                                inv,
                                s,
                                accurate,
                                q,
                                t.then(translation(Vector2D { x: -q.center().x, y: -q.center().y }))
                                    .then(scale(1.0 / s, 1.0 / s))
                                    .then(
                                    translation(Vector2D { x: q.center().x, y: q.center().y })
                                )
                            ))(
                            r,
                            inv,
                            t,
                            s,
                            accurate,
                            if s == 1.0 {
                                r
                            } else {
                                Rect {
                                    origin: r.center()
                                        - Vector2D { x: r.width * s * 0.5, y: r.height * s * 0.5 },
                                    size: r.size * s
                                }
                            }
                        ))(
                        r,
                        inv,
                        t,
                        if s == 1.0 { s } else { s * (1.0 - 0.0000000000000017763568394002505) },
                        accurate
                    ))(
                    r,
                    inv,
                    t,
                    min(
                        1.0,
                        min(
                            min(a.x - i.min_x, i.max_x - a.x) / max(a.x - b.min_x, b.max_x - a.x),
                            min(a.y - i.min_y, i.max_y - a.y) / max(a.y - b.min_y, b.max_y - a.y)
                        )
                    ), // Bound subtraction must retain the affine half-extents
                    // within 32 epsilon; reject cancellation, not an undersized
                    // but plausible result. This is a precision check only.
                    abs(
                        max(a.x - b.min_x, b.max_x - a.x)
                            / (abs(inv.m11) * (r.width * 0.5) + abs(inv.m21) * (r.height * 0.5))
                            - 1.0
                    )
                        <= 0.000000000000007105427357601002
                        && abs(
                            max(a.y - b.min_y, b.max_y - a.y)
                                / (abs(inv.m12) * (r.width * 0.5) + abs(inv.m22) * (r.height * 0.5))
                                - 1.0
                        )
                            <= 0.000000000000007105427357601002
                ))(r, i, inv, t, inv.transform_point(r.center()), inv.outer_transformed_rect(r)))(
            crop,
            image,
            transform.inverse(),
            transform
        );
    }

    out anchor: Point2D := transform.inverse().transform_point(crop.center()) require {
        @feasible_anchor anchor.is_finite() && anchor.x > image.min_x && anchor.x < image.max_x
            && anchor.y > image.min_y
            && anchor.y < image.max_y;
    };
    // Closed-edge coverage, not Euclid's half-open point containment.
    // Use the image coordinate/extent scale for the absolute rounding budget.
    out coverage: bool := (|b: Rect, i: Rect, e: f64|
        b.min_x >= i.min_x - e && b.max_x <= i.max_x + e && b.min_y >= i.min_y - e
            && b.max_y <= i.max_y + e)(
        transform.inverse().outer_transformed_rect(crop),
        image,
        0.000000000000014210854715202004
            * max(
                max(image.width, image.height),
                max(
                    max(abs(image.min_x), abs(image.max_x)),
                    max(abs(image.min_y), abs(image.max_y))
                )
            )
    ) require {
        @closed_edges coverage;
    };
}
}

Coordinate frames and source memory

image is an axis-aligned source-image rectangle. transform maps image points to the frame containing the axis-aligned requested crop. The runner constructs straightening rotations around the source-image point under the crop center. The relationship also works with arbitrary invertible affine translations, nonuniform scales, reflections, and shears; it does not assume a rotation or an origin-centered anchor.

The relationship reads and writes crop. Adam preserves the user’s source aspiration while it recomputes the effective value. Tightening the angle shrinks the crop uniformly; loosening it springs back. An explicit crop write replaces the aspiration. There is no extra requested-crop cell or helper-owned memory. Every typed, noncapturing IIFE forwards its needed values explicitly.

The expression maps the requested center and the four-corner outer bounds through the inverse transform. It divides each nearest-image-edge distance by the corresponding maximum inverse half-extent, then selects the minimum with one. This gives the largest uniform shrink about that center. Bounding the transformed image instead would include uncovered regions under rotation or shear. A crop with an infeasible center is an error; translating it is not a valid repair.

Numerical and error contract

Inputs need finite positive image/crop sizes, advancing finite endpoints, a finite center, a checked invertible transform, representable inverse bounds, and an image anchor strictly inside all four image edges. The runner’s validation-only write boundary checks candidates before changing source cells. It uses the general checked inverse and bounds operations, not a Rust fitting calculation.

The CEL relationship leaves a fitting crop bit-for-bit unchanged. When it must shrink, it multiplies the computed factor by 1 - 8*f64::EPSILON to round conservatively inward. The precise flag rejects inverse-bound cancellation that differs from affine support half-extents by more than 32*EPSILON; it also rejects subnormal rounding that changes either uniform size ratio by more than 64*EPSILON relative to the shrink factor. The relationship computes the fitted crop and its presentation together, while the source request is still available. It checks the fitted center, the presented source-image anchor, and all four effective corners mapped through the inverse and presentation against the requested center and overlay corners. Each component must agree within 64*EPSILON * max(requested.width, requested.height). A large absolute overlay offset does not relax this extent-relative budget. Finite presentation coefficients alone do not guarantee these postconditions: reciprocal-scale cancellation can move the anchor or distort the overlay.

The named sufficient_fit_and_presentation_precision requirement reports insufficient precision, even when fitting, coverage, and coefficient finiteness pass. The precise flag is validation metadata, not another requested rectangle. The expression contains these policies openly, without a crop-specific operation.

Acceptance uses a 64*f64::EPSILON scale-relative tolerance for closed-edge coverage, center preservation, maximality, and repeated application. For image coverage the scale is the maximum image extent or absolute image endpoint; crop-center and presentation comparisons use the requested overlay extent as specified above. For dimensionless maximality comparisons the scale is one. Ordinary Euclid point containment is half-open; crop coverage allows corners on maximum edges. The conservative result is maximal within this tolerance, not an exact-real arithmetic guarantee. Identity preserves a fitting crop exactly; applying the expression to its effective crop again is stable within tolerance.

Finite extreme magnitudes are supported when these computations remain representable. Nonfinite values, singular/lost-cofactor inverses, overflow, complete underflow, collapsed endpoints, infeasible centers, zero effective sizes, unrepresentable reciprocal scales, or insufficient precision produce a checked evaluation error or a named failed Adam requirement. Propagation alone does not mean requirements passed: the runner checks every requirement and refuses invalid effective values. Inspect errors and requirements instead of silently accepting an empty or numerically misleading crop.

Presentation

The runner reads parsed.sheet.source::<Rect>(crop_id) for the unchanged overlay, never the previously fitted value. shrink describes the current source-to-effective size ratio. presentation applies the image transform and then its reciprocal shrink around the same crop center. Thus the effective crop fills the original overlay and the same image anchor stays under its center, provided the explicit precision requirement passes. In a.then(b), a runs first. A fixed anchor alone does not certify the overlay corners.

The executable chapter test also covers unequal source/overlay magnitudes. With scale(3.0, 3.0), an image centered at zero of size 1e-6 × 1e-6 and a requested crop centered at zero of size 1e6 × 1e6 pass, preserving the center and overlay mapping despite a shrink near 3e-12. In contrast, an image with both origin coordinates 0.03333283332557231 and size 1e-6 × 1e-6, and a requested crop with both origin coordinates -499999.9 and size 1e6 × 1e6, fail the precision requirement under the same transform. All presentation coefficients remain finite, but the anchor maps to 0.10000228881835938 instead of the requested 0.09999999997671694, exceeding the approximately 1.42e-8 overlay budget. Rejection preserves the source aspiration; it does not translate the crop or silently narrow support to rotations or uniform scales.

For a [0,100] × [0,100] image, a centered square at 45 degrees has side 100 / sqrt(2) within 1e-12. For [0,100] × [0,80] with a requested 80 × 40 crop centered at (20,40), identity yields origin (0,30) and size (40,20) within tolerance. The center never moves to hide infeasibility.

Pan and zoom policy

#![allow(unused)]
fn main() {
// Standalone demonstration; the live graph owns a separate Adam sheet.
// content includes the renderer's fit margin; empty content uses the viewport.
sheet pan_zoom {
    source content: Rect = Rect {
        origin: Point2D { x: 0.0, y: 0.0 },
        size: Size2D { width: 100.0, height: 100.0 }
    };
    source viewport: Size2D = Size2D { width: 200.0, height: 200.0 } require {
        @nonzero_viewport viewport.is_finite() && viewport.width > 0.0 && viewport.height > 0.0;
    };
    out bounds: Rect := if content.width > 0.0 && content.height > 0.0 { content } else {
        Rect { origin: Point2D { x: 0.0, y: 0.0 }, size: viewport }
    };
    out fit_scale: f64 := min(
        viewport.width / max(bounds.width, 1.0),
        viewport.height / max(bounds.height, 1.0)
    );
    out fit_pan: Vector2D := Vector2D {
        x: viewport.width * 0.5 - fit_scale * bounds.center().x,
        y: viewport.height * 0.5 - fit_scale * bounds.center().y
    };
    cell zoom: f64 = 2.0;
    cell pan: Vector2D = Vector2D { x: 0.0, y: 0.0 };
    relationship { zoom := clamp(zoom, fit_scale, max(fit_scale, 8.0)); }
    relationship {
        // Compare actual endpoints: extent arithmetic can round differently at fit.
        pan := Vector2D {
            x: if viewport.width - zoom * bounds.max_x >= -zoom * bounds.min_x {
                viewport.width * 0.5 - zoom * bounds.center().x
            } else {
                clamp(pan.x, viewport.width - zoom * bounds.max_x, -zoom * bounds.min_x)
            },
            y: if viewport.height - zoom * bounds.max_y >= -zoom * bounds.min_y {
                viewport.height * 0.5 - zoom * bounds.center().y
            } else {
                clamp(pan.y, viewport.height - zoom * bounds.max_y, -zoom * bounds.min_y)
            }
        };
    }
    out view: Transform2D := scale(zoom, zoom).then(translation(pan));
}
}

The caller supplies measured visible-node bounds including the fit margin and a positive viewport. Empty content falls back to viewport bounds. Fit uses min(viewport.width / max(width,1), viewport.height / max(height,1)). Zoom clamps to [fit_scale, max(fit_scale,8)]. Each pan axis centers undersized content; otherwise it clamps to the actual scaled content edges. Self-referencing writes retain pan/zoom aspirations. For initial fit or reset, the caller writes fit_scale to zoom and fit_pan to pan.

This standalone sheet demonstrates the policy. The live graph uses its own Adam sheet in adam-web-ui, with a Dioxus proposal/approval bridge shared by Begin’s desktop and web builds and the book’s graph inspectors. JavaScript owns measurement, layout, and gestures, but renders only matching Adam approvals. The bridge waits for real positive viewport observations and retains the last valid viewport during transient zero-size observations. See graph view ownership and verification for the live model, lifecycle, and interaction contracts.

Lexical Conventions

An Adam source file is tokenized as Rust/CEL tokens (via proc_macro2): identifiers, integer and float literals (with optional type suffixes), string literals, and punctuation, exactly as cel-parser’s own lexical grammar defines them. Adam adds exactly one lexical extension on top of CEL’s own conventions: doc comments.

Comments

// starts a line comment; /* ... */ a block comment — the same two forms C, Rust, and CEL all share:

// a whole-line comment
cell width: i32 = 1920; // a trailing comment
/* a block comment, on one line or several */

Doc comments

/// immediately before a cell, source, relationship, conditional, or out declaration, and //! immediately before the sheet keyword itself, are Adam’s own addition to CEL’s lexical grammar: doc comments, recovered by the language server and the formatter, and otherwise inert — they carry no meaning when the sheet resolves:

//! A sheet describing a simple resize dialog.
sheet image_resize {
    /// The image's width in pixels, before any resampling.
    cell width_pixels: i32 = 1920; // a trailing comment
    /* a block comment,
       on several lines */
    cell height_pixels: i32 = 1080;
}

Keywords and reserved identifiers

Keywords: sheet, cell, source, relationship, conditional, out, require, filter. None of these can be used as a cell or sheet name. _ is not a keyword but is reserved in two specific positions: a conditional’s default branch (_ => { ... }, the default branch and reverting to source section), and inside a filter expression (the candidate value, the filter grammar); elsewhere it is an ordinary identifier.

Punctuation

: (type annotation), = (cell initializer), := (binding/output body), => (conditional branch), ; (declaration terminator), , (list separator), { } (block delimiters), ( ) (tuple/grouping delimiters).

Program Style

Canonical formatting

Adam ships its own formatter (adam fmt, backed by format_sheet) with one canonical layout: 4-space indentation, opening braces on the same line as the keyword that introduces them, and no space before a declaration’s closing ;. Given input that doesn’t already follow this layout, formatting normalizes it:

sheet s {
    cell x: i32 = 1;
    cell y: i32 = 2;
}

A formatter run is expected to be idempotent (formatting already-canonical source reproduces it unchanged) and preserves every comment, doc comment, and blank line exactly where it appeared, including a file-header comment before sheet itself and a trailing comment before a block’s own closing }. adam-lsp’s textDocument/formatting handler refuses to format a sheet with any recorded syntax error rather than guess at intent: format_sheet’s own precondition is that sheet.errors is empty.

A conditional branch’s trailing , (the grammar allows one after each branch’s closing }) is always omitted by the formatter, even though the parser still accepts it on input, so canonical Adam source never has one.

Reference Manual

This appendix is for looking things up, not reading start to finish: it restates the rules from the preceding chapters in one terse pass, plus the grammar and error messages in full. Where a rule needs justification or an example, the appropriate chapter is linked instead of repeating it here.

Grammar

sheet              = "sheet" identifier "{" { sheet_item } "}".
sheet_item         = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
                       | source_decl).

cell_decl          = "cell" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
cell_type_init     = (":" type_expr ["=" expression]) | ("=" expression).
cell_filter        = "filter" expression.
source_decl        = "source" identifier cell_type_init [ cell_filter ] [ require_block ] ";".

type_expr          = identifier [ "(" type_expr { "," type_expr } ")" ]
                    | "[" type_expr "]"
                    | "(" [ type_expr ["," [ type_expr { "," type_expr } ]] ] ")".

relationship_decl  = "relationship" "{" { binding } "}".
binding            = binding_target ":=" expression ";".
binding_target     = identifier | "(" identifier { "," identifier } [ "," ] ")".

conditional_decl   = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (expression | "_") "=>" "{" { relationship_decl } "}" [ "," ].

out_decl           = "out" identifier [ ":" type_expr ] ":=" expression
                       [ cell_filter ] [ require_block ] ";".
require_block      = "require" "{" { requirement } "}".
requirement        = [ "@" identifier ] expression ";".

expression and everything it expands to (literal, identifier, operators, if/else, as casts, ranges, closures, function calls) is CEL grammar, defined by cel-parser; see expressions are CEL.

cell_filter and require_block both attach identically to all three of cell_decl, source_decl, and out_decl — neither has a cell-kind restriction (see a source cell can be filtered too).

A cell_decl’s grammar also has a design-level provision for an optional trailing ":=" expression clause (making a cell double as a relationship-bound output in one declaration), not implemented as of this writing; only the "=" expression one-time initializer and the cell_filter/require_block clauses shown above exist today.

Sheets and namespaces

  • A sheet’s own name has no runtime meaning; it is not otherwise referenceable.
  • cell, source, and out declarations share one namespace. Declaring the same name twice, in any combination of the three, is a “duplicate cell” error.
  • Record constructor/type names occupy a separate namespace from values. TypeName { field: expr, ... } uses an installed schema; its name and field labels are not dependencies. Members expr.name and expr.name() dispatch a unary operation directly, ignoring same-named cells; expr.name(a) passes the receiver first. (expr.name)() instead calls the operation’s result. Only value references contribute dependencies, including inside closures.
  • No forward references. An identifier is only recognized as a cell dependency if that cell was declared earlier in the same sheet’s token order. Referencing an undeclared name is an “undeclared cell” error. See names and declaration order.

Cells and source cells

  • cell name: T;: requires T to have a registered default; the cell starts at that default.
  • cell name = expr;: expr is evaluated with no cell scope: it may not reference any other cell. The cell’s type is inferred from expr’s result type.
  • cell name: T = expr;: both forms combined; expr’s inferred type must equal T exactly, or the sheet fails to parse with a “type mismatch” error.
  • Compound cells (tuple types, arrays, and callable signatures) use canonical whole-value ownership. Host code accesses them through ParsedSheet::read_compound, write_compound, and source_compound with exact recursive representation witnesses. Arrays default to empty, tuples default recursively, and callables require an initializer. Every stored cell requires cloning and value equality.
  • Compound reads return independent owners; source_compound reads the original aspiration, even when filtering changes the effective value. Wrong complete read/write witnesses fail at the external boundary before a write mutates state. The private CellValue retains one canonical owner, not per-element erasure.
  • Prepared relationship, output, filter, and requirement frames bind recursive storage once during sheet construction. Propagation executes the raw CEL engine without recursive shape checks. Value-dependent failures still propagate.
  • Fn((A,), R) and Fn((), R) denote one and zero parameters. Callable cells, including empty arrays of callables, fail without equality; CEL container storage alone does not require equality. Debug formatting is independent.
  • source name: T; / source name = expr; / source name: T = expr;: identical rules to the three cell forms above, evaluated the same way — a source declaration is a cell declaration in every respect except its fixed CellKind (below).
  • Every cell has a fixed kind, assigned once at declaration and never reassigned: a plain cell may be a planner source or claimed as a method’s output, chosen per round; a source cell is always a source, never claimable as any method’s output; an out cell is always derived by its own fixed writer, never write()-able. See source cells for source and outputs and requirements for out.

See sheets, cells, and types and the type registry for the built-in type table.

The type registry

Type nameDefault
i8 i16 i32 i64 i128 isize0
u8 u16 u32 u64 u128 usize0
f32 f640.0
boolfalse
String""

A host application can register additional Rust types under new Adam type names via TypeRegistry::register (with a Default) or TypeRegistry::register_no_default (without one, such a type can only be used with an explicit initializer, never a bare : T declaration). This is a Rust-level embedding decision made before parsing a sheet, not something sheet source itself can do.

Record syntax requires a separate OpLookup::register_record schema, and dot operations require host implementations (optionally with registered operation signatures). The host passes its lookup as the third argument to check_sheet(&sheet, &registry, &lookup) so static checking sees the same metadata as execution. Known custom annotations and results retain complete leaf descriptors and do not unify with unrelated types; truly unresolved values and custom calls without metadata remain best-effort, with all children checked.

Record fields are required exactly once and match their types exactly: no implicit integer-to-float conversion. Unknown constructors and unknown/duplicate/missing/mistyped fields are errors. Field evaluation follows written order. Empty schemas accept {}; non-empty fields permit a trailing comma (call arguments do not). Records must be parenthesized in if conditions and conditional subjects unless inside a parenthesized, call, tuple, or array subexpression. Members are never assignment targets; binding targets remain whole cell names or tuple patterns. See the executable host example.

RangeInclusive<T> recognition for range filters is pre-registered for exactly the built-in numeric types above and is not extensible per custom type in the current design.

Relationships and the solver

  • A relationship names one or more bindings; exactly one is selected each time the sheet resolves.
  • Selection is driven by cell strength, a write-recency counter: a cell’s own declaration and any explicit write both bump it, so before any explicit write, declaration order alone ranks every cell: later declared is “fresher.” The solver tries, freshest first, to leave each cell a source (unclaimed by any binding), keeping the attempt only if a valid, acyclic assignment still exists across every active relationship. See strength: who gets to stay a source.
  • Every method in a relationship must reference the same inputs ∪ outputs cell set as every other method in that relationship, or resolution fails to parse with `methods in a relationship must reference the same set of cells`. See the rules a relationship’s methods must satisfy.
  • A method’s own outputs must be duplicate-free, and no two methods in a relationship may share an identical outputs set, or resolution fails to parse with `a method's outputs must be duplicate-free, and no two methods in a relationship may share an outputs set`. See the rules a relationship’s methods must satisfy.
  • A cell may appear in both a method’s inputs and its own outputs — a self-referencing method — which is explicitly allowed. See self-referencing methods.
  • Resolving the sheet fails with a conflict if no valid assignment exists at all, or a cycle if every valid assignment forms a closed dependency loop with no source anywhere in it. See when no assignment exists.
  • A binding’s left-hand side destructures a tuple result element-wise when parenthesized with more than one name, or exactly one name plus a trailing comma; a bare name or a single parenthesized name with no comma binds the whole result directly. See destructuring bindings.
  • A source cell can never be a binding’s output: a relationship (or conditional branch) naming one as an output is a parse-time error. See source cells.
  • Whether a cell was left unclaimed (a source) by the last resolution is queryable by a host; see the host embedding API.

Conditionals

  • The match subject is a cell or a deduced expression; each branch’s literal must match its inferred type exactly.
  • Only the currently-active branch’s relationships participate in a round’s solve; every other branch’s relationships are invisible to the planner that round.
  • _ => { ... }, if present, must be the last branch and matches any value no named branch lists. With no default and no match, none of the conditional’s relationships are active, and their would-be output cells are left as sources.
  • A branch body holds only relationship declarations: no cell declarations, no nested conditional.
  • A relationship with exactly one method is forced: its output cell is claimed every round, regardless of strength. Sheet::is_forced reports this. See forced cells.

See conditionals.

Filters

  • cell_filter = "filter" expression, trailing a cell_decl, source_decl, or out_decl — a filter attaches to any cell kind, with no per-kind restriction (see a source cell can be filtered too). A filter carries no name; _ inside the expression denotes the candidate value (of the cell’s own declared type); every other identifier is a deduced dependency. The expression must reference _ at least once (unless it’s a range expression, lo..=hi, which is exempt) and must produce the filtered cell’s own type.
  • Writing a cell never applies a filter. A filter is applied live, each time the sheet resolves, against the cell’s current value.
  • A filtered cell keeps a raw source value (last written, untouched by any filter forever) and a computed derived value (the filter’s live output, recomputed every time the sheet resolves); reading the cell returns the derived value when present, the source value otherwise.
  • A filter attached to a cell a relationship currently claims (a derived cell that round) is diagnostic-only: it never corrects the value, only flags a mismatch, queryable by a host; see the host embedding API. The same is true, unconditionally, of a filter on an out cell, since an out cell is always derived.
  • At most one filter per cell. Compound filters must produce the cell’s exact recursive shape.

See the filters chapter for the full model and worked examples.

Outputs and requirements

  • out name := expr; declares a new cell, always derived by expr and never writable directly — not by a host write, not a relationship, not another out — but otherwise an ordinary, freely-referenceable cell: any later declaration may read it by name exactly like any other already-declared cell. See outputs and requirements.
  • require { [ "@" name ] expr; ... } attaches boolean checks, each with an optional @name label. Unlike filter, require is not tied to out: a require block may trail a cell, source, or out declaration’s initializer, with the same meaning in every case. Each requirement’s own dependencies are deduced separately from its declaration’s own expression. A failing requirement never stops the sheet from resolving, or its cell’s own value from being computed: it’s reported as a diagnostic, nothing more, queryable by a host (see the host embedding API).

See requirements: diagnostics, not gates and cell declarations for require on a plain cell, and source cells for require on a source cell.

Error messages

Adam reports every diagnostic as a ParseError carrying a source span; there is no separate runtime error type for a malformed sheet; if parse_str returns Ok, the sheet is syntactically and structurally valid (though it may still fail when resolved for the solver reasons in relationships and the solver). Selected messages, verbatim:

Message (abbreviated)Cause
duplicate cell \name``a cell/source/out name reused
undeclared cell \name``a name referenced before its declaration
expected \:` or `=` in cell declaration`a cell with neither a type nor an initializer
expected \:` or `=` in source declaration`a source with neither a type nor an initializer
type \T` has no default; provide `= …``a bare : T cell where T has no Default
type mismatch: expected \T`, got `U``a declared type disagreeing with an inferred one
expression produced no valuea body expression that isn’t a value-producing CEL expression
cannot infer a type: unregistered type \T``a result containing a leaf without an Adam registration
filter must reference \_``a non-range filter body that never mentions _
filter must produce \T``a filter body whose result type doesn’t match the cell
filter range bounds must be \T``a lo..=hi filter whose element type doesn’t match the cell
Adam cell equality capability unavailable for \T``a compound value, such as a callable, lacking value equality
output \name`: type mismatch: …`a relationship binding output’s declared vs. actual type
output expression has arity N but M output(s) declareda destructuring binding’s tuple arity mismatch
requirement[ \name`]: expected `bool`, got `T``a requirement body that isn’t boolean — the `name` segment appears only when the requirement is labeled; the colon is always present
methods in a relationship must reference the same set of cellsa relationship’s methods have different inputs ∪ outputs sets
a method's outputs must be duplicate-free, and no two methods in a relationship may share an outputs settwo methods in one relationship claim the same outputs set, or one method repeats a cell in its own outputs

The host embedding API

This book documents the language; the Rust API a host application uses to parse and drive a sheet is documented by the crates themselves:

  • AdamParser: parses source into a live ParsedSheet, which derefs to Sheet.
  • TypeRegistry: the type-name-to-Rust-type table a parser is built with; TypeRegistry::new pre-populates the built-ins in the type registry.
  • OpLookup: the function-library table a parser is built with; this book’s own examples install cel-std via support::parser (see adam-lang-book’s own crate source).
  • Sheet: read, write, propagate, is_source, is_forced, cell_kind, filter_*, cell_requirements, cell_requirements_valid, violated_requirements, and every other runtime operation this book has used throughout.
  • AdamAstParser / format_sheet / check_sheet: the span-carrying CST, formatter, and static type checker behind the language server and adam fmt (see program style), distinct from AdamParser’s eager compile-to-Sheet path.