From 223fe03b539d2a8ed1f62f8d1299858df8fee6cd Mon Sep 17 00:00:00 2001 From: Yan Chen <48968912+chenyan-dfinity@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:50:46 -0700 Subject: [PATCH] more improvements for Rust bindgen (#558) * add clone for configs * add open_path * report unused * add stub * add rust parser to extract cdk functions; didc to take external.rust section * fix mode * refactor to report span * checkpoint, use codespan_reporting * good * fix * move rust_check to cdk * bump version * add def_use analysis * add custom target for didc * readme * generate only subset of bindings * s/method/func/ * checkpoint * checkpoint * checkpoint * use context * fix * add config_source * check unused at the property level * no scoped path * specialize result type * fix * prompt color * add use_type test * update tests * bump version * fix --- Cargo.lock | 5 +- Changelog.md | 17 +- rust/candid/Cargo.toml | 3 +- rust/candid/src/pretty/candid.rs | 3 + rust/candid/tests/serde.rs | 18 +- rust/candid_parser/Cargo.toml | 6 +- rust/candid_parser/src/bindings/analysis.rs | 54 +- rust/candid_parser/src/bindings/rust.rs | 523 ++++++++++++------ .../candid_parser/src/bindings/rust_agent.hbs | 3 + rust/candid_parser/src/bindings/rust_call.hbs | 3 + rust/candid_parser/src/bindings/rust_stub.hbs | 3 + rust/candid_parser/src/configs.rs | 169 ++++-- rust/candid_parser/src/random.rs | 136 ++++- rust/candid_parser/src/utils.rs | 16 + rust/candid_parser/tests/assets/example.did | 5 +- rust/candid_parser/tests/assets/example.toml | 1 + .../tests/assets/ok/example.d.ts | 14 +- .../candid_parser/tests/assets/ok/example.did | 9 +- rust/candid_parser/tests/assets/ok/example.js | 16 +- rust/candid_parser/tests/assets/ok/example.mo | 12 +- rust/candid_parser/tests/assets/ok/example.rs | 32 +- tools/didc/src/main.rs | 9 +- 22 files changed, 787 insertions(+), 270 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c2e8e51..832aa886 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,13 +180,14 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "candid" -version = "0.10.8" +version = "0.10.9" dependencies = [ "anyhow", "bincode", "binread", "byteorder", "candid_derive", + "candid_parser", "hex", "ic_principal", "leb128", @@ -215,7 +216,7 @@ dependencies = [ [[package]] name = "candid_parser" -version = "0.2.0-beta.1" +version = "0.2.0-beta.2" dependencies = [ "anyhow", "arbitrary", diff --git a/Changelog.md b/Changelog.md index ddca135f..0114116f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,30 +3,25 @@ ## 2024-05-03 -### candid_parser 0.2.0-beta.0 +### candid_parser 0.2.0-beta * Breaking changes: + Rewrite `configs` and `random` modules, adapting TOML format as the parser. `configs` module is no longer under a feature flag, and no longer depend on dhall. + Rewrite Rust bindgen to use the new `configs` module. Use `emit_bindgen` to generate type bindings, and use `output_handlebar` to provide a handlebar template for the generated module. `compile` function provides a default template. The generated file without config is exactly the same as before. - -* TO-DOs - + Spec for path and matching semantics - + Warning for unused path - + Rust bindgen: - * Generate `use_type` tests. How to handle recursive types? - * Threading state through `nominalize` - * When the path starts with method, duplicate type definition when necessary. - * Number label renaming +* Non-breaking changes: + + `utils::check_rust_type` function to check if a Rust type implements the provided candid type. ## 2024-04-11 -### Candid 0.10.5 -- 0.10.8 +### Candid 0.10.5 -- 0.10.9 * Switch `HashMap` to `BTreeMap` in serialization and `T::ty()`. This leads to around 20% perf improvement for serializing complicated types. * Disable memoization for unrolled types in serialization to save cycle cost. In some cases, type table can get slightly larger, but it's worth the trade off. * Fix bug in `text_size` * Fix decoding cost calculation overflow * Fix length check in decoding principal type +* Implement `CandidType` for `serde_bytes::ByteArray` +* Add `pretty::candid::pp_init_args` function to pretty print init args ## 2024-02-27 diff --git a/rust/candid/Cargo.toml b/rust/candid/Cargo.toml index 6a61a33b..326d7bac 100644 --- a/rust/candid/Cargo.toml +++ b/rust/candid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "candid" -version = "0.10.8" +version = "0.10.9" edition = "2021" rust-version.workspace = true authors = ["DFINITY Team"] @@ -39,6 +39,7 @@ rand.workspace = true serde_cbor = "0.11.2" serde_json = "1.0.74" bincode = "1.3.3" +candid_parser = { path = "../candid_parser" } [features] bignum = ["dep:num-bigint", "dep:num-traits"] diff --git a/rust/candid/src/pretty/candid.rs b/rust/candid/src/pretty/candid.rs index c012da9d..5f67607d 100644 --- a/rust/candid/src/pretty/candid.rs +++ b/rust/candid/src/pretty/candid.rs @@ -205,6 +205,9 @@ fn pp_actor(ty: &Type) -> RcDoc { } } +pub fn pp_init_args<'a>(env: &'a TypeEnv, args: &'a [Type]) -> RcDoc<'a> { + pp_defs(env).append(pp_args(args)) +} pub fn compile(env: &TypeEnv, actor: &Option) -> String { match actor { None => pp_defs(env).pretty(LINE_WIDTH).to_string(), diff --git a/rust/candid/tests/serde.rs b/rust/candid/tests/serde.rs index f42133a9..87113a46 100644 --- a/rust/candid/tests/serde.rs +++ b/rust/candid/tests/serde.rs @@ -2,6 +2,7 @@ use candid::{ decode_one_with_config, encode_one, CandidType, Decode, DecoderConfig, Deserialize, Encode, Int, Nat, }; +use candid_parser::utils::check_rust_type; #[test] fn test_error() { @@ -263,6 +264,8 @@ fn test_struct() { // with memoization on the unrolled type, type table will have 2 entries. // all_check(list, "4449444c026e016c02a0d2aca8047c90eddae70400010000"); all_check(list, "4449444c036e016c02a0d2aca8047c90eddae704026e01010000"); + check_rust_type::("type List = record {head: int; tail: opt List}; (List)").unwrap(); + check_rust_type::("type T = record {head: int; tail: opt T}; (T)").unwrap(); } #[test] @@ -289,6 +292,10 @@ fn optional_fields() { bar: bool, baz: Option, } + check_rust_type::( + "(record { foo: opt nat8; bar: bool; baz: opt variant { Foo; Bar: bool; Baz: bool }})", + ) + .unwrap(); let bytes = encode(&OldStruct { bar: true, baz: Some(Old::Foo), @@ -334,6 +341,13 @@ fn test_equivalent_types() { struct TypeB { typea: Option, } + check_rust_type::( + r#" +type A = record { typeb: opt B }; +type B = record { typea: opt A }; +(record { typeas: vec A })"#, + ) + .unwrap(); // Encode to the following types leads to equivalent but different representations of TypeA all_check( RootType { typeas: Vec::new() }, @@ -569,6 +583,7 @@ fn test_tuple() { (Int::from(42), "💩".to_string()), "4449444c016c02007c017101002a04f09f92a9", ); + check_rust_type::<(Int, String)>("(record {int; text})").unwrap(); let none: Option = None; let bytes = hex("4449444c046c04007c017e020103026d7c6e036c02a0d2aca8047c90eddae7040201002b010302030400"); @@ -677,6 +692,7 @@ fn test_field_rename() { #[serde(rename = "a-b")] B(u8), } + check_rust_type::(r#"(variant { "1-2 + 3": int8; "a-b": nat8 })"#).unwrap(); all_check(E2::A(42), "4449444c016b02b684a7027bb493ee970d770100012a"); #[derive(CandidType, Deserialize, PartialEq, Debug)] struct S2 { @@ -698,7 +714,7 @@ fn test_generics() { g1: T, g2: E, } - + check_rust_type::>("(record {g1: int32; g2: bool})").unwrap(); let res = G { g1: Int::from(42), g2: true, diff --git a/rust/candid_parser/Cargo.toml b/rust/candid_parser/Cargo.toml index 77051a3a..ecf6052c 100644 --- a/rust/candid_parser/Cargo.toml +++ b/rust/candid_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "candid_parser" -version = "0.2.0-beta.1" +version = "0.2.0-beta.2" edition = "2021" rust-version.workspace = true authors = ["DFINITY Team"] @@ -33,13 +33,13 @@ logos = "0.14" convert_case = "0.6" handlebars = "5.1" toml = { version = "0.8", default-features = false, features = ["parse"] } +console = "0.15" arbitrary = { workspace = true, optional = true } fake = { version = "2.4", optional = true } rand = { version = "0.8", optional = true } num-traits = { workspace = true, optional = true } dialoguer = { version = "0.11", default-features = false, features = ["editor", "completion"], optional = true } -console = { version = "0.15", optional = true } ctrlc = { version = "3.4", optional = true } [dev-dependencies] @@ -49,7 +49,7 @@ rand.workspace = true [features] random = ["dep:arbitrary", "dep:fake", "dep:rand", "dep:num-traits"] -assist = ["dep:dialoguer", "dep:console", "dep:ctrlc"] +assist = ["dep:dialoguer", "dep:ctrlc"] all = ["random", "assist"] # docs.rs-specific configuration diff --git a/rust/candid_parser/src/bindings/analysis.rs b/rust/candid_parser/src/bindings/analysis.rs index 6d8f717c..8a133a69 100644 --- a/rust/candid_parser/src/bindings/analysis.rs +++ b/rust/candid_parser/src/bindings/analysis.rs @@ -1,6 +1,17 @@ use crate::Result; use candid::types::{Type, TypeEnv, TypeInner}; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; + +/// Select a subset of methods from an actor. +pub fn project_methods(env: &TypeEnv, actor: &Option, methods: &[String]) -> Option { + let service = env.as_service(actor.as_ref()?).ok()?; + let filtered = service + .iter() + .filter(|(name, _)| methods.contains(name)) + .cloned() + .collect(); + Some(TypeInner::Service(filtered).into()) +} /// Same as chase_actor, with seen set as part of the type. Used for chasing type names from type definitions. pub fn chase_type<'a>( @@ -53,6 +64,47 @@ pub fn chase_actor<'a>(env: &'a TypeEnv, actor: &'a Type) -> Result chase_type(&mut seen, &mut res, env, actor)?; Ok(res) } +/// Given an actor, return a map from variable names to the (methods, arg) that use them. +pub fn chase_def_use<'a>( + env: &'a TypeEnv, + actor: &'a Type, +) -> Result>> { + let mut res = BTreeMap::new(); + let actor = env.trace_type(actor)?; + if let TypeInner::Class(args, _) = actor.as_ref() { + for (i, arg) in args.iter().enumerate() { + let mut used = Vec::new(); + chase_type(&mut BTreeSet::new(), &mut used, env, arg)?; + for var in used { + res.entry(var.to_string()) + .or_insert_with(Vec::new) + .push(format!("init.arg{}", i)); + } + } + } + for (id, ty) in env.as_service(&actor)? { + let func = env.as_func(ty)?; + for (i, arg) in func.args.iter().enumerate() { + let mut used = Vec::new(); + chase_type(&mut BTreeSet::new(), &mut used, env, arg)?; + for var in used { + res.entry(var.to_string()) + .or_insert_with(Vec::new) + .push(format!("{}.arg{}", id, i)); + } + } + for (i, arg) in func.rets.iter().enumerate() { + let mut used = Vec::new(); + chase_type(&mut BTreeSet::new(), &mut used, env, arg)?; + for var in used { + res.entry(var.to_string()) + .or_insert_with(Vec::new) + .push(format!("{}.ret{}", id, i)); + } + } + } + Ok(res) +} pub fn chase_types<'a>(env: &'a TypeEnv, tys: &'a [Type]) -> Result> { let mut seen = BTreeSet::new(); diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index 3e66a2f0..facad992 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -1,10 +1,11 @@ use super::analysis::{chase_actor, infer_rec}; use crate::{ - configs::{ConfigState, ConfigTree, Configs, StateElem}, + configs::{ConfigState, ConfigTree, Configs, Context, StateElem}, Deserialize, }; use candid::pretty::utils::*; use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; +use console::style; use convert_case::{Case, Casing}; use pretty::RcDoc; use serde::Serialize; @@ -19,34 +20,65 @@ pub struct BindingConfig { visibility: Option, } impl ConfigState for BindingConfig { - fn merge_config(&mut self, config: &Self, elem: Option<&StateElem>, _is_recursive: bool) { + fn merge_config(&mut self, config: &Self, ctx: Option) -> Vec { + let mut res = Vec::new(); self.name.clone_from(&config.name); + res.push("name"); // match use_type can survive across types, so that label.use_type works - if !matches!(elem, Some(StateElem::Label(_))) { + if ctx + .as_ref() + .is_some_and(|ctx| matches!(ctx.elem, StateElem::Type(_) | StateElem::TypeStr(_))) + { if let Some(use_type) = &config.use_type { self.use_type = Some(use_type.clone()); + res.push("use_type"); } } else { self.use_type.clone_from(&config.use_type); + res.push("use_type"); } // matched attributes can survive across labels, so that record.attributes works - if matches!(elem, Some(StateElem::Label(_))) { + if ctx + .as_ref() + .is_some_and(|ctx| matches!(ctx.elem, StateElem::Label(_))) + { if let Some(attr) = &config.attributes { self.attributes = Some(attr.clone()); + res.push("attributes"); } } else { self.attributes.clone_from(&config.attributes); + res.push("attributes"); } if config.visibility.is_some() { self.visibility.clone_from(&config.visibility); + res.push("visibility"); } + res.into_iter().map(|f| f.to_string()).collect() } fn update_state(&mut self, _elem: &StateElem) {} fn restore_state(&mut self, _elem: &StateElem) {} + fn list_properties(&self) -> Vec { + let mut res = Vec::new(); + if self.name.is_some() { + res.push("name"); + } + if self.use_type.is_some() { + res.push("use_type"); + } + if self.attributes.is_some() { + res.push("attributes"); + } + if self.visibility.is_some() { + res.push("visibility"); + } + res.into_iter().map(|f| f.to_string()).collect() + } } -pub struct State<'a> { +struct State<'a> { state: crate::configs::State<'a, BindingConfig>, recs: RecPoints<'a>, + tests: BTreeMap, } type RecPoints<'a> = BTreeSet<&'a str>; @@ -59,6 +91,17 @@ pub(crate) fn is_tuple(fs: &[Field]) -> bool { .enumerate() .any(|(i, field)| field.id.get_id() != (i as u32)) } +fn as_result(fs: &[Field]) -> Option<(&Type, &Type)> { + match fs { + [Field { id: ok, ty: t_ok }, Field { id: err, ty: t_err }] + if **ok == Label::Named("Ok".to_string()) + && **err == Label::Named("Err".to_string()) => + { + Some((t_ok, t_err)) + } + _ => None, + } +} 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", @@ -97,13 +140,46 @@ fn pp_vis<'a>(vis: &Option) -> RcDoc<'a> { None => RcDoc::text("pub "), } } + impl<'a> State<'a> { + fn generate_test(&mut self, src: &Type, use_type: &str) { + if self.tests.contains_key(use_type) { + return; + } + let def_list = chase_actor(self.state.env, src).unwrap(); + let env = TypeEnv( + self.state + .env + .0 + .iter() + .filter(|(k, _)| def_list.contains(&k.as_str())) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + ); + let src = candid::pretty::candid::pp_init_args(&env, &[src.clone()]) + .pretty(80) + .to_string(); + let match_path = self.state.config_source.get("use_type").unwrap().join("."); + let test_name = use_type.replace(|c: char| !c.is_ascii_alphanumeric(), "_"); + let body = format!( + r##"#[test] +fn test_{test_name}() {{ + // Generated from {match_path}.use_type = "{use_type}" + let candid_src = r#"{src}"#; + candid_parser::utils::check_rust_type::<{use_type}>(candid_src).unwrap(); +}}"## + ); + self.tests.insert(use_type.to_string(), body); + } fn pp_ty<'b>(&mut self, ty: &'b Type, is_ref: bool) -> RcDoc<'b> { use TypeInner::*; let elem = StateElem::Type(ty); let old = self.state.push_state(&elem); let res = if let Some(t) = &self.state.config.use_type { - RcDoc::text(t.clone()) + let res = RcDoc::text(t.clone()); + self.generate_test(ty, &t.clone()); + self.state.update_stats("use_type"); + res } else { match ty.as_ref() { Null => str("()"), @@ -125,7 +201,9 @@ impl<'a> State<'a> { Empty => str("candid::Empty"), Var(ref id) => { let name = if let Some(name) = &self.state.config.name { - RcDoc::text(name.clone()) + let res = RcDoc::text(name.clone()); + self.state.update_stats("name"); + res } else { ident(id, Some(Case::Pascal)) }; @@ -141,8 +219,16 @@ impl<'a> State<'a> { Vec(ref t) if matches!(t.as_ref(), Nat8) => str("serde_bytes::ByteBuf"), Vec(ref t) => str("Vec").append(enclose("<", self.pp_ty(t, is_ref), ">")), Record(ref fs) => self.pp_record_fields(fs, false, is_ref), - Variant(_) => unreachable!(), // not possible after rewriting - Func(_) => unreachable!(), // not possible after rewriting + Variant(ref fs) => { + // only possible for result variant + let (ok, err) = as_result(fs).unwrap(); + let body = self + .pp_ty(ok, is_ref) + .append(", ") + .append(self.pp_ty(err, is_ref)); + str("std::result::Result").append(enclose("<", body, ">")) + } + Func(_) => unreachable!(), // not possible after rewriting Service(_) => unreachable!(), // not possible after rewriting Class(_, _) => unreachable!(), Knot(_) | Unknown | Future => unreachable!(), @@ -153,6 +239,7 @@ impl<'a> State<'a> { } fn pp_label<'b>(&mut self, id: &'b SharedLabel, is_variant: bool, need_vis: bool) -> RcDoc<'b> { let vis = if need_vis { + self.state.update_stats("visibility"); pp_vis(&self.state.config.visibility) } else { RcDoc::nil() @@ -164,10 +251,13 @@ impl<'a> State<'a> { .clone() .map(|s| RcDoc::text(s).append(RcDoc::line())) .unwrap_or(RcDoc::nil()); + self.state.update_stats("attributes"); match &**id { Label::Named(id) => { let (doc, is_rename) = if let Some(name) = &self.state.config.name { - (RcDoc::text(name.clone()), true) + let res = (RcDoc::text(name.clone()), true); + self.state.update_stats("name"); + res } else { let case = if is_variant { Some(Case::Pascal) } else { None }; ident_(id, case) @@ -193,6 +283,7 @@ impl<'a> State<'a> { let lab = i.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); let vis = if need_vis { + self.state.update_stats("visibility"); pp_vis(&self.state.config.visibility) } else { RcDoc::nil() @@ -270,6 +361,9 @@ impl<'a> State<'a> { .clone() .map(RcDoc::text) .unwrap_or_else(|| ident(id, Some(Case::Pascal))); + self.state.update_stats("name"); + self.state.update_stats("visibility"); + self.state.update_stats("attributes"); let vis = pp_vis(&self.state.config.visibility); let derive = self .state @@ -294,13 +388,23 @@ impl<'a> State<'a> { .append(self.pp_record_fields(fs, true, false)) .append(separator) } - TypeInner::Variant(fs) => derive - .append(RcDoc::line()) - .append(vis) - .append("enum ") - .append(name) - .append(" ") - .append(self.pp_variant_fields(fs)), + TypeInner::Variant(fs) => { + if as_result(fs).is_some() { + vis.append(kwd("type")) + .append(name) + .append(" = ") + .append(self.pp_ty(ty, false)) + .append(";") + } else { + derive + .append(RcDoc::line()) + .append(vis) + .append("enum ") + .append(name) + .append(" ") + .append(self.pp_variant_fields(fs)) + } + } TypeInner::Func(func) => str("candid::define_function!(") .append(vis) .append(name) @@ -396,6 +500,7 @@ impl<'a> State<'a> { .name .clone() .unwrap_or_else(|| ident(id, Some(Case::Snake)).pretty(LINE_WIDTH).to_string()); + self.state.update_stats("name"); let args: Vec<_> = func .args .iter() @@ -409,6 +514,7 @@ impl<'a> State<'a> { .name .clone() .unwrap_or_else(|| lab.clone()); + self.state.update_stats("name"); let res = self.pp_ty(ty, true); self.state.pop_state(old, StateElem::Label(&lab)); (name, res) @@ -465,6 +571,7 @@ impl<'a> State<'a> { .name .clone() .unwrap_or_else(|| lab.clone()); + self.state.update_stats("name"); let res = self.pp_ty(ty, true); self.state.pop_state(old, StateElem::Label(&lab)); (name, res.pretty(LINE_WIDTH).to_string()) @@ -489,6 +596,7 @@ pub struct Output { pub type_defs: String, pub methods: Vec, pub init_args: Option>, + pub tests: String, } #[derive(Serialize, Debug)] pub struct Method { @@ -499,29 +607,37 @@ pub struct Method { pub mode: String, } pub fn emit_bindgen(tree: &Config, env: &TypeEnv, actor: &Option) -> (Output, Vec) { - let (env, actor) = nominalize_all(env, actor); - let def_list: Vec<_> = if let Some(actor) = &actor { + let mut state = NominalState { + state: crate::configs::State::new(&tree.0, env), + }; + let (env, actor) = state.nominalize_all(actor); + let old_stats = state.state.stats.clone(); + let def_list = if let Some(actor) = &actor { chase_actor(&env, actor).unwrap() } else { - env.0.iter().map(|pair| pair.0.as_ref()).collect() + env.0.iter().map(|pair| pair.0.as_ref()).collect::>() }; let recs = infer_rec(&env, &def_list).unwrap(); let mut state = State { state: crate::configs::State::new(&tree.0, &env), recs, + tests: BTreeMap::new(), }; + state.state.stats = old_stats; let defs = state.pp_defs(&def_list); let (methods, init_args) = if let Some(actor) = &actor { state.pp_actor(actor) } else { (Vec::new(), None) }; + let tests = state.tests.into_values().collect::>().join("\n"); let unused = state.state.report_unused(); ( Output { type_defs: defs.pretty(LINE_WIDTH).to_string(), methods, init_args, + tests, }, unused, ) @@ -535,12 +651,14 @@ pub fn output_handlebar(output: Output, config: ExternalConfig, template: &str) type_defs: String, methods: Vec, init_args: Option>, + tests: String, } let data = HBOutput { type_defs: output.type_defs, methods: output.methods, external: config.0, init_args: output.init_args, + tests: output.tests, }; hbs.render_template(template, &data).unwrap() } @@ -587,7 +705,11 @@ pub fn compile( }; let (output, unused) = emit_bindgen(tree, env, actor); for e in unused { - eprintln!("WARNING: path {e} is unused"); + eprintln!( + "{} path {} is unused", + style("WARNING:").red().bold(), + style(e).green() + ); } output_handlebar(output, external, &source) } @@ -615,170 +737,231 @@ fn path_to_var(path: &[TypePath]) -> String { .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) +struct NominalState<'a> { + state: crate::configs::State<'a, BindingConfig>, +} +impl<'a> NominalState<'a> { + // Convert structural typing to nominal typing to fit Rust's type system + fn nominalize(&mut self, env: &mut TypeEnv, path: &mut Vec, t: &Type) -> Type { + let elem = StateElem::Type(t); + let old = if matches!(t.as_ref(), TypeInner::Func(_)) { + // strictly speaking, we want to avoid func label from the main service. But this is probably good enough. + None + } else { + Some(self.state.push_state(&elem)) + }; + let res = match t.as_ref() { + TypeInner::Opt(ty) => { + path.push(TypePath::Opt); + let ty = self.nominalize(env, path, ty); + path.pop(); + TypeInner::Opt(ty) } - } - 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) + TypeInner::Vec(ty) => { + path.push(TypePath::Vec); + let ty = self.nominalize(env, path, ty); + path.pop(); + TypeInner::Vec(ty) } - 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::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 }| { + let lab = id.to_string(); + let elem = StateElem::Label(&lab); + let old = self.state.push_state(&elem); + path.push(TypePath::RecordField(id.to_string())); + let ty = self.nominalize(env, path, ty); + path.pop(); + self.state.pop_state(old, elem); + Field { id: id.clone(), ty } + }) + .collect(); + TypeInner::Record(fs) + } else { + let new_var = if let Some(name) = &self.state.config.name { + let res = name.to_string(); + self.state.update_stats("name"); + res + } else { + path_to_var(path) + }; + let ty = self.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::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); + TypeInner::Variant(fs) => { + if matches!(path.last(), None | Some(TypePath::Id(_))) || as_result(fs).is_some() { + let fs: Vec<_> = fs + .iter() + .map(|Field { id, ty }| { + let lab = id.to_string(); + let old = self.state.push_state(&StateElem::Label(&lab)); + path.push(TypePath::VariantField(id.to_string())); + let ty = self.nominalize(env, path, ty); path.pop(); - ty + self.state.pop_state(old, StateElem::Label(&lab)); + Field { id: id.clone(), 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); + .collect(); + TypeInner::Variant(fs) + } else { + let new_var = if let Some(name) = &self.state.config.name { + let res = name.to_string(); + self.state.update_stats("name"); + res + } else { + path_to_var(path) + }; + let ty = self.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 lab = format!("arg{i}"); + let old = self.state.push_state(&StateElem::Label(&lab)); + let idx = if i == 0 { + "".to_string() + } else { + i.to_string() + }; + path.push(TypePath::Func(format!("arg{idx}"))); + let ty = self.nominalize(env, path, &ty); + path.pop(); + self.state.pop_state(old, StateElem::Label(&lab)); + ty + }) + .collect(), + rets: func + .rets + .into_iter() + .enumerate() + .map(|(i, ty)| { + let lab = format!("ret{i}"); + let old = self.state.push_state(&StateElem::Label(&lab)); + let idx = if i == 0 { + "".to_string() + } else { + i.to_string() + }; + path.push(TypePath::Func(format!("ret{idx}"))); + let ty = self.nominalize(env, path, &ty); + path.pop(); + self.state.pop_state(old, StateElem::Label(&lab)); + ty + }) + .collect(), + }) + } + Some(_) => { + let new_var = if let Some(name) = &self.state.config.name { + let res = name.to_string(); + self.state.update_stats("name"); + res + } else { + path_to_var(path) + }; + let ty = self.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)| { + let lab = meth.to_string(); + let old = self.state.push_state(&StateElem::Label(&lab)); + path.push(TypePath::Id(meth.to_string())); + let ty = self.nominalize(env, path, ty); path.pop(); - ty + self.state.pop_state(old, StateElem::Label(&lab)); + (meth.clone(), 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); + ), + Some(_) => { + let new_var = if let Some(name) = &self.state.config.name { + let res = name.to_string(); + self.state.update_stats("name"); + res + } else { + path_to_var(path) + }; + let ty = self.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| { + let elem = StateElem::Label("init"); + let old = self.state.push_state(&elem); + path.push(TypePath::Init); + let ty = self.nominalize(env, path, ty); path.pop(); - (meth.clone(), ty) + self.state.pop_state(old, elem); + ty }) .collect(), + self.nominalize(env, path, ty), ), - 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(), + t => t.clone(), + } + .into(); + if let Some(old) = old { + self.state.pop_state(old, elem); + } + res } - .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); + fn nominalize_all(&mut self, actor: &Option) -> (TypeEnv, Option) { + let mut res = TypeEnv(Default::default()); + for (id, ty) in self.state.env.0.iter() { + let elem = StateElem::Label(id); + let old = self.state.push_state(&elem); + let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.clone())], ty); + res.0.insert(id.to_string(), ty); + self.state.pop_state(old, elem); + } + let actor = actor + .as_ref() + .map(|ty| self.nominalize(&mut res, &mut vec![], ty)); + (res, actor) } - let actor = actor - .as_ref() - .map(|ty| nominalize(&mut res, &mut vec![], ty)); - (res, actor) } fn get_hbs() -> handlebars::Handlebars<'static> { use handlebars::*; diff --git a/rust/candid_parser/src/bindings/rust_agent.hbs b/rust/candid_parser/src/bindings/rust_agent.hbs index 9fffd9b4..f4521ae2 100644 --- a/rust/candid_parser/src/bindings/rust_agent.hbs +++ b/rust/candid_parser/src/bindings/rust_agent.hbs @@ -20,3 +20,6 @@ impl<'a> {{PascalCase service_name}}<'a> { pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); // {{canister_id}} {{/if}} {{/if}} +{{#if tests}} +{{tests}} +{{/if}} diff --git a/rust/candid_parser/src/bindings/rust_call.hbs b/rust/candid_parser/src/bindings/rust_call.hbs index b12654b6..55fbf3a9 100644 --- a/rust/candid_parser/src/bindings/rust_call.hbs +++ b/rust/candid_parser/src/bindings/rust_call.hbs @@ -19,3 +19,6 @@ pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice ca pub const {{snake_case service_name}} : {{PascalCase service_name}} = {{PascalCase service_name}}(CANISTER_ID); {{/if}} {{/if}} +{{#if tests}} +{{tests}} +{{/if}} diff --git a/rust/candid_parser/src/bindings/rust_stub.hbs b/rust/candid_parser/src/bindings/rust_stub.hbs index 5a85901a..32658194 100644 --- a/rust/candid_parser/src/bindings/rust_stub.hbs +++ b/rust/candid_parser/src/bindings/rust_stub.hbs @@ -16,3 +16,6 @@ fn {{this.name}}({{#each this.args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{t unimplemented!() } {{/each}} +{{#if tests}} +{{tests}} +{{/if}} diff --git a/rust/candid_parser/src/configs.rs b/rust/candid_parser/src/configs.rs index 4b4e600b..f6a50d94 100644 --- a/rust/candid_parser/src/configs.rs +++ b/rust/candid_parser/src/configs.rs @@ -6,13 +6,18 @@ use toml::{Table, Value}; pub struct State<'a, T: ConfigState> { tree: &'a ConfigTree, - path: Vec, + pub path: Vec, open_tree: Option<&'a ConfigTree>, open_path: Vec, - stats: BTreeMap, u32>, + pub stats: BTreeMap, u32>, pub config: T, + pub config_source: BTreeMap>, pub env: &'a TypeEnv, } +pub struct ConfigBackup { + config: T, + config_source: BTreeMap>, +} #[derive(Debug)] pub enum StateElem<'a> { Type(&'a Type), @@ -33,8 +38,12 @@ pub enum ScopePos { impl<'a, T: ConfigState> State<'a, T> { pub fn new(tree: &'a ConfigTree, env: &'a TypeEnv) -> Self { let mut config = T::default(); + let mut config_source = BTreeMap::new(); if let Some(state) = &tree.state { - config.merge_config(state, None, false); + let delta = config.merge_config(state, None); + for field in delta { + config_source.insert(field, vec![]); + } } Self { tree, @@ -43,6 +52,7 @@ impl<'a, T: ConfigState> State<'a, T> { path: Vec::new(), stats: BTreeMap::new(), config, + config_source, env, } } @@ -54,7 +64,7 @@ impl<'a, T: ConfigState> State<'a, T> { self.open_path.clear(); } Some(scope) => { - let mut path = vec![format!("method:{}", scope.method)]; + let mut path = vec![format!("func:{}", scope.method)]; match self.tree.with_prefix(&path) { Some(tree) => { match scope.position { @@ -73,7 +83,9 @@ impl<'a, T: ConfigState> State<'a, T> { } } if let Some(state) = self.open_tree.unwrap().state.as_ref() { - self.config.merge_config(state, None, false); + let delta = self.config.merge_config(state, None); + let path = self.open_path.clone(); + self.update_config_source(delta, &path); } } None => { @@ -85,9 +97,9 @@ impl<'a, T: ConfigState> State<'a, T> { } } /// Update config based on the new elem in the path. Return the old state AFTER `update_state`. - pub fn push_state(&mut self, elem: &StateElem) -> T { + pub fn push_state(&mut self, elem: &StateElem) -> ConfigBackup { self.config.update_state(elem); - let old_config = self.config.clone(); + let old_config = self.to_backup(); self.path.push(elem.to_string()); let mut from_open = false; let new_state = if let Some(subtree) = self.open_tree { @@ -106,24 +118,33 @@ impl<'a, T: ConfigState> State<'a, T> { vec![] }; matched_path.extend_from_slice(&self.path[idx..]); - self.stats - .entry(matched_path) - .and_modify(|v| *v += 1) - .or_insert(1); - self.config.merge_config(state, Some(elem), is_recursive); - //eprintln!("match path: {:?}, state: {:?}", self.path, self.config); + let ctx = Context { elem, is_recursive }; + let delta = self.config.merge_config(state, Some(ctx)); + self.update_config_source(delta, &matched_path); } else { - self.config - .merge_config(&T::unmatched_config(), Some(elem), false); - //eprintln!("path: {:?}, state: {:?}", self.path, self.config); + let ctx = Context { + elem, + is_recursive: false, + }; + let delta = self.config.merge_config(&T::unmatched_config(), Some(ctx)); + for field in delta { + self.config_source.remove(&field); + } } old_config } - pub fn pop_state(&mut self, old_config: T, elem: StateElem) { - self.config = old_config; + pub fn pop_state(&mut self, old_config: ConfigBackup, elem: StateElem) { + self.restore_from_backup(old_config); assert_eq!(self.path.pop(), Some(elem.to_string())); self.config.restore_state(&elem); } + pub fn update_stats(&mut self, key: &str) { + if let Some(path) = self.config_source.get(key) { + let mut path = path.clone(); + path.push(key.to_string()); + self.stats.entry(path).and_modify(|v| *v += 1).or_insert(1); + } + } pub fn report_unused(&self) -> Vec { let mut res = BTreeSet::new(); self.tree.traverse(&mut vec![], &mut res); @@ -144,12 +165,35 @@ impl<'a, T: ConfigState> State<'a, T> { .map(|(k, v)| (k.join("."), v)) .collect() } + fn update_config_source(&mut self, delta: Vec, path: &[String]) { + for field in delta { + self.config_source.insert(field, path.to_vec()); + } + } + fn to_backup(&self) -> ConfigBackup { + ConfigBackup { + config: self.config.clone(), + config_source: self.config_source.clone(), + } + } + fn restore_from_backup(&mut self, bak: ConfigBackup) { + self.config = bak.config; + self.config_source = bak.config_source; + } +} +#[derive(Debug)] +pub struct Context<'a> { + pub elem: &'a StateElem<'a>, + pub is_recursive: bool, } pub trait ConfigState: DeserializeOwned + Default + Clone + std::fmt::Debug { - fn merge_config(&mut self, config: &Self, elem: Option<&StateElem>, is_recursive: bool); + /// Specifies the merging semantics of two configs, returns a vector of updated config fields. + fn merge_config(&mut self, config: &Self, ctx: Option) -> Vec; fn update_state(&mut self, elem: &StateElem); fn restore_state(&mut self, elem: &StateElem); + /// List the properties in the current config, used for analyzing unused properties. + fn list_properties(&self) -> Vec; fn unmatched_config() -> Self { Self::default() } @@ -240,8 +284,12 @@ impl ConfigTree { result.state.as_ref() } pub fn traverse(&self, path: &mut Vec, res: &mut BTreeSet>) { - if self.state.is_some() { - res.insert(path.clone()); + if let Some(state) = &self.state { + for prop in state.list_properties() { + let mut path = path.clone(); + path.push(prop); + res.insert(path); + } } for (k, v) in self.subtree.iter() { path.push(k.clone()); @@ -289,8 +337,8 @@ fn is_repeated(path: &[String], matched: &[String]) -> bool { } false } -fn special_key(key: &str) -> bool { - key.starts_with("method:") || key.starts_with("arg:") || key.starts_with("ret:") +pub fn is_scoped_key(key: &str) -> bool { + key.starts_with("func:") || key.starts_with("arg:") || key.starts_with("ret:") } fn generate_state_tree(v: Value) -> Result> { let mut subtree = BTreeMap::new(); @@ -301,7 +349,7 @@ fn generate_state_tree(v: Value) -> Result> { match v { Value::Table(_) => { let v = generate_state_tree(v)?; - let dep = if special_key(&k) { + let dep = if is_scoped_key(&k) { v.max_depth } else { v.max_depth + 1 @@ -356,7 +404,8 @@ fn path_name(t: &Type) -> String { TypeInner::Func(_) => "func", TypeInner::Service(_) => "service", TypeInner::Future => "future", - TypeInner::Class(..) | TypeInner::Unknown => unreachable!(), + TypeInner::Class(..) => "func:init", + TypeInner::Unknown => unreachable!(), } .to_string() } @@ -371,11 +420,14 @@ fn parse() { text: Option, } impl ConfigState for T { - fn merge_config(&mut self, config: &Self, _elem: Option<&StateElem>, is_recursive: bool) { + fn merge_config(&mut self, config: &Self, ctx: Option) -> Vec { + let mut res = vec!["depth", "text", "size"]; *self = config.clone(); - if is_recursive { + if ctx.is_some_and(|c| c.is_recursive) { self.size = Some(0); + res.pop(); } + res.into_iter().map(|f| f.to_string()).collect() } fn update_state(&mut self, _elem: &StateElem) { self.size = self.size.map(|s| s + 1); @@ -383,6 +435,19 @@ fn parse() { fn restore_state(&mut self, _elem: &StateElem) { self.size = self.size.map(|s| s - 1); } + fn list_properties(&self) -> Vec { + let mut res = Vec::new(); + if self.depth.is_some() { + res.push("depth"); + } + if self.size.is_some() { + res.push("size"); + } + if self.text.is_some() { + res.push("text"); + } + res.into_iter().map(|f| f.to_string()).collect() + } } let toml = r#" [random] @@ -391,8 +456,8 @@ val.text = "42" left.list = { depth = 1 } vec.nat8.text = "blob" Vec = { width = 2, size = 10 } -"method:f"."arg:0".list = { depth = 2, size = 20 } -"method:f".list = { depth = 3, size = 30 } +"func:f"."arg:0".list = { depth = 2, size = 20 } +"func:f".list = { depth = 3, size = 30 } "#; let configs = toml.parse::().unwrap(); let mut tree: ConfigTree = ConfigTree::from_configs("random", configs).unwrap(); @@ -464,13 +529,21 @@ Vec = { width = 2, size = 10 } }), 0, ); - assert_eq!(state.open_path, vec!["method:f", "arg:0"]); + assert_eq!(state.open_path, vec!["func:f", "arg:0"]); let old = state.push_state(&StateElem::Label("list")); + state.update_stats("depth"); + state.update_stats("size"); + state.update_stats("text"); assert_eq!(state.config.depth, Some(2)); assert_eq!(state.config.size, Some(20)); assert_eq!(state.config.text, None); - assert_eq!(old.size, None); + assert_eq!( + state.config_source.get("depth").unwrap().join("."), + "func:f.arg:0.list" + ); + assert_eq!(old.config.size, None); state.push_state(&StateElem::Label("val")); + state.update_stats("text"); assert_eq!(state.config.text, Some("42".to_string())); state.with_scope( &Some(Scope { @@ -479,29 +552,45 @@ Vec = { width = 2, size = 10 } }), 0, ); - assert_eq!(state.open_path, vec!["method:f"]); + assert_eq!(state.open_path, vec!["func:f"]); state.push_state(&StateElem::Label("list")); + state.update_stats("depth"); + state.update_stats("size"); assert_eq!(state.config.depth, Some(3)); assert_eq!(state.config.size, Some(0)); + assert_eq!( + state.config_source.get("depth").unwrap().join("."), + "func:f.list" + ); + assert_eq!(state.config_source.get("size").unwrap().join("."), "val"); state.with_scope(&None, 0); let old = state.push_state(&StateElem::Label("list")); + state.update_stats("depth"); + state.update_stats("size"); assert_eq!(state.config.depth, Some(20)); assert_eq!(state.config.size, Some(0)); - assert_eq!(old.size, Some(1)); + assert_eq!(state.config_source.get("depth").unwrap().join("."), "list"); + assert_eq!(state.config_source.get("size").unwrap().join("."), "val"); + assert_eq!(old.config.size, Some(1)); state.pop_state(old, StateElem::Label("list")); + state.update_stats("size"); + state.update_stats("depth"); assert_eq!(state.config.size, Some(0)); assert_eq!(state.config.depth, Some(3)); let stats = state.report_unused(); assert_eq!( stats.iter().map(|x| x.as_str()).collect::>(), [ - "Vec", - "a.b.c", - "a.b.c.d", - "a.b.d", - "left.a", - "left.list", - "vec.nat8" + "Vec.size", + "a.b.c.d.depth", + "a.b.c.depth", + "a.b.d.depth", + "depth", + "func:f.list.size", + "left.a.depth", + "left.list.depth", + "list.size", + "vec.nat8.text" ] ); } diff --git a/rust/candid_parser/src/random.rs b/rust/candid_parser/src/random.rs index 0fdc81bf..81b55870 100644 --- a/rust/candid_parser/src/random.rs +++ b/rust/candid_parser/src/random.rs @@ -1,5 +1,4 @@ -use super::configs::{ConfigState, Configs, Scope, State}; -use crate::configs::StateElem; +use super::configs::{ConfigState, Configs, Context, Scope, State, StateElem}; use crate::{Error, Result}; use arbitrary::{unstructured::Int, Arbitrary, Unstructured}; use candid::types::value::{IDLArgs, IDLField, IDLValue, VariantValue}; @@ -32,19 +31,35 @@ impl Default for GenConfig { } } impl ConfigState for GenConfig { - fn merge_config(&mut self, config: &Self, _elem: Option<&StateElem>, is_recursive: bool) { - self.range = config.range.or(self.range); + fn merge_config(&mut self, config: &Self, ctx: Option) -> Vec { + let mut res = Vec::new(); + if config.range.is_some() { + self.range = config.range; + res.push("range"); + } if config.text.is_some() { self.text.clone_from(&config.text); + res.push("text"); + } + if config.width.is_some() { + self.width = config.width; + res.push("width"); } - self.width = config.width.or(self.width); if config.value.is_some() { self.value.clone_from(&config.value); + res.push("value"); } - if !is_recursive { - self.depth = config.depth.or(self.depth); - self.size = config.size.or(self.size); + if ctx.as_ref().is_some_and(|c| !c.is_recursive) { + if config.depth.is_some() { + self.depth = config.depth; + res.push("depth"); + } + if config.size.is_some() { + self.size = config.size; + res.push("size"); + } } + res.into_iter().map(|s| s.to_string()).collect() } fn update_state(&mut self, elem: &StateElem) { if let StateElem::Type(t) = elem { @@ -71,6 +86,28 @@ impl ConfigState for GenConfig { size: None, } } + fn list_properties(&self) -> Vec { + let mut res = Vec::new(); + if self.range.is_some() { + res.push("range".to_string()); + } + if self.text.is_some() { + res.push("text".to_string()); + } + if self.width.is_some() { + res.push("width".to_string()); + } + if self.value.is_some() { + res.push("value".to_string()); + } + if self.depth.is_some() { + res.push("depth".to_string()); + } + if self.size.is_some() { + res.push("size".to_string()); + } + res + } } pub struct RandState<'a>(State<'a, GenConfig>); @@ -82,6 +119,7 @@ impl<'a> RandState<'a> { let v: IDLValue = super::parse_idl_value(v)?; let v = v.annotate_type(true, self.0.env, ty)?; self.0.pop_state(old_config, StateElem::Type(ty)); + self.0.update_stats("value"); return Ok(v); } let res = Ok(match ty.as_ref() { @@ -92,24 +130,60 @@ impl<'a> RandState<'a> { TypeInner::Null => IDLValue::Null, TypeInner::Reserved => IDLValue::Reserved, TypeInner::Bool => IDLValue::Bool(u.arbitrary()?), - TypeInner::Int => IDLValue::Int(arbitrary_num::(u, self.0.config.range)?.into()), - TypeInner::Nat => IDLValue::Nat(arbitrary_num::(u, self.0.config.range)?.into()), - TypeInner::Nat8 => IDLValue::Nat8(arbitrary_num(u, self.0.config.range)?), - TypeInner::Nat16 => IDLValue::Nat16(arbitrary_num(u, self.0.config.range)?), - TypeInner::Nat32 => IDLValue::Nat32(arbitrary_num(u, self.0.config.range)?), - TypeInner::Nat64 => IDLValue::Nat64(arbitrary_num(u, self.0.config.range)?), - TypeInner::Int8 => IDLValue::Int8(arbitrary_num(u, self.0.config.range)?), - TypeInner::Int16 => IDLValue::Int16(arbitrary_num(u, self.0.config.range)?), - TypeInner::Int32 => IDLValue::Int32(arbitrary_num(u, self.0.config.range)?), - TypeInner::Int64 => IDLValue::Int64(arbitrary_num(u, self.0.config.range)?), + TypeInner::Int => { + self.0.update_stats("range"); + IDLValue::Int(arbitrary_num::(u, self.0.config.range)?.into()) + } + TypeInner::Nat => { + self.0.update_stats("range"); + IDLValue::Nat(arbitrary_num::(u, self.0.config.range)?.into()) + } + TypeInner::Nat8 => { + self.0.update_stats("range"); + IDLValue::Nat8(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Nat16 => { + self.0.update_stats("range"); + IDLValue::Nat16(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Nat32 => { + self.0.update_stats("range"); + IDLValue::Nat32(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Nat64 => { + self.0.update_stats("range"); + IDLValue::Nat64(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Int8 => { + self.0.update_stats("range"); + IDLValue::Int8(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Int16 => { + self.0.update_stats("range"); + IDLValue::Int16(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Int32 => { + self.0.update_stats("range"); + IDLValue::Int32(arbitrary_num(u, self.0.config.range)?) + } + TypeInner::Int64 => { + self.0.update_stats("range"); + IDLValue::Int64(arbitrary_num(u, self.0.config.range)?) + } TypeInner::Float32 => IDLValue::Float32(u.arbitrary()?), TypeInner::Float64 => IDLValue::Float64(u.arbitrary()?), - TypeInner::Text => IDLValue::Text(arbitrary_text( - u, - &self.0.config.text, - &self.0.config.width, - )?), + TypeInner::Text => { + self.0.update_stats("text"); + self.0.update_stats("width"); + IDLValue::Text(arbitrary_text( + u, + &self.0.config.text, + &self.0.config.width, + )?) + } TypeInner::Opt(t) => { + self.0.update_stats("depth"); + self.0.update_stats("size"); let depths = if self.0.config.depth.is_some_and(|d| d <= 0) || self.0.config.size.is_some_and(|s| s <= 0) { @@ -125,8 +199,10 @@ impl<'a> RandState<'a> { } } TypeInner::Vec(t) => { + self.0.update_stats("width"); let width = self.0.config.width.or_else(|| { let elem_size = size(self.0.env, t).unwrap_or(MAX_DEPTH); + self.0.update_stats("size"); Some(std::cmp::max(0, self.0.config.size.unwrap_or(0)) as usize / elem_size) }); let len = arbitrary_len(u, width)?; @@ -156,6 +232,8 @@ impl<'a> RandState<'a> { let choices = fs .iter() .map(|Field { ty, .. }| size(self.0.env, ty).unwrap_or(MAX_DEPTH)); + self.0.update_stats("size"); + self.0.update_stats("depth"); let sizes: Vec<_> = if self.0.config.depth.is_some_and(|d| d <= 0) || self.0.config.size.is_some_and(|s| s <= 0) { @@ -178,10 +256,14 @@ impl<'a> RandState<'a> { IDLValue::Variant(VariantValue(Box::new(field), idx as u64)) } TypeInner::Principal => IDLValue::Principal(crate::Principal::arbitrary(u)?), - TypeInner::Func(_) => IDLValue::Func( - crate::Principal::arbitrary(u)?, - arbitrary_text(u, &self.0.config.text, &self.0.config.width)?, - ), + TypeInner::Func(_) => { + self.0.update_stats("text"); + self.0.update_stats("width"); + IDLValue::Func( + crate::Principal::arbitrary(u)?, + arbitrary_text(u, &self.0.config.text, &self.0.config.width)?, + ) + } TypeInner::Service(_) => IDLValue::Service(crate::Principal::arbitrary(u)?), _ => unimplemented!(), }); diff --git a/rust/candid_parser/src/utils.rs b/rust/candid_parser/src/utils.rs index 82a1b32c..f3027180 100644 --- a/rust/candid_parser/src/utils.rs +++ b/rust/candid_parser/src/utils.rs @@ -80,3 +80,19 @@ pub fn merge_init_args(candid: &str, init: &str) -> Result<(TypeEnv, Type)> { _ => unreachable!(), } } +/// Check if a Rust type implements a Candid type. The candid type is given using the init args format. +/// Note that this only checks structural equality, not equivalence. For recursive types, it may reject +/// an unrolled type. +pub fn check_rust_type(candid_args: &str) -> Result<()> { + use crate::{types::IDLInitArgs, typing::check_init_args}; + use candid::types::{internal::TypeContainer, subtype::equal, TypeEnv}; + let parsed = candid_args.parse::()?; + let mut env = TypeEnv::new(); + let args = check_init_args(&mut env, &TypeEnv::new(), &parsed)?; + let mut rust_env = TypeContainer::new(); + let ty = rust_env.add::(); + let ty = env.merge_type(rust_env.env, ty); + let mut gamma = std::collections::HashSet::new(); + equal(&mut gamma, &env, &args[0], &ty)?; + Ok(()) +} diff --git a/rust/candid_parser/tests/assets/example.did b/rust/candid_parser/tests/assets/example.did index 755ef09e..a0a865d1 100644 --- a/rust/candid_parser/tests/assets/example.did +++ b/rust/candid_parser/tests/assets/example.did @@ -3,18 +3,19 @@ import "import/a.did"; import service "import/b/b.did"; type my_type = principal; type List = opt record { head: int; tail: List }; -type f = func (List, func (int32) -> (int64)) -> (opt List); +type f = func (List, func (int32) -> (int64)) -> (opt List, res); 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: nat; Err: empty }; service server : { f1 : (list, test: blob, opt bool) -> () oneway; g1 : (my_type, List, opt List, nested) -> (int, broker) query; h : (vec opt text, variant { A: nat; B: opt text }, opt List) -> (record { id: nat; 0x2a: record {} }); i : f; - x : (a,b) -> (opt a, opt b) composite_query; + x : (a,b) -> (opt a, opt b, variant { Ok; Err: variant {a;b} }) composite_query; } diff --git a/rust/candid_parser/tests/assets/example.toml b/rust/candid_parser/tests/assets/example.toml index 90d9f14a..8f09274f 100644 --- a/rust/candid_parser/tests/assets/example.toml +++ b/rust/candid_parser/tests/assets/example.toml @@ -9,3 +9,4 @@ my_type = { visibility = "", name = "CanisterId" } nat.use_type = "u128" BrokerFindRet = { name = "BrokerReturn", visibility = "pub" } g1 = { name = "G11", arg0.name = "id", arg1.name = "list", arg2.name = "is_okay", ret0.use_type = "i128" } +x.ret2.variant.Err.variant.name = "Error" diff --git a/rust/candid_parser/tests/assets/ok/example.d.ts b/rust/candid_parser/tests/assets/ok/example.d.ts index 56722625..d7b7c8e3 100644 --- a/rust/candid_parser/tests/assets/ok/example.d.ts +++ b/rust/candid_parser/tests/assets/ok/example.d.ts @@ -9,7 +9,7 @@ export type a = { 'a' : null } | { 'b' : b }; export type b = [bigint, bigint]; export interface broker { 'find' : ActorMethod<[string], Principal> } -export type f = ActorMethod<[List, [Principal, string]], [] | [List]>; +export type f = ActorMethod<[List, [Principal, string]], [[] | [List], res]>; export type list = [] | [node]; export type my_type = Principal; export interface nested { @@ -25,6 +25,8 @@ export interface nested { _42_ : bigint, } export interface node { 'head' : bigint, 'tail' : list } +export type res = { 'Ok' : bigint } | + { 'Err' : never }; export interface s { 'f' : t, 'g' : ActorMethod<[list], [B, tree, stream]> } export type stream = [] | [{ 'head' : bigint, 'next' : [Principal, string] }]; export type t = ActorMethod<[Principal], undefined>; @@ -48,7 +50,15 @@ export interface _SERVICE { { _42_ : {}, 'id' : bigint } >, 'i' : f, - 'x' : ActorMethod<[a, b], [[] | [a], [] | [b]]>, + 'x' : ActorMethod< + [a, b], + [ + [] | [a], + [] | [b], + { 'Ok' : null } | + { 'Err' : { 'a' : null } | { 'b' : null } }, + ] + >, } 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 feb17030..d2063d17 100644 --- a/rust/candid_parser/tests/assets/ok/example.did +++ b/rust/candid_parser/tests/assets/ok/example.did @@ -6,7 +6,7 @@ type b = record { int; nat }; type broker = service { find : (text) -> (service { current : () -> (nat32); up : () -> () }); }; -type f = func (List, func (int32) -> (int64)) -> (opt List); +type f = func (List, func (int32) -> (int64)) -> (opt List, res); type list = opt node; type my_type = principal; type nested = record { @@ -19,6 +19,7 @@ type nested = record { 42 : nat; }; type node = record { head : nat; tail : list }; +type res = variant { Ok : nat; Err : empty }; type s = service { f : t; g : (list) -> (B, tree, stream) }; type stream = opt record { head : nat; next : func () -> (stream) query }; type t = func (s) -> (); @@ -36,5 +37,9 @@ service : { record { 42 : record {}; id : nat }, ); i : f; - x : (a, b) -> (opt a, opt b) composite_query; + x : (a, b) -> ( + opt a, + opt b, + variant { Ok; Err : variant { a; b } }, + ) composite_query; } diff --git a/rust/candid_parser/tests/assets/ok/example.js b/rust/candid_parser/tests/assets/ok/example.js index 8cecb1b9..7004ca7e 100644 --- a/rust/candid_parser/tests/assets/ok/example.js +++ b/rust/candid_parser/tests/assets/ok/example.js @@ -57,9 +57,10 @@ export const idlFactory = ({ IDL }) => { [], ), }); + const res = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : IDL.Empty }); const f = IDL.Func( [List, IDL.Func([IDL.Int32], [IDL.Int64], [])], - [IDL.Opt(List)], + [IDL.Opt(List), res], [], ); const a = IDL.Variant({ 'a' : IDL.Null, 'b' : b }); @@ -87,7 +88,18 @@ export const idlFactory = ({ IDL }) => { [], ), 'i' : f, - 'x' : IDL.Func([a, b], [IDL.Opt(a), IDL.Opt(b)], ['composite_query']), + 'x' : IDL.Func( + [a, b], + [ + IDL.Opt(a), + IDL.Opt(b), + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : IDL.Variant({ 'a' : IDL.Null, 'b' : IDL.Null }), + }), + ], + ['composite_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 40c95fac..9d87e459 100644 --- a/rust/candid_parser/tests/assets/ok/example.mo +++ b/rust/candid_parser/tests/assets/ok/example.mo @@ -13,7 +13,10 @@ module { up : shared () -> async (); }; }; - public type f = shared (List, shared Int32 -> async Int64) -> async ?List; + public type f = shared (List, shared Int32 -> async Int64) -> async ( + ?List, + res, + ); public type list = ?node; public type my_type = Principal; public type nested = { @@ -26,6 +29,7 @@ module { _42_ : Nat; }; public type node = { head : Nat; tail : list }; + public type res = { #Ok : Nat; #Err : None }; 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 s -> async (); @@ -44,6 +48,10 @@ module { id : Nat; }; i : f; - x : shared composite query (a, b) -> async (?a, ?b); + x : shared composite query (a, b) -> async ( + ?a, + ?b, + { #Ok; #Err : { #a; #b } }, + ); } } diff --git a/rust/candid_parser/tests/assets/ok/example.rs b/rust/candid_parser/tests/assets/ok/example.rs index e5c735b8..84c5ae02 100644 --- a/rust/candid_parser/tests/assets/ok/example.rs +++ b/rust/candid_parser/tests/assets/ok/example.rs @@ -83,9 +83,15 @@ pub(crate) struct HRet42 {} #[derive(CandidType, Deserialize, Debug)] pub(crate) struct HRet { pub(crate) _42_: HRet42, pub(crate) id: u128 } candid::define_function!(pub(crate) FArg1 : (i32) -> (i64)); -candid::define_function!(pub(crate) F : (MyList, FArg1) -> (Option)); +pub(crate) type Res = std::result::Result; +candid::define_function!(pub(crate) F : (MyList, FArg1) -> ( + Option, + Res, + )); #[derive(CandidType, Deserialize, Debug)] pub(crate) enum A { #[serde(rename="a")] A, #[serde(rename="b")] B(B) } +#[derive(CandidType, Deserialize, Debug)] +pub(crate) enum Error { #[serde(rename="a")] A, #[serde(rename="b")] B } pub struct Service(pub Principal); impl Service { @@ -107,13 +113,33 @@ 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 } - pub async fn i(&self, arg0: MyList, arg1: FArg1) -> Result<(Option,)> { + 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,)> { + pub async fn x(&self, arg0: A, arg1: B) -> Result<(Option,Option,std::result::Result<(), Error>,)> { ic_cdk::call(self.0, "x", (arg0,arg1,)).await } } pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa 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 List = opt ListInner; +type ListInner = record { head : int; tail : List }; +(List)"#; + candid_parser::utils::check_rust_type::>(candid_src).unwrap(); +} +#[test] +fn test_i128() { + // Generated from g1.ret0.use_type = "i128" + let candid_src = r#"(int)"#; + candid_parser::utils::check_rust_type::(candid_src).unwrap(); +} +#[test] +fn test_u128() { + // Generated from nat.use_type = "u128" + let candid_src = r#"(nat)"#; + candid_parser::utils::check_rust_type::(candid_src).unwrap(); +} diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index ee59bbff..d6db3b70 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -36,6 +36,9 @@ enum Command { #[clap(short, long)] /// Specifies binding generation config in TOML syntax config: Option, + #[clap(short, long, num_args = 1.., value_delimiter = ',')] + /// Specifies a subset of methods to generate bindings. Allowed format: "-m foo,bar", "-m foo bar", "-m foo -m bar" + methods: Vec, }, /// Generate test suites for different languages Test { @@ -207,9 +210,13 @@ fn main() -> Result<()> { input, target, config, + methods, } => { let configs = load_config(&config)?; - let (env, actor) = pretty_check_file(&input)?; + let (env, mut actor) = pretty_check_file(&input)?; + if !methods.is_empty() { + actor = candid_parser::bindings::analysis::project_methods(&env, &actor, &methods); + } let content = match target.as_str() { "js" => candid_parser::bindings::javascript::compile(&env, &actor), "ts" => candid_parser::bindings::typescript::compile(&env, &actor),