1use anyhow::{Result, bail};
10
11use std::collections::HashMap;
12use std::sync::{Mutex, OnceLock};
13
14use crate::{TypeDescriptor, Value};
15
16pub const MAX_GENERIC_DEPTH: usize = 8;
21
22#[derive(Debug, Clone, Copy)]
24pub struct Field {
25 pub name: &'static str,
27 pub ty: TypeTag,
29 pub docs: &'static str,
32 pub optional: bool,
36}
37
38#[derive(Clone, Copy)]
48pub enum TypeTag {
49 String,
50 Int,
51 Float,
52 Bool,
53 List,
54 Map,
55 Pipe,
56 Handle,
57 Duration,
58 Path,
59 Semaphore,
60 Permit,
61 Any,
65 Custom(&'static TypeDescriptor),
66 ListOf(&'static TypeTag),
67 Record(&'static [Field]),
68 MapOf(&'static TypeTag),
69}
70
71pub const ANY_TAG: TypeTag = TypeTag::Any;
74
75pub const MAP_TAG: TypeTag = TypeTag::Map;
78
79pub const LIST_TAG: TypeTag = TypeTag::List;
81impl std::fmt::Debug for TypeTag {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 TypeTag::Custom(descriptor) => {
85 write!(f, "Custom({})", descriptor.name)
86 }
87 TypeTag::ListOf(element) => write!(f, "ListOf({element:?})"),
88 TypeTag::MapOf(values) => write!(f, "MapOf({values:?})"),
89 TypeTag::Record(fields) => {
90 let names: Vec<&str> = fields.iter().map(|field| field.name).collect();
91 write!(f, "Record({})", names.join(", "))
92 }
93 tag => write!(f, "{}", tag.name()),
94 }
95 }
96}
97
98impl std::fmt::Display for TypeTag {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str(self.name())
101 }
102}
103impl TypeTag {
104 pub fn name(&self) -> &'static str {
108 match self {
109 TypeTag::String => "STRING",
110 TypeTag::Int => "INT",
111 TypeTag::Float => "FLOAT",
112 TypeTag::Bool => "BOOL",
113 TypeTag::List => "LIST",
114 TypeTag::Map => "MAP",
115 TypeTag::Pipe => "PIPE",
116 TypeTag::Handle => "HANDLE",
117 TypeTag::Duration => "DURATION",
118 TypeTag::Path => "PATH",
119 TypeTag::Semaphore => "SEMAPHORE",
120 TypeTag::Permit => "PERMIT",
121 TypeTag::Any => "ANY",
122 TypeTag::Custom(descriptor) => descriptor.name,
123 TypeTag::ListOf(_) => "LIST",
124 TypeTag::MapOf(_) => "MAP",
125 TypeTag::Record(_) => "MAP",
126 }
127 }
128
129 pub fn builtin(name: &str) -> Option<TypeTag> {
133 match name {
134 "STRING" => Some(TypeTag::String),
135 "INT" => Some(TypeTag::Int),
136 "FLOAT" => Some(TypeTag::Float),
137 "BOOL" => Some(TypeTag::Bool),
138 "LIST" => Some(TypeTag::List),
139 "MAP" => Some(TypeTag::Map),
140 "PIPE" => Some(TypeTag::Pipe),
141 "HANDLE" => Some(TypeTag::Handle),
142 "DURATION" => Some(TypeTag::Duration),
143 "PATH" => Some(TypeTag::Path),
144 "SEMAPHORE" => Some(TypeTag::Semaphore),
145 "PERMIT" => Some(TypeTag::Permit),
146 "ANY" => Some(TypeTag::Any),
147 _ => None,
148 }
149 }
150
151 pub fn fields(&self) -> Option<&'static [Field]> {
153 match self {
154 TypeTag::Record(fields) => Some(fields),
155 _ => None,
156 }
157 }
158}
159
160pub fn render_structural(tag: &TypeTag) -> String {
165 match tag {
166 TypeTag::ListOf(inner) => format!("LIST<{}>", render_structural(inner)),
167 TypeTag::MapOf(values) => format!("MAP<{}>", render_structural(values)),
168 TypeTag::Record(fields) => {
169 let field_list = fields
170 .iter()
171 .map(|field| {
172 format!(
173 "{}{}: {}",
174 field.name,
175 if field.optional { "?" } else { "" },
176 render_structural(&field.ty)
177 )
178 })
179 .collect::<Vec<_>>()
180 .join(", ");
181 format!("MAP<{field_list}>")
182 }
183 tag => tag.name().to_string(),
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum Spelled {
195 Named(String),
196 Generic {
197 name: String,
198 args: Vec<SpelledField>,
199 },
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct SpelledField {
206 pub name: Option<String>,
207 pub optional: bool,
208 pub ty: Spelled,
209}
210
211pub fn canonicalize_spelling(s: &str) -> String {
219 let stripped: String = s.chars().filter(|c| !c.is_whitespace()).collect();
220 match parse_spelling(&stripped) {
221 Ok(spelled) => render_spelled(&spelled),
222 Err(_) => s.trim().to_string(),
223 }
224}
225
226pub fn render_spelled(spelled: &Spelled) -> String {
230 match spelled {
231 Spelled::Named(name) => name.clone(),
232 Spelled::Generic { name, args } => {
233 let rendered: Vec<String> = args
234 .iter()
235 .map(|field| match &field.name {
236 Some(field_name) => format!(
237 "{}{}: {}",
238 field_name,
239 if field.optional { "?" } else { "" },
240 render_spelled(&field.ty)
241 ),
242 None => render_spelled(&field.ty),
243 })
244 .collect();
245 format!("{name}<{}>", rendered.join(", "))
246 }
247 }
248}
249
250pub fn parse_spelling(s: &str) -> Result<Spelled, String> {
255 let text: String = s.chars().filter(|c| !c.is_whitespace()).collect();
256 let (spelled, rest) = parse_spelled_one(&text, 0)?;
257 if !rest.is_empty() {
258 return Err(format!(
259 "unexpected trailing text in type spelling: {rest:?}"
260 ));
261 }
262 Ok(spelled)
263}
264
265fn parse_spelled_one(text: &str, depth: usize) -> Result<(Spelled, &str), String> {
266 if depth > MAX_GENERIC_DEPTH {
267 return Err(format!(
268 "type spelling exceeds maximum generic depth of {MAX_GENERIC_DEPTH}: {text:?}"
269 ));
270 }
271 let mut chars = text.char_indices();
272 let Some((_, first)) = chars.next() else {
273 return Err("expected type name, found end of spelling".to_string());
274 };
275 if !first.is_ascii_uppercase() {
276 return Err(format!("expected uppercase type name, found {first:?}"));
277 }
278 let mut end = first.len_utf8();
279 for (idx, ch) in chars {
280 if ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_' {
281 end = idx + ch.len_utf8();
282 } else {
283 break;
284 }
285 }
286 let (name, mut rest) = text.split_at(end);
287 let name = name.to_string();
288 if !rest.starts_with('<') {
289 return Ok((Spelled::Named(name), rest));
290 }
291 rest = &rest[1..];
292 let mut args = Vec::new();
293 loop {
294 let (field_name, optional, after_name) = parse_spelled_field_name(rest);
295 let (arg, after_arg) = parse_spelled_one(after_name, depth + 1)?;
296 args.push(SpelledField {
297 name: field_name,
298 optional,
299 ty: arg,
300 });
301 rest = after_arg;
302 if let Some(tail) = rest.strip_prefix(',') {
303 rest = tail;
304 continue;
305 }
306 if let Some(tail) = rest.strip_prefix('>') {
307 rest = tail;
308 break;
309 }
310 return Err(format!(
311 "expected ',' or '>' in type spelling, found {rest:?}"
312 ));
313 }
314 Ok((Spelled::Generic { name, args }, rest))
315}
316
317fn parse_spelled_field_name(text: &str) -> (Option<String>, bool, &str) {
322 let mut chars = text.char_indices();
323 let Some((_, first)) = chars.next() else {
324 return (None, false, text);
325 };
326 if !first.is_ascii_lowercase() {
327 return (None, false, text);
328 };
329 let mut end = first.len_utf8();
330 for (idx, ch) in chars {
331 if ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' {
332 end = idx + ch.len_utf8();
333 } else {
334 break;
335 }
336 }
337 let (candidate, rest) = text.split_at(end);
338 let (optional, rest) = match rest.strip_prefix('?') {
339 Some(after) => (true, after),
340 None => (false, rest),
341 };
342 match rest.strip_prefix(':') {
343 Some(after) => (Some(candidate.to_string()), optional, after),
344 None => (None, false, text),
345 }
346}
347
348static COMPOSED_TAGS: OnceLock<Mutex<HashMap<String, &'static TypeTag>>> = OnceLock::new();
353
354fn composed_table() -> &'static Mutex<HashMap<String, &'static TypeTag>> {
355 COMPOSED_TAGS.get_or_init(|| Mutex::new(HashMap::new()))
356}
357
358pub fn lookup_composed(canonical: &str) -> Option<&'static TypeTag> {
361 composed_table()
362 .lock()
363 .unwrap_or_else(|poisoned| poisoned.into_inner())
364 .get(canonical)
365 .copied()
366}
367
368#[allow(clippy::disallowed_methods)]
375pub fn intern_composed(canonical: String, tag: TypeTag) -> &'static TypeTag {
376 let mut table = composed_table()
377 .lock()
378 .unwrap_or_else(|poisoned| poisoned.into_inner());
379 if let Some(hit) = table.get(&canonical).copied() {
380 return hit;
381 }
382 let leaked: &'static TypeTag = Box::leak(Box::new(tag));
383 table.insert(canonical, leaked);
384 leaked
385}
386
387pub fn check_value(tag: &TypeTag, value: &Value) -> Result<()> {
391 check_value_at(tag, value, "$")
392}
393
394pub fn check_value_at(tag: &TypeTag, value: &Value, path: &str) -> Result<()> {
400 match tag {
401 TypeTag::Any => Ok(()),
402 TypeTag::String if value.as_str().is_some() => Ok(()),
403 TypeTag::Int if value.as_i64().is_some() => Ok(()),
404 TypeTag::Float if value.as_f64().is_some() => Ok(()),
405 TypeTag::Bool if value.as_bool().is_some() => Ok(()),
406 TypeTag::Pipe if value.as_pipe_handle().is_some() => Ok(()),
407 TypeTag::Handle if value.as_handle().is_some() => Ok(()),
408 TypeTag::Duration if value.as_duration().is_some() => Ok(()),
409 TypeTag::Path if value.as_path().is_some() => Ok(()),
410 TypeTag::Semaphore if value.as_semaphore().is_some() => Ok(()),
411 TypeTag::Permit if value.as_permit().is_some() => Ok(()),
412 TypeTag::List => match value.as_list() {
413 Some(_) => Ok(()),
414 None => bail!("{path}: expected LIST, got {}", value.type_name()),
415 },
416 TypeTag::Map => match value.as_map() {
417 Some(_) => Ok(()),
418 None => bail!("{path}: expected MAP, got {}", value.type_name()),
419 },
420 TypeTag::ListOf(element) => match value.as_list() {
421 Some(items) => {
422 for (idx, item) in items.iter().enumerate() {
423 check_value_at(element, item, &format!("{path}[{idx}]"))?;
424 }
425 Ok(())
426 }
427 None => bail!("{path}: expected LIST, got {}", value.type_name()),
428 },
429 TypeTag::MapOf(values) => match value.as_map() {
430 Some(map) => {
431 for (key, item) in map.iter() {
432 check_value_at(values, item, &format!("{path}.{key}"))?;
433 }
434 Ok(())
435 }
436 None => bail!(
437 "{path}: expected {}, got {}",
438 render_structural(tag),
439 value.type_name()
440 ),
441 },
442 TypeTag::Record(fields) => match value.as_map() {
443 Some(map) => {
444 for field in *fields {
445 match map.get(field.name) {
446 Some(item) => {
447 check_value_at(&field.ty, item, &format!("{path}.{}", field.name))?;
448 }
449 None if field.optional => {}
450 None => bail!(
451 "{path}: missing field '{}'; expected fields: {}",
452 field.name,
453 field_list(fields),
454 ),
455 }
456 }
457 for key in map.keys() {
458 if !fields.iter().any(|field| field.name == key) {
459 bail!(
460 "{path}: unknown field '{key}'; expected fields: {}",
461 field_list(fields),
462 );
463 }
464 }
465 Ok(())
466 }
467 None => bail!("{path}: expected MAP, got {}", value.type_name()),
468 },
469 TypeTag::Custom(descriptor) => {
470 if value.type_name() == descriptor.name {
471 Ok(())
472 } else {
473 bail!(
474 "{path}: expected {}, got {}",
475 descriptor.name,
476 value.type_name(),
477 )
478 }
479 }
480 _ => bail!("{path}: expected {}, got {}", tag.name(), value.type_name()),
481 }
482}
483
484fn field_list(fields: &[Field]) -> String {
485 fields
486 .iter()
487 .map(|field| field.name)
488 .collect::<Vec<_>>()
489 .join(", ")
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use std::collections::BTreeMap;
496
497 fn map(entries: &[(&str, Value)]) -> Value {
498 Value::map(
499 entries
500 .iter()
501 .map(|(k, v)| ((*k).to_string(), v.clone()))
502 .collect::<BTreeMap<_, _>>(),
503 )
504 }
505
506 #[test]
507 fn builtin_round_trips_through_names() {
508 for name in [
509 "STRING",
510 "INT",
511 "FLOAT",
512 "BOOL",
513 "LIST",
514 "MAP",
515 "PIPE",
516 "HANDLE",
517 "DURATION",
518 "PATH",
519 "SEMAPHORE",
520 "PERMIT",
521 ] {
522 let tag = TypeTag::builtin(name).expect("builtin");
523 assert_eq!(tag.name(), name);
524 }
525 assert!(TypeTag::builtin("NOPE").is_none());
526 assert!(TypeTag::builtin("string").is_none());
527 }
528
529 #[test]
530 fn scalars_conform_by_discriminant() {
531 assert!(check_value(&TypeTag::String, &Value::string("x".to_string())).is_ok());
532 assert!(check_value(&TypeTag::Int, &Value::int(1)).is_ok());
533 assert!(check_value(&TypeTag::String, &Value::int(1)).is_err());
534 assert!(check_value(&TypeTag::Int, &Value::float(3.0)).is_err());
536 }
537
538 #[test]
539 fn record_rejects_missing_and_extra_fields() {
540 static FIELDS: &[Field] = &[
541 Field {
542 name: "a",
543 ty: TypeTag::String,
544 docs: "",
545 optional: false,
546 },
547 Field {
548 name: "b",
549 ty: TypeTag::Int,
550 docs: "",
551 optional: false,
552 },
553 ];
554 let tag = TypeTag::Record(FIELDS);
555 let good = map(&[("a", Value::string("x".to_string())), ("b", Value::int(1))]);
556 assert!(check_value(&tag, &good).is_ok());
557 let missing = map(&[("a", Value::string("x".to_string()))]);
558 let err = check_value(&tag, &missing).expect_err("missing must fail");
559 assert!(format!("{err:#}").contains('b'), "names the field: {err:#}");
560 let extra = map(&[
561 ("a", Value::string("x".to_string())),
562 ("b", Value::int(1)),
563 ("c", Value::int(2)),
564 ]);
565 let err = check_value(&tag, &extra).expect_err("extra must fail");
566 assert!(format!("{err:#}").contains('c'), "names the field: {err:#}");
567 let mistyped = map(&[
568 ("a", Value::string("x".to_string())),
569 ("b", Value::string("nope".to_string())),
570 ]);
571 assert!(check_value(&tag, &mistyped).is_err());
572 }
573
574 #[test]
575 fn record_optional_fields_may_be_absent() {
576 static FIELDS: &[Field] = &[
579 Field {
580 name: "held",
581 ty: TypeTag::Int,
582 docs: "",
583 optional: false,
584 },
585 Field {
586 name: "permit",
587 ty: TypeTag::String,
588 docs: "",
589 optional: true,
590 },
591 ];
592 let tag = TypeTag::Record(FIELDS);
593 let full = map(&[
594 ("held", Value::int(1)),
595 ("permit", Value::string("p".to_string())),
596 ]);
597 assert!(check_value(&tag, &full).is_ok());
598 let missing_optional = map(&[("held", Value::int(0))]);
599 assert!(check_value(&tag, &missing_optional).is_ok());
600 let missing_required = map(&[("permit", Value::string("p".to_string()))]);
601 let err = check_value(&tag, &missing_required).expect_err("missing required must fail");
602 assert!(
603 format!("{err:#}").contains("held"),
604 "names the field: {err:#}"
605 );
606 let mistyped_optional = map(&[("held", Value::int(1)), ("permit", Value::int(2))]);
607 assert!(check_value(&tag, &mistyped_optional).is_err());
608 }
609
610 #[test]
611 fn list_of_checks_elements_with_index() {
612 let tag = TypeTag::ListOf(&TypeTag::Int);
613 let good = Value::list(vec![Value::int(1), Value::int(2)]);
614 assert!(check_value(&tag, &good).is_ok());
615 let bad = Value::list(vec![Value::int(1), Value::string("x".to_string())]);
616 let err = check_value(&tag, &bad).expect_err("bad element must fail");
617 assert!(
618 format!("{err:#}").contains("[1]"),
619 "names the index: {err:#}"
620 );
621 }
622
623 #[test]
624 fn canonicalization_unifies_input_spacing() {
625 assert_eq!(canonicalize_spelling("LIST<MAP>"), "LIST<MAP>");
628 assert_eq!(canonicalize_spelling("LIST< MAP >"), "LIST<MAP>");
629 assert_eq!(
630 canonicalize_spelling("MAP<name:STRING,age:INT>"),
631 "MAP<name: STRING, age: INT>"
632 );
633 assert_eq!(
634 canonicalize_spelling("MAP<name : STRING>"),
635 "MAP<name: STRING>"
636 );
637 }
638
639 #[test]
640 fn spellings_parse_to_shapes() {
641 assert_eq!(
642 parse_spelling("LIST<MAP>").expect("parses"),
643 Spelled::Generic {
644 name: "LIST".to_string(),
645 args: vec![SpelledField {
646 name: None,
647 optional: false,
648 ty: Spelled::Named("MAP".to_string()),
649 }],
650 }
651 );
652 assert_eq!(
653 parse_spelling("MAP<name: STRING>").expect("parses"),
654 Spelled::Generic {
655 name: "MAP".to_string(),
656 args: vec![SpelledField {
657 name: Some("name".to_string()),
658 optional: false,
659 ty: Spelled::Named("STRING".to_string()),
660 }],
661 }
662 );
663 assert!(parse_spelling("LIST<A, B>").is_ok());
666 assert!(parse_spelling("MAP<STRING>").is_ok());
667 }
668
669 #[test]
670 fn optional_field_spelling_parses_and_renders() {
671 assert_eq!(
674 parse_spelling("MAP<permit?: PERMIT>").expect("parses"),
675 Spelled::Generic {
676 name: "MAP".to_string(),
677 args: vec![SpelledField {
678 name: Some("permit".to_string()),
679 optional: true,
680 ty: Spelled::Named("PERMIT".to_string()),
681 }],
682 }
683 );
684 assert_eq!(
685 canonicalize_spelling("MAP<permit ? : PERMIT>"),
686 "MAP<permit?: PERMIT>"
687 );
688 }
689
690 #[test]
691 fn unknown_map_spelling_parses_and_renders() {
692 assert_eq!(
695 parse_spelling("MAP<ANY>").expect("parses"),
696 Spelled::Generic {
697 name: "MAP".to_string(),
698 args: vec![SpelledField {
699 name: None,
700 optional: false,
701 ty: Spelled::Named("ANY".to_string()),
702 }],
703 }
704 );
705 assert_eq!(canonicalize_spelling("MAP< ANY >"), "MAP<ANY>");
706 assert_eq!(
707 render_structural(&TypeTag::MapOf(&TypeTag::Any)),
708 "MAP<ANY>"
709 );
710 }
711
712 #[test]
713 fn map_of_checks_values_with_paths() {
714 let tag = TypeTag::MapOf(&TypeTag::Int);
717 let good = map(&[("a", Value::int(1))]);
718 assert!(check_value(&tag, &good).is_ok());
719 let bad = map(&[("a", Value::string("x".to_string()))]);
720 let err = check_value(&tag, &bad).expect_err("bad value must fail");
721 assert!(format!("{err:#}").contains(".a"), "names the key: {err:#}");
722 let not_map = Value::list(vec![Value::int(1)]);
723 let err = check_value(&tag, ¬_map).expect_err("non-map must fail");
724 assert!(format!("{err:#}").contains("MAP<INT>"), "{err:#}");
725 }
726
727 #[test]
728 fn spellings_reject_malformed_input() {
729 for bad in [
730 "",
731 "LIST<>",
732 "LIST<A,>",
733 "MAP<name:>",
734 "LIST<A",
735 "list<MAP>",
736 "LIST<A>>",
737 ] {
738 assert!(parse_spelling(bad).is_err(), "{bad:?} must fail");
739 }
740 let mut deep = "INT".to_string();
742 for _ in 0..MAX_GENERIC_DEPTH + 1 {
743 deep = format!("LIST<{deep}>");
744 }
745 let err = parse_spelling(&deep).expect_err("over-deep spelling must fail");
746 assert!(err.contains("maximum generic depth"), "{err}");
747 }
748
749 #[test]
750 fn identical_spellings_share_one_interned_pointer() {
751 let first = intern_composed("LIST<MAP>".to_string(), TypeTag::ListOf(&TypeTag::Map));
752 let second = intern_composed("LIST<MAP>".to_string(), TypeTag::ListOf(&TypeTag::Map));
753 assert!(
754 std::ptr::eq(first, second),
755 "same canonical spelling interns once"
756 );
757 }
758
759 #[test]
760 fn structural_rendering_names_contents() {
761 static FIELDS: &[Field] = &[Field {
762 name: "name",
763 ty: TypeTag::String,
764 docs: "",
765 optional: false,
766 }];
767 static RECORD: TypeTag = TypeTag::Record(FIELDS);
768 static LIST: TypeTag = TypeTag::ListOf(&RECORD);
769 assert_eq!(render_structural(&LIST), "LIST<MAP<name: STRING>>");
770 assert_eq!(render_structural(&TypeTag::Int), "INT");
771 }
772}