diff --git a/CHANGELOG.md b/CHANGELOG.md index 042d169b..73a4c12e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### candid_parser +* **Breaking changes:** + + Motoko binding: type alias names are now emitted in PascalCase (e.g. `my_type` → `MyType`, `list` → `List`). Names that cannot be unambiguously converted — those not starting with a lowercase letter, those where two names would collide to the same PascalCase form, or those whose PascalCase form is already taken by another type — fall back to the original escaped name. Any code referencing generated Motoko type aliases by name will need to be updated. + * Bug fixes: + Motoko binding: emit `Float32` for Candid `float32` instead of panicking. `float32` support was added to Motoko in version 1.4.0. diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index 326b7fc6..57ee6f67 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -7,6 +7,7 @@ use candid::pretty::utils::*; use candid::types::{Field, FuncMode, Function, Label, SharedLabel, Type, TypeInner}; use candid::TypeEnv; use pretty::RcDoc; +use std::collections::{HashMap, HashSet}; // The definition of tuple is language specific. fn is_tuple(t: &Type) -> bool { @@ -78,47 +79,104 @@ static KEYWORDS: [&str; 48] = [ "while", "with", ]; -fn escape(id: &str, is_method: bool) -> RcDoc<'_> { - if KEYWORDS.contains(&id) { - str(id).append("_") + +fn escape_str(id: &str) -> String { + if KEYWORDS.contains(&id) || (is_valid_as_id(id) && id.ends_with('_')) { + format!("{id}_") } else if is_valid_as_id(id) { - if id.ends_with('_') { - str(id).append("_") - } else { - str(id) - } - } else if !is_method { - str("_") - .append(candid::idl_hash(id).to_string()) - .append("_") + id.to_string() } else { + format!("_{}_", candid::idl_hash(id)) + } +} + +fn escape(id: &str, is_method: bool) -> RcDoc<'_> { + if is_method && !KEYWORDS.contains(&id) && !is_valid_as_id(id) { panic!("Candid method {id} is not a valid Motoko id"); } + RcDoc::text(escape_str(id)) } -fn pp_ty_rich<'a>(ty: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { +// Strips underscores and capitalises each segment start. Returns None for +// names not starting with lowercase (left as-is). Trailing '_' is consumed. +fn to_pascal_case(s: &str) -> Option { + if !s.starts_with(|c: char| c.is_ascii_lowercase()) { + return None; + } + let mut out = String::with_capacity(s.len()); + let mut capitalize = true; + for c in s.chars() { + if c == '_' { + capitalize = true; + } else if capitalize { + out.extend(c.to_uppercase()); + capitalize = false; + } else { + out.push(c); + } + } + Some(out) +} + +// Precomputes display names for all env type aliases. +fn build_names(env: &TypeEnv) -> HashMap { + // Baseline: every id gets its escaped original name (collision-free). + let mut names: HashMap = env + .0 + .keys() + .map(|id| { + let f = escape_str(id); + // escape_str doesn't reserve "Self". + (id.clone(), if f == "Self" { format!("{f}_") } else { f }) + }) + .collect(); + + // Upgrade to PascalCase where the name is unclaimed (first alphabetically wins). + let mut taken: HashSet = names.values().cloned().collect(); + for id in env.0.keys() { + if let Some(pascal) = to_pascal_case(id) { + if pascal != "Self" && !taken.contains(&pascal) { + let display = names.get_mut(id).unwrap(); + taken.remove(display.as_str()); + taken.insert(pascal.clone()); + *display = pascal; + } + } + } + names +} + +#[derive(Copy, Clone)] +struct BindingCtx<'a> { + env: &'a TypeEnv, + names: &'a HashMap, +} + +fn pp_ty_rich<'a>(ctx: BindingCtx<'a>, ty: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { match (ty.as_ref(), syntax) { (TypeInner::Service(ref meths), Some(IDLType::ServT(methods))) => { - pp_service(meths, Some(methods)) + pp_service(ctx, meths, Some(methods)) } (TypeInner::Class(ref args, t), Some(IDLType::ClassT(_, syntax_t))) => { - pp_class((args, t), Some(syntax_t)) + pp_class(ctx, (args, t), Some(syntax_t)) } (TypeInner::Record(ref fields), Some(IDLType::RecordT(syntax_fields))) => { - pp_record(fields, Some(syntax_fields)) + pp_record(ctx, fields, Some(syntax_fields)) } (TypeInner::Variant(ref fields), Some(IDLType::VariantT(syntax_fields))) => { - pp_variant(fields, Some(syntax_fields)) + pp_variant(ctx, fields, Some(syntax_fields)) } (TypeInner::Opt(ref inner), Some(IDLType::OptT(syntax))) => { - str("?").append(pp_ty_rich(inner, Some(syntax))) + str("?").append(pp_ty_rich(ctx, inner, Some(syntax))) } - (TypeInner::Vec(ref inner), Some(IDLType::VecT(syntax))) => pp_vec(inner, Some(syntax)), - (_, _) => pp_ty(ty), + (TypeInner::Vec(ref inner), Some(IDLType::VecT(syntax))) => { + pp_vec(ctx, inner, Some(syntax)) + } + (_, _) => pp_ty(ctx, ty), } } -fn pp_ty(ty: &Type) -> RcDoc<'_> { +fn pp_ty<'a>(ctx: BindingCtx<'a>, ty: &'a Type) -> RcDoc<'a> { use TypeInner::*; match ty.as_ref() { Null => str("Null"), @@ -138,15 +196,15 @@ fn pp_ty(ty: &Type) -> RcDoc<'_> { Text => str("Text"), Reserved => str("Any"), Empty => str("None"), - Var(ref s) => escape(s, false), + Var(ref s) => RcDoc::text(ctx.names.get(s).cloned().unwrap_or_else(|| escape_str(s))), Principal => str("Principal"), - Opt(ref t) => str("?").append(pp_ty(t)), - Vec(ref t) => pp_vec(t, None), - Record(ref fs) => pp_record(fs, None), - Variant(ref fs) => pp_variant(fs, None), - Func(ref func) => pp_function(func), - Service(ref serv) => pp_service(serv, None), - Class(ref args, ref t) => pp_class((args, t), None), + Opt(ref t) => str("?").append(pp_ty(ctx, t)), + Vec(ref t) => pp_vec(ctx, t, None), + Record(ref fs) => pp_record(ctx, fs, None), + Variant(ref fs) => pp_variant(ctx, fs, None), + Func(ref func) => pp_function(ctx, func), + Service(ref serv) => pp_service(ctx, serv, None), + Class(ref args, ref t) => pp_class(ctx, (args, t), None), Knot(_) | Unknown | Future => unreachable!(), } } @@ -161,9 +219,9 @@ fn pp_label(id: &SharedLabel) -> RcDoc<'_> { } } -fn pp_function(func: &Function) -> RcDoc<'_> { - let args = pp_args(&func.args); - let rets = pp_rets(&func.rets); +fn pp_function<'a>(ctx: BindingCtx<'a>, func: &'a Function) -> RcDoc<'a> { + let args = pp_args(ctx, &func.args); + let rets = pp_rets(ctx, &func.rets); match func.modes.as_slice() { [FuncMode::Oneway] => kwd("shared").append(args).append(" -> ").append("()"), [FuncMode::Query] => kwd("shared query") @@ -185,27 +243,32 @@ fn pp_function(func: &Function) -> RcDoc<'_> { } .nest(INDENT_SPACE) } -fn pp_args(args: &[Type]) -> RcDoc<'_> { + +fn pp_args<'a>(ctx: BindingCtx<'a>, args: &'a [Type]) -> RcDoc<'a> { match args { [ty] => { if is_tuple(ty) { - enclose("(", pp_ty(ty), ")") + enclose("(", pp_ty(ctx, ty), ")") } else { - pp_ty(ty) + pp_ty(ctx, ty) } } _ => { - let doc = concat(args.iter().map(pp_ty), ","); + let doc = concat(args.iter().map(|ty| pp_ty(ctx, ty)), ","); enclose("(", doc, ")") } } } -fn pp_rets(args: &[Type]) -> RcDoc<'_> { - pp_args(args) +fn pp_rets<'a>(ctx: BindingCtx<'a>, args: &'a [Type]) -> RcDoc<'a> { + pp_args(ctx, args) } -fn pp_service<'a>(serv: &'a [(String, Type)], syntax: Option<&'a [syntax::Binding]>) -> RcDoc<'a> { +fn pp_service<'a>( + ctx: BindingCtx<'a>, + serv: &'a [(String, Type)], + syntax: Option<&'a [syntax::Binding]>, +) -> RcDoc<'a> { let methods = serv.iter().map(|(id, func)| { let mut docs = RcDoc::nil(); let mut syntax_field_ty = None; @@ -217,21 +280,21 @@ fn pp_service<'a>(serv: &'a [(String, Type)], syntax: Option<&'a [syntax::Bindin } docs.append(escape(id, true)) .append(" : ") - .append(pp_ty_rich(func, syntax_field_ty)) + .append(pp_ty_rich(ctx, func, syntax_field_ty)) }); kwd("actor").append(enclose_space("{", concat(methods, ";"), "}")) } -fn pp_tuple<'a>(fields: &'a [Field]) -> RcDoc<'a> { - let tuple = concat(fields.iter().map(|f| pp_ty(&f.ty)), ","); +fn pp_tuple<'a>(ctx: BindingCtx<'a>, fields: &'a [Field]) -> RcDoc<'a> { + let tuple = concat(fields.iter().map(|f| pp_ty(ctx, &f.ty)), ","); enclose("(", tuple, ")") } -fn pp_vec<'a>(inner: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { +fn pp_vec<'a>(ctx: BindingCtx<'a>, inner: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { if matches!(inner.as_ref(), TypeInner::Nat8) { str("Blob") } else { - enclose("[", pp_ty_rich(inner, syntax), "]") + enclose("[", pp_ty_rich(ctx, inner, syntax), "]") } } @@ -250,20 +313,28 @@ fn find_field<'a>( (docs, syntax_field_ty) } -fn pp_record<'a>(fields: &'a [Field], syntax: Option<&'a [syntax::TypeField]>) -> RcDoc<'a> { +fn pp_record<'a>( + ctx: BindingCtx<'a>, + fields: &'a [Field], + syntax: Option<&'a [syntax::TypeField]>, +) -> RcDoc<'a> { if is_tuple_fields(fields) { - return pp_tuple(fields); + return pp_tuple(ctx, fields); } let fields = fields.iter().map(|field| { let (docs, syntax_field) = find_field(syntax, &field.id); docs.append(pp_label(&field.id)) .append(" : ") - .append(pp_ty_rich(&field.ty, syntax_field)) + .append(pp_ty_rich(ctx, &field.ty, syntax_field)) }); enclose_space("{", concat(fields, ";"), "}") } -fn pp_variant<'a>(fields: &'a [Field], syntax: Option<&'a [syntax::TypeField]>) -> RcDoc<'a> { +fn pp_variant<'a>( + ctx: BindingCtx<'a>, + fields: &'a [Field], + syntax: Option<&'a [syntax::TypeField]>, +) -> RcDoc<'a> { if fields.is_empty() { return str("{#}"); } @@ -272,7 +343,7 @@ fn pp_variant<'a>(fields: &'a [Field], syntax: Option<&'a [syntax::TypeField]>) let doc = docs.append(str("#")).append(pp_label(&field.id)); if *field.ty != TypeInner::Null { doc.append(" : ") - .append(pp_ty_rich(&field.ty, syntax_field)) + .append(pp_ty_rich(ctx, &field.ty, syntax_field)) } else { doc } @@ -280,11 +351,15 @@ fn pp_variant<'a>(fields: &'a [Field], syntax: Option<&'a [syntax::TypeField]>) enclose_space("{", concat(fields, ";"), "}") } -fn pp_class<'a>((args, ty): (&'a [Type], &'a Type), syntax: Option<&'a IDLType>) -> RcDoc<'a> { - let doc = pp_args(args).append(" -> async "); +fn pp_class<'a>( + ctx: BindingCtx<'a>, + (args, ty): (&'a [Type], &'a Type), + syntax: Option<&'a IDLType>, +) -> RcDoc<'a> { + let doc = pp_args(ctx, args).append(" -> async "); match ty.as_ref() { - TypeInner::Service(_) => doc.append(pp_ty_rich(ty, syntax)), - TypeInner::Var(_) => doc.append(pp_ty(ty)), + TypeInner::Service(_) => doc.append(pp_ty_rich(ctx, ty, syntax)), + TypeInner::Var(_) => doc.append(pp_ty(ctx, ty)), _ => unreachable!(), } } @@ -293,21 +368,22 @@ fn pp_docs<'a>(docs: &'a [String]) -> RcDoc<'a> { lines(docs.iter().map(|line| RcDoc::text("/// ").append(line))) } -fn pp_defs<'a>(env: &'a TypeEnv, prog: &'a IDLMergedProg) -> RcDoc<'a> { - lines(env.0.iter().map(|(id, ty)| { +fn pp_defs<'a>(ctx: BindingCtx<'a>, prog: &'a IDLMergedProg) -> RcDoc<'a> { + lines(ctx.env.0.iter().map(|(id, ty)| { let syntax = prog.lookup(id); let docs = syntax .map(|b| pp_docs(b.docs.as_ref())) .unwrap_or(RcDoc::nil()); + let name = ctx.names[id].clone(); docs.append(kwd("public type")) - .append(escape(id, false)) + .append(RcDoc::text(name)) .append(" = ") - .append(pp_ty_rich(ty, syntax.map(|b| &b.typ))) + .append(pp_ty_rich(ctx, ty, syntax.map(|b| &b.typ))) .append(";") })) } -fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { +fn pp_actor<'a>(ctx: BindingCtx<'a>, ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { let self_doc = kwd("public type Self ="); match ty.as_ref() { TypeInner::Service(ref serv) => match syntax { @@ -316,9 +392,10 @@ fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { docs, }) => { let docs = pp_docs(docs); - docs.append(self_doc).append(pp_service(serv, Some(fields))) + docs.append(self_doc) + .append(pp_service(ctx, serv, Some(fields))) } - _ => pp_service(serv, None), + _ => pp_service(ctx, serv, None), }, TypeInner::Class(ref args, ref t) => match syntax { Some(IDLActorType { @@ -327,33 +404,45 @@ fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { }) => { let docs = pp_docs(docs); docs.append(self_doc) - .append(pp_class((args, t), Some(syntax_t))) + .append(pp_class(ctx, (args, t), Some(syntax_t))) } - _ => self_doc.append(pp_class((args, t), None)), + _ => self_doc.append(pp_class(ctx, (args, t), None)), }, - TypeInner::Var(_) => self_doc.append(pp_ty(ty)), + TypeInner::Var(_) => self_doc.append(pp_ty(ctx, ty)), _ => unreachable!(), } } -pub fn compile(env: &TypeEnv, actor: &Option, prog: &IDLMergedProg) -> String { +// Separate from `compile` so that `names` and `syntax_actor` (locals in +// `compile`) are provably live for the entire lifetime of the RcDoc they feed. +fn compile_inner<'a>( + ctx: BindingCtx<'a>, + actor: &'a Option, + syntax_actor: Option<&'a IDLActorType>, + prog: &'a IDLMergedProg, +) -> String { let header = r#"// This is a generated Motoko binding. // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. "#; - let syntax_actor = prog.resolve_actor().ok().flatten(); let doc = match actor { - None => pp_defs(env, prog), - Some(actor) => { - let defs = pp_defs(env, prog); - let actor = pp_actor(actor, syntax_actor.as_ref()); - defs.append(actor) - } + None => pp_defs(ctx, prog), + Some(actor) => pp_defs(ctx, prog).append(pp_actor(ctx, actor, syntax_actor)), }; - let doc = RcDoc::text(header) + RcDoc::text(header) .append(RcDoc::line()) .append("module ") .append(enclose_space("{", doc, "}")) .pretty(LINE_WIDTH) - .to_string(); - doc + .to_string() +} + +pub fn compile(env: &TypeEnv, actor: &Option, prog: &IDLMergedProg) -> String { + let syntax_actor = prog.resolve_actor().ok().flatten(); + let names = build_names(env); + compile_inner( + BindingCtx { env, names: &names }, + actor, + syntax_actor.as_ref(), + prog, + ) } diff --git a/rust/candid_parser/tests/assets/ok/actor.mo b/rust/candid_parser/tests/assets/ok/actor.mo index 98531f3f..1e453f78 100644 --- a/rust/candid_parser/tests/assets/ok/actor.mo +++ b/rust/candid_parser/tests/assets/ok/actor.mo @@ -2,15 +2,15 @@ // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. module { - public type f = shared Int8 -> async Int8; - public type g = f; - public type h = shared f -> async f; - public type o = ?o; + public type F = shared Int8 -> async Int8; + public type G = F; + public type H = shared F -> async F; + public type O = ?O; public type Self = actor { - f : shared Nat -> async h; - g : f; - h : g; - h2 : h; - o : shared o -> async o; + f : shared Nat -> async H; + g : F; + h : G; + h2 : H; + o : shared O -> async O; } } diff --git a/rust/candid_parser/tests/assets/ok/comment.mo b/rust/candid_parser/tests/assets/ok/comment.mo index 789f1428..b0d103b4 100644 --- a/rust/candid_parser/tests/assets/ok/comment.mo +++ b/rust/candid_parser/tests/assets/ok/comment.mo @@ -4,6 +4,6 @@ module { /// line comment /// - public type id = Nat8; + public type Id = Nat8; } diff --git a/rust/candid_parser/tests/assets/ok/example.mo b/rust/candid_parser/tests/assets/ok/example.mo index 8dd8d9bf..732295cf 100644 --- a/rust/candid_parser/tests/assets/ok/example.mo +++ b/rust/candid_parser/tests/assets/ok/example.mo @@ -14,20 +14,20 @@ module { public type a = { #a; #b : b }; public type b = (Int, Nat); /// Doc comment for broker service - public type broker = actor { + public type Broker = actor { find : shared Text -> async actor { current : shared () -> async Nat32; up : shared () -> async (); }; }; - public type f = shared (List, shared Int32 -> async Int64) -> async ( + public type F = shared (List, shared Int32 -> async Int64) -> async ( ?List, - res, + Res, ); - public type list = ?node; + public type list = ?Node; /// Doc comment for prim type - public type my_type = Principal; - public type my_variant = { + public type MyType = Principal; + public type MyVariant = { /// Doc comment for my_variant field a #a : { /// Doc comment for my_variant field a field b @@ -46,7 +46,7 @@ module { }; }; /// Doc comment for nested type - public type nested = { + public type Nested = { _0_ : Nat; _1_ : Nat; /// Doc comment for nested record @@ -57,14 +57,14 @@ module { _42_ : Nat; }; /// Doc comment for nested_records - public type nested_records = { + public type NestedRecords = { /// Doc comment for nested_records field nested nested : ?{ /// Doc comment for nested_records field nested_field nested_field : Text; }; }; - public type nested_res = { + public type NestedRes = { #Ok : { #Ok; #Err }; #Err : { /// Doc comment for Ok in nested variant @@ -73,9 +73,9 @@ module { #Err : { _0_ : Int }; }; }; - public type node = { head : Nat; tail : list }; + public type Node = { head : Nat; tail : list }; /// Doc comment for res type - public type res = { + public type Res = { /// Doc comment for Ok variant #Ok : (Int, Nat); /// Doc comment for Err variant @@ -86,36 +86,36 @@ module { }; }; /// Doc comment for service id - public type s = actor { f : t; g : shared list -> async (B, tree, stream) }; - public type stream = ?{ head : Nat; next : shared query () -> async stream }; - public type t = shared s -> async (); - public type tree = { - #branch : { val : Int; left : tree; right : tree }; + 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 (); + public type Tree = { + #branch : { val : Int; left : Tree; right : Tree }; #leaf : Int; }; /// Doc comment for service public type Self = actor { /// Doc comment for f1 method of service f1 : shared (list, Blob, ?Bool) -> (); - g1 : shared query (my_type, List, ?List, nested) -> async ( + g1 : shared query (MyType, List, ?List, Nested) -> async ( Int, - broker, - nested_res, + Broker, + NestedRes, ); h : shared ([?Text], { #A : Nat; #B : ?Text }, ?List) -> async { _42_ : {}; id : Nat; }; /// Doc comment for i method of service - i : f; + i : F; x : shared composite query (a, b) -> async ( ?a, ?b, { #Ok : { result : Text }; #Err : { #a; #b } }, ); - y : shared query nested_records -> async ((nested_records, my_variant)); - f : t; - g : shared list -> async (B, tree, stream); + y : shared query NestedRecords -> async ((NestedRecords, MyVariant)); + f : T; + g : shared list -> async (B, Tree, Stream); /// Doc comment for imported bbbbb service method bbbbb : shared b -> async (); } diff --git a/rust/candid_parser/tests/assets/ok/fieldnat.mo b/rust/candid_parser/tests/assets/ok/fieldnat.mo index 64f77183..723877de 100644 --- a/rust/candid_parser/tests/assets/ok/fieldnat.mo +++ b/rust/candid_parser/tests/assets/ok/fieldnat.mo @@ -2,14 +2,14 @@ // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. module { - public type non_tuple = { _1_ : Text; _2_ : Text }; - public type tuple = (Text, Text); + public type NonTuple = { _1_ : Text; _2_ : Text }; + public type Tuple = (Text, Text); public type Self = actor { bab : shared (Int, Nat) -> async (); bar : shared { _50_ : Int } -> async { #e20; #e30 }; bas : shared ((Int, Int)) -> async ((Text, Nat)); baz : shared { _2_ : Int; _50_ : Nat } -> async {}; - bba : shared tuple -> async non_tuple; + bba : shared Tuple -> async NonTuple; bib : shared { _0_ : Int } -> async { #_0_ : Int }; foo : shared { _2_ : Int } -> async { _2_ : Int; _2 : Int }; } diff --git a/rust/candid_parser/tests/assets/ok/keyword.mo b/rust/candid_parser/tests/assets/ok/keyword.mo index 2802968e..3608837f 100644 --- a/rust/candid_parser/tests/assets/ok/keyword.mo +++ b/rust/candid_parser/tests/assets/ok/keyword.mo @@ -2,26 +2,26 @@ // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. module { - public type if_ = { - #branch : { val : Int; left : if_; right : if_ }; + public type If = { + #branch : { val : Int; left : If; right : If }; #leaf : Int; }; - public type list = ?node; - public type node = { head : Nat; tail : list }; - public type o = ?o; - public type return_ = actor { f : t; g : shared list -> async (if_, stream) }; - public type stream = ?{ head : Nat; next : shared query () -> async stream }; - public type t = shared return_ -> async (); + public type List = ?Node; + public type Node = { head : Nat; tail : List }; + public type O = ?O; + public type Return = actor { f : T; g : shared List -> async (If, Stream) }; + public type Stream = ?{ head : Nat; next : shared query () -> async Stream }; + public type T = shared Return -> async (); public type Self = actor { Oneway : shared () -> (); - f__ : shared o -> async o; + f__ : shared O -> async O; field : shared { test : Nat16; _1291438163_ : Nat8 } -> async {}; fieldnat : shared { _2_ : Int; _50_ : Nat } -> async { _0_ : Int }; oneway : shared Nat8 -> (); oneway__ : shared Nat8 -> (); query_ : shared query Blob -> async Blob; - return_ : shared o -> async o; - service : t; + return_ : shared O -> async O; + service : T; tuple : shared ((Int, Blob, Text)) -> async ((Int, Nat8)); variant : shared { #A; #B; #C; #D : Float } -> async (); } diff --git a/rust/candid_parser/tests/assets/ok/management.mo b/rust/candid_parser/tests/assets/ok/management.mo index 0609c738..34157330 100644 --- a/rust/candid_parser/tests/assets/ok/management.mo +++ b/rust/candid_parser/tests/assets/ok/management.mo @@ -2,81 +2,79 @@ // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. module { - public type bitcoin_address = Text; - public type bitcoin_network = { #mainnet; #testnet }; - public type block_hash = Blob; - public type canister_id = Principal; - public type canister_settings = { + public type BitcoinAddress = Text; + public type BitcoinNetwork = { #mainnet; #testnet }; + public type BlockHash = Blob; + public type CanisterId = Principal; + public type CanisterSettings = { freezing_threshold : ?Nat; controllers : ?[Principal]; memory_allocation : ?Nat; compute_allocation : ?Nat; }; - public type definite_canister_settings = { + public type DefiniteCanisterSettings = { freezing_threshold : Nat; controllers : [Principal]; memory_allocation : Nat; compute_allocation : Nat; }; - public type ecdsa_curve = { #secp256k1 }; - public type get_balance_request = { - network : bitcoin_network; - address : bitcoin_address; + public type EcdsaCurve = { #secp256k1 }; + public type GetBalanceRequest = { + network : BitcoinNetwork; + address : BitcoinAddress; min_confirmations : ?Nat32; }; - public type get_current_fee_percentiles_request = { - network : bitcoin_network; - }; - public type get_utxos_request = { - network : bitcoin_network; + public type GetCurrentFeePercentilesRequest = { network : BitcoinNetwork }; + public type GetUtxosRequest = { + network : BitcoinNetwork; filter : ?{ #page : Blob; #min_confirmations : Nat32 }; - address : bitcoin_address; + address : BitcoinAddress; }; - public type get_utxos_response = { + public type GetUtxosResponse = { next_page : ?Blob; tip_height : Nat32; - tip_block_hash : block_hash; - utxos : [utxo]; + tip_block_hash : BlockHash; + utxos : [Utxo]; }; - public type http_header = { value : Text; name : Text }; - public type http_response = { + public type HttpHeader = { value : Text; name : Text }; + public type HttpResponse = { status : Nat; body : Blob; - headers : [http_header]; + headers : [HttpHeader]; }; - public type millisatoshi_per_byte = Nat64; - public type outpoint = { txid : Blob; vout : Nat32 }; - public type satoshi = Nat64; - public type send_transaction_request = { + public type MillisatoshiPerByte = Nat64; + public type Outpoint = { txid : Blob; vout : Nat32 }; + public type Satoshi = Nat64; + public type SendTransactionRequest = { transaction : Blob; - network : bitcoin_network; + network : BitcoinNetwork; }; - public type user_id = Principal; - public type utxo = { height : Nat32; value : satoshi; outpoint : outpoint }; - public type wasm_module = Blob; + public type UserId = Principal; + public type Utxo = { height : Nat32; value : Satoshi; outpoint : Outpoint }; + public type WasmModule = Blob; public type Self = actor { - bitcoin_get_balance : shared get_balance_request -> async satoshi; - bitcoin_get_current_fee_percentiles : shared get_current_fee_percentiles_request -> async [ - millisatoshi_per_byte + bitcoin_get_balance : shared GetBalanceRequest -> async Satoshi; + bitcoin_get_current_fee_percentiles : shared GetCurrentFeePercentilesRequest -> async [ + MillisatoshiPerByte ]; - bitcoin_get_utxos : shared get_utxos_request -> async get_utxos_response; - bitcoin_send_transaction : shared send_transaction_request -> async (); - canister_status : shared { canister_id : canister_id } -> async { + bitcoin_get_utxos : shared GetUtxosRequest -> async GetUtxosResponse; + bitcoin_send_transaction : shared SendTransactionRequest -> async (); + canister_status : shared { canister_id : CanisterId } -> async { status : { #stopped; #stopping; #running }; memory_size : Nat; cycles : Nat; - settings : definite_canister_settings; + settings : DefiniteCanisterSettings; idle_cycles_burned_per_day : Nat; module_hash : ?Blob; }; - create_canister : shared { settings : ?canister_settings } -> async { - canister_id : canister_id; + create_canister : shared { settings : ?CanisterSettings } -> async { + canister_id : CanisterId; }; - delete_canister : shared { canister_id : canister_id } -> async (); - deposit_cycles : shared { canister_id : canister_id } -> async (); + delete_canister : shared { canister_id : CanisterId } -> async (); + deposit_cycles : shared { canister_id : CanisterId } -> async (); ecdsa_public_key : shared { - key_id : { name : Text; curve : ecdsa_curve }; - canister_id : ?canister_id; + key_id : { name : Text; curve : EcdsaCurve }; + canister_id : ?CanisterId; derivation_path : [Blob]; } -> async { public_key : Blob; chain_code : Blob }; http_request : shared { @@ -87,39 +85,39 @@ module { transform : ?{ function : shared query { context : Blob; - response : http_response; - } -> async http_response; + response : HttpResponse; + } -> async HttpResponse; context : Blob; }; - headers : [http_header]; - } -> async http_response; + headers : [HttpHeader]; + } -> async HttpResponse; install_code : shared { arg : Blob; - wasm_module : wasm_module; + wasm_module : WasmModule; mode : { #reinstall; #upgrade; #install }; - canister_id : canister_id; + canister_id : CanisterId; } -> async (); provisional_create_canister_with_cycles : shared { - settings : ?canister_settings; - specified_id : ?canister_id; + settings : ?CanisterSettings; + specified_id : ?CanisterId; amount : ?Nat; - } -> async { canister_id : canister_id }; + } -> async { canister_id : CanisterId }; provisional_top_up_canister : shared { - canister_id : canister_id; + canister_id : CanisterId; amount : Nat; } -> async (); raw_rand : shared () -> async Blob; sign_with_ecdsa : shared { - key_id : { name : Text; curve : ecdsa_curve }; + key_id : { name : Text; curve : EcdsaCurve }; derivation_path : [Blob]; message_hash : Blob; } -> async { signature : Blob }; - start_canister : shared { canister_id : canister_id } -> async (); - stop_canister : shared { canister_id : canister_id } -> async (); - uninstall_code : shared { canister_id : canister_id } -> async (); + start_canister : shared { canister_id : CanisterId } -> async (); + stop_canister : shared { canister_id : CanisterId } -> async (); + uninstall_code : shared { canister_id : CanisterId } -> async (); update_settings : shared { canister_id : Principal; - settings : canister_settings; + settings : CanisterSettings; } -> async (); } } diff --git a/rust/candid_parser/tests/assets/ok/pascal_collision.d.ts b/rust/candid_parser/tests/assets/ok/pascal_collision.d.ts new file mode 100644 index 00000000..3e1f1ee2 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/pascal_collision.d.ts @@ -0,0 +1,15 @@ +import type { Principal } from '@icp-sdk/core/principal'; +import type { ActorMethod } from '@icp-sdk/core/agent'; +import type { IDL } from '@icp-sdk/core/candid'; + +/** + * PascalCase output collides with a verbatim env key — foo_baz should fall back. + */ +export type FooBaz = bigint; +export type fooBar = string; +/** + * Two names that map to the same PascalCase form — first alphabetically wins, second falls back. + */ +export type foo_bar = bigint; +export type foo_baz = string; + diff --git a/rust/candid_parser/tests/assets/ok/pascal_collision.did b/rust/candid_parser/tests/assets/ok/pascal_collision.did new file mode 100644 index 00000000..fb31dbab --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/pascal_collision.did @@ -0,0 +1,7 @@ +// Two names that map to the same PascalCase form — first alphabetically wins, second falls back. +type foo_bar = nat; +type fooBar = text; +// PascalCase output collides with a verbatim env key — foo_baz should fall back. +type FooBaz = nat; +type foo_baz = text; + diff --git a/rust/candid_parser/tests/assets/ok/pascal_collision.js b/rust/candid_parser/tests/assets/ok/pascal_collision.js new file mode 100644 index 00000000..a0b3ee07 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/pascal_collision.js @@ -0,0 +1,5 @@ +const FooBaz = IDL.Nat; +const fooBar = IDL.Text; +const foo_bar = IDL.Nat; +const foo_baz = IDL.Text; + diff --git a/rust/candid_parser/tests/assets/ok/pascal_collision.mo b/rust/candid_parser/tests/assets/ok/pascal_collision.mo new file mode 100644 index 00000000..142144b2 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/pascal_collision.mo @@ -0,0 +1,12 @@ +// This is a generated Motoko binding. +// Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. + +module { + /// PascalCase output collides with a verbatim env key — foo_baz should fall back. + public type FooBaz = Nat; + public type FooBar = Text; + /// Two names that map to the same PascalCase form — first alphabetically wins, second falls back. + public type foo_bar = Nat; + public type foo_baz = Text; + +} diff --git a/rust/candid_parser/tests/assets/ok/pascal_collision.rs b/rust/candid_parser/tests/assets/ok/pascal_collision.rs new file mode 100644 index 00000000..1ec9dc1f --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/pascal_collision.rs @@ -0,0 +1,14 @@ +// This is an experimental feature to generate Rust binding from Candid. +// You may want to manually adjust some of the types. +#![allow(dead_code, unused_imports)] +use candid::{self, CandidType, Deserialize, Principal}; +use ic_cdk::api::call::CallResult as Result; + +/// PascalCase output collides with a verbatim env key — foo_baz should fall back. +pub type FooBaz = candid::Nat; +pub type FooBar = String; +/// Two names that map to the same PascalCase form — first alphabetically wins, second falls back. +pub type FooBar = candid::Nat; +pub type FooBaz = String; + + diff --git a/rust/candid_parser/tests/assets/ok/recursion.mo b/rust/candid_parser/tests/assets/ok/recursion.mo index cdb3eb51..50a33912 100644 --- a/rust/candid_parser/tests/assets/ok/recursion.mo +++ b/rust/candid_parser/tests/assets/ok/recursion.mo @@ -4,15 +4,15 @@ module { public type A = B; public type B = ?A; - public type list = ?node; - public type node = { head : Nat; tail : list }; + public type List = ?Node; + public type Node = { head : Nat; tail : List }; /// Doc comment for service id - public type s = actor { f : t; g : shared list -> async (B, tree, stream) }; - public type stream = ?{ head : Nat; next : shared query () -> async stream }; - public type t = shared s -> async (); - public type tree = { - #branch : { val : Int; left : tree; right : tree }; + 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 (); + public type Tree = { + #branch : { val : Int; left : Tree; right : Tree }; #leaf : Int; }; - public type Self = s + public type Self = S } diff --git a/rust/candid_parser/tests/assets/ok/recursive_class.mo b/rust/candid_parser/tests/assets/ok/recursive_class.mo index 7d6af941..f5ba8006 100644 --- a/rust/candid_parser/tests/assets/ok/recursive_class.mo +++ b/rust/candid_parser/tests/assets/ok/recursive_class.mo @@ -2,6 +2,6 @@ // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. module { - public type s = actor { next : shared () -> async s }; - public type Self = s -> async s + public type S = actor { next : shared () -> async S }; + public type Self = S -> async S } diff --git a/rust/candid_parser/tests/assets/ok/self_type.d.ts b/rust/candid_parser/tests/assets/ok/self_type.d.ts new file mode 100644 index 00000000..aa7baa06 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/self_type.d.ts @@ -0,0 +1,13 @@ +import type { Principal } from '@icp-sdk/core/principal'; +import type { ActorMethod } from '@icp-sdk/core/agent'; +import type { IDL } from '@icp-sdk/core/candid'; + +/** + * Verbatim "Self" in env — falls back to "Self_" to avoid shadowing pp_actor output. + */ +export type Self = string; +/** + * "self" would PascalCase to "Self" which is reserved — falls back to "self". + */ +export type self = bigint; + diff --git a/rust/candid_parser/tests/assets/ok/self_type.did b/rust/candid_parser/tests/assets/ok/self_type.did new file mode 100644 index 00000000..cf61bd6e --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/self_type.did @@ -0,0 +1,5 @@ +// "self" would PascalCase to "Self" which is reserved — falls back to "self". +type self = nat; +// Verbatim "Self" in env — falls back to "Self_" to avoid shadowing pp_actor output. +type Self = text; + diff --git a/rust/candid_parser/tests/assets/ok/self_type.js b/rust/candid_parser/tests/assets/ok/self_type.js new file mode 100644 index 00000000..35f2aa41 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/self_type.js @@ -0,0 +1,3 @@ +const Self = IDL.Text; +const self = IDL.Nat; + diff --git a/rust/candid_parser/tests/assets/ok/self_type.mo b/rust/candid_parser/tests/assets/ok/self_type.mo new file mode 100644 index 00000000..1a8fca16 --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/self_type.mo @@ -0,0 +1,10 @@ +// This is a generated Motoko binding. +// Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. + +module { + /// Verbatim "Self" in env — falls back to "Self_" to avoid shadowing pp_actor output. + public type Self_ = Text; + /// "self" would PascalCase to "Self" which is reserved — falls back to "self". + public type self = Nat; + +} diff --git a/rust/candid_parser/tests/assets/ok/self_type.rs b/rust/candid_parser/tests/assets/ok/self_type.rs new file mode 100644 index 00000000..17feaa0f --- /dev/null +++ b/rust/candid_parser/tests/assets/ok/self_type.rs @@ -0,0 +1,12 @@ +// This is an experimental feature to generate Rust binding from Candid. +// You may want to manually adjust some of the types. +#![allow(dead_code, unused_imports)] +use candid::{self, CandidType, Deserialize, Principal}; +use ic_cdk::api::call::CallResult as Result; + +/// Verbatim "Self" in env — falls back to "Self_" to avoid shadowing pp_actor output. +pub type Self_ = String; +/// "self" would PascalCase to "Self" which is reserved — falls back to "self". +pub type Self_ = candid::Nat; + + diff --git a/rust/candid_parser/tests/assets/pascal_collision.did b/rust/candid_parser/tests/assets/pascal_collision.did new file mode 100644 index 00000000..f573bba9 --- /dev/null +++ b/rust/candid_parser/tests/assets/pascal_collision.did @@ -0,0 +1,6 @@ +// Two names that map to the same PascalCase form — first alphabetically wins, second falls back. +type foo_bar = nat; +type fooBar = text; +// PascalCase output collides with a verbatim env key — foo_baz should fall back. +type FooBaz = nat; +type foo_baz = text; diff --git a/rust/candid_parser/tests/assets/self_type.did b/rust/candid_parser/tests/assets/self_type.did new file mode 100644 index 00000000..5f7cfda2 --- /dev/null +++ b/rust/candid_parser/tests/assets/self_type.did @@ -0,0 +1,4 @@ +// "self" would PascalCase to "Self" which is reserved — falls back to "self". +type self = nat; +// Verbatim "Self" in env — falls back to "Self_" to avoid shadowing pp_actor output. +type Self = text;