Skip to main content

mangle_analysis/
type_expr.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Type expression utilities for Mangle's structural type system.
16//!
17//! Type expressions are represented as IR instructions (`Inst::ApplyFn` for
18//! compound types, `Inst::Name` for base types). This module provides:
19//!
20//! - Predicates (`is_struct_type`, `is_union_type`, etc.)
21//! - Accessors (`struct_type_field`, `list_type_arg`, etc.)
22//! - Wellformedness validation (`wellformed_type`)
23//! - Type conformance (`set_conforms`, `type_conforms`)
24//! - HasType runtime validation (`has_type`)
25//! - TaggedUnion expansion
26
27use anyhow::{Result, anyhow, bail};
28use fxhash::{FxHashMap, FxHashSet};
29use mangle_ir::{Inst, InstId, Ir, NameId};
30
31// Type constructor names.
32pub const FN_STRUCT: &str = "fn:Struct";
33pub const FN_UNION: &str = "fn:Union";
34pub const FN_TAGGED_UNION: &str = "fn:TaggedUnion";
35pub const FN_SINGLETON: &str = "fn:Singleton";
36pub const FN_LIST: &str = "fn:List";
37pub const FN_MAP: &str = "fn:Map";
38pub const FN_PAIR: &str = "fn:Pair";
39pub const FN_TUPLE: &str = "fn:Tuple";
40pub const FN_FUN: &str = "fn:Fun";
41pub const FN_REL: &str = "fn:Rel";
42pub const FN_OPTION: &str = "fn:Option";
43pub const FN_OPT: &str = "fn:opt";
44pub const FN_EMPTY_TYPE: &str = "fn:EmptyType";
45
46// ---------------------------------------------------------------------------
47// Helpers
48// ---------------------------------------------------------------------------
49
50/// Returns the function name of an `ApplyFn` instruction, or `None`.
51fn apply_fn_name<'a>(ir: &'a Ir, id: InstId) -> Option<&'a str> {
52    if let Inst::ApplyFn { function, .. } = ir.get(id) {
53        Some(ir.resolve_name(*function))
54    } else {
55        None
56    }
57}
58
59/// Returns the args of an `ApplyFn` instruction, or `None`.
60fn apply_fn_args(ir: &Ir, id: InstId) -> Option<&[InstId]> {
61    if let Inst::ApplyFn { args, .. } = ir.get(id) {
62        Some(args.as_slice())
63    } else {
64        None
65    }
66}
67
68/// Returns the name string of a `Name` instruction, or `None`.
69fn name_str<'a>(ir: &'a Ir, id: InstId) -> Option<&'a str> {
70    if let Inst::Name(n) = ir.get(id) {
71        Some(ir.resolve_name(*n))
72    } else {
73        None
74    }
75}
76
77/// Returns the `NameId` of a `Name` instruction, or `None`.
78fn name_id(ir: &Ir, id: InstId) -> Option<NameId> {
79    if let Inst::Name(n) = ir.get(id) {
80        Some(*n)
81    } else {
82        None
83    }
84}
85
86/// True if this is the empty type sentinel `fn:EmptyType()`.
87pub fn is_empty_type(ir: &Ir, id: InstId) -> bool {
88    apply_fn_name(ir, id) == Some(FN_EMPTY_TYPE)
89}
90
91/// True if this is `/any`.
92pub fn is_any(ir: &Ir, id: InstId) -> bool {
93    name_str(ir, id) == Some("/any")
94}
95
96/// Finds or creates a `Name` instruction in the IR.
97pub fn find_or_create_name(ir: &mut Ir, name: &str) -> InstId {
98    if let Some(name_id) = ir.name_store.lookup(name) {
99        for (idx, inst) in ir.insts.iter().enumerate() {
100            if let Inst::Name(n) = inst {
101                if *n == name_id {
102                    return InstId::new(idx);
103                }
104            }
105        }
106    }
107    let n = ir.intern_name(name);
108    ir.add_inst(Inst::Name(n))
109}
110
111/// Creates or finds the empty type sentinel `fn:EmptyType()`.
112pub fn empty_type(ir: &mut Ir) -> InstId {
113    for (idx, inst) in ir.insts.iter().enumerate() {
114        if let Inst::ApplyFn { function, args } = inst {
115            if ir.resolve_name(*function) == FN_EMPTY_TYPE && args.is_empty() {
116                return InstId::new(idx);
117            }
118        }
119    }
120    let fn_name = ir.intern_name(FN_EMPTY_TYPE);
121    ir.add_inst(Inst::ApplyFn {
122        function: fn_name,
123        args: vec![],
124    })
125}
126
127// ---------------------------------------------------------------------------
128// Predicates
129// ---------------------------------------------------------------------------
130
131pub fn is_struct_type(ir: &Ir, id: InstId) -> bool {
132    apply_fn_name(ir, id) == Some(FN_STRUCT)
133}
134
135pub fn is_union_type(ir: &Ir, id: InstId) -> bool {
136    apply_fn_name(ir, id) == Some(FN_UNION)
137}
138
139pub fn is_singleton_type(ir: &Ir, id: InstId) -> bool {
140    apply_fn_name(ir, id) == Some(FN_SINGLETON)
141}
142
143pub fn is_tagged_union_type(ir: &Ir, id: InstId) -> bool {
144    apply_fn_name(ir, id) == Some(FN_TAGGED_UNION)
145}
146
147pub fn is_list_type(ir: &Ir, id: InstId) -> bool {
148    apply_fn_name(ir, id) == Some(FN_LIST)
149}
150
151pub fn is_map_type(ir: &Ir, id: InstId) -> bool {
152    apply_fn_name(ir, id) == Some(FN_MAP)
153}
154
155pub fn is_pair_type(ir: &Ir, id: InstId) -> bool {
156    apply_fn_name(ir, id) == Some(FN_PAIR)
157}
158
159pub fn is_tuple_type(ir: &Ir, id: InstId) -> bool {
160    apply_fn_name(ir, id) == Some(FN_TUPLE)
161}
162
163pub fn is_fun_type(ir: &Ir, id: InstId) -> bool {
164    apply_fn_name(ir, id) == Some(FN_FUN)
165}
166
167pub fn is_rel_type(ir: &Ir, id: InstId) -> bool {
168    apply_fn_name(ir, id) == Some(FN_REL)
169}
170
171pub fn is_option_type(ir: &Ir, id: InstId) -> bool {
172    apply_fn_name(ir, id) == Some(FN_OPTION)
173}
174
175pub fn is_opt_field(ir: &Ir, id: InstId) -> bool {
176    apply_fn_name(ir, id) == Some(FN_OPT)
177}
178
179/// True if this is a base type name constant (e.g. `/number`, `/string`, `/any`).
180pub fn is_base_type(ir: &Ir, id: InstId) -> bool {
181    if let Some(name) = name_str(ir, id) {
182        matches!(
183            name,
184            "/any"
185                | "/bot"
186                | "/number"
187                | "/float64"
188                | "/string"
189                | "/bytes"
190                | "/name"
191                | "/bool"
192                | "/time"
193                | "/duration"
194                | "/unit"
195        )
196    } else {
197        false
198    }
199}
200
201/// True if this is a `Inst::Name` (any name constant, including base types
202/// and user-defined name hierarchy members like `/animal/dog`).
203pub fn is_name_const(ir: &Ir, id: InstId) -> bool {
204    matches!(ir.get(id), Inst::Name(_))
205}
206
207/// True if this is a type variable (`Inst::Var`).
208pub fn is_type_var(ir: &Ir, id: InstId) -> bool {
209    matches!(ir.get(id), Inst::Var(_))
210}
211
212// ---------------------------------------------------------------------------
213// Accessors
214// ---------------------------------------------------------------------------
215
216/// Returns the element type of a `fn:List` type expression.
217pub fn list_type_arg(ir: &Ir, id: InstId) -> Option<InstId> {
218    let args = apply_fn_args(ir, id)?;
219    if apply_fn_name(ir, id) != Some(FN_LIST) {
220        return None;
221    }
222    args.first().copied()
223}
224
225/// Returns `(key_type, value_type)` of a `fn:Map` type expression.
226pub fn map_type_args(ir: &Ir, id: InstId) -> Option<(InstId, InstId)> {
227    let args = apply_fn_args(ir, id)?;
228    if apply_fn_name(ir, id) != Some(FN_MAP) || args.len() != 2 {
229        return None;
230    }
231    Some((args[0], args[1]))
232}
233
234/// Returns the alternative type expressions of a `fn:Union`.
235pub fn union_type_args(ir: &Ir, id: InstId) -> Option<&[InstId]> {
236    if apply_fn_name(ir, id) != Some(FN_UNION) {
237        return None;
238    }
239    apply_fn_args(ir, id)
240}
241
242/// Returns the tag field `InstId` (a Name) of a `fn:TaggedUnion`.
243pub fn tagged_union_tag_field(ir: &Ir, id: InstId) -> Option<InstId> {
244    if apply_fn_name(ir, id) != Some(FN_TAGGED_UNION) {
245        return None;
246    }
247    apply_fn_args(ir, id).and_then(|args| args.first().copied())
248}
249
250/// Returns `(tags, variant_struct_types)` for a `fn:TaggedUnion`.
251/// Tags are at odd indices starting from 1, variant structs at even indices from 2.
252pub fn tagged_union_variants(ir: &Ir, id: InstId) -> Option<(Vec<InstId>, Vec<InstId>)> {
253    if apply_fn_name(ir, id) != Some(FN_TAGGED_UNION) {
254        return None;
255    }
256    let args = apply_fn_args(ir, id)?;
257    if args.len() < 3 || args.len() % 2 == 0 {
258        return None;
259    }
260    let mut tags = Vec::new();
261    let mut structs = Vec::new();
262    for i in (1..args.len()).step_by(2) {
263        tags.push(args[i]);
264        structs.push(args[i + 1]);
265    }
266    Some((tags, structs))
267}
268
269/// Iterates struct type args, yielding `(field_name_id, field_type_id, is_optional)`.
270///
271/// Struct args are a flat list where:
272/// - Required fields contribute 2 consecutive args: `[Name(field), type_expr]`
273/// - Optional fields contribute 1 arg: `[ApplyFn("fn:opt", [Name(field), type_expr])]`
274pub fn struct_type_fields(ir: &Ir, id: InstId) -> Vec<(InstId, InstId, bool)> {
275    let args = match apply_fn_args(ir, id) {
276        Some(a) if is_struct_type(ir, id) => a,
277        _ => return Vec::new(),
278    };
279    let mut result = Vec::new();
280    let mut i = 0;
281    while i < args.len() {
282        if is_opt_field(ir, args[i]) {
283            // Optional field: fn:opt(field_name, field_type)
284            if let Some(opt_args) = apply_fn_args(ir, args[i]) {
285                if opt_args.len() == 2 {
286                    result.push((opt_args[0], opt_args[1], true));
287                }
288            }
289            i += 1;
290        } else {
291            // Required field: field_name, field_type
292            if i + 1 < args.len() {
293                result.push((args[i], args[i + 1], false));
294            }
295            i += 2;
296        }
297    }
298    result
299}
300
301/// Returns the type of a struct field by name (simple struct only).
302pub fn struct_type_field(ir: &Ir, type_id: InstId, field: NameId) -> Option<InstId> {
303    if !is_struct_type(ir, type_id) {
304        return None;
305    }
306    let field_name = ir.resolve_name(field);
307    for (fname_id, ftype_id, _optional) in struct_type_fields(ir, type_id) {
308        if let Some(n) = name_str(ir, fname_id) {
309            if n == field_name {
310                return Some(ftype_id);
311            }
312        }
313    }
314    None
315}
316
317/// Returns the type of a struct field by name. Handles Struct, Union, and TaggedUnion
318/// by projecting the field from each alternative and returning their upper bound.
319pub fn struct_type_field_deep(ir: &mut Ir, type_id: InstId, field: NameId) -> Option<InstId> {
320    if is_struct_type(ir, type_id) {
321        return struct_type_field(ir, type_id, field);
322    }
323    if is_tagged_union_type(ir, type_id) {
324        let tag_field = tagged_union_tag_field(ir, type_id)?;
325        let (tags, structs) = tagged_union_variants(ir, type_id)?;
326        let field_name = ir.resolve_name(field).to_string();
327        let tag_field_name = name_str(ir, tag_field)?.to_string();
328
329        if field_name == tag_field_name {
330            // Tag field type = union of singleton types for each tag value.
331            let singleton_fn = ir.intern_name(FN_SINGLETON);
332            let mut tag_types = Vec::new();
333            for tag in &tags {
334                let s = ir.add_inst(Inst::ApplyFn {
335                    function: singleton_fn,
336                    args: vec![*tag],
337                });
338                tag_types.push(s);
339            }
340            return Some(new_union_or_single(ir, tag_types));
341        }
342        let mut field_types = Vec::new();
343        for variant_struct in &structs {
344            let vfield_name = ir.intern_name(&field_name);
345            if let Some(ft) = struct_type_field(ir, *variant_struct, vfield_name) {
346                field_types.push(ft);
347            }
348        }
349        if field_types.is_empty() {
350            return None;
351        }
352        let ctx = TypeContext::default();
353        return Some(upper_bound(ir, &ctx, &field_types));
354    }
355    if is_union_type(ir, type_id) {
356        let args = union_type_args(ir, type_id)?.to_vec();
357        let mut field_types = Vec::new();
358        for alt in &args {
359            if let Some(ft) = struct_type_field_deep(ir, *alt, field) {
360                field_types.push(ft);
361            } else {
362                return None; // Field not present in all alternatives.
363            }
364        }
365        if field_types.is_empty() {
366            return None;
367        }
368        let ctx = TypeContext::default();
369        return Some(upper_bound(ir, &ctx, &field_types));
370    }
371    None
372}
373
374// ---------------------------------------------------------------------------
375// TaggedUnion expansion
376// ---------------------------------------------------------------------------
377
378/// Expands `fn:TaggedUnion(tag_field, tag1, struct1, tag2, struct2, ...)`
379/// into `fn:Union(fn:Struct(tag_field, fn:Singleton(tag1), ...fields1), ...)`.
380///
381/// Uses `fn:Singleton` for tag field values (precise expansion for HasType).
382pub fn expand_tagged_union_type(ir: &mut Ir, tu_id: InstId) -> Result<InstId> {
383    let (tag_field_id, tags, structs) = {
384        let args = apply_fn_args(ir, tu_id)
385            .ok_or_else(|| anyhow!("not a TaggedUnion"))?
386            .to_vec();
387        if args.len() < 3 || args.len() % 2 == 0 {
388            bail!("TaggedUnion: bad arity");
389        }
390        let tag_field = args[0];
391        let mut tags = Vec::new();
392        let mut structs = Vec::new();
393        for i in (1..args.len()).step_by(2) {
394            tags.push(args[i]);
395            structs.push(args[i + 1]);
396        }
397        (tag_field, tags, structs)
398    };
399
400    let singleton_name = ir.intern_name(FN_SINGLETON);
401    let struct_name = ir.intern_name(FN_STRUCT);
402    let union_name = ir.intern_name(FN_UNION);
403
404    let mut variant_structs = Vec::new();
405    for (tag_id, struct_id) in tags.iter().zip(structs.iter()) {
406        // Build fn:Singleton(tag_value)
407        let singleton = ir.add_inst(Inst::ApplyFn {
408            function: singleton_name,
409            args: vec![*tag_id],
410        });
411
412        // Collect fields from variant struct
413        let variant_fields: Vec<InstId> =
414            apply_fn_args(ir, *struct_id).unwrap_or(&[]).to_vec();
415
416        // Build new struct: [tag_field, Singleton(tag), ...variant_fields]
417        let mut new_args = vec![tag_field_id, singleton];
418        new_args.extend_from_slice(&variant_fields);
419
420        let new_struct = ir.add_inst(Inst::ApplyFn {
421            function: struct_name,
422            args: new_args,
423        });
424        variant_structs.push(new_struct);
425    }
426
427    let union_id = ir.add_inst(Inst::ApplyFn {
428        function: union_name,
429        args: variant_structs,
430    });
431    Ok(union_id)
432}
433
434/// Like `expand_tagged_union_type` but uses `/name` instead of `fn:Singleton`
435/// for the tag field type. This is less precise but compatible with the bounds
436/// checker's type inference (which infers `/name` for name constants in facts).
437pub fn expand_tagged_union_for_bounds(ir: &mut Ir, tu_id: InstId) -> Result<InstId> {
438    let (tag_field_id, tags, structs) = {
439        let args = apply_fn_args(ir, tu_id)
440            .ok_or_else(|| anyhow!("not a TaggedUnion"))?
441            .to_vec();
442        if args.len() < 3 || args.len() % 2 == 0 {
443            bail!("TaggedUnion: bad arity");
444        }
445        let tag_field = args[0];
446        let mut tags = Vec::new();
447        let mut structs = Vec::new();
448        for i in (1..args.len()).step_by(2) {
449            tags.push(args[i]);
450            structs.push(args[i + 1]);
451        }
452        (tag_field, tags, structs)
453    };
454
455    let name_type = ir.intern_name("/name");
456    let name_type_id = ir.add_inst(Inst::Name(name_type));
457    let struct_name = ir.intern_name(FN_STRUCT);
458    let union_name = ir.intern_name(FN_UNION);
459
460    let mut variant_structs = Vec::new();
461    for (_tag_id, struct_id) in tags.iter().zip(structs.iter()) {
462        let variant_fields: Vec<InstId> =
463            apply_fn_args(ir, *struct_id).unwrap_or(&[]).to_vec();
464
465        let mut new_args = vec![tag_field_id, name_type_id];
466        new_args.extend_from_slice(&variant_fields);
467
468        let new_struct = ir.add_inst(Inst::ApplyFn {
469            function: struct_name,
470            args: new_args,
471        });
472        variant_structs.push(new_struct);
473    }
474
475    let union_id = ir.add_inst(Inst::ApplyFn {
476        function: union_name,
477        args: variant_structs,
478    });
479    Ok(union_id)
480}
481
482// ---------------------------------------------------------------------------
483// Wellformedness
484// ---------------------------------------------------------------------------
485
486/// Type variable context: maps variable NameId to its bound (an InstId type expr).
487pub type TypeContext = FxHashMap<NameId, InstId>;
488
489/// Validates that a type expression is well-formed.
490pub fn wellformed_type(ir: &Ir, ctx: &TypeContext, id: InstId) -> Result<()> {
491    match ir.get(id) {
492        Inst::Name(n) => {
493            let name = ir.resolve_name(*n);
494            // Base types and name constants are always well-formed.
495            if is_base_type(ir, id) || name.starts_with('/') {
496                return Ok(());
497            }
498            bail!("unknown type name: {}", name)
499        }
500        Inst::Var(v) => {
501            if ctx.contains_key(v) {
502                Ok(())
503            } else {
504                bail!(
505                    "type variable {} not in context",
506                    ir.resolve_name(*v)
507                )
508            }
509        }
510        Inst::ApplyFn { function, args } => {
511            let fname = ir.resolve_name(*function);
512            match fname {
513                FN_STRUCT => check_struct_type_expr(ir, ctx, args),
514                FN_UNION => {
515                    if args.len() < 2 {
516                        bail!("fn:Union requires at least 2 alternatives");
517                    }
518                    for arg in args {
519                        wellformed_type(ir, ctx, *arg)?;
520                    }
521                    Ok(())
522                }
523                FN_SINGLETON => {
524                    if args.len() != 1 {
525                        bail!("fn:Singleton requires exactly 1 argument");
526                    }
527                    // Argument must be a constant.
528                    match ir.get(args[0]) {
529                        Inst::Name(_) | Inst::Number(_) | Inst::String(_)
530                        | Inst::Float(_) | Inst::Bool(_) | Inst::Time(_)
531                        | Inst::Duration(_) => Ok(()),
532                        _ => bail!("fn:Singleton argument must be a constant"),
533                    }
534                }
535                FN_LIST => {
536                    if args.len() != 1 {
537                        bail!("fn:List requires exactly 1 argument");
538                    }
539                    wellformed_type(ir, ctx, args[0])
540                }
541                FN_MAP => {
542                    if args.len() != 2 {
543                        bail!("fn:Map requires exactly 2 arguments");
544                    }
545                    wellformed_type(ir, ctx, args[0])?;
546                    wellformed_type(ir, ctx, args[1])
547                }
548                FN_PAIR => {
549                    if args.len() != 2 {
550                        bail!("fn:Pair requires exactly 2 arguments");
551                    }
552                    wellformed_type(ir, ctx, args[0])?;
553                    wellformed_type(ir, ctx, args[1])
554                }
555                FN_TUPLE => {
556                    if args.len() < 2 {
557                        bail!("fn:Tuple requires at least 2 arguments");
558                    }
559                    for arg in args {
560                        wellformed_type(ir, ctx, *arg)?;
561                    }
562                    Ok(())
563                }
564                FN_OPTION => {
565                    if args.len() != 1 {
566                        bail!("fn:Option requires exactly 1 argument");
567                    }
568                    wellformed_type(ir, ctx, args[0])
569                }
570                FN_FUN => check_fun_type_expr(ir, ctx, args),
571                FN_REL => {
572                    if args.is_empty() {
573                        bail!("fn:Rel requires at least 1 argument");
574                    }
575                    for arg in args {
576                        wellformed_type(ir, ctx, *arg)?;
577                    }
578                    Ok(())
579                }
580                FN_TAGGED_UNION => check_tagged_union_type_expr(ir, ctx, args),
581                FN_OPT => {
582                    // fn:opt is only valid inside fn:Struct, checked there.
583                    if args.len() != 2 {
584                        bail!("fn:opt requires exactly 2 arguments");
585                    }
586                    if !is_name_const(ir, args[0]) {
587                        bail!("fn:opt first argument must be a name constant");
588                    }
589                    wellformed_type(ir, ctx, args[1])
590                }
591                other => bail!("unknown type constructor: {}", other),
592            }
593        }
594        _ => bail!("not a valid type expression"),
595    }
596}
597
598fn check_struct_type_expr(ir: &Ir, ctx: &TypeContext, args: &[InstId]) -> Result<()> {
599    let mut i = 0;
600    let mut seen_fields = FxHashSet::default();
601    while i < args.len() {
602        if is_opt_field(ir, args[i]) {
603            // Optional field: fn:opt(field_name, field_type)
604            let opt_args = apply_fn_args(ir, args[i])
605                .ok_or_else(|| anyhow!("malformed fn:opt"))?;
606            if opt_args.len() != 2 {
607                bail!("fn:opt requires 2 arguments");
608            }
609            let field_name = name_str(ir, opt_args[0])
610                .ok_or_else(|| anyhow!("fn:opt field name must be a name constant"))?;
611            if !seen_fields.insert(field_name.to_string()) {
612                bail!("duplicate struct field: {}", field_name);
613            }
614            wellformed_type(ir, ctx, opt_args[1])?;
615            i += 1;
616        } else {
617            // Required field: field_name, field_type
618            if i + 1 >= args.len() {
619                bail!("fn:Struct: odd number of args (missing type for last field)");
620            }
621            let field_name = name_str(ir, args[i])
622                .ok_or_else(|| anyhow!("fn:Struct field name must be a name constant"))?;
623            if !seen_fields.insert(field_name.to_string()) {
624                bail!("duplicate struct field: {}", field_name);
625            }
626            wellformed_type(ir, ctx, args[i + 1])?;
627            i += 2;
628        }
629    }
630    Ok(())
631}
632
633fn check_tagged_union_type_expr(
634    ir: &Ir,
635    ctx: &TypeContext,
636    args: &[InstId],
637) -> Result<()> {
638    if args.len() < 3 {
639        bail!("fn:TaggedUnion requires at least 3 arguments (tag_field, tag, struct)");
640    }
641    if args.len() % 2 == 0 {
642        bail!("fn:TaggedUnion requires odd number of arguments");
643    }
644
645    // Arg 0: tag field must be a name constant.
646    let tag_field_name = name_str(ir, args[0])
647        .ok_or_else(|| anyhow!("fn:TaggedUnion tag field must be a name constant"))?;
648
649    let mut seen_tags = FxHashSet::default();
650    for i in (1..args.len()).step_by(2) {
651        // Tag value must be a name constant.
652        let tag_name = name_str(ir, args[i])
653            .ok_or_else(|| anyhow!("fn:TaggedUnion variant tag must be a name constant"))?;
654        if !seen_tags.insert(tag_name.to_string()) {
655            bail!("fn:TaggedUnion duplicate variant tag: {}", tag_name);
656        }
657
658        // Variant type must be a struct type expression.
659        let variant = args[i + 1];
660        if !is_struct_type(ir, variant) {
661            bail!(
662                "fn:TaggedUnion variant type for {} must be fn:Struct",
663                tag_name
664            );
665        }
666        wellformed_type(ir, ctx, variant)?;
667
668        // Tag field must NOT appear in variant struct.
669        for (fname_id, _ftype_id, _optional) in struct_type_fields(ir, variant) {
670            if let Some(n) = name_str(ir, fname_id) {
671                if n == tag_field_name {
672                    bail!(
673                        "fn:TaggedUnion tag field {} must not appear in variant struct for {}",
674                        tag_field_name,
675                        tag_name
676                    );
677                }
678            }
679        }
680    }
681
682    Ok(())
683}
684
685fn check_fun_type_expr(ir: &Ir, ctx: &TypeContext, args: &[InstId]) -> Result<()> {
686    if args.is_empty() {
687        bail!("fn:Fun requires at least 1 argument (the result type)");
688    }
689    for arg in args {
690        wellformed_type(ir, ctx, *arg)?;
691    }
692    Ok(())
693}
694
695// ---------------------------------------------------------------------------
696// Type Conformance
697// ---------------------------------------------------------------------------
698
699/// Checks if `left` set-conforms to `right`.
700///
701/// - Union on left: ALL alternatives must conform to `right`.
702/// - Union on right: `left` must conform to SOME alternative.
703/// - TaggedUnion: expanded before checking.
704pub fn set_conforms(ir: &Ir, ctx: &TypeContext, left: InstId, right: InstId) -> bool {
705    // Identity.
706    if left == right {
707        return true;
708    }
709
710    // /any is top type.
711    if name_str(ir, right) == Some("/any") {
712        return true;
713    }
714    // /bot is bottom type.
715    if name_str(ir, left) == Some("/bot") {
716        return true;
717    }
718
719    // Expand tagged unions on both sides (conceptually—we can't mutate ir here,
720    // so we handle them by delegating to their variant structure).
721    // TaggedUnion on left: each variant struct (with tag field) must conform.
722    if is_tagged_union_type(ir, left) {
723        return tagged_union_set_conforms_left(ir, ctx, left, right);
724    }
725    // TaggedUnion on right: left must conform to some expanded variant.
726    if is_tagged_union_type(ir, right) {
727        return tagged_union_set_conforms_right(ir, ctx, left, right);
728    }
729
730    // Union on left: ALL alternatives must conform.
731    if let Some(left_args) = union_type_args(ir, left) {
732        let left_args = left_args.to_vec();
733        return left_args
734            .iter()
735            .all(|alt| set_conforms(ir, ctx, *alt, right));
736    }
737
738    // Union on right: left must conform to SOME alternative.
739    if let Some(right_args) = union_type_args(ir, right) {
740        let right_args = right_args.to_vec();
741        return right_args
742            .iter()
743            .any(|alt| set_conforms(ir, ctx, left, *alt));
744    }
745
746    type_conforms(ir, ctx, left, right)
747}
748
749/// Checks structural type conformance (non-union, non-tagged-union).
750pub fn type_conforms(ir: &Ir, ctx: &TypeContext, left: InstId, right: InstId) -> bool {
751    if left == right {
752        return true;
753    }
754    if name_str(ir, right) == Some("/any") {
755        return true;
756    }
757    if name_str(ir, left) == Some("/bot") {
758        return true;
759    }
760
761    // Name hierarchy: /foo/bar conforms to /foo, /name, /any.
762    if let (Some(left_name), Some(right_name)) = (name_str(ir, left), name_str(ir, right)) {
763        if right_name == "/name" && left_name.starts_with('/') {
764            return true;
765        }
766        return left_name.starts_with(right_name)
767            && (left_name.len() == right_name.len()
768                || left_name.as_bytes().get(right_name.len()) == Some(&b'/'));
769    }
770
771    // Singleton conformance: fn:Singleton(c) <: T if c has type T.
772    if is_singleton_type(ir, left) {
773        if let Some(args) = apply_fn_args(ir, left) {
774            if args.len() == 1 {
775                return const_has_base_type(ir, args[0], right);
776            }
777        }
778    }
779
780    // Type variable: look up bound in context.
781    if let Inst::Var(v) = ir.get(left) {
782        if let Some(&bound) = ctx.get(v) {
783            return set_conforms(ir, ctx, bound, right);
784        }
785    }
786    if let Inst::Var(v) = ir.get(right) {
787        if let Some(&bound) = ctx.get(v) {
788            return set_conforms(ir, ctx, left, bound);
789        }
790    }
791
792    let left_fn = apply_fn_name(ir, left);
793    let right_fn = apply_fn_name(ir, right);
794    if left_fn != right_fn {
795        return false;
796    }
797
798    match left_fn {
799        Some(FN_LIST) => {
800            // Covariant.
801            let la = apply_fn_args(ir, left).unwrap();
802            let ra = apply_fn_args(ir, right).unwrap();
803            la.len() == 1 && ra.len() == 1 && set_conforms(ir, ctx, la[0], ra[0])
804        }
805        Some(FN_MAP) => {
806            // Covariant in both key and value.
807            let la = apply_fn_args(ir, left).unwrap();
808            let ra = apply_fn_args(ir, right).unwrap();
809            la.len() == 2
810                && ra.len() == 2
811                && set_conforms(ir, ctx, la[0], ra[0])
812                && set_conforms(ir, ctx, la[1], ra[1])
813        }
814        Some(FN_PAIR) => {
815            let la = apply_fn_args(ir, left).unwrap();
816            let ra = apply_fn_args(ir, right).unwrap();
817            la.len() == 2
818                && ra.len() == 2
819                && set_conforms(ir, ctx, la[0], ra[0])
820                && set_conforms(ir, ctx, la[1], ra[1])
821        }
822        Some(FN_TUPLE) => {
823            let la = apply_fn_args(ir, left).unwrap();
824            let ra = apply_fn_args(ir, right).unwrap();
825            la.len() == ra.len()
826                && la
827                    .iter()
828                    .zip(ra.iter())
829                    .all(|(l, r)| set_conforms(ir, ctx, *l, *r))
830        }
831        Some(FN_STRUCT) => struct_type_conforms(ir, ctx, left, right),
832        Some(FN_SINGLETON) => {
833            let la = apply_fn_args(ir, left).unwrap();
834            let ra = apply_fn_args(ir, right).unwrap();
835            la.len() == 1 && ra.len() == 1 && ir_eq(ir, la[0], ra[0])
836        }
837        Some(FN_FUN) => {
838            // Covariant in codomain, contravariant in domain.
839            let la = apply_fn_args(ir, left).unwrap();
840            let ra = apply_fn_args(ir, right).unwrap();
841            if la.len() != ra.len() || la.is_empty() {
842                return false;
843            }
844            // Result type (first arg): covariant.
845            if !set_conforms(ir, ctx, la[0], ra[0]) {
846                return false;
847            }
848            // Argument types: contravariant.
849            la[1..]
850                .iter()
851                .zip(ra[1..].iter())
852                .all(|(l, r)| set_conforms(ir, ctx, *r, *l))
853        }
854        Some(FN_REL) => {
855            let la = apply_fn_args(ir, left).unwrap();
856            let ra = apply_fn_args(ir, right).unwrap();
857            la.len() == ra.len()
858                && la
859                    .iter()
860                    .zip(ra.iter())
861                    .all(|(l, r)| set_conforms(ir, ctx, *l, *r))
862        }
863        Some(FN_OPTION) => {
864            let la = apply_fn_args(ir, left).unwrap();
865            let ra = apply_fn_args(ir, right).unwrap();
866            la.len() == 1 && ra.len() == 1 && set_conforms(ir, ctx, la[0], ra[0])
867        }
868        _ => false,
869    }
870}
871
872/// Struct subtyping: `left` conforms to `right` if:
873/// - All required fields of `right` are present in `left` with conforming types.
874/// - Optional fields of `right` may be absent in `left`.
875/// - `left` may have extra fields.
876fn struct_type_conforms(
877    ir: &Ir,
878    ctx: &TypeContext,
879    left: InstId,
880    right: InstId,
881) -> bool {
882    let left_fields = struct_type_fields(ir, left);
883    let right_fields = struct_type_fields(ir, right);
884
885    // Build map of left fields: field_name_str -> (type_id, is_optional).
886    let left_map: FxHashMap<String, (InstId, bool)> = left_fields
887        .iter()
888        .filter_map(|(fname, ftype, opt)| {
889            name_str(ir, *fname).map(|n| (n.to_string(), (*ftype, *opt)))
890        })
891        .collect();
892
893    for (fname, ftype, is_optional) in &right_fields {
894        if let Some(fname_str) = name_str(ir, *fname) {
895            match left_map.get(fname_str) {
896                Some((left_type, _left_opt)) => {
897                    if !set_conforms(ir, ctx, *left_type, *ftype) {
898                        return false;
899                    }
900                }
901                None => {
902                    // Required field missing in left.
903                    if !is_optional {
904                        return false;
905                    }
906                }
907            }
908        }
909    }
910    true
911}
912
913/// Helper: checks whether a TaggedUnion on the left conforms to some right type.
914fn tagged_union_set_conforms_left(
915    ir: &Ir,
916    ctx: &TypeContext,
917    left: InstId,
918    right: InstId,
919) -> bool {
920    // Each variant of the tagged union must conform to right.
921    if let Some((tags, structs)) = tagged_union_variants(ir, left) {
922        let tag_field = tagged_union_tag_field(ir, left).unwrap();
923        for (tag, variant_struct) in tags.iter().zip(structs.iter()) {
924            // Conceptually: the expanded struct has tag_field: Singleton(tag) + variant fields.
925            // We check conformance structurally without actually creating the expanded node.
926            if !expanded_variant_conforms(ir, ctx, tag_field, *tag, *variant_struct, right) {
927                return false;
928            }
929        }
930        true
931    } else {
932        false
933    }
934}
935
936/// Helper: checks whether some left type conforms to a TaggedUnion on the right.
937fn tagged_union_set_conforms_right(
938    ir: &Ir,
939    ctx: &TypeContext,
940    left: InstId,
941    right: InstId,
942) -> bool {
943    // left must conform to at least one expanded variant of the tagged union.
944    if let Some((tags, structs)) = tagged_union_variants(ir, right) {
945        let tag_field = tagged_union_tag_field(ir, right).unwrap();
946        for (tag, variant_struct) in tags.iter().zip(structs.iter()) {
947            if expanded_variant_conforms_right(ir, ctx, left, tag_field, *tag, *variant_struct) {
948                return true;
949            }
950        }
951        false
952    } else {
953        false
954    }
955}
956
957/// Checks that an expanded variant (tag_field: /name, ...variant_fields)
958/// conforms to `right`.
959fn expanded_variant_conforms(
960    ir: &Ir,
961    ctx: &TypeContext,
962    _tag_field: InstId,
963    _tag: InstId,
964    variant_struct: InstId,
965    right: InstId,
966) -> bool {
967    // For bounds-style checking: expanded variant uses /name for tag field.
968    // We can't easily construct the expanded struct without &mut Ir,
969    // so we check the variant struct directly and accept that the tag field
970    // adds conformance to /name.
971    // Simple approximation: the variant struct conforms if right is /any or
972    // right is also a struct that the variant matches.
973    if name_str(ir, right) == Some("/any") {
974        return true;
975    }
976    // If right is a struct type, check that variant fields are a superset.
977    if is_struct_type(ir, right) {
978        // The variant struct + tag field must cover right's fields.
979        return set_conforms(ir, ctx, variant_struct, right);
980    }
981    // If right is a union, check that the expanded variant conforms to some alt.
982    if let Some(alts) = union_type_args(ir, right) {
983        let alts = alts.to_vec();
984        return alts.iter().any(|alt| {
985            expanded_variant_conforms(ir, ctx, _tag_field, _tag, variant_struct, *alt)
986        });
987    }
988    false
989}
990
991/// Checks that `left` conforms to an expanded variant of a tagged union.
992fn expanded_variant_conforms_right(
993    ir: &Ir,
994    ctx: &TypeContext,
995    left: InstId,
996    _tag_field: InstId,
997    _tag: InstId,
998    variant_struct: InstId,
999) -> bool {
1000    // left conforms to the expanded variant if it conforms to the variant struct
1001    // (ignoring the tag field, which is added by expansion).
1002    set_conforms(ir, ctx, left, variant_struct)
1003}
1004
1005/// Checks if a constant instruction has a given base type.
1006fn const_has_base_type(ir: &Ir, const_id: InstId, type_id: InstId) -> bool {
1007    let type_name = match name_str(ir, type_id) {
1008        Some(n) => n,
1009        None => return false,
1010    };
1011    match ir.get(const_id) {
1012        Inst::Number(_) => type_name == "/number" || type_name == "/any",
1013        Inst::Float(_) => type_name == "/float64" || type_name == "/any",
1014        Inst::String(_) => type_name == "/string" || type_name == "/any",
1015        Inst::Bool(_) => type_name == "/bool" || type_name == "/any",
1016        Inst::Time(_) => type_name == "/time" || type_name == "/any",
1017        Inst::Duration(_) => type_name == "/duration" || type_name == "/any",
1018        Inst::Bytes(_) => type_name == "/bytes" || type_name == "/any",
1019        Inst::Name(n) => {
1020            if type_name == "/name" || type_name == "/any" {
1021                return true;
1022            }
1023            // Name hierarchy: /foo/bar has type /foo.
1024            let name = ir.resolve_name(*n);
1025            name.starts_with(type_name)
1026                && (name.len() == type_name.len()
1027                    || name.as_bytes().get(type_name.len()) == Some(&b'/'))
1028        }
1029        _ => false,
1030    }
1031}
1032
1033/// Checks structural IR equality (same instruction kind and content).
1034fn ir_eq(ir: &Ir, a: InstId, b: InstId) -> bool {
1035    if a == b {
1036        return true;
1037    }
1038    match (ir.get(a), ir.get(b)) {
1039        (Inst::Name(na), Inst::Name(nb)) => na == nb,
1040        (Inst::Number(na), Inst::Number(nb)) => na == nb,
1041        (Inst::Float(fa), Inst::Float(fb)) => fa.to_bits() == fb.to_bits(),
1042        (Inst::String(sa), Inst::String(sb)) => sa == sb,
1043        (Inst::Bool(ba), Inst::Bool(bb)) => ba == bb,
1044        _ => false,
1045    }
1046}
1047
1048// ---------------------------------------------------------------------------
1049// Upper bound / Lower bound / Intersection
1050// ---------------------------------------------------------------------------
1051
1052/// Computes the upper bound (least common supertype) of a set of type expressions.
1053///
1054/// Flattens unions, eliminates redundant types via SetConforms, returns single
1055/// type or Union. Returns EmptyType for empty input, `/any` if any input is `/any`.
1056pub fn upper_bound(ir: &mut Ir, ctx: &TypeContext, type_exprs: &[InstId]) -> InstId {
1057    let mut worklist = Vec::new();
1058    for &te in type_exprs {
1059        if is_any(ir, te) {
1060            return find_or_create_name(ir, "/any");
1061        }
1062        if let Some(args) = union_type_args(ir, te) {
1063            let args = args.to_vec();
1064            worklist.extend(args);
1065        } else {
1066            worklist.push(te);
1067        }
1068    }
1069    if worklist.is_empty() {
1070        return empty_type(ir);
1071    }
1072    let mut reduced = vec![worklist[0]];
1073    'outer: for &te in &worklist[1..] {
1074        for i in 0..reduced.len() {
1075            if set_conforms(ir, ctx, te, reduced[i]) {
1076                continue 'outer;
1077            }
1078            if set_conforms(ir, ctx, reduced[i], te) {
1079                reduced[i] = te;
1080                continue 'outer;
1081            }
1082        }
1083        reduced.push(te);
1084    }
1085    if reduced.len() == 1 {
1086        return reduced[0];
1087    }
1088    new_union_or_single(ir, reduced)
1089}
1090
1091/// Computes the intersection (greatest lower bound) of two type expressions.
1092pub fn intersect_type(ir: &mut Ir, ctx: &TypeContext, a: InstId, b: InstId) -> InstId {
1093    if ir_eq(ir, a, b) {
1094        return a;
1095    }
1096    if is_any(ir, a) {
1097        return b;
1098    }
1099    if is_any(ir, b) {
1100        return a;
1101    }
1102    // Type variable resolution.
1103    if let Inst::Var(v) = ir.get(a) {
1104        let v = *v;
1105        if let Some(&bound) = ctx.get(&v) {
1106            return intersect_type(ir, ctx, bound, b);
1107        }
1108        return empty_type(ir);
1109    }
1110    if let Inst::Var(v) = ir.get(b) {
1111        let v = *v;
1112        if let Some(&bound) = ctx.get(&v) {
1113            return intersect_type(ir, ctx, a, bound);
1114        }
1115        return empty_type(ir);
1116    }
1117    if set_conforms(ir, ctx, a, b) {
1118        return a;
1119    }
1120    if set_conforms(ir, ctx, b, a) {
1121        return b;
1122    }
1123    // Union decomposition on a.
1124    if is_union_type(ir, a) {
1125        let args = union_type_args(ir, a).unwrap().to_vec();
1126        let mut res = Vec::new();
1127        for elem in args {
1128            let u = intersect_type(ir, ctx, elem, b);
1129            if !is_empty_type(ir, u) {
1130                res.push(u);
1131            }
1132        }
1133        return upper_bound(ir, ctx, &res);
1134    }
1135    // Union decomposition on b.
1136    if is_union_type(ir, b) {
1137        let args = union_type_args(ir, b).unwrap().to_vec();
1138        let mut res = Vec::new();
1139        for elem in args {
1140            if set_conforms(ir, ctx, a, elem) {
1141                res.push(a);
1142            } else if set_conforms(ir, ctx, elem, a) {
1143                res.push(elem);
1144            }
1145        }
1146        return upper_bound(ir, ctx, &res);
1147    }
1148    empty_type(ir)
1149}
1150
1151/// Computes the lower bound (most general common subtype) of a set of type expressions.
1152///
1153/// Repeatedly intersects types. Returns EmptyType if intersection is empty.
1154pub fn lower_bound(ir: &mut Ir, ctx: &TypeContext, type_exprs: &[InstId]) -> InstId {
1155    let any = find_or_create_name(ir, "/any");
1156    let mut result = any;
1157    for &t in type_exprs {
1158        result = intersect_type(ir, ctx, result, t);
1159        if is_empty_type(ir, result) {
1160            return result;
1161        }
1162    }
1163    result
1164}
1165
1166// ---------------------------------------------------------------------------
1167// HasType — Runtime type checking
1168// ---------------------------------------------------------------------------
1169
1170/// Checks if a concrete IR constant value matches a type expression.
1171///
1172/// This is used at system boundaries (fact loading, external input) to validate
1173/// that values conform to declared types. It is NOT needed for derived facts
1174/// when the bounds checker has already proven conformance statically.
1175pub fn has_type(ir: &Ir, type_expr: InstId, value: InstId) -> bool {
1176    // Base type (Name constant like /number, /string, ...).
1177    if let Inst::Name(_) = ir.get(type_expr) {
1178        return const_has_base_type(ir, value, type_expr);
1179    }
1180
1181    // Type variable: accept anything (existentially quantified).
1182    if let Inst::Var(_) = ir.get(type_expr) {
1183        return true;
1184    }
1185
1186    let fname = match apply_fn_name(ir, type_expr) {
1187        Some(n) => n,
1188        None => return false,
1189    };
1190    let args = apply_fn_args(ir, type_expr).unwrap();
1191
1192    match fname {
1193        FN_SINGLETON => {
1194            args.len() == 1 && ir_eq(ir, value, args[0])
1195        }
1196
1197        FN_UNION => args.iter().any(|alt| has_type(ir, *alt, value)),
1198
1199        FN_TAGGED_UNION => {
1200            // Tagged unions are structs at runtime. Check each expanded variant.
1201            if let Some((tags, structs)) = tagged_union_variants(ir, type_expr) {
1202                let tag_field = tagged_union_tag_field(ir, type_expr).unwrap();
1203                for (tag, variant_struct) in tags.iter().zip(structs.iter()) {
1204                    if has_type_tagged_variant(ir, tag_field, *tag, *variant_struct, value) {
1205                        return true;
1206                    }
1207                }
1208            }
1209            false
1210        }
1211
1212        FN_LIST => {
1213            if args.len() != 1 {
1214                return false;
1215            }
1216            match ir.get(value) {
1217                Inst::List(elems) => {
1218                    let elems = elems.clone();
1219                    elems.iter().all(|e| has_type(ir, args[0], *e))
1220                }
1221                _ => false,
1222            }
1223        }
1224
1225        FN_MAP => {
1226            if args.len() != 2 {
1227                return false;
1228            }
1229            match ir.get(value) {
1230                Inst::Map { keys, values } => {
1231                    let keys = keys.clone();
1232                    let values = values.clone();
1233                    keys.iter().all(|k| has_type(ir, args[0], *k))
1234                        && values.iter().all(|v| has_type(ir, args[1], *v))
1235                }
1236                _ => false,
1237            }
1238        }
1239
1240        FN_PAIR => {
1241            if args.len() != 2 {
1242                return false;
1243            }
1244            match ir.get(value) {
1245                Inst::List(elems) if elems.len() == 2 => {
1246                    let elems = elems.clone();
1247                    has_type(ir, args[0], elems[0]) && has_type(ir, args[1], elems[1])
1248                }
1249                _ => false,
1250            }
1251        }
1252
1253        FN_STRUCT => has_type_struct(ir, type_expr, value),
1254
1255        FN_OPTION => {
1256            if args.len() != 1 {
1257                return false;
1258            }
1259            // Option<T> matches T or /unit.
1260            if let Some(n) = name_str(ir, value) {
1261                if n == "/unit" {
1262                    return true;
1263                }
1264            }
1265            has_type(ir, args[0], value)
1266        }
1267
1268        _ => false,
1269    }
1270}
1271
1272/// Checks if a value matches a struct type expression.
1273fn has_type_struct(ir: &Ir, type_id: InstId, value: InstId) -> bool {
1274    let type_fields = struct_type_fields(ir, type_id);
1275    let value_struct = match ir.get(value) {
1276        Inst::Struct { fields, values } => Some((fields.clone(), values.clone())),
1277        _ => return false,
1278    };
1279    let (vfields, vvalues) = value_struct.unwrap();
1280
1281    // Build map of value fields.
1282    let value_map: FxHashMap<String, InstId> = vfields
1283        .iter()
1284        .zip(vvalues.iter())
1285        .map(|(f, v)| (ir.resolve_name(*f).to_string(), *v))
1286        .collect();
1287
1288    // Check all required type fields are present and match.
1289    for (fname_id, ftype_id, is_optional) in &type_fields {
1290        if let Some(fname) = name_str(ir, *fname_id) {
1291            match value_map.get(fname) {
1292                Some(val_id) => {
1293                    if !has_type(ir, *ftype_id, *val_id) {
1294                        return false;
1295                    }
1296                }
1297                None => {
1298                    if !is_optional {
1299                        return false;
1300                    }
1301                }
1302            }
1303        }
1304    }
1305
1306    // Check no extra fields beyond what the type declares.
1307    let type_field_names: FxHashSet<String> = type_fields
1308        .iter()
1309        .filter_map(|(f, _, _)| name_str(ir, *f).map(|s| s.to_string()))
1310        .collect();
1311    for vf in &vfields {
1312        let vf_name = ir.resolve_name(*vf);
1313        if !type_field_names.contains(vf_name) {
1314            return false;
1315        }
1316    }
1317
1318    true
1319}
1320
1321/// Checks if a struct value matches a specific tagged union variant.
1322fn has_type_tagged_variant(
1323    ir: &Ir,
1324    tag_field: InstId,
1325    tag: InstId,
1326    variant_struct: InstId,
1327    value: InstId,
1328) -> bool {
1329    let (vfields, vvalues) = match ir.get(value) {
1330        Inst::Struct { fields, values } => (fields.clone(), values.clone()),
1331        _ => return false,
1332    };
1333
1334    // Check that the tag field has the right value.
1335    let tag_field_name = match name_str(ir, tag_field) {
1336        Some(n) => n,
1337        None => return false,
1338    };
1339
1340    let tag_idx = vfields
1341        .iter()
1342        .position(|f| ir.resolve_name(*f) == tag_field_name);
1343    match tag_idx {
1344        Some(idx) => {
1345            if !ir_eq(ir, vvalues[idx], tag) {
1346                return false;
1347            }
1348        }
1349        None => return false,
1350    }
1351
1352    // Check that the remaining fields match the variant struct type.
1353    let type_fields = struct_type_fields(ir, variant_struct);
1354
1355    // Build a map of value fields (excluding tag field).
1356    let value_map: FxHashMap<String, InstId> = vfields
1357        .iter()
1358        .zip(vvalues.iter())
1359        .filter(|(f, _)| ir.resolve_name(**f) != tag_field_name)
1360        .map(|(f, v)| (ir.resolve_name(*f).to_string(), *v))
1361        .collect();
1362
1363    // Check variant struct fields.
1364    for (fname_id, ftype_id, is_optional) in &type_fields {
1365        if let Some(fname) = name_str(ir, *fname_id) {
1366            match value_map.get(fname) {
1367                Some(val_id) => {
1368                    if !has_type(ir, *ftype_id, *val_id) {
1369                        return false;
1370                    }
1371                }
1372                None => {
1373                    if !is_optional {
1374                        return false;
1375                    }
1376                }
1377            }
1378        }
1379    }
1380
1381    // Check no extra fields beyond tag + variant fields.
1382    let type_field_names: FxHashSet<String> = type_fields
1383        .iter()
1384        .filter_map(|(f, _, _)| name_str(ir, *f).map(|s| s.to_string()))
1385        .collect();
1386    for vf in &vfields {
1387        let name = ir.resolve_name(*vf);
1388        if name != tag_field_name && !type_field_names.contains(name) {
1389            return false;
1390        }
1391    }
1392
1393    true
1394}
1395
1396// ---------------------------------------------------------------------------
1397// Relation type utilities (for bounds checker)
1398// ---------------------------------------------------------------------------
1399
1400/// Builds a `fn:Rel(t1, t2, ...)` type from a slice of argument types.
1401pub fn new_rel_type(ir: &mut Ir, arg_types: &[InstId]) -> InstId {
1402    let rel_name = ir.intern_name(FN_REL);
1403    ir.add_inst(Inst::ApplyFn {
1404        function: rel_name,
1405        args: arg_types.to_vec(),
1406    })
1407}
1408
1409/// Builds `fn:Union(alts...)` or returns the single type if only one.
1410pub fn new_union_or_single(ir: &mut Ir, alts: Vec<InstId>) -> InstId {
1411    if alts.len() == 1 {
1412        return alts[0];
1413    }
1414    let union_name = ir.intern_name(FN_UNION);
1415    ir.add_inst(Inst::ApplyFn {
1416        function: union_name,
1417        args: alts,
1418    })
1419}
1420
1421/// Extracts the argument types from a `fn:Rel(t1, t2, ...)`.
1422pub fn rel_type_args(ir: &Ir, id: InstId) -> Option<&[InstId]> {
1423    if apply_fn_name(ir, id) != Some(FN_REL) {
1424        return None;
1425    }
1426    apply_fn_args(ir, id)
1427}
1428
1429/// Builds `fn:Rel` types from declaration bounds.
1430/// Each BoundDecl becomes one `fn:Rel`.
1431pub fn rel_types_from_decl(ir: &mut Ir, bounds: &[InstId]) -> Vec<InstId> {
1432    bounds
1433        .iter()
1434        .filter_map(|bound_id| {
1435            if let Inst::BoundDecl { base_terms } = ir.get(*bound_id) {
1436                let terms = base_terms.clone();
1437                Some(new_rel_type(ir, &terms))
1438            } else {
1439                None
1440            }
1441        })
1442        .collect()
1443}
1444
1445/// Gets the type context (all type variables mapped to `/any`) from
1446/// the type variables appearing in a type expression.
1447pub fn get_type_context(ir: &Ir, id: InstId) -> TypeContext {
1448    let mut ctx = TypeContext::default();
1449    collect_type_vars(ir, id, &mut ctx);
1450    ctx
1451}
1452
1453fn collect_type_vars(ir: &Ir, id: InstId, ctx: &mut TypeContext) {
1454    match ir.get(id) {
1455        Inst::Var(v) => {
1456            if !ctx.contains_key(v) {
1457                // Map to /any as default bound. We'd need &mut Ir to create the
1458                // /any InstId, so we use a sentinel. The caller can handle this.
1459                // For now, insert with the same id as a placeholder.
1460                ctx.insert(*v, id);
1461            }
1462        }
1463        Inst::ApplyFn { args, .. } => {
1464            for arg in args {
1465                collect_type_vars(ir, *arg, ctx);
1466            }
1467        }
1468        _ => {}
1469    }
1470}
1471
1472/// Extracts alternatives from a possibly-union-wrapped relation type.
1473///
1474/// If `id` is `fn:Union(R1, R2, ...)`, returns `[R1, R2, ...]`.
1475/// Otherwise returns `[id]`.
1476pub fn rel_type_alternatives(ir: &Ir, id: InstId) -> Vec<InstId> {
1477    if let Some(args) = union_type_args(ir, id) {
1478        args.to_vec()
1479    } else {
1480        vec![id]
1481    }
1482}
1483
1484/// Creates a single relation type or union from a list of alternatives.
1485pub fn rel_type_from_alternatives(ir: &mut Ir, alts: Vec<InstId>) -> InstId {
1486    if alts.is_empty() {
1487        return empty_type(ir);
1488    }
1489    new_union_or_single(ir, alts)
1490}
1491
1492/// Removes types conforming to `to_remove` from a union type.
1493/// Returns the remaining union, or EmptyType if nothing remains.
1494pub fn remove_from_union_type(ir: &mut Ir, to_remove: InstId, union_type: InstId) -> InstId {
1495    let ctx = TypeContext::default();
1496    if let Some(args) = union_type_args(ir, union_type) {
1497        let args = args.to_vec();
1498        let remaining: Vec<InstId> = args
1499            .into_iter()
1500            .filter(|alt| !set_conforms(ir, &ctx, *alt, to_remove))
1501            .collect();
1502        if remaining.is_empty() {
1503            return empty_type(ir);
1504        }
1505        return new_union_or_single(ir, remaining);
1506    }
1507    // Not a union: check if the whole type conforms to to_remove.
1508    if set_conforms(ir, &ctx, union_type, to_remove) {
1509        return empty_type(ir);
1510    }
1511    union_type
1512}
1513
1514/// Creates a `fn:List(elem_type)` type expression.
1515pub fn new_list_type(ir: &mut Ir, elem: InstId) -> InstId {
1516    let name = ir.intern_name(FN_LIST);
1517    ir.add_inst(Inst::ApplyFn {
1518        function: name,
1519        args: vec![elem],
1520    })
1521}
1522
1523/// Creates a `fn:Map(key_type, val_type)` type expression.
1524pub fn new_map_type(ir: &mut Ir, key: InstId, val: InstId) -> InstId {
1525    let name = ir.intern_name(FN_MAP);
1526    ir.add_inst(Inst::ApplyFn {
1527        function: name,
1528        args: vec![key, val],
1529    })
1530}
1531
1532/// Creates a `fn:Struct(field1, type1, ...)` type expression.
1533pub fn new_struct_type(ir: &mut Ir, args: Vec<InstId>) -> InstId {
1534    let name = ir.intern_name(FN_STRUCT);
1535    ir.add_inst(Inst::ApplyFn {
1536        function: name,
1537        args,
1538    })
1539}
1540
1541/// Creates a `fn:Tuple(t1, t2, ...)` type expression.
1542pub fn new_tuple_type(ir: &mut Ir, args: Vec<InstId>) -> InstId {
1543    let name = ir.intern_name(FN_TUPLE);
1544    ir.add_inst(Inst::ApplyFn {
1545        function: name,
1546        args,
1547    })
1548}
1549
1550/// Applies a substitution (type variable -> type) to a type expression.
1551/// Returns a new InstId with all type variables replaced.
1552pub fn apply_subst(ir: &mut Ir, id: InstId, subst: &FxHashMap<NameId, InstId>) -> InstId {
1553    if subst.is_empty() {
1554        return id;
1555    }
1556    match ir.get(id) {
1557        Inst::Var(v) => {
1558            let v = *v;
1559            if let Some(&replacement) = subst.get(&v) {
1560                replacement
1561            } else {
1562                id
1563            }
1564        }
1565        Inst::ApplyFn { function, args } => {
1566            let function = *function;
1567            let args = args.clone();
1568            let new_args: Vec<InstId> = args
1569                .iter()
1570                .map(|a| apply_subst(ir, *a, subst))
1571                .collect();
1572            if new_args == args {
1573                return id;
1574            }
1575            ir.add_inst(Inst::ApplyFn {
1576                function,
1577                args: new_args,
1578            })
1579        }
1580        _ => id,
1581    }
1582}
1583
1584/// Collects all type variables from a type expression into a set.
1585pub fn collect_vars(ir: &Ir, id: InstId, vars: &mut FxHashSet<NameId>) {
1586    match ir.get(id) {
1587        Inst::Var(v) => {
1588            vars.insert(*v);
1589        }
1590        Inst::ApplyFn { args, .. } => {
1591            for arg in args {
1592                collect_vars(ir, *arg, vars);
1593            }
1594        }
1595        _ => {}
1596    }
1597}
1598
1599// ---------------------------------------------------------------------------
1600// Tests
1601// ---------------------------------------------------------------------------
1602
1603#[cfg(test)]
1604mod tests {
1605    use super::*;
1606
1607    fn make_name(ir: &mut Ir, s: &str) -> InstId {
1608        let n = ir.intern_name(s);
1609        ir.add_inst(Inst::Name(n))
1610    }
1611
1612    fn make_apply(ir: &mut Ir, fn_name: &str, args: Vec<InstId>) -> InstId {
1613        let n = ir.intern_name(fn_name);
1614        ir.add_inst(Inst::ApplyFn {
1615            function: n,
1616            args,
1617        })
1618    }
1619
1620    // -- Wellformedness tests --
1621
1622    #[test]
1623    fn wellformed_base_types() {
1624        let mut ir = Ir::new();
1625        let ctx = TypeContext::default();
1626        let num = make_name(&mut ir, "/number");
1627        let str_ = make_name(&mut ir, "/string");
1628        let any = make_name(&mut ir, "/any");
1629        assert!(wellformed_type(&ir, &ctx, num).is_ok());
1630        assert!(wellformed_type(&ir, &ctx, str_).is_ok());
1631        assert!(wellformed_type(&ir, &ctx, any).is_ok());
1632    }
1633
1634    #[test]
1635    fn wellformed_struct() {
1636        let mut ir = Ir::new();
1637        let ctx = TypeContext::default();
1638        let x = make_name(&mut ir, "/x");
1639        let num = make_name(&mut ir, "/number");
1640        let y = make_name(&mut ir, "/y");
1641        let str_ = make_name(&mut ir, "/string");
1642        let s = make_apply(&mut ir, FN_STRUCT, vec![x, num, y, str_]);
1643        assert!(wellformed_type(&ir, &ctx, s).is_ok());
1644    }
1645
1646    #[test]
1647    fn wellformed_struct_odd_args() {
1648        let mut ir = Ir::new();
1649        let ctx = TypeContext::default();
1650        let x = make_name(&mut ir, "/x");
1651        let num = make_name(&mut ir, "/number");
1652        let y = make_name(&mut ir, "/y");
1653        // Odd number of args without fn:opt.
1654        let s = make_apply(&mut ir, FN_STRUCT, vec![x, num, y]);
1655        assert!(wellformed_type(&ir, &ctx, s).is_err());
1656    }
1657
1658    #[test]
1659    fn wellformed_tagged_union() {
1660        let mut ir = Ir::new();
1661        let ctx = TypeContext::default();
1662        let kind = make_name(&mut ir, "/kind");
1663        let move_ = make_name(&mut ir, "/move");
1664        let x = make_name(&mut ir, "/x");
1665        let num = make_name(&mut ir, "/number");
1666        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![x, num]);
1667        let quit = make_name(&mut ir, "/quit");
1668        let quit_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1669        let tu = make_apply(
1670            &mut ir,
1671            FN_TAGGED_UNION,
1672            vec![kind, move_, move_struct, quit, quit_struct],
1673        );
1674        assert!(wellformed_type(&ir, &ctx, tu).is_ok());
1675    }
1676
1677    #[test]
1678    fn wellformed_tagged_union_negative_too_few_args() {
1679        let mut ir = Ir::new();
1680        let ctx = TypeContext::default();
1681        let kind = make_name(&mut ir, "/kind");
1682        let tu = make_apply(&mut ir, FN_TAGGED_UNION, vec![kind]);
1683        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1684    }
1685
1686    #[test]
1687    fn wellformed_tagged_union_negative_even_args() {
1688        let mut ir = Ir::new();
1689        let ctx = TypeContext::default();
1690        let kind = make_name(&mut ir, "/kind");
1691        let move_ = make_name(&mut ir, "/move");
1692        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1693        let quit = make_name(&mut ir, "/quit");
1694        // 4 args (even) is invalid.
1695        let tu = make_apply(
1696            &mut ir,
1697            FN_TAGGED_UNION,
1698            vec![kind, move_, move_struct, quit],
1699        );
1700        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1701    }
1702
1703    #[test]
1704    fn wellformed_tagged_union_negative_dup_tags() {
1705        let mut ir = Ir::new();
1706        let ctx = TypeContext::default();
1707        let kind = make_name(&mut ir, "/kind");
1708        let move_ = make_name(&mut ir, "/move");
1709        let s1 = make_apply(&mut ir, FN_STRUCT, vec![]);
1710        let move2 = make_name(&mut ir, "/move");
1711        let s2 = make_apply(&mut ir, FN_STRUCT, vec![]);
1712        let tu = make_apply(
1713            &mut ir,
1714            FN_TAGGED_UNION,
1715            vec![kind, move_, s1, move2, s2],
1716        );
1717        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1718    }
1719
1720    #[test]
1721    fn wellformed_tagged_union_negative_tag_field_in_variant() {
1722        let mut ir = Ir::new();
1723        let ctx = TypeContext::default();
1724        let kind = make_name(&mut ir, "/kind");
1725        let move_ = make_name(&mut ir, "/move");
1726        // Variant struct has /kind field — disallowed.
1727        let kind2 = make_name(&mut ir, "/kind");
1728        let num = make_name(&mut ir, "/number");
1729        let s = make_apply(&mut ir, FN_STRUCT, vec![kind2, num]);
1730        let tu = make_apply(&mut ir, FN_TAGGED_UNION, vec![kind, move_, s]);
1731        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1732    }
1733
1734    // -- Conformance tests --
1735
1736    #[test]
1737    fn conforms_base_types() {
1738        let mut ir = Ir::new();
1739        let ctx = TypeContext::default();
1740        let num = make_name(&mut ir, "/number");
1741        let any = make_name(&mut ir, "/any");
1742        let str_ = make_name(&mut ir, "/string");
1743
1744        assert!(set_conforms(&ir, &ctx, num, any));
1745        assert!(set_conforms(&ir, &ctx, num, num));
1746        assert!(!set_conforms(&ir, &ctx, num, str_));
1747    }
1748
1749    #[test]
1750    fn conforms_name_hierarchy() {
1751        let mut ir = Ir::new();
1752        let ctx = TypeContext::default();
1753        let foo = make_name(&mut ir, "/foo");
1754        let foo_bar = make_name(&mut ir, "/foo/bar");
1755        let name = make_name(&mut ir, "/name");
1756
1757        assert!(set_conforms(&ir, &ctx, foo_bar, foo));
1758        assert!(set_conforms(&ir, &ctx, foo, name));
1759        assert!(set_conforms(&ir, &ctx, foo_bar, name));
1760        assert!(!set_conforms(&ir, &ctx, foo, foo_bar));
1761    }
1762
1763    #[test]
1764    fn conforms_singleton() {
1765        let mut ir = Ir::new();
1766        let ctx = TypeContext::default();
1767        let move_ = make_name(&mut ir, "/move");
1768        let singleton = make_apply(&mut ir, FN_SINGLETON, vec![move_]);
1769        let name = make_name(&mut ir, "/name");
1770        let num = make_name(&mut ir, "/number");
1771
1772        assert!(set_conforms(&ir, &ctx, singleton, name));
1773        assert!(!set_conforms(&ir, &ctx, singleton, num));
1774    }
1775
1776    #[test]
1777    fn conforms_union() {
1778        let mut ir = Ir::new();
1779        let ctx = TypeContext::default();
1780        let num = make_name(&mut ir, "/number");
1781        let str_ = make_name(&mut ir, "/string");
1782        let union = make_apply(&mut ir, FN_UNION, vec![num, str_]);
1783
1784        // Union conforms to /any.
1785        let any = make_name(&mut ir, "/any");
1786        assert!(set_conforms(&ir, &ctx, union, any));
1787        // Number conforms to Union<number, string>.
1788        assert!(set_conforms(&ir, &ctx, num, union));
1789        // Union<number, string> does NOT conform to /number.
1790        assert!(!set_conforms(&ir, &ctx, union, num));
1791    }
1792
1793    #[test]
1794    fn conforms_struct_subtyping() {
1795        let mut ir = Ir::new();
1796        let ctx = TypeContext::default();
1797        // .Struct</x: /number, /y: /string>
1798        let x = make_name(&mut ir, "/x");
1799        let num = make_name(&mut ir, "/number");
1800        let y = make_name(&mut ir, "/y");
1801        let str_ = make_name(&mut ir, "/string");
1802        let wider = make_apply(&mut ir, FN_STRUCT, vec![x, num, y, str_]);
1803        // .Struct</x: /number>
1804        let x2 = make_name(&mut ir, "/x");
1805        let num2 = make_name(&mut ir, "/number");
1806        let narrower = make_apply(&mut ir, FN_STRUCT, vec![x2, num2]);
1807
1808        // Wider (more fields) conforms to narrower.
1809        assert!(set_conforms(&ir, &ctx, wider, narrower));
1810        // Narrower does NOT conform to wider (missing required /y).
1811        assert!(!set_conforms(&ir, &ctx, narrower, wider));
1812    }
1813
1814    #[test]
1815    fn conforms_list() {
1816        let mut ir = Ir::new();
1817        let ctx = TypeContext::default();
1818        let num = make_name(&mut ir, "/number");
1819        let any = make_name(&mut ir, "/any");
1820        let list_num = make_apply(&mut ir, FN_LIST, vec![num]);
1821        let list_any = make_apply(&mut ir, FN_LIST, vec![any]);
1822
1823        assert!(set_conforms(&ir, &ctx, list_num, list_any));
1824        assert!(!set_conforms(&ir, &ctx, list_any, list_num));
1825    }
1826
1827    // -- HasType tests --
1828
1829    #[test]
1830    fn has_type_base() {
1831        let mut ir = Ir::new();
1832        let num_type = make_name(&mut ir, "/number");
1833        let str_type = make_name(&mut ir, "/string");
1834        let val_42 = ir.add_inst(Inst::Number(42));
1835        let val_hello = {
1836            let s = ir.intern_string("hello");
1837            ir.add_inst(Inst::String(s))
1838        };
1839
1840        assert!(has_type(&ir, num_type, val_42));
1841        assert!(!has_type(&ir, str_type, val_42));
1842        assert!(has_type(&ir, str_type, val_hello));
1843    }
1844
1845    #[test]
1846    fn has_type_struct() {
1847        let mut ir = Ir::new();
1848        // Type: .Struct</x: /number>
1849        let x_name = make_name(&mut ir, "/x");
1850        let num_type = make_name(&mut ir, "/number");
1851        let struct_type = make_apply(&mut ir, FN_STRUCT, vec![x_name, num_type]);
1852
1853        // Value: {/x: 42}
1854        let x_field = ir.intern_name("/x");
1855        let val_42 = ir.add_inst(Inst::Number(42));
1856        let val_struct = ir.add_inst(Inst::Struct {
1857            fields: vec![x_field],
1858            values: vec![val_42],
1859        });
1860
1861        assert!(has_type(&ir, struct_type, val_struct));
1862    }
1863
1864    #[test]
1865    fn has_type_tagged_union() {
1866        let mut ir = Ir::new();
1867
1868        // TaggedUnion</kind, /move: .Struct</x: /number>, /quit: .Struct<>>
1869        let kind = make_name(&mut ir, "/kind");
1870        let move_tag = make_name(&mut ir, "/move");
1871        let x_name = make_name(&mut ir, "/x");
1872        let num_type = make_name(&mut ir, "/number");
1873        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![x_name, num_type]);
1874        let quit_tag = make_name(&mut ir, "/quit");
1875        let quit_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1876        let tu = make_apply(
1877            &mut ir,
1878            FN_TAGGED_UNION,
1879            vec![kind, move_tag, move_struct, quit_tag, quit_struct],
1880        );
1881
1882        // Value: {/kind: /move, /x: 10}
1883        let kind_f = ir.intern_name("/kind");
1884        let move_v = ir.intern_name("/move");
1885        let move_val = ir.add_inst(Inst::Name(move_v));
1886        let x_f = ir.intern_name("/x");
1887        let val_10 = ir.add_inst(Inst::Number(10));
1888        let val_move = ir.add_inst(Inst::Struct {
1889            fields: vec![kind_f, x_f],
1890            values: vec![move_val, val_10],
1891        });
1892        assert!(has_type(&ir, tu, val_move));
1893
1894        // Value: {/kind: /quit}
1895        let kind_f2 = ir.intern_name("/kind");
1896        let quit_v = ir.intern_name("/quit");
1897        let quit_val = ir.add_inst(Inst::Name(quit_v));
1898        let val_quit = ir.add_inst(Inst::Struct {
1899            fields: vec![kind_f2],
1900            values: vec![quit_val],
1901        });
1902        assert!(has_type(&ir, tu, val_quit));
1903
1904        // Value: {/kind: /bad}
1905        let kind_f3 = ir.intern_name("/kind");
1906        let bad_v = ir.intern_name("/bad");
1907        let bad_val = ir.add_inst(Inst::Name(bad_v));
1908        let val_bad = ir.add_inst(Inst::Struct {
1909            fields: vec![kind_f3],
1910            values: vec![bad_val],
1911        });
1912        assert!(!has_type(&ir, tu, val_bad));
1913    }
1914
1915    // -- Expansion tests --
1916
1917    #[test]
1918    fn expand_tagged_union() {
1919        let mut ir = Ir::new();
1920        let kind = make_name(&mut ir, "/kind");
1921        let move_ = make_name(&mut ir, "/move");
1922        let x = make_name(&mut ir, "/x");
1923        let num = make_name(&mut ir, "/number");
1924        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![x, num]);
1925        let quit = make_name(&mut ir, "/quit");
1926        let quit_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1927        let tu = make_apply(
1928            &mut ir,
1929            FN_TAGGED_UNION,
1930            vec![kind, move_, move_struct, quit, quit_struct],
1931        );
1932
1933        let expanded = expand_tagged_union_type(&mut ir, tu).unwrap();
1934        assert!(is_union_type(&ir, expanded));
1935        let alts = union_type_args(&ir, expanded).unwrap();
1936        assert_eq!(alts.len(), 2);
1937        // First variant should be a struct with /kind, Singleton(/move), /x, /number.
1938        assert!(is_struct_type(&ir, alts[0]));
1939        // Second variant should be a struct with /kind, Singleton(/quit).
1940        assert!(is_struct_type(&ir, alts[1]));
1941    }
1942
1943    // -- UpperBound / LowerBound tests --
1944
1945    #[test]
1946    fn upper_bound_basic() {
1947        let mut ir = Ir::new();
1948        let ctx = TypeContext::default();
1949        let num = make_name(&mut ir, "/number");
1950        let str_ = make_name(&mut ir, "/string");
1951
1952        // UpperBound of [/number] = /number.
1953        assert_eq!(upper_bound(&mut ir, &ctx, &[num]), num);
1954        // UpperBound of [/number, /string] = fn:Union(/number, /string).
1955        let ub = upper_bound(&mut ir, &ctx, &[num, str_]);
1956        assert!(is_union_type(&ir, ub));
1957        // UpperBound of [] = EmptyType.
1958        let ub_empty = upper_bound(&mut ir, &ctx, &[]);
1959        assert!(is_empty_type(&ir, ub_empty));
1960    }
1961
1962    #[test]
1963    fn upper_bound_eliminates_redundant() {
1964        let mut ir = Ir::new();
1965        let ctx = TypeContext::default();
1966        let num = make_name(&mut ir, "/number");
1967        let any = make_name(&mut ir, "/any");
1968
1969        // UpperBound([/number, /any]) = /any.
1970        let ub = upper_bound(&mut ir, &ctx, &[num, any]);
1971        assert!(is_any(&ir, ub));
1972    }
1973
1974    #[test]
1975    fn upper_bound_name_hierarchy() {
1976        let mut ir = Ir::new();
1977        let ctx = TypeContext::default();
1978        let animal = make_name(&mut ir, "/animal");
1979        let animal_dog = make_name(&mut ir, "/animal/dog");
1980
1981        // /animal/dog <: /animal, so UpperBound = /animal.
1982        let ub = upper_bound(&mut ir, &ctx, &[animal, animal_dog]);
1983        assert_eq!(ub, animal);
1984    }
1985
1986    #[test]
1987    fn lower_bound_basic() {
1988        let mut ir = Ir::new();
1989        let ctx = TypeContext::default();
1990        let num = make_name(&mut ir, "/number");
1991        let any = make_name(&mut ir, "/any");
1992
1993        // LowerBound([/number, /any]) = /number.
1994        let lb = lower_bound(&mut ir, &ctx, &[num, any]);
1995        assert_eq!(lb, num);
1996    }
1997
1998    #[test]
1999    fn lower_bound_empty() {
2000        let mut ir = Ir::new();
2001        let ctx = TypeContext::default();
2002        let num = make_name(&mut ir, "/number");
2003        let str_ = make_name(&mut ir, "/string");
2004
2005        // LowerBound([/number, /string]) = EmptyType (disjoint).
2006        let lb = lower_bound(&mut ir, &ctx, &[num, str_]);
2007        assert!(is_empty_type(&ir, lb));
2008    }
2009
2010    #[test]
2011    fn intersect_type_with_union() {
2012        let mut ir = Ir::new();
2013        let ctx = TypeContext::default();
2014        let num = make_name(&mut ir, "/number");
2015        let str_ = make_name(&mut ir, "/string");
2016        let union_ns = make_apply(&mut ir, FN_UNION, vec![num, str_]);
2017
2018        // intersect(Union(/number, /string), /number) = /number.
2019        let result = intersect_type(&mut ir, &ctx, union_ns, num);
2020        assert_eq!(result, num);
2021    }
2022}