diff --git a/rust/candid/src/pretty/candid.rs b/rust/candid/src/pretty/candid.rs index 62cc4f60e..3de74f786 100644 --- a/rust/candid/src/pretty/candid.rs +++ b/rust/candid/src/pretty/candid.rs @@ -5,6 +5,8 @@ use crate::types::{ }; use pretty::RcDoc; +const DOC_COMMENT_LINE_PREFIX: &str = "/// "; + static KEYWORDS: [&str; 30] = [ "import", "service", @@ -141,7 +143,9 @@ pub(crate) fn pp_field(field: &TypeField, is_variant: bool) -> RcDoc { } else { kwd(" :").append(pp_ty(&field.typ)) }; - pp_label(&field.label).append(ty_doc) + pp_doc_comment(field.doc_comment.as_ref()) + .append(pp_label(&field.label)) + .append(ty_doc) } fn pp_fields(fs: &[TypeField], is_variant: bool) -> RcDoc { @@ -189,22 +193,32 @@ pub fn pp_modes(modes: &[FuncMode]) -> RcDoc { fn pp_service(serv: &[Binding]) -> RcDoc { let doc = concat( - serv.iter().map(|Binding { id, typ }| { - let func_doc = match typ { - IDLType::FuncT(ref f) => pp_function(f), - IDLType::VarT(_) => pp_ty(typ), - _ => unreachable!(), - }; - pp_text(id).append(kwd(" :")).append(func_doc) - }), + serv.iter().map( + |Binding { + id, + typ, + doc_comment, + }| { + let func_doc = match typ { + IDLType::FuncT(ref f) => pp_function(f), + IDLType::VarT(_) => pp_ty(typ), + _ => unreachable!(), + }; + pp_doc_comment(doc_comment.as_ref()) + .append(pp_text(id)) + .append(kwd(" :")) + .append(func_doc) + }, + ), ";", ); enclose_space("{", doc, "}") } fn pp_defs(env: &IDLMergedProg) -> RcDoc { - lines(env.get_types().iter().map(|(id, typ)| { - kwd("type") + lines(env.get_types().iter().map(|(id, typ, doc_comment)| { + pp_doc_comment(*doc_comment) + .append(kwd("type")) .append(ident(id)) .append(kwd("=")) .append(pp_ty(typ)) @@ -220,6 +234,20 @@ fn pp_actor(ty: &IDLType) -> RcDoc { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + if let Some(comment_lines) = comment_lines { + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + doc_comment +} + pub fn pp_init_args<'a>(env: &'a IDLMergedProg, args: &'a [IDLArgType]) -> RcDoc<'a> { pp_defs(env).append(pp_args(args)) } @@ -228,7 +256,9 @@ pub fn compile(env: &IDLMergedProg) -> String { None => pp_defs(env).pretty(LINE_WIDTH).to_string(), Some(actor) => { let defs = pp_defs(env); - let actor = kwd("service :").append(pp_actor(actor)); + let actor = pp_doc_comment(actor.doc_comment.as_ref()) + .append(kwd("service :")) + .append(pp_actor(&actor.typ)); let doc = defs.append(actor); doc.pretty(LINE_WIDTH).to_string() } diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index 98d25e61a..d237b38f1 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{collections::HashMap, fmt}; use crate::types::{ArgType, Field, FuncMode, Function, Label, Type, TypeInner}; @@ -183,6 +183,7 @@ impl IDLArgType { pub struct TypeField { pub label: Label, pub typ: IDLType, + pub doc_comment: Option>, } impl From for Field { @@ -205,12 +206,19 @@ pub enum Dec { pub struct Binding { pub id: String, pub typ: IDLType, + pub doc_comment: Option>, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] +pub struct IDLActorType { + pub typ: IDLType, + pub doc_comment: Option>, +} + +#[derive(Debug, Clone)] pub struct IDLProg { pub decs: Vec, - pub actor: Option, + pub actor: Option, } #[derive(Debug)] @@ -223,7 +231,7 @@ pub struct IDLInitArgs { #[derive(Debug, Default)] pub struct IDLMergedProg { types: Vec, - pub actor: Option, + pub actor: Option, } impl From for IDLMergedProg { @@ -235,6 +243,15 @@ impl From for IDLMergedProg { } } +impl From for IDLMergedProg { + fn from(other_actor: IDLActorType) -> Self { + Self { + types: vec![], + actor: Some(other_actor), + } + } +} + impl From> for IDLMergedProg { fn from(bindings: Vec) -> Self { Self { @@ -260,14 +277,25 @@ impl IDLMergedProg { self.types.extend(types); } - pub fn set_actor(&mut self, other: Option) { + pub fn set_actor(&mut self, other: Option) { self.actor = other; } + pub fn set_comments_in_actor(&mut self, doc_comments: &HashMap>) { + self.actor = self.actor.as_ref().map(|t| IDLActorType { + typ: set_comments_in_type(&t.typ, doc_comments), + doc_comment: t.doc_comment.clone(), + }); + } + pub fn find_type(&self, id: &str) -> Result<&IDLType, String> { + self.find_binding(id).map(|b| &b.typ) + } + + pub fn find_binding(&self, id: &str) -> Result<&Binding, String> { self.types .iter() - .find_map(|t| if t.id == id { Some(&t.typ) } else { None }) + .find(|t| t.id == id) .ok_or(format!("Type identifier not found: {id}")) } @@ -279,8 +307,11 @@ impl IDLMergedProg { } } - pub fn get_types(&self) -> Vec<(&str, &IDLType)> { - self.types.iter().map(|t| (t.id.as_str(), &t.typ)).collect() + pub fn get_types(&self) -> Vec<(&str, &IDLType, Option<&Vec>)> { + self.types + .iter() + .map(|t| (t.id.as_str(), &t.typ, t.doc_comment.as_ref())) + .collect() } pub fn get_bindings(&self) -> Vec { @@ -328,3 +359,72 @@ impl IDLMergedProg { Err(format!("cannot find method {id}")) } } + +fn set_comments_in_type(t: &IDLType, doc_comments: &HashMap>) -> IDLType { + match t { + IDLType::PrimT(prim) => IDLType::PrimT(prim.clone()), + IDLType::VarT(id) => IDLType::VarT(id.clone()), + IDLType::FuncT(func) => IDLType::FuncT(FuncType { + modes: func.modes.clone(), + args: func + .args + .iter() + .map(|a| IDLArgType { + typ: set_comments_in_type(&a.typ, doc_comments), + name: a.name.clone(), + }) + .collect(), + rets: func + .rets + .iter() + .map(|r| set_comments_in_type(r, doc_comments)) + .collect(), + }), + IDLType::OptT(t) => IDLType::OptT(Box::new(set_comments_in_type(t, doc_comments))), + IDLType::VecT(t) => IDLType::VecT(Box::new(set_comments_in_type(t, doc_comments))), + IDLType::RecordT(fields) => { + let fields = fields + .iter() + .map(|f| TypeField { + label: f.label.clone(), + typ: set_comments_in_type(&f.typ, doc_comments), + doc_comment: doc_comments.get(&f.label.to_string()).cloned(), + }) + .collect(); + IDLType::RecordT(fields) + } + IDLType::ServT(methods) => { + let methods = methods + .iter() + .map(|m| Binding { + id: m.id.clone(), + typ: set_comments_in_type(&m.typ, doc_comments), + doc_comment: doc_comments.get(&m.id).cloned(), + }) + .collect(); + IDLType::ServT(methods) + } + IDLType::VariantT(fields) => { + let fields = fields + .iter() + .map(|f| TypeField { + label: f.label.clone(), + typ: set_comments_in_type(&f.typ, doc_comments), + doc_comment: doc_comments.get(&f.label.to_string()).cloned(), + }) + .collect(); + IDLType::VariantT(fields) + } + IDLType::ClassT(args, t) => { + let args = args + .iter() + .map(|a| IDLArgType { + typ: set_comments_in_type(&a.typ, doc_comments), + name: a.name.clone(), + }) + .collect(); + IDLType::ClassT(args, Box::new(set_comments_in_type(t, doc_comments))) + } + IDLType::PrincipalT => IDLType::PrincipalT, + } +} diff --git a/rust/candid/src/types/type_env.rs b/rust/candid/src/types/type_env.rs index 0235e473c..2fa8b0d72 100644 --- a/rust/candid/src/types/type_env.rs +++ b/rust/candid/src/types/type_env.rs @@ -240,6 +240,7 @@ impl TypeEnv { TypeField { label: field.id.as_ref().clone(), typ: self.as_idl_type(&field.ty), + doc_comment: None, } } @@ -256,6 +257,7 @@ impl TypeEnv { .map(|(id, t)| Binding { id: id.clone(), typ: self.as_idl_type(t), + doc_comment: None, }) .collect() } diff --git a/rust/candid/tests/types.rs b/rust/candid/tests/types.rs index 5abf54374..847dbedaf 100644 --- a/rust/candid/tests/types.rs +++ b/rust/candid/tests/types.rs @@ -156,10 +156,12 @@ fn test_struct() { TypeField { label: Label::Named("bar".to_string()), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, TypeField { label: Label::Named("foo".to_string()), typ: IDLType::PrimT(PrimType::Int), + doc_comment: None, }, ]) ); @@ -181,10 +183,12 @@ fn test_struct() { TypeField { label: Label::Named("g1".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("g2".to_string()), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, ]) ); @@ -205,10 +209,12 @@ fn test_struct() { TypeField { label: Label::Named("head".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("tail".to_string()), typ: IDLType::OptT(Box::new(IDLType::VarT("List".to_string()))), + doc_comment: None, }, ]) ); @@ -229,10 +235,12 @@ fn test_struct() { TypeField { label: Label::Named("head".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("tail".to_string()), typ: IDLType::OptT(Box::new(IDLType::VarT("GenericList".to_string()))), + doc_comment: None, }, ]) ); @@ -250,10 +258,12 @@ fn test_struct() { TypeField { label: Label::Named("head".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("tail".to_string()), typ: IDLType::OptT(Box::new(IDLType::VarT("GenericList".to_string()))), + doc_comment: None, }, ]) ); @@ -292,12 +302,15 @@ fn test_variant() { TypeField { label: Label::Id(0), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, TypeField { label: Label::Id(1), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, ]), + doc_comment: None, }, TypeField { label: Label::Named("Baz".to_string()), @@ -305,20 +318,25 @@ fn test_variant() { TypeField { label: Label::Named("a".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("b".to_string()), typ: IDLType::PrimT(PrimType::Nat32), + doc_comment: None, }, ]), + doc_comment: None, }, TypeField { label: Label::Named("Foo".to_string()), typ: IDLType::PrimT(PrimType::Null), + doc_comment: None, }, TypeField { label: Label::Named("Newtype".to_string()), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, ]) ); diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 60133a391..2fff7b72b 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -122,7 +122,7 @@ pub(crate) fn export_service(path: Option) -> TokenStream { }); let service = quote! { use #candid::types::{CandidType, Function, Type, ArgType, TypeInner}; - use #candid::types::syntax::{Binding, IDLMergedProg}; + use #candid::types::syntax::{Binding, IDLMergedProg, IDLActorType}; let mut service = Vec::<(String, Type)>::new(); let mut env = #candid::types::internal::TypeContainer::new(); #(#gen_tys)* @@ -145,9 +145,13 @@ pub(crate) fn export_service(path: Option) -> TokenStream { let bindings = env.env.0.iter().map(|(id, t)| Binding { id: id.clone(), typ: env.as_idl_type(t), + doc_comment: None, }).collect::>(); let mut idl_merged_prog = IDLMergedProg::from(bindings); - idl_merged_prog.set_actor(Some(env.as_idl_type(&actor))); + idl_merged_prog.set_actor(Some(IDLActorType { + typ: env.as_idl_type(&actor), + doc_comment: None, + })); let result = #candid::pretty::candid::compile(&idl_merged_prog); format!("{}", result) diff --git a/rust/candid_parser/src/bindings/analysis.rs b/rust/candid_parser/src/bindings/analysis.rs index b8c84038c..35172182b 100644 --- a/rust/candid_parser/src/bindings/analysis.rs +++ b/rust/candid_parser/src/bindings/analysis.rs @@ -87,7 +87,7 @@ pub fn chase_actor<'a>(prog: &'a IDLMergedProg, actor: &'a IDLType) -> Result Result>> { let mut res = BTreeMap::new(); let actor = prog.actor.as_ref().ok_or_else(|| Error::msg("no actor"))?; - let actor = prog.trace_type(actor).map_err(Error::msg)?; + let actor = prog.trace_type(&actor.typ).map_err(Error::msg)?; if let IDLType::ClassT(args, _) = &actor { for (i, arg) in args.iter().enumerate() { let mut used = Vec::new(); diff --git a/rust/candid_parser/src/bindings/javascript.rs b/rust/candid_parser/src/bindings/javascript.rs index bc72b0d4a..724890159 100644 --- a/rust/candid_parser/src/bindings/javascript.rs +++ b/rust/candid_parser/src/bindings/javascript.rs @@ -192,8 +192,13 @@ fn pp_modes(modes: &[candid::types::FuncMode]) -> RcDoc { fn pp_service(serv: &[Binding]) -> RcDoc { let doc = concat( - serv.iter() - .map(|Binding { id, typ }| quote_ident(id).append(kwd(":")).append(pp_ty(typ))), + serv.iter().map( + |Binding { + id, + typ, + doc_comment: _, + }| quote_ident(id).append(kwd(":")).append(pp_ty(typ)), + ), ",", ); enclose_space("({", doc, "})") @@ -249,16 +254,18 @@ pub fn compile(prog: &IDLMergedProg) -> String { doc.pretty(LINE_WIDTH).to_string() } Some(actor) => { - let def_list = chase_actor(prog, actor).unwrap(); + let def_list = chase_actor(prog, &actor.typ).unwrap(); let recs = infer_rec(prog, &def_list).unwrap(); let defs = pp_defs(prog, &def_list, &recs); - let init = if let IDLType::ClassT(ref args, _) = actor { + let init = if let IDLType::ClassT(ref args, _) = actor.typ { args.iter().map(|arg| arg.typ.clone()).collect::>() } else { Vec::new() }; let init = init.as_slice(); - let actor = kwd("return").append(pp_actor(actor, &recs)).append(";"); + let actor = kwd("return") + .append(pp_actor(&actor.typ, &recs)) + .append(";"); let body = defs.append(actor); let doc = str("export const idlFactory = ({ IDL }) => ") .append(enclose_space("{", body, "};")); diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index a41dc387e..ae1c5734e 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -10,6 +10,8 @@ use candid::types::{ }; use pretty::RcDoc; +const DOC_COMMENT_LINE_PREFIX: &str = "/// "; + fn is_tuple(t: &IDLType) -> bool { match t { IDLType::RecordT(ref fs) => { @@ -162,17 +164,20 @@ fn pp_label(id: &Label) -> RcDoc { } fn pp_field(field: &TypeField) -> RcDoc { - pp_label(&field.label) + pp_doc_comment(field.doc_comment.as_ref()) + .append(pp_label(&field.label)) .append(" : ") .append(pp_ty(&field.typ)) } + fn pp_variant(field: &TypeField) -> RcDoc { - let doc = str("#").append(pp_label(&field.label)); - if !field.typ.is_null() { - doc.append(" : ").append(pp_ty(&field.typ)) + let label = str("#").append(pp_label(&field.label)); + let doc = if !field.typ.is_null() { + label.append(" : ").append(pp_ty(&field.typ)) } else { - doc - } + label + }; + pp_doc_comment(field.doc_comment.as_ref()).append(doc) } fn pp_function(func: &FuncType) -> RcDoc { @@ -199,6 +204,7 @@ fn pp_function(func: &FuncType) -> RcDoc { } .nest(INDENT_SPACE) } + fn pp_args(args: &[IDLArgType]) -> RcDoc { match args { [ty] => { @@ -245,16 +251,21 @@ fn pp_rets(args: &[IDLType]) -> RcDoc { fn pp_service(serv: &[Binding]) -> RcDoc { let doc = concat( - serv.iter() - .map(|b| escape(&b.id, true).append(" : ").append(pp_ty(&b.typ))), + serv.iter().map(|b| { + pp_doc_comment(b.doc_comment.as_ref()) + .append(escape(&b.id, true)) + .append(" : ") + .append(pp_ty(&b.typ)) + }), ";", ); kwd("actor").append(enclose_space("{", doc, "}")) } -fn pp_defs<'a>(bindings: &[(&'a str, &'a IDLType)]) -> RcDoc<'a> { - lines(bindings.iter().map(|(id, typ)| { - kwd("public type") +fn pp_defs<'a>(bindings: &[(&'a str, &'a IDLType, Option<&'a Vec>)]) -> RcDoc<'a> { + lines(bindings.iter().map(|(id, typ, doc_comment)| { + pp_doc_comment(*doc_comment) + .append(kwd("public type")) .append(escape(id, false)) .append(" = ") .append(pp_ty(typ)) @@ -270,6 +281,20 @@ fn pp_actor(ty: &IDLType) -> RcDoc { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + if let Some(comment_lines) = comment_lines { + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + doc_comment +} + pub fn compile(prog: &IDLMergedProg) -> String { let header = r#"// This is a generated Motoko binding. // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. @@ -279,7 +304,9 @@ pub fn compile(prog: &IDLMergedProg) -> String { None => pp_defs(&bindings), Some(actor) => { let defs = pp_defs(&bindings); - let actor = kwd("public type Self =").append(pp_actor(actor)); + let actor = pp_doc_comment(actor.doc_comment.as_ref()) + .append(kwd("public type Self =")) + .append(pp_actor(&actor.typ)); defs.append(actor) } }; diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index eb12def6e..bf4dfb4ad 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -4,7 +4,7 @@ use crate::{ Deserialize, }; use candid::types::{ - syntax::{Binding, FuncType, IDLArgType, IDLType, PrimType, TypeField}, + syntax::{Binding, FuncType, IDLActorType, IDLArgType, IDLType, PrimType, TypeField}, Label, }; use candid::{pretty::utils::*, types::syntax::IDLMergedProg}; @@ -14,6 +14,8 @@ use serde::Serialize; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; +const DOC_COMMENT_LINE_PREFIX: &str = "/// "; + #[derive(Default, Deserialize, Clone, Debug)] pub struct BindingConfig { name: Option, @@ -101,18 +103,22 @@ fn as_result(fs: &[TypeField]) -> Option<(&IDLType, &IDLType, bool)> { [TypeField { label: ok, typ: t_ok, + doc_comment: _, }, TypeField { label: err, typ: t_err, + doc_comment: _, }] if *ok == Label::Named("Ok".to_string()) && *err == Label::Named("Err".to_string()) => { Some((t_ok, t_err, false)) } [TypeField { label: ok, typ: t_ok, + doc_comment: _, }, TypeField { label: err, typ: t_err, + doc_comment: _, }] if *ok == Label::Named("ok".to_string()) && *err == Label::Named("err".to_string()) => { Some((t_ok, t_err, true)) } @@ -165,6 +171,27 @@ fn pp_vis<'a>(vis: &Option) -> RcDoc<'a> { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + if let Some(comment_lines) = comment_lines { + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + doc_comment +} + +/// (actor methods, init args, actor comment lines) +type PpActorRet = ( + Vec, + Option>, + Option>, +); + impl<'a> State<'a> { fn generate_test(&mut self, src: &IDLType, use_type: &str) { if self.tests.contains_key(use_type) { @@ -255,7 +282,7 @@ fn test_{test_name}() {{ RecordT(ref fs) => self.pp_record_fields(fs, false, is_ref), VariantT(ref fs) => { // only possible for result variant - let (ok, err, is_motoko) = as_result(fs).unwrap(); + let (ok_typ, err_typ, is_motoko) = as_result(fs).unwrap(); // This is a hacky way to redirect Result type let old = self .state @@ -273,10 +300,10 @@ fn test_{test_name}() {{ self.state .pop_state(old, StateElem::TypeStr("std::result::Result")); let old = self.state.push_state(&StateElem::Label("Ok")); - let ok = self.pp_ty(ok, is_ref); + let ok = self.pp_ty(ok_typ, is_ref); self.state.pop_state(old, StateElem::Label("Ok")); let old = self.state.push_state(&StateElem::Label("Err")); - let err = self.pp_ty(err, is_ref); + let err = self.pp_ty(err_typ, is_ref); self.state.pop_state(old, StateElem::Label("Err")); let body = ok.append(", ").append(err); RcDoc::text(result).append(enclose("<", body, ">")) @@ -360,10 +387,11 @@ fn test_{test_name}() {{ ) -> RcDoc<'b> { let lab = field.label.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); - let res = self + let f = self .pp_label(&field.label, false, need_vis) .append(kwd(":")) .append(self.pp_ty(&field.typ, is_ref)); + let res = pp_doc_comment(field.doc_comment.as_ref()).append(f); self.state.pop_state(old, StateElem::Label(&lab)); res } @@ -397,7 +425,7 @@ fn test_{test_name}() {{ fn pp_variant_field<'b>(&mut self, field: &'b TypeField) -> RcDoc<'b> { let lab = field.label.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); - let res = match &field.typ { + let f = match &field.typ { IDLType::PrimT(PrimType::Null) => self.pp_label(&field.label, true, false), IDLType::RecordT(fs) => self .pp_label(&field.label, true, false) @@ -408,6 +436,7 @@ fn test_{test_name}() {{ ")", )), }; + let res = pp_doc_comment(field.doc_comment.as_ref()).append(f); self.state.pop_state(old, StateElem::Label(&lab)); res } @@ -427,7 +456,7 @@ fn test_{test_name}() {{ self.state.pop_state(old, StateElem::Label(id)); continue; } - let ty = self.state.prog.find_type(id).unwrap(); + let binding = self.state.prog.find_binding(id).unwrap(); let name = self .state .config @@ -446,9 +475,9 @@ fn test_{test_name}() {{ .clone() .map(RcDoc::text) .unwrap_or(RcDoc::text("#[derive(CandidType, Deserialize)]")); - let line = match ty { + let line = match &binding.typ { IDLType::RecordT(fs) => { - let separator = if is_tuple(ty) { + let separator = if is_tuple(&binding.typ) { RcDoc::text(";") } else { RcDoc::nil() @@ -467,7 +496,7 @@ fn test_{test_name}() {{ vis.append(kwd("type")) .append(name) .append(" = ") - .append(self.pp_ty(ty, false)) + .append(self.pp_ty(&binding.typ, false)) .append(";") } else { derive @@ -499,19 +528,23 @@ fn test_{test_name}() {{ .append("struct ") .append(name) // TODO: Unfortunately, the visibility of the inner newtype is also controlled by var.visibility - .append(enclose("(", vis.append(self.pp_ty(ty, false)), ")")) + .append(enclose( + "(", + vis.append(self.pp_ty(&binding.typ, false)), + ")", + )) .append(";") } else { vis.append(kwd("type")) .append(name) .append(" = ") - .append(self.pp_ty(ty, false)) + .append(self.pp_ty(&binding.typ, false)) .append(";") } } }; self.state.pop_state(old, StateElem::Label(id)); - res.push(line) + res.push(pp_doc_comment(binding.doc_comment.as_ref()).append(line)); } lines(res.into_iter()) } @@ -581,8 +614,9 @@ fn test_{test_name}() {{ self.state.pop_state(old, lab); res } - fn pp_function(&mut self, id: &str, func: &FuncType) -> Method { + fn pp_function(&mut self, binding: &Binding, func: &FuncType) -> Method { use candid::types::internal::FuncMode; + let id = &binding.id; let old = self.state.push_state(&StateElem::Label(id)); let name = self .state @@ -641,13 +675,14 @@ fn test_{test_name}() {{ .map(|x| x.pretty(LINE_WIDTH).to_string()) .collect(), mode, + doc_comment_lines: binding.doc_comment.clone().unwrap_or_default(), }; self.state.pop_state(old, StateElem::Label(id)); res } - fn pp_actor(&mut self, actor: &IDLType) -> (Vec, Option>) { - let actor = self.state.prog.trace_type(actor).unwrap(); - let init = if let IDLType::ClassT(args, _) = &actor { + fn pp_actor(&mut self, actor: &IDLActorType) -> PpActorRet { + let actor_typ = self.state.prog.trace_type(&actor.typ).unwrap(); + let init = if let IDLType::ClassT(args, _) = &actor_typ { let old = self.state.push_state(&StateElem::Label("init")); let args: Vec<_> = args .iter() @@ -672,13 +707,13 @@ fn test_{test_name}() {{ } else { None }; - let serv = self.state.prog.service_methods(&actor).unwrap(); + let serv = self.state.prog.service_methods(&actor_typ).unwrap(); let mut res = Vec::new(); for binding in serv.iter() { let func = self.state.prog.as_func(&binding.typ).unwrap(); - res.push(self.pp_function(&binding.id, func)); + res.push(self.pp_function(binding, func)); } - (res, init) + (res, init, actor.doc_comment.clone()) } } #[derive(Serialize, Debug)] @@ -686,6 +721,7 @@ pub struct Output { pub type_defs: String, pub methods: Vec, pub init_args: Option>, + pub actor_comment_lines: Vec, pub tests: String, } #[derive(Serialize, Debug)] @@ -695,6 +731,7 @@ pub struct Method { pub args: Vec<(String, String)>, pub rets: Vec, pub mode: String, + pub doc_comment_lines: Vec, } pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec) { let mut state = NominalState { @@ -703,7 +740,7 @@ pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec let env = state.nominalize_all(); let old_stats = state.state.stats.clone(); let def_list = if let Some(actor) = &env.actor { - chase_actor(&env, actor).unwrap() + chase_actor(&env, &actor.typ).unwrap() } else { env.types_ids() }; @@ -715,10 +752,10 @@ pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec }; state.state.stats = old_stats; let defs = state.pp_defs(&def_list); - let (methods, init_args) = if let Some(actor) = &env.actor { + let (methods, init_args, actor_doc_comment) = if let Some(actor) = &env.actor { state.pp_actor(actor) } else { - (Vec::new(), None) + (Vec::new(), None, None) }; let tests = state.tests.into_values().collect::>().join("\n"); let unused = state.state.report_unused(); @@ -727,6 +764,7 @@ pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec type_defs: defs.pretty(LINE_WIDTH).to_string(), methods, init_args, + actor_comment_lines: actor_doc_comment.unwrap_or_default(), tests, }, unused, @@ -741,6 +779,7 @@ pub fn output_handlebar(output: Output, config: ExternalConfig, template: &str) type_defs: String, methods: Vec, init_args: Option>, + actor_comment_lines: Vec, tests: String, } let data = HBOutput { @@ -748,6 +787,7 @@ pub fn output_handlebar(output: Output, config: ExternalConfig, template: &str) methods: output.methods, external: config.0, init_args: output.init_args, + actor_comment_lines: output.actor_comment_lines, tests: output.tests, }; hbs.render_template(template, &data).unwrap() @@ -838,6 +878,7 @@ impl NominalState<'_> { env: &mut IDLMergedProg, path: &mut Vec, t: &IDLType, + parent_doc_comment: Option<&Vec>, ) -> IDLType { let elem = StateElem::Type(t); let old = if matches!(t, IDLType::FuncT(_)) { @@ -849,13 +890,13 @@ impl NominalState<'_> { let res = match t { IDLType::OptT(ty) => { path.push(TypePath::Opt); - let ty = self.nominalize(env, path, ty); + let ty = self.nominalize(env, path, ty, None); path.pop(); IDLType::OptT(Box::new(ty)) } IDLType::VecT(ty) => { path.push(TypePath::Vec); - let ty = self.nominalize(env, path, ty); + let ty = self.nominalize(env, path, ty, None); path.pop(); IDLType::VecT(Box::new(ty)) } @@ -867,17 +908,17 @@ impl NominalState<'_> { { let fs: Vec<_> = fs .iter() - .map(|TypeField { label, typ }| { - let lab = label.to_string(); + .map(|field| { + let lab = field.label.to_string(); let elem = StateElem::Label(&lab); let old = self.state.push_state(&elem); path.push(TypePath::RecordField(lab.clone())); - let ty = self.nominalize(env, path, typ); + let ty = self.nominalize(env, path, &field.typ, None); path.pop(); self.state.pop_state(old, elem); TypeField { - label: label.clone(), typ: ty, + ..field.clone() } }) .collect(); @@ -894,10 +935,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::RecordT(fs.to_vec()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -907,21 +950,25 @@ impl NominalState<'_> { if matches!(path.last(), None | Some(TypePath::Id(_))) || is_result { let fs: Vec<_> = fs .iter() - .map(|TypeField { label, typ }| { - let lab = label.to_string(); + .map(|field| { + let lab = field.label.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); - if is_result { + let doc_comment = if is_result { // so that inner record gets a new name path.push(TypePath::ResultField(lab.clone())); + // Only preserve comments on the field if it's a result, + // because the result cannot have comments on the inner types + field.doc_comment.as_ref() } else { path.push(TypePath::VariantField(lab.clone())); - } - let ty = self.nominalize(env, path, typ); + None + }; + let ty = self.nominalize(env, path, &field.typ, doc_comment); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); TypeField { - label: label.clone(), typ: ty, + ..field.clone() } }) .collect(); @@ -938,10 +985,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::VariantT(fs.to_vec()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -964,7 +1013,7 @@ impl NominalState<'_> { i.to_string() }; path.push(TypePath::Func(format!("arg{idx}"))); - let ty = self.nominalize(env, path, &arg.typ); + let ty = self.nominalize(env, path, &arg.typ, None); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); IDLArgType { @@ -986,7 +1035,7 @@ impl NominalState<'_> { i.to_string() }; path.push(TypePath::Func(format!("ret{idx}"))); - let ty = self.nominalize(env, path, &ty); + let ty = self.nominalize(env, path, &ty, None); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); ty @@ -1006,10 +1055,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::FuncT(func.clone()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -1017,14 +1068,17 @@ impl NominalState<'_> { IDLType::ServT(serv) => match path.last() { None | Some(TypePath::Id(_)) => IDLType::ServT( serv.iter() - .map(|Binding { id, typ }| { - let lab = id.to_string(); + .map(|binding| { + let lab = binding.id.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); path.push(TypePath::Id(lab.clone())); - let ty = self.nominalize(env, path, typ); + let ty = self.nominalize(env, path, &binding.typ, None); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); - Binding { id: lab, typ: ty } + Binding { + typ: ty, + ..binding.clone() + } }) .collect(), ), @@ -1040,10 +1094,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::ServT(serv.clone()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -1054,7 +1110,7 @@ impl NominalState<'_> { let elem = StateElem::Label("init"); let old = self.state.push_state(&elem); path.push(TypePath::Init); - let ty = self.nominalize(env, path, &arg.typ); + let ty = self.nominalize(env, path, &arg.typ, None); path.pop(); self.state.pop_state(old, elem); IDLArgType { @@ -1063,7 +1119,7 @@ impl NominalState<'_> { } }) .collect(), - Box::new(self.nominalize(env, path, ty)), + Box::new(self.nominalize(env, path, ty, None)), ), t => t.clone(), }; @@ -1075,22 +1131,21 @@ impl NominalState<'_> { fn nominalize_all(&mut self) -> IDLMergedProg { let mut res = IDLMergedProg::new(); - for (id, typ) in self.state.prog.get_types() { + for (id, typ, doc_comment) in self.state.prog.get_types() { let elem = StateElem::Label(id); let old = self.state.push_state(&elem); - let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.to_string())], typ); + let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.to_string())], typ, None); res.insert_binding(Binding { id: id.to_string(), typ: ty, + doc_comment: doc_comment.cloned(), }); self.state.pop_state(old, elem); } - let actor = self - .state - .prog - .actor - .as_ref() - .map(|ty| self.nominalize(&mut res, &mut vec![], ty)); + let actor = self.state.prog.actor.as_ref().map(|a| IDLActorType { + typ: self.nominalize(&mut res, &mut vec![], &a.typ, None), + doc_comment: a.doc_comment.clone(), + }); res.set_actor(actor); res } diff --git a/rust/candid_parser/src/bindings/rust_agent.hbs b/rust/candid_parser/src/bindings/rust_agent.hbs index 4e0526254..06782b650 100644 --- a/rust/candid_parser/src/bindings/rust_agent.hbs +++ b/rust/candid_parser/src/bindings/rust_agent.hbs @@ -6,9 +6,15 @@ type Result = std::result::Result; {{type_defs}} {{#if methods}} +{{#each actor_comment_lines}} +/// {{this}} +{{/each}} pub struct {{PascalCase service_name}}<'a>(pub Principal, pub &'a ic_agent::Agent); impl<'a> {{PascalCase service_name}}<'a> { {{#each methods}} + {{#each this.doc_comment_lines}} + /// {{this}} + {{/each}} pub async fn {{this.name}}(&self{{#each this.args}}, {{this.0}}: &{{this.1}}{{/each}}) -> Result<{{vec_to_arity this.rets}}> { let args = Encode!({{#each this.args}}&{{this.0}}{{#unless @last}},{{/unless}}{{/each}})?; let bytes = self.1.{{#if (eq this.mode "update")}}update{{else}}query{{/if}}(&self.0, "{{escape_debug this.original_name}}").with_arg(args).{{#if (eq this.mode "update")}}call_and_wait{{else}}call{{/if}}().await?; @@ -17,7 +23,8 @@ impl<'a> {{PascalCase service_name}}<'a> { {{/each}} } {{#if canister_id}} -pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); // {{canister_id}} +/// Canister ID: `{{canister_id}}` +pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); {{/if}} {{/if}} {{#if tests}} diff --git a/rust/candid_parser/src/bindings/rust_call.hbs b/rust/candid_parser/src/bindings/rust_call.hbs index 788fc0db3..6697c6747 100644 --- a/rust/candid_parser/src/bindings/rust_call.hbs +++ b/rust/candid_parser/src/bindings/rust_call.hbs @@ -6,16 +6,26 @@ use ic_cdk::api::call::CallResult as Result; {{type_defs}} {{#if methods}} +{{#each actor_comment_lines}} +/// {{this}} +{{/each}} pub struct {{PascalCase service_name}}(pub Principal); impl {{PascalCase service_name}} { {{#each methods}} + {{#each this.doc_comment_lines}} + /// {{this}} + {{/each}} pub async fn {{this.name}}(&self{{#each this.args}}, {{this.0}}: &{{this.1}}{{/each}}) -> Result<({{#each this.rets}}{{this}},{{/each}})> { ic_cdk::call(self.0, "{{escape_debug this.original_name}}", ({{#each this.args}}{{this.0}},{{/each}})).await } {{/each}} } {{#if canister_id}} -pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); // {{canister_id}} +/// Canister ID: `{{canister_id}}` +pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); +{{#each actor_comment_lines}} +/// {{this}} +{{/each}} pub const {{snake_case service_name}} : {{PascalCase service_name}} = {{PascalCase service_name}}(CANISTER_ID); {{/if}} {{/if}} diff --git a/rust/candid_parser/src/bindings/rust_stub.hbs b/rust/candid_parser/src/bindings/rust_stub.hbs index a0cf2c4cd..5f23dc1b2 100644 --- a/rust/candid_parser/src/bindings/rust_stub.hbs +++ b/rust/candid_parser/src/bindings/rust_stub.hbs @@ -11,6 +11,9 @@ fn init({{#each init_args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{ } {{/if}} {{#each methods}} +{{#each this.doc_comment_lines}} +/// {{this}} +{{/each}} #[ic_cdk::{{cdk_attribute this.mode this.name this.original_name}}] fn {{this.name}}({{#each this.args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{/each}}) -> {{vec_to_arity this.rets}} { unimplemented!() diff --git a/rust/candid_parser/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs index a8b825345..8e957b7bf 100644 --- a/rust/candid_parser/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -6,6 +6,10 @@ use candid::types::{ }; use pretty::RcDoc; +const DOC_COMMENT_PREFIX: &str = "/**"; +const DOC_COMMENT_LINE_PREFIX: &str = " * "; +const DOC_COMMENT_SUFFIX: &str = " */"; + fn pp_prim_ty(ty: &PrimType) -> RcDoc { use PrimType::*; match ty { @@ -125,7 +129,8 @@ fn pp_label(id: &Label) -> RcDoc { } fn pp_field<'a>(prog: &'a IDLMergedProg, field: &'a TypeField, is_ref: bool) -> RcDoc<'a> { - pp_label(&field.label) + pp_doc_comment(field.doc_comment.as_ref()) + .append(pp_label(&field.label)) .append(kwd(":")) .append(pp_ty(prog, &field.typ, is_ref)) } @@ -151,14 +156,23 @@ fn pp_function<'a>(prog: &'a IDLMergedProg, func: &'a FuncType) -> RcDoc<'a> { fn pp_service<'a>(prog: &'a IDLMergedProg, serv: &'a [Binding]) -> RcDoc<'a> { let doc = concat( - serv.iter().map(|Binding { id, typ }| { - let func = match typ { - IDLType::FuncT(ref func) => pp_function(prog, func), - IDLType::VarT(ref id) => ident(id), - _ => unreachable!(), - }; - quote_ident(id).append(kwd(":")).append(func) - }), + serv.iter().map( + |Binding { + id, + typ, + doc_comment, + }| { + let func = match typ { + IDLType::FuncT(ref func) => pp_function(prog, func), + IDLType::VarT(ref id) => ident(id), + _ => unreachable!(), + }; + pp_doc_comment(doc_comment.as_ref()) + .append(quote_ident(id)) + .append(kwd(":")) + .append(func) + }, + ), ",", ); enclose_space("{", doc, "}") @@ -166,8 +180,9 @@ fn pp_service<'a>(prog: &'a IDLMergedProg, serv: &'a [Binding]) -> RcDoc<'a> { fn pp_defs<'a>(prog: &'a IDLMergedProg, def_list: &'a [&'a str]) -> RcDoc<'a> { lines(def_list.iter().map(|id| { - let ty = prog.find_type(id).unwrap(); - let export = match ty { + let binding = prog.find_binding(id).unwrap(); + let ty = &binding.typ; + let doc = match ty { IDLType::RecordT(_) if !ty.is_tuple() => kwd("export interface") .append(ident(id)) .append(" ") @@ -192,7 +207,7 @@ fn pp_defs<'a>(prog: &'a IDLMergedProg, def_list: &'a [&'a str]) -> RcDoc<'a> { .append(pp_ty(prog, ty, false)) .append(";"), }; - export + pp_doc_comment(binding.doc_comment.as_ref()).append(doc) })) } @@ -207,6 +222,29 @@ fn pp_actor<'a>(prog: &'a IDLMergedProg, ty: &'a IDLType) -> RcDoc<'a> { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + let mut is_empty = true; + if let Some(comment_lines) = comment_lines { + is_empty = comment_lines.is_empty(); + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + if !is_empty { + doc_comment = RcDoc::text(DOC_COMMENT_PREFIX) + .append(RcDoc::hardline()) + .append(doc_comment) + .append(RcDoc::text(DOC_COMMENT_SUFFIX)) + .append(RcDoc::hardline()); + } + doc_comment +} + pub fn compile(prog: &IDLMergedProg) -> String { let header = r#"import type { Principal } from '@dfinity/principal'; import type { ActorMethod } from '@dfinity/agent'; @@ -216,7 +254,8 @@ import type { IDL } from '@dfinity/candid'; let defs = pp_defs(prog, &def_list); let actor = match &prog.actor { None => RcDoc::nil(), - Some(actor) => pp_actor(prog, actor) + Some(actor) => pp_doc_comment(actor.doc_comment.as_ref()) + .append(pp_actor(prog, &actor.typ)) .append(RcDoc::line()) .append("export declare const idlFactory: IDL.InterfaceFactory;") .append(RcDoc::line()) diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index dd22238eb..462efb0c6 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,12 +1,12 @@ use super::test::{Assert, Input, Test}; -use super::token::{Token, error, error2, LexicalError, Span}; +use super::token::{Token, error, error2, LexicalError, Span, TriviaMap}; use candid::{Principal, types::Label}; -use candid::types::syntax::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs, IDLArgType}; +use candid::types::syntax::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs, IDLArgType, IDLActorType}; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use candid::types::{TypeEnv, FuncMode}; use candid::utils::check_unique; -grammar; +grammar(trivia: Option<&TriviaMap>); extern { type Location = usize; @@ -174,7 +174,7 @@ pub Typ: IDLType = { Label::Unnamed(_) => { id = id + 1; Label::Unnamed(id - 1) }, ref l => { id = l.get_id() + 1; l.clone() }, }; - TypeField { label, typ: f.typ.clone() } + TypeField { label, typ: f.typ.clone(), doc_comment: f.doc_comment.clone() } }).collect(); fs.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); check_unique(fs.iter().map(|f| &f.label)).map_err(|e| error2(e, span))?; @@ -202,19 +202,19 @@ PrimTyp: IDLType = { } FieldTyp: TypeField = { - ":" =>? Ok(TypeField { label: Label::Id(n), typ: t }), - ":" => TypeField { label: Label::Named(n), typ: t }, + ":" =>? Ok(TypeField { label: Label::Id(id), typ, doc_comment }), + ":" => TypeField { label: Label::Named(n), typ, doc_comment }, } RecordFieldTyp: TypeField = { FieldTyp => <>, - Typ => TypeField { label: Label::Unnamed(0), typ: <> }, + => TypeField { label: Label::Unnamed(0), typ, doc_comment }, } VariantFieldTyp: TypeField = { FieldTyp => <>, - Name => TypeField { label: Label::Named(<>), typ: IDLType::PrimT(PrimType::Null) }, - FieldId =>? Ok(TypeField { label: Label::Id(<>), typ: IDLType::PrimT(PrimType::Null) }), + => TypeField { label: Label::Named(n), typ: IDLType::PrimT(PrimType::Null), doc_comment }, + =>? Ok(TypeField { label: Label::Id(id), typ: IDLType::PrimT(PrimType::Null), doc_comment }), } ArgTupTyp: Vec = "(" > ")" =>? { @@ -253,13 +253,13 @@ ActorTyp: Vec = { } MethTyp: Binding = { - ":" => Binding { id: n, typ: IDLType::FuncT(f) }, - ":" => Binding { id: n, typ: IDLType::VarT(id) }, + ":" => Binding { id: n, typ: IDLType::FuncT(f), doc_comment }, + ":" => Binding { id: n, typ: IDLType::VarT(id), doc_comment }, } // Type declarations Def: Dec = { - "type" "=" => Dec::TypD(Binding { id: id, typ: t }), + "type" "=" => Dec::TypD(Binding { id: id, typ: t, doc_comment }), "import" => Dec::ImportType(<>), "import" "service" => Dec::ImportServ(<>), } @@ -269,9 +269,9 @@ Actor: IDLType = { "id" => IDLType::VarT(<>), } -MainActor: IDLType = { - "service" "id"? ":" ";"? => <>, - "service" "id"? ":" "->" ";"? => IDLType::ClassT(args, Box::new(t)), +MainActor: IDLActorType = { + "service" "id"? ":" ";"? => IDLActorType { typ: t, doc_comment }, + "service" "id"? ":" "->" ";"? => IDLActorType { typ: IDLType::ClassT(args, Box::new(t)), doc_comment }, } pub IDLProg: IDLProg = { @@ -327,3 +327,9 @@ SepBy: Vec = { #[inline] Sp: (T, Span) = => (t, l..r); + +#[inline] +DocComment: Option> = + => { + trivia.and_then(|t| t.borrow().get(&l).cloned()) + }; diff --git a/rust/candid_parser/src/lib.rs b/rust/candid_parser/src/lib.rs index ba0596dd7..2ece94517 100644 --- a/rust/candid_parser/src/lib.rs +++ b/rust/candid_parser/src/lib.rs @@ -119,6 +119,10 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod error; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + pub use error::{ pretty_parse, pretty_parse_idl_prog, pretty_parse_idl_types, pretty_wrap, Error, Result, }; @@ -143,31 +147,33 @@ pub mod random; pub mod test; pub fn parse_idl_prog(str: &str) -> Result { - let lexer = token::Tokenizer::new(str); - Ok(grammar::IDLProgParser::new().parse(lexer)?) + let trivia: token::TriviaMap = Rc::new(RefCell::new(HashMap::new())); + let lexer = token::Tokenizer::new_with_trivia(str, trivia.clone()); + let res = grammar::IDLProgParser::new().parse(Some(&trivia.clone()), lexer)?; + Ok(res) } pub fn parse_idl_init_args(str: &str) -> Result { let lexer = token::Tokenizer::new(str); - Ok(grammar::IDLInitArgsParser::new().parse(lexer)?) + Ok(grammar::IDLInitArgsParser::new().parse(None, lexer)?) } pub fn parse_idl_type(str: &str) -> Result { let lexer = token::Tokenizer::new(str); - Ok(grammar::TypParser::new().parse(lexer)?) + Ok(grammar::TypParser::new().parse(None, lexer)?) } pub fn parse_idl_types(str: &str) -> Result { let lexer = token::Tokenizer::new(str); - Ok(grammar::TypsParser::new().parse(lexer)?) + Ok(grammar::TypsParser::new().parse(None, lexer)?) } pub fn parse_idl_args(s: &str) -> crate::Result { let lexer = token::Tokenizer::new(s); - Ok(grammar::ArgsParser::new().parse(lexer)?) + Ok(grammar::ArgsParser::new().parse(None, lexer)?) } pub fn parse_idl_value(s: &str) -> crate::Result { let lexer = token::Tokenizer::new(s); - Ok(grammar::ArgParser::new().parse(lexer)?) + Ok(grammar::ArgParser::new().parse(None, lexer)?) } diff --git a/rust/candid_parser/src/random.rs b/rust/candid_parser/src/random.rs index 1c3f9e517..75bc6a12b 100644 --- a/rust/candid_parser/src/random.rs +++ b/rust/candid_parser/src/random.rs @@ -217,7 +217,12 @@ impl RandState<'_> { } IDLType::RecordT(fs) => { let mut res = Vec::new(); - for TypeField { label, typ } in fs.iter() { + for TypeField { + label, + typ, + doc_comment: _, + } in fs.iter() + { let lab_str = label.to_string(); let elem = StateElem::Label(&lab_str); let old_config = self.0.push_state(&elem); @@ -245,7 +250,11 @@ impl RandState<'_> { choices.collect() }; let idx = arbitrary_variant(u, &sizes)?; - let TypeField { label, typ } = &fs[idx]; + let TypeField { + label, + typ, + doc_comment: _, + } = &fs[idx]; let lab_str = label.to_string(); let elem = StateElem::Label(&lab_str); let old_config = self.0.push_state(&elem); diff --git a/rust/candid_parser/src/test.rs b/rust/candid_parser/src/test.rs index 974c4768e..e9becf818 100644 --- a/rust/candid_parser/src/test.rs +++ b/rust/candid_parser/src/test.rs @@ -65,7 +65,7 @@ impl std::str::FromStr for Test { type Err = Error; fn from_str(str: &str) -> std::result::Result { let lexer = super::token::Tokenizer::new(str); - Ok(super::grammar::TestParser::new().parse(lexer)?) + Ok(super::grammar::TestParser::new().parse(None, lexer)?) } } diff --git a/rust/candid_parser/src/token.rs b/rust/candid_parser/src/token.rs index db7ae2d61..5dd81c4c9 100644 --- a/rust/candid_parser/src/token.rs +++ b/rust/candid_parser/src/token.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, collections::HashMap, mem, rc::Rc}; + use lalrpop_util::ParseError; use logos::{Lexer, Logos}; @@ -5,6 +7,8 @@ use logos::{Lexer, Logos}; #[logos(skip r"[ \t\r\n]+")] #[logos(skip r"//[^\n]*")] // line comment pub enum Token { + #[regex(r"///[^\n]*")] + DocComment, #[token("/*")] StartComment, #[token("=")] @@ -118,22 +122,40 @@ fn parse_number(lex: &mut Lexer) -> String { } } +fn parse_doc_comment(lex: &Lexer) -> String { + lex.slice().trim_start_matches("///").trim().to_string() +} + +pub type TriviaMap = Rc>>>; + pub struct Tokenizer<'input> { lex: Lexer<'input, Token>, + comment_buffer: Vec, + trivia: Option, } + impl<'input> Tokenizer<'input> { pub fn new(input: &'input str) -> Self { let lex = Token::lexer(input); - Tokenizer { lex } + Tokenizer { + lex, + comment_buffer: vec![], + trivia: None, + } + } + + pub fn new_with_trivia(input: &'input str, trivia: TriviaMap) -> Self { + let lex = Token::lexer(input); + Tokenizer { + lex, + comment_buffer: vec![], + trivia: Some(trivia), + } } } pub type Span = std::ops::Range; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Spanned { - pub span: Span, - pub value: T, -} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct LexicalError { pub err: String, @@ -179,6 +201,13 @@ impl Iterator for Tokenizer<'_> { let err = format!("Unknown token {}", self.lex.slice()); Some(Err(LexicalError::new(err, span))) } + Ok(Token::DocComment) => { + let content = parse_doc_comment(&self.lex); + if self.trivia.is_some() { + self.comment_buffer.push(content.to_string()); + } + self.next() + } Ok(Token::StartComment) => { let mut lex = self.lex.to_owned().morph::(); let mut nesting = 1; @@ -278,7 +307,15 @@ impl Iterator for Tokenizer<'_> { self.lex = lex.morph::(); Some(Ok((span.start, Token::Text(result), self.lex.span().end))) } - Ok(token) => Some(Ok((span.start, token, span.end))), + Ok(token) => { + if let Some(trivia) = &mut self.trivia { + if !self.comment_buffer.is_empty() { + let content: Vec = mem::take(&mut self.comment_buffer); + trivia.borrow_mut().insert(span.start, content); + } + } + Some(Ok((span.start, token, span.end))) + } } } } diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index 7f9f609c6..afabe55a2 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -1,26 +1,52 @@ use crate::{parse_idl_prog, pretty_parse_idl_prog, Error, Result}; use candid::types::{ syntax::{ - Binding, Dec, IDLArgType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, PrimType, TypeField, + Binding, Dec, IDLActorType, IDLArgType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, + PrimType, TypeField, }, ArgType, Field, Function, Type, TypeEnv, TypeInner, }; use candid::utils::check_unique; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; pub struct Env<'a> { pub te: &'a mut TypeEnv, + doc_comments_in_actor_types: HashMap>, + actor_doc_comment: Option>, + can_insert_doc_comments: bool, pub pre: bool, } +impl<'a> Env<'a> { + fn new_with_te(te: &'a mut TypeEnv) -> Self { + Self { + te, + doc_comments_in_actor_types: HashMap::new(), + actor_doc_comment: None, + can_insert_doc_comments: false, + pre: false, + } + } + + /// Insert doc comments if [Env::can_insert_doc_comments] is true and there is a doc comment. + fn insert_comments_if_allowed(&mut self, id: &str, doc_comment: Option<&Vec>) { + if !self.can_insert_doc_comments { + return; + } + + if let Some(doc_comment) = doc_comment { + self.doc_comments_in_actor_types + .insert(id.to_string(), doc_comment.to_vec()); + } + } +} + /// Convert candid AST to internal Type pub fn ast_to_type(env: &TypeEnv, ast: &IDLType) -> Result { - let env = Env { - te: &mut env.clone(), - pre: false, - }; - check_type(&env, ast) + let mut te = env.clone(); + let mut env = Env::new_with_te(&mut te); + check_type(&mut env, ast) } fn check_prim(prim: &PrimType) -> Type { @@ -46,7 +72,7 @@ fn check_prim(prim: &PrimType) -> Type { .into() } -pub fn check_type(env: &Env, t: &IDLType) -> Result { +pub fn check_type(env: &mut Env, t: &IDLType) -> Result { match t { IDLType::PrimT(prim) => Ok(check_prim(prim)), IDLType::VarT(id) => { @@ -103,14 +129,14 @@ pub fn check_type(env: &Env, t: &IDLType) -> Result { } } -fn check_arg(env: &Env, arg: &IDLArgType) -> Result { +fn check_arg(env: &mut Env, arg: &IDLArgType) -> Result { Ok(ArgType { name: arg.name.clone(), typ: check_type(env, &arg.typ)?, }) } -fn check_fields(env: &Env, fs: &[TypeField]) -> Result> { +fn check_fields(env: &mut Env, fs: &[TypeField]) -> Result> { // field label duplication is checked in the parser let mut res = Vec::new(); for f in fs.iter() { @@ -120,11 +146,12 @@ fn check_fields(env: &Env, fs: &[TypeField]) -> Result> { ty, }; res.push(field); + env.insert_comments_if_allowed(&f.label.to_string(), f.doc_comment.as_ref()); } Ok(res) } -fn check_meths(env: &Env, ms: &[Binding]) -> Result> { +fn check_meths(env: &mut Env, ms: &[Binding]) -> Result> { // binding duplication is checked in the parser let mut res = Vec::new(); for meth in ms.iter() { @@ -136,6 +163,7 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { ))); } res.push((meth.id.to_owned(), t)); + env.insert_comments_if_allowed(&meth.id, meth.doc_comment.as_ref()); } Ok(res) } @@ -143,7 +171,11 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { fn check_defs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { match dec { - Dec::TypD(Binding { id, typ }) => { + Dec::TypD(Binding { + id, + typ, + doc_comment: _, + }) => { let t = check_type(env, typ)?; env.te.0.insert(id.to_string(), t); } @@ -155,7 +187,12 @@ fn check_defs(env: &mut Env, decs: &[Dec]) -> Result<()> { fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { - if let Dec::TypD(Binding { id, typ: _ }) = dec { + if let Dec::TypD(Binding { + id, + typ: _, + doc_comment: _, + }) = dec + { let duplicate = env.te.0.insert(id.to_string(), TypeInner::Unknown.into()); if duplicate.is_some() { return Err(Error::msg(format!("duplicate binding for {id}"))); @@ -170,8 +207,11 @@ fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { Ok(()) } -fn check_actor(env: &Env, actor: &Option) -> Result> { - match actor { +fn check_actor(env: &mut Env, actor: &Option) -> Result> { + env.can_insert_doc_comments = true; + env.actor_doc_comment = actor.as_ref().and_then(|a| a.doc_comment.clone()); + + let res = match actor.as_ref().map(|a| &a.typ) { None => Ok(None), Some(IDLType::ClassT(ts, t)) => { let mut args = Vec::new(); @@ -187,7 +227,11 @@ fn check_actor(env: &Env, actor: &Option) -> Result> { env.te.as_service(&t)?; Ok(Some(t)) } - } + }; + + env.can_insert_doc_comments = false; + + res } fn resolve_path(base: &Path, file: &str) -> PathBuf { @@ -235,9 +279,9 @@ fn load_imports( /// Type check IDLProg and adds bindings to type environment. Returns /// the main actor if present. This function ignores the imports. pub fn check_prog(te: &mut TypeEnv, prog: &IDLProg) -> Result> { - let mut env = Env { te, pre: false }; + let mut env = Env::new_with_te(te); check_decs(&mut env, &prog.decs)?; - check_actor(&env, &prog.actor) + check_actor(&mut env, &prog.actor) } /// Type check init args extracted from canister metadata candid:args. /// Need to provide `main_env`, because init args may refer to variables from the main did file. @@ -246,12 +290,12 @@ pub fn check_init_args( main_env: &TypeEnv, prog: &IDLInitArgs, ) -> Result> { - let mut env = Env { te, pre: false }; + let mut env = Env::new_with_te(te); check_decs(&mut env, &prog.decs)?; env.te.merge(main_env)?; let mut args = Vec::new(); for arg in prog.args.iter() { - args.push(check_arg(&env, arg)?); + args.push(check_arg(&mut env, arg)?); } Ok(args) } @@ -325,10 +369,7 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I .collect(); let mut te = TypeEnv::new(); - let mut env = Env { - te: &mut te, - pre: false, - }; + let mut env = Env::new_with_te(&mut te); let mut idl_merged_prog = IDLMergedProg::new(); let mut actor: Option = None; @@ -338,7 +379,7 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I check_decs(&mut env, &code.decs)?; idl_merged_prog.add_decs(&code.decs); if *include_serv { - let t = check_actor(&env, &code.actor)?; + let t = check_actor(&mut env, &code.actor)?; actor = merge_actor(&env, &actor, &t, name)?; } } @@ -346,12 +387,16 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I check_decs(&mut env, &prog.decs)?; idl_merged_prog.add_decs(&prog.decs); - let mut res = check_actor(&env, &prog.actor)?; + let mut res = check_actor(&mut env, &prog.actor)?; if actor.is_some() { res = merge_actor(&env, &res, &actor, "")?; } - idl_merged_prog.set_actor(res.clone().map(|t| env.te.as_idl_type(&t))); + idl_merged_prog.set_actor(res.clone().map(|t| IDLActorType { + typ: env.te.as_idl_type(&t), + doc_comment: env.actor_doc_comment.clone(), + })); + idl_merged_prog.set_comments_in_actor(&env.doc_comments_in_actor_types); Ok((te, res, idl_merged_prog)) } diff --git a/rust/candid_parser/src/utils.rs b/rust/candid_parser/src/utils.rs index 89c870437..9873c831c 100644 --- a/rust/candid_parser/src/utils.rs +++ b/rust/candid_parser/src/utils.rs @@ -1,7 +1,7 @@ use crate::{check_prog, pretty_check_file, pretty_parse_idl_prog, Error, Result}; use candid::{ types::{ - syntax::{Binding, IDLMergedProg, IDLType}, + syntax::{Binding, IDLActorType, IDLMergedProg, IDLType}, Type, TypeInner, }, TypeEnv, @@ -68,25 +68,28 @@ pub fn instantiate_candid(candid: CandidSource) -> Result<(Vec, (TypeEnv, _ => unreachable!(), }) } -pub fn get_metadata(env: &IDLMergedProg) -> Option { - let serv = env.actor.as_ref()?; - let serv = env.trace_type(serv).ok()?; +pub fn get_metadata(prog: &IDLMergedProg) -> Option { + let serv = prog.actor.as_ref().map(|a| &a.typ)?; + let serv = prog.trace_type(serv).ok()?; let serv = match &serv { IDLType::ClassT(_, ty) => ty.as_ref(), IDLType::ServT(_) => &serv, _ => unreachable!(), }; - let def_list = crate::bindings::analysis::chase_actor(env, serv).ok()?; - let mut filtered = IDLMergedProg::new(); + let def_list = crate::bindings::analysis::chase_actor(prog, serv).ok()?; + let mut filtered = IDLMergedProg::from(IDLActorType { + typ: serv.clone(), + doc_comment: prog.actor.as_ref().and_then(|a| a.doc_comment.clone()), + }); for d in def_list { - if let Ok(typ) = env.find_type(d) { + if let Ok(typ) = prog.find_type(d) { filtered.insert_binding(Binding { id: d.to_string(), typ: typ.clone(), + doc_comment: None, }); } } - filtered.set_actor(Some(serv.clone())); Some(candid::pretty::candid::compile(&filtered)) } diff --git a/rust/candid_parser/tests/assets/example.did b/rust/candid_parser/tests/assets/example.did index a5a47fee6..6b7c8da28 100644 --- a/rust/candid_parser/tests/assets/example.did +++ b/rust/candid_parser/tests/assets/example.did @@ -1,22 +1,85 @@ import service "recursion.did"; import "import/a.did"; import service "import/b/b.did"; +/// Doc comment for prim type type my_type = principal; -type List = opt record { head: int; tail: List }; +/// Doc comment for List +type List = opt record { + /// Doc comment for List head + head: int; + /// Doc comment for List tail + tail: List +}; type f = func (List, func (int32) -> (int64)) -> (opt List, res); +/// Doc comment for broker service type broker = service { find : (name: text) -> (service {up:() -> (); current:() -> (nat32)}); }; -type nested = record { nat; nat; record {nat;int;}; record { nat; 0x2a:nat; nat8; }; 42:nat; 40:nat; variant{ A; 0x2a; B; C }; }; -type res = variant { Ok: record{int;nat}; Err: record{ error: text } }; -type nested_res = variant { Ok: variant { Ok; Err }; Err: variant { Ok: record { content: text }; Err: record {int} } }; +/// Doc comment for nested type +type nested = record { + nat; + nat; + /// Doc comment for nested record + record {nat;int;}; + record { nat; 0x2a:nat; nat8; }; + 42:nat; + 40:nat; + variant{ A; 0x2a; B; C }; +}; +type res = variant { + /// Doc comment for Ok variant + Ok: record{int;nat}; + /// Doc comment for Err variant + Err: record{ + /// Doc comment for error field in Err variant, + /// on multiple lines + error: text + } +}; +type nested_res = variant { Ok: variant { Ok; Err }; Err: variant { + /// Doc comment for Ok in nested variant + Ok: record { content: text }; + /// Doc comment for Err in nested variant + Err: record { int } +} }; +/// Doc comment for nested_records +type nested_records = record { + /// Doc comment for nested_records field nested + nested: opt record { + /// Doc comment for nested_records field nested_field + nested_field: text + } +}; +type my_variant = variant { + /// Doc comment for my_variant field a + a: record { + /// Doc comment for my_variant field a field b + b: text; + }; + /// Doc comment for my_variant field c + c: opt record { + /// Doc comment for my_variant field c field d + d: text; + } +} +/// Doc comment for service service server : { + /// Doc comment for f1 method of service f1 : (list, test: blob, opt bool) -> () oneway; g1 : (my_type, List, opt List, nested) -> (int, broker, nested_res) query; - h : (vec opt text, variant { A: nat; B: opt text }, opt List) -> (record { id: nat; 0x2a: record {} }); + h : (vec opt text, variant { A: nat; B: opt text }, opt List) -> ( + record { + /// Doc comment for id field in h method return + id: nat; + /// Doc comment for 0x2a field in h method return + 0x2a: record {}; + } + ); + /// Doc comment for i method of service i : f; x : (a,b) -> (opt a, opt b, variant { Ok: record { result: text }; Err: variant {a;b} }) composite_query; + y : (nested_records) -> (record { nested_records; my_variant }) query; } diff --git a/rust/candid_parser/tests/assets/import/a.did b/rust/candid_parser/tests/assets/import/a.did index ebdcddc77..defa071e4 100644 --- a/rust/candid_parser/tests/assets/import/a.did +++ b/rust/candid_parser/tests/assets/import/a.did @@ -1,5 +1,9 @@ import "b/b.did"; +/// Doc comment for a in imported file type a = variant {a;b:b}; +/// Doc comment for service in imported file, +/// ignored if the service in the importing file has a doc comment service : (a,b) -> { + /// Doc comment for f in imported service f : (b) -> (a); } diff --git a/rust/candid_parser/tests/assets/import/b/b.did b/rust/candid_parser/tests/assets/import/b/b.did index b28cb7773..3d6309f80 100644 --- a/rust/candid_parser/tests/assets/import/b/b.did +++ b/rust/candid_parser/tests/assets/import/b/b.did @@ -1,4 +1,8 @@ +/// Doc comment for b in imported file type b = record { int;nat }; +/// Doc comment for service in imported file, +/// ignored if the service in the importing file has a doc comment service : { + /// Doc comment for bbbbb method in imported service bbbbb : (b) -> (); } diff --git a/rust/candid_parser/tests/assets/ok/actor.rs b/rust/candid_parser/tests/assets/ok/actor.rs index fd5f40acc..616b3680a 100644 --- a/rust/candid_parser/tests/assets/ok/actor.rs +++ b/rust/candid_parser/tests/assets/ok/actor.rs @@ -28,6 +28,7 @@ impl Service { ic_cdk::call(self.0, "o", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/cyclic.rs b/rust/candid_parser/tests/assets/ok/cyclic.rs index 33eaa02fe..cb12343cc 100644 --- a/rust/candid_parser/tests/assets/ok/cyclic.rs +++ b/rust/candid_parser/tests/assets/ok/cyclic.rs @@ -18,6 +18,7 @@ impl Service { ic_cdk::call(self.0, "f", (arg0,arg1,arg2,arg3,arg4,arg5,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/empty.rs b/rust/candid_parser/tests/assets/ok/empty.rs index 4477e9f99..899477cc8 100644 --- a/rust/candid_parser/tests/assets/ok/empty.rs +++ b/rust/candid_parser/tests/assets/ok/empty.rs @@ -27,6 +27,7 @@ impl Service { ic_cdk::call(self.0, "h", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/escape.rs b/rust/candid_parser/tests/assets/ok/escape.rs index 0fde3e5d9..96feb0727 100644 --- a/rust/candid_parser/tests/assets/ok/escape.rs +++ b/rust/candid_parser/tests/assets/ok/escape.rs @@ -22,6 +22,7 @@ impl Service { ic_cdk::call(self.0, "\n\'\"\'\'\"\"\r\t", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/example.d.ts b/rust/candid_parser/tests/assets/ok/example.d.ts index 89620882c..42c3c3720 100644 --- a/rust/candid_parser/tests/assets/ok/example.d.ts +++ b/rust/candid_parser/tests/assets/ok/example.d.ts @@ -13,16 +13,48 @@ export type tree = { export interface s { 'f' : t, 'g' : ActorMethod<[list], [B, tree, stream]> } export type t = ActorMethod<[Principal], undefined>; export type stream = [] | [{ 'head' : bigint, 'next' : [Principal, string] }]; +/** + * Doc comment for b in imported file + */ export type b = [bigint, bigint]; +/** + * Doc comment for a in imported file + */ export type a = { 'a' : null } | { 'b' : b }; +/** + * Doc comment for prim type + */ export type my_type = Principal; -export type List = [] | [{ 'head' : bigint, 'tail' : List }]; +/** + * Doc comment for List + */ +export type List = [] | [ + { + /** + * Doc comment for List head + */ + 'head' : bigint, + /** + * Doc comment for List tail + */ + 'tail' : List, + } +]; export type f = ActorMethod<[List, [Principal, string]], [[] | [List], res]>; +/** + * Doc comment for broker service + */ export interface broker { 'find' : ActorMethod<[string], Principal> } +/** + * Doc comment for nested type + */ export interface nested { _0_ : bigint, _1_ : bigint, + /** + * Doc comment for nested record + */ _2_ : [bigint, bigint], _3_ : { _0_ : bigint, _42_ : bigint, _43_ : number }, _40_ : bigint, @@ -32,13 +64,91 @@ export interface nested { { 'C' : null }, _42_ : bigint, } -export type res = { 'Ok' : [bigint, bigint] } | - { 'Err' : { 'error' : string } }; +export type res = { + /** + * Doc comment for Ok variant + */ + 'Ok' : [bigint, bigint] + } | + { + /** + * Doc comment for Err variant + */ + 'Err' : { + /** + * Doc comment for error field in Err variant, + * on multiple lines + */ + 'error' : string, + } + }; export type nested_res = { 'Ok' : { 'Ok' : null } | { 'Err' : null } } | - { 'Err' : { 'Ok' : { 'content' : string } } | { 'Err' : [bigint] } }; + { + 'Err' : { + /** + * Doc comment for Ok in nested variant + */ + 'Ok' : { 'content' : string } + } | + { + /** + * Doc comment for Err in nested variant + */ + 'Err' : [bigint] + } + }; +/** + * Doc comment for nested_records + */ +export interface nested_records { + /** + * Doc comment for nested_records field nested + */ + 'nested' : [] | [ + { + /** + * Doc comment for nested_records field nested_field + */ + 'nested_field' : string, + } + ], +} +export type my_variant = { + /** + * Doc comment for my_variant field a + */ + 'a' : { + /** + * Doc comment for my_variant field a field b + */ + 'b' : string, + } + } | + { + /** + * Doc comment for my_variant field c + */ + 'c' : [] | [ + { + /** + * Doc comment for my_variant field c field d + */ + 'd' : string, + } + ] + }; +/** + * Doc comment for service + */ export interface _SERVICE { + /** + * Doc comment for bbbbb method in imported service + */ 'bbbbb' : ActorMethod<[b], undefined>, 'f' : t, + /** + * Doc comment for f1 method of service + */ 'f1' : ActorMethod<[list, Uint8Array | number[], [] | [boolean]], undefined>, 'g' : ActorMethod<[list], [B, tree, stream]>, 'g1' : ActorMethod< @@ -52,8 +162,20 @@ export interface _SERVICE { { 'B' : [] | [string] }, [] | [List], ], - { _42_ : {}, 'id' : bigint } + { + /** + * Doc comment for 0x2a field in h method return + */ + _42_ : {}, + /** + * Doc comment for id field in h method return + */ + 'id' : bigint, + } >, + /** + * Doc comment for i method of service + */ 'i' : f, 'x' : ActorMethod< [a, b], @@ -64,6 +186,7 @@ export interface _SERVICE { { 'Err' : { 'a' : null } | { 'b' : null } }, ] >, + 'y' : ActorMethod<[nested_records], [nested_records, my_variant]>, } export declare const idlFactory: IDL.InterfaceFactory; export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; diff --git a/rust/candid_parser/tests/assets/ok/example.did b/rust/candid_parser/tests/assets/ok/example.did index 0767aee59..adaa2614f 100644 --- a/rust/candid_parser/tests/assets/ok/example.did +++ b/rust/candid_parser/tests/assets/ok/example.did @@ -9,41 +9,97 @@ type tree = variant { type s = service { f : t; g : (list) -> (B, tree, stream) }; type t = func (server : s) -> (); type stream = opt record { head : nat; next : func () -> (stream) query }; +/// Doc comment for b in imported file type b = record { int; nat }; +/// Doc comment for a in imported file type a = variant { a; b : b }; +/// Doc comment for prim type type my_type = principal; -type List = opt record { head : int; tail : List }; +/// Doc comment for List +type List = opt record { + /// Doc comment for List head + head : int; + /// Doc comment for List tail + tail : List; +}; type f = func (List, func (int32) -> (int64)) -> (opt List, res); +/// Doc comment for broker service type broker = service { find : (name : text) -> (service { current : () -> (nat32); up : () -> () }); }; +/// Doc comment for nested type type nested = record { 0 : nat; 1 : nat; + /// Doc comment for nested record 2 : record { nat; int }; 3 : record { 0 : nat; 42 : nat; 43 : nat8 }; 40 : nat; 41 : variant { 42; A; B; C }; 42 : nat; }; -type res = variant { Ok : record { int; nat }; Err : record { error : text } }; +type res = variant { + /// Doc comment for Ok variant + Ok : record { int; nat }; + /// Doc comment for Err variant + Err : record { + /// Doc comment for error field in Err variant, + /// on multiple lines + error : text; + }; +}; type nested_res = variant { Ok : variant { Ok; Err }; - Err : variant { Ok : record { content : text }; Err : record { int } }; + Err : variant { + /// Doc comment for Ok in nested variant + Ok : record { content : text }; + /// Doc comment for Err in nested variant + Err : record { int }; + }; +}; +/// Doc comment for nested_records +type nested_records = record { + /// Doc comment for nested_records field nested + nested : opt record { + /// Doc comment for nested_records field nested_field + nested_field : text; + }; +}; +type my_variant = variant { + /// Doc comment for my_variant field a + a : record { + /// Doc comment for my_variant field a field b + b : text; + }; + /// Doc comment for my_variant field c + c : opt record { + /// Doc comment for my_variant field c field d + d : text; + }; }; +/// Doc comment for service service : { + /// Doc comment for bbbbb method in imported service bbbbb : (b) -> (); f : t; + /// Doc comment for f1 method of service f1 : (list, test : blob, opt bool) -> () oneway; g : (list) -> (B, tree, stream); g1 : (my_type, List, opt List, nested) -> (int, broker, nested_res) query; h : (vec opt text, variant { A : nat; B : opt text }, opt List) -> ( - record { 42 : record {}; id : nat }, + record { + /// Doc comment for 0x2a field in h method return + 42 : record {}; + /// Doc comment for id field in h method return + id : nat; + }, ); + /// Doc comment for i method of service i : f; x : (a, b) -> ( opt a, opt b, variant { Ok : record { result : text }; Err : variant { a; b } }, ) composite_query; + y : (nested_records) -> (record { nested_records; my_variant }) query; } diff --git a/rust/candid_parser/tests/assets/ok/example.js b/rust/candid_parser/tests/assets/ok/example.js index 4602f2a1b..b289ebe3f 100644 --- a/rust/candid_parser/tests/assets/ok/example.js +++ b/rust/candid_parser/tests/assets/ok/example.js @@ -74,6 +74,13 @@ export const idlFactory = ({ IDL }) => { [], ); const a = IDL.Variant({ 'a' : IDL.Null, 'b' : b }); + const nested_records = IDL.Record({ + 'nested' : IDL.Opt(IDL.Record({ 'nested_field' : IDL.Text })), + }); + const my_variant = IDL.Variant({ + 'a' : IDL.Record({ 'b' : IDL.Text }), + 'c' : IDL.Opt(IDL.Record({ 'd' : IDL.Text })), + }); return IDL.Service({ 'bbbbb' : IDL.Func([b], [], []), 'f' : t, @@ -110,6 +117,11 @@ export const idlFactory = ({ IDL }) => { ], ['composite_query'], ), + 'y' : IDL.Func( + [nested_records], + [IDL.Tuple(nested_records, my_variant)], + ['query'], + ), }); }; export const init = ({ IDL }) => { return []; }; diff --git a/rust/candid_parser/tests/assets/ok/example.mo b/rust/candid_parser/tests/assets/ok/example.mo index 3a909f1ca..eeae24bfc 100644 --- a/rust/candid_parser/tests/assets/ok/example.mo +++ b/rust/candid_parser/tests/assets/ok/example.mo @@ -13,37 +13,86 @@ module { public type s = actor { f : t; g : shared list -> async (B, tree, stream) }; public type t = shared (server : s) -> async (); public type stream = ?{ head : Nat; next : shared query () -> async stream }; + /// Doc comment for b in imported file public type b = (Int, Nat); + /// Doc comment for a in imported file public type a = { #a; #b : b }; + /// Doc comment for prim type public type my_type = Principal; - public type List = ?{ head : Int; tail : List }; + /// Doc comment for List + public type List = ?{ + /// Doc comment for List head + head : Int; + /// Doc comment for List tail + tail : List; + }; public type f = shared (List, shared Int32 -> async Int64) -> async ( ?List, res, ); + /// Doc comment for broker service public type broker = actor { find : shared (name : Text) -> async actor { current : shared () -> async Nat32; up : shared () -> async (); }; }; + /// Doc comment for nested type public type nested = { _0_ : Nat; _1_ : Nat; + /// Doc comment for nested record _2_ : (Nat, Int); _3_ : { _0_ : Nat; _42_ : Nat; _43_ : Nat8 }; _40_ : Nat; _41_ : { #_42_ ; #A; #B; #C }; _42_ : Nat; }; - public type res = { #Ok : (Int, Nat); #Err : { error : Text } }; + public type res = { + /// Doc comment for Ok variant + #Ok : (Int, Nat); + /// Doc comment for Err variant + #Err : { + /// Doc comment for error field in Err variant, + /// on multiple lines + error : Text; + }; + }; public type nested_res = { #Ok : { #Ok; #Err }; - #Err : { #Ok : { content : Text }; #Err : { _0_ : Int } }; + #Err : { + /// Doc comment for Ok in nested variant + #Ok : { content : Text }; + /// Doc comment for Err in nested variant + #Err : { _0_ : Int }; + }; + }; + /// Doc comment for nested_records + public type nested_records = { + /// Doc comment for nested_records field nested + nested : ?{ + /// Doc comment for nested_records field nested_field + nested_field : Text; + }; + }; + public type my_variant = { + /// Doc comment for my_variant field a + #a : { + /// Doc comment for my_variant field a field b + b : Text; + }; + /// Doc comment for my_variant field c + #c : ?{ + /// Doc comment for my_variant field c field d + d : Text; + }; }; + /// Doc comment for service public type Self = actor { + /// Doc comment for bbbbb method in imported service bbbbb : shared b -> async (); f : t; + /// Doc comment for f1 method of service f1 : shared (list, test : Blob, ?Bool) -> (); g : shared list -> async (B, tree, stream); g1 : shared query (my_type, List, ?List, nested) -> async ( @@ -52,14 +101,18 @@ module { nested_res, ); h : shared ([?Text], { #A : Nat; #B : ?Text }, ?List) -> async { + /// Doc comment for 0x2a field in h method return _42_ : {}; + /// Doc comment for id field in h method return id : Nat; }; + /// Doc comment for i method of service i : f; x : shared composite query (a, b) -> async ( ?a, ?b, { #Ok : { result : Text }; #Err : { #a; #b } }, ); + y : shared query nested_records -> async ((nested_records, my_variant)); } } diff --git a/rust/candid_parser/tests/assets/ok/example.rs b/rust/candid_parser/tests/assets/ok/example.rs index 14ec4611d..6e745065b 100644 --- a/rust/candid_parser/tests/assets/ok/example.rs +++ b/rust/candid_parser/tests/assets/ok/example.rs @@ -4,6 +4,7 @@ use candid::{self, CandidType, Deserialize, Principal}; use ic_cdk::api::call::CallResult as Result; +/// Doc comment for b in imported file #[derive(CandidType, Deserialize, Debug)] pub(crate) struct B (pub(crate) candid::Int,pub(crate) u128,); #[derive(CandidType, Deserialize, Debug)] @@ -33,15 +34,19 @@ candid::define_service!(pub(crate) S : { "g" : candid::func!((List) -> (B, Tree, Stream)); }); candid::define_function!(pub(crate) T : (S) -> ()); +/// Doc comment for prim type type CanisterId = Principal; #derive[CandidType, Deserialize, Clone] pub(crate) struct ListInner { + /// Doc comment for List head #[serde(skip_deserializing)] #[serde(rename="head")] HEAD: candid::Int, + /// Doc comment for List tail #[serde(skip_deserializing)] tail: Arc, } +/// Doc comment for List #[derive(CandidType, Deserialize, Debug)] pub(crate) struct MyList(pub(crate) Option); #[derive(CandidType, Deserialize, Debug)] @@ -59,10 +64,12 @@ pub(crate) enum Nested41 { B, C, } +/// Doc comment for nested type #[derive(CandidType, Deserialize, Debug)] pub(crate) struct Nested { pub(crate) _0_: u128, pub(crate) _1_: u128, + /// Doc comment for nested record pub(crate) _2_: (u128,candid::Int,), pub(crate) _3_: Nested3, pub(crate) _40_: u128, @@ -73,9 +80,11 @@ candid::define_service!(pub BrokerReturn : { "current" : candid::func!(() -> (u32)); "up" : candid::func!(() -> ()); }); +/// Doc comment for broker service candid::define_service!(pub(crate) Broker : { "find" : candid::func!((String) -> (BrokerReturn)); }); +/// Doc comment for Ok in nested variant #[derive(CandidType, Deserialize, Debug)] pub(crate) struct NestedResErrOk { pub(crate) content: String } pub(crate) type NestedRes = std::result::Result< @@ -86,30 +95,72 @@ pub(crate) enum HArg1 { A(u128), B(Option) } #[derive(CandidType, Deserialize, Debug)] pub(crate) struct HRet42 {} #[derive(CandidType, Deserialize, Debug)] -pub(crate) struct HRet { pub(crate) _42_: HRet42, pub(crate) id: u128 } +pub(crate) struct HRet { + /// Doc comment for 0x2a field in h method return + pub(crate) _42_: HRet42, + /// Doc comment for id field in h method return + pub(crate) id: u128, +} candid::define_function!(pub(crate) FArg1 : (i32) -> (i64)); +/// Doc comment for Err variant #[derive(CandidType, Deserialize, Debug)] -pub(crate) struct ResErr { pub(crate) error: String } +pub(crate) struct ResErr { + /// Doc comment for error field in Err variant, + /// on multiple lines + pub(crate) error: String, +} pub(crate) type Res = std::result::Result<(candid::Int,u128,), ResErr>; candid::define_function!(pub(crate) F : (MyList, FArg1) -> ( Option, Res, )); +/// Doc comment for a in imported file #[derive(CandidType, Deserialize, Debug)] pub(crate) enum A { #[serde(rename="a")] A, #[serde(rename="b")] B(B) } #[derive(CandidType, Deserialize, Debug)] pub(crate) struct XRet2Ok { pub(crate) result: String } #[derive(CandidType, Deserialize, Debug)] pub(crate) enum Error { #[serde(rename="a")] A, #[serde(rename="b")] B } +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct NestedRecordsNestedInner { + /// Doc comment for nested_records field nested_field + pub(crate) nested_field: String, +} +/// Doc comment for nested_records +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct NestedRecords { + /// Doc comment for nested_records field nested + pub(crate) nested: Option, +} +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct MyVariantCInner { + /// Doc comment for my_variant field c field d + pub(crate) d: String, +} +#[derive(CandidType, Deserialize, Debug)] +pub(crate) enum MyVariant { + /// Doc comment for my_variant field a + #[serde(rename="a")] + A{ + /// Doc comment for my_variant field a field b + b: String, + }, + /// Doc comment for my_variant field c + #[serde(rename="c")] + C(Option), +} +/// Doc comment for service pub struct Service(pub Principal); impl Service { + /// Doc comment for bbbbb method in imported service pub async fn bbbbb(&self, arg0: &B) -> Result<()> { ic_cdk::call(self.0, "bbbbb", (arg0,)).await } pub async fn f(&self, server: &S) -> Result<()> { ic_cdk::call(self.0, "f", (server,)).await } + /// Doc comment for f1 method of service pub async fn f_1(&self, arg0: &List, test: &serde_bytes::ByteBuf, arg2: &Option) -> Result<()> { ic_cdk::call(self.0, "f1", (arg0,test,arg2,)).await } @@ -122,19 +173,31 @@ impl Service { pub async fn h(&self, arg0: &Vec>, arg1: &HArg1, arg2: &Option) -> Result<(HRet,)> { ic_cdk::call(self.0, "h", (arg0,arg1,arg2,)).await } + /// Doc comment for i method of service pub async fn i(&self, arg0: &MyList, arg1: &FArg1) -> Result<(Option,Res,)> { ic_cdk::call(self.0, "i", (arg0,arg1,)).await } pub async fn x(&self, arg0: &A, arg1: &B) -> Result<(Option,Option,std::result::Result,)> { ic_cdk::call(self.0, "x", (arg0,arg1,)).await } + pub async fn y(&self, arg0: &NestedRecords) -> Result<((NestedRecords,MyVariant,),)> { + ic_cdk::call(self.0, "y", (arg0,)).await + } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); +/// Doc comment for service pub const service : Service = Service(CANISTER_ID); #[test] fn test_Arc_MyList_() { // Generated from ListInner.record.tail.use_type = "Arc" - let candid_src = r#"type ListInner = record { head : int; tail : List }; + let candid_src = r#"type ListInner = record { + /// Doc comment for List head + head : int; + /// Doc comment for List tail + tail : List; +}; +/// Doc comment for List type List = opt ListInner; (List)"#; candid_parser::utils::check_rust_type::>(candid_src).unwrap(); diff --git a/rust/candid_parser/tests/assets/ok/fieldnat.rs b/rust/candid_parser/tests/assets/ok/fieldnat.rs index 3d335e76b..43c063fd1 100644 --- a/rust/candid_parser/tests/assets/ok/fieldnat.rs +++ b/rust/candid_parser/tests/assets/ok/fieldnat.rs @@ -51,6 +51,7 @@ impl Service { ic_cdk::call(self.0, "foo", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/inline_methods.rs b/rust/candid_parser/tests/assets/ok/inline_methods.rs index edc700b8d..7fc443b3a 100644 --- a/rust/candid_parser/tests/assets/ok/inline_methods.rs +++ b/rust/candid_parser/tests/assets/ok/inline_methods.rs @@ -46,6 +46,7 @@ impl Service { ic_cdk::call(self.0, "high_order_fn_via_record_inline", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/keyword.rs b/rust/candid_parser/tests/assets/ok/keyword.rs index e0b8ddb7b..d25756994 100644 --- a/rust/candid_parser/tests/assets/ok/keyword.rs +++ b/rust/candid_parser/tests/assets/ok/keyword.rs @@ -76,6 +76,7 @@ impl Service { ic_cdk::call(self.0, "variant", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/management.rs b/rust/candid_parser/tests/assets/ok/management.rs index 311f8e1d6..87b5bcf95 100644 --- a/rust/candid_parser/tests/assets/ok/management.rs +++ b/rust/candid_parser/tests/assets/ok/management.rs @@ -301,5 +301,6 @@ impl<'a> Service<'a> { Ok(Decode!(&bytes)?) } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); diff --git a/rust/candid_parser/tests/assets/ok/recursion.rs b/rust/candid_parser/tests/assets/ok/recursion.rs index 3eb52d95f..239310042 100644 --- a/rust/candid_parser/tests/assets/ok/recursion.rs +++ b/rust/candid_parser/tests/assets/ok/recursion.rs @@ -38,6 +38,7 @@ impl Service { ic_cdk::call(self.0, "g", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/recursive_class.rs b/rust/candid_parser/tests/assets/ok/recursive_class.rs index ce01cd0ea..6678e8ca0 100644 --- a/rust/candid_parser/tests/assets/ok/recursive_class.rs +++ b/rust/candid_parser/tests/assets/ok/recursive_class.rs @@ -12,6 +12,7 @@ impl Service { ic_cdk::call(self.0, "next", ()).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/service.rs b/rust/candid_parser/tests/assets/ok/service.rs index 631c2095a..cbcf60cd7 100644 --- a/rust/candid_parser/tests/assets/ok/service.rs +++ b/rust/candid_parser/tests/assets/ok/service.rs @@ -30,6 +30,7 @@ impl Service { ic_cdk::call(self.0, "asVariant", ()).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/unicode.rs b/rust/candid_parser/tests/assets/ok/unicode.rs index 87c596f46..b5bfbb954 100644 --- a/rust/candid_parser/tests/assets/ok/unicode.rs +++ b/rust/candid_parser/tests/assets/ok/unicode.rs @@ -42,6 +42,7 @@ impl Service { ic_cdk::call(self.0, "👀", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index 5615f01c2..59e600510 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -142,7 +142,7 @@ impl TypeAnnotation { .actor .as_ref() .ok_or_else(|| Error::msg("Cannot use --method with a non-service did file"))?; - let func = idl_prog.get_method(actor, meth).map_err(Error::msg)?; + let func = idl_prog.get_method(&actor.typ, meth).map_err(Error::msg)?; match mode { Mode::Encode => func.args.iter().map(|arg| arg.typ.clone()).collect(), Mode::Decode => func.rets.clone(),