Skip to main content

candid/types/
subtype.rs

1use super::internal::{find_type, Field, Label, Type, TypeInner};
2use crate::types::TypeEnv;
3use crate::utils::RecursionDepth;
4use crate::{Error, Result};
5use anyhow::Context;
6use std::collections::{HashMap, HashSet};
7use std::fmt;
8
9pub type Gamma = HashSet<(Type, Type)>;
10
11/// Error reporting style for the special opt rule
12#[derive(Debug, Copy, Clone)]
13pub enum OptReport {
14    Silence,
15    Warning,
16    Error,
17}
18/// Check if t1 <: t2
19pub fn subtype(gamma: &mut Gamma, env: &TypeEnv, t1: &Type, t2: &Type) -> Result<()> {
20    subtype_(
21        OptReport::Warning,
22        gamma,
23        env,
24        t1,
25        t2,
26        &RecursionDepth::new(),
27    )
28}
29/// Check if t1 <: t2, and report the special opt rule as `Slience`, `Warning` or `Error`.
30pub fn subtype_with_config(
31    report: OptReport,
32    gamma: &mut Gamma,
33    env: &TypeEnv,
34    t1: &Type,
35    t2: &Type,
36) -> Result<()> {
37    subtype_(report, gamma, env, t1, t2, &RecursionDepth::new())
38}
39
40/// A single incompatibility found during subtype checking.
41#[derive(Debug, Clone)]
42pub struct Incompatibility {
43    /// Path to the incompatible element, from outermost to innermost.
44    /// e.g., `["method \"transfer\"", "return type", "record field \"status\""]`
45    pub path: Vec<String>,
46    /// Description of the specific incompatibility.
47    pub message: String,
48}
49
50impl fmt::Display for Incompatibility {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        if self.path.is_empty() {
53            write!(f, "{}", self.message)
54        } else {
55            for (i, segment) in self.path.iter().enumerate() {
56                if i > 0 {
57                    write!(f, " > ")?;
58                }
59                write!(f, "{segment}")?;
60            }
61            write!(f, ": {}", self.message)
62        }
63    }
64}
65
66/// Format a list of incompatibilities as a grouped, hierarchical report.
67///
68/// Errors are grouped by their shared path prefixes so that, for example,
69/// five errors under `method "transfer"` appear together rather than as
70/// five separate top-level items.
71///
72/// ```text
73/// method "transfer":
74///   return type:
75///     - record field a: text is not a subtype of nat
76///     - record field b: nat is not a subtype of bool
77///   input type:
78///     - missing required field amount (type nat)
79///
80/// method "get_user":
81///   - missing in new interface
82/// ```
83pub fn format_report(errors: &[Incompatibility]) -> String {
84    if errors.is_empty() {
85        return String::new();
86    }
87
88    // Build a tree: each node is either a branch (has children keyed by path segment)
89    // or a leaf (has a message).
90    struct Node {
91        children: Vec<(String, Node)>,
92        messages: Vec<String>,
93    }
94
95    impl Node {
96        fn new() -> Self {
97            Node {
98                children: Vec::new(),
99                messages: Vec::new(),
100            }
101        }
102        fn child(&mut self, key: &str) -> &mut Node {
103            if let Some(pos) = self.children.iter().position(|(k, _)| k == key) {
104                &mut self.children[pos].1
105            } else {
106                self.children.push((key.to_string(), Node::new()));
107                let last = self.children.len() - 1;
108                &mut self.children[last].1
109            }
110        }
111        fn insert(&mut self, path: &[String], message: &str) {
112            if path.is_empty() {
113                self.messages.push(message.to_string());
114            } else {
115                self.child(&path[0]).insert(&path[1..], message);
116            }
117        }
118        fn render(&self, out: &mut String, indent: usize) {
119            let pad = "  ".repeat(indent);
120            for msg in &self.messages {
121                out.push_str(&pad);
122                out.push_str("- ");
123                out.push_str(msg);
124                out.push('\n');
125            }
126            for (key, child) in &self.children {
127                // If the child has exactly one message and no sub-children, inline it.
128                if child.children.is_empty() && child.messages.len() == 1 {
129                    out.push_str(&pad);
130                    out.push_str(key);
131                    out.push_str(": ");
132                    out.push_str(&child.messages[0]);
133                    out.push('\n');
134                } else {
135                    out.push_str(&pad);
136                    out.push_str(key);
137                    out.push_str(":\n");
138                    child.render(out, indent + 1);
139                }
140            }
141        }
142    }
143
144    let mut root = Node::new();
145    for e in errors {
146        root.insert(&e.path, &e.message);
147    }
148
149    let mut out = String::new();
150    root.render(&mut out, 0);
151    // Remove trailing newline
152    if out.ends_with('\n') {
153        out.pop();
154    }
155    out
156}
157
158/// Check if `t1 <: t2`, collecting **all** incompatibilities instead of stopping at the first.
159///
160/// Returns an empty `Vec` when `t1` is indeed a subtype of `t2`.
161/// This is intended for upgrade compatibility reports where users need to see
162/// every breaking change at once.
163pub fn subtype_check_all(
164    gamma: &mut Gamma,
165    env: &TypeEnv,
166    t1: &Type,
167    t2: &Type,
168) -> Vec<Incompatibility> {
169    let mut errors = Vec::new();
170    subtype_collect_(
171        OptReport::Warning,
172        gamma,
173        env,
174        t1,
175        t2,
176        &RecursionDepth::new(),
177        &mut Vec::new(),
178        &mut errors,
179        false,
180    );
181    errors
182}
183
184/// Internal collecting variant of `subtype_`. Instead of short-circuiting on
185/// the first error, this continues through all fields/methods/args and pushes
186/// every incompatibility it finds into `errors`.
187#[allow(clippy::too_many_arguments)]
188fn subtype_collect_(
189    report: OptReport,
190    gamma: &mut Gamma,
191    env: &TypeEnv,
192    t1: &Type,
193    t2: &Type,
194    depth: &RecursionDepth,
195    path: &mut Vec<String>,
196    errors: &mut Vec<Incompatibility>,
197    is_input: bool,
198) {
199    let _guard = match depth.guard() {
200        Ok(g) => g,
201        Err(_) => {
202            errors.push(Incompatibility {
203                path: path.clone(),
204                message: "recursion limit exceeded".to_string(),
205            });
206            return;
207        }
208    };
209    use TypeInner::*;
210    if t1 == t2 {
211        return;
212    }
213    // Handle Var/Knot (type variables / recursive types)
214    if matches!(t1.as_ref(), Var(_) | Knot(_)) || matches!(t2.as_ref(), Var(_) | Knot(_)) {
215        if !gamma.insert((t1.clone(), t2.clone())) {
216            return; // co-inductive: assume OK
217        }
218        let before = errors.len();
219        match (t1.as_ref(), t2.as_ref()) {
220            (Var(id), _) => subtype_collect_(
221                report,
222                gamma,
223                env,
224                env.rec_find_type_with_depth(id, depth).unwrap(),
225                t2,
226                depth,
227                path,
228                errors,
229                is_input,
230            ),
231            (_, Var(id)) => subtype_collect_(
232                report,
233                gamma,
234                env,
235                t1,
236                env.rec_find_type_with_depth(id, depth).unwrap(),
237                depth,
238                path,
239                errors,
240                is_input,
241            ),
242            (Knot(id), _) => subtype_collect_(
243                report,
244                gamma,
245                env,
246                &find_type(id).unwrap(),
247                t2,
248                depth,
249                path,
250                errors,
251                is_input,
252            ),
253            (_, Knot(id)) => subtype_collect_(
254                report,
255                gamma,
256                env,
257                t1,
258                &find_type(id).unwrap(),
259                depth,
260                path,
261                errors,
262                is_input,
263            ),
264            (_, _) => unreachable!(),
265        };
266        if errors.len() > before {
267            gamma.remove(&(t1.clone(), t2.clone()));
268        }
269        return;
270    }
271    match (t1.as_ref(), t2.as_ref()) {
272        (_, Reserved) => (),
273        (Empty, _) => (),
274        (Nat, Int) => (),
275        (Vec(ty1), Vec(ty2)) => {
276            subtype_collect_(report, gamma, env, ty1, ty2, depth, path, errors, is_input);
277        }
278        (Null, Opt(_)) => (),
279        // For opt rules we delegate to the existing subtype_ to test the condition,
280        // since these are probes, not things that generate multiple independent errors.
281        (Opt(ty1), Opt(ty2)) if subtype_(report, gamma, env, ty1, ty2, depth).is_ok() => {}
282        (_, Opt(ty2))
283            if subtype_(report, gamma, env, t1, ty2, depth).is_ok()
284                && !matches!(
285                    env.trace_type_with_depth(ty2, depth)
286                        .map(|t| t.as_ref().clone()),
287                    Ok(Null | Reserved | Opt(_))
288                ) => {}
289        (_, Opt(_)) => {
290            let msg = format!("WARNING: {t1} <: {t2} due to special subtyping rules involving optional types/fields (see https://github.com/dfinity/candid/blob/c7659ca/spec/Candid.md#upgrading-and-subtyping). This means the two interfaces have diverged, which could cause data loss.");
291            match report {
292                OptReport::Silence => (),
293                OptReport::Warning => eprintln!("{msg}"),
294                OptReport::Error => {
295                    errors.push(Incompatibility {
296                        path: path.clone(),
297                        message: msg,
298                    });
299                }
300            };
301        }
302        (Record(fs1), Record(fs2)) => {
303            let fields: HashMap<_, _> = fs1.iter().map(|Field { id, ty }| (id, ty)).collect();
304            for Field { id, ty: ty2 } in fs2 {
305                match fields.get(id) {
306                    Some(ty1) => {
307                        path.push(format!("record field {id}"));
308                        subtype_collect_(
309                            report, gamma, env, ty1, ty2, depth, path, errors, is_input,
310                        );
311                        path.pop();
312                    }
313                    None => {
314                        let is_optional = env
315                            .trace_type_with_depth(ty2, depth)
316                            .map(|t| matches!(t.as_ref(), Null | Reserved | Opt(_)))
317                            .unwrap_or(false);
318                        if !is_optional {
319                            errors.push(Incompatibility {
320                                path: path.clone(),
321                                message: if is_input {
322                                    format!(
323                                        "new service requires field {id} (type {ty2}), \
324                                         which old callers don't provide and is not optional"
325                                    )
326                                } else {
327                                    format!(
328                                        "new type is missing required field {id} (type {ty2}), \
329                                         which is expected by the old type and is not optional"
330                                    )
331                                },
332                            });
333                        }
334                    }
335                }
336            }
337        }
338        (Variant(fs1), Variant(fs2)) => {
339            let fields: HashMap<_, _> = fs2.iter().map(|Field { id, ty }| (id, ty)).collect();
340            for Field { id, ty: ty1 } in fs1 {
341                match fields.get(id) {
342                    Some(ty2) => {
343                        path.push(format!("variant field {id}"));
344                        subtype_collect_(
345                            report, gamma, env, ty1, ty2, depth, path, errors, is_input,
346                        );
347                        path.pop();
348                    }
349                    None => {
350                        errors.push(Incompatibility {
351                            path: path.clone(),
352                            message: if is_input {
353                                format!(
354                                    "old callers may send variant case {id}, \
355                                     which the new service no longer handles"
356                                )
357                            } else {
358                                format!(
359                                    "new variant has field {id} that does not exist in the old type"
360                                )
361                            },
362                        });
363                    }
364                }
365            }
366        }
367        (Service(ms1), Service(ms2)) => {
368            let meths: HashMap<_, _> = ms1.iter().cloned().collect();
369            for (name, ty2) in ms2 {
370                match meths.get(name) {
371                    Some(ty1) => {
372                        path.push(format!("method \"{name}\""));
373                        subtype_collect_(report, gamma, env, ty1, ty2, depth, path, errors, false);
374                        path.pop();
375                    }
376                    None => {
377                        errors.push(Incompatibility {
378                            path: path.clone(),
379                            message: format!(
380                                "method \"{name}\" is expected by the old interface but missing in the new one"
381                            ),
382                        });
383                    }
384                }
385            }
386        }
387        (Func(f1), Func(f2)) => {
388            if f1.modes != f2.modes {
389                errors.push(Incompatibility {
390                    path: path.clone(),
391                    message: format!(
392                        "function annotation changed from {old} to {new}",
393                        old = if f2.modes.is_empty() {
394                            "update".to_string()
395                        } else {
396                            pp_modes(&f2.modes)
397                        },
398                        new = if f1.modes.is_empty() {
399                            "update".to_string()
400                        } else {
401                            pp_modes(&f1.modes)
402                        },
403                    ),
404                });
405                // Don't return early - also check arg/ret compatibility
406            }
407            // Check each argument directly instead of wrapping in a tuple record,
408            // so we get clean error paths like "input argument 1" instead of "record field 0".
409            check_func_params(
410                report, gamma, env, &f2.args, &f1.args, depth, path, errors, "input", true,
411            );
412            check_func_params(
413                report, gamma, env, &f1.rets, &f2.rets, depth, path, errors, "return", false,
414            );
415        }
416        (Class(_, t), _) => {
417            subtype_collect_(report, gamma, env, t, t2, depth, path, errors, is_input);
418        }
419        (_, Class(_, t)) => {
420            subtype_collect_(report, gamma, env, t1, t, depth, path, errors, is_input);
421        }
422        (Unknown, _) => unreachable!(),
423        (_, Unknown) => unreachable!(),
424        (_, _) => {
425            errors.push(Incompatibility {
426                path: path.clone(),
427                message: format!("{t1} is not a subtype of {t2}"),
428            });
429        }
430    }
431}
432
433/// Check function parameters (args or rets) for subtype compatibility,
434/// collecting all errors. Handles the record-tuple wrapping so that single-arg
435/// functions don't produce misleading "record field 0" paths.
436///
437/// For inputs (contravariant): `sub_params` = old args, `sup_params` = new args.
438/// For outputs (covariant): `sub_params` = new rets, `sup_params` = old rets.
439#[allow(clippy::too_many_arguments)]
440fn check_func_params(
441    report: OptReport,
442    gamma: &mut Gamma,
443    env: &TypeEnv,
444    sub_params: &[Type],
445    sup_params: &[Type],
446    depth: &RecursionDepth,
447    path: &mut Vec<String>,
448    errors: &mut Vec<Incompatibility>,
449    label: &str,    // "input" or "return"
450    is_input: bool, // affects wording
451) {
452    // Use the same tuple wrapping as the original subtype_ for correctness,
453    // but when there's a single parameter, check it directly to avoid noise.
454    if sub_params.len() == 1 && sup_params.len() == 1 {
455        path.push(format!("{label} type"));
456        subtype_collect_(
457            report,
458            gamma,
459            env,
460            &sub_params[0],
461            &sup_params[0],
462            depth,
463            path,
464            errors,
465            is_input,
466        );
467        path.pop();
468    } else {
469        let sub_tuple = to_tuple(sub_params);
470        let sup_tuple = to_tuple(sup_params);
471        path.push(if sub_params.len() == sup_params.len() {
472            format!("{label} types")
473        } else if is_input {
474            // sub_params = old args, sup_params = new args (contravariant swap)
475            format!(
476                "{label} types (old has {} arg{}, new has {})",
477                sub_params.len(),
478                if sub_params.len() == 1 { "" } else { "s" },
479                sup_params.len()
480            )
481        } else {
482            format!(
483                "{label} types (old has {} value{}, new has {})",
484                sup_params.len(),
485                if sup_params.len() == 1 { "" } else { "s" },
486                sub_params.len()
487            )
488        });
489        subtype_collect_(
490            report, gamma, env, &sub_tuple, &sup_tuple, depth, path, errors, is_input,
491        );
492        path.pop();
493    }
494}
495
496fn pp_modes(modes: &[super::internal::FuncMode]) -> String {
497    if modes.is_empty() {
498        return String::new();
499    }
500    modes
501        .iter()
502        .map(|m| match m {
503            super::internal::FuncMode::Oneway => "oneway",
504            super::internal::FuncMode::Query => "query",
505            super::internal::FuncMode::CompositeQuery => "composite_query",
506        })
507        .collect::<Vec<_>>()
508        .join(" ")
509}
510
511fn subtype_(
512    report: OptReport,
513    gamma: &mut Gamma,
514    env: &TypeEnv,
515    t1: &Type,
516    t2: &Type,
517    depth: &RecursionDepth,
518) -> Result<()> {
519    let _guard = depth.guard()?;
520    use TypeInner::*;
521    if t1 == t2 {
522        return Ok(());
523    }
524    if matches!(t1.as_ref(), Var(_) | Knot(_)) || matches!(t2.as_ref(), Var(_) | Knot(_)) {
525        if !gamma.insert((t1.clone(), t2.clone())) {
526            return Ok(());
527        }
528        let res = match (t1.as_ref(), t2.as_ref()) {
529            (Var(id), _) => subtype_(
530                report,
531                gamma,
532                env,
533                env.rec_find_type_with_depth(id, depth).unwrap(),
534                t2,
535                depth,
536            ),
537            (_, Var(id)) => subtype_(
538                report,
539                gamma,
540                env,
541                t1,
542                env.rec_find_type_with_depth(id, depth).unwrap(),
543                depth,
544            ),
545            (Knot(id), _) => subtype_(report, gamma, env, &find_type(id).unwrap(), t2, depth),
546            (_, Knot(id)) => subtype_(report, gamma, env, t1, &find_type(id).unwrap(), depth),
547            (_, _) => unreachable!(),
548        };
549        if res.is_err() {
550            gamma.remove(&(t1.clone(), t2.clone()));
551        }
552        return res;
553    }
554    match (t1.as_ref(), t2.as_ref()) {
555        (_, Reserved) => Ok(()),
556        (Empty, _) => Ok(()),
557        (Nat, Int) => Ok(()),
558        (Vec(ty1), Vec(ty2)) => subtype_(report, gamma, env, ty1, ty2, depth),
559        (Null, Opt(_)) => Ok(()),
560        (Opt(ty1), Opt(ty2)) if subtype_(report, gamma, env, ty1, ty2, depth).is_ok() => Ok(()),
561        (_, Opt(ty2))
562            if subtype_(report, gamma, env, t1, ty2, depth).is_ok()
563                && !matches!(
564                    env.trace_type_with_depth(ty2, depth)?.as_ref(),
565                    Null | Reserved | Opt(_)
566                ) =>
567        {
568            Ok(())
569        }
570        (_, Opt(_)) => {
571            let msg = format!("WARNING: {t1} <: {t2} due to special subtyping rules involving optional types/fields (see https://github.com/dfinity/candid/blob/c7659ca/spec/Candid.md#upgrading-and-subtyping). This means the two interfaces have diverged, which could cause data loss.");
572            match report {
573                OptReport::Silence => (),
574                OptReport::Warning => eprintln!("{msg}"),
575                OptReport::Error => return Err(Error::msg(msg)),
576            };
577            Ok(())
578        }
579        (Record(fs1), Record(fs2)) => {
580            let fields: HashMap<_, _> = fs1.iter().map(|Field { id, ty }| (id, ty)).collect();
581            for Field { id, ty: ty2 } in fs2 {
582                match fields.get(id) {
583                    Some(ty1) => {
584                        subtype_(report, gamma, env, ty1, ty2, depth).with_context(|| {
585                            format!("Record field {id}: {ty1} is not a subtype of {ty2}")
586                        })?
587                    }
588                    None => {
589                        if !matches!(
590                            env.trace_type_with_depth(ty2, depth)?.as_ref(),
591                            Null | Reserved | Opt(_)
592                        ) {
593                            return Err(Error::msg(format!("Record field {id}: {ty2} is only in the expected type and is not of type opt, null or reserved")));
594                        }
595                    }
596                }
597            }
598            Ok(())
599        }
600        (Variant(fs1), Variant(fs2)) => {
601            let fields: HashMap<_, _> = fs2.iter().map(|Field { id, ty }| (id, ty)).collect();
602            for Field { id, ty: ty1 } in fs1 {
603                match fields.get(id) {
604                    Some(ty2) => {
605                        subtype_(report, gamma, env, ty1, ty2, depth).with_context(|| {
606                            format!("Variant field {id}: {ty1} is not a subtype of {ty2}")
607                        })?
608                    }
609                    None => {
610                        return Err(Error::msg(format!(
611                            "Variant field {id} not found in the expected type"
612                        )));
613                    }
614                }
615            }
616            Ok(())
617        }
618        (Service(ms1), Service(ms2)) => {
619            let meths: HashMap<_, _> = ms1.iter().cloned().collect();
620            for (name, ty2) in ms2 {
621                match meths.get(name) {
622                    Some(ty1) => {
623                        subtype_(report, gamma, env, ty1, ty2, depth).with_context(|| {
624                            format!("Method {name}: {ty1} is not a subtype of {ty2}")
625                        })?
626                    }
627                    None => {
628                        return Err(Error::msg(format!(
629                            "Method {name} is only in the expected type"
630                        )));
631                    }
632                }
633            }
634            Ok(())
635        }
636        (Func(f1), Func(f2)) => {
637            if f1.modes != f2.modes {
638                return Err(Error::msg("Function mode mismatch"));
639            }
640            let args1 = to_tuple(&f1.args);
641            let args2 = to_tuple(&f2.args);
642            let rets1 = to_tuple(&f1.rets);
643            let rets2 = to_tuple(&f2.rets);
644            subtype_(report, gamma, env, &args2, &args1, depth)
645                .context("Subtype fails at function input type")?;
646            subtype_(report, gamma, env, &rets1, &rets2, depth)
647                .context("Subtype fails at function return type")?;
648            Ok(())
649        }
650        // This only works in the first order case, but service constructor only appears at the top level according to the spec.
651        (Class(_, t), _) => subtype_(report, gamma, env, t, t2, depth),
652        (_, Class(_, t)) => subtype_(report, gamma, env, t1, t, depth),
653        (Unknown, _) => unreachable!(),
654        (_, Unknown) => unreachable!(),
655        (_, _) => Err(Error::msg(format!("{t1} is not a subtype of {t2}"))),
656    }
657}
658
659/// Check if t1 and t2 are structurally equivalent, ignoring the variable naming differences.
660/// Note that this is more strict than `t1 <: t2` and `t2 <: t1`, because of the special opt rule.
661pub fn equal(gamma: &mut Gamma, env: &TypeEnv, t1: &Type, t2: &Type) -> Result<()> {
662    equal_impl(gamma, env, t1, t2, &RecursionDepth::new())
663}
664
665fn equal_impl(
666    gamma: &mut Gamma,
667    env: &TypeEnv,
668    t1: &Type,
669    t2: &Type,
670    depth: &RecursionDepth,
671) -> Result<()> {
672    let _guard = depth.guard()?;
673    use TypeInner::*;
674    if t1 == t2 {
675        return Ok(());
676    }
677    if matches!(t1.as_ref(), Var(_) | Knot(_)) || matches!(t2.as_ref(), Var(_) | Knot(_)) {
678        if !gamma.insert((t1.clone(), t2.clone())) {
679            return Ok(());
680        }
681        let res = match (t1.as_ref(), t2.as_ref()) {
682            (Var(id), _) => equal_impl(
683                gamma,
684                env,
685                env.rec_find_type_with_depth(id, depth).unwrap(),
686                t2,
687                depth,
688            ),
689            (_, Var(id)) => equal_impl(
690                gamma,
691                env,
692                t1,
693                env.rec_find_type_with_depth(id, depth).unwrap(),
694                depth,
695            ),
696            (Knot(id), _) => equal_impl(gamma, env, &find_type(id).unwrap(), t2, depth),
697            (_, Knot(id)) => equal_impl(gamma, env, t1, &find_type(id).unwrap(), depth),
698            (_, _) => unreachable!(),
699        };
700        if res.is_err() {
701            gamma.remove(&(t1.clone(), t2.clone()));
702        }
703        return res;
704    }
705    match (t1.as_ref(), t2.as_ref()) {
706        (Opt(ty1), Opt(ty2)) => equal_impl(gamma, env, ty1, ty2, depth),
707        (Vec(ty1), Vec(ty2)) => equal_impl(gamma, env, ty1, ty2, depth),
708        (Record(fs1), Record(fs2)) | (Variant(fs1), Variant(fs2)) => {
709            assert_length(fs1, fs2, |x| x.id.clone(), |x| x.to_string())
710                .context("Different field length")?;
711            for (f1, f2) in fs1.iter().zip(fs2.iter()) {
712                if f1.id != f2.id {
713                    return Err(Error::msg(format!(
714                        "Field name mismatch: {} and {}",
715                        f1.id, f2.id
716                    )));
717                }
718                equal_impl(gamma, env, &f1.ty, &f2.ty, depth).context(format!(
719                    "Field {} has different types: {} and {}",
720                    f1.id, f1.ty, f2.ty
721                ))?;
722            }
723            Ok(())
724        }
725        (Service(ms1), Service(ms2)) => {
726            assert_length(ms1, ms2, |x| x.0.clone(), |x| format!("method {x}"))
727                .context("Different method length")?;
728            for (m1, m2) in ms1.iter().zip(ms2.iter()) {
729                if m1.0 != m2.0 {
730                    return Err(Error::msg(format!(
731                        "Method name mismatch: {} and {}",
732                        m1.0, m2.0
733                    )));
734                }
735                equal_impl(gamma, env, &m1.1, &m2.1, depth).context(format!(
736                    "Method {} has different types: {} and {}",
737                    m1.0, m1.1, m2.1
738                ))?;
739            }
740            Ok(())
741        }
742        (Func(f1), Func(f2)) => {
743            if f1.modes != f2.modes {
744                return Err(Error::msg("Function mode mismatch"));
745            }
746            let args1 = to_tuple(&f1.args);
747            let args2 = to_tuple(&f2.args);
748            let rets1 = to_tuple(&f1.rets);
749            let rets2 = to_tuple(&f2.rets);
750            equal_impl(gamma, env, &args1, &args2, depth)
751                .context("Mismatch in function input type")?;
752            equal_impl(gamma, env, &rets1, &rets2, depth)
753                .context("Mismatch in function return type")?;
754            Ok(())
755        }
756        (Class(init1, ty1), Class(init2, ty2)) => {
757            let init_1 = to_tuple(init1);
758            let init_2 = to_tuple(init2);
759            equal_impl(gamma, env, &init_1, &init_2, depth).context(format!(
760                "Mismatch in init args: {} and {}",
761                pp_args(init1),
762                pp_args(init2)
763            ))?;
764            equal_impl(gamma, env, ty1, ty2, depth)
765        }
766        (Unknown, _) => unreachable!(),
767        (_, Unknown) => unreachable!(),
768        (_, _) => Err(Error::msg(format!("{t1} is not equal to {t2}"))),
769    }
770}
771
772fn assert_length<I, F, K, D>(left: &[I], right: &[I], get_key: F, display: D) -> Result<()>
773where
774    F: Fn(&I) -> K + Clone,
775    K: std::hash::Hash + std::cmp::Eq,
776    D: Fn(&K) -> String,
777{
778    let l = left.len();
779    let r = right.len();
780    if l == r {
781        return Ok(());
782    }
783    let left: HashSet<_> = left.iter().map(get_key.clone()).collect();
784    let right: HashSet<_> = right.iter().map(get_key).collect();
785    if l < r {
786        let mut diff = right.difference(&left);
787        Err(Error::msg(format!(
788            "Left side is missing {}",
789            display(diff.next().unwrap())
790        )))
791    } else {
792        let mut diff = left.difference(&right);
793        Err(Error::msg(format!(
794            "Right side is missing {}",
795            display(diff.next().unwrap())
796        )))
797    }
798}
799
800fn to_tuple(args: &[Type]) -> Type {
801    TypeInner::Record(
802        args.iter()
803            .enumerate()
804            .map(|(i, ty)| Field {
805                id: Label::Id(i as u32).into(),
806                ty: ty.clone(),
807            })
808            .collect(),
809    )
810    .into()
811}
812#[cfg(not(feature = "printer"))]
813fn pp_args(args: &[crate::types::Type]) -> String {
814    use std::fmt::Write;
815    let mut s = String::new();
816    write!(&mut s, "(").unwrap();
817    for arg in args.iter() {
818        write!(&mut s, "{:?}, ", arg).unwrap();
819    }
820    write!(&mut s, ")").unwrap();
821    s
822}
823#[cfg(feature = "printer")]
824fn pp_args(args: &[crate::types::Type]) -> String {
825    use crate::pretty::candid::pp_args;
826    pp_args(args).pretty(80).to_string()
827}