Skip to content

Commit

Permalink
Rework generated API docs (paritytech#13178)
Browse files Browse the repository at this point in the history
  • Loading branch information
athei authored and melekes committed Feb 12, 2023
1 parent 0f794b4 commit 6a723e8
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 94 deletions.
219 changes: 127 additions & 92 deletions frame/contracts/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
extern crate alloc;

use alloc::{
collections::BTreeMap,
format,
string::{String, ToString},
vec::Vec,
};
use core::cmp::Reverse;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned, ToTokens};
Expand Down Expand Up @@ -160,7 +162,7 @@ struct EnvDef {
/// Parsed host function definition.
struct HostFn {
item: syn::ItemFn,
module: String,
version: u8,
name: String,
returns: HostFnReturn,
is_stable: bool,
Expand Down Expand Up @@ -208,20 +210,19 @@ impl HostFn {
let span = item.span();
let mut attrs = item.attrs.clone();
attrs.retain(|a| !a.path.is_ident("doc"));
let mut maybe_module = None;
let mut maybe_version = None;
let mut is_stable = true;
let mut alias_to = None;
let mut not_deprecated = true;
while let Some(attr) = attrs.pop() {
let ident = attr.path.get_ident().ok_or(err(span, msg))?.to_string();
match ident.as_str() {
"version" => {
if maybe_module.is_some() {
if maybe_version.is_some() {
return Err(err(span, "#[version] can only be specified once"))
}
let ver: u8 =
attr.parse_args::<syn::LitInt>().and_then(|lit| lit.base10_parse())?;
maybe_module = Some(format!("seal{}", ver));
maybe_version =
Some(attr.parse_args::<syn::LitInt>().and_then(|lit| lit.base10_parse())?);
},
"unstable" => {
if !is_stable {
Expand Down Expand Up @@ -341,7 +342,7 @@ impl HostFn {

Ok(Self {
item,
module: maybe_module.unwrap_or_else(|| "seal0".to_string()),
version: maybe_version.unwrap_or_default(),
name,
returns,
is_stable,
Expand All @@ -355,6 +356,10 @@ impl HostFn {
_ => Err(err(span, &msg)),
}
}

fn module(&self) -> String {
format!("seal{}", self.version)
}
}

impl EnvDef {
Expand Down Expand Up @@ -409,109 +414,143 @@ fn is_valid_special_arg(idx: usize, arg: &FnArg) -> bool {
matches!(*pat.ty, syn::Type::Infer(_))
}

/// Expands documentation for host functions.
fn expand_docs(def: &mut EnvDef) -> TokenStream2 {
let mut modules = def.host_funcs.iter().map(|f| f.module.clone()).collect::<Vec<_>>();
modules.sort();
modules.dedup();

let doc_selector = |a: &syn::Attribute| a.path.is_ident("doc");
let docs = modules.iter().map(|m| {
let funcs = def.host_funcs.iter_mut().map(|f| {
if *m == f.module {
// Remove auxiliary args: `ctx: _` and `memory: _`
f.item.sig.inputs = f
.item
.sig
.inputs
.iter()
.skip(2)
.map(|p| p.clone())
.collect::<Punctuated<FnArg, Comma>>();
let func_decl = f.item.sig.to_token_stream();
let func_doc = if let Some(origin_fn) = &f.alias_to {
let alias_doc = format!(
"This is just an alias function to [`{0}()`][`Self::{0}`] with backwards-compatible prefixed identifier.",
origin_fn,
);
quote! { #[doc = #alias_doc] }

} else {
let func_docs = f.item.attrs.iter().filter(|a| doc_selector(a)).map(|d| {
let docs = d.to_token_stream();
quote! { #docs }
});
let unstable_notice = if !f.is_stable {
let warning = "\n # Unstable\n\n \
This function is unstable and it is a subject to change (or removal) in the future.\n \
Do not deploy a contract using it to a production chain.";
quote! { #[doc = #warning] }
} else {
quote! {}
};
quote! {
#( #func_docs )*
#unstable_notice
}
};
quote! {
#func_doc
#func_decl;
}
} else {
quote! {}
}
});
fn expand_func_doc(func: &HostFn) -> TokenStream2 {
// Remove auxiliary args: `ctx: _` and `memory: _`
let func_decl = {
let mut sig = func.item.sig.clone();
sig.inputs = sig
.inputs
.iter()
.skip(2)
.map(|p| p.clone())
.collect::<Punctuated<FnArg, Comma>>();
sig.to_token_stream()
};
let func_doc = {
let func_docs = if let Some(origin_fn) = &func.alias_to {
let alias_doc = format!(
"This is just an alias function to [`{0}()`][`Self::{0}`] with backwards-compatible prefixed identifier.",
origin_fn,
);
quote! { #[doc = #alias_doc] }
} else {
let docs = func.item.attrs.iter().filter(|a| a.path.is_ident("doc")).map(|d| {
let docs = d.to_token_stream();
quote! { #docs }
});
quote! { #( #docs )* }
};
let deprecation_notice = if !func.not_deprecated {
let warning = "\n # Deprecated\n\n \
This function is deprecated and will be removed in future versions.\n \
No new code or contracts with this API can be deployed.";
quote! { #[doc = #warning] }
} else {
quote! {}
};
let import_notice = {
let info = format!(
"\n# Wasm Import Statement\n```wat\n(import \"seal{}\" \"{}\" (func ...))\n```",
func.version, func.name,
);
quote! { #[doc = #info] }
};
let unstable_notice = if !func.is_stable {
let warning = "\n # Unstable\n\n \
This function is unstable and it is a subject to change (or removal) in the future.\n \
Do not deploy a contract using it to a production chain.";
quote! { #[doc = #warning] }
} else {
quote! {}
};
quote! {
#deprecation_notice
#func_docs
#import_notice
#unstable_notice
}
};
quote! {
#func_doc
#func_decl;
}
}

let module = Ident::new(m, Span::call_site());
let module_doc = format!(
"Documentation of the API available to contracts by importing `{}` WASM module.",
module
);
/// Expands documentation for host functions.
fn expand_docs(def: &EnvDef) -> TokenStream2 {
// Create the `Current` trait with only the newest versions
// we sort so that only the newest versions make it into `docs`
let mut current_docs = BTreeMap::new();
let mut funcs: Vec<_> = def.host_funcs.iter().filter(|f| f.alias_to.is_none()).collect();
funcs.sort_unstable_by_key(|func| Reverse(func.version));
for func in funcs {
if current_docs.contains_key(&func.name) {
continue
}
current_docs.insert(func.name.clone(), expand_func_doc(&func));
}
let current_docs = current_docs.values();

// Create the `legacy` module with all functions
// Maps from version to list of functions that have this version
let mut legacy_doc = BTreeMap::<u8, Vec<TokenStream2>>::new();
for func in def.host_funcs.iter() {
legacy_doc.entry(func.version).or_default().push(expand_func_doc(&func));
}
let legacy_doc = legacy_doc.into_iter().map(|(version, funcs)| {
let doc = format!("All functions available in the **seal{}** module", version);
let version = Ident::new(&format!("Version{version}"), Span::call_site());
quote! {
#[doc = #module_doc]
pub mod #module {
use crate::wasm::runtime::{TrapReason, ReturnCode};
/// Every function in this trait represents (at least) one function that can be imported by a contract.
///
/// The function's identifier is to be set as the name in the import definition.
/// Where it is specifically indicated, an _alias_ function having `seal_`-prefixed identifier and
/// just the same signature and body, is also available (for backwards-compatibility purposes).
pub trait Api {
#( #funcs )*
}
#[doc = #doc]
pub trait #version {
#( #funcs )*
}
}
});

quote! {
#( #docs )*
/// Contains only the latest version of each function.
///
/// In reality there are more functions available but they are all obsolete: When a function
/// is updated a new **version** is added and the old versions stays available as-is.
/// We only list the newest version here. Some functions are available under additional
/// names (aliases) for historic reasons which are omitted here.
///
/// If you want an overview of all the functions available to a contact all you need
/// to look at is this trait. It contains only the latest version of each
/// function and no aliases. If you are writing a contract(language) from scratch
/// this is where you should look at.
pub trait Current {
#( #current_docs )*
}
#( #legacy_doc )*
}
}

/// Expands environment definiton.
/// Should generate source code for:
/// - implementations of the host functions to be added to the wasm runtime environment (see
/// `expand_impls()`).
fn expand_env(def: &mut EnvDef, docs: bool) -> TokenStream2 {
fn expand_env(def: &EnvDef, docs: bool) -> TokenStream2 {
let impls = expand_impls(def);
let docs = docs.then_some(expand_docs(def)).unwrap_or(TokenStream2::new());

quote! {
pub struct Env;
#impls
/// Contains the documentation of the API available to contracts.
/// Documentation of the API (host functions) available to contracts.
///
/// In order to generate this documentation, pass `doc` attribute to the [`#[define_env]`][`macro@define_env`] macro:
/// `#[define_env(doc)]`, and then run `cargo doc`.
/// The `Current` trait might be the most useful doc to look at. The versioned
/// traits only exist for reference: If trying to find out if a specific version of
/// `pallet-contracts` contains a certain function.
///
/// This module is not meant to be used by any code. Rather, it is meant to be consumed by humans through rustdoc.
/// # Note
///
/// Every function described in this module's sub module's traits uses this sub module's identifier
/// as its imported module name. The identifier of the function is the function's imported name.
/// According to the [WASM spec of imports](https://webassembly.github.io/spec/core/text/modules.html#text-import).
/// This module is not meant to be used by any code. Rather, it is meant to be
/// consumed by humans through rustdoc.
#[cfg(doc)]
pub mod api_doc {
use super::{TrapReason, ReturnCode};
#docs
}
}
Expand All @@ -520,7 +559,7 @@ fn expand_env(def: &mut EnvDef, docs: bool) -> TokenStream2 {
/// Generates for every host function:
/// - real implementation, to register it in the contract execution environment;
/// - dummy implementation, to be used as mocks for contract validation step.
fn expand_impls(def: &mut EnvDef) -> TokenStream2 {
fn expand_impls(def: &EnvDef) -> TokenStream2 {
let impls = expand_functions(def, true, quote! { crate::wasm::Runtime<E> });
let dummy_impls = expand_functions(def, false, quote! { () });

Expand Down Expand Up @@ -553,16 +592,12 @@ fn expand_impls(def: &mut EnvDef) -> TokenStream2 {
}
}

fn expand_functions(
def: &mut EnvDef,
expand_blocks: bool,
host_state: TokenStream2,
) -> TokenStream2 {
fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2) -> TokenStream2 {
let impls = def.host_funcs.iter().map(|f| {
// skip the context and memory argument
let params = f.item.sig.inputs.iter().skip(2);
let (module, name, body, wasm_output, output) = (
&f.module,
f.module(),
&f.name,
&f.item.block,
f.returns.to_wasm_sig(),
Expand Down
7 changes: 5 additions & 2 deletions frame/contracts/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ mod runtime;

#[cfg(feature = "runtime-benchmarks")]
pub use crate::wasm::code_cache::reinstrument;

#[cfg(doc)]
pub use crate::wasm::runtime::api_doc;

#[cfg(test)]
pub use tests::MockExt;

pub use crate::wasm::{
prepare::TryInstantiate,
runtime::{
Expand All @@ -45,8 +50,6 @@ use frame_support::dispatch::{DispatchError, DispatchResult};
use sp_core::Get;
use sp_runtime::RuntimeDebug;
use sp_std::prelude::*;
#[cfg(test)]
pub use tests::MockExt;
use wasmi::{
Config as WasmiConfig, Engine, Instance, Linker, Memory, MemoryType, Module, StackLimits, Store,
};
Expand Down

0 comments on commit 6a723e8

Please sign in to comment.