The Adam Programming Language
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
Many of an application’s properties are related to each other: a document’s own structure
holds invariants among them, a command’s arguments and result constrain one another, and some
relationships exist purely to construct a new argument or document value, such as
pixels == inches * resolution so that entering any two of the three determines the third.
An application usually hand-codes each of 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, so behavior
that would otherwise be hand-written event logic happens automatically.
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. Which cell is the source and which is derived is decided
based on the cell strength. Typically, cells modified most recently have greater strength.
About this book
This book has three parts: a tutorial introduction you can read start to finish, chapters that
go back over the same ground in more detail, and a terse reference manual for looking things up.
Every Adam source fragment shown in a fenced adam block is exactly what you’d type into a
.adm2 file; a fenced text block instead shows grammar notation (EBNF), not Adam source
itself.
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 own examples install
cel-std.
A Tutorial Introduction
Adam programs are called sheets, a term borrowed from spreadsheets. This chapter is an informal tour of Adam; later chapters go over the same ground in more detail, and the reference manual provides a full specification.
An Adam sheet declares the relationships among a set of properties called cells. These relationships are maintained when edits occur, providing correct behaviors for user interfaces and scripting without having to code complex event handling logic.
Every source fragment below is the complete description used to generate the UI, UI behavior, and graph visualization. The UI is live, so you can change the values and explore the behavior. Within an application, a sheet instance is typically bound to a UI construct or scripting system with human- readable text labels, instead of cell identifiers.
A first sheet
An Adam program is a single sheet, named, with a body of declarations between braces. A simple
sheet declares a couple of source cells:
sheet hello {
source width = 1920;
source height = 1080;
}
A cell is a named, typed storage location: the basic unit of state in a property model. width
and height are i32-typed cells, each given an initial value (the types are deduced from the
initial value). A source cell is like a spreadsheet’s value cell: it holds a value written into
it.
A sheet’s body is a sequence of declarations. A sheet describes a graph of cells and the
constraints between them. The graph for hello is just the 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 method on the out cell can reference other cells in the sheet, and the calculation is reapplied when those values change. In the graph representation, the method is a relationship and drawn as a circle between the cells. The heavy arrows and border around the out cell indicate that the value is forced by the relationship. A forced cell’s value is not editable.
See the outputs chapter for the full treatment.
Cells and Relationships
A plain cell declaration acts as a source or out cell. Cells are connected by one or more
relationships, each a bundle of methods that satisfy the relationship but solve for a different
term (or set of terms).
For example, if we have two values \(a\) and \(b\) where \(a = 2b\), that can be represented as:
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;
}
}
For any active relationship, exactly one method is selected to execute. The method chosen is based
on the strength of the cells. Cells that have been written more recently have a 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);
}
}
This example also demonstrates two additional features.
- A method can be self-referential, naming a cell as both a dependency and a result. In such a case, the method must be idempotent.
- When a cell value is derived in terms of itself via a self-referential method (or filter). The last written value is preserved.
You can see the effect of the second behavior by sliding a to 100 which will pull b and c to
100 and then slide a back to 0. b and c will return to their prior values.
In the relationships chapter, you will see relationships are not limited in their arity (you can have n-way relationships with each method solving for 1 or more cells).
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 value, in this case any source cell value is preserved and restored when the conditional is removed. The UI for a forced cell value will typically disable the control.
sheet conditional_forced {
source constrain = false;
cell a = 5 filter 1..=100;
conditional constrain {
true => {
relationship {
a := 42;
}
}
}
}
Requirements
Filters on source cells were introduced in the above [[Filters]] section. For an out cell or a cell in an out role, violating a filter will trigger a diagnostic.
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. For source cell or when a cell is writable, the requirements act are a boolean expression that act as a filter rejecting any input that doesn’t satisfy the requirements.
For an out cell, violating a requirement will trigger a diagnostic. 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
That’s the whole language. The remaining chapters, starting with sheets, cells, and types, cover each construct in the depth this chapter skipped past, and the reference manual gives you the full grammar and every built-in type in one place.
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 name | Rust type | Default |
|---|---|---|
i8, i16, i32, i64, i128, isize | the matching signed integer | 0 |
u8, u16, u32, u64, u128, usize | the matching unsigned integer | 0 |
f32, f64 | the matching float | 0.0 |
bool | bool | false |
String | String | "" |
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).
Source Cells
Grammar
source_decl = "source" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
A source declaration looks exactly like a cell
declaration: the same cell_type_init (a type, an initializer, or both), evaluated the same
way, with no cell scope; the same optional trailing filter clause and require
block. source and cell even share one namespace with out, so declaring source width and
cell width in the same sheet is the same “duplicate cell” error two cell width declarations
would be.
sheet resize {
source width: i32 = 1920;
source height: i32 = 1080;
out area := width * height;
}
Both width and height above are declared source: ordinary inputs to the out that
multiplies them, with nothing in this sheet ever claiming either as a relationship or
conditional output.
Always a source, never derived
A source cell is the mirror image of an out cell. Where an out cell is
always derived by its own fixed writer, a source cell is always left alone by the solver:
never claimable as a relationship binding’s output, a conditional branch’s output, or an
out’s own writer target. Naming a source cell on a binding’s left-hand side is an error.
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 opts out of that entirely: whatever a host last wrote to it (or its own declared
initializer, before any write) is always its value, unconditionally.
A source cell can be filtered too
A source cell is ordinary in every other respect: it can be read anywhere a plain cell
can, written directly at any time (that’s the whole point — a source cell always reflects
whatever was last supplied from outside the sheet), can carry a require block for domain
diagnostics, exactly like a cell or out:
sheet s {
source width: i32 = 1920 require {
@positive width > 0;
};
}
and can carry a filter clause, exactly like a cell or out: whatever a host
writes to a filtered source cell is conformed the same way a filtered plain cell’s value
is, live, whenever the sheet resolves:
sheet s {
source level: i32 = 50 filter 0..=100;
}
See the filters chapter for filter’s full rules — everything there applies to a source
cell unchanged.
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, ®istry, &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.
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 filter carries no name — a cell can have at most one, so nothing
ever needs to distinguish “which filter.”
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
This is the single most important rule in this chapter: writing a cell always stores exactly the value it was given, filter or no filter. A filter is applied live, when the sheet resolves, against the cell’s own current value, never synchronously at the moment of the write.
sheet s {
source level: i32 = 50 filter 0..=100;
}
Writing a filtered cell never fails because of the filter, and never reports an error on the filter’s account. Whatever you write is exactly what a read shows until the sheet next resolves: the same “a read reflects the last full resolution, not a per-write side effect” rule every other cell in a sheet already follows.
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’s live output always lands in the derived value, never in the source, so a filtered cell’s original input is never destroyed, even after many rounds of clamping. If a dynamic bound loosens back up, the cell springs back to exactly what was last written, not to some intermediate clamped value:
sheet spring_back {
source max: i32 = 100 filter 0..=200;
source level: i32 = 50 filter 0..=max;
}
This is the same rule the default branch and reverting to source section already showed for a relationship’s method: a method’s output (and a filter’s output) always lands in the derived value, so nothing a computation produces can ever permanently overwrite what was actually written.
Range filters
A filter expression whose type is CEL’s lo..=hi range (over any type this book’s
built-in numeric types supports) is recognized
structurally as a range filter: resolving the sheet clamps into [lo, hi] instead of
running the expression as an arbitrary function of _, and the sheet can report the range’s
current live bounds without needing a candidate value at all:
sheet s {
source level: i32 = 50 filter 0..=100;
}
A range filter’s body is exempt from the “must reference _” rule stated in the Grammar above: a genuine range
expression like 0..=max has no reason to mention _ at all, since both endpoints are
independent of the value being conformed. This book’s own live examples mount an editable
widget bound to the filtered cell whose displayed min/max track the range’s current live
bounds — try the example in the Filters section of the
tutorial.
Derived cells: diagnosed, never corrected
A filter attaches to one cell, but that cell isn’t always a source: a relationship may claim it instead (see the relationships chapter). When that happens, the filter no longer has any authority to change the value: it only observes. The sheet still resolves successfully, and the out-of-range value is still what a read returns; the sheet simply records that the filter is violated:
sheet diagnose_only {
cell bound: i32 = 100 filter 0..=100;
source driver: i32 = 500;
relationship {
bound := driver;
}
}
Resolving the sheet never fails because of a filter violation, on either side (source or derived): a filter is a diagnostic and a self-correction mechanism, never a gate. A host UI can query which cells currently have a violated filter; see the host embedding API that exposes this.
A filter on an output cell
A filter isn’t limited to a plain cell: an out declaration’s grammar carries
the same optional cell_filter clause (see Grammar), trailing its := initializer instead of a cell
declaration’s own initializer. Everything above applies unchanged — an out cell is always
derived (derived cells: diagnosed, never corrected is the only case that ever actually applies to one), so a filter attached to an
out cell is a pure diagnostic, exactly like a filter on any other cell a relationship currently
claims:
sheet s {
source width: i32 = 4;
out area := width filter 0..=100;
}
A source declaration’s grammar carries the same optional cell_filter clause
too — see a source cell can be filtered too. A filter attaches to
any cell kind (cell, source, or out) exactly the same way; the grammar has no
per-kind restriction.
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 new cell computed by exactly one expression: there’s no alternative
binding to choose between, unlike a relationship. Its dependencies are
deduced the same way. The : type_expr annotation is
optional; when absent, the output’s type is inferred from the initializer, the same rule
built-in types and inference gives for a plain cell. An out may
also carry a filter clause, exactly like a plain cell; see the filters chapter.
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
out shares one namespace with cell: declaring out result := ...; after (or before) a
cell result in the same sheet is a duplicate-name error, exactly like two cell declarations
would be. An output cell is an ordinary cell for reading: a later declaration in the same
sheet can reference an earlier out by name in its own expression — as a relationship input, a
conditional’s match subject, a filter argument, or another out’s own initializer — exactly
like referencing any other already-declared cell. What stays restricted is writing: an
output’s cell can never be produced by more than one method, and can never be written directly,
not by a host write, not by a relationship binding, not by a second out. Its value
comes from its own initializer and nothing else. The initializer is purely functional;
resolving the sheet may reuse its result rather than evaluate it again:
sheet s {
source width: i32 = 10;
out area := width * 2;
out doubled_area := area * 2;
}
Requirements: diagnostics, not gates
require is not an outputs-only mechanism: a require { ... } block can trail a plain cell
or a source declaration’s initializer too, with the same meaning described here
— see cell declarations and source cells for those forms. A
filter clause is just as unrestricted — see
a filter on an output cell for a filter attached to an out
declaration. Trailing an out’s initializer, require { ... } names zero or more boolean
checks. Each requirement’s own dependencies are deduced separately from the output’s
initializer; a requirement commonly reads the output’s own value by name, alongside whatever
other cells it needs:
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 never stops the sheet from resolving, and never stops area from being
computed and readable; a host can query which requirements are currently failing precisely
because nothing else in the sheet notices a requirement failing on its own (see the
host embedding API). A requirement’s @name is
optional — present only when an application wants to report which requirement failed; it
happens to read naturally when it echoes a cell name (@not_too_big, @width_max), but it
isn’t a cell reference and doesn’t have to match one.
Multiple requirements
An output can list any number of requirements; a host can query exactly the ones currently failing, identified individually (see the host embedding API):
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 } [ "," ] ")".
Each binding inside a relationship block is a candidate method: an expression whose
dependencies are deduced from whichever
already-declared cells it references, paired with the cell(s) named on its left of :=. A
relationship’s bindings are alternatives, not a sequence: at any moment, exactly one of them
is selected, and only the selected one’s output cell(s) are actually written when the sheet
resolves. Other bindings may contribute to self-referencing seed construction. Selection
does not guarantee that an expression is evaluated; the runtime may reuse its result.
A source cell can never be named on a binding’s left-hand side —
it’s always a source, by construction, never
a method’s output — and an out cell can be a binding’s input but never its
output either, since an out already has its own fixed writer.
Strength: who gets to stay a source
Every cell carries a strength, a write-recency counter: resolving the sheet re-derives the stalest cells it safely can and leaves the freshest cells alone. Two things bump a cell’s strength: an explicit write, and, once only, the cell’s own declaration. Before any explicit write has happened, declaration order alone orders every cell’s freshness, earliest declared being stalest. The tutorial’s Cells and Relationships section walks through the simplest case of this rule. A write never touches strength itself except to promote the written cell to “freshest of all”; reading a cell never changes it.
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 the same relationship must reference exactly the same set of cells — the
union of that method’s own inputs and outputs — as every other method in that
relationship; violating this fails to parse with methods in a relationship must reference the same set of cells. A relationship models one fixed group of related cells; its
methods differ only in which subset of that group they treat as the output, using an
“ignore an input” pattern — not in which cells they touch at all. The multiplication triangle from
the tutorial’s Cells and Relationships section
satisfies this: all three of c := a * b, a := c / b, and
b := c / a reference the same {a, b, c}.
A method’s own outputs list must be duplicate-free, and no two methods in the same
relationship may claim an identical outputs set; violating either fails to parse with
a method's outputs must be duplicate-free, and no two methods in a relationship may share an outputs set. The planner treats a method’s output set as one indivisible claim, so two
methods claiming the same set would make that claim ambiguous. The triangle’s three output
sets — {c}, {a}, {b} — are pairwise distinct, as required.
A cell may appear in both a method’s inputs and its own outputs — a self-referencing
method — which is explicitly allowed and has its own rules; see the
relationships-continued chapter.
A shared-cell example
Cells can be shared across more than one relationship, letting the solver’s strength
preference cross relationship boundaries. Four cells, two relationships, with b and c
shared between them:
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 writed(b := d/corc := d/b), sodcan stay a source.c: withdalready pinned as a source, the second relationship’s only way to avoid writingdisb := d/c, which doesn’t touchceither, soccan also stay a source, as long as the first relationship also avoids writing it (a := c/bdoes).b: now both relationships are already spoken for in a way that avoids writingbonly by the first relationship (a := c/b), but the second relationship’s only binding that doesn’t writecordisb := d/c, which does writeb. There is no way left to leaveba source, so the attempt fails andbstays claimed by the second relationship.a: stalest, and the first relationship’s remaining choice isa := c/b;ais derived.
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;
}
}
Whether a cell came out of the last resolution as a source, left alone rather than derived, is useful for a host UI deciding whether a field should be editable.
When no assignment exists
Every relationship in a sheet must end up with exactly one selected binding once the sheet
resolves; if that’s not possible, resolution fails instead of silently picking something
inconsistent. Two relationships that both, unconditionally, insist on writing the same cell
can never both be satisfied, and resolving fails with no valid method assignment (overconstrained) (Error::Conflict).
A subtler failure is a cycle: an assignment exists, but every valid choice of bindings
forms a closed loop with no cell left as a source anywhere in the loop, and resolving 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.
Destructuring a binding’s output across more than one cell, and a binding that references its own output cell, are covered next, in the relationships-continued chapter.
Relationships Continued: Destructuring and Self-Referencing Methods
Destructuring bindings
A binding’s left-hand side can name more than one output cell by parenthesizing it, in which case the right-hand side must be a tuple expression of matching arity, split element-wise:
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,) := ... (trailing comma mandatory, matching Rust’s
own 1-tuple pattern) both destructure; a bare a := ... or the equivalent single parenthesized
(a) := ... (mere grouping, no comma) instead binds the right-hand side’s whole result
(including a tuple-typed one) directly to the one named cell. Destructuring and direct-bind are
otherwise governed by the same type-matching rules as any other binding: each output’s declared
type must structurally match what the expression actually produces.
Self-referencing methods
A method’s expression may reference the very cell it writes — a self-referencing method — which the rules a relationship’s methods must satisfy section already noted is explicitly allowed: a cell may appear in both a method’s inputs and its own outputs. Each time the sheet resolves, a self-referencing method reads its own cell’s source value — never a previous round’s derived value — the same source/derived split the raw value is never lost section already introduced for filters:
sheet clamp_via_relationship {
cell level: i32 = 0;
relationship {
level := min(level, 0);
}
}
Writing level above never applies the clamp itself — exactly like a filter, the correction
happens live, the next time the sheet resolves, against level’s own raw value, and that raw
value survives underneath the clamp forever.
Self-referencing methods must be idempotent
A self-referencing method exists to correct its own cell into whatever set of values the
relationship enforces. That only makes sense if reapplying the method to its own already-corrected
output leaves it unchanged — f(f(x)) == f(x). min(level, 0) above satisfies this: once
level is at most 0, computing min of that value and 0 again produces the same value.
The solver never checks this — nothing about add_relationship inspects a method’s function for
idempotence, and nothing about propagate would refuse to resolve a sheet whose self-referencing
method isn’t idempotent. It is purely a correctness obligation on the sheet author. A
self-referencing binding built from a genuinely non-idempotent operation — a literal swap between
two cells’ current values, for instance, rather than a one-sided correction like min/max/
clamp — has no single well-defined corrected value for the solver to settle on.
Conditionals
Grammar
conditional_decl = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (expression | "_") "=>" "{" { relationship_decl } "}" [ "," ].
A conditional evaluates its match subject (the expression right after the
conditional keyword) and activates the one branch, if any, whose literal equals the current
match value. Only that branch’s relationship blocks participate in the round’s solve; every
other branch’s relationships are invisible to the planner, exactly as if they weren’t declared.
A branch’s own body holds nothing but relationship declarations: a conditional cannot
declare cells, and cannot nest another conditional directly inside a branch.
The match subject
A bare cell name (conditional mode { ... }) is the common case: the match value is just that
cell’s current value. The match subject can also be a general expression over several
already-declared cells, deduced exactly like a
relationship binding’s body:
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, in which case each branch literal is a tuple
expression of the same shape.
Forced cells
A relationship with exactly one method has no alternative binding to choose: its output cell
is claimed every time the sheet resolves, regardless of strength. Such a cell is forced
— Sheet::is_forced reports this, and it’s false for a cell whose relationship has two or
more methods, even if strength happens to pick the same direction every round.
A common convention in a host UI is to disable the editable widget for a forced cell:
writing it would have no lasting effect once the sheet next resolves, so there’s nothing
useful for the user to type into. This is a UI convention, not a language rule — adam-lang
and adam-rs never disable anything themselves; a host is always free to accept the write
anyway (see shadow state: forced and self-referencing cells below for what happens if it does).
Shadow state: forced and self-referencing cells
The raw value is never lost section’s filters and the self-referencing methods section’s self-referencing methods both keep a cell’s own raw source value forever, underneath whatever derived value a live correction currently computes. A forced cell works the same way: forcing it shadows its own source, never overwrites it.
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 — two methods,
low := min(low, high) and high := max(low, high), exactly like the triangle from the relationships chapter: only one is ever selected, and which one is decided by strength. 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 own source is still 4 — the write
that actually changed something (high) never touched low‘s source at all. Switching back
to mode == 0 reselects low := min(low, high) (still the stalest cell) and recomputes it
fresh from both cells’ own sources — 4 and 42 — not from the stale 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. Either binding can fire; which one
does is strength’s call, exactly as in the relationships chapter,
never both at once.
The default branch and reverting to source
_ => { ... } matches whatever value none of the named branches list and (because it’s
defined that way) must be the last branch textually; a named branch written after _ is a
syntax error. Leaving off _ entirely is legal: if the match value doesn’t equal any branch’s
literal, none of the conditional’s relationships are active that round, and every cell that
would otherwise be one of their outputs stays free, reverting to its own last
externally-written value (or its declared default), not to whatever the branch last computed
for it. This is the same shadow-state mechanism the previous section just showed for a forced cell,
triggered here by no branch matching at all rather than by switching branches: a relationship
that stops being active can never have written a cell’s source, so the cell has nothing to
revert to except that untouched source.
sheet no_default {
source mode: i32 = 0;
cell x: f64 = 1.0;
conditional mode {
0i32 => {
relationship {
x := 100.0;
}
}
}
}
Nested and chained conditionals
Two conditionals in the same sheet compose freely at the sheet level: one conditional’s
output cells can be another’s match subject or a relationship input, exactly like any other
cell, even though a branch body itself can only hold relationships, not another
conditional. A resize dialog, for example, might have two sibling conditionals this way: 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.
It does not claim complete inference.
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
This is the actual library example, included rather than copied. Run
cargo run -p cel-euclid --features adam --example adam_geometry to execute it.
cargo test -p adam-lang-book --test geometry verifies these same included
sheets through the book’s parser. These cross-library demonstrations are
executable Rust-driven examples, not live inspector mounts or a crop UI.
#![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. There is no extra requested-crop cell or helper-owned memory.
Every typed, noncapturing IIFE forwards its needed values explicitly.
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.
The expression contains these policies openly, without a crop-specific operation.
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 or silently narrow support to rotations or uniform scales.
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
An Adam source file is tokenized as Rust/CEL tokens (via proc_macro2): identifiers, integer
and float literals (with optional type suffixes), string literals, and punctuation, exactly as
cel-parser’s own lexical grammar defines them. Adam adds exactly
one lexical extension on top of CEL’s own conventions: doc comments.
Comments
// starts a line comment; /* ... */ a block comment — the same two forms C, Rust, and CEL
all share:
// 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 the sheet keyword itself, are Adam’s own addition to
CEL’s lexical grammar: doc comments, recovered by the language server and the formatter, and
otherwise inert — they carry no meaning when the sheet resolves:
//! 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
Keywords: sheet, cell, source, relationship, conditional, out, require,
filter. None of these can be used as a cell or sheet name. _ is not a keyword but is
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 ships its own formatter (adam fmt, backed by
format_sheet) with one canonical layout: 4-space
indentation, opening braces on the same line as the keyword that introduces them, and no space
before a declaration’s closing ;. Given input that doesn’t already follow this layout,
formatting normalizes it:
sheet s {
cell x: i32 = 1;
cell y: i32 = 2;
}
A formatter run is expected to be idempotent (formatting already-canonical source
reproduces it unchanged) and preserves every comment, doc comment, and blank line exactly
where it appeared, including a file-header comment before sheet itself and a trailing comment
before a block’s own closing }. adam-lsp’s textDocument/formatting handler refuses to
format a sheet with any recorded syntax error rather than guess at intent:
format_sheet’s own precondition is that sheet.errors
is empty.
A conditional branch’s trailing , (the grammar allows one after each branch’s closing })
is always omitted by the formatter, even though the parser still accepts it on input, so
canonical Adam source never has one.
Reference Manual
This appendix is for looking things up, not reading start to finish: it restates the rules from the preceding chapters in one terse pass, plus the grammar and error messages in full. Where a rule needs justification or an example, the appropriate chapter is linked instead of repeating it here.
Grammar
sheet = "sheet" identifier "{" { sheet_item } "}".
sheet_item = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
| source_decl).
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 } "}".
binding = binding_target ":=" expression ";".
binding_target = identifier | "(" identifier { "," identifier } [ "," ] ")".
conditional_decl = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (expression | "_") "=>" "{" { relationship_decl } "}" [ "," ].
out_decl = "out" identifier [ ":" type_expr ] ":=" expression
[ cell_filter ] [ require_block ] ";".
require_block = "require" "{" { requirement } "}".
requirement = [ "@" identifier ] expression ";".
expression and everything it expands to (literal, identifier, operators, if/else,
as casts, ranges, closures, function calls) is CEL grammar, defined by cel-parser; see
expressions are CEL.
cell_filter and require_block both attach identically to all three of cell_decl,
source_decl, and out_decl — neither has a cell-kind restriction (see
a source cell can be filtered too).
A cell_decl’s grammar also has a design-level provision for an optional trailing
":=" expression clause (making a cell double as a relationship-bound output in one
declaration), not implemented as of this writing; only the "=" expression one-time
initializer and the cell_filter/require_block clauses shown above exist today.
Sheets and namespaces
- A sheet’s own name has no runtime meaning; it is not otherwise referenceable.
cell,source, andoutdeclarations 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. Membersexpr.nameandexpr.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. - No forward references. An identifier is only recognized as a cell dependency if that cell was declared earlier in the same sheet’s token order. Referencing an undeclared name is an “undeclared cell” error. See names and declaration order.
Cells and source cells
cell name: T;: requiresTto have a registered default; the cell starts at that default.cell name = expr;:expris evaluated with no cell scope: it may not reference any other cell. The cell’s type is inferred fromexpr’s result type.cell name: T = expr;: both forms combined;expr’s inferred type must equalTexactly, 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, andsource_compoundwith 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_compoundreads 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. The privateCellValueretains one canonical owner, not per-element erasure. - 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)andFn((), 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;/source name: T = expr;: identical rules to the threecellforms above, evaluated the same way — asourcedeclaration is acelldeclaration in every respect except its fixedCellKind(below).- Every cell has a fixed kind, assigned once at declaration and never reassigned: a plain
cellmay be a planner source or claimed as a method’s output, chosen per round; asourcecell is always a source, never claimable as any method’s output; anoutcell is always derived by its own fixed writer, neverwrite()-able. See source cells forsourceand outputs and requirements forout.
See sheets, cells, and types and the type registry for the built-in type table.
The type registry
| Type name | Default |
|---|---|
i8 i16 i32 i64 i128 isize | 0 |
u8 u16 u32 u64 u128 usize | 0 |
f32 f64 | 0.0 |
bool | false |
String | "" |
A host application can register additional Rust types under new Adam type names via
TypeRegistry::register (with a
Default) or
TypeRegistry::register_no_default
(without one, such a type can only be used with an explicit initializer, never a bare : T
declaration). This is a Rust-level embedding decision made before parsing a sheet, not
something sheet source itself can do.
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, ®istry, &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 {}; non-empty fields permit a trailing
comma (call arguments do not). 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
relationshipnames one or morebindings; 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 ∪ outputscell set as every other method in that relationship, or resolution fails to parse 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
outputsmust be duplicate-free, and no two methods in a relationship may share an identicaloutputsset, or resolution fails to parse 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 cell may appear in both a method’s inputs and its own outputs — a self-referencing method — which is explicitly allowed. 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 a trailing comma; a bare name or a single parenthesized name with no comma binds the whole result directly. See destructuring bindings.
- A
sourcecell can never be a binding’s output: arelationship(orconditionalbranch) 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
relationshipdeclarations: nocelldeclarations, no nestedconditional. - A relationship with exactly one method is forced: its output cell is claimed every
round, regardless of strength.
Sheet::is_forcedreports this. See forced cells.
See conditionals.
Filters
cell_filter = "filter" expression, trailing acell_decl,source_decl, orout_decl— a filter attaches to any cell kind, with no per-kind restriction (see a source cell can be filtered too). A filter carries no name;_inside the expression denotes the candidate value (of the cell’s own declared type); every other identifier is a deduced dependency. The expression must reference_at least once (unless it’s a range expression,lo..=hi, which is exempt) and must produce the filtered cell’s own type.- Writing a cell never applies a filter. A filter is applied live, each time the sheet resolves, against the cell’s current value.
- A filtered cell keeps a raw source value (last written, untouched by any filter forever) and a computed derived value (the filter’s live output, recomputed every time the sheet resolves); reading the cell returns the derived value when present, the source value otherwise.
- 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
outcell, since anoutcell 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 new cell, always derived byexprand never writable directly — not by a host write, not arelationship, not anotherout— but otherwise an ordinary, freely-referenceable cell: any later declaration may read it by name exactly like any other already-declared cell. See outputs and requirements.require { [ "@" name ] expr; ... }attaches boolean checks, each with an optional@namelabel. Unlikefilter,requireis not tied toout: arequireblock may trail acell,source, oroutdeclaration’s initializer, with the same meaning in every case. Eachrequirement’s own dependencies are deduced separately from its declaration’s own expression. A failing requirement never stops the sheet from resolving, or its cell’s own value from being computed: it’s reported as a diagnostic, nothing more, queryable by a host (see the host 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
Adam reports every diagnostic as a ParseError
carrying a source span; there is no separate runtime error type for a malformed sheet; if
parse_str returns Ok, the sheet is syntactically and structurally valid (though it may
still fail when resolved for the solver reasons in relationships and the solver).
Selected messages, verbatim:
| 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 value | a 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) declared | a 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 cells | a 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 set | two methods in one relationship claim the same outputs set, or one method repeats a cell in its own outputs |
The host embedding API
This book documents the language; the Rust API a host application uses to parse and drive a sheet is documented by the crates themselves:
AdamParser: parses source into a liveParsedSheet, which derefs toSheet.TypeRegistry: the type-name-to-Rust-type table a parser is built with;TypeRegistry::newpre-populates the built-ins in the type registry.OpLookup: the function-library table a parser is built with; this book’s own examples installcel-stdviasupport::parser(seeadam-lang-book’s own crate source).Sheet:read,write,propagate,is_source,is_forced,cell_kind,filter_*,cell_requirements,cell_requirements_valid,violated_requirements, and every other runtime operation this book has used throughout.AdamAstParser/format_sheet/check_sheet: the span-carrying CST, formatter, and static type checker behind the language server andadam fmt(see program style), distinct fromAdamParser’s eager compile-to-Sheetpath.