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
127impl fmt::Display for Atomic {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Atomic::TString => write!(f, "string"),
131 Atomic::TLiteralString(s) => write!(f, "\"{s}\""),
132 Atomic::TCallableString => write!(f, "callable-string"),
133 Atomic::TClassString(None) => write!(f, "class-string"),
134 Atomic::TClassString(Some(cls)) => write!(f, "class-string<{cls}>"),
135 Atomic::TNonEmptyString => write!(f, "non-empty-string"),
136 Atomic::TNumericString => write!(f, "numeric-string"),
137
138 Atomic::TInt => write!(f, "int"),
139 Atomic::TLiteralInt(n) => write!(f, "{n}"),
140 Atomic::TIntRange { min, max } => match (min, max) {
141 (None, None) => write!(f, "int"),
142 (lo, hi) => {
143 let lo = lo.map_or_else(|| "min".to_string(), |n| n.to_string());
144 let hi = hi.map_or_else(|| "max".to_string(), |n| n.to_string());
145 write!(f, "int<{lo}, {hi}>")
146 }
147 },
148 Atomic::TPositiveInt => write!(f, "positive-int"),
149 Atomic::TNegativeInt => write!(f, "negative-int"),
150 Atomic::TNonNegativeInt => write!(f, "non-negative-int"),
151
152 Atomic::TFloat | Atomic::TIntegralFloat => write!(f, "float"),
153 Atomic::TLiteralFloat(high, low) => {
154 let bits = ((*high as u64) << 32) | (*low as u32 as u64);
155 let value = f64::from_bits(bits);
156 write!(f, "{value}")
157 }
158
159 Atomic::TBool => write!(f, "bool"),
160 Atomic::TTrue => write!(f, "true"),
161 Atomic::TFalse => write!(f, "false"),
162
163 Atomic::TNull => write!(f, "null"),
164 Atomic::TVoid => write!(f, "void"),
165 Atomic::TNever => write!(f, "never"),
166 Atomic::TMixed => write!(f, "mixed"),
167 Atomic::TScalar => write!(f, "scalar"),
168 Atomic::TNumeric => write!(f, "numeric"),
169
170 Atomic::TObject => write!(f, "object"),
171 Atomic::TNamedObject { fqcn, type_params } => {
172 if type_params.is_empty() || type_params.iter().all(is_exactly_mixed) {
177 write!(f, "{fqcn}")
178 } else {
179 write!(f, "{fqcn}<")?;
180 write_joined(f, type_params.iter(), ", ")?;
181 f.write_str(">")
182 }
183 }
184 Atomic::TStaticObject { fqcn } => write!(f, "static({fqcn})"),
185 Atomic::TSelf { fqcn } => write!(f, "self({fqcn})"),
186 Atomic::TParent { fqcn } => write!(f, "parent({fqcn})"),
187
188 Atomic::TCallable {
189 params,
190 return_type,
191 is_pure,
192 } => {
193 if *is_pure {
194 f.write_str("pure-")?;
195 }
196 match (params, return_type) {
197 (None, None) => f.write_str("callable"),
198 (Some(params), return_type) => {
199 f.write_str("callable(")?;
200 write_param_types(f, params)?;
201 match return_type {
202 Some(r) => write!(f, "): {r}"),
203 None => f.write_str("): mixed"),
204 }
205 }
206 (None, Some(ret)) => write!(f, "callable(): {ret}"),
207 }
208 }
209 Atomic::TClosure { data } => {
210 if data.is_pure {
211 f.write_str("pure-")?;
212 }
213 f.write_str("Closure(")?;
214 write_param_types(f, &data.params)?;
215 write!(f, "): {}", data.return_type)
216 }
217
218 Atomic::TArray { key, value } => {
219 if is_exactly_mixed(value) && (is_exactly_mixed(key) || key.is_array_key()) {
224 write!(f, "array")
225 } else {
226 write!(f, "array<{key}, {value}>")
227 }
228 }
229 Atomic::TList { value } => {
230 if is_exactly_mixed(value) {
231 write!(f, "list")
232 } else {
233 write!(f, "list<{value}>")
234 }
235 }
236 Atomic::TNonEmptyArray { key, value } => {
237 if is_exactly_mixed(value) && (is_exactly_mixed(key) || key.is_array_key()) {
238 write!(f, "non-empty-array")
239 } else {
240 write!(f, "non-empty-array<{key}, {value}>")
241 }
242 }
243 Atomic::TNonEmptyList { value } => {
244 if is_exactly_mixed(value) {
245 write!(f, "non-empty-list")
246 } else {
247 write!(f, "non-empty-list<{value}>")
248 }
249 }
250 Atomic::TKeyedArray { properties, .. } => {
251 f.write_str("array{")?;
252 for (i, (k, v)) in properties.iter().enumerate() {
253 if i > 0 {
254 f.write_str(", ")?;
255 }
256 match k {
257 crate::atomic::ArrayKey::String(s) => write!(f, "'{s}'")?,
258 crate::atomic::ArrayKey::Int(n) => write!(f, "{n}")?,
259 }
260 if v.optional {
261 f.write_str("?")?;
262 }
263 write!(f, ": {}", v.ty)?;
264 }
265 f.write_str("}")
266 }
267
268 Atomic::TTemplateParam { name, .. } => write!(f, "{name}"),
269 Atomic::TKeyOf { target } => write!(f, "key-of<{target}>"),
270 Atomic::TValueOf { target } => write!(f, "value-of<{target}>"),
271 Atomic::TConditional { data } => {
272 let (subject, if_true, if_false) = (&data.subject, &data.if_true, &data.if_false);
273 match &data.param_name {
274 Some(name) => write!(f, "(${name} is {subject} ? {if_true} : {if_false})"),
275 None => write!(f, "({subject} is ? {if_true} : {if_false})"),
276 }
277 }
278
279 Atomic::TInterfaceString(None) => write!(f, "interface-string"),
280 Atomic::TInterfaceString(Some(iface)) => write!(f, "interface-string<{iface}>"),
281 Atomic::TEnumString => write!(f, "enum-string"),
282 Atomic::TTraitString => write!(f, "trait-string"),
283 Atomic::TLiteralEnumCase {
284 enum_fqcn,
285 case_name,
286 } => {
287 write!(f, "{enum_fqcn}::{case_name}")
288 }
289
290 Atomic::TIntersection { parts } => {
291 let mut printed: Vec<&Type> = Vec::new();
296 let mut first = true;
297 for part in parts.iter() {
298 if printed.contains(&part) {
299 continue;
300 }
301 printed.push(part);
302 if !first {
303 f.write_str("&")?;
304 }
305 first = false;
306 write!(f, "{part}")?;
307 }
308 Ok(())
309 }
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn int_range_unbounded_displays_as_int() {
320 assert_eq!(
321 format!(
322 "{}",
323 Atomic::TIntRange {
324 min: None,
325 max: None
326 }
327 ),
328 "int"
329 );
330 }
331
332 #[test]
333 fn int_range_bounded_min_displays_range() {
334 assert_eq!(
335 format!(
336 "{}",
337 Atomic::TIntRange {
338 min: Some(0),
339 max: None
340 }
341 ),
342 "int<0, max>"
343 );
344 }
345
346 #[test]
347 fn int_range_bounded_max_displays_range() {
348 assert_eq!(
349 format!(
350 "{}",
351 Atomic::TIntRange {
352 min: None,
353 max: Some(100)
354 }
355 ),
356 "int<min, 100>"
357 );
358 }
359
360 #[test]
361 fn int_range_fully_bounded_displays_range() {
362 assert_eq!(
363 format!(
364 "{}",
365 Atomic::TIntRange {
366 min: Some(1),
367 max: Some(10)
368 }
369 ),
370 "int<1, 10>"
371 );
372 }
373
374 #[test]
375 fn unbounded_int_range_in_union_displays_as_int() {
376 let mut u = Type::empty();
377 u.add_type(Atomic::TIntRange {
378 min: None,
379 max: None,
380 });
381 u.add_type(Atomic::TFalse);
382 assert_eq!(format!("{u}"), "int|false");
383 }
384
385 #[test]
386 fn array_of_mixed_mixed_collapses_to_array() {
387 let atomic = Atomic::TArray {
388 key: Box::new(Type::mixed()),
389 value: Box::new(Type::mixed()),
390 };
391 assert_eq!(format!("{atomic}"), "array");
392 }
393
394 #[test]
395 fn array_of_array_key_mixed_collapses_to_array() {
396 let atomic = Atomic::TArray {
397 key: Box::new(Type::array_key()),
398 value: Box::new(Type::mixed()),
399 };
400 assert_eq!(format!("{atomic}"), "array");
401 }
402
403 #[test]
404 fn array_of_int_mixed_does_not_collapse() {
405 let atomic = Atomic::TArray {
406 key: Box::new(Type::int()),
407 value: Box::new(Type::mixed()),
408 };
409 assert_eq!(format!("{atomic}"), "array<int, mixed>");
410 }
411
412 #[test]
413 fn array_of_mixed_string_does_not_collapse() {
414 let atomic = Atomic::TArray {
415 key: Box::new(Type::mixed()),
416 value: Box::new(Type::string()),
417 };
418 assert_eq!(format!("{atomic}"), "array<mixed, string>");
419 }
420
421 #[test]
422 fn non_empty_array_of_mixed_mixed_collapses() {
423 let atomic = Atomic::TNonEmptyArray {
424 key: Box::new(Type::mixed()),
425 value: Box::new(Type::mixed()),
426 };
427 assert_eq!(format!("{atomic}"), "non-empty-array");
428 }
429
430 #[test]
431 fn list_of_mixed_collapses_to_list() {
432 let atomic = Atomic::TList {
433 value: Box::new(Type::mixed()),
434 };
435 assert_eq!(format!("{atomic}"), "list");
436 }
437
438 #[test]
439 fn list_of_string_does_not_collapse() {
440 let atomic = Atomic::TList {
441 value: Box::new(Type::string()),
442 };
443 assert_eq!(format!("{atomic}"), "list<string>");
444 }
445
446 #[test]
447 fn non_empty_list_of_mixed_collapses() {
448 let atomic = Atomic::TNonEmptyList {
449 value: Box::new(Type::mixed()),
450 };
451 assert_eq!(format!("{atomic}"), "non-empty-list");
452 }
453
454 #[test]
455 fn named_object_all_mixed_params_collapses_to_bare_name() {
456 let atomic = Atomic::TNamedObject {
457 fqcn: "Traversable".into(),
458 type_params: crate::union::vec_to_type_params(vec![Type::mixed(), Type::mixed()]),
459 };
460 assert_eq!(format!("{atomic}"), "Traversable");
461 }
462
463 #[test]
464 fn named_object_with_one_concrete_param_does_not_collapse() {
465 let atomic = Atomic::TNamedObject {
466 fqcn: "Traversable".into(),
467 type_params: crate::union::vec_to_type_params(vec![Type::int(), Type::mixed()]),
468 };
469 assert_eq!(format!("{atomic}"), "Traversable<int, mixed>");
470 }
471
472 #[test]
473 fn named_object_with_mixed_bounded_template_param_does_not_collapse() {
474 let template_param = Atomic::TTemplateParam {
478 name: "T".into(),
479 as_type: Box::new(Type::mixed()),
480 defining_entity: "MyClass".into(),
481 };
482 let atomic = Atomic::TNamedObject {
483 fqcn: "MyClass".into(),
484 type_params: crate::union::vec_to_type_params(vec![Type::single(template_param)]),
485 };
486 assert_eq!(format!("{atomic}"), "MyClass<T>");
487 }
488
489 fn bare_traversable() -> Atomic {
490 Atomic::TNamedObject {
491 fqcn: "Traversable".into(),
492 type_params: crate::union::empty_type_params(),
493 }
494 }
495
496 #[test]
497 fn array_mixed_mixed_or_traversable_collapses_to_iterable() {
498 let mut u = Type::single(Atomic::TArray {
499 key: Box::new(Type::array_key()),
500 value: Box::new(Type::mixed()),
501 });
502 u.add_type(bare_traversable());
503 assert_eq!(format!("{u}"), "iterable");
504 }
505
506 #[test]
507 fn parameterized_array_or_matching_traversable_collapses_to_iterable() {
508 let key = Type::string();
509 let value = Type::int();
510 let mut u = Type::single(Atomic::TArray {
511 key: Box::new(key.clone()),
512 value: Box::new(value.clone()),
513 });
514 u.add_type(Atomic::TNamedObject {
515 fqcn: "Traversable".into(),
516 type_params: crate::union::vec_to_type_params(vec![key, value]),
517 });
518 assert_eq!(format!("{u}"), "iterable<string, int>");
519 }
520
521 #[test]
522 fn iterable_collapse_preserves_other_union_members() {
523 let mut u = Type::single(Atomic::TArray {
524 key: Box::new(Type::array_key()),
525 value: Box::new(Type::mixed()),
526 });
527 u.add_type(bare_traversable());
528 u.add_type(Atomic::TNull);
529 assert_eq!(format!("{u}"), "iterable|null");
530 }
531
532 #[test]
533 fn array_with_mismatched_bare_traversable_does_not_collapse() {
534 let mut u = Type::single(Atomic::TArray {
539 key: Box::new(Type::int()),
540 value: Box::new(Type::string()),
541 });
542 u.add_type(bare_traversable());
543 assert_eq!(format!("{u}"), "array<int, string>|Traversable");
544 }
545
546 #[test]
547 fn array_with_non_matching_parameterized_traversable_does_not_collapse() {
548 let mut u = Type::single(Atomic::TArray {
549 key: Box::new(Type::int()),
550 value: Box::new(Type::string()),
551 });
552 u.add_type(Atomic::TNamedObject {
553 fqcn: "Traversable".into(),
554 type_params: crate::union::vec_to_type_params(vec![Type::string(), Type::int()]),
555 });
556 assert_eq!(
557 format!("{u}"),
558 "array<int, string>|Traversable<string, int>"
559 );
560 }
561
562 fn named_object(fqcn: &str) -> Atomic {
563 Atomic::TNamedObject {
564 fqcn: fqcn.into(),
565 type_params: crate::union::empty_type_params(),
566 }
567 }
568
569 #[test]
570 fn intersection_drops_exact_duplicate_part() {
571 let atomic = Atomic::TIntersection {
572 parts: crate::union::vec_to_type_params(vec![
573 Type::single(named_object("Foo")),
574 Type::single(named_object("Foo")),
575 ]),
576 };
577 assert_eq!(format!("{atomic}"), "Foo");
578 }
579
580 #[test]
581 fn intersection_keeps_distinct_parts() {
582 let atomic = Atomic::TIntersection {
583 parts: crate::union::vec_to_type_params(vec![
584 Type::single(named_object("Countable")),
585 Type::single(named_object("Traversable")),
586 ]),
587 };
588 assert_eq!(format!("{atomic}"), "Countable&Traversable");
589 }
590
591 #[test]
592 fn intersection_drops_duplicate_among_three_parts() {
593 let atomic = Atomic::TIntersection {
594 parts: crate::union::vec_to_type_params(vec![
595 Type::single(named_object("Foo")),
596 Type::single(named_object("Bar")),
597 Type::single(named_object("Foo")),
598 ]),
599 };
600 assert_eq!(format!("{atomic}"), "Foo&Bar");
601 }
602}