Skip to main content

adam_lang/
lib.rs

1//! # adam-lang
2//!
3//! A DSL parser for property models. Parses an adam-lang source string and produces
4//! a live [`ParsedSheet`] (sheet plus cell names in declaration order).
5//!
6//! # Grammar
7//!
8//! ```text
9//! sheet              = "sheet" identifier "{" { sheet_item } "}".
10//! sheet_item         = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
11//!                        | source_decl).
12//! cell_decl          = "cell" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
13//! cell_filter        = "filter" expression.
14//! cell_type_init     = (":" type_expr ["=" expression]) | ("=" expression).
15//! source_decl        = "source" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
16//! type_expr          = identifier [ "(" type_expr { "," type_expr } ")" ]
17//!                    | "[" type_expr "]"
18//!                    | "(" [ type_expr ["," [ type_expr { "," type_expr } ]] ] ")".
19//! relationship_decl  = "relationship" "{" { binding } "}".
20//! binding            = binding_target ":=" expression ";".
21//! binding_target     = identifier | "(" identifier { "," identifier } [ "," ] ")".
22//! conditional_decl   = "conditional" expression "{" { conditional_branch } "}".
23//! conditional_branch = (literal_pattern | "_") "=>" "{" { relationship_decl } "}" [ "," ].
24//! out_decl           = "out" identifier [":" type_expr] ":=" expression
25//!                        [ cell_filter ] [ require_block ] ";".
26//! require_block      = "require" "{" { requirement } "}".
27//! requirement        = [ "@" identifier ] expression ";".
28//! ```
29//!
30//! The design spec for `cell_decl` also calls for an optional trailing `":=" expression`
31//! clause, not shown above because **this crate does not yet implement it** — see
32//! `docs/superpowers/specs/2026-08-19-adam-lang-syntax-design.md`'s "Explicitly out of scope"
33//! section; it's deferred pending a forward-reference/hoisting decision. Only `cell_decl`'s
34//! `"=" expression` one-time initializer and its optional `cell_filter`/`require_block`
35//! clauses are implemented today.
36//!
37//! `expression` and its descendants (`literal`, `identifier`, and the rest of the
38//! CEL expression grammar), as well as `literal_pattern` (a bare literal, optionally negated by
39//! a leading `-`, matching Rust's own `LiteralPattern` rule — CEL has no constant-expression
40//! syntax in pattern position), are defined by `cel_parser` — see that crate's own
41//! [`# Grammar`](../cel_parser/index.html#grammar) section. That embedded CEL grammar now includes
42//! postfix array type ascriptions such as `[left, right]: [Custom]` and `[]: [[i32]]`. The
43//! ascription names the complete array type, resolves leaf names through the active
44//! [`TypeRegistry`], and reuses recursive tuple/array/callable type syntax. Persistent cells
45//! support arbitrary compositions of these shapes and registered generic leaves. Arrays
46//! default to empty, tuples default recursively, and callables require an initializer.
47//! Every stored cell requires cloning and value equality: CEL permits callable storage, but
48//! Adam rejects a callable cell without equality rather than inventing function equality.
49//! Registered concrete leaves keep [`Sheet`](adam_rs::Sheet)'s Rust read/write access;
50//! compound owners use [`ParsedSheet::read_compound`], [`ParsedSheet::write_compound`], and
51//! [`ParsedSheet::source_compound`] with canonical representation witnesses. Relationships,
52//! filters, outputs, conditionals, and requirements validate complete frames at construction
53//! and run the existing raw CEL engine without recursively checking types on propagation.
54//! The private `CellValue` owns one complete compound value, not individual erased
55//! array elements. Checked host writes reject mismatched recursive witnesses before
56//! mutation. Reads clone independent owners; source reads preserve aspirations.
57//! Formatting is a separate capability: [`prepare_compound_cell_formatter`] binds
58//! it without adding equality or clone requirements.
59//!
60//! ```
61//! use adam_lang::{AdamParser, TypeRegistry};
62//! use cel_parser::OpLookup;
63//! use cel_runtime::{Array, ArrayOf, Cons, IntoCStackList, Leaf, Nil};
64//! type Rows = ArrayOf<Cons<Leaf<i32>, Cons<Leaf<bool>, Nil>>>;
65//! let mut parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
66//!     .parse_str("sheet s { source rows: [(i32, bool)]; out copy := rows; }").unwrap();
67//! let id = parsed.cell_names["rows"].0;
68//! parsed.write_compound::<Rows>(id, Array::from_vec(vec![(7, true).into_c_stack_list()])).unwrap();
69//! parsed.propagate().unwrap();
70//! assert_eq!(parsed.read_compound::<Rows>(parsed.output_names["copy"]).unwrap().len(), 1);
71//! assert_eq!(parsed.source_compound::<Rows>(id).unwrap().as_slice()[0].1, 7);
72//! ```
73//!
74//! A `cell_filter`'s `expression` names no explicit parameter list: `_` always refers to the
75//! candidate value being conformed (of the filtered cell's own declared type), and every other
76//! identifier that names an already-declared cell is a dependency, deduced exactly as a
77//! `relationship` binding's/`out` declaration's/`conditional`'s own `expression` deduces its
78//! inputs — see [`ast::CellFilter`]. `_` is reserved inside a filter expression only.
79//!
80//! # Example
81//!
82//! `AdamParser::new` takes an [`OpLookup`](cel_parser::OpLookup) instance. See
83//! [`OpLookup::push_library_scope`](cel_parser::OpLookup::push_library_scope) for how to
84//! install one (e.g. `cel-std`) before parsing.
85//!
86//! ```rust
87//! use adam_lang::{AdamParser, TypeRegistry};
88//! use cel_parser::OpLookup;
89//!
90//! let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
91//! let parsed = parser.parse_str(r#"
92//!     sheet image_resize {
93//!         cell width:  f64 = 1920.0;
94//!         cell height: f64 = 1080.0;
95//!         cell area:   f64;
96//!     }
97//! "#).unwrap();
98//! ```
99//!
100//! Built-in and registered Adam types are both visible inside embedded CEL array ascriptions:
101//!
102//! ```rust
103//! use adam_lang::{AdamParser, TypeRegistry};
104//! use cel_parser::OpLookup;
105//! use cel_runtime::{ArrayOf, Leaf, ValueRepr};
106//!
107//! #[derive(Clone, Debug, PartialEq)]
108//! struct Custom(i32);
109//!
110//! #[derive(Clone)]
111//! struct CountFn;
112//!
113//! let mut lookup = OpLookup::new();
114//! lookup.push_scope(|name, segment, arity, _span| match (name, arity) {
115//!     ("left", 0) => {
116//!         segment.op0(|| Custom(1));
117//!         Ok(true)
118//!     }
119//!     ("right", 0) => {
120//!         segment.op0(|| Custom(2));
121//!         Ok(true)
122//!     }
123//!     ("count", 0) => {
124//!         segment.op0(|| CountFn);
125//!         Ok(true)
126//!     }
127//!     ("()", 2) => {
128//!         let array = &segment.peek_stack_infos(1)[0].value_type;
129//!         if array.same_shape(&ArrayOf::<Leaf<i32>>::value_type()) {
130//!             segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<i32>>, Leaf<i32>, _>(
131//!                 |_callee, values| values.len() as i32)?;
132//!         } else if array.same_shape(&ArrayOf::<Leaf<Custom>>::value_type()) {
133//!             segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<Custom>>, Leaf<i32>, _>(
134//!                 |_callee, values| values.len() as i32)?;
135//!         } else {
136//!             segment.op2_repr::<Leaf<CountFn>, ArrayOf<ArrayOf<Leaf<Custom>>>, Leaf<i32>, _>(
137//!                 |_callee, values| values.len() as i32)?;
138//!         }
139//!         Ok(true)
140//!     }
141//!     _ => Ok(false),
142//! });
143//!
144//! let mut types = TypeRegistry::new();
145//! types.register_no_default::<Custom>("Custom");
146//! let mut parser = AdamParser::new(types, lookup);
147//! let parsed = parser.parse_str(
148//!     "sheet s { \
149//!         cell scalar: i32 = count([1, 2]: [i32]); \
150//!         cell values: i32 = count([left, right]: [Custom]); \
151//!         cell empty: i32 = count([]: [Custom]); \
152//!         cell nested: i32 = count([[]: [Custom]]: [[Custom]]); \
153//!     }",
154//! ).unwrap();
155//!
156//! let (scalar, _) = parsed.cell_names["scalar"];
157//! assert_eq!(*parsed.read::<i32>(scalar).unwrap(), 2);
158//! let (values, _) = parsed.cell_names["values"];
159//! assert_eq!(*parsed.read::<i32>(values).unwrap(), 2);
160//! let (empty, _) = parsed.cell_names["empty"];
161//! assert_eq!(*parsed.read::<i32>(empty).unwrap(), 0);
162//! let (nested, _) = parsed.cell_names["nested"];
163//! assert_eq!(*parsed.read::<i32>(nested).unwrap(), 1);
164//! ```
165//!
166//! Embedded CEL array diagnostics stay exact:
167//!
168//! ```rust
169//! use adam_lang::{AdamParser, TypeRegistry};
170//! use cel_parser::OpLookup;
171//!
172//! let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
173//! let err = parser
174//!     .parse_str("sheet s { cell values = []: [Missing]; }")
175//!     .unwrap_err();
176//! assert_eq!(err.message(), "unknown type `Missing`");
177//! ```
178//!
179//! ```rust
180//! use adam_lang::{AdamAstParser, TypeRegistry, check_sheet};
181//!
182//! let sheet = AdamAstParser::new()
183//!     .parse_str("sheet s { out values := [0, 1]: [f64]; }")
184//!     .unwrap();
185//! let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
186//! assert_eq!(diagnostics.len(), 1);
187//! assert_eq!(
188//!     diagnostics[0].message(),
189//!     "array elements must match the annotation exactly: expected `f64`, found `i32`"
190//! );
191//! ```
192
193pub mod ast;
194mod ast_parser;
195mod cell_value;
196mod error_labels;
197mod fmt;
198mod parser;
199mod token_cursor;
200mod trivia;
201pub mod type_registry;
202mod typecheck;
203
204// adam-lang reuses cel_parser::ParseError directly; no new error type is introduced.
205// All parse errors carry a proc_macro2::Span for source-location diagnostics.
206pub use ast_parser::AdamAstParser;
207pub use cel_parser::ParseError;
208pub use cell_value::{debug_compound_cell, prepare_compound_cell_formatter};
209pub use fmt::{FormatSourceError, format_sheet, format_source};
210pub use parser::{AdamParser, ParsedSheet};
211pub use trivia::attach_trivia;
212pub use type_registry::TypeRegistry;
213pub use typecheck::check_sheet;