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

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