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

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