1use std::collections::{BTreeMap, BTreeSet};
3
4use crate::compiler_error::CompilerFailure;
5use crate::typechecker::infer::narrowing::LiteralValue;
6use crate::types::{ObjectField, Type};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum PrimBase {
11 String,
12 Number,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct JsonUnionStrategy {
17 pub string: PerTagStrategy,
18 pub number: PerTagStrategy,
19 pub boolean: PerTagStrategy,
20 pub null: PerTagStrategy,
21 pub object: PerTagStrategy,
22 pub array: PerTagStrategy,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum PerTagStrategy {
27 Empty,
28 Single(Type),
29 LiteralSet {
30 base: PrimBase,
31 values: Vec<LiteralValue>,
32 },
33 ObjectCascade(ObjectDispatch),
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum ObjectDispatch {
38 ByLiteralTagValue {
39 field: String,
40 arms: Vec<(LiteralValue, Type)>,
41 },
42 ByRequiredFieldName {
43 variants: Vec<Type>,
44 },
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum JsonNarrowReason {
49 Internal(CompilerFailure),
50 UnsupportedTarget {
51 ty: Type,
52 },
53 ArrayVsArray {
54 variants: Vec<Type>,
55 },
56 PrimitiveOverlap {
57 base: PrimBase,
58 variants: Vec<Type>,
59 },
60 ObjectAmbiguous {
62 winner: Type,
63 loser: Type,
64 },
65 OptionalDiscriminator {
66 field: String,
67 variants: Vec<Type>,
68 },
69}
70
71pub type AliasExpander<'a> = &'a dyn Fn(&Type) -> Type;
76
77fn no_expand(ty: &Type) -> Type {
80 ty.peel().clone()
81}
82
83pub fn resolve_json_union_strategy(
87 members: &[Type],
88) -> Result<JsonUnionStrategy, Vec<JsonNarrowReason>> {
89 resolve_json_union_strategy_with(members, &no_expand)
90}
91
92pub fn resolve_json_union_strategy_with(
95 members: &[Type],
96 expand: AliasExpander<'_>,
97) -> Result<JsonUnionStrategy, Vec<JsonNarrowReason>> {
98 let mut buckets = Buckets::default();
99 for m in members {
100 buckets.push(m, expand, &mut Vec::new())?;
101 }
102
103 let mut reasons = Vec::new();
104 let string = resolve_primitive_bucket(PrimBase::String, &buckets.string, &mut reasons);
105 let number = resolve_primitive_bucket(PrimBase::Number, &buckets.number, &mut reasons);
106 let boolean = resolve_singleton_bucket(&buckets.boolean)?;
107 let null = resolve_singleton_bucket(&buckets.null)?;
108 let object = resolve_object_bucket(&buckets.object, &mut reasons, expand)?;
109 let array = resolve_array_bucket(&buckets.array, &mut reasons);
110
111 if reasons.is_empty() {
112 Ok(JsonUnionStrategy {
113 string,
114 number,
115 boolean,
116 null,
117 object,
118 array,
119 })
120 } else {
121 Err(reasons)
122 }
123}
124
125#[derive(Default)]
126struct Buckets {
127 string: Vec<Type>,
128 number: Vec<Type>,
129 boolean: Vec<Type>,
130 null: Vec<Type>,
131 object: Vec<Type>,
132 array: Vec<Type>,
133}
134
135impl Buckets {
136 fn push(
143 &mut self,
144 ty: &Type,
145 expand: AliasExpander<'_>,
146 active: &mut Vec<Type>,
147 ) -> Result<(), Vec<JsonNarrowReason>> {
148 if active.contains(ty) {
149 return Err(vec![JsonNarrowReason::UnsupportedTarget { ty: ty.clone() }]);
150 }
151 active.push(ty.clone());
152 let kind = expand(ty);
153 match kind.peel() {
154 Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => {
155 self.string.push(ty.clone());
156 }
157 Type::Number | Type::NumberLiteral(_) | Type::NumberEnum { .. } => {
158 self.number.push(ty.clone());
159 }
160 Type::Boolean | Type::BooleanLiteral(_) => self.boolean.push(ty.clone()),
161 Type::Null => self.null.push(ty.clone()),
162 Type::Object { .. } => self.object.push(ty.clone()),
163 Type::Array(_) => self.array.push(ty.clone()),
164 Type::Union(members) if !matches!(ty.peel(), Type::Union(_)) => {
166 for m in members {
167 self.push(m, expand, active)?;
168 }
169 }
170 _ => return Err(vec![JsonNarrowReason::UnsupportedTarget { ty: ty.clone() }]),
171 }
172 active.pop();
173 Ok(())
174 }
175}
176
177fn resolve_singleton_bucket(variants: &[Type]) -> Result<PerTagStrategy, Vec<JsonNarrowReason>> {
178 Ok(match variants {
179 [] => PerTagStrategy::Empty,
180 [t] => PerTagStrategy::Single(t.clone()),
181 _ => {
182 return Err(vec![JsonNarrowReason::UnsupportedTarget {
183 ty: Type::union(variants.to_vec()),
184 }]);
185 }
186 })
187}
188
189fn resolve_primitive_bucket(
190 base: PrimBase,
191 variants: &[Type],
192 reasons: &mut Vec<JsonNarrowReason>,
193) -> PerTagStrategy {
194 match variants {
195 [] => PerTagStrategy::Empty,
196 [t] => PerTagStrategy::Single(t.clone()),
197 _ => {
198 let values: Option<Vec<LiteralValue>> = variants
199 .iter()
200 .map(|t| literal_value_of(t.peel()))
201 .collect();
202 if let Some(values) = values {
203 PerTagStrategy::LiteralSet { base, values }
204 } else {
205 reasons.push(JsonNarrowReason::PrimitiveOverlap {
206 base,
207 variants: variants.to_vec(),
208 });
209 PerTagStrategy::Empty
210 }
211 }
212 }
213}
214
215fn resolve_array_bucket(variants: &[Type], reasons: &mut Vec<JsonNarrowReason>) -> PerTagStrategy {
216 match variants {
217 [] => PerTagStrategy::Empty,
218 [t] => PerTagStrategy::Single(t.clone()),
219 _ => {
220 reasons.push(JsonNarrowReason::ArrayVsArray {
221 variants: variants.to_vec(),
222 });
223 PerTagStrategy::Empty
224 }
225 }
226}
227
228fn resolve_object_bucket(
229 variants: &[Type],
230 reasons: &mut Vec<JsonNarrowReason>,
231 expand: AliasExpander<'_>,
232) -> Result<PerTagStrategy, Vec<JsonNarrowReason>> {
233 match variants {
234 [] => return Ok(PerTagStrategy::Empty),
235 [t] => return Ok(PerTagStrategy::Single(t.clone())),
236 _ => {}
237 }
238
239 if let Some(dispatch) = try_literal_tag_value(variants, expand)? {
240 return Ok(PerTagStrategy::ObjectCascade(dispatch));
241 }
242
243 Ok(match try_required_field_name(variants, expand) {
244 Ok(dispatch) => PerTagStrategy::ObjectCascade(dispatch),
245 Err(object_reasons) => {
246 reasons.extend(object_reasons);
247 PerTagStrategy::Empty
248 }
249 })
250}
251
252fn literal_value_of(ty: &Type) -> Option<LiteralValue> {
253 match ty.peel() {
254 Type::StringLiteral(s) => Some(LiteralValue::String(s.clone())),
255 Type::NumberLiteral(n) => Some(LiteralValue::Number(*n)),
256 Type::BooleanLiteral(b) => Some(LiteralValue::Boolean(*b)),
257 _ => None,
258 }
259}
260
261fn try_literal_tag_value(
262 variants: &[Type],
263 expand: AliasExpander<'_>,
264) -> Result<Option<ObjectDispatch>, Vec<JsonNarrowReason>> {
265 let expanded: Vec<Type> = variants.iter().map(expand).collect();
268 let field_maps: Vec<&BTreeMap<String, ObjectField>> = expanded
269 .iter()
270 .map(|t| match t.peel() {
271 Type::Object { fields, .. } => Ok(fields),
272 _ => Err(invalid_object_bucket()),
273 })
274 .collect::<Result<_, _>>()?;
275
276 let first = field_maps.first().ok_or_else(invalid_object_bucket)?;
278 let candidates: Vec<&String> = first
279 .iter()
280 .filter(|(_, f)| !f.optional && literal_value_of(&f.ty).is_some())
281 .map(|(name, _)| name)
282 .collect();
283
284 'fields: for name in candidates {
285 let mut arms: Vec<(LiteralValue, Type)> = Vec::with_capacity(variants.len());
286 let mut seen_values: BTreeSet<LiteralValue> = BTreeSet::new();
287 for (variant, fields) in variants.iter().zip(&field_maps) {
288 let field = match fields.get(name) {
289 Some(f) if !f.optional => f,
290 _ => continue 'fields,
291 };
292 let Some(lit) = literal_value_of(&field.ty) else {
293 continue 'fields;
294 };
295 if !seen_values.insert(lit.clone()) {
296 continue 'fields;
297 }
298 arms.push((lit, variant.clone()));
299 }
300 return Ok(Some(ObjectDispatch::ByLiteralTagValue {
301 field: name.clone(),
302 arms,
303 }));
304 }
305 Ok(None)
306}
307
308fn try_required_field_name(
309 variants: &[Type],
310 expand: AliasExpander<'_>,
311) -> Result<ObjectDispatch, Vec<JsonNarrowReason>> {
312 let expanded: Vec<Type> = variants.iter().map(expand).collect();
313 let required_sets: Vec<BTreeSet<&str>> = expanded
314 .iter()
315 .map(required_field_names)
316 .collect::<Result<_, _>>()?;
317 let all_sets: Vec<BTreeSet<&str>> = expanded
318 .iter()
319 .map(all_field_names)
320 .collect::<Result<_, _>>()?;
321
322 let mut reasons = Vec::new();
323 for i in 0..variants.len() {
324 for j in (i + 1)..variants.len() {
325 let ri = &required_sets[i];
326 let rj = &required_sets[j];
327 let i_sub_j = ri.is_subset(rj);
328 let j_sub_i = rj.is_subset(ri);
329 if !(i_sub_j || j_sub_i) {
330 continue;
331 }
332
333 let ai = &all_sets[i];
336 let aj = &all_sets[j];
337 let all_disjoint = !ai.is_subset(aj) && !aj.is_subset(ai);
338 if all_disjoint {
339 let optional_diff: Vec<&str> = ai
340 .symmetric_difference(aj)
341 .filter(|n| !ri.contains(*n) && !rj.contains(*n))
342 .copied()
343 .collect();
344 let field = optional_diff
345 .first()
346 .map(std::string::ToString::to_string)
347 .unwrap_or_default();
348 reasons.push(JsonNarrowReason::OptionalDiscriminator {
349 field,
350 variants: vec![variants[i].clone(), variants[j].clone()],
351 });
352 } else {
353 let (winner, loser) = if i_sub_j {
354 (variants[i].clone(), variants[j].clone())
355 } else {
356 (variants[j].clone(), variants[i].clone())
357 };
358 reasons.push(JsonNarrowReason::ObjectAmbiguous { winner, loser });
359 }
360 }
361 }
362
363 if reasons.is_empty() {
364 Ok(ObjectDispatch::ByRequiredFieldName {
365 variants: variants.to_vec(),
366 })
367 } else {
368 Err(reasons)
369 }
370}
371
372fn required_field_names(ty: &Type) -> Result<BTreeSet<&str>, Vec<JsonNarrowReason>> {
373 Ok(match ty.peel() {
374 Type::Object { fields, .. } => fields
375 .iter()
376 .filter(|(_, f)| !f.optional)
377 .map(|(k, _)| k.as_str())
378 .collect(),
379 _ => return Err(invalid_object_bucket()),
380 })
381}
382
383fn all_field_names(ty: &Type) -> Result<BTreeSet<&str>, Vec<JsonNarrowReason>> {
384 Ok(match ty.peel() {
385 Type::Object { fields, .. } => fields.keys().map(std::string::String::as_str).collect(),
386 _ => return Err(invalid_object_bucket()),
387 })
388}
389
390fn invalid_object_bucket() -> Vec<JsonNarrowReason> {
391 vec![JsonNarrowReason::Internal(super::invariant_failure(
392 "JSON object bucket contains inconsistent expanded metadata",
393 ))]
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::types::LiteralF64;
400
401 fn s_lit(s: &str) -> Type {
402 Type::StringLiteral(s.to_string())
403 }
404 fn n_lit(n: f64) -> Type {
405 Type::NumberLiteral(LiteralF64(n))
406 }
407 fn obj(fields: &[(&str, Type, bool)]) -> Type {
408 let mut map = BTreeMap::new();
409 for (name, ty, optional) in fields {
410 map.insert(
411 (*name).to_string(),
412 ObjectField {
413 ty: ty.clone(),
414 optional: *optional,
415 readonly: false,
416 method: false,
417 },
418 );
419 }
420 Type::Object {
421 index: None,
422 fields: map,
423 }
424 }
425
426 fn canonical(members: Vec<Type>) -> Vec<Type> {
427 match Type::union(members) {
428 Type::Union(m) => m,
429 single => vec![single],
430 }
431 }
432
433 #[test]
434 fn primitive_mix_string_number() {
435 let members = canonical(vec![Type::String, Type::Number]);
436 let strat = resolve_json_union_strategy(&members).expect("resolves");
437 assert_eq!(strat.string, PerTagStrategy::Single(Type::String));
438 assert_eq!(strat.number, PerTagStrategy::Single(Type::Number));
439 }
440
441 #[test]
442 fn literal_set_strings() {
443 let members = canonical(vec![s_lit("a"), s_lit("b"), s_lit("c")]);
444 let strat = resolve_json_union_strategy(&members).expect("resolves");
445 match strat.string {
446 PerTagStrategy::LiteralSet { base, values } => {
447 assert_eq!(base, PrimBase::String);
448 assert_eq!(values.len(), 3);
449 }
450 other => panic!("expected LiteralSet, got {other:?}"),
451 }
452 }
453
454 #[test]
455 fn literal_set_numbers() {
456 let members = canonical(vec![n_lit(200.0), n_lit(404.0), n_lit(500.0)]);
457 let strat = resolve_json_union_strategy(&members).expect("resolves");
458 match strat.number {
459 PerTagStrategy::LiteralSet { base, .. } => assert_eq!(base, PrimBase::Number),
460 other => panic!("expected LiteralSet, got {other:?}"),
461 }
462 }
463
464 #[test]
465 fn literal_subsumed_by_base() {
466 let members = canonical(vec![s_lit("foo"), Type::String]);
467 let reasons = resolve_json_union_strategy(&members).expect_err("fails");
468 assert!(matches!(
469 reasons.as_slice(),
470 [JsonNarrowReason::PrimitiveOverlap {
471 base: PrimBase::String,
472 ..
473 }]
474 ));
475 }
476
477 #[test]
478 fn t_or_null_folds_into_strategy() {
479 let user = obj(&[("id", Type::Number, false)]);
480 let members = canonical(vec![user.clone(), Type::Null]);
481 let strat = resolve_json_union_strategy(&members).expect("resolves");
482 assert_eq!(strat.null, PerTagStrategy::Single(Type::Null));
483 assert!(matches!(strat.object, PerTagStrategy::Single(_)));
484 }
485
486 #[test]
487 fn object_cascade_by_literal_tag() {
488 let v1 = obj(&[("kind", s_lit("ok"), false), ("data", Type::String, false)]);
489 let v2 = obj(&[("kind", s_lit("err"), false), ("code", Type::Number, false)]);
490 let members = canonical(vec![v1, v2]);
491 let strat = resolve_json_union_strategy(&members).expect("resolves");
492 match strat.object {
493 PerTagStrategy::ObjectCascade(ObjectDispatch::ByLiteralTagValue { field, arms }) => {
494 assert_eq!(field, "kind");
495 assert_eq!(arms.len(), 2);
496 }
497 other => panic!("expected ByLiteralTagValue, got {other:?}"),
498 }
499 }
500
501 #[test]
502 fn object_cascade_by_required_field_name() {
503 let v1 = obj(&[("a", Type::Number, false)]);
504 let v2 = obj(&[("b", Type::String, false)]);
505 let members = canonical(vec![v1, v2]);
506 let strat = resolve_json_union_strategy(&members).expect("resolves");
507 assert!(matches!(
508 strat.object,
509 PerTagStrategy::ObjectCascade(ObjectDispatch::ByRequiredFieldName { .. })
510 ));
511 }
512
513 #[test]
514 fn shared_shape_with_different_tag_values() {
515 let v1 = obj(&[("kind", s_lit("a"), false), ("x", Type::Number, false)]);
516 let v2 = obj(&[("kind", s_lit("b"), false), ("x", Type::Number, false)]);
517 let members = canonical(vec![v1, v2]);
518 let strat = resolve_json_union_strategy(&members).expect("resolves");
519 assert!(matches!(
520 strat.object,
521 PerTagStrategy::ObjectCascade(ObjectDispatch::ByLiteralTagValue { .. })
522 ));
523 }
524
525 #[test]
526 fn object_ambiguous_subset_required() {
527 let v1 = obj(&[("a", Type::Number, false)]);
528 let v2 = obj(&[("a", Type::Number, false), ("b", Type::String, false)]);
529 let members = canonical(vec![v1, v2]);
530 let reasons = resolve_json_union_strategy(&members).expect_err("fails");
531 assert!(matches!(
532 reasons.as_slice(),
533 [JsonNarrowReason::ObjectAmbiguous { .. }]
534 ));
535 }
536
537 #[test]
538 fn optional_discriminator() {
539 let v1 = obj(&[("kind", s_lit("a"), true), ("x", Type::Number, false)]);
540 let v2 = obj(&[("tag", s_lit("b"), true), ("x", Type::Number, false)]);
541 let members = canonical(vec![v1, v2]);
542 let reasons = resolve_json_union_strategy(&members).expect_err("fails");
543 assert!(matches!(
544 reasons.as_slice(),
545 [JsonNarrowReason::OptionalDiscriminator { .. }]
546 ));
547 }
548
549 #[test]
550 fn array_vs_array() {
551 let members = canonical(vec![
552 Type::Array(Box::new(Type::Number)),
553 Type::Array(Box::new(Type::String)),
554 ]);
555 let reasons = resolve_json_union_strategy(&members).expect_err("fails");
556 assert!(matches!(
557 reasons.as_slice(),
558 [JsonNarrowReason::ArrayVsArray { .. }]
559 ));
560 }
561
562 #[test]
563 fn mixed_buckets_resolve() {
564 let circle = obj(&[("kind", s_lit("circle"), false), ("r", Type::Number, false)]);
565 let rect = obj(&[
566 ("kind", s_lit("rect"), false),
567 ("w", Type::Number, false),
568 ("h", Type::Number, false),
569 ]);
570 let members = canonical(vec![Type::Number, circle, rect, Type::Null]);
571 let strat = resolve_json_union_strategy(&members).expect("resolves");
572 assert_eq!(strat.number, PerTagStrategy::Single(Type::Number));
573 assert_eq!(strat.null, PerTagStrategy::Single(Type::Null));
574 assert!(matches!(
575 strat.object,
576 PerTagStrategy::ObjectCascade(ObjectDispatch::ByLiteralTagValue { .. })
577 ));
578 assert_eq!(strat.boolean, PerTagStrategy::Empty);
579 assert_eq!(strat.string, PerTagStrategy::Empty);
580 assert_eq!(strat.array, PerTagStrategy::Empty);
581 }
582}
583
584#[cfg(test)]
585mod invariant_tests {
586 use super::*;
587
588 #[test]
589 fn unsupported_and_duplicate_bucket_members_are_rejected() {
590 for members in [
591 vec![Type::Unknown],
592 vec![Type::Null, Type::Null],
593 vec![Type::BooleanLiteral(true), Type::BooleanLiteral(false)],
594 ] {
595 assert!(resolve_json_union_strategy(&members).is_err());
596 }
597 }
598
599 #[test]
600 fn inconsistent_object_expansion_is_an_internal_failure() {
601 let calls = std::cell::Cell::new(0);
602 let object = Type::Object {
603 fields: BTreeMap::new(),
604 index: None,
605 };
606 let expand = |_: &Type| {
607 calls.set(calls.get() + 1);
608 if calls.get() <= 2 {
609 object.clone()
610 } else {
611 Type::Number
612 }
613 };
614 let error = resolve_json_union_strategy_with(&[object.clone(), object.clone()], &expand)
615 .unwrap_err();
616 assert!(matches!(
617 error.as_slice(),
618 [JsonNarrowReason::Internal(CompilerFailure::Internal { .. })]
619 ));
620 }
621}