diff --git a/CHANGELOG.md b/CHANGELOG.md index afa1b720b..fa3ff6eb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,8 @@ my_variant_field : nat; }; ``` + + Adds the `IDLMergedProg` struct, used to collect the syntax types when parsing Candid declarations. + + Supports reflecting doc comments from Candid declarations to the Motoko generated bindings. The `candid_parser::bindings::motoko::compile` function now takes a `&IDLMergedProg` parameter. ### candid_derive diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index 179e17ea9..ded058b6a 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -1,4 +1,9 @@ -use crate::types::{FuncMode, Label}; +use crate::{ + idl_hash, + types::{FuncMode, Label}, +}; +use anyhow::{anyhow, bail, Context, Result}; +use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IDLType { @@ -111,7 +116,7 @@ pub struct Binding { pub docs: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IDLActorType { pub typ: IDLType, pub docs: Vec, @@ -123,8 +128,122 @@ pub struct IDLProg { pub actor: Option, } +impl IDLProg { + pub fn typ_decs(decs: Vec) -> impl Iterator { + decs.into_iter().filter_map(|d| { + if let Dec::TypD(bindings) = d { + Some(bindings) + } else { + None + } + }) + } +} + #[derive(Debug)] pub struct IDLInitArgs { pub decs: Vec, pub args: Vec, } + +#[derive(Debug)] +pub struct IDLMergedProg { + typ_decs: Vec, + main_actor: Option, + service_imports: Vec<(String, IDLActorType)>, +} + +impl IDLMergedProg { + pub fn new(prog: IDLProg) -> IDLMergedProg { + IDLMergedProg { + typ_decs: IDLProg::typ_decs(prog.decs).collect(), + main_actor: prog.actor, + service_imports: vec![], + } + } + + pub fn merge(&mut self, is_service_import: bool, name: String, prog: IDLProg) -> Result<()> { + self.typ_decs.extend(IDLProg::typ_decs(prog.decs)); + if is_service_import { + let actor = prog + .actor + .with_context(|| format!("Imported service file \"{name}\" has no main service"))?; + self.service_imports.push((name, actor)); + } + Ok(()) + } + + pub fn lookup(&self, id: &str) -> Option<&Binding> { + self.typ_decs.iter().find(|b| b.id == id) + } + + pub fn decs(&self) -> Vec { + self.typ_decs.iter().map(|b| Dec::TypD(b.clone())).collect() + } + + pub fn resolve_actor(&self) -> Result> { + let (init_args, top_level_docs, mut methods) = match &self.main_actor { + None => { + if self.service_imports.is_empty() { + return Ok(None); + } else { + (None, vec![], vec![]) + } + } + Some(t) if self.service_imports.is_empty() => return Ok(Some(t.clone())), + Some(IDLActorType { + typ: IDLType::ClassT(args, inner), + docs, + }) => ( + Some(args.clone()), + docs.clone(), + self.chase_service(*inner.clone(), None)?, + ), + Some(ty) => ( + None, + ty.docs.clone(), + self.chase_service(ty.typ.clone(), None)?, + ), + }; + + for (name, typ) in &self.service_imports { + methods.extend(self.chase_service(typ.typ.clone(), Some(name))?); + } + + let mut hashes: HashMap = HashMap::new(); + for method in &methods { + let name = &method.id; + if let Some(previous) = hashes.insert(idl_hash(name), name) { + bail!("Duplicate imported method name: label '{name}' hash collision with '{previous}'") + } + } + + let typ = if let Some(args) = init_args { + IDLType::ClassT(args, Box::new(IDLType::ServT(methods))) + } else { + IDLType::ServT(methods) + }; + Ok(Some(IDLActorType { + typ, + docs: top_level_docs, + })) + } + + // NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs + fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result> { + match ty { + IDLType::VarT(v) => { + let resolved = self + .typ_decs + .iter() + .find(|b| b.id == v) + .with_context(|| format!("Unbound type identifier {v}"))?; + self.chase_service(resolved.typ.clone(), import_name) + } + IDLType::ServT(bindings) => Ok(bindings), + ty => Err(import_name + .map(|name| anyhow!("Imported service file \"{name}\" has a service constructor")) + .unwrap_or(anyhow!("not a service type: {:?}", ty))), + } + } +} diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index 29dac5dba..455902585 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -3,6 +3,7 @@ use candid::pretty::candid::is_valid_as_id; use candid::pretty::utils::*; +use candid::types::syntax::{self, IDLActorType, IDLMergedProg, IDLType}; use candid::types::{ArgType, Field, FuncMode, Function, Label, SharedLabel, Type, TypeInner}; use candid::TypeEnv; use pretty::RcDoc; @@ -10,20 +11,23 @@ use pretty::RcDoc; // The definition of tuple is language specific. fn is_tuple(t: &Type) -> bool { match t.as_ref() { - TypeInner::Record(ref fs) => { - if fs.len() <= 1 { - return false; - } - for (i, field) in fs.iter().enumerate() { - if field.id.get_id() != (i as u32) { - return false; - } - } - true - } + TypeInner::Record(ref fs) => is_tuple_fields(fs), _ => false, } } + +fn is_tuple_fields(fs: &[Field]) -> bool { + if fs.len() <= 1 { + return false; + } + for (i, field) in fs.iter().enumerate() { + if field.id.get_id() != (i as u32) { + return false; + } + } + true +} + static KEYWORDS: [&str; 48] = [ "actor", "and", @@ -92,6 +96,27 @@ fn escape(id: &str, is_method: bool) -> RcDoc { } } +fn pp_ty_rich<'a>(ty: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { + match (ty.as_ref(), syntax) { + (TypeInner::Service(ref meths), Some(IDLType::ServT(methods))) => { + pp_service(meths, Some(methods)) + } + (TypeInner::Class(ref args, t), Some(IDLType::ClassT(_, syntax_t))) => { + pp_class((args, t), Some(syntax_t)) + } + (TypeInner::Record(ref fields), Some(IDLType::RecordT(syntax_fields))) => { + pp_record(fields, Some(syntax_fields)) + } + (TypeInner::Variant(ref fields), Some(IDLType::VariantT(syntax_fields))) => { + pp_variant(fields, Some(syntax_fields)) + } + (TypeInner::Opt(ref inner), Some(IDLType::OptT(syntax))) => { + str("?").append(pp_ty_rich(inner, Some(syntax))) + } + (_, _) => pp_ty(ty), + } +} + fn pp_ty(ty: &Type) -> RcDoc { use TypeInner::*; match ty.as_ref() { @@ -117,33 +142,11 @@ fn pp_ty(ty: &Type) -> RcDoc { Opt(ref t) => str("?").append(pp_ty(t)), Vec(ref t) if matches!(t.as_ref(), Nat8) => str("Blob"), Vec(ref t) => enclose("[", pp_ty(t), "]"), - Record(ref fs) => { - if is_tuple(ty) { - let tuple = concat(fs.iter().map(|f| pp_ty(&f.ty)), ","); - enclose("(", tuple, ")") - } else { - let fields = concat(fs.iter().map(pp_field), ";"); - enclose_space("{", fields, "}") - } - } - Variant(ref fs) => { - if fs.is_empty() { - str("{#}") - } else { - let fields = concat(fs.iter().map(pp_variant), ";"); - enclose_space("{", fields, "}") - } - } + Record(ref fs) => pp_record(fs, None), + Variant(ref fs) => pp_variant(fs, None), Func(ref func) => pp_function(func), - Service(ref serv) => pp_service(serv), - Class(ref args, ref t) => { - let doc = pp_args(args).append(" -> async "); - match t.as_ref() { - Service(ref serv) => doc.append(pp_service(serv)), - Var(ref s) => doc.append(s), - _ => unreachable!(), - } - } + Service(ref serv) => pp_service(serv, None), + Class(ref args, ref t) => pp_class((args, t), None), Knot(_) | Unknown | Future => unreachable!(), } } @@ -158,18 +161,6 @@ fn pp_label(id: &SharedLabel) -> RcDoc { } } -fn pp_field(field: &Field) -> RcDoc { - pp_label(&field.id).append(" : ").append(pp_ty(&field.ty)) -} -fn pp_variant(field: &Field) -> RcDoc { - let doc = str("#").append(pp_label(&field.id)); - if *field.ty != TypeInner::Null { - doc.append(" : ").append(pp_ty(&field.ty)) - } else { - doc - } -} - fn pp_function(func: &Function) -> RcDoc { let args = pp_args(&func.args); let rets = pp_rets(&func.rets); @@ -238,49 +229,156 @@ fn pp_rets(args: &[Type]) -> RcDoc { } } -fn pp_service(serv: &[(String, Type)]) -> RcDoc { +fn pp_service<'a>(serv: &'a [(String, Type)], syntax: Option<&'a [syntax::Binding]>) -> RcDoc<'a> { let doc = concat( - serv.iter() - .map(|(id, func)| escape(id, true).append(" : ").append(pp_ty(func))), + serv.iter().map(|(id, func)| { + let mut docs = RcDoc::nil(); + let mut syntax_field_ty = None; + if let Some(bs) = syntax { + if let Some(b) = bs.iter().find(|b| &b.id == id) { + docs = pp_docs(&b.docs); + syntax_field_ty = Some(&b.typ) + } + } + docs.append(escape(id, true)) + .append(" : ") + .append(pp_ty_rich(func, syntax_field_ty)) + }), ";", ); kwd("actor").append(enclose_space("{", doc, "}")) } -fn pp_defs(env: &TypeEnv) -> RcDoc { +fn pp_tuple<'a>(fields: &'a [Field]) -> RcDoc<'a> { + let tuple = concat(fields.iter().map(|f| pp_ty(&f.ty)), ","); + enclose("(", tuple, ")") +} + +fn find_field<'a>( + fields: Option<&'a [syntax::TypeField]>, + label: &'a Label, +) -> (RcDoc<'a>, Option<&'a syntax::IDLType>) { + let mut docs = RcDoc::nil(); + let mut syntax_field_ty = None; + if let Some(bs) = fields { + if let Some(field) = bs.iter().find(|b| b.label == *label) { + docs = pp_docs(&field.docs); + syntax_field_ty = Some(&field.typ); + } + }; + (docs, syntax_field_ty) +} + +fn pp_record<'a>(fields: &'a [Field], syntax: Option<&'a [syntax::TypeField]>) -> RcDoc<'a> { + if is_tuple_fields(fields) { + return pp_tuple(fields); + } + let doc = concat( + fields.iter().map(|field| { + let (docs, syntax_field) = find_field(syntax, &field.id); + docs.append(pp_label(&field.id)) + .append(" : ") + .append(pp_ty_rich(&field.ty, syntax_field)) + }), + ";", + ); + enclose_space("{", doc, "}") +} + +fn pp_variant<'a>(fields: &'a [Field], syntax: Option<&'a [syntax::TypeField]>) -> RcDoc<'a> { + if fields.is_empty() { + return str("{#}"); + } + let doc = concat( + fields.iter().map(|field| { + let (docs, syntax_field) = find_field(syntax, &field.id); + let doc = docs.append(str("#")).append(pp_label(&field.id)); + if *field.ty != TypeInner::Null { + doc.append(" : ") + .append(pp_ty_rich(&field.ty, syntax_field)) + } else { + doc + } + }), + ";", + ); + enclose_space("{", doc, "}") +} + +fn pp_class<'a>((args, ty): (&'a [ArgType], &'a Type), syntax: Option<&'a IDLType>) -> RcDoc<'a> { + let doc = pp_args(args).append(" -> async "); + match ty.as_ref() { + TypeInner::Service(_) => doc.append(pp_ty_rich(ty, syntax)), + TypeInner::Var(_) => doc.append(pp_ty(ty)), + _ => unreachable!(), + } +} + +fn pp_docs<'a>(docs: &'a [String]) -> RcDoc<'a> { + lines(docs.iter().map(|line| RcDoc::text("/// ").append(line))) +} + +fn pp_defs<'a>(env: &'a TypeEnv, prog: &'a IDLMergedProg) -> RcDoc<'a> { lines(env.0.iter().map(|(id, ty)| { - kwd("public type") + let syntax = prog.lookup(id); + let docs = syntax + .map(|b| pp_docs(b.docs.as_ref())) + .unwrap_or(RcDoc::nil()); + docs.append(kwd("public type")) .append(escape(id, false)) .append(" = ") - .append(pp_ty(ty)) + .append(pp_ty_rich(ty, syntax.map(|b| &b.typ))) .append(";") })) } -fn pp_actor(ty: &Type) -> RcDoc { +fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { + let self_doc = kwd("public type Self ="); match ty.as_ref() { - TypeInner::Service(ref serv) => pp_service(serv), - TypeInner::Var(_) | TypeInner::Class(_, _) => pp_ty(ty), + TypeInner::Service(ref serv) => match syntax { + Some(IDLActorType { + typ: IDLType::ServT(ref fields), + docs, + }) => { + let docs = pp_docs(docs); + docs.append(self_doc).append(pp_service(serv, Some(fields))) + } + _ => pp_service(serv, None), + }, + TypeInner::Class(ref args, ref t) => match syntax { + Some(IDLActorType { + typ: IDLType::ClassT(_, syntax_t), + docs, + }) => { + let docs = pp_docs(docs); + docs.append(self_doc) + .append(pp_class((args, t), Some(syntax_t))) + } + _ => self_doc.append(pp_class((args, t), None)), + }, + TypeInner::Var(_) => self_doc.append(pp_ty(ty)), _ => unreachable!(), } } -pub fn compile(env: &TypeEnv, actor: &Option) -> String { +pub fn compile(env: &TypeEnv, actor: &Option, 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. "#; + let syntax_actor = prog.resolve_actor().ok().flatten(); let doc = match actor { - None => pp_defs(env), + None => pp_defs(env, prog), Some(actor) => { - let defs = pp_defs(env); - let actor = kwd("public type Self =").append(pp_actor(actor)); + let defs = pp_defs(env, prog); + let actor = pp_actor(actor, syntax_actor.as_ref()); defs.append(actor) } }; - RcDoc::text(header) + let doc = RcDoc::text(header) .append(RcDoc::line()) .append("module ") .append(enclose_space("{", doc, "}")) .pretty(LINE_WIDTH) - .to_string() + .to_string(); + doc } diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index f78333583..54d6c4c2e 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -1,11 +1,11 @@ use crate::{parse_idl_prog, pretty_parse_idl_prog, Error, Result}; use candid::types::{ syntax::{ - Binding, Dec, IDLActorType, IDLArgType, IDLInitArgs, 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, BTreeSet}; use std::path::{Path, PathBuf}; @@ -279,43 +279,7 @@ pub fn check_init_args( Ok(args) } -fn merge_actor( - env: &Env, - actor: &Option, - imported: &Option, - file: &str, -) -> Result> { - match imported { - None => Err(Error::msg(format!( - "Imported service file {file:?} has no main service" - ))), - Some(t) => { - let t = env.te.trace_type(t)?; - match t.as_ref() { - TypeInner::Class(_, _) => Err(Error::msg(format!( - "Imported service file {file:?} has a service constructor" - ))), - TypeInner::Service(meths) => match actor { - None => Ok(Some(t)), - Some(t) => { - let t = env.te.trace_type(t)?; - let serv = env.te.as_service(&t)?; - let mut ms: Vec<_> = serv.iter().chain(meths.iter()).cloned().collect(); - ms.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - check_unique(ms.iter().map(|m| &m.0)).map_err(|e| { - Error::msg(format!("Duplicate imported method name: {e}")) - })?; - let res: Type = TypeInner::Service(ms).into(); - Ok(Some(res)) - } - }, - _ => unreachable!(), - } - } - } -} - -fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option)> { +fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, IDLMergedProg)> { let base = if file.is_absolute() { file.parent().unwrap().to_path_buf() } else { @@ -335,40 +299,29 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option)> let mut visited = BTreeMap::new(); let mut imports = Vec::new(); load_imports(is_pretty, &base, &mut visited, &prog, &mut imports)?; - let imports: Vec<_> = imports - .iter() - .map(|file| match visited.get(&file.0) { - Some(x) => (*x, &file.0, &file.1), - None => unreachable!(), - }) - .collect(); + + let mut merged_prog: IDLMergedProg = IDLMergedProg::new(prog); + for (path, name) in imports { + let include_service = visited.get(&path).unwrap(); + let code = std::fs::read_to_string(path)?; + let prog = parse_idl_prog(&code)?; + merged_prog.merge(*include_service, name, prog)?; + } + let mut te = TypeEnv::new(); let mut env = Env { te: &mut te, pre: false, }; - let mut actor: Option = None; - for (include_serv, path, name) in imports.iter() { - let code = std::fs::read_to_string(path)?; - let code = parse_idl_prog(&code)?; - check_decs(&mut env, &code.decs)?; - if *include_serv { - let t = check_actor(&env, &code.actor)?; - actor = merge_actor(&env, &actor, &t, name)?; - } - } - check_decs(&mut env, &prog.decs)?; - let mut res = check_actor(&env, &prog.actor)?; - if actor.is_some() { - res = merge_actor(&env, &res, &actor, "")?; - } - Ok((te, res)) + check_decs(&mut env, &merged_prog.decs())?; + let res = check_actor(&env, &merged_prog.resolve_actor()?)?; + Ok((te, res, merged_prog)) } /// Type check did file including the imports. -pub fn check_file(file: &Path) -> Result<(TypeEnv, Option)> { +pub fn check_file(file: &Path) -> Result<(TypeEnv, Option, IDLMergedProg)> { check_file_(file, false) } -pub fn pretty_check_file(file: &Path) -> Result<(TypeEnv, Option)> { +pub fn pretty_check_file(file: &Path) -> Result<(TypeEnv, Option, IDLMergedProg)> { check_file_(file, true) } diff --git a/rust/candid_parser/src/utils.rs b/rust/candid_parser/src/utils.rs index 5074a87ee..c9e868ef7 100644 --- a/rust/candid_parser/src/utils.rs +++ b/rust/candid_parser/src/utils.rs @@ -13,7 +13,10 @@ pub enum CandidSource<'a> { impl CandidSource<'_> { pub fn load(&self) -> Result<(TypeEnv, Option)> { Ok(match self { - CandidSource::File(path) => pretty_check_file(path)?, + CandidSource::File(path) => { + let (env, actor, _) = pretty_check_file(path)?; + (env, actor) + } CandidSource::Text(str) => { let ast = pretty_parse_idl_prog("", str)?; let mut env = TypeEnv::new(); diff --git a/rust/candid_parser/tests/assets/bad_import_duplicate_method.did b/rust/candid_parser/tests/assets/bad_import_duplicate_method.did new file mode 100644 index 000000000..964bcece8 --- /dev/null +++ b/rust/candid_parser/tests/assets/bad_import_duplicate_method.did @@ -0,0 +1,5 @@ +import service "actor.did"; + +service : { + f : () -> (); +} diff --git a/rust/candid_parser/tests/assets/bad_import_no_service.did b/rust/candid_parser/tests/assets/bad_import_no_service.did new file mode 100644 index 000000000..5ce42fac6 --- /dev/null +++ b/rust/candid_parser/tests/assets/bad_import_no_service.did @@ -0,0 +1,5 @@ +import service "import/no_service.did"; + +service : { + f : () -> (); +} diff --git a/rust/candid_parser/tests/assets/class.did b/rust/candid_parser/tests/assets/class.did index fb1fdfb7f..94f41b030 100644 --- a/rust/candid_parser/tests/assets/class.did +++ b/rust/candid_parser/tests/assets/class.did @@ -1,6 +1,8 @@ type Profile = record { age: nat8; name: text }; type List = opt record { int; List }; +// Doc comment for class service service : (int, l : List, Profile) -> { + // Doc comment for get method in class service get : () -> (List); set : (List) -> (List); } diff --git a/rust/candid_parser/tests/assets/example.did b/rust/candid_parser/tests/assets/example.did index a5a47fee6..64e803fea 100644 --- a/rust/candid_parser/tests/assets/example.did +++ b/rust/candid_parser/tests/assets/example.did @@ -1,22 +1,84 @@ 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, currently ignored + id: nat; + // Doc comment for 0x2a field in h method return, currently ignored + 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/b/b.did b/rust/candid_parser/tests/assets/import/b/b.did index b28cb7773..2cd82f4c3 100644 --- a/rust/candid_parser/tests/assets/import/b/b.did +++ b/rust/candid_parser/tests/assets/import/b/b.did @@ -1,4 +1,5 @@ type b = record { int;nat }; service : { + // Doc comment for imported bbbbb service method bbbbb : (b) -> (); } diff --git a/rust/candid_parser/tests/assets/import/no_service.did b/rust/candid_parser/tests/assets/import/no_service.did new file mode 100644 index 000000000..d72a8ed00 --- /dev/null +++ b/rust/candid_parser/tests/assets/import/no_service.did @@ -0,0 +1 @@ +type Dummy = record {}; diff --git a/rust/candid_parser/tests/assets/management.did b/rust/candid_parser/tests/assets/management.did index 04f425222..59af39ed6 100644 --- a/rust/candid_parser/tests/assets/management.did +++ b/rust/candid_parser/tests/assets/management.did @@ -121,7 +121,6 @@ service ic : { }; }) -> (http_response); - // Threshold ECDSA signature ecdsa_public_key : (record { canister_id : opt canister_id; derivation_path : vec blob; @@ -133,13 +132,11 @@ service ic : { key_id : record { curve: ecdsa_curve; name: text }; }) -> (record { signature : blob }); - // bitcoin interface bitcoin_get_balance: (get_balance_request) -> (satoshi); bitcoin_get_utxos: (get_utxos_request) -> (get_utxos_response); bitcoin_send_transaction: (send_transaction_request) -> (); bitcoin_get_current_fee_percentiles: (get_current_fee_percentiles_request) -> (vec millisatoshi_per_byte); - // provisional interfaces for the pre-ledger world provisional_create_canister_with_cycles : (record { amount: opt nat; settings : opt canister_settings; diff --git a/rust/candid_parser/tests/assets/ok/bad_import3.fail b/rust/candid_parser/tests/assets/ok/bad_import3.fail index d8d926d26..9e40cc438 100644 --- a/rust/candid_parser/tests/assets/ok/bad_import3.fail +++ b/rust/candid_parser/tests/assets/ok/bad_import3.fail @@ -1 +1 @@ -Duplicate imported method name: label 'f' hash collision with 'f' +duplicate binding for f diff --git a/rust/candid_parser/tests/assets/ok/bad_import_duplicate_method.fail b/rust/candid_parser/tests/assets/ok/bad_import_duplicate_method.fail new file mode 100644 index 000000000..d8d926d26 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/bad_import_duplicate_method.fail @@ -0,0 +1 @@ +Duplicate imported method name: label 'f' hash collision with 'f' diff --git a/rust/candid_parser/tests/assets/ok/bad_import_no_service.fail b/rust/candid_parser/tests/assets/ok/bad_import_no_service.fail new file mode 100644 index 000000000..719bb17cd --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/bad_import_no_service.fail @@ -0,0 +1 @@ +Imported service file "import/no_service.did" has no main service diff --git a/rust/candid_parser/tests/assets/ok/class.mo b/rust/candid_parser/tests/assets/ok/class.mo index 41ab07b63..454a3f2f8 100644 --- a/rust/candid_parser/tests/assets/ok/class.mo +++ b/rust/candid_parser/tests/assets/ok/class.mo @@ -4,7 +4,9 @@ module { public type List = ?(Int, List); public type Profile = { age : Nat8; name : Text }; + /// Doc comment for class service public type Self = (Int, l : List, Profile) -> async actor { + /// Doc comment for get method in class service get : shared () -> async List; set : shared List -> async List; } diff --git a/rust/candid_parser/tests/assets/ok/comment.mo b/rust/candid_parser/tests/assets/ok/comment.mo index baf858e78..789f14289 100644 --- a/rust/candid_parser/tests/assets/ok/comment.mo +++ b/rust/candid_parser/tests/assets/ok/comment.mo @@ -2,6 +2,8 @@ // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. module { + /// line comment + /// public type id = Nat8; } diff --git a/rust/candid_parser/tests/assets/ok/example.d.ts b/rust/candid_parser/tests/assets/ok/example.d.ts index a3975f37b..78b8ee194 100644 --- a/rust/candid_parser/tests/assets/ok/example.d.ts +++ b/rust/candid_parser/tests/assets/ok/example.d.ts @@ -12,6 +12,8 @@ export interface broker { 'find' : ActorMethod<[string], Principal> } export type f = ActorMethod<[List, [Principal, string]], [[] | [List], res]>; export type list = [] | [node]; export type my_type = Principal; +export type my_variant = { 'a' : { 'b' : string } } | + { 'c' : [] | [{ 'd' : string }] }; export interface nested { _0_ : bigint, _1_ : bigint, @@ -24,6 +26,9 @@ export interface nested { { 'C' : null }, _42_ : bigint, } +export interface nested_records { + 'nested' : [] | [{ 'nested_field' : string }], +} export type nested_res = { 'Ok' : { 'Ok' : null } | { 'Err' : null } } | { 'Err' : { 'Ok' : { 'content' : string } } | { 'Err' : [bigint] } }; export interface node { 'head' : bigint, 'tail' : list } @@ -37,10 +42,7 @@ export type tree = { } | { 'leaf' : bigint }; export interface _SERVICE { - 'bbbbb' : ActorMethod<[b], undefined>, - 'f' : t, 'f1' : ActorMethod<[list, Uint8Array | number[], [] | [boolean]], undefined>, - 'g' : ActorMethod<[list], [B, tree, stream]>, 'g1' : ActorMethod< [my_type, List, [] | [List], nested], [bigint, Principal, nested_res] @@ -64,6 +66,10 @@ export interface _SERVICE { { 'Err' : { 'a' : null } | { 'b' : null } }, ] >, + 'y' : ActorMethod<[nested_records], [nested_records, my_variant]>, + 'f' : t, + 'g' : ActorMethod<[list], [B, tree, stream]>, + 'bbbbb' : ActorMethod<[b], undefined>, } 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 6bd434f09..f6abfd99c 100644 --- a/rust/candid_parser/tests/assets/ok/example.did +++ b/rust/candid_parser/tests/assets/ok/example.did @@ -9,6 +9,10 @@ type broker = service { type f = func (List, func (int32) -> (int64)) -> (opt List, res); type list = opt node; type my_type = principal; +type my_variant = variant { + a : record { b : text }; + c : opt record { d : text }; +}; type nested = record { 0 : nat; 1 : nat; @@ -18,6 +22,7 @@ type nested = record { 41 : variant { 42; A; B; C }; 42 : nat; }; +type nested_records = record { nested : opt record { nested_field : text } }; type nested_res = variant { Ok : variant { Ok; Err }; Err : variant { Ok : record { content : text }; Err : record { int } }; @@ -32,10 +37,7 @@ type tree = variant { leaf : int; }; service : { - bbbbb : (b) -> (); - f : t; 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 }, @@ -46,4 +48,8 @@ service : { opt b, variant { Ok : record { result : text }; Err : variant { a; b } }, ) composite_query; + y : (nested_records) -> (record { nested_records; my_variant }) query; + f : t; + g : (list) -> (B, tree, stream); + bbbbb : (b) -> (); } diff --git a/rust/candid_parser/tests/assets/ok/example.js b/rust/candid_parser/tests/assets/ok/example.js index 4602f2a1b..53e277aaf 100644 --- a/rust/candid_parser/tests/assets/ok/example.js +++ b/rust/candid_parser/tests/assets/ok/example.js @@ -5,30 +5,8 @@ export const idlFactory = ({ IDL }) => { const stream = IDL.Rec(); const t = IDL.Rec(); const tree = IDL.Rec(); - const b = IDL.Tuple(IDL.Int, IDL.Nat); const node = IDL.Record({ 'head' : IDL.Nat, 'tail' : list }); list.fill(IDL.Opt(node)); - const A = B; - B.fill(IDL.Opt(A)); - tree.fill( - IDL.Variant({ - 'branch' : IDL.Record({ 'val' : IDL.Int, 'left' : tree, 'right' : tree }), - 'leaf' : IDL.Int, - }) - ); - stream.fill( - IDL.Opt( - IDL.Record({ - 'head' : IDL.Nat, - 'next' : IDL.Func([], [stream], ['query']), - }) - ) - ); - const s = IDL.Service({ - 'f' : t, - 'g' : IDL.Func([list], [B, tree, stream], []), - }); - t.fill(IDL.Func([s], [], [])); const my_type = IDL.Principal; List.fill(IDL.Opt(IDL.Record({ 'head' : IDL.Int, 'tail' : List }))); const nested = IDL.Record({ @@ -73,16 +51,42 @@ export const idlFactory = ({ IDL }) => { [IDL.Opt(List), res], [], ); + const b = IDL.Tuple(IDL.Int, IDL.Nat); const a = IDL.Variant({ 'a' : IDL.Null, 'b' : b }); - return IDL.Service({ - 'bbbbb' : IDL.Func([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 })), + }); + const A = B; + B.fill(IDL.Opt(A)); + tree.fill( + IDL.Variant({ + 'branch' : IDL.Record({ 'val' : IDL.Int, 'left' : tree, 'right' : tree }), + 'leaf' : IDL.Int, + }) + ); + stream.fill( + IDL.Opt( + IDL.Record({ + 'head' : IDL.Nat, + 'next' : IDL.Func([], [stream], ['query']), + }) + ) + ); + const s = IDL.Service({ 'f' : t, + 'g' : IDL.Func([list], [B, tree, stream], []), + }); + t.fill(IDL.Func([s], [], [])); + return IDL.Service({ 'f1' : IDL.Func( [list, IDL.Vec(IDL.Nat8), IDL.Opt(IDL.Bool)], [], ['oneway'], ), - 'g' : IDL.Func([list], [B, tree, stream], []), 'g1' : IDL.Func( [my_type, List, IDL.Opt(List), nested], [IDL.Int, broker, nested_res], @@ -110,6 +114,14 @@ export const idlFactory = ({ IDL }) => { ], ['composite_query'], ), + 'y' : IDL.Func( + [nested_records], + [IDL.Tuple(nested_records, my_variant)], + ['query'], + ), + 'f' : t, + 'g' : IDL.Func([list], [B, tree, stream], []), + 'bbbbb' : IDL.Func([b], [], []), }); }; 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 76bb1f058..7821f1318 100644 --- a/rust/candid_parser/tests/assets/ok/example.mo +++ b/rust/candid_parser/tests/assets/ok/example.mo @@ -4,9 +4,16 @@ module { public type A = B; public type B = ?A; - 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 a = { #a; #b : b }; public type b = (Int, Nat); + /// Doc comment for broker service public type broker = actor { find : shared (name : Text) -> async actor { current : shared () -> async Nat32; @@ -18,22 +25,60 @@ module { res, ); public type list = ?node; + /// Doc comment for prim type public type my_type = Principal; + 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 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; }; + /// 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 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 }; + }; }; public type node = { head : Nat; tail : list }; - 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; + }; + }; + /// Doc comment for service id public type s = actor { f : t; g : shared list -> async (B, tree, stream) }; public type stream = ?{ head : Nat; next : shared query () -> async stream }; public type t = shared (server : s) -> async (); @@ -41,11 +86,10 @@ module { #branch : { val : Int; left : tree; right : tree }; #leaf : Int; }; + /// Doc comment for service public type Self = actor { - 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 ( Int, broker, @@ -55,11 +99,17 @@ module { _42_ : {}; 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)); + f : t; + g : shared list -> async (B, tree, stream); + /// Doc comment for imported bbbbb service method + bbbbb : shared b -> async (); } } diff --git a/rust/candid_parser/tests/assets/ok/example.rs b/rust/candid_parser/tests/assets/ok/example.rs index c4a9d4002..1ce15b36f 100644 --- a/rust/candid_parser/tests/assets/ok/example.rs +++ b/rust/candid_parser/tests/assets/ok/example.rs @@ -4,35 +4,10 @@ use candid::{self, CandidType, Deserialize, Principal}; use ic_cdk::api::call::CallResult as Result; -#[derive(CandidType, Deserialize, Debug)] -pub(crate) struct B (pub(crate) candid::Int,pub(crate) u128,); #[derive(CandidType, Deserialize, Debug)] pub(crate) struct Node { pub(crate) head: u128, pub(crate) tail: Box } #[derive(CandidType, Deserialize, Debug)] pub(crate) struct List(pub(crate) Option); -pub(crate) type A = Box; -#[derive(CandidType, Deserialize, Debug)] -pub(crate) struct B(pub(crate) Option); -#[derive(CandidType, Deserialize, Debug)] -pub(crate) enum Tree { - #[serde(rename="branch")] - Branch{ val: candid::Int, left: Box, right: Box }, - #[serde(rename="leaf")] - Leaf(candid::Int), -} -candid::define_function!(pub(crate) StreamInnerNext : () -> (Stream) query); -#[derive(CandidType, Deserialize, Debug)] -pub(crate) struct StreamInner { - pub(crate) head: u128, - pub(crate) next: StreamInnerNext, -} -#[derive(CandidType, Deserialize, Debug)] -pub(crate) struct Stream(pub(crate) Option); -candid::define_service!(pub(crate) S : { - "f" : T::ty(); - "g" : candid::func!((List) -> (B, Tree, Stream)); -}); -candid::define_function!(pub(crate) T : (S) -> ()); type CanisterId = Principal; #derive[CandidType, Deserialize, Clone] pub(crate) struct ListInner { @@ -96,26 +71,57 @@ candid::define_function!(pub(crate) F : (MyList, FArg1) -> ( Res, )); #[derive(CandidType, Deserialize, Debug)] +pub(crate) struct B (pub(crate) candid::Int,pub(crate) u128,); +#[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 { pub(crate) nested_field: String } +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct NestedRecords { + pub(crate) nested: Option, +} +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct MyVariantCInner { pub(crate) d: String } +#[derive(CandidType, Deserialize, Debug)] +pub(crate) enum MyVariant { + #[serde(rename="a")] + A{ b: String }, + #[serde(rename="c")] + C(Option), +} +pub(crate) type A = Box; +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct B(pub(crate) Option); +#[derive(CandidType, Deserialize, Debug)] +pub(crate) enum Tree { + #[serde(rename="branch")] + Branch{ val: candid::Int, left: Box, right: Box }, + #[serde(rename="leaf")] + Leaf(candid::Int), +} +candid::define_function!(pub(crate) StreamInnerNext : () -> (Stream) query); +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct StreamInner { + pub(crate) head: u128, + pub(crate) next: StreamInnerNext, +} +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct Stream(pub(crate) Option); +candid::define_service!(pub(crate) S : { + "f" : T::ty(); + "g" : candid::func!((List) -> (B, Tree, Stream)); +}); +candid::define_function!(pub(crate) T : (S) -> ()); pub struct Service(pub Principal); impl 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 - } 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 } - pub async fn g(&self, arg0: &List) -> Result<(B,Tree,Stream,)> { - ic_cdk::call(self.0, "g", (arg0,)).await - } pub async fn G11(&self, id: &CanisterId, list: &MyList, is_okay: &Option, arg3: &Nested) -> Result<(i128,Broker,NestedRes,)> { ic_cdk::call(self.0, "g1", (id,list,is_okay,arg3,)).await } @@ -128,6 +134,18 @@ impl Service { 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 async fn f(&self, server: &S) -> Result<()> { + ic_cdk::call(self.0, "f", (server,)).await + } + pub async fn g(&self, arg0: &List) -> Result<(B,Tree,Stream,)> { + ic_cdk::call(self.0, "g", (arg0,)).await + } + pub async fn bbbbb(&self, arg0: &B) -> Result<()> { + ic_cdk::call(self.0, "bbbbb", (arg0,)).await + } } pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/recursion.mo b/rust/candid_parser/tests/assets/ok/recursion.mo index 6cc51fe02..b23248144 100644 --- a/rust/candid_parser/tests/assets/ok/recursion.mo +++ b/rust/candid_parser/tests/assets/ok/recursion.mo @@ -6,6 +6,7 @@ module { public type B = ?A; public type list = ?node; public type node = { head : Nat; tail : list }; + /// Doc comment for service id public type s = actor { f : t; g : shared list -> async (B, tree, stream) }; public type stream = ?{ head : Nat; next : shared query () -> async stream }; public type t = shared (server : s) -> async (); diff --git a/rust/candid_parser/tests/assets/recursion.did b/rust/candid_parser/tests/assets/recursion.did index 9ec25c1be..2a0b86b22 100644 --- a/rust/candid_parser/tests/assets/recursion.did +++ b/rust/candid_parser/tests/assets/recursion.did @@ -6,6 +6,7 @@ type tree = variant { leaf : int; branch : record { left : tree; val : int; right : tree }; }; +// Doc comment for service id 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}; diff --git a/rust/candid_parser/tests/parse_type.rs b/rust/candid_parser/tests/parse_type.rs index 2385b9a32..0ad70344c 100644 --- a/rust/candid_parser/tests/parse_type.rs +++ b/rust/candid_parser/tests/parse_type.rs @@ -120,7 +120,7 @@ fn compiler_test(resource: &str) { let candid_path = base_path.join(filename); match check_file(&candid_path) { - Ok((env, actor)) => { + Ok((env, actor, prog)) => { { let mut output = mint.new_goldenfile(filename.with_extension("did")).unwrap(); let content = compile(&env, &actor); @@ -131,13 +131,14 @@ fn compiler_test(resource: &str) { } { match filename.file_name().unwrap().to_str().unwrap() { - "unicode.did" | "escape.did" => { - check_error(|| motoko::compile(&env, &actor), "not a valid Motoko id") - } + "unicode.did" | "escape.did" => check_error( + || motoko::compile(&env, &actor, &prog), + "not a valid Motoko id", + ), _ => { let mut output = mint.new_goldenfile(filename.with_extension("mo")).unwrap(); - let content = motoko::compile(&env, &actor); + let content = motoko::compile(&env, &actor, &prog); writeln!(output, "{content}").unwrap(); } } diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index 89959ddd9..c95dedb79 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -123,7 +123,8 @@ impl TypeAnnotation { } fn get_types(&self, mode: Mode) -> candid_parser::Result<(TypeEnv, Vec)> { let (env, actor) = if let Some(ref file) = self.defs { - pretty_check_file(file)? + let (env, actor, _) = pretty_check_file(file)?; + (env, actor) } else { (TypeEnv::new(), None) }; @@ -181,9 +182,9 @@ fn main() -> Result<()> { previous, strict, } => { - let (mut env, opt_t1) = pretty_check_file(&input)?; + let (mut env, opt_t1, _) = pretty_check_file(&input)?; if let Some(previous) = previous { - let (env2, opt_t2) = pretty_check_file(&previous)?; + let (env2, opt_t2, _) = pretty_check_file(&previous)?; match (opt_t1, opt_t2) { (Some(t1), Some(t2)) => { let mut gamma = HashSet::new(); @@ -202,7 +203,8 @@ fn main() -> Result<()> { } Command::Subtype { defs, ty1, ty2 } => { let (env, _) = if let Some(file) = defs { - pretty_check_file(&file)? + let (env, actor, _) = pretty_check_file(&file)?; + (env, actor) } else { (TypeEnv::new(), None) }; @@ -217,7 +219,7 @@ fn main() -> Result<()> { methods, } => { let configs = load_config(&config)?; - let (env, mut actor) = pretty_check_file(&input)?; + let (env, mut actor, prog) = pretty_check_file(&input)?; if !methods.is_empty() { actor = Some(candid_parser::bindings::analysis::project_methods( &env, &actor, methods, @@ -227,7 +229,7 @@ fn main() -> Result<()> { "js" => candid_parser::bindings::javascript::compile(&env, &actor), "ts" => candid_parser::bindings::typescript::compile(&env, &actor), "did" => candid_parser::pretty::candid::compile(&env, &actor), - "mo" => candid_parser::bindings::motoko::compile(&env, &actor), + "mo" => candid_parser::bindings::motoko::compile(&env, &actor, &prog), "rs" => { use candid_parser::bindings::rust::{compile, Config, ExternalConfig}; let external = configs