typhoon_instruction_builder/
lib.rs1use {
2 crate::{
3 generator::{ClientGenerator, CpiGenerator, Generator},
4 resolver::Resolver,
5 },
6 cargo_manifest::{Dependency, Manifest},
7 hashbrown::{HashMap, HashSet},
8 heck::ToKebabCase,
9 proc_macro2::{Span, TokenStream},
10 quote::{format_ident, quote, ToTokens},
11 std::path::Path,
12 syn::{parse::Parse, parse_macro_input, visit::Visit, Ident, Item, Token},
13 typhoon_syn::{Argument, Arguments, Context, Instruction, InstructionArg, InstructionsList},
14};
15
16mod generator;
17mod mod_path;
18mod resolver;
19
20#[proc_macro]
21pub fn generate_instructions_client(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
22 let instructions = parse_macro_input!(input as GeneratorContext);
23
24 instructions
25 .generate::<ClientGenerator>()
26 .into_token_stream()
27 .into()
28}
29
30#[proc_macro]
31pub fn generate_cpi_client(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
32 let instructions = parse_macro_input!(input as GeneratorContext);
33
34 instructions
35 .generate::<CpiGenerator>()
36 .into_token_stream()
37 .into()
38}
39
40#[derive(Default)]
41struct GeneratorContext {
42 pub instructions: HashMap<usize, Instruction>,
43 pub context: HashMap<String, Context>,
44 pub arg_structs: HashMap<String, Vec<Argument>>,
45}
46
47impl GeneratorContext {
48 pub fn from_resolver(
49 ix_list: InstructionsList,
50 resolver: Resolver,
51 filter: Option<HashSet<Ident>>,
52 ) -> Self {
53 let mut gen = GeneratorContext::default();
54 let (mut instructions_map, mut contexts_map) = Self::parse_items(&resolver.items);
55
56 for (index, ident) in ix_list.0 {
57 if let Some(ref filter_set) = filter {
58 if !filter_set.contains(&ident) {
59 continue;
60 }
61 }
62
63 if let Some(instruction) = instructions_map.remove(&ident) {
64 gen.instructions.insert(index, instruction);
65 }
66 }
67
68 for ix in gen.instructions.values() {
69 for (_, arg_value) in &ix.args {
70 if let InstructionArg::Context(ctx_name) = arg_value {
71 let ctx_name = ctx_name.to_string();
72 if let Some(context) = contexts_map.remove(&ctx_name) {
73 if let Some(Arguments::Values(ref args)) = context.arguments {
74 gen.arg_structs
75 .entry(format!("{}Args", context.name))
76 .or_insert_with(|| args.to_vec());
77 }
78
79 gen.context.insert(ctx_name, context);
80 }
81 }
82 }
83 }
84 gen
85 }
86
87 fn parse_items(items: &[Item]) -> (HashMap<Ident, Instruction>, HashMap<String, Context>) {
88 let mut instructions = HashMap::new();
89 let mut contexts = HashMap::new();
90
91 for item in items {
92 match item {
93 Item::Fn(item_fn) => {
94 if let Ok(ix) = Instruction::try_from(item_fn) {
95 instructions.insert(ix.name.clone(), ix);
96 }
97 }
98 Item::Struct(item_struct) => {
99 if let Ok(ctx) = Context::try_from(item_struct) {
100 contexts.insert(ctx.name.to_string(), ctx);
101 }
102 }
103 _ => continue,
104 }
105 }
106
107 (instructions, contexts)
108 }
109
110 pub fn generate<T: Generator>(&self) -> TokenStream {
111 let extra_token: Vec<TokenStream> = self.arg_structs.iter().map(|(name, v)| {
112 let struct_name = format_ident!("{name}");
113 let fields = v
114 .iter()
115 .map(|Argument { name, ty }: &Argument| quote!(pub #name: #ty));
116 quote! {
117 #[derive(Debug, PartialEq, bytemuck::AnyBitPattern, bytemuck::NoUninit, Copy, Clone)]
118 #[repr(C)]
119 pub struct #struct_name {
120 #(#fields),*
121 }
122 }
123 }).collect();
124 T::generate_token(&self.instructions, &self.context, quote!(#(#extra_token)*))
125 }
126}
127
128impl Parse for GeneratorContext {
129 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
130 let krate: Ident = input.parse()?;
131 let crate_name = krate.to_string();
132 let crate_kebab_name = crate_name.to_kebab_case();
133 let cargo_toml_dir = std::env::var("CARGO_MANIFEST_DIR")
134 .map_err(|_| syn::Error::new(Span::call_site(), "Not in valid rust project."))?;
135 let manifest = Manifest::from_path(format!("{cargo_toml_dir}/Cargo.toml"))
137 .map_err(|_| syn::Error::new(Span::call_site(), "Invalid Cargo.toml file."))?;
138
139 let package_relative_path = get_package_path(&manifest, &crate_kebab_name).ok_or(
140 syn::Error::new(Span::call_site(), "Cannot find the package."),
141 )?;
142
143 let package_absolute_path = format!("{cargo_toml_dir}/{package_relative_path}");
144 let path = Path::new(&package_absolute_path);
145 let file = read_and_parse_file(path)?;
146
147 let mut ix_list = InstructionsList::default();
148 ix_list.visit_file(&file);
149
150 let mut resolver = Resolver::new(path, true);
151 resolver.visit_file(&file);
152
153 if input.peek(Token![,]) {
154 input.parse::<Token![,]>()?;
155 let content;
156 let _ = syn::bracketed!(content in input);
157 let idents = content.parse_terminated(Ident::parse, Token![,])?;
158 let instructions = HashSet::from_iter(idents);
159 Ok(Self::from_resolver(ix_list, resolver, Some(instructions)))
160 } else {
161 Ok(Self::from_resolver(ix_list, resolver, None))
162 }
163 }
164}
165
166fn read_and_parse_file(source_file: impl AsRef<Path>) -> syn::Result<syn::File> {
167 let file_content = std::fs::read_to_string(&source_file)
168 .map_err(|err| syn::Error::new(Span::call_site(), err.to_string()))?;
169
170 syn::parse_file(&file_content)
171}
172
173fn get_package_path(manifest: &Manifest, crate_name: &str) -> Option<String> {
174 let package = manifest.package.as_ref()?;
175 if package.name.to_kebab_case() == crate_name {
176 Some("src/lib.rs".to_string())
177 } else {
178 let dependency: Dependency = package
179 .metadata
180 .as_ref()?
181 .get("typhoon")?
182 .as_table()?
183 .get("builder-dependencies")?
184 .as_table()?
185 .iter()
186 .find_map(|(key, value)| (key.to_kebab_case() == crate_name).then_some(value))?
187 .clone()
188 .try_into()
189 .ok()?;
190 Some(format!(
191 "{}/src/lib.rs",
192 dependency.detail()?.path.as_ref()?
193 ))
194 }
195}