Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Non-breaking changes:
+ Preserve Rust doc comments on exported Candid types, record fields, and variant members when generating `.did` files via `#[derive(CandidType)]`

## 2026-02-27

### Candid 0.10.24
Expand Down
98 changes: 93 additions & 5 deletions rust/candid/src/pretty/candid.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::collections::HashMap;

use crate::pretty::utils::*;
use crate::types::{Field, FuncMode, Function, Label, SharedLabel, Type, TypeEnv, TypeInner};
use crate::types::{
Field, FieldDoc, FuncMode, Function, Label, SharedLabel, Type, TypeDoc, TypeDocs, TypeEnv,
TypeInner,
};
use pretty::RcDoc;

static KEYWORDS: [&str; 30] = [
Expand Down Expand Up @@ -125,6 +128,12 @@ pub fn pp_docs<'a>(docs: &'a [String]) -> RcDoc<'a> {
lines(docs.iter().map(|line| RcDoc::text("// ").append(line)))
}

fn maybe_pp_docs<'a>(docs: Option<&'a [String]>) -> RcDoc<'a> {
docs.filter(|docs| !docs.is_empty())
.map(pp_docs)
.unwrap_or_else(RcDoc::nil)
}

/// This function is kept for backward compatibility.
///
/// It is recommended to use [`pp_label_raw`] instead, which accepts a [`Label`].
Expand Down Expand Up @@ -153,6 +162,56 @@ fn pp_fields(fs: &[Field], is_variant: bool) -> RcDoc<'_> {
enclose_space("{", concat(fields, ";"), "}")
}

fn pp_field_with_doc<'a>(
field: &'a Field,
is_variant: bool,
doc: Option<&'a FieldDoc>,
) -> RcDoc<'a> {
let docs = maybe_pp_docs(doc.map(|doc| doc.docs.as_slice()));
let ty_doc = if is_variant && *field.ty == TypeInner::Null {
RcDoc::nil()
} else {
kwd(" :").append(pp_ty_with_doc(
&field.ty,
doc.and_then(|doc| doc.ty.as_deref()),
))
};
docs.append(pp_label_raw(&field.id)).append(ty_doc)
}

fn pp_fields_with_doc<'a>(
fs: &'a [Field],
is_variant: bool,
doc: Option<&'a TypeDoc>,
) -> RcDoc<'a> {
let fields = fs.iter().map(|field| {
let field_doc = doc.and_then(|doc| doc.fields.get(&field.id.get_id()));
pp_field_with_doc(field, is_variant, field_doc)
});
enclose_space("{", concat(fields, ";"), "}")
}

fn has_field_docs(doc: Option<&TypeDoc>) -> bool {
doc.map(|doc| doc.fields.values().any(|field| !field.is_empty()))
.unwrap_or(false)
}

fn pp_ty_with_doc<'a>(ty: &'a Type, doc: Option<&'a TypeDoc>) -> RcDoc<'a> {
use TypeInner::*;
match ty.as_ref() {
Record(ref fs) => {
if ty.is_tuple() && !has_field_docs(doc) {
let tuple = concat(fs.iter().map(|f| pp_ty_with_doc(&f.ty, None)), ";");
kwd("record").append(enclose_space("{", tuple, "}"))
} else {
kwd("record").append(pp_fields_with_doc(fs, false, doc))
}
}
Variant(ref fs) => kwd("variant").append(pp_fields_with_doc(fs, true, doc)),
ty => pp_ty_inner(ty),
}
}
Comment thread
lwshang marked this conversation as resolved.

pub fn pp_function(func: &Function) -> RcDoc<'_> {
let args = pp_args(&func.args);
let rets = pp_rets(&func.rets);
Expand Down Expand Up @@ -204,7 +263,7 @@ fn pp_service<'a>(serv: &'a [(String, Type)], docs: Option<&'a DocComments>) ->
enclose_space("{", doc, "}")
}

fn pp_defs(env: &TypeEnv) -> RcDoc<'_> {
fn pp_defs_plain(env: &TypeEnv) -> RcDoc<'_> {
lines(env.0.iter().map(|(id, ty)| {
kwd("type")
.append(ident(id))
Expand All @@ -214,6 +273,18 @@ fn pp_defs(env: &TypeEnv) -> RcDoc<'_> {
}))
}

