The Adam Programming Language
This book is 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
Application properties constrain one another. For example,
pixels == inches * resolution lets any two values determine the third.
Such relationships preserve document invariants, constrain command arguments and results,
or construct new values.
Applications often implement 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.
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. Cell strength determines which cell is the source
and which is derived. Typically, cells modified most recently have greater strength.
About this book
The tutorial introduces Adam through examples; the following chapters explain each construct,
and the reference manual collects the grammar and rules.
Fenced adam blocks contain .adm2 source; fenced text blocks contain grammar notation (EBNF).
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 examples install
cel-std.
A Tutorial Introduction
Adam programs are called sheets, a term borrowed from spreadsheets. This tutorial introduces the language through live examples.
An Adam sheet declares the relationships among a set of properties called cells. These relationships determine how edits propagate through a user interface or scripting system.
Each source fragment defines the cells and relationships shown in its live inspector and graph. Edit values to explore how they propagate. Applications typically bind a sheet to controls or scripts with human-readable labels rather than cell identifiers.
A first sheet
The first example defines one named sheet, with declarations between braces.
Modules may contain multiple definitions and imports. This sheet declares two source cells:
sheet hello {
source width = 1920;
source height = 1080;
}
A cell is a named, typed storage location. Adam infers width and height as i32
from their initial values. A source cell holds a value supplied from outside the sheet,
like a spreadsheet’s value cell.
A sheet describes a graph of cells and constraints. The graph for hello has 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 output’s method can read other cells; resolution updates its result when those inputs change. The graph draws the relationship as a circle between cells. Heavy arrows and the output cell’s border mark a forced value. The live inspector disables editing for forced cells.
See the outputs chapter for the full treatment.
Cells and Relationships
A plain cell may be a source or a derived output.
Relationships connect cells through alternative methods, each solving for a different
cell or set of cells.
This relationship maintains \(a = 2b\):
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;
}
}
Resolution selects exactly one method per active relationship, based on cell strength. Cells written more recently have 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);
}
}
The methods here are self-referencing: each reads a cell it also writes. Such a method must be idempotent, leaving its corrected output unchanged when reapplied. Self-referencing methods and filters preserve the cell’s last-written source value.
Slide a to 100 to pull b and c to 100, then slide a back to 0.
b and c return to their prior values.
Relationships may connect any number of cells, with each method solving for one or more; see the relationships chapter.
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 while preserving its source value. Releasing the relationship restores that source. Host UIs commonly disable controls for forced cells.
sheet conditional_forced {
source constrain = false;
cell a = 5 filter 1..=100;
conditional constrain {
true => {
relationship {
a := 42;
}
}
}
}
Requirements
A filter on a derived cell reports a violation without changing the value:
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. Each requirement is a Boolean diagnostic, not an input gate: a source or writable cell retains a value that violates a requirement, and propagation reports the requirement’s failed validity.
The same rule applies to an out cell. 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
Continue with sheets, cells, and types for declaration and type rules, or use the reference manual to look up grammar, built-in types, and errors.
Sheets, Cells, and Types
Sheets
The examples begin with a single root sheet. An Adam module may also contain imports and multiple root or parameterized sheet definitions:
sheet name {
/* cell, relationship, conditional, and out declarations, in any order,
except that each identifier must be declared before it is referenced —
see "Names and declaration order" below. */
}
The sheet’s name (name above) identifies the definition for module resolution and
root selection. Host tooling also uses it for display.
Cell declarations
cell_decl = "cell" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
cell_type_init = (":" type_expr ["=" expression]) | ("=" expression).
A cell needs a type, an initial value, or both:
cell width: i32; // type only — needs a registered default
cell height: i32 = 1080; // type and initializer
cell area = 0; // initializer only — type is inferred
cell width; is a syntax error (“expected : or = in cell declaration”).
A cell may also carry a trailing filter clause for a domain constraint,
a require block for boolean diagnostics,
or both. These clauses also apply to source and out cells.
A cell’s initializer may reference literals and CEL operators, but not other cells: it’s
evaluated before the sheet has any state to read.
To compute one cell from others, use a relationship or an
out declaration instead.
Built-in types and inference
Adam ships with the following types pre-registered, each with a Default value used when
a cell declares a type but no initializer:
| Type 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 | "" |
Without a : type_expr annotation, the initializer’s result determines the cell’s type.
CEL defaults an unsuffixed integer literal such as 0 to i32 and an unsuffixed float
such as 0.0 to f64; see the CEL literal grammar.
When both a type annotation and an initializer are present, their types must agree exactly,
or parsing fails with “type mismatch: expected T, got U”
(see error messages).
A host application can register additional Rust types under Adam type names through the type registry.
Cells with no default
A type registered without a Default (via the embedding API’s register_no_default) can
still be used for a cell, but only with an initializer; declaring one with a bare : T and no
= ... fails to parse (“type T has no default; provide = ...”). Every built-in type in the
table above has a default, so this only matters for a host-registered custom type.
Tuple types
type_expr = identifier [ "(" type_expr { "," type_expr } ")" ]
| "[" type_expr "]"
| "(" [ type_expr ["," [ type_expr { "," type_expr } ]] ] ")".
A parenthesized type list is a tuple type. () is the empty tuple;
(T) groups the type T; (T,) is a one-element tuple with a required comma;
(T, U, ...) is a tuple with multiple elements:
sheet s {
cell point: (f64, f64) = (0.0, 0.0);
}
Tuples nest: (i32, (f64, String)) is a 2-tuple whose second element is itself a 2-tuple.
Compound cells own canonical CEL values. Host code uses ParsedSheet::read_compound and
write_compound with complete representation witnesses. For example,
Cons<Leaf<i32>, Cons<Leaf<f64>, Nil>> witnesses (i32, f64). Reads return independent
owned clones; writes validate the complete recursive shape before changing the cell.
source_compound reads the original aspiration independently of any filter result.
Concrete registered leaves retain the ordinary Rust read::<T> / write APIs.
Arrays, generic leaves, and callables
Arrays and tuples compose recursively: [(i32, [f64])] is an array of pairs whose second
field is another array. An array defaults to empty without requiring an element default;
a tuple defaults each child recursively. A missing leaf default names that leaf.
Register a generic foreign leaf under its full source name, such as RangeInclusive(i32);
containing array and tuple shapes need no additional registration.
Fn((A, B), R) describes a callable with two parameters; (A,) and () describe one
and zero parameters. A callable has no default and requires an initializer. Every Adam
cell also requires cloning and value equality: CEL can store callable values, but Adam
rejects a callable cell when its descriptor lacks equality.
Registered host function markers retain the concrete
capabilities their type registration supplies.
Executable host boundary
A host supplies ArrayOf, Cons, Nil, and Leaf witnesses
to the checked ParsedSheet boundary. A wrong nested shape fails before mutation.
Cloning and equality are required for persistence/propagation; debug formatting
is an independent capability. Containers themselves do not impose those bounds.
An empty array of callable elements also lacks equality when its element descriptor does.
This example defaults recursive compounds, writes an array of concrete tuple lists, propagates it to an output, and reads the source aspiration:
#![allow(unused)]
fn main() {
/// Reads, writes, and propagates canonical arrays while retaining source aspirations.
#[test]
fn compound_cell_interoperability() {
use adam_lang::{AdamParser, TypeRegistry};
use cel_parser::OpLookup;
use cel_runtime::{Array, ArrayOf, Cons, IntoCStackList, IntoRustTuple, Leaf, Nil};
/// Represents an integer key paired with an array of measurements.
type Row = Cons<Leaf<i32>, Cons<ArrayOf<Leaf<f64>>, Nil>>;
/// Represents a canonical array of keyed measurement rows.
type Rows = ArrayOf<Row>;
let mut sheet = AdamParser::new(TypeRegistry::new(), OpLookup::new())
.parse_str(
"sheet data {
source rows: [(i32, [f64])];
cell defaults: (i32, [f64]);
cell unit: ();
out copy := rows;
}",
)
.unwrap();
let rows = sheet.cell_names["rows"].0;
let defaults = sheet
.read_compound::<Row>(sheet.cell_names["defaults"].0)
.unwrap()
.into_rust_tuple();
assert_eq!(defaults.0, 0);
assert!(defaults.1.is_empty());
assert!(sheet.read_compound::<Rows>(rows).unwrap().is_empty());
sheet
.read_compound::<Nil>(sheet.cell_names["unit"].0)
.unwrap();
sheet
.write_compound::<Rows>(
rows,
Array::from_vec(vec![(7, Array::from_vec(vec![1.5])).into_c_stack_list()]),
)
.unwrap();
sheet.propagate().unwrap();
let copied = sheet
.read_compound::<Rows>(sheet.output_names["copy"])
.unwrap()
.into_vec()
.pop()
.unwrap()
.into_rust_tuple();
assert_eq!(copied.0, 7);
assert_eq!(copied.1.as_slice(), [1.5]);
assert_eq!(
sheet.source_compound::<Rows>(rows).unwrap().as_slice()[0].1,
7
);
assert!(
sheet
.write_compound::<ArrayOf<Leaf<u32>>>(rows, Array::from_vec(vec![99]))
.is_err()
);
assert_eq!(sheet.read_compound::<Rows>(rows).unwrap().len(), 1);
assert_eq!(
sheet.source_compound::<Rows>(rows).unwrap().as_slice()[0].1,
7
);
}
}
This example checks the missing-equality error:
#![allow(unused)]
fn main() {
/// Rejects persistent callable cells without inventing function equality.
#[test]
fn callable_cells_require_equality() {
let error =
adam_lang::AdamParser::new(adam_lang::TypeRegistry::new(), cel_parser::OpLookup::new())
.parse_str("sheet data { cell functions = []: [Fn((i32,), [i32])]; }")
.err()
.unwrap();
assert!(
error
.to_string()
.contains("equality capability unavailable")
);
}
}
The Inspector formats arrays and tuples recursively when debug capabilities exist, including typed empty arrays and unit. Compound edits are explicitly unsupported and leave the cell unchanged; primitive editors retain their ordinary behavior. Formatting does not imply an editable source round trip.
Names and declaration order
A cell’s name must be unique across the sheet. cell, source,
and out declarations share one namespace; declaring both
cell result: i32 = 0; and out result := ...; is a duplicate-name error.
Referencing a name before its declaration, whether as a binding output or an expression
dependency, is an
“undeclared cell name” error (see error messages).
Source Cells
Grammar
source_decl = "source" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
A source declaration uses the same type and initializer rules as a
cell, including evaluation without cell scope.
It accepts an optional filter clause and require block.
source, cell, and out share one namespace; declaring both source width
and cell width is a “duplicate cell” error.
sheet resize {
source width: i32 = 1920;
source height: i32 = 1080;
out area := width * height;
}
width and height supply inputs to the output that multiplies them.
Always a source, never derived
A source cell cannot be claimed as a method’s output, including in a conditional branch.
Naming it on a binding’s left-hand side is an error. An out cell,
by contrast, always has its own fixed writer.
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 retains its last external write, or its initializer before any write.
A filter may conform its effective value without changing that source value.
A source cell can be filtered too
A source cell can be read wherever a plain cell can and written directly by the host.
A require block reports domain diagnostics:
sheet s {
source width: i32 = 1920 require {
@positive width > 0;
};
}
A filter clause conforms its effective value during resolution,
not during a write:
sheet s {
source level: i32 = 50 filter 0..=100;
}
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, ®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 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.
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 cell can have at most one filter; filters have no names.
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
Writing a cell stores exactly the supplied value without applying its filter. The filter applies when the sheet resolves.
sheet s {
source level: i32 = 50 filter 0..=100;
}
The filter cannot cause a write to fail or report an error. A read returns the written value until the sheet next resolves.
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 writes only the derived value, preserving the original input across resolutions. If a dynamic bound loosens, the cell returns toward its last-written value rather than an intermediate clamped value:
sheet spring_back {
source max: i32 = 100 filter 0..=200;
source level: i32 = 50 filter 0..=max;
}
Self-referencing and forced methods also preserve source values; see reverting to source.
Range filters
A CEL inclusive range over a built-in numeric type
defines a range filter. Resolution clamps the candidate into [lo, hi].
The sheet can report the current bounds independently of a candidate value:
sheet s {
source level: i32 = 50 filter 0..=100;
}
A range filter need not reference _; its endpoints are independent of the candidate.
The tutorial’s live example displays bounds that track the range.
Derived cells: diagnosed, never corrected
When a relationship claims a filtered cell, the filter diagnoses violations without changing the derived value. A violation does not prevent resolution; reads still return the relationship’s result:
sheet diagnose_only {
cell bound: i32 = 100 filter 0..=100;
source driver: i32 = 500;
relationship {
bound := driver;
}
}
A filter violation on a source or derived cell never causes resolution to fail. Hosts can query violations through the embedding API.
A filter on an output cell
An out declaration accepts a filter after its := initializer.
An output cell is always derived, so its filter only diagnoses violations:
sheet s {
source width: i32 = 4;
out area := width filter 0..=100;
}
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 cell computed by one expression, with
deduced dependencies.
The optional : type_expr annotation must match the expression’s type; without it,
Adam infers the type. An output may also carry a filter clause.
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
An output shares the cell namespace and must have a unique name.
Later declarations may read it as a relationship input, conditional subject, filter
argument, or another output’s input. Only its own expression may write it:
host writes, relationship bindings, and additional out declarations cannot.
The expression is purely functional; resolution may reuse its result:
sheet s {
source width: i32 = 10;
out area := width * 2;
out doubled_area := area * 2;
}
Requirements: diagnostics, not gates
A require { ... } block attaches zero or more boolean checks to a cell,
source, or out declaration, after any filter.
Each requirement deduces its dependencies separately from the initializer.
It may read the cell it checks and any other declared cells:
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 does not stop resolution or prevent a cell’s value from being
computed and read. Hosts can query failures through the
embedding API.
The optional @name identifies a requirement for reporting; it is not a cell reference.
Multiple requirements
A declaration can list any number of requirements, which hosts can query individually:
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 { "," identifier } ] ")".
Each binding defines a candidate method: an expression with
deduced inputs and the outputs named to the left
of :=. Resolution selects exactly one method per active relationship.
Only that method writes its outputs; other methods may contribute to self-referencing
seed construction. Selection does not guarantee evaluation; the runtime may reuse a result.
Neither a source nor an out cell may be a binding’s output.
Both may be inputs.
Strength: who gets to stay a source
Every cell carries a strength, a write-recency counter. The solver prefers to derive the stalest cells and leave the freshest as sources. A declaration gives a cell its initial strength, so later declarations start stronger. An explicit write promotes its cell to freshest; reading never changes strength. See the tutorial example.
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 a relationship must reference the same inputs ∪ outputs cell set.
A violation fails at parse time with methods in a relationship must reference the same set of cells. Methods differ in which cells they derive, not which group
they constrain. The tutorial’s multiplication triangle satisfies this rule:
c := a * b, a := c / b, and b := c / a all reference {a, b, c}.
A method’s outputs list must be duplicate-free, and no two methods in a relationship
may claim the same output set. Either violation fails at parse time with
a method's outputs must be duplicate-free, and no two methods in a relationship may share an outputs set. The triangle’s output sets, {c}, {a}, and {b}, are distinct.
A self-referencing method includes a cell in both its inputs and outputs; see the relationships-continued chapter.
A shared-cell example
Cells may participate in multiple relationships. Here, two relationships share b and c:
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: keeping bothdandcas sources selectsb := d/cin the second relationship. The first can selecta := c/b, so both sources can be preserved.b: preservingcanddrequires the second relationship to writeb. The solver cannot also leavebas a source.a: the first relationship’s remaining choice isa := c/b, derivinga.
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;
}
}
Hosts can query which cells remained sources to choose which fields to make editable.
Reuse a component
A parameterized sheet names a reusable component. Its concrete formal parameters
alias caller-owned cells. A sheet without parentheses defines a root; sheet empty() {}
defines a zero-parameter component, not a root.
This inline definition uses the same three floating multiplication methods as the
triangle above. Binding factor to a fixed source removes the method that would
write it, leaving the two directions between value and result.
sheet multiply(a: f64, b: f64, product: f64) {
relationship {
product := a * b;
a := product / b;
b := product / a;
}
}
sheet inline {
source factor: f64 = 3.0;
cell value: f64 = 2.0;
cell result: f64 = 0.0;
relationship multiply(product: result, a: value, b: factor);
}
Each selected division needs a nonzero divisor; callers requiring finite arithmetic also require finite operands and representable results. Components do not infer inverse equations or choose directions from current numeric values. Floating operations retain IEEE 754 behavior, including infinities and NaNs; they do not report the checked errors that integer division reports.
The shipped adam-lang/examples/relationships.adm2 also defines finite ordered
bounds. composed.adm2 imports both definitions, aliases multiply as mul, and
uses the same three-way floating component twice with distinct named actuals.
Those files contain several roots and a component-only module, so hosts register
both modules and select a root explicitly:
#![allow(unused)]
fn main() {
/// Compiles the shipped modules, selects a root, and retains execution after owner drop.
#[test]
fn imported_components() {
use adam_lang::{ModuleCompiler, ModuleSet, TypeRegistry};
let mut modules = ModuleSet::new();
modules
.insert_source(
"relationships".parse().unwrap(),
"relationships.adm2",
include_str!("../../adam-lang/examples/relationships.adm2"),
)
.unwrap();
modules
.insert_source(
"composed".parse().unwrap(),
"composed.adm2",
include_str!("../../adam-lang/examples/composed.adm2"),
)
.unwrap();
// These scalar examples need only builtin CEL operations.
let compiled = ModuleCompiler::new(TypeRegistry::new(), cel_parser::OpLookup::new())
.compile(modules)
.unwrap();
let path = "composed".parse().unwrap();
assert_eq!(
compiled.roots(&path).collect::<Vec<_>>(),
["ordered_demo", "source_specialized", "repeated_products"]
);
assert!(
compiled
.roots(&"relationships".parse().unwrap())
.next()
.is_none()
);
assert!(compiled.instantiate_root(&path, "multiply").is_err());
let mut parsed = compiled
.instantiate_root(&path, "source_specialized")
.unwrap();
drop(compiled);
let product = parsed.cell_names["product"].0;
let value = parsed.cell_names["value"].0;
parsed.sheet.write(value, 7.0f64).unwrap();
parsed.propagate().unwrap();
assert_eq!(*parsed.read::<f64>(product).unwrap(), 21.0);
}
}
The imported examples need only built-in arithmetic and comparisons.
Hosts using the image examples register Euclid types and
install cel-std and cel-euclid on the lookup before creating the immutable
compiler or parser. An empty lookup does not supply those optional functions.
For source-free Rust construction, run cargo run -p adam-lang --example templates.
Its native path builds TemplateBuilder/MethodRecipe factories and instantiates
them into an ordinary adam_rs::Sheet; it never parses source. The checked integer
example uses Rust’s truncating division, so exact inverse multiplication requires
a divisible product. The parsed examples use f64.
cel-euclid/examples/image_constraints.adm2 extracts the active resampling axis,
an aspect relationship and the crop fit from the existing examples.
resize_straighten.adm2 binds two axis instances and opts into
aspect(width: width_inches, height: height_inches, ratio: width_per_height).
The caller supplies the positive finite width_per_height source, initially
1600.0 / 1200.0. Equating the original width/height percentages gives
width = height * ratio; the reverse direction divides width by that nonzero
ratio. Changing only width to 4 inches derives height as 3 inches; changing only
height back to 4 inches restores width to 1600.0 / 300.0 inches.
Dimensions and selected products/quotients must stay positive, finite and
representable. This relation constrains physical dimensions; the pixel axes still
round independently, so integer pixel counts need not have an exact ratio.
Ordinary derived dimensions update their raw source slots;
edited source dimensions, filtered pixel requests and the self-referencing crop
retain their caller aspirations under the ordinary sheet rules.
The root feeds an ordinary
relationship-produced image rectangle into the crop component. The caller owns
sources, filters, requirements and the crop aspiration. Private anchor/coverage
outputs remain diagnostics, never actual arguments.
This caller chooses filters that conform pixel requests to the original
300000-pixel limit while preserving their source aspirations; the original
begin example instead reports that limit through requirements alone. The
reusable axis installs neither policy, so other callers retain their own choice.
The axis retains ties-even pixel rounding and checked casts. The caller selects
the reusable aspect relation separately; neither component implements the original
example’s resample toggle or other UI machinery. The fit retains its
8-epsilon inward guard, 32-epsilon cancellation check and 64-epsilon overlay and
coverage budgets. The caller rotates about the image point under the requested
crop center. Singular transforms fail checked inversion. Insufficient precision
invalidates an explicit requirement, rather than producing a promised valid fit.
Read requirement validity after propagation; a successful propagation alone does
not certify coverage. Resize, rotation and reversal preserve source(crop).
See the component reference for binding, conditional eligibility, ownership and error rules.
When no assignment exists
Resolution requires exactly one selected binding per active relationship.
Two unconditional relationships that can only write the same cell cannot both be
satisfied; resolution fails with no valid method assignment (overconstrained) (Error::Conflict).
A cycle occurs when assignments exist, but every choice of bindings
forms a closed dependency loop without a source. Resolution 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.
Relationships Continued: Destructuring and Self-Referencing Methods
Destructuring bindings
A parenthesized binding target destructures a tuple result into output cells. The result must have matching arity and element types:
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,) := ... both destructure. Parenthesized
binding targets require the first comma, so (a) := ... is invalid. A bare
a := ... binds the right-hand side’s whole result (including a tuple-typed one)
directly to the named cell. Each output’s declared type must structurally match its result.
Self-referencing methods
A self-referencing method reads a cell it also writes. During resolution, it reads that cell’s source value, not its previous derived value. The source remains separate from the computed correction, as it does for filters:
sheet clamp_via_relationship {
cell level: i32 = 0;
relationship {
level := min(level, 0);
}
}
Writing level stores its raw value. The next resolution applies the correction
without overwriting that source.
Self-referencing methods must be idempotent
A self-referencing method must be idempotent: reapplying it to its corrected output
leaves the output unchanged, f(f(x)) == f(x). min(level, 0) satisfies this:
once level is at most 0, applying min again returns the same value.
The sheet author must ensure idempotence; neither construction nor propagation checks it. A non-idempotent operation, such as swapping two cells’ values, does not define a stable correction.
Conditionals
Grammar
conditional_decl = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (literal_pattern | "_") "=>" "{" { relationship_decl | relationship_use } "}".
literal_pattern = [ "-" ] literal.
A conditional evaluates its match subject, the expression after the keyword,
and activates the branch, if any, whose literal equals the result.
Only that branch’s relationships participate in resolution.
Branch bodies accept relationship declarations and component uses, but no cell
declarations or nested conditionals.
Branches have no comma separator or terminator.
The match subject
The match subject may be a cell name (conditional mode { ... }) or an expression
with deduced dependencies:
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, but tuple expressions are not literal
patterns, so such a conditional can use only the _ default branch.
Forced cells
A relationship with exactly one method claims its output whenever active,
regardless of strength. The output is forced, as reported by Sheet::is_forced.
This query returns false for a relationship with multiple methods, even when strength
selects the same direction every round.
Host UIs commonly disable editing for forced cells because resolution overrides their writes. The language does not require this convention; a host may accept a write and preserve it as shadow state.
Shadow state: forced and self-referencing cells
A forced cell preserves its source underneath its derived value, as filters and self-referencing methods do.
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.
Strength selects one of its two methods, low := min(low, high) or
high := max(low, high). 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 source is still 4.
Writing high did not change low‘s source. Switching back
to mode == 0 reselects low := min(low, high) (still the stalest cell) and recomputes it
from both cells’ sources, 4 and 42, rather than the 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.
The default branch and reverting to source
_ => { ... } matches any value not listed by a named branch and must appear last.
A named branch after _ is a syntax error. The default branch is optional:
if the match value doesn’t equal any branch’s
literal, none of the conditional’s relationships are active that round, and their outputs
become freely writable. Forced and self-referencing cells preserve a separate source
value, so releasing those cells restores their last external write or declared default.
Ordinary cells in reversible relationships need not retain a separate shadow; releasing
the relationship does not promise to undo their last derived value.
The previous section demonstrates
the shadow-state mechanism for forced and self-referencing cells.
sheet no_default {
source mode: i32 = 0;
cell x: f64 = 1.0;
conditional mode {
0i32 => {
relationship {
x := 100.0;
}
}
}
}
Nested and chained conditionals
Sibling conditionals may read one another’s outputs as match subjects or relationship inputs.
Branch bodies cannot contain conditionals. A resize dialog might use two sibling
conditionals, 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.
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
Run cargo run -p cel-euclid --features adam --example adam_geometry to execute
this library example. The geometry examples use a Rust host rather than live inspectors.
#![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.
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.
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.
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
Adam uses CEL’s lexical conventions: identifiers, integer and float literals with optional type suffixes, string literals, and punctuation. Adam also recognizes doc comments.
Comments
// starts a line comment; /* ... */ encloses a block comment:
// 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 sheet introduce doc comments.
The language server and formatter retain them; they do not affect sheet resolution:
//! 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
Adam recognizes sheet, use, as, cell, source, relationship, conditional,
out, require, and filter in their grammar positions. They are contextual:
cell names may use these words. Sheet definitions and module names reject _,
sheet, use, as, empty names, and raw identifiers. _ is also
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 fmt, backed by format_sheet, applies
one canonical layout: four-space indentation, opening braces on the keyword’s line,
and no space before a declaration’s closing ;:
sheet s {
cell x: i32 = 1;
cell y: i32 = 2;
}
Formatting is idempotent: formatting canonical source leaves it unchanged.
It preserves comments, doc comments, and blank lines in their original positions,
including before sheet and before closing braces.
The language server refuses formatting when syntax errors are recorded.
Callers of format_sheet must supply a sheet with an empty sheet.errors list.
Commas separate list items; they do not terminate lists. Only singleton tuple values, types and destructuring bindings require a terminal comma to distinguish them from grouping. Conditional branches have no comma separator or terminator.
Reference Manual
This appendix collects Adam’s grammar, type rules, solver behavior, and error messages.
Grammar
module = { use_decl | sheet }.
use_decl = "use" qualified_name [ "as" identifier ] ";".
sheet = "sheet" identifier [ "(" [ parameter { "," parameter } ] ")" ]
"{" { sheet_item } "}".
parameter = identifier ":" type_expr.
sheet_item = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
| source_decl | relationship_use).
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 } "}".
relationship_use = "relationship" qualified_name "(" [ actual { "," actual } ] ")" ";".
actual = identifier ":" identifier.
qualified_name = identifier { "::" identifier }.
binding = binding_target ":=" expression ";".
binding_target = identifier | "(" identifier "," [ identifier { "," identifier } ] ")".
conditional_decl = "conditional" expression "{" { conditional_branch } "}".
conditional_branch = (literal_pattern | "_") "=>" "{"
{ relationship_decl | relationship_use } "}".
out_decl = "out" identifier [ ":" type_expr ] ":=" expression
[ cell_filter ] [ require_block ] ";".
require_block = "require" "{" { requirement } "}".
requirement = [ "@" identifier ] expression ";".
expression, literals, identifiers, operators, if/else, casts, ranges, closures,
and function calls use CEL grammar; see
expressions are CEL.
cell_filter and require_block may attach to cell_decl, source_decl, and out_decl (see
a source cell can be filtered too).
Sheets and namespaces
- Root names identify explicit host selections. Parameterized sheet names identify
components used by named
relationshipinvocations. 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. - Cell dependencies must be declared earlier in the same sheet. Referencing an undeclared name is an “undeclared cell” error. See names and declaration order.
Components, modules and roots
sheet app { ... }defines a root;sheet component(...) { ... }defines a reusable component, even with an empty parameter list. A root is not an importable component. Imports refer to declared exports, not imported aliases.relationship component(formal: actual, ...);binds each formal exactly once. Binding order does not matter. Actuals must be distinct, previously declared ordinary cells or fixed sources with exactly matching complete concrete types. Expressions, literals andoutcells cannot be actuals. Parameters have no initializers, filters or requirements, and cannot become private outputs.- Components may own private cells, outputs and policies. Their local names do not enter the root’s public name maps. Each use allocates fresh private state and relationships; repeated uses do not merge relationships.
- Callers retain boundary filters, requirements, strength and source aspirations.
Fixed-source actuals remove every method that would write them, including nested
writes. This specialization uses cell kind, not the last round’s
is_source. An empty specialized relationship fails construction, even in an inactive branch. - Conditional uses require a transitively relationship-only component. Private cells, outputs or policies make a definition ineligible there. Unconditional uses may contain those declarations. Final combined graphs must satisfy the ordinary conditional guard-independence rules.
- Hosts supply explicit
ModulePathidentities and source owners toModuleSet. The core performs no filesystem IO, path inference or package discovery.ModuleCompiler::compilechecks all definitions, including unused ones, without executing host expressions. Install operation libraries and register custom types before creating the immutable compiler or parser. CompiledModules::rootsenumerates root names;instantiate_root(&module, name)selects one explicitly. Component-only modules need no root.AdamParser::parse_stris the convenience entry for a source with exactly one root and no externally supplied imports.- Construction runs defaults, initializers and factories, but never propagates. Compiled templates and live methods retain their immutable environments after earlier owners drop. Native factories must create fresh execution state and obey the pure-method contract. A failed native instantiation leaves its destination safely discardable, not necessarily unchanged.
- Compile/construction diagnostics own their actual source identities and labels;
runtime errors use
ParsedSheet::locate_error. Source-free native diagnostics retain native context without manufactured locations. Checked division, inversion and conversion failures still require handling. Requirement violations instead set validity flags, which callers inspect after propagation. - Components have concrete types, not generic type parameters. They do not infer equations, inject caller policies or turn private outputs into bindable cells.
The relationships chapter executes inline and imported examples. For VS Code, map every module, including the importing root:
{
"adam-lang.modules": {
"relationships": "adam-lang/examples/relationships.adm2",
"composed": "adam-lang/examples/composed.adm2",
"image_constraints": "cel-euclid/examples/image_constraints.adm2",
"resize_straighten": "cel-euclid/examples/resize_straighten.adm2"
}
}
Paths are relative to the first workspace folder. The extension sends owned
path/uri/text snapshots to the language server; the core does not read files.
UI hosts render owning module errors with
adam_web_ui::format_module_diagnostic(&diagnostic, &Renderer::plain()).
The separate format_parse_error adapter takes the original text and display name
for an anonymous source-only parse; do not substitute root text for imported
definition locations.
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. - 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;, andsource name: T = expr;use the correspondingcelltype and initializer rules, but have a fixed source kind.- 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 and rejects direct writes. 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
For a complete Dioxus consumer with application type/operation registration, explicit root selection, bound controls, recording, and host recovery policy, see the Dioxus integration guide.
| 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 registers additional Rust types under Adam names with
TypeRegistry::register (with a
Default) or
TypeRegistry::register_no_default
(without one). A type without a default requires an initializer; a bare : T declaration
fails at parse time. Registration happens in the host before parsing, not in sheet source.
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 {}; record fields and call arguments reject
trailing commas. 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 parsing fails 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 parsing fails 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 self-referencing method includes a cell in both its inputs and outputs. 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 its required comma. A bare name binds the whole result directly; a single parenthesized name without a comma is invalid. 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
- A
filterclause may follow acell,source, oroutinitializer. Filters have no names._denotes a candidate of the cell’s declared type; other cell references are deduced dependencies. A non-range filter must reference_and return exactly the cell’s type. A range filterlo..=hineed not reference_; its bounds must match the cell’s numeric type. Violations are parse-time errors. - Writing a cell never applies its filter. Filtering occurs during resolution.
- A filtered cell preserves its last-written source value separately from its computed derived value. Reads return the derived value when present, otherwise the source.
- 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 cell derived only byexpr. Host writes, relationships, and additionaloutdeclarations cannot write it. Later declarations may read it; see outputs and requirements.require { [ "@" name ] expr; ... }attaches boolean checks, each with an optional@namelabel to acell,source, oroutdeclaration, after any filter. Each requirement deduces its dependencies separately from the declaration’s expression. A failing requirement does not stop resolution or value computation; hosts query its diagnostic through the 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
parse_str reports malformed sheets as ParseError
with source spans. Success establishes syntactic and structural validity, but resolution
may still fail; see relationships and the solver.
The following table lists selected messages:
| 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
The crate documentation describes the Rust APIs for parsing and driving sheets:
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 parser’s function-library table. This book’s examples installcel-std.Sheet:read,write,propagate,is_source,is_forced,cell_kind,filter_*,cell_requirements,cell_requirements_valid,violated_requirements, and other runtime operations.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.