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#[derive(Debug, Copy, Clone)]
13pub enum OptReport {
14 Silence,
15 Warning,
16 Error,
17}
18pub 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}
29pub 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#[derive(Debug, Clone)]
42pub struct Incompatibility {
43 pub path: Vec<String>,
46 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
66pub fn format_report(errors: &[Incompatibility]) -> String {
84 if errors.is_empty() {
85 return String::new();
86 }
87
88 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 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 if out.ends_with('\n') {
153 out.pop();
154 }
155 out
156}
157
158pub 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#[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 if matches!(t1.as_ref(), Var(_) | Knot(_)) || matches!(t2.as_ref(), Var(_) | Knot(_)) {
215 if !gamma.insert((t1.clone(), t2.clone())) {
216 return; }
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 (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 }
407 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#[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, is_input: bool, ) {
452 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 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 (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
659pub 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}