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

This book is 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

Application properties constrain one another. For example, pixels == inches * resolution lets any two values determine the third. Such relationships preserve document invariants, constrain command arguments and results, or construct new values.

Applications often implement 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.

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. Cell strength determines which cell is the source and which is derived. Typically, cells modified most recently have greater strength.

About this book

The tutorial introduces Adam through examples; the following chapters explain each construct, and the reference manual collects the grammar and rules. Fenced adam blocks contain .adm2 source; fenced text blocks contain grammar notation (EBNF).

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 examples install cel-std.

A Tutorial Introduction

Adam programs are called sheets, a term borrowed from spreadsheets. This tutorial introduces the language through live examples.

An Adam sheet declares the relationships among a set of properties called cells. These relationships determine how edits propagate through a user interface or scripting system.

Each source fragment defines the cells and relationships shown in its live inspector and graph. Edit values to explore how they propagate. Applications typically bind a sheet to controls or scripts with human-readable labels rather than cell identifiers.

A first sheet

The first example defines one named sheet, with declarations between braces. Modules may contain multiple definitions and imports. This sheet declares two source cells:

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

A cell is a named, typed storage location. Adam infers width and height as i32 from their initial values. A source cell holds a value supplied from outside the sheet, like a spreadsheet’s value cell.

A sheet describes a graph of cells and constraints. The graph for hello has 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 output’s method can read other cells; resolution updates its result when those inputs change. The graph draws the relationship as a circle between cells. Heavy arrows and the output cell’s border mark a forced value. The live inspector disables editing for forced cells.

See the outputs chapter for the full treatment.

Cells and Relationships

A plain cell may be a source or a derived output. Relationships connect cells through alternative methods, each solving for a different cell or set of cells.

This relationship maintains \(a = 2b\):

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;
    }
}

Resolution selects exactly one method per active relationship, based on cell strength. Cells written more recently have 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);
    }
}

The methods here are self-referencing: each reads a cell it also writes. Such a method must be idempotent, leaving its corrected output unchanged when reapplied. Self-referencing methods and filters preserve the cell’s last-written source value.

Slide a to 100 to pull b and c to 100, then slide a back to 0. b and c return to their prior values.

Relationships may connect any number of cells, with each method solving for one or more; see the relationships chapter.

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 while preserving its source value. Releasing the relationship restores that source. Host UIs commonly disable controls for forced cells.

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

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

Requirements

A filter on a derived cell reports a violation without changing the value:

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. Each requirement is a Boolean diagnostic, not an input gate: a source or writable cell retains a value that violates a requirement, and propagation reports the requirement’s failed validity.

The same rule applies to an out cell. 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

Continue with sheets, cells, and types for declaration and type rules, or use the reference manual to look up grammar, built-in types, and errors.

Sheets, Cells, and Types

Sheets

The examples begin with a single root sheet. An Adam module may also contain imports and multiple root or parameterized sheet definitions:

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 name (name above) identifies the definition for module resolution and root selection. Host tooling also uses it for 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

cell width; is a syntax error (“expected : or = in cell declaration”). A cell may also carry a trailing filter clause for a domain constraint, a require block for boolean diagnostics, or both. These clauses also apply to source and out cells.

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""

Without a : type_expr annotation, the initializer’s result determines the cell’s type. CEL defaults an unsuffixed integer literal such as 0 to i32 and an unsuffixed float such as 0.0 to f64; see the CEL literal grammar. When both a type annotation and an initializer are present, their types must agree exactly, or parsing fails with “type mismatch: expected T, got U” (see error messages).

A host application can register additional Rust types under Adam type names through 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; (T) groups the type T; (T,) is a one-element tuple with a required comma; (T, U, ...) is a tuple with multiple elements:

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. Registered host function markers retain the concrete capabilities their type registration supplies.

Executable host boundary

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. An empty array of callable elements also lacks equality when its element descriptor does.

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};

    /// Represents an integer key paired with an array of measurements.
    type Row = Cons<Leaf<i32>, Cons<ArrayOf<Leaf<f64>>, Nil>>;
    /// Represents a canonical array of keyed measurement rows.
    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
    );
}
}

This example checks the missing-equality error:

#![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 sheet. cell, source, and out declarations share one namespace; declaring both cell result: i32 = 0; and out result := ...; is a duplicate-name error.

Referencing a name before its declaration, whether as a binding output or an expression 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 uses the same type and initializer rules as a cell, including evaluation without cell scope. It accepts an optional filter clause and require block. source, cell, and out share one namespace; declaring both source width and cell width is a “duplicate cell” error.

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

    out area := width * height;
}

