Expand description
§adam-lang
A DSL parser for property models. Parses an adam-lang source string and produces
a live ParsedSheet (sheet plus cell names in declaration order).
§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_filter = "filter" expression.
cell_type_init = (":" type_expr ["=" expression]) | ("=" 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 ";".Every production uses strict LL(1) parsing: a comma commits to another item. Only singleton tuple values, types and destructuring bindings require a terminal comma. Conditional branches have no comma separator or terminator.
A sheet without a parameter list is a root; a sheet with a parameter list
defines a reusable component. AdamParser::parse_str constructs a module’s
sole root through ModuleCompiler. Collections with several roots use
CompiledModules::instantiate_root for explicit selection. Construction
checks the final combined graph without propagation. check_module checks
registered sources and imports without executing any expressions.
§Reusable components and explicit roots
A component aliases caller-owned cells; it does not copy their values. Formal
parameters have concrete types and no initializers, filters or requirements.
Each use supplies every formal exactly once, in any named order, with distinct
actual cells of exactly matching complete types. Actuals may be ordinary cells
or fixed sources, never out cells, expressions or literals. A component may
allocate private cells and diagnostic outputs, but cannot attach policies to
parameters or turn parameters into outputs. The caller owns boundary policies.
The following example registers both the library and importing root. It uses builtin arithmetic only; hosts needing standard or custom functions install their operation libraries before moving the lookup into the immutable compiler.
use adam_lang::{ModuleCompiler, ModuleSet, TypeRegistry};
let mut modules = ModuleSet::new();
modules.insert_source("numbers".parse()?, "numbers.adm2", r#"
sheet multiply(a: f64, b: f64, product: f64) {
relationship {
product := a * b;
a := product / b;
b := product / a;
}
}
"#)?;
modules.insert_source("app".parse()?, "app.adm2", r#"
use numbers::multiply as mul;
sheet app {
source factor: f64 = 3.0;
cell value: f64 = 2.0;
cell result: f64 = 0.0;
relationship mul(product: result, b: factor, a: value);
}
sheet other {}
"#)?;
let compiled = ModuleCompiler::new(TypeRegistry::new(), cel_parser::OpLookup::new())
.compile(modules).map_err(|mut errors| errors.remove(0))?;
let path = "app".parse()?;
assert_eq!(compiled.roots(&path).collect::<Vec<_>>(), ["app", "other"]);
let mut parsed = compiled.instantiate_root(&path, "app")?;
drop(compiled); // Live methods and diagnostics retain their own resources.
let value = parsed.cell_names["value"].0;
let result = parsed.cell_names["result"].0;
assert_eq!(*parsed.read::<f64>(result).unwrap(), 0.0); // No propagation yet.
parsed.sheet.write(value, 7.0f64).unwrap();
parsed.propagate().unwrap();
assert_eq!(*parsed.read::<f64>(result).unwrap(), 21.0);
parsed.sheet.write(result, 12.0f64).unwrap();
parsed.propagate().unwrap();
assert_eq!(*parsed.read::<f64>(value).unwrap(), 4.0);Source specialization removes methods that write fixed sources, including through nested uses. It does not infer equations or inspect current values. An empty specialized relationship fails construction, even in an inactive conditional branch. Conditional uses must contain only relationships transitively: private cells, outputs and policies make a component ineligible. Unconditional uses may contain those private declarations.
Compilation checks every definition without executing host expressions;
construction runs defaults, initializers and factories but does not propagate.
Runtime division and conversion errors still require handling. Requirements
report validity, not propagation failure; inspect
Sheet::cell_requirements_valid.
Native instantiation failures leave the destination safely discardable, not
necessarily unchanged. Owning ModuleDiagnostic labels identify real files;
ParsedSheet::locate_error resolves runtime errors to definition/use sites.
No filesystem loader, implicit module search or generic type parameter exists.
Imports name declared components, not roots or imported aliases.
The design spec for cell_decl also calls for an optional trailing ":=" expression
clause, not shown above because this crate does not yet implement it — see
docs/superpowers/specs/2026-08-19-adam-lang-syntax-design.md’s “Explicitly out of scope”
section; it’s deferred pending a forward-reference/hoisting decision. Only cell_decl’s
"=" expression one-time initializer and its optional cell_filter/require_block
clauses are implemented today.
expression and its descendants (literal, identifier, and the rest of the
CEL expression grammar), as well as literal_pattern (a bare literal, optionally negated by
a leading -, matching Rust’s own LiteralPattern rule — CEL has no constant-expression
syntax in pattern position), are defined by cel_parser — see that crate’s own
# Grammar section. That embedded CEL grammar now includes
postfix array type ascriptions such as [left, right]: [Custom] and []: [[i32]]. The
ascription names the complete array type, resolves leaf names through the active
TypeRegistry, and reuses recursive tuple/array/callable type syntax. Persistent cells
support arbitrary compositions of these shapes and registered generic leaves. Arrays
default to empty, tuples default recursively, and callables require an initializer.
Every stored cell requires cloning and value equality: CEL permits callable storage, but
Adam rejects a callable cell without equality rather than inventing function equality.
Registered concrete leaves keep Sheet’s Rust read/write access;
compound owners use ParsedSheet::read_compound, ParsedSheet::write_compound, and
ParsedSheet::source_compound with canonical representation witnesses. Relationships,
filters, outputs, conditionals, and requirements validate complete frames at construction
and run the existing raw CEL engine without recursively checking types on propagation.
The private CellValue owns one complete compound value, not individual erased
array elements. Checked host writes reject mismatched recursive witnesses before
mutation. Reads clone independent owners; source reads preserve aspirations.
Formatting is a separate capability: prepare_compound_cell_formatter binds
it without adding equality or clone requirements.
use adam_lang::{AdamParser, TypeRegistry};
use cel_parser::OpLookup;
use cel_runtime::{Array, ArrayOf, Cons, IntoCStackList, Leaf, Nil};
type Rows = ArrayOf<Cons<Leaf<i32>, Cons<Leaf<bool>, Nil>>>;
let mut parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
.parse_str("sheet s { source rows: [(i32, bool)]; out copy := rows; }").unwrap();
let id = parsed.cell_names["rows"].0;
parsed.write_compound::<Rows>(id, Array::from_vec(vec![(7, true).into_c_stack_list()])).unwrap();
parsed.propagate().unwrap();
assert_eq!(parsed.read_compound::<Rows>(parsed.output_names["copy"]).unwrap().len(), 1);
assert_eq!(parsed.source_compound::<Rows>(id).unwrap().as_slice()[0].1, 7);A cell_filter’s expression names no explicit parameter list: _ always refers to the
candidate value being conformed (of the filtered cell’s own declared type), and every other
identifier that names an already-declared cell is a dependency, deduced exactly as a
relationship binding’s/out declaration’s/conditional’s own expression deduces its
inputs — see ast::CellFilter. _ is reserved inside a filter expression only.
§Example
AdamParser::new takes an OpLookup instance. See
OpLookup::push_library_scope for how to
install one (e.g. cel-std) before parsing.
use adam_lang::{AdamParser, TypeRegistry};
use cel_parser::OpLookup;
let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
let parsed = parser.parse_str(r#"
sheet image_resize {
cell width: f64 = 1920.0;
cell height: f64 = 1080.0;
cell area: f64;
}
"#).unwrap();Built-in and registered Adam types are both visible inside embedded CEL array ascriptions:
use adam_lang::{AdamParser, TypeRegistry};
use cel_parser::OpLookup;
use cel_runtime::{ArrayOf, Leaf, ValueRepr};
#[derive(Clone, Debug, PartialEq)]
struct Custom(i32);
#[derive(Clone)]
struct CountFn;
let mut lookup = OpLookup::new();
lookup.push_scope(|name, segment, arity, _span| match (name, arity) {
("left", 0) => {
segment.op0(|| Custom(1));
Ok(true)
}
("right", 0) => {
segment.op0(|| Custom(2));
Ok(true)
}
("count", 0) => {
segment.op0(|| CountFn);
Ok(true)
}
("()", 2) => {
let array = &segment.peek_stack_infos(1)[0].value_type;
if array.same_shape(&ArrayOf::<Leaf<i32>>::value_type()) {
segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<i32>>, Leaf<i32>, _>(
|_callee, values| values.len() as i32)?;
} else if array.same_shape(&ArrayOf::<Leaf<Custom>>::value_type()) {
segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<Custom>>, Leaf<i32>, _>(
|_callee, values| values.len() as i32)?;
} else {
segment.op2_repr::<Leaf<CountFn>, ArrayOf<ArrayOf<Leaf<Custom>>>, Leaf<i32>, _>(
|_callee, values| values.len() as i32)?;
}
Ok(true)
}
_ => Ok(false),
});
let mut types = TypeRegistry::new();
types.register_no_default::<Custom>("Custom");
let mut parser = AdamParser::new(types, lookup);
let parsed = parser.parse_str(
"sheet s { \
cell scalar: i32 = count([1, 2]: [i32]); \
cell values: i32 = count([left, right]: [Custom]); \
cell empty: i32 = count([]: [Custom]); \
cell nested: i32 = count([[]: [Custom]]: [[Custom]]); \
}",
).unwrap();
let (scalar, _) = parsed.cell_names["scalar"];
assert_eq!(*parsed.read::<i32>(scalar).unwrap(), 2);
let (values, _) = parsed.cell_names["values"];
assert_eq!(*parsed.read::<i32>(values).unwrap(), 2);
let (empty, _) = parsed.cell_names["empty"];
assert_eq!(*parsed.read::<i32>(empty).unwrap(), 0);
let (nested, _) = parsed.cell_names["nested"];
assert_eq!(*parsed.read::<i32>(nested).unwrap(), 1);Embedded CEL array diagnostics stay exact:
use adam_lang::{AdamParser, TypeRegistry};
use cel_parser::OpLookup;
let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
let err = parser
.parse_str("sheet s { cell values = []: [Missing]; }")
.unwrap_err();
assert_eq!(err.message(), "unknown type `Missing`");use adam_lang::{AdamAstParser, TypeRegistry, check_sheet};
let sheet = AdamAstParser::new()
.parse_str("sheet s { out values := [0, 1]: [f64]; }")
.unwrap();
let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
assert_eq!(diagnostics.len(), 1);
assert_eq!(
diagnostics[0].message(),
"array elements must match the annotation exactly: expected `f64`, found `i32`"
);Re-exports§
pub use composition::AuthoredItem;pub use composition::AuthoredRole;pub use composition::CellOrigin;pub use composition::CellSlot;pub use composition::CompositionBuilder;pub use composition::InstanceOrigin;pub use composition::MethodRecipe;pub use composition::OccurrenceOrigin;pub use composition::TypedTemplate;pub use module::CompiledModules;pub use module::ModuleCompiler;pub use module::ModulePath;pub use module::ModuleSet;pub use module::PreparedRoot;pub use module::RootCell;pub use module::check_module;pub use source_map::ModuleCause;pub use source_map::ModuleDiagnostic;pub use source_map::SourceId;pub use source_map::SourceLabel;pub use source_map::SourceLocation;pub use source_map::SourceMap;pub use type_registry::TypeRegistry;
Modules§
- ast
- A span-carrying adam-lang structural AST, built by
crate::AdamAstParseras an alternative tocrate::AdamParser’s directadam_rs::Sheetconstruction. Method bodies and cell initializers reference [cel_parser::Expr]/[cel_parser::lex_lexer::Literal] directly. Carries no resolved types, noTypeRegistrylookups, and never fails on semantic grounds (unknown type name, literal/type mismatch, undeclared cell, arity mismatch) — those checks are deferred to a later, separate compile-to-Sheetphase, mirroring [cel_parser::AstContext]’s design. - composition
- Builds complete-shape templates without parsing source text.
- module
- Compiles supplied modules into immutable factories without filesystem access or execution.
- source_
map - Owning source identities for diagnostics across module collections.
- type_
registry - Type registry for adam-lang cell declarations.
Structs§
- Adam
AstParser - Parses adam-lang source strings into
ast::Sheettrees, instead of executing into a liveadam_rs::Sheet(seecrate::AdamParserfor that path). - Adam
Parser - Owns one immutable type/operation environment for repeatable root construction.
- Parse
Error - A parse error carrying the original
proc_macro2::Spanof the offending token or expression start, plus an optional end span for range errors. - Parsed
Sheet - Owns a live [
Sheet], ordered public root names, complete shapes and provenance. - Sheet
Diagnostics - Retains source and native provenance without owning or borrowing a
Sheet.
Enums§
- Format
Source Error - Error returned by
format_sourcewhensourcecan’t be safely formatted.
Functions§
- attach_
module_ trivia - Attaches every file-boundary comment and each sheet’s existing nested trivia.
- attach_
trivia - Recovers comments/blank-lines from every gap in
sheet— a leading comment before thesheetkeyword itself, its own top-level items, and every nestedrelationship/conditionalbody — attaching each to the nearest following node. - check_
sheet - Checks
sheetagainst registered types and operation/record metadata, returning diagnostics. Never fails — an absent Adam annotation, an unresolved identifier, an Adamcell/source/outannotationregistrydoesn’t resolve, or a custom call without metadata falls back to [cel_parser::Ty::Any] and are silently skipped, not reported. CEL array annotations inside expressions use the same registry-backed CEL resolver, so unknown leaf names such as[]: [Missing]are still reported. Known custom annotations retain their complete leaf descriptors. Constructors and members uselookup’s schemas/signatures, not value names; unknown constructors and incompatible known fields/operands produce diagnostics. - debug_
compound_ cell - Formats a canonical compound cell at the checked host boundary.
- format_
module - Formats every module item in order, preserving attached file-boundary comments.
- format_
sheet - Pretty-prints
sheetback to adam-lang source text — see the module doc for the printing rules. - format_
source - Parses and formats
sourceas a complete adam-lang module. - prepare_
compound_ cell_ formatter - Prepares canonical compound-cell formatting from the cell owner’s debug capability.