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//! module             = { use_decl | sheet }.
10//! use_decl           = "use" qualified_name [ "as" identifier ] ";".
11//! sheet              = "sheet" identifier [ "(" [ parameter { "," parameter } ] ")" ]
12//!                        "{" { sheet_item } "}".
13//! parameter          = identifier ":" type_expr.
14//! sheet_item         = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
15//!                        | source_decl | relationship_use).
16//! cell_decl          = "cell" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
17//! cell_filter        = "filter" expression.
18//! cell_type_init     = (":" type_expr ["=" expression]) | ("=" expression).
19//! source_decl        = "source" identifier cell_type_init [ cell_filter ] [ require_block ] ";".
20//! type_expr          = identifier [ "(" type_expr { "," type_expr } ")" ]
21//!                    | "[" type_expr "]"
22//!                    | "(" [ type_expr ["," [ type_expr { "," type_expr } ]] ] ")".
23//! relationship_decl  = "relationship" "{" { binding } "}".
24//! relationship_use   = "relationship" qualified_name "(" [ actual { "," actual } ] ")" ";".
25//! actual             = identifier ":" identifier.
26//! qualified_name     = identifier { "::" identifier }.
27//! binding            = binding_target ":=" expression ";".
28//! binding_target     = identifier | "(" identifier "," [ identifier { "," identifier } ] ")".
29//! conditional_decl   = "conditional" expression "{" { conditional_branch } "}".
30//! conditional_branch = (literal_pattern | "_") "=>" "{"
31//!                        { relationship_decl | relationship_use } "}".
32//! out_decl           = "out" identifier [":" type_expr] ":=" expression
33//!                        [ cell_filter ] [ require_block ] ";".
34//! require_block      = "require" "{" { requirement } "}".
35//! requirement        = [ "@" identifier ] expression ";".
36//! ```
37//!
38//! Every production uses strict LL(1) parsing: a comma commits to another item.
39//! Only singleton tuple values, types and destructuring bindings require a terminal comma.
40//! Conditional branches have no comma separator or terminator.
41//!
42//! A sheet without a parameter list is a root; a sheet with a parameter list
43//! defines a reusable component. [`AdamParser::parse_str`] constructs a module's
44//! sole root through [`ModuleCompiler`]. Collections with several roots use
45//! [`CompiledModules::instantiate_root`] for explicit selection. Construction
46//! checks the final combined graph without propagation. [`check_module`] checks
47//! registered sources and imports without executing any expressions.
48//!
49//! # Reusable components and explicit roots
50//!
51//! A component aliases caller-owned cells; it does not copy their values. Formal
52//! parameters have concrete types and no initializers, filters or requirements.
53//! Each use supplies every formal exactly once, in any named order, with distinct
54//! actual cells of exactly matching complete types. Actuals may be ordinary cells
55//! or fixed sources, never `out` cells, expressions or literals. A component may
56//! allocate private cells and diagnostic outputs, but cannot attach policies to
57//! parameters or turn parameters into outputs. The caller owns boundary policies.
58//!
59//! The following example registers both the library and importing root. It uses
60//! builtin arithmetic only; hosts needing standard or custom functions install
61//! their operation libraries before moving the lookup into the immutable compiler.
62//!
63//! ```
64//! use adam_lang::{ModuleCompiler, ModuleSet, TypeRegistry};
65//! let mut modules = ModuleSet::new();
66//! modules.insert_source("numbers".parse()?, "numbers.adm2", r#"
67//!     sheet multiply(a: f64, b: f64, product: f64) {
68//!         relationship {
69//!             product := a * b;
70//!             a := product / b;
71//!             b := product / a;
72//!         }
73//!     }
74//! "#)?;
75//! modules.insert_source("app".parse()?, "app.adm2", r#"
76//!     use numbers::multiply as mul;
77//!     sheet app {
78//!         source factor: f64 = 3.0;
79//!         cell value: f64 = 2.0;
80//!         cell result: f64 = 0.0;
81//!         relationship mul(product: result, b: factor, a: value);
82//!     }
83//!     sheet other {}
84//! "#)?;
85//! let compiled = ModuleCompiler::new(TypeRegistry::new(), cel_parser::OpLookup::new())
86//!     .compile(modules).map_err(|mut errors| errors.remove(0))?;
87//! let path = "app".parse()?;
88//! assert_eq!(compiled.roots(&path).collect::<Vec<_>>(), ["app", "other"]);
89//! let mut parsed = compiled.instantiate_root(&path, "app")?;
90//! drop(compiled); // Live methods and diagnostics retain their own resources.
91//! let value = parsed.cell_names["value"].0;
92//! let result = parsed.cell_names["result"].0;
93//! assert_eq!(*parsed.read::<f64>(result).unwrap(), 0.0); // No propagation yet.
94//! parsed.sheet.write(value, 7.0f64).unwrap();
95//! parsed.propagate().unwrap();
96//! assert_eq!(*parsed.read::<f64>(result).unwrap(), 21.0);
97//! parsed.sheet.write(result, 12.0f64).unwrap();
98//! parsed.propagate().unwrap();
99//! assert_eq!(*parsed.read::<f64>(value).unwrap(), 4.0);
100//! # Ok::<(), adam_lang::ModuleDiagnostic>(())
101//! ```
102//!
103//! Source specialization removes methods that write fixed sources, including
104//! through nested uses. It does not infer equations or inspect current values.
105//! An empty specialized relationship fails construction, even in an inactive
106//! conditional branch. Conditional uses must contain only relationships
107//! transitively: private cells, outputs and policies make a component ineligible.
108//! Unconditional uses may contain those private declarations.
109//!
110//! Compilation checks every definition without executing host expressions;
111//! construction runs defaults, initializers and factories but does not propagate.
112//! Runtime division and conversion errors still require handling. Requirements
113//! report validity, not propagation failure; inspect
114//! [`Sheet::cell_requirements_valid`](adam_rs::Sheet::cell_requirements_valid).
115//! Native instantiation failures leave the destination safely discardable, not
116//! necessarily unchanged. Owning [`ModuleDiagnostic`] labels identify real files;
117//! [`ParsedSheet::locate_error`] resolves runtime errors to definition/use sites.
118//! No filesystem loader, implicit module search or generic type parameter exists.
119//! Imports name declared components, not roots or imported aliases.
120//!
121//! The design spec for `cell_decl` also calls for an optional trailing `":=" expression`
122//! clause, not shown above because **this crate does not yet implement it** — see
123//! `docs/superpowers/specs/2026-08-19-adam-lang-syntax-design.md`'s "Explicitly out of scope"
124//! section; it's deferred pending a forward-reference/hoisting decision. Only `cell_decl`'s
125//! `"=" expression` one-time initializer and its optional `cell_filter`/`require_block`
126//! clauses are implemented today.
127//!
128//! `expression` and its descendants (`literal`, `identifier`, and the rest of the
129//! CEL expression grammar), as well as `literal_pattern` (a bare literal, optionally negated by
130//! a leading `-`, matching Rust's own `LiteralPattern` rule — CEL has no constant-expression
131//! syntax in pattern position), are defined by `cel_parser` — see that crate's own
132//! [`# Grammar`](../cel_parser/index.html#grammar) section. That embedded CEL grammar now includes
133//! postfix array type ascriptions such as `[left, right]: [Custom]` and `[]: [[i32]]`. The
134//! ascription names the complete array type, resolves leaf names through the active
135//! [`TypeRegistry`], and reuses recursive tuple/array/callable type syntax. Persistent cells
136//! support arbitrary compositions of these shapes and registered generic leaves. Arrays
137//! default to empty, tuples default recursively, and callables require an initializer.
138//! Every stored cell requires cloning and value equality: CEL permits callable storage, but
139//! Adam rejects a callable cell without equality rather than inventing function equality.
140//! Registered concrete leaves keep [`Sheet`](adam_rs::Sheet)'s Rust read/write access;
141//! compound owners use [`ParsedSheet::read_compound`], [`ParsedSheet::write_compound`], and
142//! [`ParsedSheet::source_compound`] with canonical representation witnesses. Relationships,
143//! filters, outputs, conditionals, and requirements validate complete frames at construction
144//! and run the existing raw CEL engine without recursively checking types on propagation.
145//! The private `CellValue` owns one complete compound value, not individual erased
146//! array elements. Checked host writes reject mismatched recursive witnesses before
147//! mutation. Reads clone independent owners; source reads preserve aspirations.
148//! Formatting is a separate capability: [`prepare_compound_cell_formatter`] binds
149//! it without adding equality or clone requirements.
150//!
151//! ```
152//! use adam_lang::{AdamParser, TypeRegistry};
153//! use cel_parser::OpLookup;
154//! use cel_runtime::{Array, ArrayOf, Cons, IntoCStackList, Leaf, Nil};
155//! type Rows = ArrayOf<Cons<Leaf<i32>, Cons<Leaf<bool>, Nil>>>;
156//! let mut parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
157//!     .parse_str("sheet s { source rows: [(i32, bool)]; out copy := rows; }").unwrap();
158//! let id = parsed.cell_names["rows"].0;
159//! parsed.write_compound::<Rows>(id, Array::from_vec(vec![(7, true).into_c_stack_list()])).unwrap();
160//! parsed.propagate().unwrap();
161//! assert_eq!(parsed.read_compound::<Rows>(parsed.output_names["copy"]).unwrap().len(), 1);
162//! assert_eq!(parsed.source_compound::<Rows>(id).unwrap().as_slice()[0].1, 7);
163//! ```
164//!
165//! A `cell_filter`'s `expression` names no explicit parameter list: `_` always refers to the
166//! candidate value being conformed (of the filtered cell's own declared type), and every other
167//! identifier that names an already-declared cell is a dependency, deduced exactly as a
168//! `relationship` binding's/`out` declaration's/`conditional`'s own `expression` deduces its
169//! inputs — see [`ast::CellFilter`]. `_` is reserved inside a filter expression only.
170//!
171//! # Example
172//!
173//! `AdamParser::new` takes an [`OpLookup`](cel_parser::OpLookup) instance. See
174//! [`OpLookup::push_library_scope`](cel_parser::OpLookup::push_library_scope) for how to
175//! install one (e.g. `cel-std`) before parsing.
176//!
177//! ```rust
178//! use adam_lang::{AdamParser, TypeRegistry};
179//! use cel_parser::OpLookup;
180//!
181//! let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
182//! let parsed = parser.parse_str(r#"
183//!     sheet image_resize {
184//!         cell width:  f64 = 1920.0;
185//!         cell height: f64 = 1080.0;
186//!         cell area:   f64;
187//!     }
188//! "#).unwrap();
189//! ```
190//!
191//! Built-in and registered Adam types are both visible inside embedded CEL array ascriptions:
192//!
193//! ```rust
194//! use adam_lang::{AdamParser, TypeRegistry};
195//! use cel_parser::OpLookup;
196//! use cel_runtime::{ArrayOf, Leaf, ValueRepr};
197//!
198//! #[derive(Clone, Debug, PartialEq)]
199//! struct Custom(i32);
200//!
201//! #[derive(Clone)]
202//! struct CountFn;
203//!
204//! let mut lookup = OpLookup::new();
205//! lookup.push_scope(|name, segment, arity, _span| match (name, arity) {
206//!     ("left", 0) => {
207//!         segment.op0(|| Custom(1));
208//!         Ok(true)
209//!     }
210//!     ("right", 0) => {
211//!         segment.op0(|| Custom(2));
212//!         Ok(true)
213//!     }
214//!     ("count", 0) => {
215//!         segment.op0(|| CountFn);
216//!         Ok(true)
217//!     }
218//!     ("()", 2) => {
219//!         let array = &segment.peek_stack_infos(1)[0].value_type;
220//!         if array.same_shape(&ArrayOf::<Leaf<i32>>::value_type()) {
221//!             segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<i32>>, Leaf<i32>, _>(
222//!                 |_callee, values| values.len() as i32)?;
223//!         } else if array.same_shape(&ArrayOf::<Leaf<Custom>>::value_type()) {
224//!             segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<Custom>>, Leaf<i32>, _>(
225//!                 |_callee, values| values.len() as i32)?;
226//!         } else {
227//!             segment.op2_repr::<Leaf<CountFn>, ArrayOf<ArrayOf<Leaf<Custom>>>, Leaf<i32>, _>(
228//!                 |_callee, values| values.len() as i32)?;
229//!         }
230//!         Ok(true)
231//!     }
232//!     _ => Ok(false),
233//! });
234//!
235//! let mut types = TypeRegistry::new();
236//! types.register_no_default::<Custom>("Custom");
237//! let mut parser = AdamParser::new(types, lookup);
238//! let parsed = parser.parse_str(
239//!     "sheet s { \
240//!         cell scalar: i32 = count([1, 2]: [i32]); \
241//!         cell values: i32 = count([left, right]: [Custom]); \
242//!         cell empty: i32 = count([]: [Custom]); \
243//!         cell nested: i32 = count([[]: [Custom]]: [[Custom]]); \
244//!     }",
245//! ).unwrap();
246//!
247//! let (scalar, _) = parsed.cell_names["scalar"];
248//! assert_eq!(*parsed.read::<i32>(scalar).unwrap(), 2);
249//! let (values, _) = parsed.cell_names["values"];
250//! assert_eq!(*parsed.read::<i32>(values).unwrap(), 2);
251//! let (empty, _) = parsed.cell_names["empty"];
252//! assert_eq!(*parsed.read::<i32>(empty).unwrap(), 0);
253//! let (nested, _) = parsed.cell_names["nested"];
254//! assert_eq!(*parsed.read::<i32>(nested).unwrap(), 1);
255//! ```
256//!
257//! Embedded CEL array diagnostics stay exact:
258//!
259//! ```rust
260//! use adam_lang::{AdamParser, TypeRegistry};
261//! use cel_parser::OpLookup;
262//!
263//! let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
264//! let err = parser
265//!     .parse_str("sheet s { cell values = []: [Missing]; }")
266//!     .unwrap_err();
267//! assert_eq!(err.message(), "unknown type `Missing`");
268//! ```
269//!
270//! ```rust
271//! use adam_lang::{AdamAstParser, TypeRegistry, check_sheet};
272//!
273//! let sheet = AdamAstParser::new()
274//!     .parse_str("sheet s { out values := [0, 1]: [f64]; }")
275//!     .unwrap();
276//! let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
277//! assert_eq!(diagnostics.len(), 1);
278//! assert_eq!(
279//!     diagnostics[0].message(),
280//!     "array elements must match the annotation exactly: expected `f64`, found `i32`"
281//! );
282//! ```
283
284pub mod ast;
285mod ast_parser;
286mod cell_value;
287pub mod composition;
288mod error_labels;
289mod fmt;
290pub mod module;
291mod parser;
292mod sheet_diagnostics;
293pub mod source_map;
294mod token_cursor;
295mod trivia;
296pub mod type_registry;
297mod typecheck;
298
299// Live-root errors keep their owning ModuleDiagnostic in ParseError's source chain.
300pub use ast_parser::AdamAstParser;
301pub use cel_parser::ParseError;
302pub use cell_value::{debug_compound_cell, prepare_compound_cell_formatter};
303pub use composition::{
304    AuthoredItem, AuthoredRole, CellOrigin, CellSlot, CompositionBuilder, InstanceOrigin,
305    MethodRecipe, OccurrenceOrigin, TypedTemplate,
306};
307pub use fmt::{FormatSourceError, format_module, format_sheet, format_source};
308pub use module::{
309    CompiledModules, ModuleCompiler, ModulePath, ModuleSet, PreparedRoot, RootCell, check_module,
310};
311pub use parser::{AdamParser, ParsedSheet};
312pub use sheet_diagnostics::SheetDiagnostics;
313pub use source_map::{
314    ModuleCause, ModuleDiagnostic, SourceId, SourceLabel, SourceLocation, SourceMap,
315};
316pub use trivia::{attach_module_trivia, attach_trivia};
317pub use type_registry::TypeRegistry;
318pub use typecheck::check_sheet;