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

Expressions and Dependency Deduction

Expressions are CEL

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

No standard library of its own

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

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

Cell initializers see no cells

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

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

Deduced dependencies

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

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

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

Host records and members

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

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

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

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

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

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

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

“Expression produced no value”

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