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