diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3ff6eb6..25f2da21d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Breaking changes: + `pp_args` and `pp_init_args` now require a `&[ArgType]` parameter. The `pp_rets` function has been added, with the signature of the old `pp_args`. + + `pretty::candid::pp_label` now takes a `&Label` parameter, instead of a `&SharedLabel`. * Non-breaking changes: + The following structs have been moved from the `candid_parser` crate to the `candid::types::syntax` module: diff --git a/rust/candid/src/pretty/candid.rs b/rust/candid/src/pretty/candid.rs index 44d8894a3..2f2d2a858 100644 --- a/rust/candid/src/pretty/candid.rs +++ b/rust/candid/src/pretty/candid.rs @@ -1,7 +1,5 @@ use crate::pretty::utils::*; -use crate::types::{ - ArgType, Field, FuncMode, Function, Label, SharedLabel, Type, TypeEnv, TypeInner, -}; +use crate::types::{ArgType, Field, FuncMode, Function, Label, Type, TypeEnv, TypeInner}; use pretty::RcDoc; static KEYWORDS: [&str; 30] = [ @@ -128,8 +126,8 @@ pub fn pp_ty_inner(ty: &TypeInner) -> RcDoc { } } -pub fn pp_label(id: &SharedLabel) -> RcDoc { - match &**id { +pub fn pp_label(id: &Label) -> RcDoc { + match id { Label::Named(id) => pp_text(id), Label::Id(_) | Label::Unnamed(_) => RcDoc::as_string(id), } @@ -238,7 +236,7 @@ pub fn compile(env: &TypeEnv, actor: &Option) -> String { #[cfg_attr(docsrs, doc(cfg(feature = "value")))] #[cfg(feature = "value")] pub mod value { - use super::{ident_string, pp_text}; + use super::{ident_string, pp_label}; use crate::pretty::utils::*; use crate::types::value::{IDLArgs, IDLField, IDLValue}; use crate::types::Label; @@ -440,13 +438,6 @@ pub mod value { } } - fn pp_label(id: &Label) -> RcDoc { - match id { - Label::Named(id) => pp_text(id), - Label::Id(_) | Label::Unnamed(_) => RcDoc::as_string(id), - } - } - fn pp_field(depth: usize, field: &IDLField, is_variant: bool) -> RcDoc { let val_doc = if is_variant && field.val == IDLValue::Null { RcDoc::nil() diff --git a/rust/candid/src/pretty/mod.rs b/rust/candid/src/pretty/mod.rs index 7797e8dcc..5eda13cf4 100644 --- a/rust/candid/src/pretty/mod.rs +++ b/rust/candid/src/pretty/mod.rs @@ -1,4 +1,5 @@ //! pretty printer for Candid type and value pub mod candid; +pub mod syntax; pub mod utils; diff --git a/rust/candid/src/pretty/syntax.rs b/rust/candid/src/pretty/syntax.rs new file mode 100644 index 000000000..e4ea26e71 --- /dev/null +++ b/rust/candid/src/pretty/syntax.rs @@ -0,0 +1,177 @@ +use pretty::RcDoc; + +use crate::{ + pretty::{ + candid::{pp_label, pp_modes, pp_text}, + utils::{concat, enclose, enclose_space, ident, kwd, lines, str, INDENT_SPACE, LINE_WIDTH}, + }, + types::syntax::{ + Binding, FuncType, IDLActorType, IDLArgType, IDLMergedProg, IDLType, PrimType, TypeField, + }, +}; + +fn pp_ty(ty: &IDLType) -> RcDoc { + use IDLType::*; + match ty { + PrimT(PrimType::Null) => str("null"), + PrimT(PrimType::Bool) => str("bool"), + PrimT(PrimType::Nat) => str("nat"), + PrimT(PrimType::Int) => str("int"), + PrimT(PrimType::Nat8) => str("nat8"), + PrimT(PrimType::Nat16) => str("nat16"), + PrimT(PrimType::Nat32) => str("nat32"), + PrimT(PrimType::Nat64) => str("nat64"), + PrimT(PrimType::Int8) => str("int8"), + PrimT(PrimType::Int16) => str("int16"), + PrimT(PrimType::Int32) => str("int32"), + PrimT(PrimType::Int64) => str("int64"), + PrimT(PrimType::Float32) => str("float32"), + PrimT(PrimType::Float64) => str("float64"), + PrimT(PrimType::Text) => str("text"), + PrimT(PrimType::Reserved) => str("reserved"), + PrimT(PrimType::Empty) => str("empty"), + VarT(ref s) => str(s), + PrincipalT => str("principal"), + OptT(ref t) => pp_opt(t), + VecT(ref t) => pp_vec(t), + RecordT(ref fs) => pp_record(fs, ty.is_tuple()), + VariantT(ref fs) => pp_variant(fs), + FuncT(ref func) => pp_function(func), + ServT(ref serv) => pp_service(serv), + ClassT(ref args, ref t) => pp_class(args, t), + } +} + +fn pp_field(field: &TypeField, is_variant: bool) -> RcDoc { + let docs = pp_docs(&field.docs); + let ty_doc = if is_variant && field.typ == IDLType::PrimT(PrimType::Null) { + RcDoc::nil() + } else { + kwd(" :").append(pp_ty(&field.typ)) + }; + docs.append(pp_label(&field.label)).append(ty_doc) +} + +fn pp_fields(fs: &[TypeField], is_variant: bool) -> RcDoc { + let fields = fs.iter().map(|f| pp_field(f, is_variant)); + enclose_space("{", concat(fields, ";"), "}") +} + +fn pp_opt(ty: &IDLType) -> RcDoc { + kwd("opt").append(pp_ty(ty)) +} + +fn pp_vec(ty: &IDLType) -> RcDoc { + if matches!(ty, IDLType::PrimT(PrimType::Nat8)) { + str("blob") + } else { + kwd("vec").append(pp_ty(ty)) + } +} + +fn pp_record(fs: &[TypeField], is_tuple: bool) -> RcDoc { + if is_tuple { + let tuple = concat(fs.iter().map(|f| pp_ty(&f.typ)), ";"); + kwd("record").append(enclose_space("{", tuple, "}")) + } else { + kwd("record").append(pp_fields(fs, false)) + } +} + +fn pp_variant(fs: &[TypeField]) -> RcDoc { + kwd("variant").append(pp_fields(fs, true)) +} + +fn pp_function(func: &FuncType) -> RcDoc { + kwd("func").append(pp_method(func)) +} + +fn pp_method(func: &FuncType) -> RcDoc { + let args = pp_args(&func.args); + let rets = pp_rets(&func.rets); + let modes = pp_modes(&func.modes); + args.append(" ->") + .append(RcDoc::space()) + .append(rets.append(modes)) + .nest(INDENT_SPACE) +} + +fn pp_args(args: &[IDLArgType]) -> RcDoc { + let args = args.iter().map(|arg| { + if let Some(name) = &arg.name { + pp_text(name).append(kwd(" :")).append(pp_ty(&arg.typ)) + } else { + pp_ty(&arg.typ) + } + }); + let doc = concat(args, ","); + enclose("(", doc, ")") +} + +fn pp_rets(rets: &[IDLType]) -> RcDoc { + let doc = concat(rets.iter().map(pp_ty), ","); + enclose("(", doc, ")") +} + +fn pp_service(methods: &[Binding]) -> RcDoc { + kwd("service").append(pp_service_methods(methods)) +} + +fn pp_service_methods(methods: &[Binding]) -> RcDoc { + let methods = methods.iter().map(|b| { + let docs = pp_docs(&b.docs); + let func_doc = match b.typ { + IDLType::FuncT(ref f) => pp_method(f), + IDLType::VarT(_) => pp_ty(&b.typ), + _ => unreachable!(), + }; + docs.append(pp_text(&b.id)) + .append(kwd(" :")) + .append(func_doc) + }); + let doc = concat(methods, ";"); + enclose_space("{", doc, "}") +} + +fn pp_class<'a>(args: &'a [IDLArgType], t: &'a IDLType) -> RcDoc<'a> { + let doc = pp_args(args).append(" ->").append(RcDoc::space()); + match t { + IDLType::ServT(ref serv) => doc.append(pp_service_methods(serv)), + IDLType::VarT(ref s) => doc.append(s), + _ => unreachable!(), + } +} + +fn pp_defs(prog: &IDLMergedProg) -> RcDoc { + lines(prog.bindings().map(|b| { + let docs = pp_docs(&b.docs); + docs.append(kwd("type")) + .append(ident(&b.id)) + .append(kwd("=")) + .append(pp_ty(&b.typ)) + .append(";") + })) +} + +fn pp_docs<'a>(docs: &'a [String]) -> RcDoc<'a> { + lines(docs.iter().map(|line| RcDoc::text("// ").append(line))) +} + +fn pp_actor(actor: &IDLActorType) -> RcDoc { + let docs = pp_docs(&actor.docs); + let service_doc = match actor.typ { + IDLType::ServT(ref serv) => pp_service_methods(serv), + IDLType::VarT(_) | IDLType::ClassT(_, _) => pp_ty(&actor.typ), + _ => unreachable!(), + }; + docs.append(kwd("service :")).append(service_doc) +} + +pub fn pretty_print(prog: &IDLMergedProg) -> String { + let actor = prog.resolve_actor().ok().flatten(); + let doc = match actor.as_ref() { + None => pp_defs(prog), + Some(actor) => pp_defs(prog).append(pp_actor(actor)), + }; + doc.pretty(LINE_WIDTH).to_string() +} diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index ded058b6a..6babd6ff4 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -19,6 +19,22 @@ pub enum IDLType { PrincipalT, } +impl IDLType { + pub fn is_tuple(&self) -> bool { + match self { + IDLType::RecordT(ref fs) => { + for (i, field) in fs.iter().enumerate() { + if field.label.get_id() != (i as u32) { + return false; + } + } + true + } + _ => false, + } + } +} + #[derive(Debug, Clone)] pub struct IDLTypes { pub args: Vec, @@ -181,6 +197,10 @@ impl IDLMergedProg { self.typ_decs.iter().map(|b| Dec::TypD(b.clone())).collect() } + pub fn bindings(&self) -> impl Iterator { + self.typ_decs.iter() + } + pub fn resolve_actor(&self) -> Result> { let (init_args, top_level_docs, mut methods) = match &self.main_actor { None => { diff --git a/rust/candid_parser/tests/assets/ok/class.did b/rust/candid_parser/tests/assets/ok/class.did index 4850a508e..a33497f3e 100644 --- a/rust/candid_parser/tests/assets/ok/class.did +++ b/rust/candid_parser/tests/assets/ok/class.did @@ -1,6 +1,8 @@ -type List = opt record { int; List }; type Profile = record { age : nat8; name : text }; +type List = opt record { int; List }; +// Doc comment for class service service : (int, l : List, Profile) -> { + // Doc comment for get method in class service get : () -> (List); set : (List) -> (List); } diff --git a/rust/candid_parser/tests/assets/ok/comment.did b/rust/candid_parser/tests/assets/ok/comment.did index 04bf164ff..89cf08e14 100644 --- a/rust/candid_parser/tests/assets/ok/comment.did +++ b/rust/candid_parser/tests/assets/ok/comment.did @@ -1,2 +1,4 @@ +// line comment +// type id = nat8; diff --git a/rust/candid_parser/tests/assets/ok/cyclic.did b/rust/candid_parser/tests/assets/ok/cyclic.did index eebf3369f..fd0c4b40e 100644 --- a/rust/candid_parser/tests/assets/ok/cyclic.did +++ b/rust/candid_parser/tests/assets/ok/cyclic.did @@ -1,6 +1,6 @@ type A = opt B; -type B = opt C; type C = A; +type B = opt C; type X = Y; type Y = Z; type Z = A; diff --git a/rust/candid_parser/tests/assets/ok/example.did b/rust/candid_parser/tests/assets/ok/example.did index aa5eb7095..48980d55d 100644 --- a/rust/candid_parser/tests/assets/ok/example.did +++ b/rust/candid_parser/tests/assets/ok/example.did @@ -1,47 +1,100 @@ -type A = B; -type B = opt A; -type List = opt record { head : int; tail : List }; -type a = variant { a; b : b }; -type b = record { int; nat }; -type broker = service { - find : (name : text) -> (service { current : () -> (nat32); up : () -> () }); +// Doc comment for prim type +type my_type = principal; +// Doc comment for List +type List = opt record { + // Doc comment for List head + head : int; + // Doc comment for List tail + tail : List; }; type f = func (List, func (int32) -> (int64)) -> (opt List, res); -type list = opt node; -type my_type = principal; -type my_variant = variant { - a : record { b : text }; - c : opt record { d : text; e : vec record { f : nat } }; +// Doc comment for broker service +type broker = service { + find : (name : text) -> (service { current : () -> (nat32); up : () -> () }); }; +// Doc comment for nested type type nested = record { 0 : nat; 1 : nat; + // Doc comment for nested record 2 : record { nat; int }; 3 : record { 0 : nat; 42 : nat; 43 : nat8 }; 40 : nat; 41 : variant { 42; A; B; C }; 42 : nat; }; -type nested_records = record { nested : opt record { nested_field : text } }; +// Doc comment for res type +type res = variant { + // Doc comment for Ok variant + Ok : record { int; nat }; + // Doc comment for Err variant + Err : record { + // Doc comment for error field in Err variant, + // on multiple lines + error : text; + }; +}; type nested_res = variant { Ok : variant { Ok; Err }; - Err : variant { Ok : record { content : text }; Err : record { int } }; + Err : variant { + // Doc comment for Ok in nested variant + Ok : record { content : text }; + // Doc comment for Err in nested variant + Err : record { int }; + }; +}; +// Doc comment for nested_records +type nested_records = record { + // Doc comment for nested_records field nested + nested : opt record { + // Doc comment for nested_records field nested_field + nested_field : text; + }; }; +type my_variant = variant { + // Doc comment for my_variant field a + a : record { + // Doc comment for my_variant field a field b + b : text; + }; + // Doc comment for my_variant field c + c : opt record { + // Doc comment for my_variant field c field d + d : text; + e : vec record { + // Doc comment for my_variant field c field e inner vec element + f : nat; + }; + }; +}; +type A = B; +type B = opt A; type node = record { head : nat; tail : list }; -type res = variant { Ok : record { int; nat }; Err : record { error : text } }; -type s = service { f : t; g : (list) -> (B, tree, stream) }; -type stream = opt record { head : nat; next : func () -> (stream) query }; -type t = func (server : s) -> (); +type list = opt node; type tree = variant { branch : record { val : int; left : tree; right : tree }; leaf : int; }; +// Doc comment for service id +type s = service { f : t; g : (list) -> (B, tree, stream) }; +type t = func (server : s) -> (); +type stream = opt record { head : nat; next : func () -> (stream) query }; +type b = record { int; nat }; +type a = variant { a; b : b }; +// Doc comment for service service : { + // Doc comment for f1 method of service f1 : (list, test : blob, opt bool) -> () oneway; g1 : (my_type, List, opt List, nested) -> (int, broker, nested_res) query; h : (vec opt text, variant { A : nat; B : opt text }, opt List) -> ( - record { 42 : record {}; id : nat }, + record { + // Doc comment for 0x2a field in h method return, currently ignored + 42 : record {}; + // Doc comment for id field in h method return, currently ignored + id : nat; + }, ); + // Doc comment for i method of service i : f; x : (a, b) -> ( opt a, @@ -51,5 +104,6 @@ service : { y : (nested_records) -> (record { nested_records; my_variant }) query; f : t; g : (list) -> (B, tree, stream); + // Doc comment for imported bbbbb service method bbbbb : (b) -> (); } diff --git a/rust/candid_parser/tests/assets/ok/fieldnat.did b/rust/candid_parser/tests/assets/ok/fieldnat.did index 921423bb4..1f5e2a585 100644 --- a/rust/candid_parser/tests/assets/ok/fieldnat.did +++ b/rust/candid_parser/tests/assets/ok/fieldnat.did @@ -1,5 +1,5 @@ -type non_tuple = record { 1 : text; 2 : text }; type tuple = record { text; text }; +type non_tuple = record { 1 : text; 2 : text }; service : { bab : (two : int, nat) -> (); bar : (record { "2" : int }) -> (variant { e20; e30 }); diff --git a/rust/candid_parser/tests/assets/ok/inline_methods.did b/rust/candid_parser/tests/assets/ok/inline_methods.did index 6e568643f..7a0589bd5 100644 --- a/rust/candid_parser/tests/assets/ok/inline_methods.did +++ b/rust/candid_parser/tests/assets/ok/inline_methods.did @@ -1,7 +1,7 @@ type Fn = func (nat) -> (nat) query; type Gn = Fn; -type R = record { x : nat; fn : Fn; gn : Gn; nested : record { fn : Gn } }; type RInline = record { x : nat; fn : func (nat) -> (nat) query }; +type R = record { x : nat; fn : Fn; gn : Gn; nested : record { fn : Gn } }; service : { add_two : (nat) -> (nat); fn : Fn; diff --git a/rust/candid_parser/tests/assets/ok/keyword.did b/rust/candid_parser/tests/assets/ok/keyword.did index 43965aefa..9b1d2cec1 100644 --- a/rust/candid_parser/tests/assets/ok/keyword.did +++ b/rust/candid_parser/tests/assets/ok/keyword.did @@ -1,13 +1,13 @@ +type o = opt o; +type node = record { head : nat; tail : list }; +type list = opt node; type if = variant { branch : record { val : int; left : if; right : if }; leaf : int; }; -type list = opt node; -type node = record { head : nat; tail : list }; -type o = opt o; type return = service { f : t; g : (list) -> (if, stream) }; -type stream = opt record { head : nat; next : func () -> (stream) query }; type t = func (server : return) -> (); +type stream = opt record { head : nat; next : func () -> (stream) query }; service : { Oneway : () -> () oneway; f_ : (o) -> (o); diff --git a/rust/candid_parser/tests/assets/ok/management.did b/rust/candid_parser/tests/assets/ok/management.did index 4af885072..db9ccda59 100644 --- a/rust/candid_parser/tests/assets/ok/management.did +++ b/rust/candid_parser/tests/assets/ok/management.did @@ -1,7 +1,6 @@ -type bitcoin_address = text; -type bitcoin_network = variant { mainnet; testnet }; -type block_hash = blob; type canister_id = principal; +type user_id = principal; +type wasm_module = blob; type canister_settings = record { freezing_threshold : opt nat; controllers : opt vec principal; @@ -14,40 +13,41 @@ type definite_canister_settings = record { memory_allocation : nat; compute_allocation : nat; }; -type ecdsa_curve = variant { secp256k1 }; -type get_balance_request = record { - network : bitcoin_network; - address : bitcoin_address; - min_confirmations : opt nat32; +type http_header = record { value : text; name : text }; +type http_response = record { + status : nat; + body : blob; + headers : vec http_header; }; -type get_current_fee_percentiles_request = record { network : bitcoin_network }; +type ecdsa_curve = variant { secp256k1 }; +type satoshi = nat64; +type bitcoin_network = variant { mainnet; testnet }; +type bitcoin_address = text; +type block_hash = blob; +type outpoint = record { txid : blob; vout : nat32 }; +type utxo = record { height : nat32; value : satoshi; outpoint : outpoint }; type get_utxos_request = record { network : bitcoin_network; filter : opt variant { page : blob; min_confirmations : nat32 }; address : bitcoin_address; }; +type get_current_fee_percentiles_request = record { network : bitcoin_network }; type get_utxos_response = record { next_page : opt blob; tip_height : nat32; tip_block_hash : block_hash; utxos : vec utxo; }; -type http_header = record { value : text; name : text }; -type http_response = record { - status : nat; - body : blob; - headers : vec http_header; +type get_balance_request = record { + network : bitcoin_network; + address : bitcoin_address; + min_confirmations : opt nat32; }; -type millisatoshi_per_byte = nat64; -type outpoint = record { txid : blob; vout : nat32 }; -type satoshi = nat64; type send_transaction_request = record { transaction : blob; network : bitcoin_network; }; -type user_id = principal; -type utxo = record { height : nat32; value : satoshi; outpoint : outpoint }; -type wasm_module = blob; +type millisatoshi_per_byte = nat64; service : { bitcoin_get_balance : (get_balance_request) -> (satoshi); bitcoin_get_current_fee_percentiles : ( diff --git a/rust/candid_parser/tests/assets/ok/recursion.did b/rust/candid_parser/tests/assets/ok/recursion.did index bc4278a62..4dd70d2e7 100644 --- a/rust/candid_parser/tests/assets/ok/recursion.did +++ b/rust/candid_parser/tests/assets/ok/recursion.did @@ -1,12 +1,13 @@ type A = B; type B = opt A; -type list = opt node; type node = record { head : nat; tail : list }; -type s = service { f : t; g : (list) -> (B, tree, stream) }; -type stream = opt record { head : nat; next : func () -> (stream) query }; -type t = func (server : s) -> (); +type list = opt node; type tree = variant { branch : record { val : int; left : tree; right : tree }; leaf : int; }; +// Doc comment for service id +type s = service { f : t; g : (list) -> (B, tree, stream) }; +type t = func (server : s) -> (); +type stream = opt record { head : nat; next : func () -> (stream) query }; service : s diff --git a/rust/candid_parser/tests/assets/ok/service.did b/rust/candid_parser/tests/assets/ok/service.did index 672d699b5..7dc21afac 100644 --- a/rust/candid_parser/tests/assets/ok/service.did +++ b/rust/candid_parser/tests/assets/ok/service.did @@ -1,6 +1,6 @@ -type Func = func () -> (Service); type Service = service { f : Func }; type Service2 = Service; +type Func = func () -> (Service); service : { asArray : () -> (vec Service2, vec Func) query; asPrincipal : () -> (Service2, Func); diff --git a/rust/candid_parser/tests/parse_type.rs b/rust/candid_parser/tests/parse_type.rs index 236cba517..e38ba62d6 100644 --- a/rust/candid_parser/tests/parse_type.rs +++ b/rust/candid_parser/tests/parse_type.rs @@ -1,4 +1,4 @@ -use candid::pretty::candid::compile; +use candid::pretty::syntax::pretty_print; use candid::types::syntax::{Dec, IDLType}; use candid::types::TypeEnv; use candid_parser::bindings::{javascript, motoko, rust, typescript}; @@ -123,9 +123,10 @@ fn compiler_test(resource: &str) { Ok((env, actor, prog)) => { { let mut output = mint.new_goldenfile(filename.with_extension("did")).unwrap(); - let content = compile(&env, &actor); + let content = pretty_print(&prog); // Type check output - let ast = parse_idl_prog(&content).unwrap(); + let ast = parse_idl_prog(&content) + .unwrap_or_else(|_| panic!("failed to parse candid. Content: {content}")); check_prog(&mut TypeEnv::new(), &ast).unwrap(); writeln!(output, "{content}").unwrap(); } diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index 133d64cfa..46b072ef9 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -228,7 +228,7 @@ fn main() -> Result<()> { let content = match target.as_str() { "js" => candid_parser::bindings::javascript::compile(&env, &actor), "ts" => candid_parser::bindings::typescript::compile(&env, &actor, &prog), - "did" => candid_parser::pretty::candid::compile(&env, &actor), + "did" => candid_parser::candid::pretty::syntax::pretty_print(&prog), "mo" => candid_parser::bindings::motoko::compile(&env, &actor, &prog), "rs" => { use candid_parser::bindings::rust::{compile, Config, ExternalConfig};