1use adam_lang::type_registry::TypeShape;
9use adam_rs::{CellId, Error, Sheet};
10pub use annotate_snippets::Renderer;
11use cel_parser::{FormatRustcStyle, SpanContext, SpanLabel};
12use indexmap::IndexMap;
13use std::any::TypeId;
14
15pub type WriteStrFn = Box<dyn Fn(&mut Sheet, &str) -> Result<(), Error>>;
17
18pub struct CellMeta {
20 pub label: String,
22 pub is_bool: bool,
25 pub is_numeric: bool,
29 pub is_integer: bool,
35 pub display: Box<dyn Fn(&Sheet) -> String>,
37 pub write_str: WriteStrFn,
41 #[allow(clippy::type_complexity)]
46 pub range: Option<Box<dyn Fn(&Sheet) -> (f64, f64)>>,
47}
48
49pub struct Labels {
51 pub cells: IndexMap<CellId, CellMeta>,
53}
54
55impl Labels {
56 pub fn new() -> Self {
58 Self {
59 cells: IndexMap::new(),
60 }
61 }
62
63 pub fn add_cell<T>(&mut self, id: CellId, label: &str)
68 where
69 T: std::any::Any + std::fmt::Display + std::str::FromStr + 'static,
70 T::Err: std::fmt::Display,
71 {
72 self.cells.insert(
73 id,
74 CellMeta {
75 label: label.to_owned(),
76 is_bool: TypeId::of::<T>() == TypeId::of::<bool>(),
77 is_numeric: false,
78 is_integer: false,
79 display: Box::new(move |sheet| {
80 sheet
81 .read::<T>(id)
82 .map(|v| format!("{}", v))
83 .unwrap_or_else(|_| "?".to_owned())
84 }),
85 write_str: Box::new(move |sheet, s| {
86 let value = s.parse::<T>().map_err(|e| Error::MethodFailed {
87 error: anyhow::anyhow!("parse error: {}", e),
88 sites: vec![],
89 })?;
90 sheet.write(id, value)
91 }),
92 range: None,
93 },
94 );
95 }
96
97 pub fn add_compound_cell(&mut self, sheet: &Sheet, id: CellId, label: &str) {
121 let formatter = adam_lang::prepare_compound_cell_formatter(sheet, id);
122 self.cells.insert(
123 id,
124 CellMeta {
125 label: label.to_owned(),
126 is_bool: false,
127 is_numeric: false,
128 is_integer: false,
129 display: Box::new(move |sheet| {
130 formatter
131 .as_ref()
132 .ok()
133 .and_then(|format| format(sheet).ok())
134 .unwrap_or_else(|| "?".to_owned())
135 }),
136 write_str: Box::new(|_sheet, _s| {
137 Err(Error::MethodFailed {
138 error: anyhow::anyhow!("editing compound cells is not yet supported"),
139 sites: vec![],
140 })
141 }),
142 range: None,
143 },
144 );
145 }
146}
147
148impl Default for Labels {
149 fn default() -> Self {
151 Self::new()
152 }
153}
154
155pub fn format_rounded(v: f64) -> String {
171 let s = format!("{v:.2}");
172 let s = s.trim_end_matches('0');
173 let s = s.trim_end_matches('.');
174 if s == "-0" {
175 "0".to_string()
176 } else {
177 s.to_string()
178 }
179}
180
181trait ToF64Display {
187 fn to_f64_display(&self) -> f64;
188}
189
190macro_rules! impl_to_f64_display {
191 ($($T:ty),*) => {
192 $(impl ToF64Display for $T {
193 fn to_f64_display(&self) -> f64 {
194 *self as f64
195 }
196 })*
197 };
198}
199impl_to_f64_display!(
200 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
201);
202
203pub fn labels_from_cell_names(
214 sheet: &Sheet,
215 cell_names: &IndexMap<String, (CellId, TypeShape)>,
216) -> Labels {
217 let mut labels = Labels::new();
218 for (name, (id, shape)) in cell_names {
219 let id = *id;
220 let type_id = match shape {
221 TypeShape::Named(type_id) => *type_id,
222 TypeShape::Tuple(_) | TypeShape::Array(_) => {
223 labels.add_compound_cell(sheet, id, name);
224 continue;
225 }
226 TypeShape::Callable(_, _) => continue,
227 };
228 macro_rules! try_numeric_ty {
229 ($T:ty) => {
230 if type_id == TypeId::of::<$T>() {
231 labels.add_cell::<$T>(id, name);
232 mark_numeric::<$T>(&mut labels, sheet, id, true);
233 continue;
234 }
235 };
236 }
237 try_numeric_ty!(i8);
238 try_numeric_ty!(i16);
239 try_numeric_ty!(i32);
240 try_numeric_ty!(i64);
241 try_numeric_ty!(i128);
242 try_numeric_ty!(isize);
243 try_numeric_ty!(u8);
244 try_numeric_ty!(u16);
245 try_numeric_ty!(u32);
246 try_numeric_ty!(u64);
247 try_numeric_ty!(u128);
248 try_numeric_ty!(usize);
249
250 macro_rules! try_numeric_float_ty {
251 ($T:ty) => {
252 if type_id == TypeId::of::<$T>() {
253 labels.add_cell::<$T>(id, name);
254 if let Some(meta) = labels.cells.get_mut(&id) {
255 meta.display = Box::new(move |sheet| {
256 sheet
257 .read::<$T>(id)
258 .map(|v| format_rounded(*v as f64))
259 .unwrap_or_else(|_| "?".to_owned())
260 });
261 }
262 mark_numeric::<$T>(&mut labels, sheet, id, false);
263 continue;
264 }
265 };
266 }
267 try_numeric_float_ty!(f32);
268 try_numeric_float_ty!(f64);
269
270 macro_rules! try_ty {
271 ($T:ty) => {
272 if type_id == TypeId::of::<$T>() {
273 labels.add_cell::<$T>(id, name);
274 continue;
275 }
276 };
277 }
278 try_ty!(bool);
279 try_ty!(String);
280 }
281 labels
282}
283
284fn mark_numeric<T: std::any::Any + Clone + ToF64Display>(
287 labels: &mut Labels,
288 sheet: &Sheet,
289 id: CellId,
290 is_integer: bool,
291) {
292 let Some(meta) = labels.cells.get_mut(&id) else {
293 return;
294 };
295 meta.is_numeric = true;
296 meta.is_integer = is_integer;
297 if matches!(
298 sheet.filter_kind(id),
299 Some(adam_rs::FilterKind::Range { .. })
300 ) {
301 meta.range = Some(Box::new(move |sheet: &Sheet| {
302 sheet
303 .filter_range::<T>(id)
304 .map(|(lo, hi)| (lo.to_f64_display(), hi.to_f64_display()))
305 .unwrap_or((0.0, 0.0))
306 }));
307 }
308}
309
310pub fn format_adam_error(
329 e: &Error,
330 parsed: &adam_lang::ParsedSheet,
331 source: &str,
332 file_name: &str,
333 renderer: &Renderer,
334) -> String {
335 if let Error::MethodFailed { error, .. } = e
337 && error.downcast_ref::<SpanContext>().is_some()
338 {
339 return error.format_rustc_style(source, file_name, 1, renderer);
340 }
341 let located = parsed.locate_error(e);
342 match located.as_slice() {
343 [] => e.to_string(),
344 [(span, _label)] => SpanContext::new(*span).format_rustc_style(
345 &e.to_string(),
346 source,
347 file_name,
348 1,
349 renderer,
350 ),
351 many => {
352 let labels: Vec<SpanLabel> = many
353 .iter()
354 .map(|(span, label)| SpanLabel {
355 span: *span,
356 label: label.clone(),
357 })
358 .collect();
359 cel_parser::format_multi_span(&e.to_string(), &labels, source, file_name, 1, renderer)
360 }
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use adam_rs::Sheet as AdamSheet;
368
369 fn parse(src: &str) -> adam_lang::ParsedSheet {
372 use adam_lang::{AdamParser, TypeRegistry};
373 use cel_parser::OpLookup;
374 AdamParser::new(TypeRegistry::new(), OpLookup::new())
375 .parse_str(src)
376 .unwrap()
377 }
378
379 #[test]
380 fn format_adam_error_invalid_id_falls_back_to_display() {
381 let parsed = parse("sheet s { cell a: i32 = 0; }");
382 let msg = format_adam_error(
383 &Error::InvalidId,
384 &parsed,
385 "sheet s { cell a: i32 = 0; }",
386 "test.adm2",
387 &Renderer::styled(),
388 );
389 assert_eq!(msg, "invalid cell or relationship id");
390 }
391
392 #[test]
393 fn format_adam_error_method_failed_renders_caret_diagnostic() {
394 use cel_parser::{SourceSpan, SpanContext};
395
396 let parsed = parse("sheet s { cell a: i32 = 0; }");
397 let source = "1i32 / 0i32";
398 let span = SourceSpan::new(1, 0, 1, 11);
399 let inner = anyhow::anyhow!("division by zero").context(SpanContext::new(span));
400 let err = Error::MethodFailed {
401 error: inner,
402 sites: vec![],
403 };
404
405 let msg = format_adam_error(&err, &parsed, source, "test.adm2", &Renderer::styled());
406
407 assert!(msg.contains("division by zero"), "{msg}");
408 assert!(msg.contains(source), "{msg}");
409 }
410
411 #[test]
412 fn format_adam_error_plain_renderer_has_no_ansi_escape_codes() {
413 use cel_parser::{SourceSpan, SpanContext};
414
415 let parsed = parse("sheet s { cell a: i32 = 0; }");
416 let source = "1i32 / 0i32";
417 let span = SourceSpan::new(1, 0, 1, 11);
418 let inner = anyhow::anyhow!("division by zero").context(SpanContext::new(span));
419 let err = Error::MethodFailed {
420 error: inner,
421 sites: vec![],
422 };
423
424 let msg = format_adam_error(&err, &parsed, source, "test.adm2", &Renderer::plain());
425
426 assert!(msg.contains("division by zero"), "{msg}");
427 assert!(
428 !msg.contains('\u{1b}'),
429 "expected no ANSI escapes, got: {msg}"
430 );
431 }
432
433 #[test]
434 fn format_adam_error_method_failed_falls_back_to_a_single_located_span_without_a_span_context()
435 {
436 use adam_rs::ErrorSite;
437
438 let source =
439 "sheet s {\n cell a: i32;\n relationship {\n a := 1i32;\n }\n}";
440 let parsed = parse(source);
441 let &(rel_id, idx) = parsed.method_spans.keys().next().expect("one relationship");
442
443 let err = Error::MethodFailed {
444 error: anyhow::anyhow!("boom"),
445 sites: vec![ErrorSite::Method(rel_id, idx)],
446 };
447
448 let msg = format_adam_error(&err, &parsed, source, "test.adm2", &Renderer::plain());
449
450 assert!(msg.contains("boom"), "{msg}");
451 assert!(msg.contains("a := 1i32;"), "{msg}");
452 }
453
454 #[test]
455 fn format_adam_error_cycle_renders_a_multi_span_backtrace() {
456 use adam_lang::{AdamParser, TypeRegistry};
457 use cel_parser::OpLookup;
458 let src = "sheet s { cell x: i32 = 0; cell y: i32 = 0; relationship { x := y + 1i32; } relationship { y := x + 1i32; } }";
459 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
460 let mut parsed = parser.parse_str(src).unwrap();
461 let err = parsed.propagate().unwrap_err();
462 let msg = format_adam_error(&err, &parsed, src, "t.adm2", &Renderer::plain());
463 assert!(msg.contains("cycle"), "{msg}");
464 assert!(msg.contains("x :=") && msg.contains("y :="), "{msg}");
466 }
467
468 #[test]
469 fn format_rounded_trims_trailing_zeros_and_point() {
470 assert_eq!(format_rounded(86.666666666667), "86.67");
471 assert_eq!(format_rounded(300.0), "300");
472 assert_eq!(format_rounded(2.5), "2.5");
473 assert_eq!(format_rounded(0.0), "0");
474 }
475
476 #[test]
477 fn format_rounded_negative_zero_has_no_minus_sign() {
478 assert_eq!(format_rounded(-0.0), "0");
479 assert_eq!(format_rounded(-0.001), "0");
480 }
481
482 #[test]
483 fn labels_from_cell_names_marks_float_cells_as_numeric_but_not_integer() {
484 let mut sheet = AdamSheet::new();
485 let a = sheet.add_cell(2.0_f64);
486
487 let mut cell_names = IndexMap::new();
488 cell_names.insert("a".to_string(), (a, TypeShape::Named(TypeId::of::<f64>())));
489
490 let labels = labels_from_cell_names(&sheet, &cell_names);
491
492 assert!(labels.cells[&a].is_numeric);
493 assert!(!labels.cells[&a].is_integer);
494 }
495
496 #[test]
497 fn labels_from_cell_names_rounds_float_display_to_two_decimals() {
498 let mut sheet = AdamSheet::new();
499 let a = sheet.add_cell(86.666666666667_f64);
500
501 let mut cell_names = IndexMap::new();
502 cell_names.insert("a".to_string(), (a, TypeShape::Named(TypeId::of::<f64>())));
503
504 let labels = labels_from_cell_names(&sheet, &cell_names);
505
506 assert_eq!((labels.cells[&a].display)(&sheet), "86.67");
507 }
508
509 #[test]
510 fn labels_from_cell_names_builds_entries_for_supported_types() {
511 let mut sheet = AdamSheet::new();
512 let a = sheet.add_cell(2.0_f64);
513 let b = sheet.add_cell(3_i32);
514 let c = sheet.add_cell(true);
515 let d = sheet.add_cell("hi".to_string());
516
517 let mut cell_names = IndexMap::new();
518 cell_names.insert("a".to_string(), (a, TypeShape::Named(TypeId::of::<f64>())));
519 cell_names.insert("b".to_string(), (b, TypeShape::Named(TypeId::of::<i32>())));
520 cell_names.insert("c".to_string(), (c, TypeShape::Named(TypeId::of::<bool>())));
521 cell_names.insert(
522 "d".to_string(),
523 (d, TypeShape::Named(TypeId::of::<String>())),
524 );
525
526 let labels = labels_from_cell_names(&sheet, &cell_names);
527
528 assert_eq!(labels.cells.len(), 4);
529 assert_eq!((labels.cells[&a].display)(&sheet), "2");
530 assert_eq!((labels.cells[&b].display)(&sheet), "3");
531 assert_eq!((labels.cells[&c].display)(&sheet), "true");
532 assert_eq!((labels.cells[&d].display)(&sheet), "hi");
533 assert!(!labels.cells[&a].is_bool);
534 assert!(!labels.cells[&b].is_bool);
535 assert!(labels.cells[&c].is_bool);
536 assert!(!labels.cells[&d].is_bool);
537 }
538
539 #[test]
540 fn labels_from_cell_names_includes_tuple_typed_cells() {
541 let sheet = tuple_sheet();
542 let pair = sheet.cell_names["pair"].0;
543
544 let mut cell_names = IndexMap::new();
545 cell_names.insert(
546 "pair".to_string(),
547 (
548 pair,
549 TypeShape::Tuple(vec![
550 TypeShape::Named(TypeId::of::<i32>()),
551 TypeShape::Named(TypeId::of::<f64>()),
552 ]),
553 ),
554 );
555
556 let labels = labels_from_cell_names(&sheet, &cell_names);
557
558 assert_eq!(labels.cells.len(), 1);
559 assert_eq!((labels.cells[&pair].display)(&sheet), "(3, 4.5)");
560 assert!(!labels.cells[&pair].is_bool);
561 }
562
563 #[test]
565 fn labels_display_compound_values_and_reject_edits() {
566 let mut sheet =
567 adam_lang::AdamParser::new(adam_lang::TypeRegistry::new(), cel_parser::OpLookup::new())
568 .parse_str(
569 "sheet s {
570 cell xs: [(i32, bool)] = [(3, true), (4, false)];
571 cell empty: [i32];
572 cell nested: ([(i32, bool)], [i32]) = ([(7, true)], []: [i32]);
573 cell unit: ();
574 cell scalar: i32 = 5;
575 }",
576 )
577 .unwrap();
578 let labels = labels_from_cell_names(&sheet, &sheet.cell_names);
579 assert_eq!(labels.cells.len(), 5);
580 for (name, expected) in [
581 ("xs", "[(3, true), (4, false)]"),
582 ("empty", "[]"),
583 ("nested", "([(7, true)], [])"),
584 ("unit", "()"),
585 ] {
586 let meta = &labels.cells[&sheet.cell_names[name].0];
587 assert_eq!((meta.display)(&sheet), expected);
588 assert!(!meta.is_bool && !meta.is_numeric && !meta.is_integer);
589 let error = (meta.write_str)(&mut sheet, "[]").unwrap_err();
590 assert!(error.to_string().contains("not yet supported"));
591 assert_eq!((meta.display)(&sheet), expected);
592 }
593 let scalar = &labels.cells[&sheet.cell_names["scalar"].0];
594 assert_eq!((scalar.display)(&sheet), "5");
595 (scalar.write_str)(&mut sheet, "9").unwrap();
596 assert_eq!((scalar.display)(&sheet), "9");
597 let id = sheet.cell_names["empty"].0;
598 sheet
599 .write_compound::<cel_runtime::ArrayOf<cel_runtime::Leaf<i32>>>(
600 id,
601 cel_runtime::Array::from_vec(vec![1, 2]),
602 )
603 .unwrap();
604 assert_eq!((labels.cells[&id].display)(&sheet), "[1, 2]");
605 }
606
607 #[test]
608 fn labels_from_cell_names_preserves_declaration_order() {
609 let mut sheet = AdamSheet::new();
610 let z = sheet.add_cell(1_i32);
611 let a = sheet.add_cell(2_i32);
612
613 let mut cell_names = IndexMap::new();
614 cell_names.insert("z".to_string(), (z, TypeShape::Named(TypeId::of::<i32>())));
615 cell_names.insert("a".to_string(), (a, TypeShape::Named(TypeId::of::<i32>())));
616
617 let labels = labels_from_cell_names(&sheet, &cell_names);
618 let ids: Vec<_> = labels.cells.keys().copied().collect();
619 assert_eq!(ids, vec![z, a]);
620 }
621
622 #[test]
623 fn labels_from_cell_names_marks_numeric_cells_and_leaves_range_none_without_a_filter() {
624 let mut sheet = AdamSheet::new();
625 let a = sheet.add_cell(3_i32);
626 let b = sheet.add_cell(true);
627
628 let mut cell_names = IndexMap::new();
629 cell_names.insert("a".to_string(), (a, TypeShape::Named(TypeId::of::<i32>())));
630 cell_names.insert("b".to_string(), (b, TypeShape::Named(TypeId::of::<bool>())));
631
632 let labels = labels_from_cell_names(&sheet, &cell_names);
633
634 assert!(labels.cells[&a].is_numeric);
635 assert!(labels.cells[&a].is_integer);
636 assert!(labels.cells[&a].range.is_none());
637 assert!(!labels.cells[&b].is_numeric);
638 }
639
640 #[test]
641 fn labels_from_cell_names_populates_range_for_a_range_filtered_cell() {
642 use adam_rs::Filter;
643 use std::any::Any;
644
645 let mut sheet = AdamSheet::new();
646 let a = sheet.add_cell(50_i32);
647 let filter = Filter::range(
648 TypeId::of::<i32>(),
649 vec![],
650 vec![],
651 |value, _args| Ok(Box::new(*value.downcast_ref::<i32>().unwrap()) as Box<dyn Any>),
652 |_args| {
653 Some((
654 Box::new(0i32) as Box<dyn Any>,
655 Box::new(100i32) as Box<dyn Any>,
656 ))
657 },
658 );
659 sheet.add_filter(a, filter).unwrap();
660
661 let mut cell_names = IndexMap::new();
662 cell_names.insert("a".to_string(), (a, TypeShape::Named(TypeId::of::<i32>())));
663
664 let labels = labels_from_cell_names(&sheet, &cell_names);
665
666 let range_fn = labels.cells[&a].range.as_ref().expect("range populated");
667 assert_eq!(range_fn(&sheet), (0.0, 100.0));
668 }
669
670 fn sheet_with_one_cell() -> (AdamSheet, Labels) {
671 let mut sheet = AdamSheet::new();
672 let mut labels = Labels::new();
673 let a = sheet.add_cell(2.0_f64);
674 labels.add_cell::<f64>(a, "a");
675 (sheet, labels)
676 }
677
678 #[test]
679 fn display_closure_returns_value_string() {
680 let (sheet, labels) = sheet_with_one_cell();
681 let a_id = *labels.cells.keys().next().unwrap();
682 let display = &labels.cells[&a_id].display;
683 assert_eq!(display(&sheet), "2");
684 }
685
686 #[test]
687 fn write_str_closure_parses_and_writes() {
688 let (mut sheet, labels) = sheet_with_one_cell();
689 let a_id = *labels.cells.keys().next().unwrap();
690 assert!((labels.cells[&a_id].write_str)(&mut sheet, "5.0").is_ok());
691 let display = &labels.cells[&a_id].display;
692 assert_eq!(display(&sheet), "5");
693 }
694
695 #[test]
696 fn add_tuple_cell_display_returns_rust_debug_formatted_string() {
697 let sheet = tuple_sheet();
698 let cell_id = sheet.cell_names["pair"].0;
699 let mut labels = Labels::new();
700 labels.add_compound_cell(&sheet, cell_id, "pair");
701 let meta = labels.cells.get(&cell_id).unwrap();
702 assert_eq!((meta.display)(&sheet), "(3, 4.5)");
703 }
704
705 #[test]
706 fn add_tuple_cell_write_str_always_errs_without_mutating_the_sheet() {
707 let mut sheet = tuple_sheet();
708 let cell_id = sheet.cell_names["pair"].0;
709 let mut labels = Labels::new();
710 labels.add_compound_cell(&sheet, cell_id, "pair");
711 let meta = labels.cells.get(&cell_id).unwrap();
712 let before = adam_lang::debug_compound_cell(&sheet, cell_id).unwrap();
713 let result = (meta.write_str)(&mut sheet, "(1, 2.0)");
714 assert!(result.is_err());
715 let after = adam_lang::debug_compound_cell(&sheet, cell_id).unwrap();
716 assert_eq!(before, after);
717 }
718
719 fn tuple_sheet() -> adam_lang::ParsedSheet {
721 adam_lang::AdamParser::new(adam_lang::TypeRegistry::new(), cel_parser::OpLookup::new())
722 .parse_str("sheet s { cell pair: (i32, f64) = (3, 4.5); }")
723 .unwrap()
724 }
725
726 #[test]
728 fn compound_label_reports_unavailable_storage_without_panicking() {
729 let mut sheet = AdamSheet::new();
730 let id = sheet.add_cell(7i32);
731 let mut labels = Labels::new();
732 labels.add_compound_cell(&sheet, id, "unavailable");
733 let meta = &labels.cells[&id];
734 assert_eq!((meta.display)(&sheet), "?");
735 assert!((meta.write_str)(&mut sheet, "9").is_err());
736 assert_eq!(*sheet.read::<i32>(id).unwrap(), 7);
737 }
738}