width and height supply inputs to the output that multiplies them.

Always a source, never derived

A source cell cannot be claimed as a method’s output, including in a conditional branch. Naming it on a binding’s left-hand side is an error. An out cell, by contrast, always has its own fixed writer.

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 retains its last external write, or its initializer before any write. A filter may conform its effective value without changing that source value.

A source cell can be filtered too

A source cell can be read wherever a plain cell can and written directly by the host. A require block reports domain diagnostics:

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

A filter clause conforms its effective value during resolution, not during a write:

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

Expressions and Dependency Deduction

Expressions are CEL

Adam uses CEL expressions in cell initializers, relationship bindings, conditional subjects, outputs, requirements, and filters. cel-parser parses them; cel-runtime evaluates them. The CEL reference manual defines literals, operators, if/else, casts, ranges, function calls, and closures. This chapter explains which cells an expression may read and how Adam uses its result.

No standard library of its own

A bare OpLookup::new() provides CEL’s built-in operations but no function library. Calling an undefined function fails at parse time with an error naming the function. This book’s examples install cel-std.

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 has no cell scope. Referencing a cell, even one already declared, produces an “undeclared cell name” error at parse time; see error messages.

To compute one cell’s value from another’s, write a relationship or an out declaration.

Deduced dependencies

A relationship binding, conditional subject, output body, or requirement deduces its inputs from references to already-declared cells. No explicit parameter list is needed. Referencing the same cell more than once (a && a) counts as one input. Cells must be declared before use.

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 {}; non-empty field lists reject trailing commas. 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, where { begins the following body. 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 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 required expression must produce a value whose leaf types are registered with the TypeRegistry. Parsing reports expression produced no value for a body without a result, or cannot infer a type: unregistered type `T` for an unregistered result type. These errors identify the enclosing construct. A type annotation does not register a 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 cell can have at most one filter; filters have no names.

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

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

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

Writing never filters

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

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

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

The raw value is never lost

A filtered cell keeps two values: its raw last-written value, the source, and, when something currently claims it, a computed override, the derived value. Reading the cell always returns the derived value if one is present, the source value otherwise. A filter writes only the derived value, preserving the original input across resolutions. If a dynamic bound loosens, the cell returns toward its last-written value rather than an intermediate clamped value:

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

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

Range filters

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

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

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

Derived cells: diagnosed, never corrected

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

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

    relationship {
        bound := driver;
    }
}

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

A filter on an output cell

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

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

Errors

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

Outputs and Requirements

Grammar

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

An out declares a cell computed by one expression, with deduced dependencies. The optional : type_expr annotation must match the expression’s type; without it, Adam infers the type. An output may also carry a filter clause.

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

An output shares the cell namespace and must have a unique name. Later declarations may read it as a relationship input, conditional subject, filter argument, or another output’s input. Only its own expression may write it: host writes, relationship bindings, and additional out declarations cannot. The expression is purely functional; resolution may reuse its result:

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

Requirements: diagnostics, not gates

A require { ... } block attaches zero or more boolean checks to a cell, source, or out declaration, after any filter. Each requirement deduces its dependencies separately from the initializer. It may read the cell it checks and any other declared cells:

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 does not stop resolution or prevent a cell’s value from being computed and read. Hosts can query failures through the embedding API. The optional @name identifies a requirement for reporting; it is not a cell reference.

Multiple requirements

A declaration can list any number of requirements, which hosts can query individually:

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 { "," identifier } ] ")".

Each binding defines a candidate method: an expression with deduced inputs and the outputs named to the left of :=. Resolution selects exactly one method per active relationship. Only that method writes its outputs; other methods may contribute to self-referencing seed construction. Selection does not guarantee evaluation; the runtime may reuse a result. Neither a source nor an out cell may be a binding’s output. Both may be inputs.

Strength: who gets to stay a source

Every cell carries a strength, a write-recency counter. The solver prefers to derive the stalest cells and leave the freshest as sources. A declaration gives a cell its initial strength, so later declarations start stronger. An explicit write promotes its cell to freshest; reading never changes strength. See the tutorial example.

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 a relationship must reference the same inputs ∪ outputs cell set. A violation fails at parse time with methods in a relationship must reference the same set of cells. Methods differ in which cells they derive, not which group they constrain. The tutorial’s multiplication triangle satisfies this rule: c := a * b, a := c / b, and b := c / a all reference {a, b, c}.

