1use crate::labels::{Labels, Renderer, format_adam_error, labels_from_cell_names};
5use adam_lang::{AdamParser, ParsedSheet, TypeRegistry};
6
7pub struct BuildOutcome {
14 pub sheet_labels: Option<(ParsedSheet, Labels)>,
16 pub error: Option<String>,
18}
19
20pub fn op_lookup() -> cel_parser::OpLookup {
36 let mut lookup = cel_parser::OpLookup::new();
37 cel_std::install(&mut lookup);
38 cel_euclid::install(&mut lookup);
39 lookup
40}
41
42pub fn type_registry() -> TypeRegistry {
52 let mut types = TypeRegistry::new();
53 cel_euclid::register_adam_types(&mut types);
54 types
55}
56
57pub fn build_sheet(source: &str, file_name: &str, renderer: &Renderer) -> BuildOutcome {
66 let mut parser = AdamParser::new(type_registry(), op_lookup());
67 let mut parsed = match parser.parse_str(source) {
68 Ok(p) => p,
69 Err(e) => {
70 let msg = e.format_rustc_style(source, file_name, 1, renderer);
71 return BuildOutcome {
72 sheet_labels: None,
73 error: Some(msg),
74 };
75 }
76 };
77 let labels = labels_from_cell_names(&parsed.sheet, &parsed.cell_names);
78 match parsed.propagate() {
79 Ok(()) => {
80 parsed.clear_changed();
81 BuildOutcome {
82 sheet_labels: Some((parsed, labels)),
83 error: None,
84 }
85 }
86 Err(e) => {
87 let msg = format_adam_error(&e, &parsed, source, file_name, renderer);
88 BuildOutcome {
89 sheet_labels: Some((parsed, labels)),
90 error: Some(msg),
91 }
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
102 fn geometry_sheet_builds_in_opted_in_host() {
103 let outcome = build_sheet(
104 "sheet s { out x := (|p: Point2D| p.x)(Point2D { y: 2.0, x: 1.0 }); out n := min(3.0, 4.0); }",
105 "geometry.adm2",
106 &Renderer::plain(),
107 );
108 assert!(outcome.error.is_none(), "{:?}", outcome.error);
109 let (parsed, _) = outcome.sheet_labels.unwrap();
110 assert_eq!(*parsed.read::<f64>(parsed.cell_names["x"].0).unwrap(), 1.0);
111 for actual in [
112 include_str!("../../cel-euclid/examples/crop_straighten.adm2"),
113 include_str!("../../cel-euclid/examples/pan_zoom.adm2"),
114 ] {
115 let outcome = build_sheet(actual, "geometry.adm2", &Renderer::plain());
116 assert!(outcome.error.is_none(), "{:?}", outcome.error);
117 }
118 }
119
120 const VALID_SOURCE: &str = r#"
121 sheet s {
122 cell a: f64 = 2.0;
123 cell b: f64 = 3.0;
124 cell c: f64;
125 relationship {
126 c := a * b;
127 a := c / b;
128 b := c / a;
129 }
130 }
131 "#;
132
133 #[test]
134 fn build_sheet_valid_source_succeeds_with_no_error() {
135 let outcome = build_sheet(VALID_SOURCE, "test.adm2", &Renderer::styled());
136 assert!(outcome.sheet_labels.is_some());
137 assert!(outcome.error.is_none());
138 }
139
140 #[test]
141 fn build_sheet_parse_error_has_no_sheet_and_formatted_message() {
142 let outcome = build_sheet("sheet s { cell x }", "test.adm2", &Renderer::styled());
143 assert!(outcome.sheet_labels.is_none());
144 let msg = outcome.error.expect("expected a parse error message");
145 assert!(msg.contains("error"), "{msg}");
146 }
147
148 #[test]
149 fn build_sheet_runtime_error_still_returns_sheet_and_message() {
150 let source = "sheet s { cell x: i32 = 0; cell y: i32; relationship { y := 10i32 / x; } }";
151 let outcome = build_sheet(source, "test.adm2", &Renderer::styled());
152 assert!(
153 outcome.sheet_labels.is_some(),
154 "sheet should still be built after a propagate error"
155 );
156 assert!(outcome.error.is_some());
157 }
158
159 #[test]
160 fn build_sheet_plain_renderer_parse_error_has_no_ansi_escape_codes() {
161 let outcome = build_sheet("sheet s { cell x }", "test.adm2", &Renderer::plain());
162 let msg = outcome.error.expect("expected a parse error message");
163 assert!(
164 !msg.contains('\u{1b}'),
165 "expected no ANSI escapes, got: {msg}"
166 );
167 }
168
169 #[test]
170 fn build_sheet_plain_renderer_runtime_error_has_no_ansi_escape_codes() {
171 let source = "sheet s { cell x: i32 = 0; cell y: i32; relationship { y := 10i32 / x; } }";
172 let outcome = build_sheet(source, "test.adm2", &Renderer::plain());
173 let msg = outcome.error.expect("expected a propagate error message");
174 assert!(
175 !msg.contains('\u{1b}'),
176 "expected no ANSI escapes, got: {msg}"
177 );
178 }
179
180 #[test]
181 fn build_sheet_cycle_names_both_relationships_bindings_in_the_backtrace() {
182 let source = "sheet s { cell x: i32 = 0; cell y: i32 = 0; \
187 relationship { x := y + 1i32; } relationship { y := x + 1i32; } }";
188 let outcome = build_sheet(source, "test.adm2", &Renderer::plain());
189 assert!(
190 outcome.sheet_labels.is_some(),
191 "sheet should still be built after a propagate error"
192 );
193 let msg = outcome.error.expect("expected a cycle error message");
194 assert!(msg.contains("x :=") && msg.contains("y :="), "{msg}");
195 }
196}