mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 14:20:56 +00:00
Finish host function macro
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
mod errors;
|
||||
mod parsed_host_function;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
@@ -32,20 +34,86 @@ fn expand(input: TokenStream) -> syn::Result<TokenStream> {
|
||||
if let Some(error) = errors::combine(errors) {
|
||||
return Err(error);
|
||||
}
|
||||
if let Some(error) = errors::combine(collisions(&parsed)) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
trait HostFunctions {
|
||||
Ok(generate(&parsed))
|
||||
}
|
||||
|
||||
/// Names two declarations may not share, because the generated code would then
|
||||
/// fail to compile at a span the caller cannot see.
|
||||
fn collisions(functions: &[ParsedHostFunction]) -> Vec<syn::Error> {
|
||||
let mut errors = Vec::new();
|
||||
let mut variants = HashSet::new();
|
||||
let mut wasm_names = HashSet::new();
|
||||
|
||||
for function in functions {
|
||||
if !variants.insert(function.variant.to_string()) {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.variant,
|
||||
format!(
|
||||
"another host function already becomes the `{}` variant",
|
||||
function.variant
|
||||
),
|
||||
));
|
||||
}
|
||||
if !wasm_names.insert(function.wasm_name.value()) {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.wasm_name,
|
||||
format!(
|
||||
"another host function is already imported as `{}`",
|
||||
function.wasm_name.value()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
fn generate(functions: &[ParsedHostFunction]) -> TokenStream {
|
||||
let trait_methods = functions.iter().map(ParsedHostFunction::trait_method);
|
||||
let variants = functions
|
||||
.iter()
|
||||
.map(ParsedHostFunction::variant_declaration);
|
||||
let spec_arms = functions.iter().map(ParsedHostFunction::spec_arm);
|
||||
let all = functions.iter().map(|function| &function.variant);
|
||||
|
||||
quote! {
|
||||
/// The host ABI: one method per function a guest may import.
|
||||
pub trait HostFunctions {
|
||||
#(#trait_methods)*
|
||||
}
|
||||
|
||||
enum HostFunctionSpec {
|
||||
|
||||
/// Identifies a host function, and carries its ABI metadata.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HostFunctionSpec {
|
||||
#(#variants,)*
|
||||
}
|
||||
|
||||
impl HostFunctionSpec {
|
||||
/// Every host function, in declaration order.
|
||||
pub const ALL: &'static [Self] = &[#(Self::#all,)*];
|
||||
|
||||
/// The wasm import name and base gas cost of this function.
|
||||
pub const fn spec(self) -> HostFnSpec {
|
||||
match self {
|
||||
#(#spec_arms,)*
|
||||
}
|
||||
}
|
||||
|
||||
/// The name a guest imports this function under.
|
||||
pub const fn wasm_name(self) -> &'static str {
|
||||
self.spec().name
|
||||
}
|
||||
|
||||
/// The consensus-fixed base gas charged before the call runs.
|
||||
pub const fn gas(self) -> u64 {
|
||||
self.spec().base_gas
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct HostFunctionsInput {
|
||||
@@ -93,4 +161,78 @@ mod tests {
|
||||
let error = expand(quote! { fn missing_semicolon() }).expect_err("expected a syntax error");
|
||||
assert!(!error.to_string().is_empty());
|
||||
}
|
||||
|
||||
/// The messages of every diagnostic recorded by one failed `expand`.
|
||||
fn messages(input: TokenStream) -> Vec<String> {
|
||||
let Err(error) = expand(input) else {
|
||||
panic!("expected expansion to fail");
|
||||
};
|
||||
error.into_iter().map(|error| error.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_the_trait_the_enum_and_the_table() {
|
||||
let generated = expand(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn() -> [u8; 4];
|
||||
|
||||
#[gas = 500]
|
||||
#[wasm_name = "trace_num"]
|
||||
fn trace_num(msg: &str, number: i64);
|
||||
})
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
for expected in [
|
||||
"pub trait HostFunctions",
|
||||
"fn get_ledger_sqn (& mut self) -> [u8 ; 4] ;",
|
||||
"fn trace_num (& mut self , msg : & str , number : i64) ;",
|
||||
"pub enum HostFunctionSpec { GetLedgerSqn , TraceNum , }",
|
||||
"pub const ALL : & 'static [Self] = & [Self :: GetLedgerSqn , Self :: TraceNum ,]",
|
||||
"pub const fn spec (self) -> HostFnSpec",
|
||||
"Self :: GetLedgerSqn => HostFnSpec { name : \"ldgr_index\" , base_gas : 60u64 }",
|
||||
] {
|
||||
assert!(generated.contains(expected), "missing {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_two_functions_that_share_a_wasm_name() {
|
||||
let messages = messages(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "trace"]
|
||||
fn trace(msg: &str);
|
||||
|
||||
#[gas = 70]
|
||||
#[wasm_name = "trace"]
|
||||
fn trace_num(msg: &str, number: i64);
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("already imported as `trace`"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Names that differ only in underscores collapse to one enum variant.
|
||||
#[test]
|
||||
fn rejects_two_functions_that_share_a_variant() {
|
||||
let messages = messages(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "a"]
|
||||
fn get_ledger_sqn() -> [u8; 4];
|
||||
|
||||
#[gas = 70]
|
||||
#[wasm_name = "b"]
|
||||
fn get_ledger__sqn() -> [u8; 4];
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("`GetLedgerSqn` variant"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use syn::{Attribute, Expr, ExprLit, Lit, Signature, TraitItemFn};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{Attribute, Expr, ExprLit, Ident, Lit, LitStr, Signature, TraitItemFn, parse_quote};
|
||||
|
||||
use crate::errors;
|
||||
|
||||
@@ -12,13 +14,55 @@ const DOC: &str = "doc";
|
||||
/// One entry of a `host_functions!` block: its ABI metadata and its signature.
|
||||
pub(crate) struct ParsedHostFunction {
|
||||
pub(crate) gas: u64,
|
||||
pub(crate) wasm_name: String,
|
||||
/// Kept as the literal the user wrote, so diagnostics and the generated
|
||||
/// string both carry that span.
|
||||
pub(crate) wasm_name: LitStr,
|
||||
/// Doc comments, in source order, to re-emit on the generated items.
|
||||
pub(crate) docs: Vec<Attribute>,
|
||||
/// The enum variant this declaration becomes, spanned at the function name.
|
||||
pub(crate) variant: Ident,
|
||||
pub(crate) signature: Signature,
|
||||
}
|
||||
|
||||
impl ParsedHostFunction {
|
||||
/// `#[doc …] fn get_ledger_sqn(&mut self) -> [u8; 4];`
|
||||
pub(crate) fn trait_method(&self) -> TokenStream {
|
||||
let docs = &self.docs;
|
||||
|
||||
// The receiver is not part of the wasm ABI, so declarations omit it and
|
||||
// only the trait needs one.
|
||||
let mut signature = self.signature.clone();
|
||||
signature.inputs.insert(0, parse_quote!(&mut self));
|
||||
|
||||
quote! {
|
||||
#(#docs)*
|
||||
#signature;
|
||||
}
|
||||
}
|
||||
|
||||
/// `#[doc …] GetLedgerSqn`
|
||||
pub(crate) fn variant_declaration(&self) -> TokenStream {
|
||||
let docs = &self.docs;
|
||||
let variant = &self.variant;
|
||||
quote! {
|
||||
#(#docs)*
|
||||
#variant
|
||||
}
|
||||
}
|
||||
|
||||
/// `Self::GetLedgerSqn => HostFnSpec { name: "ldgr_index", base_gas: 60u64 }`
|
||||
pub(crate) fn spec_arm(&self) -> TokenStream {
|
||||
let Self {
|
||||
gas,
|
||||
wasm_name,
|
||||
variant,
|
||||
..
|
||||
} = self;
|
||||
quote! {
|
||||
Self::#variant => HostFnSpec { name: #wasm_name, base_gas: #gas }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(function: TraitItemFn) -> syn::Result<Self> {
|
||||
let mut gas = None;
|
||||
let mut wasm_name = None;
|
||||
@@ -97,11 +141,42 @@ impl ParsedHostFunction {
|
||||
gas,
|
||||
wasm_name,
|
||||
docs,
|
||||
variant: variant_ident(&function.sig.ident),
|
||||
signature: function.sig,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The enum variant a declaration becomes: `get_ledger_sqn` -> `GetLedgerSqn`.
|
||||
///
|
||||
/// The result carries `ident`'s span, so anything the compiler says about the
|
||||
/// variant points at the declaration that produced it.
|
||||
fn variant_ident(ident: &Ident) -> Ident {
|
||||
// `to_string` spells raw identifiers `r#type`; the `r#` is not part of the name.
|
||||
let name = ident.to_string();
|
||||
let name = name.strip_prefix("r#").unwrap_or(&name);
|
||||
|
||||
let mut pascal = String::with_capacity(name.len());
|
||||
let mut capitalize = true;
|
||||
for character in name.chars() {
|
||||
if character == '_' {
|
||||
capitalize = true;
|
||||
} else if capitalize {
|
||||
pascal.extend(character.to_uppercase());
|
||||
capitalize = false;
|
||||
} else {
|
||||
pascal.push(character);
|
||||
}
|
||||
}
|
||||
|
||||
// A name of nothing but underscores would leave `pascal` empty, and
|
||||
// `format_ident!` panics on an invalid identifier.
|
||||
if pascal.is_empty() {
|
||||
return ident.clone();
|
||||
}
|
||||
format_ident!("{pascal}", span = ident.span())
|
||||
}
|
||||
|
||||
/// Records `value`, or reports that the attribute appeared more than once.
|
||||
fn set_once<T>(slot: &mut Option<T>, value: T, attr: &Attribute) -> syn::Result<()> {
|
||||
if slot.replace(value).is_some() {
|
||||
@@ -125,12 +200,12 @@ fn int_value(attr: &Attribute) -> syn::Result<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
fn string_value(attr: &Attribute) -> syn::Result<String> {
|
||||
fn string_value(attr: &Attribute) -> syn::Result<LitStr> {
|
||||
match &attr.meta.require_name_value()?.value {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(string),
|
||||
..
|
||||
}) => Ok(string.value()),
|
||||
}) => Ok(string.clone()),
|
||||
other => Err(syn::Error::new_spanned(
|
||||
other,
|
||||
format!("`{}` expects a string literal", path_name(attr)),
|
||||
@@ -184,11 +259,66 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(parsed.gas, 60);
|
||||
assert_eq!(parsed.wasm_name, "ldgr_index");
|
||||
assert_eq!(parsed.wasm_name.value(), "ldgr_index");
|
||||
assert_eq!(parsed.signature.ident.to_string(), "get_ledger_sqn");
|
||||
assert_eq!(parsed.variant.to_string(), "GetLedgerSqn");
|
||||
assert!(parsed.docs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_variant_names_from_function_names() {
|
||||
for (function, variant) in [
|
||||
("get_ledger_sqn", "GetLedgerSqn"),
|
||||
("sha512_half", "Sha512Half"),
|
||||
("trace", "Trace"),
|
||||
("get_current_ledger_obj_field", "GetCurrentLedgerObjField"),
|
||||
("r#type", "Type"),
|
||||
// Pathological, but must not panic: no letters to capitalize.
|
||||
("__", "__"),
|
||||
] {
|
||||
let ident = format_ident!("{function}");
|
||||
assert_eq!(variant_ident(&ident).to_string(), variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trait_method_takes_a_receiver_and_ends_in_a_semicolon() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
/// Hashes `data`.
|
||||
#[gas = 2000]
|
||||
#[wasm_name = "sha512_half"]
|
||||
fn sha512_half(data: &[u8]) -> [u8; 32];
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// `///` reaches the macro as `#[doc = r"..."]`: rustc's lexer spells doc
|
||||
// comments as raw string literals.
|
||||
let method = parsed.trait_method().to_string();
|
||||
assert!(
|
||||
method.starts_with("# [doc = r\" Hashes `data`.\"]"),
|
||||
"{method}"
|
||||
);
|
||||
assert!(
|
||||
method.contains("fn sha512_half (& mut self , data : & [u8]) -> [u8 ; 32] ;"),
|
||||
"{method}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_arm_carries_the_name_and_the_gas() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn() -> [u8; 4];
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed.spec_arm().to_string(),
|
||||
"Self :: GetLedgerSqn => HostFnSpec { name : \"ldgr_index\" , base_gas : 60u64 }"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_doc_comments_in_source_order() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#![no_std]
|
||||
use xrpl_host_functions_macros::host_abi;
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use xrpl_host_functions_macros::host_functions;
|
||||
|
||||
/// Error codes a host function may return.
|
||||
///
|
||||
|
||||
92
crates/xrpl-host-functions/tests/generated_abi.rs
Normal file
92
crates/xrpl-host-functions/tests/generated_abi.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! Exercises what `host_functions!` generates: the trait is implementable and
|
||||
//! the spec table agrees with the declarations in `src/lib.rs`.
|
||||
|
||||
use xrpl_host_functions::{HASH_LEN, HostFnSpec, HostFunctionSpec, HostFunctions};
|
||||
|
||||
/// Records what it was asked to do; enough to prove the trait is usable.
|
||||
#[derive(Default)]
|
||||
struct FakeHost {
|
||||
traced: Vec<String>,
|
||||
}
|
||||
|
||||
impl HostFunctions for FakeHost {
|
||||
fn get_ledger_sqn(&mut self) -> [u8; 4] {
|
||||
7u32.to_le_bytes()
|
||||
}
|
||||
|
||||
fn get_current_ledger_obj_field(&mut self, field: i32) -> Vec<u8> {
|
||||
vec![field as u8]
|
||||
}
|
||||
|
||||
fn sha512_half(&mut self, data: &[u8]) -> [u8; HASH_LEN] {
|
||||
let mut digest = [0; HASH_LEN];
|
||||
digest[0] = data.len() as u8;
|
||||
digest
|
||||
}
|
||||
|
||||
fn trace(&mut self, msg: &str, data: &[u8], as_hex: bool) {
|
||||
self.traced.push(format!("{msg}/{}/{as_hex}", data.len()));
|
||||
}
|
||||
|
||||
fn trace_num(&mut self, msg: &str, number: i64) {
|
||||
self.traced.push(format!("{msg}={number}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_trait_is_implementable() {
|
||||
let mut host = FakeHost::default();
|
||||
|
||||
assert_eq!(host.get_ledger_sqn(), [7, 0, 0, 0]);
|
||||
assert_eq!(host.get_current_ledger_obj_field(3), vec![3]);
|
||||
assert_eq!(host.sha512_half(b"abc")[0], 3);
|
||||
host.trace("hello", b"xy", true);
|
||||
host.trace_num("count", -1);
|
||||
|
||||
assert_eq!(host.traced, ["hello/2/true", "count=-1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_spec_table_matches_the_declarations() {
|
||||
assert_eq!(HostFunctionSpec::ALL.len(), 5);
|
||||
assert_eq!(
|
||||
HostFunctionSpec::GetLedgerSqn.spec(),
|
||||
HostFnSpec {
|
||||
name: "ldgr_index",
|
||||
base_gas: 60
|
||||
}
|
||||
);
|
||||
assert_eq!(HostFunctionSpec::Sha512Half.gas(), 2000);
|
||||
assert_eq!(
|
||||
HostFunctionSpec::GetCurrentLedgerObjField.wasm_name(),
|
||||
"home_le_field"
|
||||
);
|
||||
}
|
||||
|
||||
/// `ALL` is what a wasm engine iterates to register imports, so it must be complete.
|
||||
#[test]
|
||||
fn every_variant_appears_in_all_exactly_once() {
|
||||
let mut names: Vec<&str> = HostFunctionSpec::ALL
|
||||
.iter()
|
||||
.map(|function| function.wasm_name())
|
||||
.collect();
|
||||
names.sort_unstable();
|
||||
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
"home_le_field",
|
||||
"ldgr_index",
|
||||
"sha512_half",
|
||||
"trace",
|
||||
"trace_num"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// The generated `spec` is `const`, so gas costs are available at compile time.
|
||||
#[test]
|
||||
fn the_table_is_usable_in_const_context() {
|
||||
const TRACE_GAS: u64 = HostFunctionSpec::Trace.gas();
|
||||
assert_eq!(TRACE_GAS, 500);
|
||||
}
|
||||
Reference in New Issue
Block a user