A method’s outputs list must be duplicate-free, and no two methods in a relationship may claim the same output set. Either violation fails at parse time with a method's outputs must be duplicate-free, and no two methods in a relationship may share an outputs set. The triangle’s output sets, {c}, {a}, and {b}, are distinct.

A self-referencing method includes a cell in both its inputs and outputs; see the relationships-continued chapter.

A shared-cell example

Cells may participate in multiple relationships. Here, two relationships share b and c:

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: keeping both d and c as sources selects b := d/c in the second relationship. The first can select a := c/b, so both sources can be preserved.
  • b: preserving c and d requires the second relationship to write b. The solver cannot also leave b as a source.
  • a: the first relationship’s remaining choice is a := c/b, deriving a.
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;
    }
}

Hosts can query which cells remained sources to choose which fields to make editable.

Reuse a component

A parameterized sheet names a reusable component. Its concrete formal parameters alias caller-owned cells. A sheet without parentheses defines a root; sheet empty() {} defines a zero-parameter component, not a root.

This inline definition uses the same three floating multiplication methods as the triangle above. Binding factor to a fixed source removes the method that would write it, leaving the two directions between value and result.

sheet multiply(a: f64, b: f64, product: f64) {
    relationship {
        product := a * b;
        a := product / b;
        b := product / a;
    }
}

sheet inline {
    source factor: f64 = 3.0;
    cell value: f64 = 2.0;
    cell result: f64 = 0.0;
    relationship multiply(product: result, a: value, b: factor);
}

Each selected division needs a nonzero divisor; callers requiring finite arithmetic also require finite operands and representable results. Components do not infer inverse equations or choose directions from current numeric values. Floating operations retain IEEE 754 behavior, including infinities and NaNs; they do not report the checked errors that integer division reports.

The shipped adam-lang/examples/relationships.adm2 also defines finite ordered bounds. composed.adm2 imports both definitions, aliases multiply as mul, and uses the same three-way floating component twice with distinct named actuals. Those files contain several roots and a component-only module, so hosts register both modules and select a root explicitly:

#![allow(unused)]
fn main() {
/// Compiles the shipped modules, selects a root, and retains execution after owner drop.
#[test]
fn imported_components() {
    use adam_lang::{ModuleCompiler, ModuleSet, TypeRegistry};
    let mut modules = ModuleSet::new();
    modules
        .insert_source(
            "relationships".parse().unwrap(),
            "relationships.adm2",
            include_str!("../../adam-lang/examples/relationships.adm2"),
        )
        .unwrap();
    modules
        .insert_source(
            "composed".parse().unwrap(),
            "composed.adm2",
            include_str!("../../adam-lang/examples/composed.adm2"),
        )
        .unwrap();
    // These scalar examples need only builtin CEL operations.
    let compiled = ModuleCompiler::new(TypeRegistry::new(), cel_parser::OpLookup::new())
        .compile(modules)
        .unwrap();
    let path = "composed".parse().unwrap();
    assert_eq!(
        compiled.roots(&path).collect::<Vec<_>>(),
        ["ordered_demo", "source_specialized", "repeated_products"]
    );
    assert!(
        compiled
            .roots(&"relationships".parse().unwrap())
            .next()
            .is_none()
    );
    assert!(compiled.instantiate_root(&path, "multiply").is_err());
    let mut parsed = compiled
        .instantiate_root(&path, "source_specialized")
        .unwrap();
    drop(compiled);
    let product = parsed.cell_names["product"].0;
    let value = parsed.cell_names["value"].0;
    parsed.sheet.write(value, 7.0f64).unwrap();
    parsed.propagate().unwrap();
    assert_eq!(*parsed.read::<f64>(product).unwrap(), 21.0);
}
}

The imported examples need only built-in arithmetic and comparisons. Hosts using the image examples register Euclid types and install cel-std and cel-euclid on the lookup before creating the immutable compiler or parser. An empty lookup does not supply those optional functions.

For source-free Rust construction, run cargo run -p adam-lang --example templates. Its native path builds TemplateBuilder/MethodRecipe factories and instantiates them into an ordinary adam_rs::Sheet; it never parses source. The checked integer example uses Rust’s truncating division, so exact inverse multiplication requires a divisible product. The parsed examples use f64.

