Add compile-time leaf declarations

Introduce a function-like leaf declaration macro, bind endpoint and TUI hosts to shared generated metadata, and move remote shell endpoint construction out of the leaf module into the examples and runtime assembly code.
This commit is contained in:
Michael Mikovsky
2026-04-26 13:54:44 -06:00
parent fccd61ea29
commit bc22d349bf
17 changed files with 598 additions and 170 deletions
+4
View File
@@ -146,6 +146,10 @@ pub(crate) fn expand_leaf(input: DeriveInput) -> Result<proc_macro2::TokenStream
}
}
impl #impl_generics ::unshell::protocol::tree::LeafBinding for #struct_name #ty_generics #where_clause {
type Declaration = Self;
}
impl #impl_generics #struct_name #ty_generics #where_clause {
/// Returns the canonical dotted leaf name declared for this type.
#leaf_name_warning_attr
+307
View File
@@ -0,0 +1,307 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
Error, Ident, LitStr, Result, Token, Visibility,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
use crate::utils::option_litstr_tokens;
pub(crate) struct LeafDeclarationInput {
visibility: Visibility,
name: Option<LitStr>,
id: Option<LitStr>,
org: Option<LitStr>,
product: Option<LitStr>,
version: Option<LitStr>,
endpoint_struct: Option<Ident>,
tui_struct: Option<Ident>,
procedures: Vec<Ident>,
}
impl Parse for LeafDeclarationInput {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let visibility = if input.peek(Token![pub]) {
input.parse()?
} else {
Visibility::Inherited
};
let assignments = Punctuated::<LeafAssignment, Token![,]>::parse_terminated(input)?;
let mut parsed = Self {
visibility,
name: None,
id: None,
org: None,
product: None,
version: None,
endpoint_struct: None,
tui_struct: None,
procedures: Vec::new(),
};
for assignment in assignments {
match assignment {
LeafAssignment::Name(value) => {
if parsed.name.is_some() {
return Err(Error::new_spanned(value, "duplicate leaf name"));
}
parsed.name = Some(value);
}
LeafAssignment::Id(value) => {
if parsed.id.is_some() {
return Err(Error::new_spanned(value, "duplicate leaf id"));
}
parsed.id = Some(value);
}
LeafAssignment::Org(value) => {
if parsed.org.is_some() {
return Err(Error::new_spanned(value, "duplicate leaf org"));
}
parsed.org = Some(value);
}
LeafAssignment::Product(value) => {
if parsed.product.is_some() {
return Err(Error::new_spanned(value, "duplicate leaf product"));
}
parsed.product = Some(value);
}
LeafAssignment::Version(value) => {
if parsed.version.is_some() {
return Err(Error::new_spanned(value, "duplicate leaf version"));
}
parsed.version = Some(value);
}
LeafAssignment::EndpointStruct(value) => {
if parsed.endpoint_struct.is_some() {
return Err(Error::new_spanned(value, "duplicate endpoint_struct"));
}
parsed.endpoint_struct = Some(value);
}
LeafAssignment::TuiStruct(value) => {
if parsed.tui_struct.is_some() {
return Err(Error::new_spanned(value, "duplicate tui_struct"));
}
parsed.tui_struct = Some(value);
}
LeafAssignment::Procedures(values) => {
if !parsed.procedures.is_empty() {
return Err(Error::new(input.span(), "duplicate procedures list"));
}
parsed.procedures = values;
}
}
}
if parsed.name.is_none() && parsed.id.is_none() {
return Err(Error::new(
input.span(),
"leaf! requires either `name = \"...\"` or `id = \"...\"`",
));
}
if parsed.endpoint_struct.is_none() && parsed.tui_struct.is_none() {
return Err(Error::new(
input.span(),
"leaf! requires at least one of `endpoint_struct = ...` or `tui_struct = ...`",
));
}
Ok(parsed)
}
}
enum LeafAssignment {
Name(LitStr),
Id(LitStr),
Org(LitStr),
Product(LitStr),
Version(LitStr),
EndpointStruct(Ident),
TuiStruct(Ident),
Procedures(Vec<Ident>),
}
impl Parse for LeafAssignment {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let name: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match name.to_string().as_str() {
"name" => Ok(Self::Name(input.parse()?)),
"id" => Ok(Self::Id(input.parse()?)),
"org" => Ok(Self::Org(input.parse()?)),
"product" => Ok(Self::Product(input.parse()?)),
"version" => Ok(Self::Version(input.parse()?)),
"endpoint_struct" => Ok(Self::EndpointStruct(input.parse()?)),
"tui_struct" => Ok(Self::TuiStruct(input.parse()?)),
"procedures" => {
let content;
syn::bracketed!(content in input);
let values = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?
.into_iter()
.collect::<Vec<_>>();
Ok(Self::Procedures(values))
}
_ => Err(Error::new_spanned(name, "unsupported leaf! assignment")),
}
}
}
pub(crate) fn expand_leaf_declaration(input: LeafDeclarationInput) -> Result<TokenStream> {
let _visibility = input.visibility;
let declaration_ident = format_ident!(
"__UnshellLeafDecl_{}",
input
.endpoint_struct
.as_ref()
.or(input.tui_struct.as_ref())
.expect("leaf declaration requires at least one host")
);
let id = option_litstr_tokens(input.id.as_ref());
let org = option_litstr_tokens(input.org.as_ref());
let product = option_litstr_tokens(input.product.as_ref());
let version = option_litstr_tokens(input.version.as_ref());
let leaf_name = option_litstr_tokens(input.name.as_ref());
let procedure_suffixes = input
.procedures
.iter()
.map(|procedure| LitStr::new(&normalize_suffix(&procedure.to_string()), procedure.span()))
.collect::<Vec<_>>();
let endpoint_impl = input
.endpoint_struct
.as_ref()
.map(|endpoint_struct| expand_binding_impl(endpoint_struct, &declaration_ident));
let tui_impl = input
.tui_struct
.as_ref()
.map(|tui_struct| expand_binding_impl(tui_struct, &declaration_ident));
Ok(quote! {
#[allow(non_camel_case_types)]
#[doc(hidden)]
pub struct #declaration_ident;
impl ::unshell::protocol::tree::ProtocolLeaf for #declaration_ident {
fn leaf_name() -> ::unshell::alloc::string::String {
::unshell::protocol::tree::derive_leaf_name(
::core::env!("CARGO_PKG_NAME"),
::core::env!("CARGO_PKG_VERSION_MAJOR"),
::core::env!("CARGO_PKG_VERSION_MINOR"),
::core::env!("CARGO_PKG_VERSION_PATCH"),
::core::module_path!(),
::core::stringify!(#declaration_ident),
#org,
#product,
#version,
#leaf_name,
#id,
)
}
}
impl ::unshell::protocol::tree::LeafDeclaration for #declaration_ident {
fn procedure_suffixes() -> &'static [&'static str] {
&[#(#procedure_suffixes),*]
}
}
impl #declaration_ident {
/// Returns the canonical dotted leaf name declared for this surface.
pub fn protocol_leaf_name() -> ::unshell::alloc::string::String {
<Self as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name()
}
/// Returns the canonical protocol leaf metadata for this surface.
pub fn protocol_leaf_spec() -> ::unshell::protocol::tree::LeafSpec {
<Self as ::unshell::protocol::tree::LeafDeclaration>::leaf_spec()
}
/// Resolves one local procedure suffix to its full canonical `procedure_id`.
pub fn protocol_procedure_id(
suffix: &str,
) -> ::core::option::Option<::unshell::alloc::string::String> {
<Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(suffix)
}
}
#endpoint_impl
#tui_impl
})
}
fn expand_binding_impl(host: &Ident, declaration: &Ident) -> TokenStream {
quote! {
impl ::unshell::protocol::tree::ProtocolLeaf for #host {
fn leaf_name() -> ::unshell::alloc::string::String {
<#declaration as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name()
}
}
impl ::unshell::protocol::tree::LeafBinding for #host {
type Declaration = #declaration;
}
impl ::unshell::protocol::tree::LeafDeclaration for #host {
fn procedure_suffixes() -> &'static [&'static str] {
<#declaration as ::unshell::protocol::tree::LeafDeclaration>::procedure_suffixes()
}
}
impl #host {
/// Returns the canonical dotted leaf name declared for this host.
pub fn protocol_leaf_name() -> ::unshell::alloc::string::String {
<Self as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name()
}
/// Returns the canonical protocol leaf metadata for this host.
pub fn protocol_leaf_spec() -> ::unshell::protocol::tree::LeafSpec {
<Self as ::unshell::protocol::tree::LeafDeclaration>::leaf_spec()
}
/// Resolves one local procedure suffix to its full canonical `procedure_id`.
pub fn protocol_procedure_id(
suffix: &str,
) -> ::core::option::Option<::unshell::alloc::string::String> {
<Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(suffix)
}
}
}
}
fn normalize_suffix(value: &str) -> String {
let mut normalized = String::with_capacity(value.len());
let mut previous_was_separator = false;
for character in value.chars() {
if character.is_ascii_uppercase() {
if !normalized.is_empty() && !previous_was_separator {
normalized.push('_');
}
normalized.push(character.to_ascii_lowercase());
previous_was_separator = false;
continue;
}
if character.is_ascii_lowercase() || character.is_ascii_digit() {
normalized.push(character);
previous_was_separator = false;
continue;
}
if !normalized.is_empty() && !previous_was_separator {
normalized.push('_');
previous_was_separator = true;
}
}
while normalized.ends_with('_') {
normalized.pop();
}
if normalized.is_empty() {
String::from("procedure")
} else {
normalized
}
}
+30
View File
@@ -1,6 +1,7 @@
//! Proc macros for `unshell` application-layer leaf declarations.
mod leaf;
mod leaf_decl;
mod procedure;
mod procedures;
mod utils;
@@ -8,6 +9,35 @@ mod utils;
use proc_macro::TokenStream;
use syn::{DeriveInput, ItemImpl, parse_macro_input};
/// Declares one compile-time leaf surface and binds it to endpoint and/or TUI
/// host structs.
///
/// What it is: a function-like macro that generates the shared protocol-visible
/// metadata for one leaf and applies that metadata to the listed host structs.
///
/// Why it exists: endpoint and TUI hosts should not each have to repeat the leaf
/// name and procedure inventory, and endpoint construction should not need a
/// handwritten list of procedure ids.
///
/// # Example
/// ```ignore
/// unshell::leaf! {
/// name = "remote_shell",
/// procedures = [Open, Reset, whoami],
/// endpoint_struct = RemoteShellEndpoint,
/// tui_struct = RemoteShellTui,
/// }
/// ```
#[proc_macro]
pub fn leaf(input: TokenStream) -> TokenStream {
match leaf_decl::expand_leaf_declaration(parse_macro_input!(
input as leaf_decl::LeafDeclarationInput
)) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
/// Derives canonical protocol-leaf identity helpers for one host type.
///
/// What it is: a derive macro that implements `ProtocolLeaf` and generates the
+4 -2
View File
@@ -88,11 +88,13 @@ pub(crate) fn expand_procedure(input: DeriveInput) -> Result<proc_macro2::TokenS
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! {
impl #impl_generics ::unshell::protocol::tree::StatefulProcedureMetadata<#leaf_ty>
impl #impl_generics ::unshell::protocol::tree::ProcedureMetadata
for #procedure_name #ty_generics #where_clause
where
#leaf_ty: ::unshell::protocol::tree::ProtocolLeaf,
{
type Leaf = #leaf_ty;
fn procedure_suffix() -> &'static str {
#suffix
}
@@ -101,7 +103,7 @@ pub(crate) fn expand_procedure(input: DeriveInput) -> Result<proc_macro2::TokenS
impl #impl_generics #procedure_name #ty_generics #where_clause {
/// Returns the full canonical `procedure_id` for this stateful procedure.
pub fn protocol_procedure_id() -> ::unshell::alloc::string::String {
<Self as ::unshell::protocol::tree::StatefulProcedureMetadata<#leaf_ty>>::procedure_id()
<Self as ::unshell::protocol::tree::ProcedureMetadata>::procedure_id()
}
}
})
+8 -6
View File
@@ -114,19 +114,21 @@ pub(crate) fn expand_procedures(
.collect::<Vec<_>>();
let procedure_matches = dispatch_arms.iter().map(|arm| {
let suffix = &arm.suffix_literal;
quote! { #suffix => <Self as ::unshell::protocol::tree::CallProcedures>::procedure_id(#suffix), }
quote! { #suffix => <Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(#suffix), }
});
let dispatch_checks = dispatch_arms.iter().map(|arm| arm.dispatch_tokens.clone());
Ok(quote! {
#item
impl #impl_generics_tokens ::unshell::protocol::tree::CallProcedures for #self_ty #where_clause {
type Error = #error_ty;
impl #impl_generics_tokens ::unshell::protocol::tree::LeafDeclaration for #self_ty #where_clause {
fn procedure_suffixes() -> &'static [&'static str] {
&[#(#suffix_literals),*]
}
}
impl #impl_generics_tokens ::unshell::protocol::tree::CallProcedures for #self_ty #where_clause {
type Error = #error_ty;
fn dispatch_call(
&mut self,
@@ -143,7 +145,7 @@ pub(crate) fn expand_procedures(
impl #impl_generics_tokens #self_ty #where_clause {
/// Returns the canonical protocol leaf metadata for this type.
pub fn protocol_leaf_spec() -> ::unshell::protocol::tree::LeafSpec {
<Self as ::unshell::protocol::tree::CallProcedures>::leaf_spec()
<Self as ::unshell::protocol::tree::LeafDeclaration>::leaf_spec()
}
/// Resolves one local procedure suffix to its full canonical `procedure_id`.
@@ -163,7 +165,7 @@ fn expand_call_arm(method: &ImplItemFn) -> Result<CallArm> {
let method_name = &method.sig.ident;
let suffix_literal = call_suffix_literal(method)?;
let call_id_expr = quote! {
<Self as ::unshell::protocol::tree::CallProcedures>::procedure_id(#suffix_literal)
<Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(#suffix_literal)
.expect("generated procedure id must exist")
};