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 rustc_hash::{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`.
51pub fn 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`.
60pub fn 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    // Both sides tagged unions: left's variant must match some right variant.
982    // A right variant matches when it uses the same tag field, the tag value
983    // is identical, and the variant struct on the right accepts ours.
984    if is_tagged_union_type(ir, right) {
985        if let Some((r_tags, r_structs)) = tagged_union_variants(ir, right) {
986            let r_tag_field = tagged_union_tag_field(ir, right).unwrap();
987            if !ir_eq(ir, tag_field, r_tag_field) {
988                return false;
989            }
990            return r_tags.iter().zip(r_structs.iter()).any(|(rt, rs)| {
991                ir_eq(ir, tag, *rt) && set_conforms(ir, ctx, variant_struct, *rs)
992            });
993        }
994        return false;
995    }
996    // If right is a union, check that the expanded variant conforms to some alt.
997    if let Some(alts) = union_type_args(ir, right) {
998        let alts = alts.to_vec();
999        return alts.iter().any(|alt| {
1000            expanded_variant_conforms(ir, ctx, tag_field, tag, variant_struct, *alt)
1001        });
1002    }
1003    false
1004}
1005
1006/// Checks that `left` conforms to an expanded variant of a tagged union.
1007fn expanded_variant_conforms_right(
1008    ir: &Ir,
1009    ctx: &TypeContext,
1010    left: InstId,
1011    _tag_field: InstId,
1012    _tag: InstId,
1013    variant_struct: InstId,
1014) -> bool {
1015    // left conforms to the expanded variant if it conforms to the variant struct
1016    // (ignoring the tag field, which is added by expansion).
1017    set_conforms(ir, ctx, left, variant_struct)
1018}
1019
1020/// Checks if a constant instruction has a given base type.
1021fn const_has_base_type(ir: &Ir, const_id: InstId, type_id: InstId) -> bool {
1022    let type_name = match name_str(ir, type_id) {
1023        Some(n) => n,
1024        None => return false,
1025    };
1026    match ir.get(const_id) {
1027        Inst::Number(_) => type_name == "/number" || type_name == "/any",
1028        Inst::Float(_) => type_name == "/float64" || type_name == "/any",
1029        Inst::String(_) => type_name == "/string" || type_name == "/any",
1030        Inst::Bool(_) => type_name == "/bool" || type_name == "/any",
1031        Inst::Time(_) => type_name == "/time" || type_name == "/any",
1032        Inst::Duration(_) => type_name == "/duration" || type_name == "/any",
1033        Inst::Bytes(_) => type_name == "/bytes" || type_name == "/any",
1034        Inst::Name(n) => {
1035            if type_name == "/name" || type_name == "/any" {
1036                return true;
1037            }
1038            // Name hierarchy: /foo/bar has type /foo.
1039            let name = ir.resolve_name(*n);
1040            name.starts_with(type_name)
1041                && (name.len() == type_name.len()
1042                    || name.as_bytes().get(type_name.len()) == Some(&b'/'))
1043        }
1044        _ => false,
1045    }
1046}
1047
1048/// Checks structural IR equality (same instruction kind and content).
1049fn ir_eq(ir: &Ir, a: InstId, b: InstId) -> bool {
1050    if a == b {
1051        return true;
1052    }
1053    match (ir.get(a), ir.get(b)) {
1054        (Inst::Name(na), Inst::Name(nb)) => na == nb,
1055        (Inst::Number(na), Inst::Number(nb)) => na == nb,
1056        (Inst::Float(fa), Inst::Float(fb)) => fa.to_bits() == fb.to_bits(),
1057        (Inst::String(sa), Inst::String(sb)) => sa == sb,
1058        (Inst::Bool(ba), Inst::Bool(bb)) => ba == bb,
1059        _ => false,
1060    }
1061}
1062
1063// ---------------------------------------------------------------------------
1064// Upper bound / Lower bound / Intersection
1065// ---------------------------------------------------------------------------
1066
1067/// Computes the upper bound (least common supertype) of a set of type expressions.
1068///
1069/// Flattens unions, eliminates redundant types via SetConforms, returns single
1070/// type or Union. Returns EmptyType for empty input, `/any` if any input is `/any`.
1071pub fn upper_bound(ir: &mut Ir, ctx: &TypeContext, type_exprs: &[InstId]) -> InstId {
1072    let mut worklist = Vec::new();
1073    for &te in type_exprs {
1074        if is_any(ir, te) {
1075            return find_or_create_name(ir, "/any");
1076        }
1077        if let Some(args) = union_type_args(ir, te) {
1078            let args = args.to_vec();
1079            worklist.extend(args);
1080        } else {
1081            worklist.push(te);
1082        }
1083    }
1084    if worklist.is_empty() {
1085        return empty_type(ir);
1086    }
1087    let mut reduced = vec![worklist[0]];
1088    'outer: for &te in &worklist[1..] {
1089        for i in 0..reduced.len() {
1090            if set_conforms(ir, ctx, te, reduced[i]) {
1091                continue 'outer;
1092            }
1093            if set_conforms(ir, ctx, reduced[i], te) {
1094                reduced[i] = te;
1095                continue 'outer;
1096            }
1097        }
1098        reduced.push(te);
1099    }
1100    if reduced.len() == 1 {
1101        return reduced[0];
1102    }
1103    new_union_or_single(ir, reduced)
1104}
1105
1106/// Computes the intersection (greatest lower bound) of two type expressions.
1107pub fn intersect_type(ir: &mut Ir, ctx: &TypeContext, a: InstId, b: InstId) -> InstId {
1108    if ir_eq(ir, a, b) {
1109        return a;
1110    }
1111    if is_any(ir, a) {
1112        return b;
1113    }
1114    if is_any(ir, b) {
1115        return a;
1116    }
1117    // Type variable resolution.
1118    if let Inst::Var(v) = ir.get(a) {
1119        let v = *v;
1120        if let Some(&bound) = ctx.get(&v) {
1121            return intersect_type(ir, ctx, bound, b);
1122        }
1123        return empty_type(ir);
1124    }
1125    if let Inst::Var(v) = ir.get(b) {
1126        let v = *v;
1127        if let Some(&bound) = ctx.get(&v) {
1128            return intersect_type(ir, ctx, a, bound);
1129        }
1130        return empty_type(ir);
1131    }
1132    if set_conforms(ir, ctx, a, b) {
1133        return a;
1134    }
1135    if set_conforms(ir, ctx, b, a) {
1136        return b;
1137    }
1138    // Union decomposition on a.
1139    if is_union_type(ir, a) {
1140        let args = union_type_args(ir, a).unwrap().to_vec();
1141        let mut res = Vec::new();
1142        for elem in args {
1143            let u = intersect_type(ir, ctx, elem, b);
1144            if !is_empty_type(ir, u) {
1145                res.push(u);
1146            }
1147        }
1148        return upper_bound(ir, ctx, &res);
1149    }
1150    // Union decomposition on b.
1151    if is_union_type(ir, b) {
1152        let args = union_type_args(ir, b).unwrap().to_vec();
1153        let mut res = Vec::new();
1154        for elem in args {
1155            if set_conforms(ir, ctx, a, elem) {
1156                res.push(a);
1157            } else if set_conforms(ir, ctx, elem, a) {
1158                res.push(elem);
1159            }
1160        }
1161        return upper_bound(ir, ctx, &res);
1162    }
1163    empty_type(ir)
1164}
1165
1166/// Computes the lower bound (most general common subtype) of a set of type expressions.
1167///
1168/// Repeatedly intersects types. Returns EmptyType if intersection is empty.
1169pub fn lower_bound(ir: &mut Ir, ctx: &TypeContext, type_exprs: &[InstId]) -> InstId {
1170    let any = find_or_create_name(ir, "/any");
1171    let mut result = any;
1172    for &t in type_exprs {
1173        result = intersect_type(ir, ctx, result, t);
1174        if is_empty_type(ir, result) {
1175            return result;
1176        }
1177    }
1178    result
1179}
1180
1181// ---------------------------------------------------------------------------
1182// HasType — Runtime type checking
1183// ---------------------------------------------------------------------------
1184
1185/// Checks if a concrete IR constant value matches a type expression.
1186///
1187/// This is used at system boundaries (fact loading, external input) to validate
1188/// that values conform to declared types. It is NOT needed for derived facts
1189/// when the bounds checker has already proven conformance statically.
1190pub fn has_type(ir: &Ir, type_expr: InstId, value: InstId) -> bool {
1191    // Base type (Name constant like /number, /string, ...).
1192    if let Inst::Name(_) = ir.get(type_expr) {
1193        return const_has_base_type(ir, value, type_expr);
1194    }
1195
1196    // Type variable: accept anything (existentially quantified).
1197    if let Inst::Var(_) = ir.get(type_expr) {
1198        return true;
1199    }
1200
1201    let fname = match apply_fn_name(ir, type_expr) {
1202        Some(n) => n,
1203        None => return false,
1204    };
1205    let args = apply_fn_args(ir, type_expr).unwrap();
1206
1207    match fname {
1208        FN_SINGLETON => {
1209            args.len() == 1 && ir_eq(ir, value, args[0])
1210        }
1211
1212        FN_UNION => args.iter().any(|alt| has_type(ir, *alt, value)),
1213
1214        FN_TAGGED_UNION => {
1215            // Tagged unions are structs at runtime. Check each expanded variant.
1216            if let Some((tags, structs)) = tagged_union_variants(ir, type_expr) {
1217                let tag_field = tagged_union_tag_field(ir, type_expr).unwrap();
1218                for (tag, variant_struct) in tags.iter().zip(structs.iter()) {
1219                    if has_type_tagged_variant(ir, tag_field, *tag, *variant_struct, value) {
1220                        return true;
1221                    }
1222                }
1223            }
1224            false
1225        }
1226
1227        FN_LIST => {
1228            if args.len() != 1 {
1229                return false;
1230            }
1231            match list_value_elems(ir, value) {
1232                Some(elems) => elems.iter().all(|e| has_type(ir, args[0], *e)),
1233                None => false,
1234            }
1235        }
1236
1237        FN_MAP => {
1238            if args.len() != 2 {
1239                return false;
1240            }
1241            match map_value_entries(ir, value) {
1242                Some((keys, values)) => {
1243                    keys.iter().all(|k| has_type(ir, args[0], *k))
1244                        && values.iter().all(|v| has_type(ir, args[1], *v))
1245                }
1246                None => false,
1247            }
1248        }
1249
1250        FN_PAIR => {
1251            if args.len() != 2 {
1252                return false;
1253            }
1254            match list_value_elems(ir, value) {
1255                Some(elems) if elems.len() == 2 => {
1256                    has_type(ir, args[0], elems[0]) && has_type(ir, args[1], elems[1])
1257                }
1258                _ => false,
1259            }
1260        }
1261
1262        FN_STRUCT => has_type_struct(ir, type_expr, value),
1263
1264        FN_OPTION => {
1265            if args.len() != 1 {
1266                return false;
1267            }
1268            // Option<T> matches T or /unit.
1269            if let Some(n) = name_str(ir, value) {
1270                if n == "/unit" {
1271                    return true;
1272                }
1273            }
1274            has_type(ir, args[0], value)
1275        }
1276
1277        _ => false,
1278    }
1279}
1280
1281/// Extracts `(field_name_ids, field_value_ids)` from a struct value.
1282///
1283/// Struct values have two equivalent IR representations:
1284/// 1. `Inst::Struct { fields, values }` — parallel vectors (brace syntax
1285///    lowered as a compile-time constant).
1286/// 2. `Inst::ApplyFn { function: "fn:struct", args: [n1, v1, n2, v2, ...] }`
1287///    — interleaved (brace or paren syntax lowered through the generic
1288///    ApplyFn path). The field names are `Inst::Name`.
1289///
1290/// Returns the element list for a list value in either representation:
1291/// `Inst::List(..)` (from a `Const::List`) or `Inst::ApplyFn("fn:list", ..)`.
1292fn list_value_elems(ir: &Ir, value: InstId) -> Option<Vec<InstId>> {
1293    match ir.get(value) {
1294        Inst::List(elems) => Some(elems.clone()),
1295        Inst::ApplyFn { function, args } if ir.resolve_name(*function) == "fn:list" => {
1296            Some(args.clone())
1297        }
1298        _ => None,
1299    }
1300}
1301
1302/// Returns `(keys, values)` for a map value in either representation:
1303/// `Inst::Map { keys, values }` or `Inst::ApplyFn("fn:map", [k1, v1, k2, v2, ..])`.
1304fn map_value_entries(ir: &Ir, value: InstId) -> Option<(Vec<InstId>, Vec<InstId>)> {
1305    match ir.get(value) {
1306        Inst::Map { keys, values } => Some((keys.clone(), values.clone())),
1307        Inst::ApplyFn { function, args } if ir.resolve_name(*function) == "fn:map" => {
1308            let args = args.clone();
1309            if args.len() % 2 != 0 {
1310                return None;
1311            }
1312            let mut keys = Vec::with_capacity(args.len() / 2);
1313            let mut values = Vec::with_capacity(args.len() / 2);
1314            for pair in args.chunks_exact(2) {
1315                keys.push(pair[0]);
1316                values.push(pair[1]);
1317            }
1318            Some((keys, values))
1319        }
1320        _ => None,
1321    }
1322}
1323
1324/// Returns `None` if the value is neither shape.
1325fn struct_value_fields(ir: &Ir, value: InstId) -> Option<(Vec<NameId>, Vec<InstId>)> {
1326    match ir.get(value) {
1327        Inst::Struct { fields, values } => Some((fields.clone(), values.clone())),
1328        Inst::ApplyFn { function, args } if ir.resolve_name(*function) == "fn:struct" => {
1329            let args = args.clone();
1330            if args.len() % 2 != 0 {
1331                return None;
1332            }
1333            let mut fields = Vec::with_capacity(args.len() / 2);
1334            let mut values = Vec::with_capacity(args.len() / 2);
1335            for pair in args.chunks_exact(2) {
1336                match ir.get(pair[0]) {
1337                    Inst::Name(n) => fields.push(*n),
1338                    _ => return None,
1339                }
1340                values.push(pair[1]);
1341            }
1342            Some((fields, values))
1343        }
1344        _ => None,
1345    }
1346}
1347
1348/// Checks if a value matches a struct type expression.
1349fn has_type_struct(ir: &Ir, type_id: InstId, value: InstId) -> bool {
1350    let type_fields = struct_type_fields(ir, type_id);
1351    let (vfields, vvalues) = match struct_value_fields(ir, value) {
1352        Some(pair) => pair,
1353        None => return false,
1354    };
1355
1356    // Build map of value fields.
1357    let value_map: FxHashMap<String, InstId> = vfields
1358        .iter()
1359        .zip(vvalues.iter())
1360        .map(|(f, v)| (ir.resolve_name(*f).to_string(), *v))
1361        .collect();
1362
1363    // Check all required type fields are present and match.
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 what the type declares.
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 vf_name = ir.resolve_name(*vf);
1388        if !type_field_names.contains(vf_name) {
1389            return false;
1390        }
1391    }
1392
1393    true
1394}
1395
1396/// Checks if a struct value matches a specific tagged union variant.
1397fn has_type_tagged_variant(
1398    ir: &Ir,
1399    tag_field: InstId,
1400    tag: InstId,
1401    variant_struct: InstId,
1402    value: InstId,
1403) -> bool {
1404    let (vfields, vvalues) = match struct_value_fields(ir, value) {
1405        Some(pair) => pair,
1406        None => return false,
1407    };
1408
1409    // Check that the tag field has the right value.
1410    let tag_field_name = match name_str(ir, tag_field) {
1411        Some(n) => n,
1412        None => return false,
1413    };
1414
1415    let tag_idx = vfields
1416        .iter()
1417        .position(|f| ir.resolve_name(*f) == tag_field_name);
1418    match tag_idx {
1419        Some(idx) => {
1420            if !ir_eq(ir, vvalues[idx], tag) {
1421                return false;
1422            }
1423        }
1424        None => return false,
1425    }
1426
1427    // Check that the remaining fields match the variant struct type.
1428    let type_fields = struct_type_fields(ir, variant_struct);
1429
1430    // Build a map of value fields (excluding tag field).
1431    let value_map: FxHashMap<String, InstId> = vfields
1432        .iter()
1433        .zip(vvalues.iter())
1434        .filter(|(f, _)| ir.resolve_name(**f) != tag_field_name)
1435        .map(|(f, v)| (ir.resolve_name(*f).to_string(), *v))
1436        .collect();
1437
1438    // Check variant struct fields.
1439    for (fname_id, ftype_id, is_optional) in &type_fields {
1440        if let Some(fname) = name_str(ir, *fname_id) {
1441            match value_map.get(fname) {
1442                Some(val_id) => {
1443                    if !has_type(ir, *ftype_id, *val_id) {
1444                        return false;
1445                    }
1446                }
1447                None => {
1448                    if !is_optional {
1449                        return false;
1450                    }
1451                }
1452            }
1453        }
1454    }
1455
1456    // Check no extra fields beyond tag + variant fields.
1457    let type_field_names: FxHashSet<String> = type_fields
1458        .iter()
1459        .filter_map(|(f, _, _)| name_str(ir, *f).map(|s| s.to_string()))
1460        .collect();
1461    for vf in &vfields {
1462        let name = ir.resolve_name(*vf);
1463        if name != tag_field_name && !type_field_names.contains(name) {
1464            return false;
1465        }
1466    }
1467
1468    true
1469}
1470
1471// ---------------------------------------------------------------------------
1472// Relation type utilities (for bounds checker)
1473// ---------------------------------------------------------------------------
1474
1475/// Builds a `fn:Rel(t1, t2, ...)` type from a slice of argument types.
1476pub fn new_rel_type(ir: &mut Ir, arg_types: &[InstId]) -> InstId {
1477    let rel_name = ir.intern_name(FN_REL);
1478    ir.add_inst(Inst::ApplyFn {
1479        function: rel_name,
1480        args: arg_types.to_vec(),
1481    })
1482}
1483
1484/// Builds `fn:Union(alts...)` or returns the single type if only one.
1485pub fn new_union_or_single(ir: &mut Ir, alts: Vec<InstId>) -> InstId {
1486    if alts.len() == 1 {
1487        return alts[0];
1488    }
1489    let union_name = ir.intern_name(FN_UNION);
1490    ir.add_inst(Inst::ApplyFn {
1491        function: union_name,
1492        args: alts,
1493    })
1494}
1495
1496/// Extracts the argument types from a `fn:Rel(t1, t2, ...)`.
1497pub fn rel_type_args(ir: &Ir, id: InstId) -> Option<&[InstId]> {
1498    if apply_fn_name(ir, id) != Some(FN_REL) {
1499        return None;
1500    }
1501    apply_fn_args(ir, id)
1502}
1503
1504/// Builds `fn:Rel` types from declaration bounds.
1505/// Each BoundDecl becomes one `fn:Rel`.
1506pub fn rel_types_from_decl(ir: &mut Ir, bounds: &[InstId]) -> Vec<InstId> {
1507    bounds
1508        .iter()
1509        .filter_map(|bound_id| {
1510            if let Inst::BoundDecl { base_terms } = ir.get(*bound_id) {
1511                let terms = base_terms.clone();
1512                Some(new_rel_type(ir, &terms))
1513            } else {
1514                None
1515            }
1516        })
1517        .collect()
1518}
1519
1520/// Gets the type context (all type variables mapped to `/any`) from
1521/// the type variables appearing in a type expression.
1522pub fn get_type_context(ir: &Ir, id: InstId) -> TypeContext {
1523    let mut ctx = TypeContext::default();
1524    collect_type_vars(ir, id, &mut ctx);
1525    ctx
1526}
1527
1528fn collect_type_vars(ir: &Ir, id: InstId, ctx: &mut TypeContext) {
1529    match ir.get(id) {
1530        Inst::Var(v) => {
1531            if !ctx.contains_key(v) {
1532                // Map to /any as default bound. We'd need &mut Ir to create the
1533                // /any InstId, so we use a sentinel. The caller can handle this.
1534                // For now, insert with the same id as a placeholder.
1535                ctx.insert(*v, id);
1536            }
1537        }
1538        Inst::ApplyFn { args, .. } => {
1539            for arg in args {
1540                collect_type_vars(ir, *arg, ctx);
1541            }
1542        }
1543        _ => {}
1544    }
1545}
1546
1547/// Extracts alternatives from a possibly-union-wrapped relation type.
1548///
1549/// If `id` is `fn:Union(R1, R2, ...)`, returns `[R1, R2, ...]`.
1550/// Otherwise returns `[id]`.
1551pub fn rel_type_alternatives(ir: &Ir, id: InstId) -> Vec<InstId> {
1552    if let Some(args) = union_type_args(ir, id) {
1553        args.to_vec()
1554    } else {
1555        vec![id]
1556    }
1557}
1558
1559/// Creates a single relation type or union from a list of alternatives.
1560pub fn rel_type_from_alternatives(ir: &mut Ir, alts: Vec<InstId>) -> InstId {
1561    if alts.is_empty() {
1562        return empty_type(ir);
1563    }
1564    new_union_or_single(ir, alts)
1565}
1566
1567/// Removes types conforming to `to_remove` from a union type.
1568/// Returns the remaining union, or EmptyType if nothing remains.
1569pub fn remove_from_union_type(ir: &mut Ir, to_remove: InstId, union_type: InstId) -> InstId {
1570    let ctx = TypeContext::default();
1571    if let Some(args) = union_type_args(ir, union_type) {
1572        let args = args.to_vec();
1573        let remaining: Vec<InstId> = args
1574            .into_iter()
1575            .filter(|alt| !set_conforms(ir, &ctx, *alt, to_remove))
1576            .collect();
1577        if remaining.is_empty() {
1578            return empty_type(ir);
1579        }
1580        return new_union_or_single(ir, remaining);
1581    }
1582    // Not a union: check if the whole type conforms to to_remove.
1583    if set_conforms(ir, &ctx, union_type, to_remove) {
1584        return empty_type(ir);
1585    }
1586    union_type
1587}
1588
1589/// Creates a `fn:List(elem_type)` type expression.
1590pub fn new_list_type(ir: &mut Ir, elem: InstId) -> InstId {
1591    let name = ir.intern_name(FN_LIST);
1592    ir.add_inst(Inst::ApplyFn {
1593        function: name,
1594        args: vec![elem],
1595    })
1596}
1597
1598/// Creates a `fn:Map(key_type, val_type)` type expression.
1599pub fn new_map_type(ir: &mut Ir, key: InstId, val: InstId) -> InstId {
1600    let name = ir.intern_name(FN_MAP);
1601    ir.add_inst(Inst::ApplyFn {
1602        function: name,
1603        args: vec![key, val],
1604    })
1605}
1606
1607/// Creates a `fn:Struct(field1, type1, ...)` type expression.
1608pub fn new_struct_type(ir: &mut Ir, args: Vec<InstId>) -> InstId {
1609    let name = ir.intern_name(FN_STRUCT);
1610    ir.add_inst(Inst::ApplyFn {
1611        function: name,
1612        args,
1613    })
1614}
1615
1616/// Creates a `fn:Tuple(t1, t2, ...)` type expression.
1617pub fn new_tuple_type(ir: &mut Ir, args: Vec<InstId>) -> InstId {
1618    let name = ir.intern_name(FN_TUPLE);
1619    ir.add_inst(Inst::ApplyFn {
1620        function: name,
1621        args,
1622    })
1623}
1624
1625/// Applies a substitution (type variable -> type) to a type expression.
1626/// Returns a new InstId with all type variables replaced.
1627pub fn apply_subst(ir: &mut Ir, id: InstId, subst: &FxHashMap<NameId, InstId>) -> InstId {
1628    if subst.is_empty() {
1629        return id;
1630    }
1631    match ir.get(id) {
1632        Inst::Var(v) => {
1633            let v = *v;
1634            if let Some(&replacement) = subst.get(&v) {
1635                replacement
1636            } else {
1637                id
1638            }
1639        }
1640        Inst::ApplyFn { function, args } => {
1641            let function = *function;
1642            let args = args.clone();
1643            let new_args: Vec<InstId> = args
1644                .iter()
1645                .map(|a| apply_subst(ir, *a, subst))
1646                .collect();
1647            if new_args == args {
1648                return id;
1649            }
1650            ir.add_inst(Inst::ApplyFn {
1651                function,
1652                args: new_args,
1653            })
1654        }
1655        _ => id,
1656    }
1657}
1658
1659/// Collects all type variables from a type expression into a set.
1660pub fn collect_vars(ir: &Ir, id: InstId, vars: &mut FxHashSet<NameId>) {
1661    match ir.get(id) {
1662        Inst::Var(v) => {
1663            vars.insert(*v);
1664        }
1665        Inst::ApplyFn { args, .. } => {
1666            for arg in args {
1667                collect_vars(ir, *arg, vars);
1668            }
1669        }
1670        _ => {}
1671    }
1672}
1673
1674// ---------------------------------------------------------------------------
1675// Tests
1676// ---------------------------------------------------------------------------
1677
1678#[cfg(test)]
1679mod tests {
1680    use super::*;
1681
1682    fn make_name(ir: &mut Ir, s: &str) -> InstId {
1683        let n = ir.intern_name(s);
1684        ir.add_inst(Inst::Name(n))
1685    }
1686
1687    fn make_apply(ir: &mut Ir, fn_name: &str, args: Vec<InstId>) -> InstId {
1688        let n = ir.intern_name(fn_name);
1689        ir.add_inst(Inst::ApplyFn {
1690            function: n,
1691            args,
1692        })
1693    }
1694
1695    // -- Wellformedness tests --
1696
1697    #[test]
1698    fn wellformed_base_types() {
1699        let mut ir = Ir::new();
1700        let ctx = TypeContext::default();
1701        let num = make_name(&mut ir, "/number");
1702        let str_ = make_name(&mut ir, "/string");
1703        let any = make_name(&mut ir, "/any");
1704        assert!(wellformed_type(&ir, &ctx, num).is_ok());
1705        assert!(wellformed_type(&ir, &ctx, str_).is_ok());
1706        assert!(wellformed_type(&ir, &ctx, any).is_ok());
1707    }
1708
1709    #[test]
1710    fn wellformed_struct() {
1711        let mut ir = Ir::new();
1712        let ctx = TypeContext::default();
1713        let x = make_name(&mut ir, "/x");
1714        let num = make_name(&mut ir, "/number");
1715        let y = make_name(&mut ir, "/y");
1716        let str_ = make_name(&mut ir, "/string");
1717        let s = make_apply(&mut ir, FN_STRUCT, vec![x, num, y, str_]);
1718        assert!(wellformed_type(&ir, &ctx, s).is_ok());
1719    }
1720
1721    #[test]
1722    fn wellformed_struct_odd_args() {
1723        let mut ir = Ir::new();
1724        let ctx = TypeContext::default();
1725        let x = make_name(&mut ir, "/x");
1726        let num = make_name(&mut ir, "/number");
1727        let y = make_name(&mut ir, "/y");
1728        // Odd number of args without fn:opt.
1729        let s = make_apply(&mut ir, FN_STRUCT, vec![x, num, y]);
1730        assert!(wellformed_type(&ir, &ctx, s).is_err());
1731    }
1732
1733    #[test]
1734    fn wellformed_tagged_union() {
1735        let mut ir = Ir::new();
1736        let ctx = TypeContext::default();
1737        let kind = make_name(&mut ir, "/kind");
1738        let move_ = make_name(&mut ir, "/move");
1739        let x = make_name(&mut ir, "/x");
1740        let num = make_name(&mut ir, "/number");
1741        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![x, num]);
1742        let quit = make_name(&mut ir, "/quit");
1743        let quit_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1744        let tu = make_apply(
1745            &mut ir,
1746            FN_TAGGED_UNION,
1747            vec![kind, move_, move_struct, quit, quit_struct],
1748        );
1749        assert!(wellformed_type(&ir, &ctx, tu).is_ok());
1750    }
1751
1752    #[test]
1753    fn wellformed_tagged_union_negative_too_few_args() {
1754        let mut ir = Ir::new();
1755        let ctx = TypeContext::default();
1756        let kind = make_name(&mut ir, "/kind");
1757        let tu = make_apply(&mut ir, FN_TAGGED_UNION, vec![kind]);
1758        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1759    }
1760
1761    #[test]
1762    fn wellformed_tagged_union_negative_even_args() {
1763        let mut ir = Ir::new();
1764        let ctx = TypeContext::default();
1765        let kind = make_name(&mut ir, "/kind");
1766        let move_ = make_name(&mut ir, "/move");
1767        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1768        let quit = make_name(&mut ir, "/quit");
1769        // 4 args (even) is invalid.
1770        let tu = make_apply(
1771            &mut ir,
1772            FN_TAGGED_UNION,
1773            vec![kind, move_, move_struct, quit],
1774        );
1775        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1776    }
1777
1778    #[test]
1779    fn wellformed_tagged_union_negative_dup_tags() {
1780        let mut ir = Ir::new();
1781        let ctx = TypeContext::default();
1782        let kind = make_name(&mut ir, "/kind");
1783        let move_ = make_name(&mut ir, "/move");
1784        let s1 = make_apply(&mut ir, FN_STRUCT, vec![]);
1785        let move2 = make_name(&mut ir, "/move");
1786        let s2 = make_apply(&mut ir, FN_STRUCT, vec![]);
1787        let tu = make_apply(
1788            &mut ir,
1789            FN_TAGGED_UNION,
1790            vec![kind, move_, s1, move2, s2],
1791        );
1792        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1793    }
1794
1795    #[test]
1796    fn wellformed_tagged_union_negative_tag_field_in_variant() {
1797        let mut ir = Ir::new();
1798        let ctx = TypeContext::default();
1799        let kind = make_name(&mut ir, "/kind");
1800        let move_ = make_name(&mut ir, "/move");
1801        // Variant struct has /kind field — disallowed.
1802        let kind2 = make_name(&mut ir, "/kind");
1803        let num = make_name(&mut ir, "/number");
1804        let s = make_apply(&mut ir, FN_STRUCT, vec![kind2, num]);
1805        let tu = make_apply(&mut ir, FN_TAGGED_UNION, vec![kind, move_, s]);
1806        assert!(wellformed_type(&ir, &ctx, tu).is_err());
1807    }
1808
1809    // -- Conformance tests --
1810
1811    #[test]
1812    fn conforms_base_types() {
1813        let mut ir = Ir::new();
1814        let ctx = TypeContext::default();
1815        let num = make_name(&mut ir, "/number");
1816        let any = make_name(&mut ir, "/any");
1817        let str_ = make_name(&mut ir, "/string");
1818
1819        assert!(set_conforms(&ir, &ctx, num, any));
1820        assert!(set_conforms(&ir, &ctx, num, num));
1821        assert!(!set_conforms(&ir, &ctx, num, str_));
1822    }
1823
1824    #[test]
1825    fn conforms_name_hierarchy() {
1826        let mut ir = Ir::new();
1827        let ctx = TypeContext::default();
1828        let foo = make_name(&mut ir, "/foo");
1829        let foo_bar = make_name(&mut ir, "/foo/bar");
1830        let name = make_name(&mut ir, "/name");
1831
1832        assert!(set_conforms(&ir, &ctx, foo_bar, foo));
1833        assert!(set_conforms(&ir, &ctx, foo, name));
1834        assert!(set_conforms(&ir, &ctx, foo_bar, name));
1835        assert!(!set_conforms(&ir, &ctx, foo, foo_bar));
1836    }
1837
1838    #[test]
1839    fn conforms_singleton() {
1840        let mut ir = Ir::new();
1841        let ctx = TypeContext::default();
1842        let move_ = make_name(&mut ir, "/move");
1843        let singleton = make_apply(&mut ir, FN_SINGLETON, vec![move_]);
1844        let name = make_name(&mut ir, "/name");
1845        let num = make_name(&mut ir, "/number");
1846
1847        assert!(set_conforms(&ir, &ctx, singleton, name));
1848        assert!(!set_conforms(&ir, &ctx, singleton, num));
1849    }
1850
1851    #[test]
1852    fn conforms_union() {
1853        let mut ir = Ir::new();
1854        let ctx = TypeContext::default();
1855        let num = make_name(&mut ir, "/number");
1856        let str_ = make_name(&mut ir, "/string");
1857        let union = make_apply(&mut ir, FN_UNION, vec![num, str_]);
1858
1859        // Union conforms to /any.
1860        let any = make_name(&mut ir, "/any");
1861        assert!(set_conforms(&ir, &ctx, union, any));
1862        // Number conforms to Union<number, string>.
1863        assert!(set_conforms(&ir, &ctx, num, union));
1864        // Union<number, string> does NOT conform to /number.
1865        assert!(!set_conforms(&ir, &ctx, union, num));
1866    }
1867
1868    #[test]
1869    fn conforms_struct_subtyping() {
1870        let mut ir = Ir::new();
1871        let ctx = TypeContext::default();
1872        // .Struct</x: /number, /y: /string>
1873        let x = make_name(&mut ir, "/x");
1874        let num = make_name(&mut ir, "/number");
1875        let y = make_name(&mut ir, "/y");
1876        let str_ = make_name(&mut ir, "/string");
1877        let wider = make_apply(&mut ir, FN_STRUCT, vec![x, num, y, str_]);
1878        // .Struct</x: /number>
1879        let x2 = make_name(&mut ir, "/x");
1880        let num2 = make_name(&mut ir, "/number");
1881        let narrower = make_apply(&mut ir, FN_STRUCT, vec![x2, num2]);
1882
1883        // Wider (more fields) conforms to narrower.
1884        assert!(set_conforms(&ir, &ctx, wider, narrower));
1885        // Narrower does NOT conform to wider (missing required /y).
1886        assert!(!set_conforms(&ir, &ctx, narrower, wider));
1887    }
1888
1889    #[test]
1890    fn conforms_list() {
1891        let mut ir = Ir::new();
1892        let ctx = TypeContext::default();
1893        let num = make_name(&mut ir, "/number");
1894        let any = make_name(&mut ir, "/any");
1895        let list_num = make_apply(&mut ir, FN_LIST, vec![num]);
1896        let list_any = make_apply(&mut ir, FN_LIST, vec![any]);
1897
1898        assert!(set_conforms(&ir, &ctx, list_num, list_any));
1899        assert!(!set_conforms(&ir, &ctx, list_any, list_num));
1900    }
1901
1902    // -- HasType tests --
1903
1904    #[test]
1905    fn has_type_base() {
1906        let mut ir = Ir::new();
1907        let num_type = make_name(&mut ir, "/number");
1908        let str_type = make_name(&mut ir, "/string");
1909        let val_42 = ir.add_inst(Inst::Number(42));
1910        let val_hello = {
1911            let s = ir.intern_string("hello");
1912            ir.add_inst(Inst::String(s))
1913        };
1914
1915        assert!(has_type(&ir, num_type, val_42));
1916        assert!(!has_type(&ir, str_type, val_42));
1917        assert!(has_type(&ir, str_type, val_hello));
1918    }
1919
1920    #[test]
1921    fn has_type_struct() {
1922        let mut ir = Ir::new();
1923        // Type: .Struct</x: /number>
1924        let x_name = make_name(&mut ir, "/x");
1925        let num_type = make_name(&mut ir, "/number");
1926        let struct_type = make_apply(&mut ir, FN_STRUCT, vec![x_name, num_type]);
1927
1928        // Value: {/x: 42}
1929        let x_field = ir.intern_name("/x");
1930        let val_42 = ir.add_inst(Inst::Number(42));
1931        let val_struct = ir.add_inst(Inst::Struct {
1932            fields: vec![x_field],
1933            values: vec![val_42],
1934        });
1935
1936        assert!(has_type(&ir, struct_type, val_struct));
1937    }
1938
1939    #[test]
1940    fn has_type_tagged_union() {
1941        let mut ir = Ir::new();
1942
1943        // TaggedUnion</kind, /move: .Struct</x: /number>, /quit: .Struct<>>
1944        let kind = make_name(&mut ir, "/kind");
1945        let move_tag = make_name(&mut ir, "/move");
1946        let x_name = make_name(&mut ir, "/x");
1947        let num_type = make_name(&mut ir, "/number");
1948        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![x_name, num_type]);
1949        let quit_tag = make_name(&mut ir, "/quit");
1950        let quit_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
1951        let tu = make_apply(
1952            &mut ir,
1953            FN_TAGGED_UNION,
1954            vec![kind, move_tag, move_struct, quit_tag, quit_struct],
1955        );
1956
1957        // Value: {/kind: /move, /x: 10}
1958        let kind_f = ir.intern_name("/kind");
1959        let move_v = ir.intern_name("/move");
1960        let move_val = ir.add_inst(Inst::Name(move_v));
1961        let x_f = ir.intern_name("/x");
1962        let val_10 = ir.add_inst(Inst::Number(10));
1963        let val_move = ir.add_inst(Inst::Struct {
1964            fields: vec![kind_f, x_f],
1965            values: vec![move_val, val_10],
1966        });
1967        assert!(has_type(&ir, tu, val_move));
1968
1969        // Value: {/kind: /quit}
1970        let kind_f2 = ir.intern_name("/kind");
1971        let quit_v = ir.intern_name("/quit");
1972        let quit_val = ir.add_inst(Inst::Name(quit_v));
1973        let val_quit = ir.add_inst(Inst::Struct {
1974            fields: vec![kind_f2],
1975            values: vec![quit_val],
1976        });
1977        assert!(has_type(&ir, tu, val_quit));
1978
1979        // Value: {/kind: /bad}
1980        let kind_f3 = ir.intern_name("/kind");
1981        let bad_v = ir.intern_name("/bad");
1982        let bad_val = ir.add_inst(Inst::Name(bad_v));
1983        let val_bad = ir.add_inst(Inst::Struct {
1984            fields: vec![kind_f3],
1985            values: vec![bad_val],
1986        });
1987        assert!(!has_type(&ir, tu, val_bad));
1988    }
1989
1990    #[test]
1991    fn has_type_struct_accepts_applyfn_shape() {
1992        // `fn:struct(/x, 42, /y, "hi")` (ApplyFn form — what the parser emits
1993        // for `{/x: 42, /y: "hi"}`) must satisfy .Struct</x: /number, /y: /string>.
1994        let mut ir = Ir::new();
1995        let x = make_name(&mut ir, "/x");
1996        let y = make_name(&mut ir, "/y");
1997        let num = make_name(&mut ir, "/number");
1998        let str_ = make_name(&mut ir, "/string");
1999        let stype = make_apply(&mut ir, FN_STRUCT, vec![x, num, y, str_]);
2000
2001        let x_v = make_name(&mut ir, "/x");
2002        let y_v = make_name(&mut ir, "/y");
2003        let n = ir.add_inst(Inst::Number(42));
2004        let sid = ir.intern_string("hi");
2005        let s = ir.add_inst(Inst::String(sid));
2006        let value = make_apply(&mut ir, "fn:struct", vec![x_v, n, y_v, s]);
2007        assert!(has_type(&ir, stype, value));
2008    }
2009
2010    #[test]
2011    fn has_type_list_accepts_applyfn_shape() {
2012        // `fn:list(1, 2, 3)` (ApplyFn form) must satisfy fn:List(/number).
2013        let mut ir = Ir::new();
2014        let num = make_name(&mut ir, "/number");
2015        let list_num = make_apply(&mut ir, FN_LIST, vec![num]);
2016        let n1 = ir.add_inst(Inst::Number(1));
2017        let n2 = ir.add_inst(Inst::Number(2));
2018        let n3 = ir.add_inst(Inst::Number(3));
2019        let value = make_apply(&mut ir, "fn:list", vec![n1, n2, n3]);
2020        assert!(has_type(&ir, list_num, value));
2021    }
2022
2023    #[test]
2024    fn conforms_tagged_union_identity() {
2025        // Two structurally-identical TaggedUnion types (different IR ids,
2026        // same tag field + variants) must conform to each other.
2027        let mut ir = Ir::new();
2028        let ctx = TypeContext::default();
2029        let build = |ir: &mut Ir| {
2030            let t = make_name(ir, "/type");
2031            let c = make_name(ir, "/create");
2032            let name_f = make_name(ir, "/name");
2033            let str_ = make_name(ir, "/string");
2034            let create_s = make_apply(ir, FN_STRUCT, vec![name_f, str_]);
2035            let p = make_name(ir, "/ping");
2036            let ping_s = make_apply(ir, FN_STRUCT, vec![]);
2037            make_apply(ir, FN_TAGGED_UNION, vec![t, c, create_s, p, ping_s])
2038        };
2039        let tu1 = build(&mut ir);
2040        let tu2 = build(&mut ir);
2041        assert!(set_conforms(&ir, &ctx, tu1, tu2));
2042    }
2043
2044    // -- Expansion tests --
2045
2046    #[test]
2047    fn expand_tagged_union() {
2048        let mut ir = Ir::new();
2049        let kind = make_name(&mut ir, "/kind");
2050        let move_ = make_name(&mut ir, "/move");
2051        let x = make_name(&mut ir, "/x");
2052        let num = make_name(&mut ir, "/number");
2053        let move_struct = make_apply(&mut ir, FN_STRUCT, vec![x, num]);
2054        let quit = make_name(&mut ir, "/quit");
2055        let quit_struct = make_apply(&mut ir, FN_STRUCT, vec![]);
2056        let tu = make_apply(
2057            &mut ir,
2058            FN_TAGGED_UNION,
2059            vec![kind, move_, move_struct, quit, quit_struct],
2060        );
2061
2062        let expanded = expand_tagged_union_type(&mut ir, tu).unwrap();
2063        assert!(is_union_type(&ir, expanded));
2064        let alts = union_type_args(&ir, expanded).unwrap();
2065        assert_eq!(alts.len(), 2);
2066        // First variant should be a struct with /kind, Singleton(/move), /x, /number.
2067        assert!(is_struct_type(&ir, alts[0]));
2068        // Second variant should be a struct with /kind, Singleton(/quit).
2069        assert!(is_struct_type(&ir, alts[1]));
2070    }
2071
2072    // -- UpperBound / LowerBound tests --
2073
2074    #[test]
2075    fn upper_bound_basic() {
2076        let mut ir = Ir::new();
2077        let ctx = TypeContext::default();
2078        let num = make_name(&mut ir, "/number");
2079        let str_ = make_name(&mut ir, "/string");
2080
2081        // UpperBound of [/number] = /number.
2082        assert_eq!(upper_bound(&mut ir, &ctx, &[num]), num);
2083        // UpperBound of [/number, /string] = fn:Union(/number, /string).
2084        let ub = upper_bound(&mut ir, &ctx, &[num, str_]);
2085        assert!(is_union_type(&ir, ub));
2086        // UpperBound of [] = EmptyType.
2087        let ub_empty = upper_bound(&mut ir, &ctx, &[]);
2088        assert!(is_empty_type(&ir, ub_empty));
2089    }
2090
2091    #[test]
2092    fn upper_bound_eliminates_redundant() {
2093        let mut ir = Ir::new();
2094        let ctx = TypeContext::default();
2095        let num = make_name(&mut ir, "/number");
2096        let any = make_name(&mut ir, "/any");
2097
2098        // UpperBound([/number, /any]) = /any.
2099        let ub = upper_bound(&mut ir, &ctx, &[num, any]);
2100        assert!(is_any(&ir, ub));
2101    }
2102
2103    #[test]
2104    fn upper_bound_name_hierarchy() {
2105        let mut ir = Ir::new();
2106        let ctx = TypeContext::default();
2107        let animal = make_name(&mut ir, "/animal");
2108        let animal_dog = make_name(&mut ir, "/animal/dog");
2109
2110        // /animal/dog <: /animal, so UpperBound = /animal.
2111        let ub = upper_bound(&mut ir, &ctx, &[animal, animal_dog]);
2112        assert_eq!(ub, animal);
2113    }
2114
2115    #[test]
2116    fn lower_bound_basic() {
2117        let mut ir = Ir::new();
2118        let ctx = TypeContext::default();
2119        let num = make_name(&mut ir, "/number");
2120        let any = make_name(&mut ir, "/any");
2121
2122        // LowerBound([/number, /any]) = /number.
2123        let lb = lower_bound(&mut ir, &ctx, &[num, any]);
2124        assert_eq!(lb, num);
2125    }
2126
2127    #[test]
2128    fn lower_bound_empty() {
2129        let mut ir = Ir::new();
2130        let ctx = TypeContext::default();
2131        let num = make_name(&mut ir, "/number");
2132        let str_ = make_name(&mut ir, "/string");
2133
2134        // LowerBound([/number, /string]) = EmptyType (disjoint).
2135        let lb = lower_bound(&mut ir, &ctx, &[num, str_]);
2136        assert!(is_empty_type(&ir, lb));
2137    }
2138
2139    #[test]
2140    fn intersect_type_with_union() {
2141        let mut ir = Ir::new();
2142        let ctx = TypeContext::default();
2143        let num = make_name(&mut ir, "/number");
2144        let str_ = make_name(&mut ir, "/string");
2145        let union_ns = make_apply(&mut ir, FN_UNION, vec![num, str_]);
2146
2147        // intersect(Union(/number, /string), /number) = /number.
2148        let result = intersect_type(&mut ir, &ctx, union_ns, num);
2149        assert_eq!(result, num);
2150    }
2151}