cel-euclid/examples/image_constraints.adm2 extracts the active resampling axis, an aspect relationship and the crop fit from the existing examples. resize_straighten.adm2 binds two axis instances and opts into aspect(width: width_inches, height: height_inches, ratio: width_per_height). The caller supplies the positive finite width_per_height source, initially 1600.0 / 1200.0. Equating the original width/height percentages gives width = height * ratio; the reverse direction divides width by that nonzero ratio. Changing only width to 4 inches derives height as 3 inches; changing only height back to 4 inches restores width to 1600.0 / 300.0 inches. Dimensions and selected products/quotients must stay positive, finite and representable. This relation constrains physical dimensions; the pixel axes still round independently, so integer pixel counts need not have an exact ratio. Ordinary derived dimensions update their raw source slots; edited source dimensions, filtered pixel requests and the self-referencing crop retain their caller aspirations under the ordinary sheet rules.

The root feeds an ordinary relationship-produced image rectangle into the crop component. The caller owns sources, filters, requirements and the crop aspiration. Private anchor/coverage outputs remain diagnostics, never actual arguments. This caller chooses filters that conform pixel requests to the original 300000-pixel limit while preserving their source aspirations; the original begin example instead reports that limit through requirements alone. The reusable axis installs neither policy, so other callers retain their own choice.

The axis retains ties-even pixel rounding and checked casts. The caller selects the reusable aspect relation separately; neither component implements the original example’s resample toggle or other UI machinery. The fit retains its 8-epsilon inward guard, 32-epsilon cancellation check and 64-epsilon overlay and coverage budgets. The caller rotates about the image point under the requested crop center. Singular transforms fail checked inversion. Insufficient precision invalidates an explicit requirement, rather than producing a promised valid fit. Read requirement validity after propagation; a successful propagation alone does not certify coverage. Resize, rotation and reversal preserve source(crop).

See the component reference for binding, conditional eligibility, ownership and error rules.

When no assignment exists

Resolution requires exactly one selected binding per active relationship. Two unconditional relationships that can only write the same cell cannot both be satisfied; resolution fails with no valid method assignment (overconstrained) (Error::Conflict).

A cycle occurs when assignments exist, but every choice of bindings forms a closed dependency loop without a source. Resolution 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.

Relationships Continued: Destructuring and Self-Referencing Methods

Destructuring bindings

A parenthesized binding target destructures a tuple result into output cells. The result must have matching arity and element types:

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,) := ... both destructure. Parenthesized binding targets require the first comma, so (a) := ... is invalid. A bare a := ... binds the right-hand side’s whole result (including a tuple-typed one) directly to the named cell. Each output’s declared type must structurally match its result.

Self-referencing methods

A self-referencing method reads a cell it also writes. During resolution, it reads that cell’s source value, not its previous derived value. The source remains separate from the computed correction, as it does for filters:

