From b25df5cbdf4c65f08f7ebe265053eb7a47873288 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 17 Jun 2024 09:44:29 -0400 Subject: [PATCH 1/6] beta candid_parser --- Cargo.lock | 87 ++++++++++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f543c9b13..2c3e53a88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,21 +313,24 @@ dependencies = [ [[package]] name = "candid_parser" -version = "0.1.4" +version = "0.2.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48a3da76f989cd350b7342c64c6c6008341bb6186f6832ef04e56dc50ba0fd76" +checksum = "6fa862dcc8beb3c250b335eaa388f169fa1575bb1476e09c9c78cbef88d510e4" dependencies = [ "anyhow", "candid", "codespan-reporting", "convert_case", + "handlebars", "hex", "lalrpop", "lalrpop-util", "logos", "num-bigint", "pretty", + "serde", "thiserror", + "toml", ] [[package]] @@ -961,6 +964,20 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -1436,32 +1453,33 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "logos" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" +checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68" +checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a" dependencies = [ "beef", "fnv", + "lazy_static", "proc-macro2", "quote", - "regex-syntax 0.6.29", + "regex-syntax 0.8.3", "syn 2.0.61", ] [[package]] name = "logos-derive" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" +checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a" dependencies = [ "logos-codegen", ] @@ -1660,6 +1678,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -2743,6 +2806,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index f17419d37..4ebabfba3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ ic-cdk = { path = "src/ic-cdk", version = "0.15.0" } ic-cdk-timers = { path = "src/ic-cdk-timers", version = "0.9.0" } candid = "0.10.4" -candid_parser = "0.1.4" +candid_parser = "0.2.0-beta.3" futures = "0.3" hex = "0.4" quote = "1" From df9753cc77b8e2ff146b9a27a784f11f68b85e3a Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 22 Jul 2024 14:20:41 -0400 Subject: [PATCH 2/6] upgrade candid dep --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c3e53a88..173998fe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "candid" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5902d37352dffd8bd9177a2daa6444ce3cd0279c91763fb0171c053aa04335" +checksum = "7df77a80c72fcd356cf37ff59c812f37ff06dc9a81232b3aff0a308cb5996904" dependencies = [ "anyhow", "binread", diff --git a/Cargo.toml b/Cargo.toml index 4ebabfba3..1e745cc3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ ic0 = { path = "src/ic0", version = "0.23.0" } ic-cdk = { path = "src/ic-cdk", version = "0.15.0" } ic-cdk-timers = { path = "src/ic-cdk-timers", version = "0.9.0" } -candid = "0.10.4" +candid = "0.10.9" candid_parser = "0.2.0-beta.3" futures = "0.3" hex = "0.4" From 875e2822c01e8f699fa89898ffdf436ff5bc9a9e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 22 Jul 2024 14:21:05 -0400 Subject: [PATCH 3/6] use type selector API --- src/ic-cdk-bindgen/src/lib.rs | 45 +++++++++++++------ src/ic-cdk-bindgen/src/templates/consumer.hbs | 22 +++++++++ src/ic-cdk-bindgen/src/templates/provider.hbs | 23 ++++++++++ src/ic-cdk-bindgen/src/templates/type.hbs | 23 ++++++++++ 4 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 src/ic-cdk-bindgen/src/templates/consumer.hbs create mode 100644 src/ic-cdk-bindgen/src/templates/provider.hbs create mode 100644 src/ic-cdk-bindgen/src/templates/type.hbs diff --git a/src/ic-cdk-bindgen/src/lib.rs b/src/ic-cdk-bindgen/src/lib.rs index 5bb0a091b..be9faed8e 100644 --- a/src/ic-cdk-bindgen/src/lib.rs +++ b/src/ic-cdk-bindgen/src/lib.rs @@ -1,9 +1,13 @@ use candid::Principal; +use candid_parser::bindings::rust::{emit_bindgen, output_handlebar, Config, ExternalConfig}; +use candid_parser::configs::Configs; use candid_parser::pretty_check_file; + use std::env; use std::fs; use std::io::Write; use std::path::PathBuf; +use std::str::FromStr; mod code_generator; mod error; @@ -80,27 +84,40 @@ impl Builder { // Code generation. impl Builder { fn generate(&self, target: Target) -> Result<()> { - let mut binding = code_generator::Config::new(); + let config = Config::new(Configs::from_str("").unwrap()); + let candid_path = if let Some(p) = &self.candid_path { + p.clone() + } else { + candid_path_from_env(&self.canister_name)? + }; + + let (env, actor) = pretty_check_file(&candid_path).expect("Cannot parse candid file"); + let (output, unused) = emit_bindgen(&config, &env, &actor); + // TODO: handle unused. + assert!(unused.is_empty()); + + let template = match target { + Target::Consumer => include_str!("templates/consumer.hbs"), + Target::Provider => include_str!("templates/provider.hbs"), + Target::Type => include_str!("templates/type.hbs"), + }; + + let mut external = ExternalConfig::default(); if target == Target::Consumer { let canister_id = if let Some(p) = &self.canister_id { *p } else { canister_id_from_env(&self.canister_name)? }; - binding.set_canister_id(canister_id); + external + .0 + .insert("canister_id".to_string(), canister_id.to_string()); } - binding - .set_service_name(self.canister_name.to_string()) - .set_target(target); - - let candid_path = if let Some(p) = &self.candid_path { - p.clone() - } else { - candid_path_from_env(&self.canister_name)? - }; + external + .0 + .insert("service_name".to_string(), self.canister_name.to_string()); - let (env, actor) = pretty_check_file(&candid_path).expect("Cannot parse candid file"); - let content = code_generator::compile(&binding, &env, &actor); + let content = output_handlebar(output, external, template); let out_dir = if let Some(p) = &self.out_dir { p.clone() @@ -117,7 +134,7 @@ impl Builder { let generated_path = sub_dir.join(format!("{}.rs", &self.canister_name)); let mut file = fs::File::create(generated_path)?; - file.write_all(content.as_bytes())?; + writeln!(file, "{content}")?; Ok(()) } diff --git a/src/ic-cdk-bindgen/src/templates/consumer.hbs b/src/ic-cdk-bindgen/src/templates/consumer.hbs new file mode 100644 index 000000000..cca130e00 --- /dev/null +++ b/src/ic-cdk-bindgen/src/templates/consumer.hbs @@ -0,0 +1,22 @@ +#[allow(unused_imports)] +use {{candid_crate}}::{self, CandidType, Deserialize, Principal}; +use ic_cdk::api::call::CallResult as Result; + +{{type_defs}} +{{#if methods}} +pub struct {{PascalCase service_name}}(pub Principal); +impl {{PascalCase service_name}} { + {{#each methods}} + 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}} +pub const {{snake_case service_name}} : {{PascalCase service_name}} = {{PascalCase service_name}}(CANISTER_ID); +{{/if}} +{{/if}} +{{#if tests}} +{{tests}} +{{/if}} diff --git a/src/ic-cdk-bindgen/src/templates/provider.hbs b/src/ic-cdk-bindgen/src/templates/provider.hbs new file mode 100644 index 000000000..92f5daadc --- /dev/null +++ b/src/ic-cdk-bindgen/src/templates/provider.hbs @@ -0,0 +1,23 @@ +#[allow(unused_imports)] +use {{candid_crate}}::{self, CandidType, Deserialize, Principal}; + +{{type_defs}} +{{#if init_args}} +#[ic_cdk::init] +fn init({{#each init_args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{/each}}) { + unimplemented!() +} +{{/if}} +{{#each methods}} +#[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!() +} +{{/each}} +{{#if tests}} +{{tests}} +{{/if}} +{{#if metadata}} +#[link_section = "icp:public candid:service"] +pub static __SERVICE: [u8; {{len metadata}}] = *br#"{{metadata}}"#; +{{/if}} diff --git a/src/ic-cdk-bindgen/src/templates/type.hbs b/src/ic-cdk-bindgen/src/templates/type.hbs new file mode 100644 index 000000000..92f5daadc --- /dev/null +++ b/src/ic-cdk-bindgen/src/templates/type.hbs @@ -0,0 +1,23 @@ +#[allow(unused_imports)] +use {{candid_crate}}::{self, CandidType, Deserialize, Principal}; + +{{type_defs}} +{{#if init_args}} +#[ic_cdk::init] +fn init({{#each init_args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{/each}}) { + unimplemented!() +} +{{/if}} +{{#each methods}} +#[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!() +} +{{/each}} +{{#if tests}} +{{tests}} +{{/if}} +{{#if metadata}} +#[link_section = "icp:public candid:service"] +pub static __SERVICE: [u8; {{len metadata}}] = *br#"{{metadata}}"#; +{{/if}} From 6f9f42c2736e0617a229b0cc0f8c4a3f1a099622 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 22 Jul 2024 14:25:04 -0400 Subject: [PATCH 4/6] remove code_generator which was copied from candid --- src/ic-cdk-bindgen/src/code_generator.rs | 623 ----------------------- src/ic-cdk-bindgen/src/lib.rs | 14 +- 2 files changed, 10 insertions(+), 627 deletions(-) delete mode 100644 src/ic-cdk-bindgen/src/code_generator.rs diff --git a/src/ic-cdk-bindgen/src/code_generator.rs b/src/ic-cdk-bindgen/src/code_generator.rs deleted file mode 100644 index 0688ee109..000000000 --- a/src/ic-cdk-bindgen/src/code_generator.rs +++ /dev/null @@ -1,623 +0,0 @@ -use candid::pretty::utils::*; -use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; -use candid_parser::bindings::analysis::{chase_actor, infer_rec}; -use convert_case::{Case, Casing}; -use pretty::RcDoc; -use std::collections::BTreeSet; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[allow(dead_code)] -pub enum Target { - Consumer, - Provider, - Type, -} - -#[derive(Clone)] -pub struct Config { - canister_id: Option, - service_name: String, - target: Target, -} -impl Config { - pub fn new() -> Self { - Config { - canister_id: None, - service_name: "service".to_string(), - target: Target::Consumer, - } - } - /// Only generates SERVICE struct if canister_id is not provided - pub fn set_canister_id(&mut self, id: candid::Principal) -> &mut Self { - self.canister_id = Some(id); - self - } - /// Service name when canister id is provided - pub fn set_service_name(&mut self, name: String) -> &mut Self { - self.service_name = name; - self - } - pub fn set_target(&mut self, name: Target) -> &mut Self { - self.target = name; - self - } -} -impl Default for Config { - fn default() -> Self { - Self::new() - } -} - -type RecPoints<'a> = BTreeSet<&'a str>; -// The definition of tuple is language specific. -pub(crate) fn is_tuple(fs: &[Field]) -> bool { - if fs.is_empty() { - return false; - } - !fs.iter() - .enumerate() - .any(|(i, field)| field.id.get_id() != (i as u32)) -} -static KEYWORDS: [&str; 51] = [ - "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", - "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", - "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", - "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", - "override", "priv", "typeof", "unsized", "virtual", "yield", "try", -]; -fn ident_(id: &str, case: Option) -> (RcDoc, bool) { - if id.is_empty() - || id.starts_with(|c: char| !c.is_ascii_alphabetic() && c != '_') - || id.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') - { - return (RcDoc::text(format!("_{}_", candid::idl_hash(id))), true); - } - let (is_rename, id) = if let Some(case) = case { - let new_id = id.to_case(case); - (new_id != id, new_id) - } else { - (false, id.to_owned()) - }; - if ["crate", "self", "super", "Self", "Result", "Principal"].contains(&id.as_str()) { - (RcDoc::text(format!("{id}_")), true) - } else if KEYWORDS.contains(&id.as_str()) { - (RcDoc::text(format!("r#{id}")), is_rename) - } else { - (RcDoc::text(id), is_rename) - } -} -fn ident(id: &str, case: Option) -> RcDoc { - ident_(id, case).0 -} - -fn pp_ty<'a>(ty: &'a Type, recs: &RecPoints) -> RcDoc<'a> { - use TypeInner::*; - match ty.as_ref() { - Null => str("()"), - Bool => str("bool"), - Nat => str("candid::Nat"), - Int => str("candid::Int"), - Nat8 => str("u8"), - Nat16 => str("u16"), - Nat32 => str("u32"), - Nat64 => str("u64"), - Int8 => str("i8"), - Int16 => str("i16"), - Int32 => str("i32"), - Int64 => str("i64"), - Float32 => str("f32"), - Float64 => str("f64"), - Text => str("String"), - Reserved => str("candid::Reserved"), - Empty => str("candid::Empty"), - Var(ref id) => { - let name = ident(id, Some(Case::Pascal)); - if recs.contains(id.as_str()) { - str("Box<").append(name).append(">") - } else { - name - } - } - Principal => str("Principal"), - Opt(ref t) => str("Option").append(enclose("<", pp_ty(t, recs), ">")), - // It's a bit tricky to use `deserialize_with = "serde_bytes"`. It's not working for `type t = blob` - Vec(ref t) if matches!(t.as_ref(), Nat8) => str("serde_bytes::ByteBuf"), - Vec(ref t) => str("Vec").append(enclose("<", pp_ty(t, recs), ">")), - Record(ref fs) => pp_record_fields(fs, recs, ""), - Variant(_) => unreachable!(), // not possible after rewriting - Func(_) => unreachable!(), // not possible after rewriting - Service(_) => unreachable!(), // not possible after rewriting - Class(_, _) => unreachable!(), - Knot(_) | Unknown | Future => unreachable!(), - } -} - -fn pp_label<'a>(id: &'a SharedLabel, is_variant: bool, vis: &'a str) -> RcDoc<'a> { - let vis = if vis.is_empty() { - RcDoc::nil() - } else { - kwd(vis) - }; - match &**id { - Label::Named(id) => { - let case = if is_variant { Some(Case::Pascal) } else { None }; - let (doc, is_rename) = ident_(id, case); - if is_rename { - str("#[serde(rename=\"") - .append(id.escape_debug().to_string()) - .append("\")]") - .append(RcDoc::line()) - .append(vis) - .append(doc) - } else { - vis.append(doc) - } - } - Label::Id(n) | Label::Unnamed(n) => vis.append("_").append(RcDoc::as_string(n)).append("_"), - } -} - -fn pp_record_field<'a>(field: &'a Field, recs: &RecPoints, vis: &'a str) -> RcDoc<'a> { - pp_label(&field.id, false, vis) - .append(kwd(":")) - .append(pp_ty(&field.ty, recs)) -} - -fn pp_record_fields<'a>(fs: &'a [Field], recs: &RecPoints, vis: &'a str) -> RcDoc<'a> { - if is_tuple(fs) { - let vis = if vis.is_empty() { - RcDoc::nil() - } else { - kwd(vis) - }; - let tuple = RcDoc::concat( - fs.iter() - .map(|f| vis.clone().append(pp_ty(&f.ty, recs)).append(",")), - ); - enclose("(", tuple, ")") - } else { - let fields = concat(fs.iter().map(|f| pp_record_field(f, recs, vis)), ","); - enclose_space("{", fields, "}") - } -} - -fn pp_variant_field<'a>(field: &'a Field, recs: &RecPoints) -> RcDoc<'a> { - match field.ty.as_ref() { - TypeInner::Null => pp_label(&field.id, true, ""), - TypeInner::Record(fs) => { - pp_label(&field.id, true, "").append(pp_record_fields(fs, recs, "")) - } - _ => pp_label(&field.id, true, "").append(enclose("(", pp_ty(&field.ty, recs), ")")), - } -} - -fn pp_variant_fields<'a>(fs: &'a [Field], recs: &RecPoints) -> RcDoc<'a> { - let fields = concat(fs.iter().map(|f| pp_variant_field(f, recs)), ","); - enclose_space("{", fields, "}") -} - -fn pp_defs<'a>(env: &'a TypeEnv, def_list: &'a [&'a str], recs: &'a RecPoints) -> RcDoc<'a> { - let derive = "#[derive(CandidType, Deserialize)]"; - lines(def_list.iter().map(|id| { - let ty = env.find_type(id).unwrap(); - let name = ident(id, Some(Case::Pascal)).append(" "); - let vis = "pub "; - match ty.as_ref() { - TypeInner::Record(fs) => { - let separator = if is_tuple(fs) { - RcDoc::text(";") - } else { - RcDoc::nil() - }; - str(derive) - .append(RcDoc::line()) - .append(vis) - .append("struct ") - .append(name) - .append(pp_record_fields(fs, recs, "pub")) - .append(separator) - .append(RcDoc::hardline()) - } - TypeInner::Variant(fs) => str(derive) - .append(RcDoc::line()) - .append(vis) - .append("enum ") - .append(name) - .append(pp_variant_fields(fs, recs)) - .append(RcDoc::hardline()), - TypeInner::Func(func) => str("candid::define_function!(") - .append(vis) - .append(name) - .append(": ") - .append(pp_ty_func(func)) - .append(");"), - TypeInner::Service(serv) => str("candid::define_service!(") - .append(vis) - .append(name) - .append(": ") - .append(pp_ty_service(serv)) - .append(");"), - _ => { - if recs.contains(id) { - str(derive) - .append(RcDoc::line()) - .append(vis) - .append("struct ") - .append(ident(id, Some(Case::Pascal))) - .append(enclose("(", pp_ty(ty, recs), ")")) - .append(";") - .append(RcDoc::hardline()) - } else { - str(vis) - .append(kwd("type")) - .append(name) - .append("= ") - .append(pp_ty(ty, recs)) - .append(";") - } - } - } - })) -} - -fn pp_args(args: &[Type]) -> RcDoc { - let empty = RecPoints::default(); - let doc = concat(args.iter().map(|t| pp_ty(t, &empty)), ","); - enclose("(", doc, ")") -} -fn pp_ty_func(f: &Function) -> RcDoc { - let args = pp_args(&f.args); - let rets = pp_args(&f.rets); - let modes = candid::pretty::candid::pp_modes(&f.modes); - args.append(" ->") - .append(RcDoc::space()) - .append(rets.append(modes)) - .nest(INDENT_SPACE) -} -fn pp_ty_service(serv: &[(String, Type)]) -> RcDoc { - let doc = concat( - serv.iter().map(|(id, func)| { - let func_doc = match func.as_ref() { - TypeInner::Func(ref f) => enclose("candid::func!(", pp_ty_func(f), ")"), - TypeInner::Var(_) => pp_ty(func, &RecPoints::default()).append("::ty()"), - _ => unreachable!(), - }; - RcDoc::text("\"") - .append(id) - .append(kwd("\" :")) - .append(func_doc) - }), - ";", - ); - enclose_space("{", doc, "}") -} - -fn pp_function<'a>(config: &Config, id: &'a str, func: &'a Function) -> RcDoc<'a> { - let name = ident(id, Some(Case::Snake)); - let empty = BTreeSet::new(); - let arg_prefix = str(match config.target { - Target::Consumer => "&self", - Target::Provider => unimplemented!(), - Target::Type => unimplemented!(), - }); - let args = concat( - std::iter::once(arg_prefix).chain( - func.args - .iter() - .enumerate() - .map(|(i, ty)| RcDoc::as_string(format!("arg{i}: ")).append(pp_ty(ty, &empty))), - ), - ",", - ); - let rets = match config.target { - Target::Consumer => enclose( - "(", - RcDoc::concat(func.rets.iter().map(|ty| pp_ty(ty, &empty).append(","))), - ")", - ), - Target::Provider => unimplemented!(), - Target::Type => unimplemented!(), - }; - let sig = kwd("pub async fn") - .append(name) - .append(enclose("(", args, ")")) - .append(kwd(" ->")) - .append(enclose("Result<", rets, "> ")); - let method = id.escape_debug().to_string(); - let body = match config.target { - Target::Consumer => { - let args = RcDoc::concat((0..func.args.len()).map(|i| RcDoc::text(format!("arg{i},")))); - str("ic_cdk::call(self.0, \"") - .append(method) - .append("\", ") - .append(enclose("(", args, ")")) - .append(").await") - } - Target::Provider => unimplemented!(), - Target::Type => unimplemented!(), - }; - sig.append(enclose_space("{", body, "}")) -} - -fn pp_actor<'a>(config: &'a Config, env: &'a TypeEnv, actor: &'a Type) -> RcDoc<'a> { - // TODO: currently we only generate actor for consumer - if matches!(config.target, Target::Type | Target::Provider) { - return RcDoc::nil(); - } - let serv = env.as_service(actor).unwrap(); - let body = RcDoc::intersperse( - serv.iter().map(|(id, func)| { - let func = env.as_func(func).unwrap(); - pp_function(config, id, func) - }), - RcDoc::hardline(), - ); - let struct_name = config.service_name.to_case(Case::Pascal); - let service_def = match config.target { - Target::Consumer => format!("pub struct {}(pub Principal);", struct_name), - Target::Provider => unimplemented!(), - Target::Type => unimplemented!(), - }; - let service_impl = match config.target { - Target::Consumer => format!("impl {} ", struct_name), - Target::Provider => unimplemented!(), - Target::Type => unimplemented!(), - }; - let res = RcDoc::text(service_def) - .append(RcDoc::hardline()) - .append(service_impl) - .append(enclose_space("{", body, "}")) - .append(RcDoc::hardline()); - if let Some(cid) = config.canister_id { - let slice = cid - .as_slice() - .iter() - .map(|b| b.to_string()) - .collect::>() - .join(", "); - let id = RcDoc::text(format!( - "pub const CANISTER_ID : Principal = Principal::from_slice(&[{}]); // {}", - slice, cid - )); - let instance = match config.target { - Target::Consumer => format!( - "#[allow(non_upper_case_globals)]\npub const {} : {} = {}(CANISTER_ID);", - config.service_name, struct_name, struct_name - ), - Target::Provider => unimplemented!(), - Target::Type => unimplemented!(), - }; - res.append(id).append(RcDoc::hardline()).append(instance) - } else { - res - } -} - -pub fn compile(config: &Config, env: &TypeEnv, actor: &Option) -> String { - let header = r#"// This is an experimental feature to generate Rust binding from Candid. -// You may want to manually adjust some of the types. -#[allow(unused_imports)] -use candid::{{self, CandidType, Deserialize, Principal, Encode, Decode}}; -"# - .to_string(); - let header = header - + match &config.target { - Target::Consumer => "use ic_cdk::api::call::CallResult as Result;\n", - Target::Provider => "", - Target::Type => "", - }; - - let (env, actor) = nominalize_all(env, actor); - let def_list: Vec<_> = if let Some(actor) = &actor { - chase_actor(&env, actor).unwrap() - } else { - env.0.iter().map(|pair| pair.0.as_ref()).collect() - }; - let recs = infer_rec(&env, &def_list).unwrap(); - - let defs = pp_defs(&env, &def_list, &recs); - - let doc = match config.target { - Target::Consumer => { - if let Some(actor) = &actor { - let actor = pp_actor(config, &env, actor); - defs.append(actor) - } else { - defs - } - } - Target::Provider => defs, - Target::Type => defs, - }; - let doc = RcDoc::text(header).append(RcDoc::line()).append(doc); - doc.pretty(LINE_WIDTH).to_string() -} - -pub enum TypePath { - Id(String), - Opt, - Vec, - RecordField(String), - VariantField(String), - Func(String), - Init, -} -fn path_to_var(path: &[TypePath]) -> String { - let name: Vec<&str> = path - .iter() - .map(|node| match node { - TypePath::Id(id) => id.as_str(), - TypePath::RecordField(f) | TypePath::VariantField(f) => f.as_str(), - TypePath::Opt => "inner", - TypePath::Vec => "item", - TypePath::Func(id) => id.as_str(), - TypePath::Init => "init", - }) - .collect(); - name.join("_").to_case(Case::Pascal) -} -// Convert structural typing to nominal typing to fit Rust's type system -fn nominalize(env: &mut TypeEnv, path: &mut Vec, t: &Type) -> Type { - match t.as_ref() { - TypeInner::Opt(ty) => { - path.push(TypePath::Opt); - let ty = nominalize(env, path, ty); - path.pop(); - TypeInner::Opt(ty) - } - TypeInner::Vec(ty) => { - path.push(TypePath::Vec); - let ty = nominalize(env, path, ty); - path.pop(); - TypeInner::Vec(ty) - } - TypeInner::Record(fs) => { - if matches!( - path.last(), - None | Some(TypePath::VariantField(_)) | Some(TypePath::Id(_)) - ) || is_tuple(fs) - { - let fs: Vec<_> = fs - .iter() - .map(|Field { id, ty }| { - path.push(TypePath::RecordField(id.to_string())); - let ty = nominalize(env, path, ty); - path.pop(); - Field { id: id.clone(), ty } - }) - .collect(); - TypeInner::Record(fs) - } else { - let new_var = path_to_var(path); - let ty = nominalize( - env, - &mut vec![TypePath::Id(new_var.clone())], - &TypeInner::Record(fs.to_vec()).into(), - ); - env.0.insert(new_var.clone(), ty); - TypeInner::Var(new_var) - } - } - TypeInner::Variant(fs) => match path.last() { - None | Some(TypePath::Id(_)) => { - let fs: Vec<_> = fs - .iter() - .map(|Field { id, ty }| { - path.push(TypePath::VariantField(id.to_string())); - let ty = nominalize(env, path, ty); - path.pop(); - Field { id: id.clone(), ty } - }) - .collect(); - TypeInner::Variant(fs) - } - Some(_) => { - let new_var = path_to_var(path); - let ty = nominalize( - env, - &mut vec![TypePath::Id(new_var.clone())], - &TypeInner::Variant(fs.to_vec()).into(), - ); - env.0.insert(new_var.clone(), ty); - TypeInner::Var(new_var) - } - }, - TypeInner::Func(func) => match path.last() { - None | Some(TypePath::Id(_)) => { - let func = func.clone(); - TypeInner::Func(Function { - modes: func.modes, - args: func - .args - .into_iter() - .enumerate() - .map(|(i, ty)| { - let i = if i == 0 { - "".to_string() - } else { - i.to_string() - }; - path.push(TypePath::Func(format!("arg{i}"))); - let ty = nominalize(env, path, &ty); - path.pop(); - ty - }) - .collect(), - rets: func - .rets - .into_iter() - .enumerate() - .map(|(i, ty)| { - let i = if i == 0 { - "".to_string() - } else { - i.to_string() - }; - path.push(TypePath::Func(format!("ret{i}"))); - let ty = nominalize(env, path, &ty); - path.pop(); - ty - }) - .collect(), - }) - } - Some(_) => { - let new_var = path_to_var(path); - let ty = nominalize( - env, - &mut vec![TypePath::Id(new_var.clone())], - &TypeInner::Func(func.clone()).into(), - ); - env.0.insert(new_var.clone(), ty); - TypeInner::Var(new_var) - } - }, - TypeInner::Service(serv) => match path.last() { - None | Some(TypePath::Id(_)) => TypeInner::Service( - serv.iter() - .map(|(meth, ty)| { - path.push(TypePath::Id(meth.to_string())); - let ty = nominalize(env, path, ty); - path.pop(); - (meth.clone(), ty) - }) - .collect(), - ), - Some(_) => { - let new_var = path_to_var(path); - let ty = nominalize( - env, - &mut vec![TypePath::Id(new_var.clone())], - &TypeInner::Service(serv.clone()).into(), - ); - env.0.insert(new_var.clone(), ty); - TypeInner::Var(new_var) - } - }, - TypeInner::Class(args, ty) => TypeInner::Class( - args.iter() - .map(|ty| { - path.push(TypePath::Init); - let ty = nominalize(env, path, ty); - path.pop(); - ty - }) - .collect(), - nominalize(env, path, ty), - ), - _ => return t.clone(), - } - .into() -} - -fn nominalize_all(env: &TypeEnv, actor: &Option) -> (TypeEnv, Option) { - let mut res = TypeEnv(Default::default()); - for (id, ty) in env.0.iter() { - let ty = nominalize(&mut res, &mut vec![TypePath::Id(id.clone())], ty); - res.0.insert(id.to_string(), ty); - } - let actor = actor - .as_ref() - .map(|ty| nominalize(&mut res, &mut vec![], ty)); - (res, actor) -} diff --git a/src/ic-cdk-bindgen/src/lib.rs b/src/ic-cdk-bindgen/src/lib.rs index be9faed8e..e93981798 100644 --- a/src/ic-cdk-bindgen/src/lib.rs +++ b/src/ic-cdk-bindgen/src/lib.rs @@ -9,10 +9,8 @@ use std::io::Write; use std::path::PathBuf; use std::str::FromStr; -mod code_generator; mod error; -use code_generator::Target; pub use error::IcCdkBindgenError; type Result = std::result::Result; @@ -81,9 +79,17 @@ impl Builder { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Target { + Consumer, + Provider, + Type, +} + // Code generation. impl Builder { fn generate(&self, target: Target) -> Result<()> { + // 1. Parse the candid file and generate the Output (the struct for bindings) let config = Config::new(Configs::from_str("").unwrap()); let candid_path = if let Some(p) = &self.candid_path { p.clone() @@ -96,12 +102,12 @@ impl Builder { // TODO: handle unused. assert!(unused.is_empty()); + // 2. Generate the Rust bindings using the Handlebars template let template = match target { Target::Consumer => include_str!("templates/consumer.hbs"), Target::Provider => include_str!("templates/provider.hbs"), Target::Type => include_str!("templates/type.hbs"), }; - let mut external = ExternalConfig::default(); if target == Target::Consumer { let canister_id = if let Some(p) = &self.canister_id { @@ -116,9 +122,9 @@ impl Builder { external .0 .insert("service_name".to_string(), self.canister_name.to_string()); - let content = output_handlebar(output, external, template); + // 3. Write the generated Rust bindings to the output directory let out_dir = if let Some(p) = &self.out_dir { p.clone() } else { From 94d6e4554af57db4dd42aa515e0e7f0315f61a38 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 22 Jul 2024 14:39:32 -0400 Subject: [PATCH 5/6] cleanup templates --- src/ic-cdk-bindgen/src/templates/consumer.hbs | 4 +++- src/ic-cdk-bindgen/src/templates/provider.hbs | 22 ++----------------- src/ic-cdk-bindgen/src/templates/type.hbs | 22 ++----------------- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/ic-cdk-bindgen/src/templates/consumer.hbs b/src/ic-cdk-bindgen/src/templates/consumer.hbs index cca130e00..6294e8a14 100644 --- a/src/ic-cdk-bindgen/src/templates/consumer.hbs +++ b/src/ic-cdk-bindgen/src/templates/consumer.hbs @@ -1,4 +1,5 @@ -#[allow(unused_imports)] +#[allow(dead_code, unused_imports)] + use {{candid_crate}}::{self, CandidType, Deserialize, Principal}; use ic_cdk::api::call::CallResult as Result; @@ -14,6 +15,7 @@ impl {{PascalCase service_name}} { } {{#if canister_id}} pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); // {{canister_id}} +#[allow(non_upper_case_globals)] pub const {{snake_case service_name}} : {{PascalCase service_name}} = {{PascalCase service_name}}(CANISTER_ID); {{/if}} {{/if}} diff --git a/src/ic-cdk-bindgen/src/templates/provider.hbs b/src/ic-cdk-bindgen/src/templates/provider.hbs index 92f5daadc..f9b9c9efa 100644 --- a/src/ic-cdk-bindgen/src/templates/provider.hbs +++ b/src/ic-cdk-bindgen/src/templates/provider.hbs @@ -1,23 +1,5 @@ -#[allow(unused_imports)] +#[allow(dead_code, unused_imports)] + use {{candid_crate}}::{self, CandidType, Deserialize, Principal}; {{type_defs}} -{{#if init_args}} -#[ic_cdk::init] -fn init({{#each init_args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{/each}}) { - unimplemented!() -} -{{/if}} -{{#each methods}} -#[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!() -} -{{/each}} -{{#if tests}} -{{tests}} -{{/if}} -{{#if metadata}} -#[link_section = "icp:public candid:service"] -pub static __SERVICE: [u8; {{len metadata}}] = *br#"{{metadata}}"#; -{{/if}} diff --git a/src/ic-cdk-bindgen/src/templates/type.hbs b/src/ic-cdk-bindgen/src/templates/type.hbs index 92f5daadc..f9b9c9efa 100644 --- a/src/ic-cdk-bindgen/src/templates/type.hbs +++ b/src/ic-cdk-bindgen/src/templates/type.hbs @@ -1,23 +1,5 @@ -#[allow(unused_imports)] +#[allow(dead_code, unused_imports)] + use {{candid_crate}}::{self, CandidType, Deserialize, Principal}; {{type_defs}} -{{#if init_args}} -#[ic_cdk::init] -fn init({{#each init_args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{/each}}) { - unimplemented!() -} -{{/if}} -{{#each methods}} -#[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!() -} -{{/each}} -{{#if tests}} -{{tests}} -{{/if}} -{{#if metadata}} -#[link_section = "icp:public candid:service"] -pub static __SERVICE: [u8; {{len metadata}}] = *br#"{{metadata}}"#; -{{/if}} From 79dedc60adadf03cf328a4d4c3effeae1132ec1a Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 22 Jul 2024 14:44:50 -0400 Subject: [PATCH 6/6] rm unused deps --- Cargo.lock | 2 -- src/ic-cdk-bindgen/Cargo.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 173998fe9..93eb9d023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,8 +1154,6 @@ version = "0.1.3" dependencies = [ "candid", "candid_parser", - "convert_case", - "pretty", "thiserror", ] diff --git a/src/ic-cdk-bindgen/Cargo.toml b/src/ic-cdk-bindgen/Cargo.toml index e0e504d70..38fbd7887 100644 --- a/src/ic-cdk-bindgen/Cargo.toml +++ b/src/ic-cdk-bindgen/Cargo.toml @@ -15,6 +15,4 @@ include = ["src", "Cargo.toml", "LICENSE", "README.md"] [dependencies] candid.workspace = true candid_parser.workspace = true -convert_case = "0.6" -pretty = "0.12" thiserror = "1.0"