Skip to main content

typhoon_instruction_builder/
lib.rs

1use {
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 temp_dir = env!("PROC_ARTIFACT_DIR");
136        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}