sheet clamp_via_relationship {
    cell level: i32 = 0;

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

Writing level stores its raw value. The next resolution applies the correction without overwriting that source.

Self-referencing methods must be idempotent

A self-referencing method must be idempotent: reapplying it to its corrected output leaves the output unchanged, f(f(x)) == f(x). min(level, 0) satisfies this: once level is at most 0, applying min again returns the same value.

The sheet author must ensure idempotence; neither construction nor propagation checks it. A non-idempotent operation, such as swapping two cells’ values, does not define a stable correction.

Conditionals

Grammar

conditional_decl   = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (literal_pattern | "_") "=>" "{" { relationship_decl | relationship_use } "}".
literal_pattern    = [ "-" ] literal.

A conditional evaluates its match subject, the expression after the keyword, and activates the branch, if any, whose literal equals the result. Only that branch’s relationships participate in resolution. Branch bodies accept relationship declarations and component uses, but no cell declarations or nested conditionals. Branches have no comma separator or terminator.

The match subject

The match subject may be a cell name (conditional mode { ... }) or an expression with deduced dependencies:

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, but tuple expressions are not literal patterns, so such a conditional can use only the _ default branch.

Forced cells

A relationship with exactly one method claims its output whenever active, regardless of strength. The output is forced, as reported by Sheet::is_forced. This query returns false for a relationship with multiple methods, even when strength selects the same direction every round.

Host UIs commonly disable editing for forced cells because resolution overrides their writes. The language does not require this convention; a host may accept a write and preserve it as shadow state.

Shadow state: forced and self-referencing cells

A forced cell preserves its source underneath its derived value, as filters and self-referencing methods do.

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. Strength selects one of its two methods, low := min(low, high) or high := max(low, high). 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 source is still 4. Writing high did not change low‘s source. Switching back to mode == 0 reselects low := min(low, high) (still the stalest cell) and recomputes it from both cells’ sources, 4 and 42, rather than the 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.

The default branch and reverting to source

_ => { ... } matches any value not listed by a named branch and must appear last. A named branch after _ is a syntax error. The default branch is optional: if the match value doesn’t equal any branch’s literal, none of the conditional’s relationships are active that round, and their outputs become freely writable. Forced and self-referencing cells preserve a separate source value, so releasing those cells restores their last external write or declared default. Ordinary cells in reversible relationships need not retain a separate shadow; releasing the relationship does not promise to undo their last derived value. The previous section demonstrates the shadow-state mechanism for forced and self-referencing cells.

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

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

Nested and chained conditionals

Sibling conditionals may read one another’s outputs as match subjects or relationship inputs. Branch bodies cannot contain conditionals. A resize dialog might use two sibling conditionals, 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.

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

Run cargo run -p cel-euclid --features adam --example adam_geometry to execute this library example. The geometry examples use a Rust host rather than live inspectors.

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

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.

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.

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

Adam uses CEL’s lexical conventions: identifiers, integer and float literals with optional type suffixes, string literals, and punctuation. Adam also recognizes doc comments.

Comments

// starts a line comment; /* ... */ encloses a block comment:

// 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 sheet introduce doc comments. The language server and formatter retain them; they do not affect sheet resolution:

//! 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

Adam recognizes sheet, use, as, cell, source, relationship, conditional, out, require, and filter in their grammar positions. They are contextual: cell names may use these words. Sheet definitions and module names reject _, sheet, use, as, empty names, and raw identifiers. _ is also 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 fmt, backed by format_sheet, applies one canonical layout: four-space indentation, opening braces on the keyword’s line, and no space before a declaration’s closing ;:

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

Formatting is idempotent: formatting canonical source leaves it unchanged. It preserves comments, doc comments, and blank lines in their original positions, including before sheet and before closing braces. The language server refuses formatting when syntax errors are recorded. Callers of format_sheet must supply a sheet with an empty sheet.errors list.

Commas separate list items; they do not terminate lists. Only singleton tuple values, types and destructuring bindings require a terminal comma to distinguish them from grouping. Conditional branches have no comma separator or terminator.

Reference Manual

This appendix collects Adam’s grammar, type rules, solver behavior, and error messages.

Grammar

module             = { use_decl | sheet }.
use_decl           = "use" qualified_name [ "as" identifier ] ";".
sheet              = "sheet" identifier [ "(" [ parameter { "," parameter } ] ")" ]
                       "{" { sheet_item } "}".
parameter          = identifier ":" type_expr.
sheet_item         = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
                       | source_decl | relationship_use).

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 } "}".
relationship_use   = "relationship" qualified_name "(" [ actual { "," actual } ] ")" ";".
actual             = identifier ":" identifier.
qualified_name     = identifier { "::" identifier }.
binding            = binding_target ":=" expression ";".
binding_target     = identifier | "(" identifier "," [ identifier { "," identifier } ] ")".

conditional_decl   = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (literal_pattern | "_") "=>" "{"
                       { relationship_decl | relationship_use } "}".

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

expression, literals, identifiers, operators, if/else, casts, ranges, closures, and function calls use CEL grammar; see expressions are CEL.

cell_filter and require_block may attach to cell_decl, source_decl, and out_decl (see a source cell can be filtered too).

Sheets and namespaces

  • Root names identify explicit host selections. Parameterized sheet names identify components used by named relationship invocations.
  • 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.
  • Cell dependencies must be declared earlier in the same sheet. Referencing an undeclared name is an “undeclared cell” error. See names and declaration order.