fn pp_defs<'a>(env: &'a TypeEnv, docs: &'a DocComments) -> RcDoc<'a> {
lines(env.0.iter().map(|(id, ty)| {
let type_doc = docs.lookup_type_def(id);
maybe_pp_docs(type_doc.map(|doc| doc.docs.as_slice()))
.append(kwd("type"))
.append(ident(id))
.append(kwd("="))
.append(pp_ty_with_doc(ty, type_doc))
.append(";")
}))
}

fn pp_class<'a>(args: &'a [Type], t: &'a Type, docs: Option<&'a DocComments>) -> RcDoc<'a> {
let doc = pp_args(args).append(" ->").append(RcDoc::space());
match t.as_ref() {
Expand All @@ -234,13 +305,14 @@ fn pp_actor<'a>(ty: &'a Type, docs: &'a DocComments) -> RcDoc<'a> {

/// Pretty-prints the initialization arguments for a Candid actor.
pub fn pp_init_args<'a>(env: &'a TypeEnv, args: &'a [Type]) -> RcDoc<'a> {
pp_defs(env).append(pp_args(args))
pp_defs_plain(env).append(pp_args(args))
}

/// Collects doc comments that can be passed to the [compile_with_docs] function.
#[derive(Default, Debug)]
pub struct DocComments {
service_methods: HashMap<String, Vec<String>>,
type_defs: HashMap<String, TypeDoc>,
}

impl DocComments {
Expand All @@ -255,6 +327,22 @@ impl DocComments {
pub fn lookup_service_method(&self, method: &str) -> Option<&Vec<String>> {
self.service_methods.get(method)
}

pub fn add_type_def(&mut self, name: String, doc: TypeDoc) {
if !doc.is_empty() {
self.type_defs.insert(name, doc);
}
}

pub fn lookup_type_def(&self, name: &str) -> Option<&TypeDoc> {
self.type_defs.get(name)
}

pub fn extend_types(&mut self, docs: TypeDocs) {
for (name, doc) in docs.named {
self.add_type_def(name, doc);
}
}
}

pub fn compile(env: &TypeEnv, actor: &Option<Type>) -> String {
Expand All @@ -281,9 +369,9 @@ pub fn compile(env: &TypeEnv, actor: &Option<Type>) -> String {
/// ```
pub fn compile_with_docs(env: &TypeEnv, actor: &Option<Type>, docs: &DocComments) -> String {
match actor {
None => pp_defs(env).pretty(LINE_WIDTH).to_string(),
None => pp_defs(env, docs).pretty(LINE_WIDTH).to_string(),
Some(actor) => {
let defs = pp_defs(env);
let defs = pp_defs(env, docs);
let actor = kwd("service :").append(pp_actor(actor, docs));
let doc = defs.append(actor);
doc.pretty(LINE_WIDTH).to_string()
Expand Down
66 changes: 62 additions & 4 deletions rust/candid/src/types/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,39 @@ impl TypeName {
}
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TypeDocs {
pub named: BTreeMap<String, TypeDoc>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TypeDoc {
pub docs: Vec<String>,
pub fields: BTreeMap<u32, FieldDoc>,
}

impl TypeDoc {
pub fn is_empty(&self) -> bool {
self.docs.is_empty() && self.fields.is_empty()
}
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FieldDoc {
pub docs: Vec<String>,
pub ty: Option<Box<TypeDoc>>,
}

impl FieldDoc {
pub fn is_empty(&self) -> bool {
self.docs.is_empty()
&& match self.ty.as_deref() {
None => true,
Some(doc) => doc.is_empty(),
}
}
}

/// Used for `candid_derive::export_service` to generate `TypeEnv` from `Type`.
///
/// It performs a global rewriting of `Type` to resolve:
Expand All @@ -85,11 +118,13 @@ impl TypeName {
#[derive(Default)]
pub struct TypeContainer {
pub env: crate::TypeEnv,
pub docs: TypeDocs,
}
impl TypeContainer {
pub fn new() -> Self {
TypeContainer {
env: crate::TypeEnv::new(),
docs: TypeDocs::default(),
}
}
pub fn add<T: CandidType>(&mut self) -> Type {
Expand All @@ -115,8 +150,10 @@ impl TypeContainer {
}
let id = ID.with(|n| n.borrow().get(t).cloned());
if let Some(id) = id {
self.env.0.insert(id.to_string(), res);
TypeInner::Var(id.to_string())
let name = id.to_string();
self.env.0.insert(name.clone(), res);
self.remember_named_doc(&id, &name);
TypeInner::Var(name)
} else {
// if the type is part of an enum, the id won't be recorded.
// we want to inline the type in this case.
Expand All @@ -135,8 +172,10 @@ impl TypeContainer {
.into();
let id = ID.with(|n| n.borrow().get(t).cloned());
if let Some(id) = id {
self.env.0.insert(id.to_string(), res);
TypeInner::Var(id.to_string())
let name = id.to_string();
self.env.0.insert(name.clone(), res);
self.remember_named_doc(&id, &name);
TypeInner::Var(name)
} else {
return res;
}
Expand All @@ -145,6 +184,7 @@ impl TypeContainer {
let name = id.to_string();
let ty = ENV.with(|e| e.borrow().get(id).unwrap().clone());
self.env.0.insert(id.to_string(), ty);
self.remember_named_doc(id, &name);
TypeInner::Var(name)
}
TypeInner::Func(func) => TypeInner::Func(Function {
Expand All @@ -164,6 +204,14 @@ impl TypeContainer {
}
.into()
}

fn remember_named_doc(&mut self, id: &TypeId, name: &str) {
if let Some(doc) = find_type_doc(id) {
if !doc.is_empty() {
self.docs.named.entry(name.to_string()).or_insert(doc);
}
}
}
}

#[derive(Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)]
Expand Down Expand Up @@ -644,6 +692,7 @@ pub fn unroll(t: &Type) -> Type {

thread_local! {
static ENV: RefCell<BTreeMap<TypeId, Type>> = const { RefCell::new(BTreeMap::new()) };
static DOC_ENV: RefCell<BTreeMap<TypeId, TypeDoc>> = const { RefCell::new(BTreeMap::new()) };
// only used for TypeContainer
static ID: RefCell<BTreeMap<Type, TypeId>> = const { RefCell::new(BTreeMap::new()) };
static NAME: RefCell<TypeName> = RefCell::new(TypeName::default());
Expand All @@ -653,6 +702,10 @@ pub fn find_type(id: &TypeId) -> Option<Type> {
ENV.with(|e| e.borrow().get(id).cloned())
}

pub fn find_type_doc(id: &TypeId) -> Option<TypeDoc> {
DOC_ENV.with(|e| e.borrow().get(id).cloned())
}

// only for debugging
#[allow(dead_code)]
pub(crate) fn show_env() {
Expand All @@ -664,6 +717,7 @@ pub(crate) fn env_add(id: TypeId, t: Type) {
}
pub fn env_clear() {
ENV.with(|e| e.borrow_mut().clear());
DOC_ENV.with(|e| e.borrow_mut().clear());
}

pub(crate) fn env_id(id: TypeId, t: Type) {
Expand All @@ -684,6 +738,10 @@ pub(crate) fn env_id(id: TypeId, t: Type) {
});
}

pub(crate) fn env_doc(id: TypeId, doc: TypeDoc) {
DOC_ENV.with(|e| e.borrow_mut().insert(id, doc));
}

pub fn get_type<T>(_v: &T) -> Type
where
T: CandidType,
Expand Down
9 changes: 7 additions & 2 deletions rust/candid/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ pub mod type_env;
pub mod value;

pub use self::internal::{
get_type, Field, FuncMode, Function, Label, SharedLabel, Type, TypeId, TypeInner,
get_type, Field, FieldDoc, FuncMode, Function, Label, SharedLabel, Type, TypeDoc, TypeDocs,
TypeId, TypeInner,
};
pub use type_env::TypeEnv;

Expand Down Expand Up @@ -44,14 +45,18 @@ pub trait CandidType {
self::internal::env_add(id.clone(), TypeInner::Unknown.into());
let t = Self::_ty();
self::internal::env_add(id.clone(), t.clone());
self::internal::env_id(id, t.clone());
self::internal::env_id(id.clone(), t.clone());
self::internal::env_doc(id, Self::_ty_doc());
t
}
}
fn id() -> TypeId {
TypeId::of::<Self>()
}
fn _ty() -> Type;
fn _ty_doc() -> internal::TypeDoc {
internal::TypeDoc::default()
}
// only serialize the value encoding
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
Expand Down
Loading
Loading