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

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.