Components, modules and roots

  • sheet app { ... } defines a root; sheet component(...) { ... } defines a reusable component, even with an empty parameter list. A root is not an importable component. Imports refer to declared exports, not imported aliases.
  • relationship component(formal: actual, ...); binds each formal exactly once. Binding order does not matter. Actuals must be distinct, previously declared ordinary cells or fixed sources with exactly matching complete concrete types. Expressions, literals and out cells cannot be actuals. Parameters have no initializers, filters or requirements, and cannot become private outputs.
  • Components may own private cells, outputs and policies. Their local names do not enter the root’s public name maps. Each use allocates fresh private state and relationships; repeated uses do not merge relationships.
  • Callers retain boundary filters, requirements, strength and source aspirations. Fixed-source actuals remove every method that would write them, including nested writes. This specialization uses cell kind, not the last round’s is_source. An empty specialized relationship fails construction, even in an inactive branch.
  • Conditional uses require a transitively relationship-only component. Private cells, outputs or policies make a definition ineligible there. Unconditional uses may contain those declarations. Final combined graphs must satisfy the ordinary conditional guard-independence rules.
  • Hosts supply explicit ModulePath identities and source owners to ModuleSet. The core performs no filesystem IO, path inference or package discovery. ModuleCompiler::compile checks all definitions, including unused ones, without executing host expressions. Install operation libraries and register custom types before creating the immutable compiler or parser.
  • CompiledModules::roots enumerates root names; instantiate_root(&module, name) selects one explicitly. Component-only modules need no root. AdamParser::parse_str is the convenience entry for a source with exactly one root and no externally supplied imports.
  • Construction runs defaults, initializers and factories, but never propagates. Compiled templates and live methods retain their immutable environments after earlier owners drop. Native factories must create fresh execution state and obey the pure-method contract. A failed native instantiation leaves its destination safely discardable, not necessarily unchanged.
  • Compile/construction diagnostics own their actual source identities and labels; runtime errors use ParsedSheet::locate_error. Source-free native diagnostics retain native context without manufactured locations. Checked division, inversion and conversion failures still require handling. Requirement violations instead set validity flags, which callers inspect after propagation.
  • Components have concrete types, not generic type parameters. They do not infer equations, inject caller policies or turn private outputs into bindable cells.

The relationships chapter executes inline and imported examples. For VS Code, map every module, including the importing root:

{
  "adam-lang.modules": {
    "relationships": "adam-lang/examples/relationships.adm2",
    "composed": "adam-lang/examples/composed.adm2",
    "image_constraints": "cel-euclid/examples/image_constraints.adm2",
    "resize_straighten": "cel-euclid/examples/resize_straighten.adm2"
  }
}

Paths are relative to the first workspace folder. The extension sends owned path/uri/text snapshots to the language server; the core does not read files. UI hosts render owning module errors with adam_web_ui::format_module_diagnostic(&diagnostic, &Renderer::plain()). The separate format_parse_error adapter takes the original text and display name for an anonymous source-only parse; do not substitute root text for imported definition locations.

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.
  • 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;, and source name: T = expr; use the corresponding cell type and initializer rules, but have a fixed source kind.
  • 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 and rejects direct writes. 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

For a complete Dioxus consumer with application type/operation registration, explicit root selection, bound controls, recording, and host recovery policy, see the Dioxus integration guide.

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

A host registers additional Rust types under Adam names with TypeRegistry::register (with a Default) or TypeRegistry::register_no_default (without one). A type without a default requires an initializer; a bare : T declaration fails at parse time. Registration happens in the host before parsing, not in sheet source.

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 {}; record fields and call arguments reject trailing commas. 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 parsing fails 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 parsing fails 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 self-referencing method includes a cell in both its inputs and outputs. 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 its required comma. A bare name binds the whole result directly; a single parenthesized name without a comma is invalid. 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

  • A filter clause may follow a cell, source, or out initializer. Filters have no names. _ denotes a candidate of the cell’s declared type; other cell references are deduced dependencies. A non-range filter must reference _ and return exactly the cell’s type. A range filter lo..=hi need not reference _; its bounds must match the cell’s numeric type. Violations are parse-time errors.
  • Writing a cell never applies its filter. Filtering occurs during resolution.
  • A filtered cell preserves its last-written source value separately from its computed derived value. Reads return the derived value when present, otherwise the source.
  • 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 cell derived only by expr. Host writes, relationships, and additional out declarations cannot write it. Later declarations may read it; see outputs and requirements.
  • require { [ "@" name ] expr; ... } attaches boolean checks, each with an optional @name label to a cell, source, or out declaration, after any filter. Each requirement deduces its dependencies separately from the declaration’s expression. A failing requirement does not stop resolution or value computation; hosts query its diagnostic through the 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

parse_str reports malformed sheets as ParseError with source spans. Success establishes syntactic and structural validity, but resolution may still fail; see relationships and the solver. The following table lists selected messages:

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

The crate documentation describes the Rust APIs for parsing and driving sheets:

  • 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 parser’s function-library table. This book’s examples install cel-std.
  • Sheet: read, write, propagate, is_source, is_forced, cell_kind, filter_*, cell_requirements, cell_requirements_valid, violated_requirements, and other runtime operations.
  • 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.