Basic example to try the harmony! macro

This commit is contained in:
Ian Letourneau
2025-11-10 11:50:22 -05:00
commit cc6da434c7
9 changed files with 377 additions and 0 deletions

11
harmony_macros/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "harmony_macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.42"
syn = "2.0.109"

118
harmony_macros/src/lib.rs Normal file
View File

@@ -0,0 +1,118 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{Expr, Ident, punctuated::Punctuated, token};
// Define the struct for our macro's input DSL
struct HarmonyInput {
inventory_expr: Expr,
topology_expr: Expr,
scores_array: Punctuated<Expr, token::Comma>,
}
// Custom parser for `key: value,` syntax
impl syn::parse::Parse for HarmonyInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut inventory_expr = None;
let mut topology_expr = None;
let mut scores_array = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<token::Colon>()?;
if key == "inventory" {
inventory_expr = Some(input.parse()?);
} else if key == "topology" {
topology_expr = Some(input.parse()?);
} else if key == "scores" {
let content;
syn::bracketed!(content in input);
scores_array = Some(Punctuated::parse_terminated(&content)?);
} else {
return Err(syn::Error::new(key.span(), "unknown key"));
}
// Eat the optional trailing comma
let _ = input.parse::<token::Comma>();
}
Ok(HarmonyInput {
inventory_expr: inventory_expr.ok_or_else(|| input.error("missing 'inventory' key"))?,
topology_expr: topology_expr.ok_or_else(|| input.error("missing 'topology' key"))?,
scores_array: scores_array.ok_or_else(|| input.error("missing 'scores' key"))?,
})
}
}
/// The main Harmony entrypoint macro.
///
/// This generates the `fn main()` and `tokio` runtime,
/// and calls `harmony::run_cli` with your provided
/// `inventory`, `topology`, and `scores`.
#[proc_macro]
pub fn harmony(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as HarmonyInput);
let inventory_expr = input.inventory_expr;
let topology_expr = input.topology_expr;
// Create the `Box::new()` for each score
let boxed_scores = input.scores_array.iter().map(|score_expr| {
quote! { Box::new(#score_expr) }
});
// Generate the `fn main()`
let expanded = quote! {
// This is the auto-generated main
fn main() {
// 1. Setup the Tokio runtime
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to build Tokio runtime");
runtime.block_on(async {
// 3. Evaluate the user's expressions
let inventory = #inventory_expr;
let topology = #topology_expr;
// 4. This helper function is the key.
// It lets the compiler infer `T` from `topology` and
// type-check the `scores` against it.
fn __harmony_collect_scores<T: harmony::Topology>(
// `topology` is passed to pin down the type `T`
_topology: &T,
) -> Vec<Box<dyn harmony::Score<T>>> {
let mut v: Vec<Box<dyn harmony::Score<T>>> = Vec::new();
#(
v.push(#boxed_scores);
)*
v
}
// 5. Create the scores vector
let scores_vec = {
__harmony_collect_scores(&topology)
};
// 6. Call the *real* run function
let result = harmony::run_cli(
inventory,
topology,
scores_vec,
).await;
// 7. Handle the final result
if let Err(e) = result {
eprintln!("\nError: {}", e);
std::process::exit(1);
}
});
}
};
TokenStream::from(expanded)
}