Skip to main content

candid/pretty/
candid.rs

1use std::collections::HashMap;
2
3use crate::pretty::utils::*;
4use crate::types::{Field, FuncMode, Function, Label, SharedLabel, Type, TypeEnv, TypeInner};
5use pretty::RcDoc;
6
7static KEYWORDS: [&str; 30] = [
8    "import",
9    "service",
10    "func",
11    "type",
12    "opt",
13    "vec",
14    "record",
15    "variant",
16    "blob",
17    "principal",
18    "nat",
19    "nat8",
20    "nat16",
21    "nat32",
22    "nat64",
23    "int",
24    "int8",
25    "int16",
26    "int32",
27    "int64",
28    "float32",
29    "float64",
30    "bool",
31    "text",
32    "null",
33    "reserved",
34    "empty",
35    "oneway",
36    "query",
37    "composite_query",
38];
39
40fn is_keyword(id: &str) -> bool {
41    KEYWORDS.contains(&id)
42}
43
44pub fn is_valid_as_id(id: &str) -> bool {
45    if id.is_empty() || !id.is_ascii() {
46        return false;
47    }
48    for (i, c) in id.char_indices() {
49        if i == 0 {
50            if !c.is_ascii_alphabetic() && c != '_' {
51                return false;
52            }
53        } else if !c.is_ascii_alphanumeric() && c != '_' {
54            return false;
55        }
56    }
57    true
58}
59
60fn needs_quote(id: &str) -> bool {
61    !is_valid_as_id(id) || is_keyword(id)
62}
63
64fn ident_string(id: &str) -> String {
65    if needs_quote(id) {
66        format!("\"{}\"", id.escape_debug())
67    } else {
68        id.to_string()
69    }
70}
71
72pub fn pp_text(id: &str) -> RcDoc<'_> {
73    RcDoc::text(ident_string(id))
74}
75
76pub fn pp_ty(ty: &Type) -> RcDoc<'_> {
77    pp_ty_inner(ty.as_ref())
78}
79
80pub fn pp_ty_inner(ty: &TypeInner) -> RcDoc<'_> {
81    use TypeInner::*;
82    match ty {
83        Null => str("null"),
84        Bool => str("bool"),
85        Nat => str("nat"),
86        Int => str("int"),
87        Nat8 => str("nat8"),
88        Nat16 => str("nat16"),
89        Nat32 => str("nat32"),
90        Nat64 => str("nat64"),
91        Int8 => str("int8"),
92        Int16 => str("int16"),
93        Int32 => str("int32"),
94        Int64 => str("int64"),
95        Float32 => str("float32"),
96        Float64 => str("float64"),
97        Text => str("text"),
98        Reserved => str("reserved"),
99        Empty => str("empty"),
100        Var(ref s) => str(s),
101        Principal => str("principal"),
102        Opt(ref t) => kwd("opt").append(pp_ty(t)),
103        Vec(ref t) if matches!(t.as_ref(), Nat8) => str("blob"),
104        Vec(ref t) => kwd("vec").append(pp_ty(t)),
105        Record(ref fs) => {
106            let t = Type(ty.clone().into());
107            if t.is_tuple() {
108                let tuple = concat(fs.iter().map(|f| pp_ty(&f.ty)), ";");
109                kwd("record").append(enclose_space("{", tuple, "}"))
110            } else {
111                kwd("record").append(pp_fields(fs, false))
112            }
113        }
114        Variant(ref fs) => kwd("variant").append(pp_fields(fs, true)),
115        Func(ref func) => kwd("func").append(pp_function(func)),
116        Service(ref serv) => kwd("service").append(pp_service(serv, None)),
117        Class(ref args, ref t) => pp_class(args, t, None),
118        Knot(ref id) => RcDoc::text(format!("{id}")),
119        Unknown => str("unknown"),
120        Future => str("future"),
121    }
122}
123
124pub fn pp_docs<'a>(docs: &'a [String]) -> RcDoc<'a> {
125    lines(docs.iter().map(|line| RcDoc::text("// ").append(line)))
126}
127
128/// This function is kept for backward compatibility.
129///
130/// It is recommended to use [`pp_label_raw`] instead, which accepts a [`Label`].
131pub fn pp_label(id: &SharedLabel) -> RcDoc<'_> {
132    pp_label_raw(id.as_ref())
133}
134
135pub fn pp_label_raw(id: &Label) -> RcDoc<'_> {
136    match id {
137        Label::Named(id) => pp_text(id),
138        Label::Id(_) | Label::Unnamed(_) => RcDoc::as_string(id),
139    }
140}
141
142pub(crate) fn pp_field(field: &Field, is_variant: bool) -> RcDoc<'_> {
143    let ty_doc = if is_variant && *field.ty == TypeInner::Null {
144        RcDoc::nil()
145    } else {
146        kwd(" :").append(pp_ty(&field.ty))
147    };
148    pp_label_raw(&field.id).append(ty_doc)
149}
150
151fn pp_fields(fs: &[Field], is_variant: bool) -> RcDoc<'_> {
152    let fields = fs.iter().map(|f| pp_field(f, is_variant));
153    enclose_space("{", concat(fields, ";"), "}")
154}
155
156pub fn pp_function(func: &Function) -> RcDoc<'_> {
157    let args = pp_args(&func.args);
158    let rets = pp_rets(&func.rets);
159    let modes = pp_modes(&func.modes);
160    args.append(" ->")
161        .append(RcDoc::space())
162        .append(rets.append(modes))
163        .nest(INDENT_SPACE)
164}
165
166/// Pretty-prints arguments in the form of `(type1, type2)`.
167pub fn pp_args(args: &[Type]) -> RcDoc<'_> {
168    let doc = concat(args.iter().map(pp_ty), ",");
169    enclose("(", doc, ")")
170}
171
172/// Pretty-prints return types in the form of `(type1, type2)`.
173pub fn pp_rets(args: &[Type]) -> RcDoc<'_> {
174    pp_args(args)
175}
176
177pub fn pp_mode(mode: &FuncMode) -> RcDoc<'_> {
178    match mode {
179        FuncMode::Oneway => RcDoc::text("oneway"),
180        FuncMode::Query => RcDoc::text("query"),
181        FuncMode::CompositeQuery => RcDoc::text("composite_query"),
182    }
183}
184pub fn pp_modes(modes: &[FuncMode]) -> RcDoc<'_> {
185    RcDoc::concat(modes.iter().map(|m| RcDoc::space().append(pp_mode(m))))
186}
187
188fn pp_service<'a>(serv: &'a [(String, Type)], docs: Option<&'a DocComments>) -> RcDoc<'a> {
189    let doc = concat(
190        serv.iter().map(|(id, func)| {
191            let doc = docs
192                .and_then(|docs| docs.lookup_service_method(id))
193                .map(|docs| pp_docs(docs))
194                .unwrap_or(RcDoc::nil());
195            let func_doc = match func.as_ref() {
196                TypeInner::Func(ref f) => pp_function(f),
197                TypeInner::Var(_) => pp_ty(func),
198                _ => unreachable!(),
199            };
200            doc.append(pp_text(id)).append(kwd(" :")).append(func_doc)
201        }),
202        ";",
203    );
204    enclose_space("{", doc, "}")
205}
206
207fn pp_defs(env: &TypeEnv) -> RcDoc<'_> {
208    lines(env.0.iter().map(|(id, ty)| {
209        kwd("type")
210            .append(ident(id))
211            .append(kwd("="))
212            .append(pp_ty(ty))
213            .append(";")
214    }))
215}
216
217fn pp_class<'a>(args: &'a [Type], t: &'a Type, docs: Option<&'a DocComments>) -> RcDoc<'a> {
218    let doc = pp_args(args).append(" ->").append(RcDoc::space());
219    match t.as_ref() {
220        TypeInner::Service(ref serv) => doc.append(pp_service(serv, docs)),
221        TypeInner::Var(ref s) => doc.append(s),
222        _ => unreachable!(),
223    }
224}
225
226fn pp_actor<'a>(ty: &'a Type, docs: &'a DocComments) -> RcDoc<'a> {
227    match ty.as_ref() {
228        TypeInner::Service(ref serv) => pp_service(serv, Some(docs)),
229        TypeInner::Class(ref args, ref t) => pp_class(args, t, Some(docs)),
230        TypeInner::Var(_) => pp_ty(ty),
231        _ => unreachable!(),
232    }
233}
234
235/// Pretty-prints the initialization arguments for a Candid actor.
236pub fn pp_init_args<'a>(env: &'a TypeEnv, args: &'a [Type]) -> RcDoc<'a> {
237    pp_defs(env).append(pp_args(args))
238}
239
240/// Collects doc comments that can be passed to the [compile_with_docs] function.
241#[derive(Default, Debug)]
242pub struct DocComments {
243    service_methods: HashMap<String, Vec<String>>,
244}
245
246impl DocComments {
247    pub fn empty() -> Self {
248        Self::default()
249    }
250
251    pub fn add_service_method(&mut self, method: String, doc: Vec<String>) {
252        self.service_methods.insert(method, doc);
253    }
254
255    pub fn lookup_service_method(&self, method: &str) -> Option<&Vec<String>> {
256        self.service_methods.get(method)
257    }
258}
259
260pub fn compile(env: &TypeEnv, actor: &Option<Type>) -> String {
261    compile_with_docs(env, actor, &DocComments::empty())
262}
263
264/// Same as [compile], but also accepts doc comments that are printed in the generated Candid bindings.
265///
266/// This is useful when generating Candid bindings for Rust canister methods.
267///
268/// # Example
269///
270/// ```ignore
271/// let mut doc_comments = DocComments::empty();
272/// doc_comments.add_service_method(
273///   "method_name".to_string(),
274///   vec![
275///     "Doc comment line 1".to_string(),
276///     "".to_string(), // empty lines are preserved
277///     "Doc comment line 2".to_string(),
278///   ],
279/// );
280/// let candid = compile_with_docs(&env, &actor, &doc_comments);
281/// ```
282pub fn compile_with_docs(env: &TypeEnv, actor: &Option<Type>, docs: &DocComments) -> String {
283    match actor {
284        None => pp_defs(env).pretty(LINE_WIDTH).to_string(),
285        Some(actor) => {
286            let defs = pp_defs(env);
287            let actor = kwd("service :").append(pp_actor(actor, docs));
288            let doc = defs.append(actor);
289            doc.pretty(LINE_WIDTH).to_string()
290        }
291    }
292}
293
294#[cfg_attr(docsrs, doc(cfg(feature = "value")))]
295#[cfg(feature = "value")]
296pub mod value {
297    use super::{ident_string, pp_label_raw};
298    use crate::pretty::utils::*;
299    use crate::types::value::{IDLArgs, IDLField, IDLValue};
300    use crate::types::Label;
301    use crate::utils::pp_num_str;
302    use std::fmt;
303
304    use ::pretty::RcDoc;
305
306    // TODO config this
307    const MAX_ELEMENTS_FOR_PRETTY_PRINT: usize = 10;
308
309    impl fmt::Display for IDLArgs {
310        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311            write!(f, "{}", pp_args(self).pretty(80))
312        }
313    }
314
315    impl fmt::Display for IDLValue {
316        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317            write!(
318                f,
319                "{}",
320                pp_value(MAX_ELEMENTS_FOR_PRETTY_PRINT, self).pretty(80)
321            )
322        }
323    }
324
325    impl fmt::Debug for IDLArgs {
326        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327            if self.args.len() == 1 {
328                write!(f, "({:?})", self.args[0])
329            } else {
330                let mut tup = f.debug_tuple("");
331                for arg in self.args.iter() {
332                    tup.field(arg);
333                }
334                tup.finish()
335            }
336        }
337    }
338    fn has_type_annotation(v: &IDLValue) -> bool {
339        use IDLValue::*;
340        matches!(
341            v,
342            Int(_)
343                | Nat(_)
344                | Nat8(_)
345                | Nat16(_)
346                | Nat32(_)
347                | Nat64(_)
348                | Int8(_)
349                | Int16(_)
350                | Int32(_)
351                | Int64(_)
352                | Float32(_)
353                | Float64(_)
354                | Null
355                | Reserved
356        )
357    }
358    pub fn number_to_string(v: &IDLValue) -> String {
359        use IDLValue::*;
360        match v {
361            Number(n) => n.to_string(),
362            Int(n) => n.to_string(),
363            Nat(n) => n.to_string(),
364            Nat8(n) => n.to_string(),
365            Nat16(n) => pp_num_str(&n.to_string()),
366            Nat32(n) => pp_num_str(&n.to_string()),
367            Nat64(n) => pp_num_str(&n.to_string()),
368            Int8(n) => n.to_string(),
369            Int16(n) => pp_num_str(&n.to_string()),
370            Int32(n) => pp_num_str(&n.to_string()),
371            Int64(n) => pp_num_str(&n.to_string()),
372            Float32(f) => {
373                if f.is_finite() && f.trunc() == *f {
374                    format!("{f}.0")
375                } else {
376                    f.to_string()
377                }
378            }
379            Float64(f) => {
380                if f.is_finite() && f.trunc() == *f {
381                    format!("{f}.0")
382                } else {
383                    f.to_string()
384                }
385            }
386            _ => unreachable!(),
387        }
388    }
389    impl fmt::Debug for IDLValue {
390        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391            use IDLValue::*;
392            match self {
393                Null => write!(f, "null : null"),
394                Bool(b) => write!(f, "{b}"),
395                Number(n) => write!(f, "{n}"),
396                Int(i) => write!(f, "{i} : int"),
397                Nat(n) => write!(f, "{n} : nat"),
398                Nat8(n) => write!(f, "{n} : nat8"),
399                Nat16(n) => write!(f, "{} : nat16", pp_num_str(&n.to_string())),
400                Nat32(n) => write!(f, "{} : nat32", pp_num_str(&n.to_string())),
401                Nat64(n) => write!(f, "{} : nat64", pp_num_str(&n.to_string())),
402                Int8(n) => write!(f, "{n} : int8"),
403                Int16(n) => write!(f, "{} : int16", pp_num_str(&n.to_string())),
404                Int32(n) => write!(f, "{} : int32", pp_num_str(&n.to_string())),
405                Int64(n) => write!(f, "{} : int64", pp_num_str(&n.to_string())),
406                Float32(_) => write!(f, "{} : float32", number_to_string(self)),
407                Float64(_) => write!(f, "{} : float64", number_to_string(self)),
408                Text(s) => write!(f, "{s:?}"),
409                None => write!(f, "null"),
410                Reserved => write!(f, "null : reserved"),
411                Principal(id) => write!(f, "principal \"{id}\""),
412                Service(id) => write!(f, "service \"{id}\""),
413                Func(id, meth) => write!(f, "func \"{}\".{}", id, ident_string(meth)),
414                Opt(v) if has_type_annotation(v) => write!(f, "opt ({v:?})"),
415                Opt(v) => write!(f, "opt {v:?}"),
416                Blob(b) => {
417                    write!(f, "blob \"")?;
418                    let is_ascii = b
419                        .iter()
420                        .all(|c| (0x20u8..=0x7eu8).contains(c) || [0x09, 0x0a, 0x0d].contains(c));
421                    if is_ascii {
422                        for v in b.iter() {
423                            write!(f, "{}", pp_char(*v))?;
424                        }
425                    } else {
426                        for v in b.iter() {
427                            write!(f, "\\{v:02x}")?;
428                        }
429                    }
430                    write!(f, "\"")
431                }
432                Vec(vs) => {
433                    if let Some(Nat8(_)) = vs.first() {
434                        write!(f, "blob \"")?;
435                        for v in vs.iter() {
436                            match v {
437                                // only here for completeness. The deserializer should generate IDLValue::Blob instead.
438                                Nat8(v) => write!(f, "{}", &pp_char(*v))?,
439                                _ => unreachable!(),
440                            }
441                        }
442                        write!(f, "\"")
443                    } else {
444                        write!(f, "vec {{")?;
445                        for v in vs.iter() {
446                            write!(f, " {v:?};")?
447                        }
448                        write!(f, "}}")
449                    }
450                }
451                Record(fs) => {
452                    write!(f, "record {{")?;
453                    for (i, e) in fs.iter().enumerate() {
454                        if e.id.get_id() == i as u32 {
455                            write!(f, " {:?};", e.val)?;
456                        } else {
457                            write!(f, " {e:?};")?;
458                        }
459                    }
460                    write!(f, "}}")
461                }
462                Variant(v) => {
463                    write!(f, "variant {{ ")?;
464                    if v.0.val == Null {
465                        write!(f, "{}", v.0.id)?;
466                    } else {
467                        write!(f, "{:?}", v.0)?;
468                    }
469                    write!(f, " }}")
470                }
471            }
472        }
473    }
474    impl fmt::Debug for IDLField {
475        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476            let lab = match &self.id {
477                Label::Named(id) => ident_string(id),
478                id => id.to_string(),
479            };
480            write!(f, "{} = {:?}", lab, self.val)
481        }
482    }
483
484    // The definition of tuple is language specific.
485    fn is_tuple(t: &IDLValue) -> bool {
486        match t {
487            IDLValue::Record(ref fs) => {
488                for (i, field) in fs.iter().enumerate() {
489                    if field.id.get_id() != (i as u32) {
490                        return false;
491                    }
492                }
493                true
494            }
495            _ => false,
496        }
497    }
498
499    fn pp_field(depth: usize, field: &IDLField, is_variant: bool) -> RcDoc<'_> {
500        let val_doc = if is_variant && field.val == IDLValue::Null {
501            RcDoc::nil()
502        } else {
503            kwd(" =").append(pp_value(depth - 1, &field.val))
504        };
505        pp_label_raw(&field.id).append(val_doc)
506    }
507
508    fn pp_fields(depth: usize, fields: &[IDLField]) -> RcDoc<'_> {
509        let fs = concat(fields.iter().map(|f| pp_field(depth, f, false)), ";");
510        enclose_space("{", fs, "}")
511    }
512
513    pub fn pp_char(v: u8) -> String {
514        let is_ascii = (0x20u8..=0x7eu8).contains(&v);
515        if is_ascii && v != 0x22 && v != 0x27 && v != 0x60 && v != 0x5c {
516            std::char::from_u32(v as u32).unwrap().to_string()
517        } else {
518            format!("\\{v:02x}")
519        }
520    }
521    pub fn pp_value(depth: usize, v: &IDLValue) -> RcDoc<'_> {
522        use IDLValue::*;
523        if depth == 0 {
524            return RcDoc::as_string(format!("{v:?}"));
525        }
526        match v {
527            Text(ref s) => RcDoc::as_string(format!("\"{}\"", s.escape_debug())),
528            Opt(v) if has_type_annotation(v) => {
529                kwd("opt").append(enclose("(", pp_value(depth - 1, v), ")"))
530            }
531            Opt(v) => kwd("opt").append(pp_value(depth - 1, v)),
532            Vec(vs) => {
533                if matches!(vs.first(), Some(Nat8(_))) || vs.len() > MAX_ELEMENTS_FOR_PRETTY_PRINT {
534                    RcDoc::as_string(format!("{v:?}"))
535                } else {
536                    let values = vs.iter().map(|v| pp_value(depth - 1, v));
537                    kwd("vec").append(enclose_space("{", concat(values, ";"), "}"))
538                }
539            }
540            Record(fields) => {
541                if is_tuple(v) {
542                    let fields = fields.iter().map(|f| pp_value(depth - 1, &f.val));
543                    kwd("record").append(enclose_space("{", concat(fields, ";"), "}"))
544                } else {
545                    kwd("record").append(pp_fields(depth, fields))
546                }
547            }
548            Variant(v) => {
549                kwd("variant").append(enclose_space("{", pp_field(depth, &v.0, true), "}"))
550            }
551            _ => RcDoc::as_string(format!("{v:?}")),
552        }
553    }
554
555    pub fn pp_args(args: &IDLArgs) -> RcDoc<'_> {
556        let args = args
557            .args
558            .iter()
559            .map(|v| pp_value(MAX_ELEMENTS_FOR_PRETTY_PRINT, v));
560        enclose("(", concat(args, ","), ")")
561    }
562}