1use std::fmt;
2
3use crate::atomic::Atomic;
4use crate::union::Type;
5
6fn write_joined<T: fmt::Display>(
9 f: &mut fmt::Formatter<'_>,
10 items: impl IntoIterator<Item = T>,
11 sep: &str,
12) -> fmt::Result {
13 for (i, item) in items.into_iter().enumerate() {
14 if i > 0 {
15 f.write_str(sep)?;
16 }
17 write!(f, "{item}")?;
18 }
19 Ok(())
20}
21
22fn is_exactly_mixed(t: &Type) -> bool {
29 matches!(t.types.as_slice(), [Atomic::TMixed])
30}
31
32fn write_param_types(f: &mut fmt::Formatter<'_>, params: &[crate::atomic::FnParam]) -> fmt::Result {
35 for (i, p) in params.iter().enumerate() {
36 if i > 0 {
37 f.write_str(", ")?;
38 }
39 match &p.ty {
40 Some(ty) => write!(f, "{ty}")?,
41 None => f.write_str("mixed")?,
42 }
43 }
44 Ok(())
45}
46
47fn iterable_span(types: &[Atomic]) -> Option<(usize, usize, String)> {
59 for (ai, a) in types.iter().enumerate() {
60 let Atomic::TArray { key, value } = a else {
61 continue;
62 };
63 for (ti, t) in types.iter().enumerate() {
64 if ti == ai {
65 continue;
66 }
67 let Atomic::TNamedObject { fqcn, type_params } = t else {
68 continue;
69 };
70 if fqcn.as_ref() != "Traversable" {
71 continue;
72 }
73 let is_default_key_value =
74 is_exactly_mixed(value) && (is_exactly_mixed(key) || key.is_array_key());
75 let matches = if type_params.is_empty() {
76 is_default_key_value
81 } else {
82 type_params.len() == 2
83 && &type_params[0] == key.as_ref()
84 && &type_params[1] == value.as_ref()
85 };
86 if !matches {
87 continue;
88 }
89 let rendered = if is_default_key_value {
90 "iterable".to_string()
91 } else {
92 format!("iterable<{key}, {value}>")
93 };
94 return Some((ai.min(ti), ai.max(ti), rendered));
95 }
96 }
97 None
98}
99
100impl fmt::Display for Type {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 if self.types.is_empty() {
103 return write!(f, "never");
104 }
105 if let Some((lo, hi, rendered)) = iterable_span(&self.types) {
106 let mut first = true;
107 for (i, t) in self.types.iter().enumerate() {
108 if i == hi {
109 continue;
110 }
111 if !first {
112 f.write_str("|")?;
113 }
114 first = false;
115 if i == lo {
116 f.write_str(&rendered)?;
117 } else {
118 write!(f, "{t}")?;
119 }
120 }
121 return Ok(());
122 }
123 write_joined(f, self.types.iter(), "|")
124 }
125}
126
127pub struct LiteralText<'a>(pub &'a str);
129
130impl fmt::Display for LiteralText<'_> {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 if !self
133 .0
134 .chars()
135 .any(|c| ('\u{F700}'..='\u{F7FF}').contains(&c))
136 {
137 return f.write_str(self.0);
138 }
139 for c in self.0.chars() {
140 match u32::from(c) {
141 b @ 0xF700..=0xF7FF => write!(f, "\\x{:02x}", b - 0xF700)?,
143 _ => write!(f, "{c}")?,
144 }
145 }
146 Ok(())
147 }
148}
149
150impl fmt::Display for Atomic {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 match self {
153 Atomic::TString => write!(f, "string"),
154 Atomic::TLiteralString(s) => write!(f, "\"{}\"", LiteralText(s)),
155 Atomic::TCallableString => write!(f, "callable-string"),
156 Atomic::TClassString(None) => write!(f, "class-string"),
157 Atomic::TClassString(Some(cls)) => write!(f, "class-string<{cls}>"),
158 Atomic::TNonEmptyString => write!(f, "non-empty-string"),
159 Atomic::TNumericString => write!(f, "numeric-string"),
160
161 Atomic::TInt => write!(f, "int"),
162 Atomic::TLiteralInt(n) => write!(f, "{n}"),
163 Atomic::TIntRange { min, max } => match (min, max) {
164 (None, None) => write!(f, "int"),
165 (lo, hi) => {
166 let lo = lo.map_or_else(|| "min".to_string(), |n| n.to_string());
167 let hi = hi.map_or_else(|| "max".to_string(), |n| n.to_string());
168 write!(f, "int<{lo}, {hi}>")
169 }
170 },
171 Atomic::TPositiveInt => write!(f, "positive-int"),
172 Atomic::TNegativeInt => write!(f, "negative-int"),
173 Atomic::TNonNegativeInt => write!(f, "non-negative-int"),
174
175 Atomic::TFloat | Atomic::TIntegralFloat => write!(f, "float"),
176 Atomic::TLiteralFloat(high, low) => {
177 let bits = ((*high as u64) << 32) | (*low as u32 as u64);
178 let value = f64::from_bits(bits);
179 write!(f, "{value}")
180 }
181
182 Atomic::TBool => write!(f, "bool"),
183 Atomic::TTrue => write!(f, "true"),
184 Atomic::TFalse => write!(f, "false"),
185
186 Atomic::TNull => write!(f, "null"),
187 Atomic::TVoid => write!(f, "void"),
188 Atomic::TNever => write!(f, "never"),
189 Atomic::TMixed => write!(f, "mixed"),
190 Atomic::TScalar => write!(f, "scalar"),
191 Atomic::TNumeric => write!(f, "numeric"),
192
193 Atomic::TObject => write!(f, "object"),
194 Atomic::TNamedObject { fqcn, type_params } => {
195 if type_params.is_empty() || type_params.iter().all(is_exactly_mixed) {
200 write!(f, "{fqcn}")
201 } else {
202 write!(f, "{fqcn}<")?;
203 write_joined(f, type_params.iter(), ", ")?;
204 f.write_str(">")
205 }
206 }
207 Atomic::TStaticObject { fqcn } => write!(f, "static({fqcn})"),
208 Atomic::TSelf { fqcn } => write!(f, "self({fqcn})"),
209 Atomic::TParent { fqcn } => write!(f, "parent({fqcn})"),
210
211 Atomic::TCallable {
212 params,
213 return_type,
214 is_pure,
215 } => {
216 if *is_pure {
217 f.write_str("pure-")?;
218 }
219 match (params, return_type) {
220 (None, None) => f.write_str("callable"),
221 (Some(params), return_type) => {
222 f.write_str("callable(")?;
223 write_param_types(f, params)?;
224 match return_type {
225 Some(r) => write!(f, "): {r}"),
226 None => f.write_str("): mixed"),
227 }
228 }
229 (None, Some(ret)) => write!(f, "callable(): {ret}"),
230 }
231 }
232 Atomic::TClosure { data } => {
233 if data.is_pure {
234 f.write_str("pure-")?;
235 }
236 f.write_str("Closure(")?;
237 write_param_types(f, &data.params)?;
238 write!(f, "): {}", data.return_type)
239 }
240
241 Atomic::TArray { key, value } => {
242 if is_exactly_mixed(value) && (is_exactly_mixed(key) || key.is_array_key()) {
247 write!(f, "array")
248 } else {
249 write!(f, "array<{key}, {value}>")
250 }
251 }
252 Atomic::TList { value } => {
253 if is_exactly_mixed(value) {
254 write!(f, "list")
255 } else {
256 write!(f, "list<{value}>")
257 }
258 }
259 Atomic::TNonEmptyArray { key, value } => {
260 if is_exactly_mixed(value) && (is_exactly_mixed(key) || key.is_array_key()) {
261 write!(f, "non-empty-array")
262 } else {
263 write!(f, "non-empty-array<{key}, {value}>")
264 }
265 }
266 Atomic::TNonEmptyList { value } => {
267 if is_exactly_mixed(value) {
268 write!(f, "non-empty-list")
269 } else {
270 write!(f, "non-empty-list<{value}>")
271 }
272 }
273 Atomic::TObjectShape { properties } => {
274 f.write_str("object{")?;
275 for (i, (k, v)) in properties.iter().enumerate() {
276 if i > 0 {
277 f.write_str(", ")?;
278 }
279 match k {
280 crate::atomic::ArrayKey::String(s) => f.write_str(s)?,
281 crate::atomic::ArrayKey::Int(n) => write!(f, "{n}")?,
282 }
283 if v.optional {
284 f.write_str("?")?;
285 }
286 write!(f, ": {}", v.ty)?;
287 }
288 f.write_str("}")
289 }
290 Atomic::TKeyedArray { properties, .. } => {
291 f.write_str("array{")?;
292 for (i, (k, v)) in properties.iter().enumerate() {
293 if i > 0 {
294 f.write_str(", ")?;
295 }
296 match k {
297 crate::atomic::ArrayKey::String(s) => write!(f, "'{}'", LiteralText(s))?,
298 crate::atomic::ArrayKey::Int(n) => write!(f, "{n}")?,
299 }
300 if v.optional {
301 f.write_str("?")?;
302 }
303 write!(f, ": {}", v.ty)?;
304 }
305 f.write_str("}")
306 }
307
308 Atomic::TTemplateParam { name, .. } => write!(f, "{name}"),
309 Atomic::TKeyOf { target } => write!(f, "key-of<{target}>"),
310 Atomic::TValueOf { target } => write!(f, "value-of<{target}>"),
311 Atomic::TConditional { data } => {
312 let (subject, if_true, if_false) = (&data.subject, &data.if_true, &data.if_false);
313 match &data.param_name {
314 Some(name) => write!(f, "(${name} is {subject} ? {if_true} : {if_false})"),
315 None => write!(f, "({subject} is ? {if_true} : {if_false})"),
316 }
317 }
318
319 Atomic::TInterfaceString(None) => write!(f, "interface-string"),
320 Atomic::TInterfaceString(Some(iface)) => write!(f, "interface-string<{iface}>"),
321 Atomic::TEnumString => write!(f, "enum-string"),
322 Atomic::TTraitString => write!(f, "trait-string"),
323 Atomic::TLiteralEnumCase {
324 enum_fqcn,
325 case_name,
326 } => {
327 write!(f, "{enum_fqcn}::{case_name}")
328 }
329
330 Atomic::TIntersection { parts } => {
331 let mut printed: Vec<&Type> = Vec::new();
336 let mut first = true;
337 for part in parts.iter() {
338 if printed.contains(&part) {
339 continue;
340 }
341 printed.push(part);
342 if !first {
343 f.write_str("&")?;
344 }
345 first = false;
346 write!(f, "{part}")?;
347 }
348 Ok(())
349 }
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn int_range_unbounded_displays_as_int() {
360 assert_eq!(
361 format!(
362 "{}",
363 Atomic::TIntRange {
364 min: None,
365 max: None
366 }
367 ),
368 "int"
369 );
370 }
371
372 #[test]
373 fn int_range_bounded_min_displays_range() {
374 assert_eq!(
375 format!(
376 "{}",
377 Atomic::TIntRange {
378 min: Some(0),
379 max: None
380 }
381 ),
382 "int<0, max>"
383 );
384 }
385
386 #[test]
387 fn int_range_bounded_max_displays_range() {
388 assert_eq!(
389 format!(
390 "{}",
391 Atomic::TIntRange {
392 min: None,
393 max: Some(100)
394 }
395 ),
396 "int<min, 100>"
397 );
398 }
399
400 #[test]
401 fn int_range_fully_bounded_displays_range() {
402 assert_eq!(
403 format!(
404 "{}",
405 Atomic::TIntRange {
406 min: Some(1),
407 max: Some(10)
408 }
409 ),
410 "int<1, 10>"
411 );
412 }
413
414 #[test]
415 fn unbounded_int_range_in_union_displays_as_int() {
416 let mut u = Type::empty();
417 u.add_type(Atomic::TIntRange {
418 min: None,
419 max: None,
420 });
421 u.add_type(Atomic::TFalse);
422 assert_eq!(format!("{u}"), "int|false");
423 }
424
425 #[test]
426 fn array_of_mixed_mixed_collapses_to_array() {
427 let atomic = Atomic::TArray {
428 key: Box::new(Type::mixed()),
429 value: Box::new(Type::mixed()),
430 };
431 assert_eq!(format!("{atomic}"), "array");
432 }
433
434 #[test]
435 fn array_of_array_key_mixed_collapses_to_array() {
436 let atomic = Atomic::TArray {
437 key: Box::new(Type::array_key()),
438 value: Box::new(Type::mixed()),
439 };
440 assert_eq!(format!("{atomic}"), "array");
441 }
442
443 #[test]
444 fn array_of_int_mixed_does_not_collapse() {
445 let atomic = Atomic::TArray {
446 key: Box::new(Type::int()),
447 value: Box::new(Type::mixed()),
448 };
449 assert_eq!(format!("{atomic}"), "array<int, mixed>");
450 }
451
452 #[test]
453 fn array_of_mixed_string_does_not_collapse() {
454 let atomic = Atomic::TArray {
455 key: Box::new(Type::mixed()),
456 value: Box::new(Type::string()),
457 };
458 assert_eq!(format!("{atomic}"), "array<mixed, string>");
459 }
460
461 #[test]
462 fn non_empty_array_of_mixed_mixed_collapses() {
463 let atomic = Atomic::TNonEmptyArray {
464 key: Box::new(Type::mixed()),
465 value: Box::new(Type::mixed()),
466 };
467 assert_eq!(format!("{atomic}"), "non-empty-array");
468 }
469
470 #[test]
471 fn list_of_mixed_collapses_to_list() {
472 let atomic = Atomic::TList {
473 value: Box::new(Type::mixed()),
474 };
475 assert_eq!(format!("{atomic}"), "list");
476 }
477
478 #[test]
479 fn list_of_string_does_not_collapse() {
480 let atomic = Atomic::TList {
481 value: Box::new(Type::string()),
482 };
483 assert_eq!(format!("{atomic}"), "list<string>");
484 }
485
486 #[test]
487 fn non_empty_list_of_mixed_collapses() {
488 let atomic = Atomic::TNonEmptyList {
489 value: Box::new(Type::mixed()),
490 };
491 assert_eq!(format!("{atomic}"), "non-empty-list");
492 }
493
494 #[test]
495 fn named_object_all_mixed_params_collapses_to_bare_name() {
496 let atomic = Atomic::TNamedObject {
497 fqcn: "Traversable".into(),
498 type_params: crate::union::vec_to_type_params(vec![Type::mixed(), Type::mixed()]),
499 };
500 assert_eq!(format!("{atomic}"), "Traversable");
501 }
502
503 #[test]
504 fn named_object_with_one_concrete_param_does_not_collapse() {
505 let atomic = Atomic::TNamedObject {
506 fqcn: "Traversable".into(),
507 type_params: crate::union::vec_to_type_params(vec![Type::int(), Type::mixed()]),
508 };
509 assert_eq!(format!("{atomic}"), "Traversable<int, mixed>");
510 }
511
512 #[test]
513 fn named_object_with_mixed_bounded_template_param_does_not_collapse() {
514 let template_param = Atomic::TTemplateParam {
518 name: "T".into(),
519 as_type: Box::new(Type::mixed()),
520 defining_entity: "MyClass".into(),
521 };
522 let atomic = Atomic::TNamedObject {
523 fqcn: "MyClass".into(),
524 type_params: crate::union::vec_to_type_params(vec![Type::single(template_param)]),
525 };
526 assert_eq!(format!("{atomic}"), "MyClass<T>");
527 }
528
529 fn bare_traversable() -> Atomic {
530 Atomic::TNamedObject {
531 fqcn: "Traversable".into(),
532 type_params: crate::union::empty_type_params(),
533 }
534 }
535
536 #[test]
537 fn array_mixed_mixed_or_traversable_collapses_to_iterable() {
538 let mut u = Type::single(Atomic::TArray {
539 key: Box::new(Type::array_key()),
540 value: Box::new(Type::mixed()),
541 });
542 u.add_type(bare_traversable());
543 assert_eq!(format!("{u}"), "iterable");
544 }
545
546 #[test]
547 fn parameterized_array_or_matching_traversable_collapses_to_iterable() {
548 let key = Type::string();
549 let value = Type::int();
550 let mut u = Type::single(Atomic::TArray {
551 key: Box::new(key.clone()),
552 value: Box::new(value.clone()),
553 });
554 u.add_type(Atomic::TNamedObject {
555 fqcn: "Traversable".into(),
556 type_params: crate::union::vec_to_type_params(vec![key, value]),
557 });
558 assert_eq!(format!("{u}"), "iterable<string, int>");
559 }
560
561 #[test]
562 fn iterable_collapse_preserves_other_union_members() {
563 let mut u = Type::single(Atomic::TArray {
564 key: Box::new(Type::array_key()),
565 value: Box::new(Type::mixed()),
566 });
567 u.add_type(bare_traversable());
568 u.add_type(Atomic::TNull);
569 assert_eq!(format!("{u}"), "iterable|null");
570 }
571
572 #[test]
573 fn array_with_mismatched_bare_traversable_does_not_collapse() {
574 let mut u = Type::single(Atomic::TArray {
579 key: Box::new(Type::int()),
580 value: Box::new(Type::string()),
581 });
582 u.add_type(bare_traversable());
583 assert_eq!(format!("{u}"), "array<int, string>|Traversable");
584 }
585
586 #[test]
587 fn array_with_non_matching_parameterized_traversable_does_not_collapse() {
588 let mut u = Type::single(Atomic::TArray {
589 key: Box::new(Type::int()),
590 value: Box::new(Type::string()),
591 });
592 u.add_type(Atomic::TNamedObject {
593 fqcn: "Traversable".into(),
594 type_params: crate::union::vec_to_type_params(vec![Type::string(), Type::int()]),
595 });
596 assert_eq!(
597 format!("{u}"),
598 "array<int, string>|Traversable<string, int>"
599 );
600 }
601
602 fn named_object(fqcn: &str) -> Atomic {
603 Atomic::TNamedObject {
604 fqcn: fqcn.into(),
605 type_params: crate::union::empty_type_params(),
606 }
607 }
608
609 #[test]
610 fn intersection_drops_exact_duplicate_part() {
611 let atomic = Atomic::TIntersection {
612 parts: crate::union::vec_to_type_params(vec![
613 Type::single(named_object("Foo")),
614 Type::single(named_object("Foo")),
615 ]),
616 };
617 assert_eq!(format!("{atomic}"), "Foo");
618 }
619
620 #[test]
621 fn intersection_keeps_distinct_parts() {
622 let atomic = Atomic::TIntersection {
623 parts: crate::union::vec_to_type_params(vec![
624 Type::single(named_object("Countable")),
625 Type::single(named_object("Traversable")),
626 ]),
627 };
628 assert_eq!(format!("{atomic}"), "Countable&Traversable");
629 }
630
631 #[test]
632 fn intersection_drops_duplicate_among_three_parts() {
633 let atomic = Atomic::TIntersection {
634 parts: crate::union::vec_to_type_params(vec![
635 Type::single(named_object("Foo")),
636 Type::single(named_object("Bar")),
637 Type::single(named_object("Foo")),
638 ]),
639 };
640 assert_eq!(format!("{atomic}"), "Foo&Bar");
641 }
642}