Moved HostFunctionsInput into a separte file

This commit is contained in:
Sergey Kuznetsov
2026-07-28 12:55:29 +01:00
parent 7ebd92ad3e
commit c4ce52c810
3 changed files with 445 additions and 145 deletions

View File

@@ -0,0 +1,12 @@
/// Folds accumulated diagnostics into the single error a macro can return.
///
/// `syn::Error` is itself a collection: `combine` appends, and
/// `into_compile_error` emits one `compile_error!` per recorded span. Folding
/// instead of returning the first error means every mistake in a
/// `host_functions!` block surfaces in one build rather than one per rebuild.
pub(crate) fn combine(errors: Vec<syn::Error>) -> Option<syn::Error> {
errors.into_iter().reduce(|mut first, next| {
first.combine(next);
first
})
}

View File

@@ -1,24 +1,37 @@
use proc_macro::TokenStream;
mod errors;
mod parsed_host_function;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Attribute, Expr, ExprLit, Lit, Signature, TraitItemFn,
TraitItemFn,
parse::{Parse, ParseStream},
parse2,
};
use parsed_host_function::ParsedHostFunction;
#[proc_macro]
pub fn host_functions(input: TokenStream) -> TokenStream {
pub fn host_functions(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
expand(input.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
fn expand(input: TokenStream) -> syn::Result<TokenStream> {
let HostFunctionsInput { functions } = parse2(input)?;
// let mut errors = Vec::new();
for f in functions {}
let mut parsed = Vec::with_capacity(functions.len());
let mut errors = Vec::new();
for function in functions {
match ParsedHostFunction::parse(function) {
Ok(function) => parsed.push(function),
Err(error) => errors.push(error),
}
}
if let Some(error) = errors::combine(errors) {
return Err(error);
}
Ok(quote! {
trait HostFunctions {
@@ -29,9 +42,10 @@ fn expand(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStre
}
impl HostFunctionSpec
}
.into())
impl HostFunctionSpec {
}
})
}
struct HostFunctionsInput {
@@ -48,149 +62,35 @@ impl Parse for HostFunctionsInput {
}
}
struct ParsedHostFunction {
gas: usize,
wasm_name: String,
docs: Vec<Attribute>,
signature: Signature,
}
impl ParsedHostFunction {
const GAS_PATH: &str = "gas";
const WASM_NAME_PATH: &str = "wasm_name";
fn parse(value: TraitItemFn) -> Result<Self, syn::Error> {
let mut gas = None;
let mut wasm_name = None;
let mut docs = Vec::new();
let mut errors = Vec::new();
for attr in &value.attrs {
let named = match attr.meta.require_name_value() {
Ok(n) => n,
Err(e) => {
errors.push(e);
continue;
}
};
match &named.path {
p if p.is_ident(Self::GAS_PATH) => {
let parsed_value = match Self::parse_number(&named.value) {
Ok(n) => n,
Err(e) => {
errors.push(e);
continue;
}
};
if gas.replace(parsed_value).is_some() {
errors.push(syn::Error::new_spanned(
named,
format!("duplicated {} attribute", Self::GAS_PATH),
));
}
}
p if p.is_ident(Self::WASM_NAME_PATH) => {
let parsed_value = match Self::parse_string(&named.value) {
Ok(n) => n,
Err(e) => {
errors.push(e);
continue;
}
};
if wasm_name.replace(parsed_value).is_some() {
errors.push(syn::Error::new_spanned(
named,
format!("duplicated {} attribute", Self::WASM_NAME_PATH),
));
}
}
p if p.is_ident("doc") => {
docs.push(attr.clone());
}
_ => {
errors.push(syn::Error::new_spanned(named, "unexpected attribute"));
}
}
}
if !errors.is_empty() {
return Err(errors
.into_iter()
.reduce(|mut l, r| {
l.combine(r);
l
})
.unwrap());
}
if gas.is_none() {
return Err(syn::Error::new_spanned(
&value.sig,
format!("missing {} attribute", Self::GAS_PATH),
));
}
if wasm_name.is_none() {
return Err(syn::Error::new_spanned(
&value.sig,
format!("missing {} attribute", Self::WASM_NAME_PATH),
));
}
Ok(Self {
gas: gas.unwrap(),
wasm_name: wasm_name.unwrap(),
docs,
signature: value.sig,
})
}
fn parse_number(value: &Expr) -> Result<usize, syn::Error> {
match value {
Expr::Lit(ExprLit {
lit: Lit::Int(i), ..
}) => i.base10_parse::<usize>(),
other => Err(syn::Error::new_spanned(
other,
"expected an integer literal",
)),
}
}
fn parse_string(value: &Expr) -> Result<String, syn::Error> {
match value {
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => Ok(s.value()),
other => Err(syn::Error::new_spanned(other, "expected string literal")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_gas_and_wasm_name() {
let f: TraitItemFn = syn::parse_quote! {
/// some comment
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
};
let p = ParsedHostFunction::parse(f).unwrap();
assert_eq!(p.gas, 60);
assert_eq!(p.wasm_name, "ldgr_index");
fn accepts_an_empty_block() {
expand(quote! {}).unwrap();
}
#[test]
fn rejects_unknown_attribute() {
let f: TraitItemFn = syn::parse_quote! {
#[gas = 60]
#[wsam_name = "typo"]
fn reports_mistakes_from_every_function() {
let error = expand(quote! {
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
};
assert!(ParsedHostFunction::parse(f).is_err());
#[gas = 2000]
fn sha512_half(data: &[u8]) -> [u8; 32];
})
.expect_err("expected parsing to fail");
let messages: Vec<_> = error.into_iter().map(|error| error.to_string()).collect();
assert_eq!(messages.len(), 2, "{messages:?}");
assert!(messages[0].contains("missing `#[gas"), "{messages:?}");
assert!(messages[1].contains("missing `#[wasm_name"), "{messages:?}");
}
#[test]
fn propagates_syntax_errors() {
let error = expand(quote! { fn missing_semicolon() }).expect_err("expected a syntax error");
assert!(!error.to_string().is_empty());
}
}

View File

@@ -0,0 +1,388 @@
use syn::{Attribute, Expr, ExprLit, Lit, Signature, TraitItemFn};
use crate::errors;
/// `#[gas = N]`: the base gas charged before the call runs.
const GAS: &str = "gas";
/// `#[wasm_name = "..."]`: the name the guest imports the function under.
const WASM_NAME: &str = "wasm_name";
/// `///` desugars to `#[doc = "..."]` before macro expansion.
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,
/// Doc comments, in source order, to re-emit on the generated items.
pub(crate) docs: Vec<Attribute>,
pub(crate) signature: Signature,
}
impl ParsedHostFunction {
pub(crate) fn parse(function: TraitItemFn) -> syn::Result<Self> {
let mut gas = None;
let mut wasm_name = None;
let mut docs = Vec::new();
let mut errors = Vec::new();
// Tracked separately from `gas`/`wasm_name` so a malformed attribute is
// not also reported as a missing one.
let mut saw_gas = false;
let mut saw_wasm_name = false;
for attr in function.attrs {
if attr.path().is_ident(GAS) {
saw_gas = true;
if let Err(error) = int_value(&attr).and_then(|v| set_once(&mut gas, v, &attr)) {
errors.push(error);
}
} else if attr.path().is_ident(WASM_NAME) {
saw_wasm_name = true;
if let Err(error) =
string_value(&attr).and_then(|v| set_once(&mut wasm_name, v, &attr))
{
errors.push(error);
}
} else if attr.path().is_ident(DOC) {
docs.push(attr);
} else {
errors.push(syn::Error::new_spanned(
&attr,
format!("unexpected attribute `{}`", path_name(&attr)),
));
}
}
if !saw_gas {
errors.push(syn::Error::new_spanned(
&function.sig.ident,
format!("missing `#[{GAS} = ...]` attribute"),
));
}
if !saw_wasm_name {
errors.push(syn::Error::new_spanned(
&function.sig.ident,
format!("missing `#[{WASM_NAME} = \"...\"]` attribute"),
));
}
if let Some(body) = &function.default {
errors.push(syn::Error::new_spanned(
body,
"a host function is implemented by the host, so it must not have a body",
));
}
if !function.sig.generics.params.is_empty() || function.sig.generics.where_clause.is_some()
{
errors.push(syn::Error::new_spanned(
&function.sig.ident,
"a host function must not be generic: it maps to one wasm import signature",
));
}
if let Some(receiver) = function.sig.receiver() {
errors.push(syn::Error::new_spanned(
receiver,
"the receiver is added by the macro; declare only the wasm parameters",
));
}
if let Some(error) = errors::combine(errors) {
return Err(error);
}
let (Some(gas), Some(wasm_name)) = (gas, wasm_name) else {
unreachable!("absent attributes are reported above");
};
Ok(Self {
gas,
wasm_name,
docs,
signature: function.sig,
})
}
}
/// 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() {
return Err(syn::Error::new_spanned(
attr,
format!("duplicate `{}` attribute", path_name(attr)),
));
}
Ok(())
}
fn int_value(attr: &Attribute) -> syn::Result<u64> {
match &attr.meta.require_name_value()?.value {
Expr::Lit(ExprLit {
lit: Lit::Int(int), ..
}) => int.base10_parse(),
other => Err(syn::Error::new_spanned(
other,
format!("`{}` expects an integer literal", path_name(attr)),
)),
}
}
fn string_value(attr: &Attribute) -> syn::Result<String> {
match &attr.meta.require_name_value()?.value {
Expr::Lit(ExprLit {
lit: Lit::Str(string),
..
}) => Ok(string.value()),
other => Err(syn::Error::new_spanned(
other,
format!("`{}` expects a string literal", path_name(attr)),
)),
}
}
/// The attribute's path as written, for diagnostics: `gas`, or `foo::bar`.
fn path_name(attr: &Attribute) -> String {
attr.path()
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
/// The message of every diagnostic recorded by one failed `parse`.
///
/// `expect_err` is unavailable here: it needs `T: Debug`, and syn only
/// implements `Debug` for its AST types under the `extra-traits` feature.
fn messages(function: TraitItemFn) -> Vec<String> {
let Err(error) = ParsedHostFunction::parse(function) else {
panic!("expected parsing to fail");
};
error.into_iter().map(|error| error.to_string()).collect()
}
fn doc_text(attr: &Attribute) -> String {
match &attr.meta.require_name_value().unwrap().value {
Expr::Lit(ExprLit {
lit: Lit::Str(text),
..
}) => text.value(),
_ => panic!("doc attribute is not a string literal"),
}
}
#[test]
fn reads_gas_and_wasm_name() {
let parsed = ParsedHostFunction::parse(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
})
.unwrap();
assert_eq!(parsed.gas, 60);
assert_eq!(parsed.wasm_name, "ldgr_index");
assert_eq!(parsed.signature.ident.to_string(), "get_ledger_sqn");
assert!(parsed.docs.is_empty());
}
#[test]
fn keeps_doc_comments_in_source_order() {
let parsed = ParsedHostFunction::parse(parse_quote! {
/// First line.
///
/// Third line.
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
})
.unwrap();
let docs: Vec<_> = parsed.docs.iter().map(doc_text).collect();
assert_eq!(docs, vec![" First line.", "", " Third line."]);
}
#[test]
fn preserves_parameters_and_return_type() {
let traced = ParsedHostFunction::parse(parse_quote! {
#[gas = 500]
#[wasm_name = "trace"]
fn trace(msg: &str, data: &[u8], as_hex: bool);
})
.unwrap();
assert_eq!(traced.signature.inputs.len(), 3);
assert!(matches!(traced.signature.output, syn::ReturnType::Default));
let hashed = ParsedHostFunction::parse(parse_quote! {
#[gas = 2000]
#[wasm_name = "sha512_half"]
fn sha512_half(data: &[u8]) -> [u8; 32];
})
.unwrap();
assert!(matches!(hashed.signature.output, syn::ReturnType::Type(..)));
}
#[test]
fn reports_both_missing_attributes_at_once() {
let messages = messages(parse_quote! {
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(messages.len(), 2);
assert!(messages[0].contains("missing `#[gas"), "{messages:?}");
assert!(messages[1].contains("missing `#[wasm_name"), "{messages:?}");
}
#[test]
fn names_the_unexpected_attribute() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wsam_name = "typo"]
fn get_ledger_sqn() -> [u8; 4];
});
// The typo'd attribute, plus the `wasm_name` it failed to be.
assert_eq!(messages.len(), 2);
assert!(
messages.iter().any(|m| m.contains("`wsam_name`")),
"{messages:?}"
);
}
#[test]
fn rejects_wrong_literal_types() {
let gas = messages(parse_quote! {
#[gas = "60"]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(gas.len(), 1, "{gas:?}");
assert!(
gas[0].contains("`gas` expects an integer literal"),
"{gas:?}"
);
let name = messages(parse_quote! {
#[gas = 60]
#[wasm_name = 7]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(name.len(), 1, "{name:?}");
assert!(
name[0].contains("`wasm_name` expects a string literal"),
"{name:?}"
);
}
#[test]
fn rejects_gas_that_does_not_fit_in_u64() {
let messages = messages(parse_quote! {
#[gas = 99999999999999999999999]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(messages[0].contains("number too large"), "{messages:?}");
}
#[test]
fn rejects_attribute_shapes_other_than_name_value() {
let bare = messages(parse_quote! {
#[gas]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(bare.len(), 1, "{bare:?}");
assert!(bare[0].contains("gas = ..."), "{bare:?}");
let list = messages(parse_quote! {
#[gas(60)]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(list.len(), 1, "{list:?}");
}
#[test]
fn rejects_duplicate_attributes() {
let messages = messages(parse_quote! {
#[gas = 60]
#[gas = 70]
#[wasm_name = "ldgr_index"]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(messages.len(), 2, "{messages:?}");
assert!(messages[0].contains("duplicate `gas`"), "{messages:?}");
assert!(
messages[1].contains("duplicate `wasm_name`"),
"{messages:?}"
);
}
/// A malformed attribute must not also be reported as an absent one.
#[test]
fn does_not_report_a_malformed_attribute_as_missing() {
let messages = messages(parse_quote! {
#[gas = "60"]
#[wasm_name = 7]
fn get_ledger_sqn() -> [u8; 4];
});
assert_eq!(messages.len(), 2, "{messages:?}");
assert!(
!messages.iter().any(|m| m.contains("missing")),
"{messages:?}"
);
}
#[test]
fn rejects_a_body() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4] { [0; 4] }
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(messages[0].contains("must not have a body"), "{messages:?}");
}
#[test]
fn rejects_generics() {
let parameter = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn<T>() -> T;
});
assert_eq!(parameter.len(), 1, "{parameter:?}");
assert!(
parameter[0].contains("must not be generic"),
"{parameter:?}"
);
let clause = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4] where Self: Sized;
});
assert_eq!(clause.len(), 1, "{clause:?}");
}
#[test]
fn rejects_an_explicit_receiver() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> [u8; 4];
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(messages[0].contains("receiver"), "{messages:?}");
}
}