From 6b5ed789fc856408986e8868229887354d6d4073 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Sun, 30 May 2021 12:59:33 -0700 Subject: [PATCH] lang, spl, cli: Refactor to preserve span information (#341) --- CHANGELOG.md | 38 +- cli/src/config.rs | 2 +- cli/src/main.rs | 2 +- examples/chat/programs/chat/src/lib.rs | 2 +- examples/misc/programs/misc/src/lib.rs | 2 +- lang/attribute/interface/src/lib.rs | 2 +- lang/attribute/program/src/lib.rs | 9 +- lang/derive/accounts/src/lib.rs | 9 +- lang/src/ctor.rs | 2 +- lang/src/idl.rs | 8 +- lang/syn/src/codegen/accounts.rs | 720 --------- .../src/codegen/accounts/__client_accounts.rs | 129 ++ lang/syn/src/codegen/accounts/constraints.rs | 471 ++++++ lang/syn/src/codegen/accounts/exit.rs | 39 + lang/syn/src/codegen/accounts/mod.rs | 43 + .../src/codegen/accounts/to_account_infos.rs | 34 + .../src/codegen/accounts/to_account_metas.rs | 40 + lang/syn/src/codegen/accounts/try_accounts.rs | 198 +++ lang/syn/src/codegen/program.rs | 1379 ----------------- lang/syn/src/codegen/program/accounts.rs | 65 + lang/syn/src/codegen/program/common.rs | 83 + lang/syn/src/codegen/program/cpi.rs | 118 ++ lang/syn/src/codegen/program/dispatch.rs | 215 +++ lang/syn/src/codegen/program/entry.rs | 75 + lang/syn/src/codegen/program/handlers.rs | 592 +++++++ lang/syn/src/codegen/program/instruction.rs | 194 +++ lang/syn/src/codegen/program/mod.rs | 35 + lang/syn/src/{parser => idl}/file.rs | 39 +- lang/syn/src/{idl.rs => idl/mod.rs} | 2 + lang/syn/src/lib.rs | 438 +++--- lang/syn/src/parser/accounts.rs | 429 ----- lang/syn/src/parser/accounts/constraints.rs | 385 +++++ lang/syn/src/parser/accounts/mod.rs | 218 +++ lang/syn/src/parser/mod.rs | 2 - lang/syn/src/parser/program.rs | 304 ---- lang/syn/src/parser/program/instructions.rs | 59 + lang/syn/src/parser/program/mod.rs | 59 + lang/syn/src/parser/program/state.rs | 312 ++++ spl/src/shmem.rs | 5 +- 39 files changed, 3680 insertions(+), 3078 deletions(-) delete mode 100644 lang/syn/src/codegen/accounts.rs create mode 100644 lang/syn/src/codegen/accounts/__client_accounts.rs create mode 100644 lang/syn/src/codegen/accounts/constraints.rs create mode 100644 lang/syn/src/codegen/accounts/exit.rs create mode 100644 lang/syn/src/codegen/accounts/mod.rs create mode 100644 lang/syn/src/codegen/accounts/to_account_infos.rs create mode 100644 lang/syn/src/codegen/accounts/to_account_metas.rs create mode 100644 lang/syn/src/codegen/accounts/try_accounts.rs delete mode 100644 lang/syn/src/codegen/program.rs create mode 100644 lang/syn/src/codegen/program/accounts.rs create mode 100644 lang/syn/src/codegen/program/common.rs create mode 100644 lang/syn/src/codegen/program/cpi.rs create mode 100644 lang/syn/src/codegen/program/dispatch.rs create mode 100644 lang/syn/src/codegen/program/entry.rs create mode 100644 lang/syn/src/codegen/program/handlers.rs create mode 100644 lang/syn/src/codegen/program/instruction.rs create mode 100644 lang/syn/src/codegen/program/mod.rs rename lang/syn/src/{parser => idl}/file.rs (91%) rename lang/syn/src/{idl.rs => idl/mod.rs} (99%) delete mode 100644 lang/syn/src/parser/accounts.rs create mode 100644 lang/syn/src/parser/accounts/constraints.rs create mode 100644 lang/syn/src/parser/accounts/mod.rs delete mode 100644 lang/syn/src/parser/program.rs create mode 100644 lang/syn/src/parser/program/instructions.rs create mode 100644 lang/syn/src/parser/program/mod.rs create mode 100644 lang/syn/src/parser/program/state.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4c5c6a25..ad6bb0d4dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,22 +11,28 @@ incremented for features. ## [Unreleased] -## Features +### Features -* ts: Address metadata is now optional for `anchor.workspace` clients ([#310](https://github.com/project-serum/anchor/pull/310)). * cli: Add global options for override Anchor.toml values ([#313](https://github.com/project-serum/anchor/pull/313)). * spl: Add `SetAuthority` instruction ([#307](https://github.com/project-serum/anchor/pull/307/files)). +* lang: `constraint = ` added as a replacement for (the now deprecated) string literal constraints ([#341](https://github.com/project-serum/anchor/pull/341)). +* lang: Span information is now preserved, providing informative compiler error messages ([#341](https://github.com/project-serum/anchor/pull/341)). +* ts: Address metadata is now optional for `anchor.workspace` clients ([#310](https://github.com/project-serum/anchor/pull/310)). -## Breaking Changes +### Breaking Changes * ts: Retrieving deserialized accounts from the `.account.` and `.state` namespaces now require explicitly invoking the `fetch` API. For example, `program.account.myAccount()` and `program.state()` is now `program.account.myAccount.fetch(
)` and `program.state.fetch()` ([#322](https://github.com/project-serum/anchor/pull/322)). -* lang: `#[account(associated)]` now requires `init` to be provided to create an associated account. If not provided, then the address will be assumed to exist, and a constraint will be added to ensure its correctness ([#318](https://github.com/project-serum/anchor/pull/318)). +* lang: `#[account(associated)]` now requires `init` to be provided to create an associated account. If not provided, then the address will be assumed to exist, and a constraint will be added to ensure the correctness of the address ([#318](https://github.com/project-serum/anchor/pull/318)). * lang, ts: Change account discriminator pre-image of the `#[state]` account discriminator to be namespaced by "state:" ([#320](https://github.com/project-serum/anchor/pull/320)). * lang, ts: Change domain delimiters for the pre-image of the instruciton sighash to be a single colon `:` to be consistent with accounts ([#321](https://github.com/project-serum/anchor/pull/321)). +* lang: Associated constraints no longer automatically implement `mut` ([#341](https://github.com/project-serum/anchor/pull/341)). +* lang: Associated `space` constraints must now be literal integers instead of literal strings ([#341](https://github.com/project-serum/anchor/pull/341)). + +### Fixes ## [0.6.0] - 2021-05-23 -## Features +### Features * ts: Add `program.simulate` namespace ([#266](https://github.com/project-serum/anchor/pull/266)). * ts: Introduce `Address` type, allowing one to use Base 58 encoded strings in public APIs ([#304](https://github.com/project-serum/anchor/pull/304)). @@ -36,7 +42,7 @@ incremented for features. * cli: Add `--skip-build` flag to test command ([301](https://github.com/project-serum/anchor/pull/301)). * cli: Add `anchor shell` command to spawn a node shell populated with an Anchor.toml based environment ([#303](https://github.com/project-serum/anchor/pull/303)). -## Breaking Changes +### Breaking Changes * cli: The Anchor.toml's `wallet` and `cluster` settings must now be under the `[provider]` table ([#305](https://github.com/project-serum/anchor/pull/305)). * ts: Event coder `decode` API changed to decode strings directly instead of buffers ([#292](https://github.com/project-serum/anchor/pull/292)). @@ -44,13 +50,13 @@ incremented for features. ## [0.5.0] - 2021-05-07 -## Features +### Features * client: Adds support for state instructions ([#248](https://github.com/project-serum/anchor/pull/248)). * lang: Add `anchor-debug` feature flag for logging ([#253](https://github.com/project-serum/anchor/pull/253)). * ts: Add support for u16 ([#255](https://github.com/project-serum/anchor/pull/255)). -## Breaking +### Breaking Changes * client: Renames `RequestBuilder::new` to `RequestBuilder::from` ([#248](https://github.com/project-serum/anchor/pull/248)). * lang: Renames the generated `instruction::state::Ctor` struct to `instruction::state::New` ([#248](https://github.com/project-serum/anchor/pull/248)). @@ -61,7 +67,7 @@ incremented for features. ## [0.4.4] - 2021-04-18 -## Features +### Features * lang: Allows one to specify multiple `with` targets when creating associated acconts ([#197](https://github.com/project-serum/anchor/pull/197)). * lang, ts: Add array support ([#202](https://github.com/project-serum/anchor/pull/202)). @@ -70,19 +76,19 @@ incremented for features. ## [0.4.3] - 2021-04-13 -## Features +### Features * lang: CPI clients for program state instructions ([#43](https://github.com/project-serum/anchor/pull/43)). * lang: Add `#[account(owner = )]` constraint ([#178](https://github.com/project-serum/anchor/pull/178)). * lang, cli, ts: Add `#[account(associated = )]` and `#[associated]` attributes for creating associated program accounts within programs. The TypeScript package can fetch these accounts with a new `.account..associated` (and `associatedAddress`) method ([#186](https://github.com/project-serum/anchor/pull/186)). -## Fixes +### Fixes * lang: Unused `#[account]`s are now parsed into the IDL correctly ([#177](https://github.com/project-serum/anchor/pull/177)). ## [0.4.2] - 2021-04-10 -## Features +### Features * cli: Fund Anchor.toml configured wallet when testing ([#164](https://github.com/project-serum/anchor/pull/164)). * spl: Add initialize_account instruction for spl tokens ([#166](https://github.com/project-serum/anchor/pull/166)). @@ -93,7 +99,7 @@ incremented for features. ## [0.4.0] - 2021-04-04 -## Features +### Features * cli: Specify test files to run ([#118](https://github.com/project-serum/anchor/pull/118)). * lang: Allow overriding the `#[state]` account's size ([#121](https://github.com/project-serum/anchor/pull/121)). @@ -102,7 +108,7 @@ incremented for features. * cli: TypeScript migrations ([#132](https://github.com/project-serum/anchor/pull/132)). * lang: Add `#[account(executable)]` attribute ([#140](https://github.com/project-serum/anchor/pull/140)). -## Breaking Changes +### Breaking Changes * client: Replace url str with `Cluster` struct when constructing clients ([#89](https://github.com/project-serum/anchor/pull/89)). * lang: Changes the account discriminator of `IdlAccount` to be namespaced by `"internal"` ([#128](https://github.com/project-serum/anchor/pull/128)). @@ -110,7 +116,7 @@ incremented for features. ## [0.3.0] - 2021-03-12 -## Features +### Features * ts: Allow preloading instructions for state rpc transactions ([cf9c84](https://github.com/project-serum/anchor/commit/cf9c847e4144989b5bc1936149d171e90204777b)). * ts: Export sighash coder function ([734c75](https://github.com/project-serum/anchor/commit/734c751882f43beec7ea3f0f4d988b502e3f24e4)). @@ -124,7 +130,7 @@ incremented for features. * lang: Removes `IdlInstruction::Clear` ([#107](https://github.com/project-serum/anchor/pull/107)). -## Fixes +### Fixes * cli: Propagates mocha test exit status on error ([79b791](https://github.com/project-serum/anchor/commit/79b791ffa85ffae5b6163fa853562aa568650f21)). diff --git a/cli/src/config.rs b/cli/src/config.rs index 144a798d98..0eee2cc6b5 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -210,7 +210,7 @@ pub fn read_all_programs() -> Result> { let mut r = vec![]; for f in files { let path = f?.path(); - let idl = anchor_syn::parser::file::parse(path.join("src/lib.rs"))?; + let idl = anchor_syn::idl::file::parse(path.join("src/lib.rs"))?; let lib_name = extract_lib_name(&path.join("Cargo.toml"))?; r.push(Program { lib_name, diff --git a/cli/src/main.rs b/cli/src/main.rs index cdcc7e992a..939daf587e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -633,7 +633,7 @@ fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result { fn extract_idl(file: &str) -> Result { let file = shellexpand::tilde(file); - anchor_syn::parser::file::parse(&*file) + anchor_syn::idl::file::parse(&*file) } fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { diff --git a/examples/chat/programs/chat/src/lib.rs b/examples/chat/programs/chat/src/lib.rs index da38a1fdb2..b363ad6660 100644 --- a/examples/chat/programs/chat/src/lib.rs +++ b/examples/chat/programs/chat/src/lib.rs @@ -36,7 +36,7 @@ pub mod chat { #[derive(Accounts)] pub struct CreateUser<'info> { - #[account(init, associated = authority, space = "312")] + #[account(init, associated = authority, space = 312)] user: ProgramAccount<'info, User>, #[account(signer)] authority: AccountInfo<'info>, diff --git a/examples/misc/programs/misc/src/lib.rs b/examples/misc/programs/misc/src/lib.rs index 672e9eeea7..931d88f98a 100644 --- a/examples/misc/programs/misc/src/lib.rs +++ b/examples/misc/programs/misc/src/lib.rs @@ -140,7 +140,7 @@ pub struct TestInitAssociatedAccount<'info> { #[derive(Accounts)] pub struct TestAssociatedAccount<'info> { - #[account(associated = authority, with = state, with = data)] + #[account(mut, associated = authority, with = state, with = data)] my_account: ProgramAccount<'info, TestData>, #[account(mut, signer)] authority: AccountInfo<'info>, diff --git a/lang/attribute/interface/src/lib.rs b/lang/attribute/interface/src/lib.rs index ae748c6558..fab6336138 100644 --- a/lang/attribute/interface/src/lib.rs +++ b/lang/attribute/interface/src/lib.rs @@ -201,7 +201,7 @@ pub fn interface( } }; - let sighash_arr = anchor_syn::codegen::program::sighash(&trait_name, &method_name.to_string()); + let sighash_arr = anchor_syn::codegen::program::common::sighash(&trait_name, &method_name.to_string()); let sighash_tts: proc_macro2::TokenStream = format!("{:?}", sighash_arr).parse().unwrap(); quote! { diff --git a/lang/attribute/program/src/lib.rs b/lang/attribute/program/src/lib.rs index 6deec4bfce..2531d26ef1 100644 --- a/lang/attribute/program/src/lib.rs +++ b/lang/attribute/program/src/lib.rs @@ -1,7 +1,6 @@ extern crate proc_macro; -use anchor_syn::codegen::program as program_codegen; -use anchor_syn::parser::program as program_parser; +use quote::ToTokens; use syn::parse_macro_input; /// The `#[program]` attribute defines the module containing all instruction @@ -11,7 +10,7 @@ pub fn program( _args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let program_mod = parse_macro_input!(input as syn::ItemMod); - let code = program_codegen::generate(program_parser::parse(program_mod)); - proc_macro::TokenStream::from(code) + parse_macro_input!(input as anchor_syn::Program) + .to_token_stream() + .into() } diff --git a/lang/derive/accounts/src/lib.rs b/lang/derive/accounts/src/lib.rs index 8a19e8a6a2..f7d0b67dc7 100644 --- a/lang/derive/accounts/src/lib.rs +++ b/lang/derive/accounts/src/lib.rs @@ -1,8 +1,7 @@ extern crate proc_macro; -use anchor_syn::codegen::accounts as accounts_codegen; -use anchor_syn::parser::accounts as accounts_parser; use proc_macro::TokenStream; +use quote::ToTokens; use syn::parse_macro_input; /// Implements an [`Accounts`](./trait.Accounts.html) deserializer on the given @@ -54,7 +53,7 @@ use syn::parse_macro_input; // on absurdly long lines? #[proc_macro_derive(Accounts, attributes(account))] pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { - let strct = parse_macro_input!(item as syn::ItemStruct); - let tts = accounts_codegen::generate(accounts_parser::parse(&strct)); - proc_macro::TokenStream::from(tts) + parse_macro_input!(item as anchor_syn::AccountsStruct) + .to_token_stream() + .into() } diff --git a/lang/src/ctor.rs b/lang/src/ctor.rs index 4b68c61271..97600c275b 100644 --- a/lang/src/ctor.rs +++ b/lang/src/ctor.rs @@ -1,4 +1,4 @@ -use crate::{Accounts, Sysvar}; +use crate::{Accounts, Sysvar, ToAccountInfo}; use solana_program::account_info::AccountInfo; use solana_program::sysvar::rent::Rent; diff --git a/lang/src/idl.rs b/lang/src/idl.rs index 7a1655be08..266e7167a6 100644 --- a/lang/src/idl.rs +++ b/lang/src/idl.rs @@ -49,7 +49,7 @@ pub type IdlCreateAccounts<'info> = crate::ctor::Ctor<'info>; pub struct IdlAccounts<'info> { #[account(mut, has_one = authority)] pub idl: ProgramAccount<'info, IdlAccount>, - #[account(signer, "authority.key != &Pubkey::new_from_array([0u8; 32])")] + #[account(signer, constraint = authority.key != &Pubkey::new_from_array([0u8; 32]))] pub authority: AccountInfo<'info>, } @@ -58,7 +58,7 @@ pub struct IdlAccounts<'info> { pub struct IdlCreateBuffer<'info> { #[account(init)] pub buffer: ProgramAccount<'info, IdlAccount>, - #[account(signer, "authority.key != &Pubkey::new_from_array([0u8; 32])")] + #[account(signer, constraint = authority.key != &Pubkey::new_from_array([0u8; 32]))] pub authority: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, } @@ -67,12 +67,12 @@ pub struct IdlCreateBuffer<'info> { #[derive(Accounts)] pub struct IdlSetBuffer<'info> { // The buffer with the new idl data. - #[account(mut, "buffer.authority == idl.authority")] + #[account(mut, constraint = buffer.authority == idl.authority)] pub buffer: ProgramAccount<'info, IdlAccount>, // The idl account to be updated with the buffer's data. #[account(mut, has_one = authority)] pub idl: ProgramAccount<'info, IdlAccount>, - #[account(signer, "authority.key != &Pubkey::new_from_array([0u8; 32])")] + #[account(signer, constraint = authority.key != &Pubkey::new_from_array([0u8; 32]))] pub authority: AccountInfo<'info>, } diff --git a/lang/syn/src/codegen/accounts.rs b/lang/syn/src/codegen/accounts.rs deleted file mode 100644 index 21eaed7c49..0000000000 --- a/lang/syn/src/codegen/accounts.rs +++ /dev/null @@ -1,720 +0,0 @@ -use crate::{ - AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAssociated, - ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, ConstraintOwner, - ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, Field, Ty, -}; -use heck::SnakeCase; -use quote::quote; - -pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { - // All fields without an `#[account(associated)]` attribute. - let non_associated_fields: Vec<&AccountField> = accs - .fields - .iter() - .filter(|af| !is_associated_init(af)) - .collect(); - - // Deserialization for each field - let deser_fields: Vec = accs - .fields - .iter() - .map(|af: &AccountField| { - match af { - AccountField::AccountsStruct(s) => { - let name = &s.ident; - let ty = &s.raw_field.ty; - quote! { - #[cfg(feature = "anchor-debug")] - ::solana_program::log::sol_log(stringify!(#name)); - let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?; - } - } - AccountField::Field(f) => { - // Associated fields are *first* deserialized into - // AccountInfos, and then later deserialized into - // ProgramAccounts in the "constraint check" phase. - if is_associated_init(af) { - let name = &f.ident; - quote!{ - let #name = &accounts[0]; - *accounts = &accounts[1..]; - } - } else { - let name = &f.typed_ident(); - match f.is_init { - false => quote! { - #[cfg(feature = "anchor-debug")] - ::solana_program::log::sol_log(stringify!(#name)); - let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?; - }, - true => quote! { - #[cfg(feature = "anchor-debug")] - ::solana_program::log::sol_log(stringify!(#name)); - let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?; - }, - } - } - } - } - }) - .collect(); - - // Deserialization for each *associated* field. This must be after - // the deser_fields. - let deser_associated_fields: Vec = accs - .fields - .iter() - .filter_map(|af| match af { - AccountField::AccountsStruct(_s) => None, - AccountField::Field(f) => match is_associated_init(af) { - false => None, - true => Some(f), - }, - }) - .map(|field: &Field| { - // TODO: the constraints should be sorted so that the associated - // constraint comes first. - let checks = field - .constraints - .iter() - .map(|c| generate_field_constraint(&field, c)) - .collect::>(); - quote! { - #(#checks)* - } - }) - .collect(); - - // Constraint checks for each account fields. - let access_checks: Vec = non_associated_fields - .iter() - .map(|af: &&AccountField| { - let checks: Vec = match af { - AccountField::Field(f) => f - .constraints - .iter() - .map(|c| generate_field_constraint(&f, c)) - .collect(), - AccountField::AccountsStruct(s) => s - .constraints - .iter() - .map(|c| generate_composite_constraint(&s, c)) - .collect(), - }; - quote! { - #(#checks)* - } - }) - .collect(); - - // Each field in the final deserialized accounts struct. - let return_tys: Vec = accs - .fields - .iter() - .map(|f: &AccountField| { - let name = match f { - AccountField::AccountsStruct(s) => &s.ident, - AccountField::Field(f) => &f.ident, - }; - quote! { - #name - } - }) - .collect(); - - // Exit program code-blocks for each account. - let on_save: Vec = accs - .fields - .iter() - .map(|af: &AccountField| match af { - AccountField::AccountsStruct(s) => { - let name = &s.ident; - quote! { - anchor_lang::AccountsExit::exit(&self.#name, program_id)?; - } - } - AccountField::Field(f) => { - let ident = &f.ident; - match f.is_mut { - false => quote! {}, - true => quote! { - anchor_lang::AccountsExit::exit(&self.#ident, program_id)?; - }, - } - } - }) - .collect(); - - // Implementation for `ToAccountInfos` trait. - let to_acc_infos: Vec = accs - .fields - .iter() - .map(|f: &AccountField| { - let name = match f { - AccountField::AccountsStruct(s) => &s.ident, - AccountField::Field(f) => &f.ident, - }; - quote! { - account_infos.extend(self.#name.to_account_infos()); - } - }) - .collect(); - - // Implementation for `ToAccountMetas` trait. - let to_acc_metas: Vec = accs - .fields - .iter() - .map(|f: &AccountField| { - let (name, is_signer) = match f { - AccountField::AccountsStruct(s) => (&s.ident, quote! {None}), - AccountField::Field(f) => { - let is_signer = match f.is_signer { - false => quote! {None}, - true => quote! {Some(true)}, - }; - (&f.ident, is_signer) - } - }; - quote! { - account_metas.extend(self.#name.to_account_metas(#is_signer)); - } - }) - .collect(); - - let name = &accs.ident; - let (combined_generics, trait_generics, strct_generics) = match accs.generics.lt_token { - None => (quote! {<'info>}, quote! {<'info>}, quote! {}), - Some(_) => { - let g = &accs.generics; - (quote! {#g}, quote! {#g}, quote! {#g}) - } - }; - - let account_mod_name: proc_macro2::TokenStream = format!( - "__client_accounts_{}", - accs.ident.to_string().to_snake_case() - ) - .parse() - .unwrap(); - - let account_struct_fields: Vec = accs - .fields - .iter() - .map(|f: &AccountField| match f { - AccountField::AccountsStruct(s) => { - let name = &s.ident; - let symbol: proc_macro2::TokenStream = format!( - "__client_accounts_{0}::{1}", - s.symbol.to_snake_case(), - s.symbol, - ) - .parse() - .unwrap(); - quote! { - pub #name: #symbol - } - } - AccountField::Field(f) => { - let name = &f.ident; - quote! { - pub #name: anchor_lang::solana_program::pubkey::Pubkey - } - } - }) - .collect(); - - let account_struct_metas: Vec = accs - .fields - .iter() - .map(|f: &AccountField| match f { - AccountField::AccountsStruct(s) => { - let name = &s.ident; - quote! { - account_metas.extend(self.#name.to_account_metas(None)); - } - } - AccountField::Field(f) => { - let is_signer = match f.is_signer { - false => quote! {false}, - true => quote! {true}, - }; - let meta = match f.is_mut { - false => quote! { anchor_lang::solana_program::instruction::AccountMeta::new_readonly }, - true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new }, - }; - let name = &f.ident; - quote! { - account_metas.push(#meta(self.#name, #is_signer)); - } - } - }) - .collect(); - - // Re-export all composite account structs (i.e. other structs deriving - // accounts embedded into this struct. Required because, these embedded - // structs are *not* visible from the #[program] macro, which is responsible - // for generating the `accounts` mod, which aggregates all the the generated - // accounts used for structs. - let re_exports: Vec = { - // First, dedup the exports. - let mut re_exports = std::collections::HashSet::new(); - for f in accs.fields.iter().filter_map(|f: &AccountField| match f { - AccountField::AccountsStruct(s) => Some(s), - AccountField::Field(_) => None, - }) { - re_exports.insert(format!( - "__client_accounts_{0}::{1}", - f.symbol.to_snake_case(), - f.symbol, - )); - } - - re_exports - .iter() - .map(|symbol: &String| { - let symbol: proc_macro2::TokenStream = symbol.parse().unwrap(); - quote! { - pub use #symbol; - } - }) - .collect() - }; - - quote! { - /// An internal, Anchor generated module. This is used (as an - /// implementation detail), to generate a struct for a given - /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, - /// instead of an `AccountInfo`. This is useful for clients that want - /// to generate a list of accounts, without explicitly knowing the - /// order all the fields should be in. - /// - /// To access the struct in this module, one should use the sibling - /// `accounts` module (also generated), which re-exports this. - mod #account_mod_name { - use super::*; - use anchor_lang::prelude::borsh; - #(#re_exports)* - - #[derive(anchor_lang::AnchorSerialize)] - pub struct #name { - #(#account_struct_fields),* - } - - impl anchor_lang::ToAccountMetas for #name { - fn to_account_metas(&self, is_signer: Option) -> Vec { - let mut account_metas = vec![]; - - #(#account_struct_metas)* - - account_metas - } - } - } - - impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics { - #[inline(never)] - fn try_accounts(program_id: &anchor_lang::solana_program::pubkey::Pubkey, accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>]) -> std::result::Result { - // Deserialize each account. - #(#deser_fields)* - // Deserialize each associated account. - // - // Associated accounts are treated specially, because the fields - // do deserialization + constraint checks in a single go, - // whereas all other fields, i.e. the `deser_fields`, first - // deserialize, and then do constraint checks. - #(#deser_associated_fields)* - // Perform constraint checks on each account. - #(#access_checks)* - // Success. Return the validated accounts. - Ok(#name { - #(#return_tys),* - }) - } - } - - impl#combined_generics anchor_lang::ToAccountInfos#trait_generics for #name#strct_generics { - fn to_account_infos(&self) -> Vec> { - let mut account_infos = vec![]; - - #(#to_acc_infos)* - - account_infos - } - } - - impl#combined_generics anchor_lang::ToAccountMetas for #name#strct_generics { - fn to_account_metas(&self, is_signer: Option) -> Vec { - let mut account_metas = vec![]; - - #(#to_acc_metas)* - - - account_metas - } - } - - impl#combined_generics anchor_lang::AccountsExit#trait_generics for #name#strct_generics { - fn exit(&self, program_id: &anchor_lang::solana_program::pubkey::Pubkey) -> anchor_lang::solana_program::entrypoint::ProgramResult { - #(#on_save)* - Ok(()) - } - } - } -} - -// Returns true if the given AccountField has an associated init constraint. -fn is_associated_init(af: &AccountField) -> bool { - match af { - AccountField::AccountsStruct(_s) => false, - AccountField::Field(f) => f - .constraints - .iter() - .filter(|c| match c { - Constraint::Associated(c) => c.is_init, - _ => false, - }) - .next() - .is_some(), - } -} - -pub fn generate_field_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream { - match c { - Constraint::BelongsTo(c) => generate_constraint_belongs_to(f, c), - Constraint::Signer(c) => generate_constraint_signer(f, c), - Constraint::Literal(c) => generate_constraint_literal(c), - Constraint::Owner(c) => generate_constraint_owner(f, c), - Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c), - Constraint::Seeds(c) => generate_constraint_seeds(f, c), - Constraint::Executable(c) => generate_constraint_executable(f, c), - Constraint::State(c) => generate_constraint_state(f, c), - Constraint::Associated(c) => generate_constraint_associated(f, c), - } -} - -pub fn generate_composite_constraint( - _f: &CompositeField, - c: &Constraint, -) -> proc_macro2::TokenStream { - match c { - Constraint::Literal(c) => generate_constraint_literal(c), - _ => panic!("Composite fields can only use literal constraints"), - } -} - -pub fn generate_constraint_belongs_to( - f: &Field, - c: &ConstraintBelongsTo, -) -> proc_macro2::TokenStream { - let target = c.join_target.clone(); - let ident = &f.ident; - let field = match &f.ty { - Ty::Loader(_) => quote! {#ident.load()?}, - _ => quote! {#ident}, - }; - quote! { - if &#field.#target != #target.to_account_info().key { - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes - } - } -} - -pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macro2::TokenStream { - let ident = &f.ident; - let info = match f.ty { - Ty::AccountInfo => quote! { #ident }, - Ty::ProgramAccount(_) => quote! { #ident.to_account_info() }, - _ => panic!("Invalid syntax: signer cannot be specified."), - }; - quote! { - // Don't enforce on CPI, since usually a program is signing and so - // the `try_accounts` deserializatoin will fail *if* the one - // tries to manually invoke it. - // - // This check will be performed on the other end of the invocation. - if cfg!(not(feature = "cpi")) { - if !#info.is_signer { - return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature); - } - } - } -} - -pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenStream { - let tokens = &c.tokens; - quote! { - if !(#tokens) { - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes - } - } -} - -pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2::TokenStream { - let ident = &f.ident; - let owner_target = c.owner_target.clone(); - quote! { - if #ident.to_account_info().owner != #owner_target.to_account_info().key { - return Err(ProgramError::Custom(76)); // todo: proper error. - } - } -} - -pub fn generate_constraint_rent_exempt( - f: &Field, - c: &ConstraintRentExempt, -) -> proc_macro2::TokenStream { - let ident = &f.ident; - let info = match f.ty { - Ty::AccountInfo => quote! { #ident }, - Ty::ProgramAccount(_) => quote! { #ident.to_account_info() }, - Ty::Loader(_) => quote! { #ident.to_account_info() }, - _ => panic!("Invalid syntax: rent exemption cannot be specified."), - }; - match c { - ConstraintRentExempt::Skip => quote! {}, - ConstraintRentExempt::Enforce => quote! { - if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) { - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(2)); // todo: error codes - } - }, - } -} - -pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2::TokenStream { - let name = &f.ident; - let seeds = &c.seeds; - quote! { - let program_signer = Pubkey::create_program_address( - &#seeds, - program_id, - ).map_err(|_| anchor_lang::solana_program::program_error::ProgramError::Custom(1))?; // todo - if #name.to_account_info().key != &program_signer { - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo - } - } -} - -pub fn generate_constraint_executable( - f: &Field, - _c: &ConstraintExecutable, -) -> proc_macro2::TokenStream { - let name = &f.ident; - quote! { - if !#name.to_account_info().executable { - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(5)) // todo - } - } -} - -pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2::TokenStream { - let program_target = c.program_target.clone(); - let ident = &f.ident; - let account_ty = match &f.ty { - Ty::CpiState(ty) => &ty.account_ident, - _ => panic!("Invalid state constraint"), - }; - quote! { - // Checks the given state account is the canonical state account for - // the target program. - if #ident.to_account_info().key != &anchor_lang::CpiState::<#account_ty>::address(#program_target.to_account_info().key) { - return Err(ProgramError::Custom(1)); // todo: proper error. - } - if #ident.to_account_info().owner != #program_target.to_account_info().key { - return Err(ProgramError::Custom(1)); // todo: proper error. - } - } -} - -pub fn generate_constraint_associated( - f: &Field, - c: &ConstraintAssociated, -) -> proc_macro2::TokenStream { - if c.is_init { - generate_constraint_associated_init(f, c) - } else { - generate_constraint_associated_seeds(f, c) - } -} -pub fn generate_constraint_associated_init( - f: &Field, - c: &ConstraintAssociated, -) -> proc_macro2::TokenStream { - let associated_target = c.associated_target.clone(); - let field = &f.ident; - let (account_ty, is_zero_copy) = match &f.ty { - Ty::ProgramAccount(ty) => (&ty.account_ident, false), - Ty::Loader(ty) => (&ty.account_ident, true), - _ => panic!("Invalid associated constraint"), - }; - - let space = match &f.space { - // If no explicit space param was given, serialize the type to bytes - // and take the length (with +8 for the discriminator.) - None => match is_zero_copy { - false => { - quote! { - let space = 8 + #account_ty::default().try_to_vec().unwrap().len(); - } - } - true => { - quote! { - let space = 8 + anchor_lang::__private::bytemuck::bytes_of(&#account_ty::default()).len(); - } - } - }, - // Explicit account size given. Use it. - Some(s) => quote! { - let space = #s; - }, - }; - - let payer = match &f.payer { - None => quote! { - let payer = #associated_target.to_account_info(); - }, - Some(p) => quote! { - let payer = #p.to_account_info(); - }, - }; - - let associated_pubkey_and_nonce = generate_associated_pubkey(f, c); - - let seeds_with_nonce = match f.associated_seeds.len() { - 0 => quote! { - [ - &b"anchor"[..], - #associated_target.to_account_info().key.as_ref(), - &[nonce], - ] - }, - _ => { - let seeds = to_seeds_tts(&f.associated_seeds); - quote! { - [ - &b"anchor"[..], - #associated_target.to_account_info().key.as_ref(), - #seeds - &[nonce], - ] - } - } - }; - - let account_wrapper_ty = match is_zero_copy { - false => quote! { - anchor_lang::ProgramAccount - }, - true => quote! { - anchor_lang::Loader - }, - }; - let nonce_assignment = match is_zero_copy { - false => quote! {}, - // Zero copy is not deserialized, so the data must be lazy loaded. - true => quote! { - .load_init()? - }, - }; - - quote! { - let #field: #account_wrapper_ty<#account_ty> = { - #space - #payer - - #associated_pubkey_and_nonce - - if &__associated_field != #field.key { - return Err(ProgramError::Custom(45)); // todo: proper error. - } - let lamports = rent.minimum_balance(space); - let ix = anchor_lang::solana_program::system_instruction::create_account( - payer.key, - #field.key, - lamports, - space as u64, - program_id, - ); - - let seeds = #seeds_with_nonce; - let signer = &[&seeds[..]]; - anchor_lang::solana_program::program::invoke_signed( - &ix, - &[ - - #field.clone(), - payer.clone(), - system_program.clone(), - ], - signer, - ).map_err(|e| { - anchor_lang::solana_program::msg!("Unable to create associated account"); - e - })?; - // For now, we assume all accounts created with the `associated` - // attribute have a `nonce` field in their account. - let mut pa: #account_wrapper_ty<#account_ty> = #account_wrapper_ty::try_from_init( - &#field, - )?; - pa#nonce_assignment.__nonce = nonce; - pa - }; - } -} - -pub fn generate_constraint_associated_seeds( - f: &Field, - c: &ConstraintAssociated, -) -> proc_macro2::TokenStream { - let generated_associated_pubkey_and_nonce = generate_associated_pubkey(f, c); - let name = &f.ident; - quote! { - #generated_associated_pubkey_and_nonce - if #name.to_account_info().key != &__associated_field { - // TODO: proper error. - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(45)); - } - } -} -pub fn generate_associated_pubkey(f: &Field, c: &ConstraintAssociated) -> proc_macro2::TokenStream { - let associated_target = c.associated_target.clone(); - let seeds_no_nonce = match f.associated_seeds.len() { - 0 => quote! { - [ - &b"anchor"[..], - #associated_target.to_account_info().key.as_ref(), - ] - }, - _ => { - let seeds = to_seeds_tts(&f.associated_seeds); - quote! { - [ - &b"anchor"[..], - #associated_target.to_account_info().key.as_ref(), - #seeds - ] - } - } - }; - quote! { - let (__associated_field, nonce) = Pubkey::find_program_address( - &#seeds_no_nonce, - program_id, - ); - } -} - -// Returns the inner part of the seeds slice as a token stream. -fn to_seeds_tts(seeds: &[syn::Ident]) -> proc_macro2::TokenStream { - assert!(seeds.len() > 0); - let seed_0 = &seeds[0]; - let mut tts = quote! { - #seed_0.to_account_info().key.as_ref(), - }; - for seed in &seeds[1..] { - tts = quote! { - #tts - #seed.to_account_info().key.as_ref(), - }; - } - tts -} diff --git a/lang/syn/src/codegen/accounts/__client_accounts.rs b/lang/syn/src/codegen/accounts/__client_accounts.rs new file mode 100644 index 0000000000..0c5841bc5b --- /dev/null +++ b/lang/syn/src/codegen/accounts/__client_accounts.rs @@ -0,0 +1,129 @@ +use crate::{AccountField, AccountsStruct}; +use heck::SnakeCase; +use quote::quote; + +// Generates the private `__client_accounts` mod implementation, containing +// a generated struct mapping 1-1 to the `Accounts` struct, except with +// `Pubkey`s as the types. This is generated for Rust *clients*. +pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { + let name = &accs.ident; + let account_mod_name: proc_macro2::TokenStream = format!( + "__client_accounts_{}", + accs.ident.to_string().to_snake_case() + ) + .parse() + .unwrap(); + + let account_struct_fields: Vec = accs + .fields + .iter() + .map(|f: &AccountField| match f { + AccountField::CompositeField(s) => { + let name = &s.ident; + let symbol: proc_macro2::TokenStream = format!( + "__client_accounts_{0}::{1}", + s.symbol.to_snake_case(), + s.symbol, + ) + .parse() + .unwrap(); + quote! { + pub #name: #symbol + } + } + AccountField::Field(f) => { + let name = &f.ident; + quote! { + pub #name: anchor_lang::solana_program::pubkey::Pubkey + } + } + }) + .collect(); + + let account_struct_metas: Vec = accs + .fields + .iter() + .map(|f: &AccountField| match f { + AccountField::CompositeField(s) => { + let name = &s.ident; + quote! { + account_metas.extend(self.#name.to_account_metas(None)); + } + } + AccountField::Field(f) => { + let is_signer = match f.constraints.is_signer() { + false => quote! {false}, + true => quote! {true}, + }; + let meta = match f.constraints.is_mutable() { + false => quote! { anchor_lang::solana_program::instruction::AccountMeta::new_readonly }, + true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new }, + }; + let name = &f.ident; + quote! { + account_metas.push(#meta(self.#name, #is_signer)); + } + } + }) + .collect(); + // Re-export all composite account structs (i.e. other structs deriving + // accounts embedded into this struct. Required because, these embedded + // structs are *not* visible from the #[program] macro, which is responsible + // for generating the `accounts` mod, which aggregates all the the generated + // accounts used for structs. + let re_exports: Vec = { + // First, dedup the exports. + let mut re_exports = std::collections::HashSet::new(); + for f in accs.fields.iter().filter_map(|f: &AccountField| match f { + AccountField::CompositeField(s) => Some(s), + AccountField::Field(_) => None, + }) { + re_exports.insert(format!( + "__client_accounts_{0}::{1}", + f.symbol.to_snake_case(), + f.symbol, + )); + } + + re_exports + .iter() + .map(|symbol: &String| { + let symbol: proc_macro2::TokenStream = symbol.parse().unwrap(); + quote! { + pub use #symbol; + } + }) + .collect() + }; + quote! { + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + mod #account_mod_name { + use super::*; + use anchor_lang::prelude::borsh; + #(#re_exports)* + + #[derive(anchor_lang::AnchorSerialize)] + pub struct #name { + #(#account_struct_fields),* + } + + impl anchor_lang::ToAccountMetas for #name { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let mut account_metas = vec![]; + + #(#account_struct_metas)* + + account_metas + } + } + } + } +} diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs new file mode 100644 index 0000000000..99023f7d90 --- /dev/null +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -0,0 +1,471 @@ +use crate::{ + CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo, + ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut, + ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, + ConstraintState, Field, Ty, +}; +use quote::quote; + +pub fn generate(f: &Field) -> proc_macro2::TokenStream { + let checks: Vec = linearize(&f.constraints) + .iter() + .map(|c| generate_constraint(f, c)) + .collect(); + quote! { + #(#checks)* + } +} + +pub fn generate_composite(f: &CompositeField) -> proc_macro2::TokenStream { + let checks: Vec = linearize(&f.constraints) + .iter() + .filter_map(|c| match c { + Constraint::Raw(_) => Some(c), + Constraint::Literal(_) => Some(c), + _ => panic!("Invariant violation: composite constraints can only be raw or literals"), + }) + .map(|c| generate_constraint_composite(f, c)) + .collect(); + quote! { + #(#checks)* + } +} + +// Linearizes the constraint group so that constraints with dependencies +// run after those without. +// +// The associated cosntraint should always be first since it may also create +// an account with a PDA. +pub fn linearize(c_group: &ConstraintGroup) -> Vec { + let ConstraintGroup { + init, + mutable, + signer, + belongs_to, + literal, + raw, + owner, + rent_exempt, + seeds, + executable, + state, + associated, + } = c_group.clone(); + + let mut constraints = Vec::new(); + + if let Some(c) = associated { + constraints.push(Constraint::AssociatedGroup(c)); + } + if let Some(c) = init { + constraints.push(Constraint::Init(c)); + } + if let Some(c) = mutable { + constraints.push(Constraint::Mut(c)); + } + if let Some(c) = signer { + constraints.push(Constraint::Signer(c)); + } + constraints.append( + &mut belongs_to + .into_iter() + .map(|c| Constraint::BelongsTo(c)) + .collect(), + ); + constraints.append( + &mut literal + .into_iter() + .map(|c| Constraint::Literal(c)) + .collect(), + ); + constraints.append(&mut raw.into_iter().map(|c| Constraint::Raw(c)).collect()); + if let Some(c) = owner { + constraints.push(Constraint::Owner(c)); + } + if let Some(c) = rent_exempt { + constraints.push(Constraint::RentExempt(c)); + } + if let Some(c) = seeds { + constraints.push(Constraint::Seeds(c)); + } + if let Some(c) = executable { + constraints.push(Constraint::Executable(c)); + } + if let Some(c) = state { + constraints.push(Constraint::State(c)); + } + constraints +} + +fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream { + match c { + Constraint::Init(c) => generate_constraint_init(f, c), + Constraint::Mut(c) => generate_constraint_mut(f, c), + Constraint::BelongsTo(c) => generate_constraint_belongs_to(f, c), + Constraint::Signer(c) => generate_constraint_signer(f, c), + Constraint::Literal(c) => generate_constraint_literal(c), + Constraint::Raw(c) => generate_constraint_raw(c), + Constraint::Owner(c) => generate_constraint_owner(f, c), + Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c), + Constraint::Seeds(c) => generate_constraint_seeds(f, c), + Constraint::Executable(c) => generate_constraint_executable(f, c), + Constraint::State(c) => generate_constraint_state(f, c), + Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c), + } +} + +fn generate_constraint_composite(_f: &CompositeField, c: &Constraint) -> proc_macro2::TokenStream { + match c { + Constraint::Raw(c) => generate_constraint_raw(c), + Constraint::Literal(c) => generate_constraint_literal(c), + _ => panic!("Invariant violation"), + } +} + +pub fn generate_constraint_init(_f: &Field, _c: &ConstraintInit) -> proc_macro2::TokenStream { + quote! {} +} + +pub fn generate_constraint_mut(f: &Field, _c: &ConstraintMut) -> proc_macro2::TokenStream { + let ident = &f.ident; + quote! { + if !#ident.to_account_info().is_writable { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(36)); // todo: error codes + } + } +} + +pub fn generate_constraint_belongs_to( + f: &Field, + c: &ConstraintBelongsTo, +) -> proc_macro2::TokenStream { + let target = c.join_target.clone(); + let ident = &f.ident; + let field = match &f.ty { + Ty::Loader(_) => quote! {#ident.load()?}, + _ => quote! {#ident}, + }; + quote! { + if &#field.#target != #target.to_account_info().key { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes + } + } +} + +pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macro2::TokenStream { + let ident = &f.ident; + let info = match f.ty { + Ty::AccountInfo => quote! { #ident }, + Ty::ProgramAccount(_) => quote! { #ident.to_account_info() }, + _ => panic!("Invalid syntax: signer cannot be specified."), + }; + quote! { + // Don't enforce on CPI, since usually a program is signing and so + // the `try_accounts` deserializatoin will fail *if* the one + // tries to manually invoke it. + // + // This check will be performed on the other end of the invocation. + if cfg!(not(feature = "cpi")) { + if !#info.to_account_info().is_signer { + return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature); + } + } + } +} + +pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenStream { + let lit: proc_macro2::TokenStream = { + let lit = &c.lit; + let lit_ts: proc_macro2::TokenStream = quote! {#lit}; + lit_ts.to_string().replace("\"", "").parse().unwrap() + }; + quote! { + if !(#lit) { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes + } + } +} + +pub fn generate_constraint_raw(c: &ConstraintRaw) -> proc_macro2::TokenStream { + let raw = &c.raw; + quote! { + if !(#raw) { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(14)); // todo: error codes + } + } +} + +pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2::TokenStream { + let ident = &f.ident; + let owner_target = c.owner_target.clone(); + quote! { + if #ident.to_account_info().owner != #owner_target.to_account_info().key { + return Err(ProgramError::Custom(76)); // todo: proper error. + } + } +} + +pub fn generate_constraint_rent_exempt( + f: &Field, + c: &ConstraintRentExempt, +) -> proc_macro2::TokenStream { + let ident = &f.ident; + let info = match f.ty { + Ty::AccountInfo => quote! { #ident }, + Ty::ProgramAccount(_) => quote! { #ident.to_account_info() }, + Ty::Loader(_) => quote! { #ident.to_account_info() }, + _ => panic!("Invalid syntax: rent exemption cannot be specified."), + }; + match c { + ConstraintRentExempt::Skip => quote! {}, + ConstraintRentExempt::Enforce => quote! { + if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(2)); // todo: error codes + } + }, + } +} + +pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2::TokenStream { + let name = &f.ident; + let seeds = &c.seeds; + quote! { + let program_signer = Pubkey::create_program_address( + &[#seeds], + program_id, + ).map_err(|_| anchor_lang::solana_program::program_error::ProgramError::Custom(1))?; // todo + if #name.to_account_info().key != &program_signer { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo + } + } +} + +pub fn generate_constraint_executable( + f: &Field, + _c: &ConstraintExecutable, +) -> proc_macro2::TokenStream { + let name = &f.ident; + quote! { + if !#name.to_account_info().executable { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(5)) // todo + } + } +} + +pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2::TokenStream { + let program_target = c.program_target.clone(); + let ident = &f.ident; + let account_ty = match &f.ty { + Ty::CpiState(ty) => &ty.account_ident, + _ => panic!("Invalid state constraint"), + }; + quote! { + // Checks the given state account is the canonical state account for + // the target program. + if #ident.to_account_info().key != &anchor_lang::CpiState::<#account_ty>::address(#program_target.to_account_info().key) { + return Err(ProgramError::Custom(1)); // todo: proper error. + } + if #ident.to_account_info().owner != #program_target.to_account_info().key { + return Err(ProgramError::Custom(1)); // todo: proper error. + } + } +} + +pub fn generate_constraint_associated( + f: &Field, + c: &ConstraintAssociatedGroup, +) -> proc_macro2::TokenStream { + if c.is_init { + generate_constraint_associated_init(f, c) + } else { + generate_constraint_associated_seeds(f, c) + } +} + +pub fn generate_constraint_associated_init( + f: &Field, + c: &ConstraintAssociatedGroup, +) -> proc_macro2::TokenStream { + let associated_target = c.associated_target.clone(); + let field = &f.ident; + let (account_ty, is_zero_copy) = match &f.ty { + Ty::ProgramAccount(ty) => (&ty.account_ident, false), + Ty::Loader(ty) => (&ty.account_ident, true), + _ => panic!("Invalid associated constraint"), + }; + + let space = match &c.space { + // If no explicit space param was given, serialize the type to bytes + // and take the length (with +8 for the discriminator.) + None => match is_zero_copy { + false => { + quote! { + let space = 8 + #account_ty::default().try_to_vec().unwrap().len(); + } + } + true => { + quote! { + let space = 8 + anchor_lang::__private::bytemuck::bytes_of(&#account_ty::default()).len(); + } + } + }, + // Explicit account size given. Use it. + Some(s) => quote! { + let space = #s; + }, + }; + + let payer = match &c.payer { + None => quote! { + let payer = #associated_target.to_account_info(); + }, + Some(p) => quote! { + let payer = #p.to_account_info(); + }, + }; + + let associated_pubkey_and_nonce = generate_associated_pubkey(f, c); + + let seeds_with_nonce = match c.associated_seeds.len() { + 0 => quote! { + [ + &b"anchor"[..], + #associated_target.to_account_info().key.as_ref(), + &[nonce], + ] + }, + _ => { + let seeds = to_seeds_tts(&c.associated_seeds); + quote! { + [ + &b"anchor"[..], + #associated_target.to_account_info().key.as_ref(), + #seeds + &[nonce], + ] + } + } + }; + + let account_wrapper_ty = match is_zero_copy { + false => quote! { + anchor_lang::ProgramAccount + }, + true => quote! { + anchor_lang::Loader + }, + }; + let nonce_assignment = match is_zero_copy { + false => quote! {}, + // Zero copy is not deserialized, so the data must be lazy loaded. + true => quote! { + .load_init()? + }, + }; + + quote! { + let #field: #account_wrapper_ty<#account_ty> = { + #space + #payer + + #associated_pubkey_and_nonce + + if &__associated_field != #field.key { + return Err(ProgramError::Custom(45)); // todo: proper error. + } + let lamports = rent.minimum_balance(space); + let ix = anchor_lang::solana_program::system_instruction::create_account( + payer.key, + #field.key, + lamports, + space as u64, + program_id, + ); + + let seeds = #seeds_with_nonce; + let signer = &[&seeds[..]]; + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[ + + #field.clone(), + payer.clone(), + system_program.clone(), + ], + signer, + ).map_err(|e| { + anchor_lang::solana_program::msg!("Unable to create associated account"); + e + })?; + // For now, we assume all accounts created with the `associated` + // attribute have a `nonce` field in their account. + let mut pa: #account_wrapper_ty<#account_ty> = #account_wrapper_ty::try_from_init( + &#field, + )?; + pa#nonce_assignment.__nonce = nonce; + pa + }; + } +} + +pub fn generate_constraint_associated_seeds( + f: &Field, + c: &ConstraintAssociatedGroup, +) -> proc_macro2::TokenStream { + let generated_associated_pubkey_and_nonce = generate_associated_pubkey(f, c); + let name = &f.ident; + quote! { + #generated_associated_pubkey_and_nonce + if #name.to_account_info().key != &__associated_field { + // TODO: proper error. + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(45)); + } + } +} + +pub fn generate_associated_pubkey( + _f: &Field, + c: &ConstraintAssociatedGroup, +) -> proc_macro2::TokenStream { + let associated_target = c.associated_target.clone(); + let seeds_no_nonce = match c.associated_seeds.len() { + 0 => quote! { + [ + &b"anchor"[..], + #associated_target.to_account_info().key.as_ref(), + ] + }, + _ => { + let seeds = to_seeds_tts(&c.associated_seeds); + quote! { + [ + &b"anchor"[..], + #associated_target.to_account_info().key.as_ref(), + #seeds + ] + } + } + }; + quote! { + let (__associated_field, nonce) = Pubkey::find_program_address( + &#seeds_no_nonce, + program_id, + ); + } +} + +// Returns the inner part of the seeds slice as a token stream. +fn to_seeds_tts(seeds: &[syn::Ident]) -> proc_macro2::TokenStream { + assert!(seeds.len() > 0); + let seed_0 = &seeds[0]; + let mut tts = quote! { + #seed_0.to_account_info().key.as_ref(), + }; + for seed in &seeds[1..] { + tts = quote! { + #tts + #seed.to_account_info().key.as_ref(), + }; + } + tts +} diff --git a/lang/syn/src/codegen/accounts/exit.rs b/lang/syn/src/codegen/accounts/exit.rs new file mode 100644 index 0000000000..d6b25ec6a9 --- /dev/null +++ b/lang/syn/src/codegen/accounts/exit.rs @@ -0,0 +1,39 @@ +use crate::codegen::accounts::generics; +use crate::{AccountField, AccountsStruct}; +use quote::quote; + +// Generates the `Exit` trait implementation. +pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { + let name = &accs.ident; + let (combined_generics, trait_generics, strct_generics) = generics(accs); + + let on_save: Vec = accs + .fields + .iter() + .map(|af: &AccountField| match af { + AccountField::CompositeField(s) => { + let name = &s.ident; + quote! { + anchor_lang::AccountsExit::exit(&self.#name, program_id)?; + } + } + AccountField::Field(f) => { + let ident = &f.ident; + match f.constraints.is_mutable() { + false => quote! {}, + true => quote! { + anchor_lang::AccountsExit::exit(&self.#ident, program_id)?; + }, + } + } + }) + .collect(); + quote! { + impl#combined_generics anchor_lang::AccountsExit#trait_generics for #name#strct_generics { + fn exit(&self, program_id: &anchor_lang::solana_program::pubkey::Pubkey) -> anchor_lang::solana_program::entrypoint::ProgramResult { + #(#on_save)* + Ok(()) + } + } + } +} diff --git a/lang/syn/src/codegen/accounts/mod.rs b/lang/syn/src/codegen/accounts/mod.rs new file mode 100644 index 0000000000..05ff4204e8 --- /dev/null +++ b/lang/syn/src/codegen/accounts/mod.rs @@ -0,0 +1,43 @@ +use crate::AccountsStruct; +use quote::quote; + +mod __client_accounts; +mod constraints; +mod exit; +mod to_account_infos; +mod to_account_metas; +mod try_accounts; + +pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { + let impl_try_accounts = try_accounts::generate(accs); + let impl_to_account_infos = to_account_infos::generate(accs); + let impl_to_account_metas = to_account_metas::generate(accs); + let impl_exit = exit::generate(accs); + + let __client_accounts_mod = __client_accounts::generate(accs); + + quote! { + #impl_try_accounts + #impl_to_account_infos + #impl_to_account_metas + #impl_exit + + #__client_accounts_mod + } +} + +fn generics( + accs: &AccountsStruct, +) -> ( + proc_macro2::TokenStream, + proc_macro2::TokenStream, + proc_macro2::TokenStream, +) { + match accs.generics.lt_token { + None => (quote! {<'info>}, quote! {<'info>}, quote! {}), + Some(_) => { + let g = &accs.generics; + (quote! {#g}, quote! {#g}, quote! {#g}) + } + } +} diff --git a/lang/syn/src/codegen/accounts/to_account_infos.rs b/lang/syn/src/codegen/accounts/to_account_infos.rs new file mode 100644 index 0000000000..8a689d72b5 --- /dev/null +++ b/lang/syn/src/codegen/accounts/to_account_infos.rs @@ -0,0 +1,34 @@ +use crate::codegen::accounts::generics; +use crate::{AccountField, AccountsStruct}; +use quote::quote; + +// Generates the `ToAccountInfos` trait implementation. +pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { + let name = &accs.ident; + let (combined_generics, trait_generics, strct_generics) = generics(accs); + + let to_acc_infos: Vec = accs + .fields + .iter() + .map(|f: &AccountField| { + let name = match f { + AccountField::CompositeField(s) => &s.ident, + AccountField::Field(f) => &f.ident, + }; + quote! { + account_infos.extend(self.#name.to_account_infos()); + } + }) + .collect(); + quote! { + impl#combined_generics anchor_lang::ToAccountInfos#trait_generics for #name#strct_generics { + fn to_account_infos(&self) -> Vec> { + let mut account_infos = vec![]; + + #(#to_acc_infos)* + + account_infos + } + } + } +} diff --git a/lang/syn/src/codegen/accounts/to_account_metas.rs b/lang/syn/src/codegen/accounts/to_account_metas.rs new file mode 100644 index 0000000000..7a72c38ace --- /dev/null +++ b/lang/syn/src/codegen/accounts/to_account_metas.rs @@ -0,0 +1,40 @@ +use crate::codegen::accounts::generics; +use crate::{AccountField, AccountsStruct}; +use quote::quote; + +// Generates the `ToAccountMetas` trait implementation. +pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { + let name = &accs.ident; + let (combined_generics, _trait_generics, strct_generics) = generics(accs); + + let to_acc_metas: Vec = accs + .fields + .iter() + .map(|f: &AccountField| { + let (name, is_signer) = match f { + AccountField::CompositeField(s) => (&s.ident, quote! {None}), + AccountField::Field(f) => { + let is_signer = match f.constraints.is_signer() { + false => quote! {None}, + true => quote! {Some(true)}, + }; + (&f.ident, is_signer) + } + }; + quote! { + account_metas.extend(self.#name.to_account_metas(#is_signer)); + } + }) + .collect(); + quote! { + impl#combined_generics anchor_lang::ToAccountMetas for #name#strct_generics { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let mut account_metas = vec![]; + + #(#to_acc_metas)* + + account_metas + } + } + } +} diff --git a/lang/syn/src/codegen/accounts/try_accounts.rs b/lang/syn/src/codegen/accounts/try_accounts.rs new file mode 100644 index 0000000000..abea425392 --- /dev/null +++ b/lang/syn/src/codegen/accounts/try_accounts.rs @@ -0,0 +1,198 @@ +use crate::codegen::accounts::{constraints, generics}; +use crate::{AccountField, AccountsStruct, Field, SysvarTy, Ty}; +use proc_macro2::TokenStream; +use quote::quote; + +// Generates the `Accounts` trait implementation. +pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { + let name = &accs.ident; + let (combined_generics, trait_generics, strct_generics) = generics(accs); + + // All fields without an `#[account(associated)]` attribute. + let non_associated_fields: Vec<&AccountField> = accs + .fields + .iter() + .filter(|af| !is_associated_init(af)) + .collect(); + + // Deserialization for each field + let deser_fields: Vec = accs + .fields + .iter() + .map(|af: &AccountField| { + match af { + AccountField::CompositeField(s) => { + let name = &s.ident; + let ty = &s.raw_field.ty; + quote! { + #[cfg(feature = "anchor-debug")] + ::solana_program::log::sol_log(stringify!(#name)); + let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?; + } + } + AccountField::Field(f) => { + // Associated fields are *first* deserialized into + // AccountInfos, and then later deserialized into + // ProgramAccounts in the "constraint check" phase. + if is_associated_init(af) { + let name = &f.ident; + quote!{ + let #name = &accounts[0]; + *accounts = &accounts[1..]; + } + } else { + let name = typed_ident(&f); + match f.constraints.is_init() { + false => quote! { + #[cfg(feature = "anchor-debug")] + ::solana_program::log::sol_log(stringify!(#name)); + let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?; + }, + true => quote! { + #[cfg(feature = "anchor-debug")] + ::solana_program::log::sol_log(stringify!(#name)); + let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?; + }, + } + } + } + } + }) + .collect(); + + // Deserialization for each *associated* field. This must be after + // the deser_fields. + let deser_associated_fields: Vec = accs + .fields + .iter() + .filter_map(|af| match af { + AccountField::CompositeField(_s) => None, + AccountField::Field(f) => match is_associated_init(af) { + false => None, + true => Some(f), + }, + }) + .map(|field: &Field| constraints::generate(field)) + .collect(); + + // Constraint checks for each account fields. + let access_checks: Vec = non_associated_fields + .iter() + .map(|af: &&AccountField| match af { + AccountField::Field(f) => constraints::generate(f), + AccountField::CompositeField(s) => constraints::generate_composite(s), + }) + .collect(); + + // Each field in the final deserialized accounts struct. + let return_tys: Vec = accs + .fields + .iter() + .map(|f: &AccountField| { + let name = match f { + AccountField::CompositeField(s) => &s.ident, + AccountField::Field(f) => &f.ident, + }; + quote! { + #name + } + }) + .collect(); + quote! { + impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics { + #[inline(never)] + fn try_accounts( + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>], + ) -> std::result::Result { + // Deserialize each account. + #(#deser_fields)* + // Deserialize each associated account. + // + // Associated accounts are treated specially, because the fields + // do deserialization + constraint checks in a single go, + // whereas all other fields, i.e. the `deser_fields`, first + // deserialize, and then do constraint checks. + #(#deser_associated_fields)* + // Perform constraint checks on each account. + #(#access_checks)* + // Success. Return the validated accounts. + Ok(#name { + #(#return_tys),* + }) + } + } + } +} + +// Returns true if the given AccountField has an associated init constraint. +fn is_associated_init(af: &AccountField) -> bool { + match af { + AccountField::CompositeField(_s) => false, + AccountField::Field(f) => f + .constraints + .associated + .as_ref() + .map(|f| f.is_init) + .unwrap_or(false), + } +} + +fn typed_ident(field: &Field) -> TokenStream { + let name = &field.ident; + + let ty = match &field.ty { + Ty::AccountInfo => quote! { AccountInfo }, + Ty::ProgramState(ty) => { + let account = &ty.account_ident; + quote! { + ProgramState<#account> + } + } + Ty::CpiState(ty) => { + let account = &ty.account_ident; + quote! { + CpiState<#account> + } + } + Ty::ProgramAccount(ty) => { + let account = &ty.account_ident; + quote! { + ProgramAccount<#account> + } + } + Ty::Loader(ty) => { + let account = &ty.account_ident; + quote! { + Loader<#account> + } + } + Ty::CpiAccount(ty) => { + let account = &ty.account_ident; + quote! { + CpiAccount<#account> + } + } + Ty::Sysvar(ty) => { + let account = match ty { + SysvarTy::Clock => quote! {Clock}, + SysvarTy::Rent => quote! {Rent}, + SysvarTy::EpochSchedule => quote! {EpochSchedule}, + SysvarTy::Fees => quote! {Fees}, + SysvarTy::RecentBlockhashes => quote! {RecentBlockhashes}, + SysvarTy::SlotHashes => quote! {SlotHashes}, + SysvarTy::SlotHistory => quote! {SlotHistory}, + SysvarTy::StakeHistory => quote! {StakeHistory}, + SysvarTy::Instructions => quote! {Instructions}, + SysvarTy::Rewards => quote! {Rewards}, + }; + quote! { + Sysvar<#account> + } + } + }; + + quote! { + #name: #ty + } +} diff --git a/lang/syn/src/codegen/program.rs b/lang/syn/src/codegen/program.rs deleted file mode 100644 index 9e8283ec73..0000000000 --- a/lang/syn/src/codegen/program.rs +++ /dev/null @@ -1,1379 +0,0 @@ -use crate::parser; -use crate::{IxArg, Program, State, StateIx}; -use heck::{CamelCase, SnakeCase}; -use quote::quote; - -// Namespace for calculating state instruction sighash signatures. -const SIGHASH_STATE_NAMESPACE: &str = "state"; - -// Namespace for calculating instruction sighash signatures for any instruction -// not affecting program state. -const SIGHASH_GLOBAL_NAMESPACE: &str = "global"; - -pub fn generate(program: Program) -> proc_macro2::TokenStream { - let mod_name = &program.name; - let dispatch = generate_dispatch(&program); - let handlers_non_inlined = generate_non_inlined_handlers(&program); - let methods = generate_methods(&program); - let ixs = generate_ixs(&program); - let cpi = generate_cpi(&program); - let accounts = generate_accounts(&program); - - quote! { - // TODO: remove once we allow segmented paths in `Accounts` structs. - use #mod_name::*; - - #[cfg(not(feature = "no-entrypoint"))] - anchor_lang::solana_program::entrypoint!(entry); - /// The Anchor codegen exposes a programming model where a user defines - /// a set of methods inside of a `#[program]` module in a way similar - /// to writing RPC request handlers. The macro then generates a bunch of - /// code wrapping these user defined methods into something that can be - /// executed on Solana. - /// - /// These methods fall into one of three categories, each of which - /// can be considered a different "namespace" of the program. - /// - /// 1) Global methods - regular methods inside of the `#[program]`. - /// 2) State methods - associated methods inside a `#[state]` struct. - /// 3) Interface methods - methods inside a strait struct's - /// implementation of an `#[interface]` trait. - /// - /// Care must be taken by the codegen to prevent collisions between - /// methods in these different namespaces. For this reason, Anchor uses - /// a variant of sighash to perform method dispatch, rather than - /// something like a simple enum variant discriminator. - /// - /// The execution flow of the generated code can be roughly outlined: - /// - /// * Start program via the entrypoint. - /// * Strip method identifier off the first 8 bytes of the instruction - /// data and invoke the identified method. The method identifier - /// is a variant of sighash. See docs.rs for `anchor_lang` for details. - /// * If the method identifier is an IDL identifier, execute the IDL - /// instructions, which are a special set of hardcoded instructions - /// baked into every Anchor program. Then exit. - /// * Otherwise, the method identifier is for a user defined - /// instruction, i.e., one of the methods in the user defined - /// `#[program]` module. Perform method dispatch, i.e., execute the - /// big match statement mapping method identifier to method handler - /// wrapper. - /// * Run the method handler wrapper. This wraps the code the user - /// actually wrote, deserializing the accounts, constructing the - /// context, invoking the user's code, and finally running the exit - /// routine, which typically persists account changes. - /// - /// The `entry` function here, defines the standard entry to a Solana - /// program, where execution begins. - #[cfg(not(feature = "no-entrypoint"))] - fn entry(program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8]) -> ProgramResult { - #[cfg(feature = "anchor-debug")] - { - msg!("anchor-debug is active"); - } - if ix_data.len() < 8 { - return Err(ProgramError::Custom(99)); - } - - // Split the instruction data into the first 8 byte method - // identifier (sighash) and the serialized instruction data. - let mut ix_data: &[u8] = ix_data; - let sighash: [u8; 8] = { - let mut sighash: [u8; 8] = [0; 8]; - sighash.copy_from_slice(&ix_data[..8]); - ix_data = &ix_data[8..]; - sighash - }; - - dispatch(program_id, accounts, sighash, ix_data) - .map_err(|e| { - anchor_lang::solana_program::msg!(&e.to_string()); - e - }) - } - - #dispatch - - /// Create a private module to not clutter the program's namespace. - /// Defines an entrypoint for each individual instruction handler - /// wrapper. - mod __private { - use super::*; - - #handlers_non_inlined - } - - #accounts - - #ixs - - #methods - - #cpi - } -} - -pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { - // Dispatch the state constructor. - let ctor_state_dispatch_arm = match &program.state { - None => quote! { /* no-op */ }, - Some(state) => match state.ctor_and_anchor.is_some() { - false => quote! {}, - true => { - let variant_arm = generate_ctor_variant(state); - let ctor_args = generate_ctor_args(state); - let ix_name: proc_macro2::TokenStream = - generate_ctor_variant_name().parse().unwrap(); - let sighash_arr = sighash_ctor(); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - quote! { - #sighash_tts => { - let ix = instruction::state::#ix_name::deserialize(&mut ix_data) - .map_err(|_| ProgramError::Custom(1))?; // todo: error code - let instruction::state::#variant_arm = ix; - __private::__state::__ctor(program_id, accounts, #(#ctor_args),*) - } - } - } - }, - }; - - // Dispatch the state impl instructions. - let state_dispatch_arms: Vec = match &program.state { - None => vec![], - Some(s) => s - .impl_block_and_methods - .as_ref() - .map(|(_impl_block, methods)| { - methods - .iter() - .map(|ix: &crate::StateIx| { - let ix_arg_names: Vec<&syn::Ident> = - ix.args.iter().map(|arg| &arg.name).collect(); - let name = &ix.raw_method.sig.ident.to_string(); - let ix_method_name: proc_macro2::TokenStream = - { format!("__{}", name).parse().unwrap() }; - let variant_arm = - generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); - let ix_name = generate_ix_variant_name(ix.raw_method.sig.ident.to_string()); - let sighash_arr = sighash(SIGHASH_STATE_NAMESPACE, &name); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - quote! { - #sighash_tts => { - let ix = instruction::state::#ix_name::deserialize(&mut ix_data) - .map_err(|_| ProgramError::Custom(1))?; // todo: error code - let instruction::state::#variant_arm = ix; - __private::__state::#ix_method_name(program_id, accounts, #(#ix_arg_names),*) - } - } - }) - .collect() - }) - .unwrap_or_default(), - }; - - // Dispatch all trait interface implementations. - let trait_dispatch_arms: Vec = match &program.state { - None => vec![], - Some(s) => s - .interfaces - .as_ref() - .map(|interfaces| { - interfaces - .iter() - .flat_map(|iface: &crate::StateInterface| { - iface - .methods - .iter() - .map(|m: &crate::StateIx| { - let ix_arg_names: Vec<&syn::Ident> = - m.args.iter().map(|arg| &arg.name).collect(); - let name = &m.raw_method.sig.ident.to_string(); - let ix_name: proc_macro2::TokenStream = format!("__{}_{}", iface.trait_name, name).parse().unwrap(); - let raw_args: Vec<&syn::PatType> = m - .args - .iter() - .map(|arg: &crate::IxArg| &arg.raw_arg) - .collect(); - let sighash_arr = sighash(&iface.trait_name, &m.ident.to_string()); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - let args_struct = { - if m.args.is_empty() { - quote! { - #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] - struct Args; - } - } else { - quote! { - #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] - struct Args { - #(#raw_args),* - } - } - } - }; - quote! { - #sighash_tts => { - #args_struct - let ix = Args::deserialize(&mut ix_data) - .map_err(|_| ProgramError::Custom(1))?; // todo: error code - let Args { - #(#ix_arg_names),* - } = ix; - __private::__interface::#ix_name(program_id, accounts, #(#ix_arg_names),*) - } - } - }) - .collect::>() - }) - .collect() - }) - .unwrap_or_default() - }; - - // Dispatch all global instructions. - let global_dispatch_arms: Vec = program - .ixs - .iter() - .map(|ix| { - let ix_arg_names: Vec<&syn::Ident> = ix.args.iter().map(|arg| &arg.name).collect(); - let ix_method_name = &ix.raw_method.sig.ident; - let ix_name = generate_ix_variant_name(ix.raw_method.sig.ident.to_string()); - let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &ix_method_name.to_string()); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - let variant_arm = generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); - quote! { - #sighash_tts => { - let ix = instruction::#ix_name::deserialize(&mut ix_data) - .map_err(|_| ProgramError::Custom(1))?; // todo: error code - let instruction::#variant_arm = ix; - __private::__global::#ix_method_name(program_id, accounts, #(#ix_arg_names),*) - } - } - }) - .collect(); - - quote! { - /// Performs method dispatch. - /// - /// Each method in an anchor program is uniquely defined by a namespace - /// and a rust identifier (i.e., the name given to the method). These - /// two pieces can be combined to creater a method identifier, - /// specifically, Anchor uses - /// - /// Sha256("::")[..8], - /// - /// where the namespace can be one of three types. 1) "global" for a - /// regular instruction, 2) "state" for a state struct instruction - /// handler and 3) a trait namespace (used in combination with the - /// `#[interface]` attribute), which is defined by the trait name, e.. - /// `MyTrait`. - /// - /// With this 8 byte identifier, Anchor performs method dispatch, - /// matching the given 8 byte identifier to the associated method - /// handler, which leads to user defined code being eventually invoked. - fn dispatch(program_id: &Pubkey, accounts: &[AccountInfo], sighash: [u8; 8], mut ix_data: &[u8]) -> ProgramResult { - // If the method identifier is the IDL tag, then execute an IDL - // instruction, injected into all Anchor programs. - if cfg!(not(feature = "no-idl")) { - if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() { - return __private::__idl::__idl_dispatch(program_id, accounts, &ix_data); - } - } - - match sighash { - #ctor_state_dispatch_arm - #(#state_dispatch_arms)* - #(#trait_dispatch_arms)* - #(#global_dispatch_arms)* - _ => { - msg!("Fallback functions are not supported. If you have a use case, please file an issue."); - Err(ProgramError::Custom(99)) - } - } - } - } -} - -// Generate non-inlined wrappers for each instruction handler, since Solana's -// BPF max stack size can't handle reasonable sized dispatch trees without doing -// so. -pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStream { - let program_name = &program.name; - let non_inlined_idl: proc_macro2::TokenStream = { - quote! { - // Entry for all IDL related instructions. Use the "no-idl" feature - // to eliminate this code, for example, if one wants to make the - // IDL no longer mutable or if one doesn't want to store the IDL - // on chain. - #[inline(never)] - #[cfg(not(feature = "no-idl"))] - pub fn __idl_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult { - let mut accounts = accounts; - let mut data: &[u8] = idl_ix_data; - - let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data) - .map_err(|_| ProgramError::Custom(2))?; // todo - - match ix { - anchor_lang::idl::IdlInstruction::Create { data_len } => { - let mut accounts = anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts)?; - __idl_create_account(program_id, &mut accounts, data_len)?; - accounts.exit(program_id)?; - }, - anchor_lang::idl::IdlInstruction::CreateBuffer => { - let mut accounts = anchor_lang::idl::IdlCreateBuffer::try_accounts(program_id, &mut accounts)?; - __idl_create_buffer(program_id, &mut accounts)?; - accounts.exit(program_id)?; - }, - anchor_lang::idl::IdlInstruction::Write { data } => { - let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; - __idl_write(program_id, &mut accounts, data)?; - accounts.exit(program_id)?; - }, - anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => { - let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; - __idl_set_authority(program_id, &mut accounts, new_authority)?; - accounts.exit(program_id)?; - }, - anchor_lang::idl::IdlInstruction::SetBuffer => { - let mut accounts = anchor_lang::idl::IdlSetBuffer::try_accounts(program_id, &mut accounts)?; - __idl_set_buffer(program_id, &mut accounts)?; - accounts.exit(program_id)?; - }, - } - Ok(()) - } - - #[inline(never)] - #[cfg(feature = "no-idl")] - pub fn __idl_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult { - Err(anchor_lang::solana_program::program_error::ProgramError::Custom(99)) - } - - // One time IDL account initializer. Will faill on subsequent - // invocations. - #[inline(never)] - pub fn __idl_create_account( - program_id: &Pubkey, - accounts: &mut anchor_lang::idl::IdlCreateAccounts, - data_len: u64, - ) -> ProgramResult { - if program_id != accounts.program.key { - return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(98)); // todo proper error - } - // Create the IDL's account. - let from = accounts.from.key; - let (base, nonce) = Pubkey::find_program_address(&[], program_id); - let seed = anchor_lang::idl::IdlAccount::seed(); - let owner = accounts.program.key; - let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); - // Space: account discriminator || authority pubkey || vec len || vec data - let space = 8 + 32 + 4 + data_len as usize; - let lamports = accounts.rent.minimum_balance(space); - let seeds = &[&[nonce][..]]; - let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( - from, - &to, - &base, - seed, - lamports, - space as u64, - owner, - ); - anchor_lang::solana_program::program::invoke_signed( - &ix, - &[ - accounts.from.clone(), - accounts.to.clone(), - accounts.base.clone(), - accounts.system_program.clone(), - ], - &[seeds], - )?; - - // Deserialize the newly created account. - let mut idl_account = { - let mut account_data = accounts.to.try_borrow_data()?; - let mut account_data_slice: &[u8] = &account_data; - anchor_lang::idl::IdlAccount::try_deserialize_unchecked( - &mut account_data_slice, - )? - }; - - // Set the authority. - idl_account.authority = *accounts.from.key; - - // Store the new account data. - let mut data = accounts.to.try_borrow_mut_data()?; - let dst: &mut [u8] = &mut data; - let mut cursor = std::io::Cursor::new(dst); - idl_account.try_serialize(&mut cursor)?; - - Ok(()) - } - - #[inline(never)] - pub fn __idl_create_buffer( - program_id: &Pubkey, - accounts: &mut anchor_lang::idl::IdlCreateBuffer, - ) -> ProgramResult { - let mut buffer = &mut accounts.buffer; - buffer.authority = *accounts.authority.key; - Ok(()) - } - - #[inline(never)] - pub fn __idl_write( - program_id: &Pubkey, - accounts: &mut anchor_lang::idl::IdlAccounts, - idl_data: Vec, - ) -> ProgramResult { - let mut idl = &mut accounts.idl; - idl.data.extend(idl_data); - Ok(()) - } - - #[inline(never)] - pub fn __idl_set_authority( - program_id: &Pubkey, - accounts: &mut anchor_lang::idl::IdlAccounts, - new_authority: Pubkey, - ) -> ProgramResult { - accounts.idl.authority = new_authority; - Ok(()) - } - - #[inline(never)] - pub fn __idl_set_buffer( - program_id: &Pubkey, - accounts: &mut anchor_lang::idl::IdlSetBuffer, - ) -> ProgramResult { - accounts.idl.data = accounts.buffer.data.clone(); - Ok(()) - } - } - }; - // Constructor handler. - let non_inlined_ctor: proc_macro2::TokenStream = match &program.state { - None => quote! {}, - Some(state) => match state.ctor_and_anchor.as_ref() { - None => quote! {}, - Some((_ctor, anchor_ident)) => { - let ctor_typed_args = generate_ctor_typed_args(state); - let ctor_untyped_args = generate_ctor_args(state); - let name = &state.strct.ident; - let mod_name = &program.name; - if state.is_zero_copy { - quote! { - // One time state account initializer. Will faill on subsequent - // invocations. - #[inline(never)] - pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult { - let mut remaining_accounts: &[AccountInfo] = accounts; - - // Deserialize accounts. - let ctor_accounts = anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts)?; - let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts)?; - - // Create the solana account for the ctor data. - let from = ctor_accounts.from.key; - let (base, nonce) = Pubkey::find_program_address(&[], ctor_accounts.program.key); - let seed = anchor_lang::__private::PROGRAM_STATE_SEED; - let owner = ctor_accounts.program.key; - let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); - let space = 8 + std::mem::size_of::<#name>(); - let lamports = ctor_accounts.rent.minimum_balance(std::convert::TryInto::try_into(space).unwrap()); - let seeds = &[&[nonce][..]]; - let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( - from, - &to, - &base, - seed, - lamports, - space as u64, - owner, - ); - anchor_lang::solana_program::program::invoke_signed( - &ix, - &[ - ctor_accounts.from.clone(), - ctor_accounts.to.clone(), - ctor_accounts.base.clone(), - ctor_accounts.system_program.clone(), - ], - &[seeds], - )?; - - // Zero copy deserialize. - let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from_init(&ctor_accounts.to)?; - - // Invoke the ctor in a new lexical scope so that - // the zero-copy RefMut gets dropped. Required - // so that we can subsequently run the exit routine. - { - let mut instance = loader.load_init()?; - instance.new( - anchor_lang::Context::new( - program_id, - &mut ctor_user_def_accounts, - remaining_accounts, - ), - #(#ctor_untyped_args),* - )?; - } - - // Exit routines. - ctor_user_def_accounts.exit(program_id)?; - loader.exit(program_id)?; - - Ok(()) - } - } - } else { - quote! { - // One time state account initializer. Will faill on subsequent - // invocations. - #[inline(never)] - pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult { - let mut remaining_accounts: &[AccountInfo] = accounts; - - // Deserialize accounts. - let ctor_accounts = anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts)?; - let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts)?; - - // Invoke the ctor. - let instance = #mod_name::#name::new( - anchor_lang::Context::new( - program_id, - &mut ctor_user_def_accounts, - remaining_accounts, - ), - #(#ctor_untyped_args),* - )?; - - // Create the solana account for the ctor data. - let from = ctor_accounts.from.key; - let (base, nonce) = Pubkey::find_program_address(&[], ctor_accounts.program.key); - let seed = anchor_lang::ProgramState::<#name>::seed(); - let owner = ctor_accounts.program.key; - let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); - let space = anchor_lang::__private::AccountSize::size(&instance)?; - let lamports = ctor_accounts.rent.minimum_balance(std::convert::TryInto::try_into(space).unwrap()); - let seeds = &[&[nonce][..]]; - let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( - from, - &to, - &base, - seed, - lamports, - space, - owner, - ); - anchor_lang::solana_program::program::invoke_signed( - &ix, - &[ - ctor_accounts.from.clone(), - ctor_accounts.to.clone(), - ctor_accounts.base.clone(), - ctor_accounts.system_program.clone(), - ], - &[seeds], - )?; - - // Serialize the state and save it to storage. - ctor_user_def_accounts.exit(program_id)?; - let mut data = ctor_accounts.to.try_borrow_mut_data()?; - let dst: &mut [u8] = &mut data; - let mut cursor = std::io::Cursor::new(dst); - instance.try_serialize(&mut cursor)?; - - Ok(()) - } - } - } - } - }, - }; - - // State method handlers. - let non_inlined_state_handlers: Vec = match &program.state { - None => vec![], - Some(state) => state - .impl_block_and_methods - .as_ref() - .map(|(_impl_block, methods)| { - methods - .iter() - .map(|ix| { - let ix_params: Vec<_> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); - let ix_arg_names: Vec<&syn::Ident> = - ix.args.iter().map(|arg| &arg.name).collect(); - let private_ix_name: proc_macro2::TokenStream = { - let n = format!("__{}", &ix.raw_method.sig.ident.to_string()); - n.parse().unwrap() - }; - let ix_name = &ix.raw_method.sig.ident; - let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap(); - let anchor_ident = &ix.anchor_ident; - let name = &state.strct.ident; - let mod_name = &program.name; - - if state.is_zero_copy { - quote! { - #[inline(never)] - pub fn #private_ix_name( - program_id: &Pubkey, - accounts: &[AccountInfo], - #(#ix_params),* - ) -> ProgramResult { - let mut remaining_accounts: &[AccountInfo] = accounts; - if remaining_accounts.is_empty() { - return Err(ProgramError::Custom(1)); // todo - } - - let state_account = &remaining_accounts[0]; - let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from(&state_account)?; - remaining_accounts = &remaining_accounts[1..]; - - // Deserialize the program's execution context. - let mut accounts = #anchor_ident::try_accounts( - program_id, - &mut remaining_accounts, - )?; - let ctx = Context::new(program_id, &mut accounts, remaining_accounts); - // Execute user defined function. - { - let mut state = loader.load_mut()?; - state.#ix_name( - ctx, - #(#ix_arg_names),* - )?; - } - // Serialize the state and save it to storage. - accounts.exit(program_id)?; - loader.exit(program_id)?; - - Ok(()) - } - } - } else { - quote! { - #[inline(never)] - pub fn #private_ix_name( - program_id: &Pubkey, - accounts: &[AccountInfo], - #(#ix_params),* - ) -> ProgramResult { - let mut remaining_accounts: &[AccountInfo] = accounts; - if remaining_accounts.is_empty() { - return Err(ProgramError::Custom(1)); // todo - } - - // Deserialize the program state account. - let state_account = &remaining_accounts[0]; - let mut state: #state_ty = { - let data = state_account.try_borrow_data()?; - let mut sliced: &[u8] = &data; - anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)? - }; - - remaining_accounts = &remaining_accounts[1..]; - - // Deserialize the program's execution context. - let mut accounts = #anchor_ident::try_accounts( - program_id, - &mut remaining_accounts, - )?; - let ctx = Context::new(program_id, &mut accounts, remaining_accounts); - - // Execute user defined function. - state.#ix_name( - ctx, - #(#ix_arg_names),* - )?; - - // Serialize the state and save it to storage. - accounts.exit(program_id)?; - let mut data = state_account.try_borrow_mut_data()?; - let dst: &mut [u8] = &mut data; - let mut cursor = std::io::Cursor::new(dst); - state.try_serialize(&mut cursor)?; - - Ok(()) - } - } - } - }) - .collect() - }) - .unwrap_or_default(), - }; - - // State trait handlers. - let non_inlined_state_trait_handlers: Vec = match &program.state { - None => Vec::new(), - Some(state) => state - .interfaces - .as_ref() - .map(|interfaces| { - interfaces - .iter() - .flat_map(|iface: &crate::StateInterface| { - iface - .methods - .iter() - .map(|ix| { - let ix_params: Vec<_> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); - let ix_arg_names: Vec<&syn::Ident> = - ix.args.iter().map(|arg| &arg.name).collect(); - let private_ix_name: proc_macro2::TokenStream = { - let n = format!("__{}_{}", iface.trait_name, &ix.raw_method.sig.ident.to_string()); - n.parse().unwrap() - }; - let ix_name = &ix.raw_method.sig.ident; - let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap(); - let anchor_ident = &ix.anchor_ident; - - if state.is_zero_copy { - // Easy to implement. Just need to write a test. - // Feel free to open a PR. - panic!("Trait implementations not yet implemented for zero copy state structs. Please file an issue."); - } - - if ix.has_receiver { - quote! { - #[inline(never)] - pub fn #private_ix_name( - program_id: &Pubkey, - accounts: &[AccountInfo], - #(#ix_params),* - ) -> ProgramResult { - - let mut remaining_accounts: &[AccountInfo] = accounts; - if remaining_accounts.is_empty() { - return Err(ProgramError::Custom(1)); // todo - } - - // Deserialize the program state account. - let state_account = &remaining_accounts[0]; - let mut state: #state_ty = { - let data = state_account.try_borrow_data()?; - let mut sliced: &[u8] = &data; - anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)? - }; - - remaining_accounts = &remaining_accounts[1..]; - - // Deserialize the program's execution context. - let mut accounts = #anchor_ident::try_accounts( - program_id, - &mut remaining_accounts, - )?; - let ctx = Context::new(program_id, &mut accounts, remaining_accounts); - - // Execute user defined function. - state.#ix_name( - ctx, - #(#ix_arg_names),* - )?; - - // Serialize the state and save it to storage. - accounts.exit(program_id)?; - let mut data = state_account.try_borrow_mut_data()?; - let dst: &mut [u8] = &mut data; - let mut cursor = std::io::Cursor::new(dst); - state.try_serialize(&mut cursor)?; - - Ok(()) - } - } - } else { - let state_name: proc_macro2::TokenStream = state.name.parse().unwrap(); - quote! { - #[inline(never)] - pub fn #private_ix_name( - program_id: &Pubkey, - accounts: &[AccountInfo], - #(#ix_params),* - ) -> ProgramResult { - let mut remaining_accounts: &[AccountInfo] = accounts; - let mut accounts = #anchor_ident::try_accounts( - program_id, - &mut remaining_accounts, - )?; - #state_name::#ix_name( - Context::new(program_id, &mut accounts, remaining_accounts), - #(#ix_arg_names),* - )?; - accounts.exit(program_id) - } - } - } - }) - .collect::>() - }) - .collect() - }) - .unwrap_or_default(), - }; - let non_inlined_handlers: Vec = program - .ixs - .iter() - .map(|ix| { - let ix_params: Vec<_> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); - let ix_arg_names: Vec<&syn::Ident> = ix.args.iter().map(|arg| &arg.name).collect(); - let ix_name = &ix.raw_method.sig.ident; - let anchor = &ix.anchor_ident; - - quote! { - #[inline(never)] - pub fn #ix_name( - program_id: &Pubkey, - accounts: &[AccountInfo], - #(#ix_params),* - ) -> ProgramResult { - let mut remaining_accounts: &[AccountInfo] = accounts; - let mut accounts = #anchor::try_accounts(program_id, &mut remaining_accounts)?; - #program_name::#ix_name( - Context::new(program_id, &mut accounts, remaining_accounts), - #(#ix_arg_names),* - )?; - accounts.exit(program_id) - } - } - }) - .collect(); - - quote! { - /// __idl mod defines handlers for injected Anchor IDL instructions. - pub mod __idl { - use super::*; - - #non_inlined_idl - } - - /// __state mod defines wrapped handlers for state instructions. - pub mod __state { - use super::*; - - #non_inlined_ctor - #(#non_inlined_state_handlers)* - } - - /// __interface mod defines wrapped handlers for `#[interface]` trait - /// implementations. - pub mod __interface { - use super::*; - - #(#non_inlined_state_trait_handlers)* - } - - /// __global mod defines wrapped handlers for global instructions. - pub mod __global { - use super::*; - - #(#non_inlined_handlers)* - } - } -} - -pub fn generate_ctor_variant(state: &State) -> proc_macro2::TokenStream { - let ctor_args = generate_ctor_args(state); - let ctor_variant_name: proc_macro2::TokenStream = generate_ctor_variant_name().parse().unwrap(); - if ctor_args.is_empty() { - quote! { - #ctor_variant_name - } - } else { - quote! { - #ctor_variant_name { - #(#ctor_args),* - } - } - } -} - -pub fn generate_ctor_variant_name() -> String { - "New".to_string() -} - -fn generate_ctor_typed_args(state: &State) -> Vec { - state - .ctor_and_anchor - .as_ref() - .map(|(ctor, _anchor_ident)| { - ctor.sig - .inputs - .iter() - .filter_map(|arg: &syn::FnArg| match arg { - syn::FnArg::Typed(pat_ty) => { - let mut arg_str = parser::tts_to_string(&pat_ty.ty); - arg_str.retain(|c| !c.is_whitespace()); - if arg_str.starts_with("Context<") { - return None; - } - Some(pat_ty.clone()) - } - _ => { - if !state.is_zero_copy { - panic!("Cannot pass self as parameter") - } - None - } - }) - .collect() - }) - .unwrap_or_default() -} - -fn generate_ctor_args(state: &State) -> Vec { - state - .ctor_and_anchor - .as_ref() - .map(|(ctor, _anchor_ident)| { - ctor.sig - .inputs - .iter() - .filter_map(|arg: &syn::FnArg| match arg { - syn::FnArg::Typed(pat_ty) => { - let mut arg_str = parser::tts_to_string(&pat_ty.ty); - arg_str.retain(|c| !c.is_whitespace()); - if arg_str.starts_with("Context<") { - return None; - } - Some(*pat_ty.pat.clone()) - } - _ => { - if !state.is_zero_copy { - panic!("Cannot pass self as parameter"); - } - None - } - }) - .collect() - }) - .unwrap_or_default() -} - -pub fn generate_ix_variant(name: String, args: &[IxArg]) -> proc_macro2::TokenStream { - let ix_arg_names: Vec<&syn::Ident> = args.iter().map(|arg| &arg.name).collect(); - let ix_name_camel: proc_macro2::TokenStream = { - let n = name.to_camel_case(); - n.parse().unwrap() - }; - - if args.is_empty() { - quote! { - #ix_name_camel - } - } else { - quote! { - #ix_name_camel { - #(#ix_arg_names),* - } - } - } -} - -pub fn generate_ix_variant_name(name: String) -> proc_macro2::TokenStream { - let n = name.to_camel_case(); - n.parse().unwrap() -} - -pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream { - let program_mod = &program.program_mod; - quote! { - #program_mod - } -} - -pub fn generate_ixs(program: &Program) -> proc_macro2::TokenStream { - let ctor_variant = match &program.state { - None => quote! {}, - Some(state) => { - let ctor_args: Vec = generate_ctor_typed_args(state) - .iter() - .map(|arg| { - format!("pub {}", parser::tts_to_string(&arg)) - .parse() - .unwrap() - }) - .collect(); - let strct = { - if ctor_args.is_empty() { - quote! { - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct New; - } - } else { - quote! { - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct New { - #(#ctor_args),* - } - } - } - }; - let sighash_arr = sighash_ctor(); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - quote! { - /// Instruction arguments to the `#[state]`'s `new` - /// constructor. - #strct - - impl anchor_lang::InstructionData for New { - fn data(&self) -> Vec { - let mut d = #sighash_tts.to_vec(); - d.append(&mut self.try_to_vec().expect("Should always serialize")); - d - } - } - } - } - }; - let state_method_variants: Vec = match &program.state { - None => vec![], - Some(state) => state - .impl_block_and_methods - .as_ref() - .map(|(_impl_block, methods)| { - methods - .iter() - .map(|method| { - let ix_name_camel: proc_macro2::TokenStream = method - .raw_method - .sig - .ident - .to_string() - .to_camel_case() - .parse() - .unwrap(); - let raw_args: Vec = method - .args - .iter() - .map(|arg| { - format!("pub {}", parser::tts_to_string(&arg.raw_arg)) - .parse() - .unwrap() - }) - .collect(); - - let ix_data_trait = { - let name = method.raw_method.sig.ident.to_string(); - let sighash_arr = sighash(SIGHASH_STATE_NAMESPACE, &name); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - quote! { - impl anchor_lang::InstructionData for #ix_name_camel { - fn data(&self) -> Vec { - let mut d = #sighash_tts.to_vec(); - d.append(&mut self.try_to_vec().expect("Should always serialize")); - d - } - } - } - }; - - // If no args, output a "unit" variant instead of a struct variant. - if method.args.is_empty() { - quote! { - /// Anchor generated instruction. - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct #ix_name_camel; - - #ix_data_trait - } - } else { - quote! { - /// Anchor generated instruction. - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct #ix_name_camel { - #(#raw_args),* - } - - #ix_data_trait - } - } - }) - .collect() - }) - .unwrap_or_default(), - }; - let variants: Vec = program - .ixs - .iter() - .map(|ix| { - let name = &ix.raw_method.sig.ident.to_string(); - let ix_name_camel = - proc_macro2::Ident::new(&name.to_camel_case(), ix.raw_method.sig.ident.span()); - let raw_args: Vec = ix - .args - .iter() - .map(|arg| { - format!("pub {}", parser::tts_to_string(&arg.raw_arg)) - .parse() - .unwrap() - }) - .collect(); - let ix_data_trait = { - let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - quote! { - impl anchor_lang::InstructionData for #ix_name_camel { - fn data(&self) -> Vec { - let mut d = #sighash_tts.to_vec(); - d.append(&mut self.try_to_vec().expect("Should always serialize")); - d - } - } - } - }; - // If no args, output a "unit" variant instead of a struct variant. - if ix.args.is_empty() { - quote! { - /// Instruction. - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct #ix_name_camel; - - #ix_data_trait - } - } else { - quote! { - /// Instruction. - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct #ix_name_camel { - #(#raw_args),* - } - - #ix_data_trait - } - } - }) - .collect(); - - quote! { - /// An Anchor generated module containing the program's set of - /// instructions, where each method handler in the `#[program]` mod is - /// associated with a struct defining the input arguments to the - /// method. These should be used directly, when one wants to serialize - /// Anchor instruction data, for example, when speciying - /// instructions on a client. - pub mod instruction { - use super::*; - - /// Instruction struct definitions for `#[state]` methods. - pub mod state { - use super::*; - - #ctor_variant - #(#state_method_variants)* - } - - #(#variants)* - } - } -} - -fn generate_accounts(program: &Program) -> proc_macro2::TokenStream { - let mut accounts = std::collections::HashSet::new(); - - // Go through state accounts. - if let Some(state) = &program.state { - // Ctor. - if let Some((_ctor, ctor_accounts)) = &state.ctor_and_anchor { - let macro_name = format!( - "__client_accounts_{}", - ctor_accounts.to_string().to_snake_case() - ); - accounts.insert(macro_name); - } - // Methods. - if let Some((_impl_block, methods)) = &state.impl_block_and_methods { - for ix in methods { - let anchor_ident = &ix.anchor_ident; - // TODO: move to fn and share with accounts.rs. - let macro_name = format!( - "__client_accounts_{}", - anchor_ident.to_string().to_snake_case() - ); - accounts.insert(macro_name); - } - } - } - - // Go through instruction accounts. - for ix in &program.ixs { - let anchor_ident = &ix.anchor_ident; - // TODO: move to fn and share with accounts.rs. - let macro_name = format!( - "__client_accounts_{}", - anchor_ident.to_string().to_snake_case() - ); - accounts.insert(macro_name); - } - - // Build the tokens from all accounts - let account_structs: Vec = accounts - .iter() - .map(|macro_name: &String| { - let macro_name: proc_macro2::TokenStream = macro_name.parse().unwrap(); - quote! { - pub use crate::#macro_name::*; - } - }) - .collect(); - - // TODO: calculate the account size and add it as a constant field to - // each struct here. This is convenient for Rust clients. - - quote! { - /// An Anchor generated module, providing a set of structs - /// mirroring the structs deriving `Accounts`, where each field is - /// a `Pubkey`. This is useful for specifying accounts for a client. - pub mod accounts { - #(#account_structs)* - } - } -} - -fn generate_cpi(program: &Program) -> proc_macro2::TokenStream { - // Generate cpi methods for the state struct. - // The Ctor is not exposed via CPI, since it is a one time use function. - let state_cpi_methods: Vec = program - .state - .as_ref() - .map(|state| { - state - .impl_block_and_methods - .as_ref() - .map(|(_, methods)| { - methods - .iter() - .map(|method: &StateIx| { - let accounts_ident = &method.anchor_ident; - let ix_variant = generate_ix_variant( - method.raw_method.sig.ident.to_string(), - &method.args, - ); - let method_name = &method.ident; - let args: Vec<&syn::PatType> = - method.args.iter().map(|arg| &arg.raw_arg).collect(); - - quote! { - pub fn #method_name<'a, 'b, 'c, 'info>( - ctx: CpiStateContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>, - #(#args),* - ) -> ProgramResult { - let ix = { - let ix = instruction::state::#ix_variant; - let data = anchor_lang::InstructionData::data(&ix); - let accounts = ctx.to_account_metas(None); - anchor_lang::solana_program::instruction::Instruction { - program_id: *ctx.program().key, - accounts, - data, - } - }; - let mut acc_infos = ctx.to_account_infos(); - anchor_lang::solana_program::program::invoke_signed( - &ix, - &acc_infos, - ctx.signer_seeds(), - ) - } - } - }) - .collect() - }) - .unwrap_or(vec![]) - }) - .unwrap_or(vec![]); - // Generate cpi methods for global methods. - let global_cpi_methods: Vec = program - .ixs - .iter() - .map(|ix| { - let accounts_ident = &ix.anchor_ident; - let cpi_method = { - let ix_variant = generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); - let method_name = &ix.ident; - let args: Vec<&syn::PatType> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); - let name = &ix.raw_method.sig.ident.to_string(); - let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name); - let sighash_tts: proc_macro2::TokenStream = - format!("{:?}", sighash_arr).parse().unwrap(); - quote! { - pub fn #method_name<'a, 'b, 'c, 'info>( - ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>, - #(#args),* - ) -> ProgramResult { - let ix = { - let ix = instruction::#ix_variant; - let mut ix_data = AnchorSerialize::try_to_vec(&ix) - .map_err(|_| ProgramError::InvalidInstructionData)?; - let mut data = #sighash_tts.to_vec(); - data.append(&mut ix_data); - let accounts = ctx.accounts.to_account_metas(None); - anchor_lang::solana_program::instruction::Instruction { - program_id: *ctx.program.key, - accounts, - data, - } - }; - let mut acc_infos = ctx.accounts.to_account_infos(); - acc_infos.push(ctx.program.clone()); - anchor_lang::solana_program::program::invoke_signed( - &ix, - &acc_infos, - ctx.signer_seeds, - ) - } - } - }; - - cpi_method - }) - .collect(); - quote! { - #[cfg(feature = "cpi")] - pub mod cpi { - use super::*; - - pub mod state { - use super::*; - - #(#state_cpi_methods)* - } - - #(#global_cpi_methods)* - } - } -} - -// We don't technically use sighash, because the input arguments aren't given. -// Rust doesn't have method overloading so no need to use the arguments. -// However, we do namespace methods in the preeimage so that we can use -// different traits with the same method name. -pub fn sighash(namespace: &str, name: &str) -> [u8; 8] { - let preimage = format!("{}:{}", namespace, name); - - let mut sighash = [0u8; 8]; - sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]); - sighash -} - -fn sighash_ctor() -> [u8; 8] { - let namespace = SIGHASH_STATE_NAMESPACE; - let preimage = format!("{}:new", namespace); - - let mut sighash = [0u8; 8]; - sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]); - sighash -} diff --git a/lang/syn/src/codegen/program/accounts.rs b/lang/syn/src/codegen/program/accounts.rs new file mode 100644 index 0000000000..2dfebbd214 --- /dev/null +++ b/lang/syn/src/codegen/program/accounts.rs @@ -0,0 +1,65 @@ +use crate::Program; +use heck::SnakeCase; +use quote::quote; + +pub fn generate(program: &Program) -> proc_macro2::TokenStream { + let mut accounts = std::collections::HashSet::new(); + + // Go through state accounts. + if let Some(state) = &program.state { + // Ctor. + if let Some((_ctor, ctor_accounts)) = &state.ctor_and_anchor { + let macro_name = format!( + "__client_accounts_{}", + ctor_accounts.to_string().to_snake_case() + ); + accounts.insert(macro_name); + } + // Methods. + if let Some((_impl_block, methods)) = &state.impl_block_and_methods { + for ix in methods { + let anchor_ident = &ix.anchor_ident; + // TODO: move to fn and share with accounts.rs. + let macro_name = format!( + "__client_accounts_{}", + anchor_ident.to_string().to_snake_case() + ); + accounts.insert(macro_name); + } + } + } + + // Go through instruction accounts. + for ix in &program.ixs { + let anchor_ident = &ix.anchor_ident; + // TODO: move to fn and share with accounts.rs. + let macro_name = format!( + "__client_accounts_{}", + anchor_ident.to_string().to_snake_case() + ); + accounts.insert(macro_name); + } + + // Build the tokens from all accounts + let account_structs: Vec = accounts + .iter() + .map(|macro_name: &String| { + let macro_name: proc_macro2::TokenStream = macro_name.parse().unwrap(); + quote! { + pub use crate::#macro_name::*; + } + }) + .collect(); + + // TODO: calculate the account size and add it as a constant field to + // each struct here. This is convenient for Rust clients. + + quote! { + /// An Anchor generated module, providing a set of structs + /// mirroring the structs deriving `Accounts`, where each field is + /// a `Pubkey`. This is useful for specifying accounts for a client. + pub mod accounts { + #(#account_structs)* + } + } +} diff --git a/lang/syn/src/codegen/program/common.rs b/lang/syn/src/codegen/program/common.rs new file mode 100644 index 0000000000..c17379a167 --- /dev/null +++ b/lang/syn/src/codegen/program/common.rs @@ -0,0 +1,83 @@ +use crate::parser; +use crate::{IxArg, State}; +use heck::CamelCase; +use quote::quote; + +// Namespace for calculating state instruction sighash signatures. +pub const SIGHASH_STATE_NAMESPACE: &str = "state"; + +// Namespace for calculating instruction sighash signatures for any instruction +// not affecting program state. +pub const SIGHASH_GLOBAL_NAMESPACE: &str = "global"; + +// We don't technically use sighash, because the input arguments aren't given. +// Rust doesn't have method overloading so no need to use the arguments. +// However, we do namespace methods in the preeimage so that we can use +// different traits with the same method name. +pub fn sighash(namespace: &str, name: &str) -> [u8; 8] { + let preimage = format!("{}:{}", namespace, name); + + let mut sighash = [0u8; 8]; + sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]); + sighash +} + +pub fn sighash_ctor() -> [u8; 8] { + sighash(SIGHASH_STATE_NAMESPACE, "new") +} + +pub fn generate_ix_variant(name: String, args: &[IxArg]) -> proc_macro2::TokenStream { + let ix_arg_names: Vec<&syn::Ident> = args.iter().map(|arg| &arg.name).collect(); + let ix_name_camel: proc_macro2::TokenStream = { + let n = name.to_camel_case(); + n.parse().unwrap() + }; + + if args.is_empty() { + quote! { + #ix_name_camel + } + } else { + quote! { + #ix_name_camel { + #(#ix_arg_names),* + } + } + } +} + +pub fn generate_ctor_args(state: &State) -> Vec { + generate_ctor_typed_args(state) + .iter() + .map(|pat_ty| *pat_ty.pat.clone()) + .collect() +} + +pub fn generate_ctor_typed_args(state: &State) -> Vec { + state + .ctor_and_anchor + .as_ref() + .map(|(ctor, _anchor_ident)| { + ctor.sig + .inputs + .iter() + .filter_map(|arg: &syn::FnArg| match arg { + syn::FnArg::Typed(pat_ty) => { + let mut arg_str = parser::tts_to_string(&pat_ty.ty); + arg_str.retain(|c| !c.is_whitespace()); + if arg_str.starts_with("Context<") { + return None; + } + Some(pat_ty.clone()) + } + _ => { + if !state.is_zero_copy { + panic!("Cannot pass self as parameter") + } + None + } + }) + .collect() + }) + .unwrap_or_default() +} diff --git a/lang/syn/src/codegen/program/cpi.rs b/lang/syn/src/codegen/program/cpi.rs new file mode 100644 index 0000000000..4a7e248118 --- /dev/null +++ b/lang/syn/src/codegen/program/cpi.rs @@ -0,0 +1,118 @@ +use crate::codegen::program::common::{generate_ix_variant, sighash, SIGHASH_GLOBAL_NAMESPACE}; +use crate::Program; +use crate::StateIx; +use quote::quote; + +pub fn generate(program: &Program) -> proc_macro2::TokenStream { + // Generate cpi methods for the state struct. + // The Ctor is not exposed via CPI, since it is a one time use function. + let state_cpi_methods: Vec = program + .state + .as_ref() + .map(|state| { + state + .impl_block_and_methods + .as_ref() + .map(|(_, methods)| { + methods + .iter() + .map(|method: &StateIx| { + let accounts_ident = &method.anchor_ident; + let ix_variant = generate_ix_variant( + method.raw_method.sig.ident.to_string(), + &method.args, + ); + let method_name = &method.ident; + let args: Vec<&syn::PatType> = + method.args.iter().map(|arg| &arg.raw_arg).collect(); + + quote! { + pub fn #method_name<'a, 'b, 'c, 'info>( + ctx: CpiStateContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>, + #(#args),* + ) -> ProgramResult { + let ix = { + let ix = instruction::state::#ix_variant; + let data = anchor_lang::InstructionData::data(&ix); + let accounts = ctx.to_account_metas(None); + anchor_lang::solana_program::instruction::Instruction { + program_id: *ctx.program().key, + accounts, + data, + } + }; + let mut acc_infos = ctx.to_account_infos(); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &acc_infos, + ctx.signer_seeds(), + ) + } + } + }) + .collect() + }) + .unwrap_or(vec![]) + }) + .unwrap_or(vec![]); + // Generate cpi methods for global methods. + let global_cpi_methods: Vec = program + .ixs + .iter() + .map(|ix| { + let accounts_ident = &ix.anchor_ident; + let cpi_method = { + let ix_variant = generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); + let method_name = &ix.ident; + let args: Vec<&syn::PatType> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); + let name = &ix.raw_method.sig.ident.to_string(); + let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + quote! { + pub fn #method_name<'a, 'b, 'c, 'info>( + ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>, + #(#args),* + ) -> ProgramResult { + let ix = { + let ix = instruction::#ix_variant; + let mut ix_data = AnchorSerialize::try_to_vec(&ix) + .map_err(|_| ProgramError::InvalidInstructionData)?; + let mut data = #sighash_tts.to_vec(); + data.append(&mut ix_data); + let accounts = ctx.accounts.to_account_metas(None); + anchor_lang::solana_program::instruction::Instruction { + program_id: *ctx.program.key, + accounts, + data, + } + }; + let mut acc_infos = ctx.accounts.to_account_infos(); + acc_infos.push(ctx.program.clone()); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &acc_infos, + ctx.signer_seeds, + ) + } + } + }; + + cpi_method + }) + .collect(); + quote! { + #[cfg(feature = "cpi")] + pub mod cpi { + use super::*; + + pub mod state { + use super::*; + + #(#state_cpi_methods)* + } + + #(#global_cpi_methods)* + } + } +} diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs new file mode 100644 index 0000000000..4cb205855d --- /dev/null +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -0,0 +1,215 @@ +use crate::codegen::program::common::*; +use crate::{Program, State}; +use heck::CamelCase; +use quote::quote; + +pub fn generate(program: &Program) -> proc_macro2::TokenStream { + // Dispatch the state constructor. + let ctor_state_dispatch_arm = match &program.state { + None => quote! { /* no-op */ }, + Some(state) => match state.ctor_and_anchor.is_some() { + false => quote! {}, + true => { + let variant_arm = generate_ctor_variant(state); + let ctor_args = generate_ctor_args(state); + let ix_name: proc_macro2::TokenStream = + generate_ctor_variant_name().parse().unwrap(); + let sighash_arr = sighash_ctor(); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + quote! { + #sighash_tts => { + let ix = instruction::state::#ix_name::deserialize(&mut ix_data) + .map_err(|_| ProgramError::Custom(1))?; // todo: error code + let instruction::state::#variant_arm = ix; + __private::__state::__ctor(program_id, accounts, #(#ctor_args),*) + } + } + } + }, + }; + + // Dispatch the state impl instructions. + let state_dispatch_arms: Vec = match &program.state { + None => vec![], + Some(s) => s + .impl_block_and_methods + .as_ref() + .map(|(_impl_block, methods)| { + methods + .iter() + .map(|ix: &crate::StateIx| { + let ix_arg_names: Vec<&syn::Ident> = + ix.args.iter().map(|arg| &arg.name).collect(); + let name = &ix.raw_method.sig.ident.to_string(); + let ix_method_name: proc_macro2::TokenStream = + { format!("__{}", name).parse().unwrap() }; + let variant_arm = + generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); + let ix_name = generate_ix_variant_name(ix.raw_method.sig.ident.to_string()); + let sighash_arr = sighash(SIGHASH_STATE_NAMESPACE, &name); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + quote! { + #sighash_tts => { + let ix = instruction::state::#ix_name::deserialize(&mut ix_data) + .map_err(|_| ProgramError::Custom(1))?; // todo: error code + let instruction::state::#variant_arm = ix; + __private::__state::#ix_method_name(program_id, accounts, #(#ix_arg_names),*) + } + } + }) + .collect() + }) + .unwrap_or_default(), + }; + + // Dispatch all trait interface implementations. + let trait_dispatch_arms: Vec = match &program.state { + None => vec![], + Some(s) => s + .interfaces + .as_ref() + .map(|interfaces| { + interfaces + .iter() + .flat_map(|iface: &crate::StateInterface| { + iface + .methods + .iter() + .map(|m: &crate::StateIx| { + let ix_arg_names: Vec<&syn::Ident> = + m.args.iter().map(|arg| &arg.name).collect(); + let name = &m.raw_method.sig.ident.to_string(); + let ix_name: proc_macro2::TokenStream = format!("__{}_{}", iface.trait_name, name).parse().unwrap(); + let raw_args: Vec<&syn::PatType> = m + .args + .iter() + .map(|arg: &crate::IxArg| &arg.raw_arg) + .collect(); + let sighash_arr = sighash(&iface.trait_name, &m.ident.to_string()); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + let args_struct = { + if m.args.is_empty() { + quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + struct Args; + } + } else { + quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + struct Args { + #(#raw_args),* + } + } + } + }; + quote! { + #sighash_tts => { + #args_struct + let ix = Args::deserialize(&mut ix_data) + .map_err(|_| ProgramError::Custom(1))?; // todo: error code + let Args { + #(#ix_arg_names),* + } = ix; + __private::__interface::#ix_name(program_id, accounts, #(#ix_arg_names),*) + } + } + }) + .collect::>() + }) + .collect() + }) + .unwrap_or_default() + }; + + // Dispatch all global instructions. + let global_dispatch_arms: Vec = program + .ixs + .iter() + .map(|ix| { + let ix_arg_names: Vec<&syn::Ident> = ix.args.iter().map(|arg| &arg.name).collect(); + let ix_method_name = &ix.raw_method.sig.ident; + let ix_name = generate_ix_variant_name(ix.raw_method.sig.ident.to_string()); + let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &ix_method_name.to_string()); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + let variant_arm = generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); + quote! { + #sighash_tts => { + let ix = instruction::#ix_name::deserialize(&mut ix_data) + .map_err(|_| ProgramError::Custom(1))?; // todo: error code + let instruction::#variant_arm = ix; + __private::__global::#ix_method_name(program_id, accounts, #(#ix_arg_names),*) + } + } + }) + .collect(); + + quote! { + /// Performs method dispatch. + /// + /// Each method in an anchor program is uniquely defined by a namespace + /// and a rust identifier (i.e., the name given to the method). These + /// two pieces can be combined to creater a method identifier, + /// specifically, Anchor uses + /// + /// Sha256("::")[..8], + /// + /// where the namespace can be one of three types. 1) "global" for a + /// regular instruction, 2) "state" for a state struct instruction + /// handler and 3) a trait namespace (used in combination with the + /// `#[interface]` attribute), which is defined by the trait name, e.. + /// `MyTrait`. + /// + /// With this 8 byte identifier, Anchor performs method dispatch, + /// matching the given 8 byte identifier to the associated method + /// handler, which leads to user defined code being eventually invoked. + fn dispatch(program_id: &Pubkey, accounts: &[AccountInfo], sighash: [u8; 8], mut ix_data: &[u8]) -> ProgramResult { + // If the method identifier is the IDL tag, then execute an IDL + // instruction, injected into all Anchor programs. + if cfg!(not(feature = "no-idl")) { + if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() { + return __private::__idl::__idl_dispatch(program_id, accounts, &ix_data); + } + } + + match sighash { + #ctor_state_dispatch_arm + #(#state_dispatch_arms)* + #(#trait_dispatch_arms)* + #(#global_dispatch_arms)* + _ => { + msg!("Fallback functions are not supported. If you have a use case, please file an issue."); + Err(ProgramError::Custom(99)) + } + } + } + } +} + +fn generate_ctor_variant_name() -> String { + "New".to_string() +} + +fn generate_ctor_variant(state: &State) -> proc_macro2::TokenStream { + let ctor_args = generate_ctor_args(state); + let ctor_variant_name: proc_macro2::TokenStream = generate_ctor_variant_name().parse().unwrap(); + if ctor_args.is_empty() { + quote! { + #ctor_variant_name + } + } else { + quote! { + #ctor_variant_name { + #(#ctor_args),* + } + } + } +} + +fn generate_ix_variant_name(name: String) -> proc_macro2::TokenStream { + let n = name.to_camel_case(); + n.parse().unwrap() +} diff --git a/lang/syn/src/codegen/program/entry.rs b/lang/syn/src/codegen/program/entry.rs new file mode 100644 index 0000000000..11323c8465 --- /dev/null +++ b/lang/syn/src/codegen/program/entry.rs @@ -0,0 +1,75 @@ +use crate::Program; +use quote::quote; + +pub fn generate(_program: &Program) -> proc_macro2::TokenStream { + quote! { + #[cfg(not(feature = "no-entrypoint"))] + anchor_lang::solana_program::entrypoint!(entry); + /// The Anchor codegen exposes a programming model where a user defines + /// a set of methods inside of a `#[program]` module in a way similar + /// to writing RPC request handlers. The macro then generates a bunch of + /// code wrapping these user defined methods into something that can be + /// executed on Solana. + /// + /// These methods fall into one of three categories, each of which + /// can be considered a different "namespace" of the program. + /// + /// 1) Global methods - regular methods inside of the `#[program]`. + /// 2) State methods - associated methods inside a `#[state]` struct. + /// 3) Interface methods - methods inside a strait struct's + /// implementation of an `#[interface]` trait. + /// + /// Care must be taken by the codegen to prevent collisions between + /// methods in these different namespaces. For this reason, Anchor uses + /// a variant of sighash to perform method dispatch, rather than + /// something like a simple enum variant discriminator. + /// + /// The execution flow of the generated code can be roughly outlined: + /// + /// * Start program via the entrypoint. + /// * Strip method identifier off the first 8 bytes of the instruction + /// data and invoke the identified method. The method identifier + /// is a variant of sighash. See docs.rs for `anchor_lang` for details. + /// * If the method identifier is an IDL identifier, execute the IDL + /// instructions, which are a special set of hardcoded instructions + /// baked into every Anchor program. Then exit. + /// * Otherwise, the method identifier is for a user defined + /// instruction, i.e., one of the methods in the user defined + /// `#[program]` module. Perform method dispatch, i.e., execute the + /// big match statement mapping method identifier to method handler + /// wrapper. + /// * Run the method handler wrapper. This wraps the code the user + /// actually wrote, deserializing the accounts, constructing the + /// context, invoking the user's code, and finally running the exit + /// routine, which typically persists account changes. + /// + /// The `entry` function here, defines the standard entry to a Solana + /// program, where execution begins. + #[cfg(not(feature = "no-entrypoint"))] + fn entry(program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8]) -> ProgramResult { + #[cfg(feature = "anchor-debug")] + { + msg!("anchor-debug is active"); + } + if ix_data.len() < 8 { + return Err(ProgramError::Custom(99)); + } + + // Split the instruction data into the first 8 byte method + // identifier (sighash) and the serialized instruction data. + let mut ix_data: &[u8] = ix_data; + let sighash: [u8; 8] = { + let mut sighash: [u8; 8] = [0; 8]; + sighash.copy_from_slice(&ix_data[..8]); + ix_data = &ix_data[8..]; + sighash + }; + + dispatch(program_id, accounts, sighash, ix_data) + .map_err(|e| { + anchor_lang::solana_program::msg!(&e.to_string()); + e + }) + } + } +} diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs new file mode 100644 index 0000000000..bbcb398779 --- /dev/null +++ b/lang/syn/src/codegen/program/handlers.rs @@ -0,0 +1,592 @@ +use crate::codegen::program::common::*; +use crate::Program; +use quote::quote; + +// Generate non-inlined wrappers for each instruction handler, since Solana's +// BPF max stack size can't handle reasonable sized dispatch trees without doing +// so. +pub fn generate(program: &Program) -> proc_macro2::TokenStream { + let program_name = &program.name; + let non_inlined_idl: proc_macro2::TokenStream = { + quote! { + // Entry for all IDL related instructions. Use the "no-idl" feature + // to eliminate this code, for example, if one wants to make the + // IDL no longer mutable or if one doesn't want to store the IDL + // on chain. + #[inline(never)] + #[cfg(not(feature = "no-idl"))] + pub fn __idl_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult { + let mut accounts = accounts; + let mut data: &[u8] = idl_ix_data; + + let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data) + .map_err(|_| ProgramError::Custom(2))?; // todo + + match ix { + anchor_lang::idl::IdlInstruction::Create { data_len } => { + let mut accounts = anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts)?; + __idl_create_account(program_id, &mut accounts, data_len)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::CreateBuffer => { + let mut accounts = anchor_lang::idl::IdlCreateBuffer::try_accounts(program_id, &mut accounts)?; + __idl_create_buffer(program_id, &mut accounts)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::Write { data } => { + let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; + __idl_write(program_id, &mut accounts, data)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => { + let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; + __idl_set_authority(program_id, &mut accounts, new_authority)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::SetBuffer => { + let mut accounts = anchor_lang::idl::IdlSetBuffer::try_accounts(program_id, &mut accounts)?; + __idl_set_buffer(program_id, &mut accounts)?; + accounts.exit(program_id)?; + }, + } + Ok(()) + } + + #[inline(never)] + #[cfg(feature = "no-idl")] + pub fn __idl_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult { + Err(anchor_lang::solana_program::program_error::ProgramError::Custom(99)) + } + + // One time IDL account initializer. Will faill on subsequent + // invocations. + #[inline(never)] + pub fn __idl_create_account( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlCreateAccounts, + data_len: u64, + ) -> ProgramResult { + if program_id != accounts.program.key { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(98)); // todo proper error + } + // Create the IDL's account. + let from = accounts.from.key; + let (base, nonce) = Pubkey::find_program_address(&[], program_id); + let seed = anchor_lang::idl::IdlAccount::seed(); + let owner = accounts.program.key; + let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); + // Space: account discriminator || authority pubkey || vec len || vec data + let space = 8 + 32 + 4 + data_len as usize; + let lamports = accounts.rent.minimum_balance(space); + let seeds = &[&[nonce][..]]; + let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( + from, + &to, + &base, + seed, + lamports, + space as u64, + owner, + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[ + accounts.from.clone(), + accounts.to.clone(), + accounts.base.clone(), + accounts.system_program.clone(), + ], + &[seeds], + )?; + + // Deserialize the newly created account. + let mut idl_account = { + let mut account_data = accounts.to.try_borrow_data()?; + let mut account_data_slice: &[u8] = &account_data; + anchor_lang::idl::IdlAccount::try_deserialize_unchecked( + &mut account_data_slice, + )? + }; + + // Set the authority. + idl_account.authority = *accounts.from.key; + + // Store the new account data. + let mut data = accounts.to.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + idl_account.try_serialize(&mut cursor)?; + + Ok(()) + } + + #[inline(never)] + pub fn __idl_create_buffer( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlCreateBuffer, + ) -> ProgramResult { + let mut buffer = &mut accounts.buffer; + buffer.authority = *accounts.authority.key; + Ok(()) + } + + #[inline(never)] + pub fn __idl_write( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlAccounts, + idl_data: Vec, + ) -> ProgramResult { + let mut idl = &mut accounts.idl; + idl.data.extend(idl_data); + Ok(()) + } + + #[inline(never)] + pub fn __idl_set_authority( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlAccounts, + new_authority: Pubkey, + ) -> ProgramResult { + accounts.idl.authority = new_authority; + Ok(()) + } + + #[inline(never)] + pub fn __idl_set_buffer( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlSetBuffer, + ) -> ProgramResult { + accounts.idl.data = accounts.buffer.data.clone(); + Ok(()) + } + } + }; + // Constructor handler. + let non_inlined_ctor: proc_macro2::TokenStream = match &program.state { + None => quote! {}, + Some(state) => match state.ctor_and_anchor.as_ref() { + None => quote! {}, + Some((_ctor, anchor_ident)) => { + let ctor_typed_args = generate_ctor_typed_args(state); + let ctor_untyped_args = generate_ctor_args(state); + let name = &state.strct.ident; + let mod_name = &program.name; + if state.is_zero_copy { + quote! { + // One time state account initializer. Will faill on subsequent + // invocations. + #[inline(never)] + pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + + // Deserialize accounts. + let ctor_accounts = anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts)?; + let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts)?; + + // Create the solana account for the ctor data. + let from = ctor_accounts.from.key; + let (base, nonce) = Pubkey::find_program_address(&[], ctor_accounts.program.key); + let seed = anchor_lang::__private::PROGRAM_STATE_SEED; + let owner = ctor_accounts.program.key; + let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); + let space = 8 + std::mem::size_of::<#name>(); + let lamports = ctor_accounts.rent.minimum_balance(std::convert::TryInto::try_into(space).unwrap()); + let seeds = &[&[nonce][..]]; + let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( + from, + &to, + &base, + seed, + lamports, + space as u64, + owner, + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[ + ctor_accounts.from.clone(), + ctor_accounts.to.clone(), + ctor_accounts.base.clone(), + ctor_accounts.system_program.clone(), + ], + &[seeds], + )?; + + // Zero copy deserialize. + let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from_init(&ctor_accounts.to)?; + + // Invoke the ctor in a new lexical scope so that + // the zero-copy RefMut gets dropped. Required + // so that we can subsequently run the exit routine. + { + let mut instance = loader.load_init()?; + instance.new( + anchor_lang::Context::new( + program_id, + &mut ctor_user_def_accounts, + remaining_accounts, + ), + #(#ctor_untyped_args),* + )?; + } + + // Exit routines. + ctor_user_def_accounts.exit(program_id)?; + loader.exit(program_id)?; + + Ok(()) + } + } + } else { + quote! { + // One time state account initializer. Will faill on subsequent + // invocations. + #[inline(never)] + pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + + // Deserialize accounts. + let ctor_accounts = anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts)?; + let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts)?; + + // Invoke the ctor. + let instance = #mod_name::#name::new( + anchor_lang::Context::new( + program_id, + &mut ctor_user_def_accounts, + remaining_accounts, + ), + #(#ctor_untyped_args),* + )?; + + // Create the solana account for the ctor data. + let from = ctor_accounts.from.key; + let (base, nonce) = Pubkey::find_program_address(&[], ctor_accounts.program.key); + let seed = anchor_lang::ProgramState::<#name>::seed(); + let owner = ctor_accounts.program.key; + let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); + let space = anchor_lang::__private::AccountSize::size(&instance)?; + let lamports = ctor_accounts.rent.minimum_balance(std::convert::TryInto::try_into(space).unwrap()); + let seeds = &[&[nonce][..]]; + let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( + from, + &to, + &base, + seed, + lamports, + space, + owner, + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[ + ctor_accounts.from.clone(), + ctor_accounts.to.clone(), + ctor_accounts.base.clone(), + ctor_accounts.system_program.clone(), + ], + &[seeds], + )?; + + // Serialize the state and save it to storage. + ctor_user_def_accounts.exit(program_id)?; + let mut data = ctor_accounts.to.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + instance.try_serialize(&mut cursor)?; + + Ok(()) + } + } + } + } + }, + }; + + // State method handlers. + let non_inlined_state_handlers: Vec = match &program.state { + None => vec![], + Some(state) => state + .impl_block_and_methods + .as_ref() + .map(|(_impl_block, methods)| { + methods + .iter() + .map(|ix| { + let ix_params: Vec<_> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); + let ix_arg_names: Vec<&syn::Ident> = + ix.args.iter().map(|arg| &arg.name).collect(); + let private_ix_name: proc_macro2::TokenStream = { + let n = format!("__{}", &ix.raw_method.sig.ident.to_string()); + n.parse().unwrap() + }; + let ix_name = &ix.raw_method.sig.ident; + let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap(); + let anchor_ident = &ix.anchor_ident; + let name = &state.strct.ident; + let mod_name = &program.name; + + if state.is_zero_copy { + quote! { + #[inline(never)] + pub fn #private_ix_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#ix_params),* + ) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + if remaining_accounts.is_empty() { + return Err(ProgramError::Custom(1)); // todo + } + + let state_account = &remaining_accounts[0]; + let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from(&state_account)?; + remaining_accounts = &remaining_accounts[1..]; + + // Deserialize the program's execution context. + let mut accounts = #anchor_ident::try_accounts( + program_id, + &mut remaining_accounts, + )?; + let ctx = Context::new(program_id, &mut accounts, remaining_accounts); + // Execute user defined function. + { + let mut state = loader.load_mut()?; + state.#ix_name( + ctx, + #(#ix_arg_names),* + )?; + } + // Serialize the state and save it to storage. + accounts.exit(program_id)?; + loader.exit(program_id)?; + + Ok(()) + } + } + } else { + quote! { + #[inline(never)] + pub fn #private_ix_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#ix_params),* + ) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + if remaining_accounts.is_empty() { + return Err(ProgramError::Custom(1)); // todo + } + + // Deserialize the program state account. + let state_account = &remaining_accounts[0]; + let mut state: #state_ty = { + let data = state_account.try_borrow_data()?; + let mut sliced: &[u8] = &data; + anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)? + }; + + remaining_accounts = &remaining_accounts[1..]; + + // Deserialize the program's execution context. + let mut accounts = #anchor_ident::try_accounts( + program_id, + &mut remaining_accounts, + )?; + let ctx = Context::new(program_id, &mut accounts, remaining_accounts); + + // Execute user defined function. + state.#ix_name( + ctx, + #(#ix_arg_names),* + )?; + + // Serialize the state and save it to storage. + accounts.exit(program_id)?; + let mut data = state_account.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + state.try_serialize(&mut cursor)?; + + Ok(()) + } + } + } + }) + .collect() + }) + .unwrap_or_default(), + }; + + // State trait handlers. + let non_inlined_state_trait_handlers: Vec = match &program.state { + None => Vec::new(), + Some(state) => state + .interfaces + .as_ref() + .map(|interfaces| { + interfaces + .iter() + .flat_map(|iface: &crate::StateInterface| { + iface + .methods + .iter() + .map(|ix| { + let ix_params: Vec<_> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); + let ix_arg_names: Vec<&syn::Ident> = + ix.args.iter().map(|arg| &arg.name).collect(); + let private_ix_name: proc_macro2::TokenStream = { + let n = format!("__{}_{}", iface.trait_name, &ix.raw_method.sig.ident.to_string()); + n.parse().unwrap() + }; + let ix_name = &ix.raw_method.sig.ident; + let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap(); + let anchor_ident = &ix.anchor_ident; + + if state.is_zero_copy { + // Easy to implement. Just need to write a test. + // Feel free to open a PR. + panic!("Trait implementations not yet implemented for zero copy state structs. Please file an issue."); + } + + if ix.has_receiver { + quote! { + #[inline(never)] + pub fn #private_ix_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#ix_params),* + ) -> ProgramResult { + + let mut remaining_accounts: &[AccountInfo] = accounts; + if remaining_accounts.is_empty() { + return Err(ProgramError::Custom(1)); // todo + } + + // Deserialize the program state account. + let state_account = &remaining_accounts[0]; + let mut state: #state_ty = { + let data = state_account.try_borrow_data()?; + let mut sliced: &[u8] = &data; + anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)? + }; + + remaining_accounts = &remaining_accounts[1..]; + + // Deserialize the program's execution context. + let mut accounts = #anchor_ident::try_accounts( + program_id, + &mut remaining_accounts, + )?; + let ctx = Context::new(program_id, &mut accounts, remaining_accounts); + + // Execute user defined function. + state.#ix_name( + ctx, + #(#ix_arg_names),* + )?; + + // Serialize the state and save it to storage. + accounts.exit(program_id)?; + let mut data = state_account.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + state.try_serialize(&mut cursor)?; + + Ok(()) + } + } + } else { + let state_name: proc_macro2::TokenStream = state.name.parse().unwrap(); + quote! { + #[inline(never)] + pub fn #private_ix_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#ix_params),* + ) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + let mut accounts = #anchor_ident::try_accounts( + program_id, + &mut remaining_accounts, + )?; + #state_name::#ix_name( + Context::new(program_id, &mut accounts, remaining_accounts), + #(#ix_arg_names),* + )?; + accounts.exit(program_id) + } + } + } + }) + .collect::>() + }) + .collect() + }) + .unwrap_or_default(), + }; + let non_inlined_handlers: Vec = program + .ixs + .iter() + .map(|ix| { + let ix_params: Vec<_> = ix.args.iter().map(|arg| &arg.raw_arg).collect(); + let ix_arg_names: Vec<&syn::Ident> = ix.args.iter().map(|arg| &arg.name).collect(); + let ix_name = &ix.raw_method.sig.ident; + let anchor = &ix.anchor_ident; + + quote! { + #[inline(never)] + pub fn #ix_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#ix_params),* + ) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + let mut accounts = #anchor::try_accounts(program_id, &mut remaining_accounts)?; + #program_name::#ix_name( + Context::new(program_id, &mut accounts, remaining_accounts), + #(#ix_arg_names),* + )?; + accounts.exit(program_id) + } + } + }) + .collect(); + + quote! { + /// Create a private module to not clutter the program's namespace. + /// Defines an entrypoint for each individual instruction handler + /// wrapper. + mod __private { + use super::*; + /// __idl mod defines handlers for injected Anchor IDL instructions. + pub mod __idl { + use super::*; + + #non_inlined_idl + } + + /// __state mod defines wrapped handlers for state instructions. + pub mod __state { + use super::*; + + #non_inlined_ctor + #(#non_inlined_state_handlers)* + } + + /// __interface mod defines wrapped handlers for `#[interface]` trait + /// implementations. + pub mod __interface { + use super::*; + + #(#non_inlined_state_trait_handlers)* + } + + /// __global mod defines wrapped handlers for global instructions. + pub mod __global { + use super::*; + + #(#non_inlined_handlers)* + } + } + } +} diff --git a/lang/syn/src/codegen/program/instruction.rs b/lang/syn/src/codegen/program/instruction.rs new file mode 100644 index 0000000000..02068d932c --- /dev/null +++ b/lang/syn/src/codegen/program/instruction.rs @@ -0,0 +1,194 @@ +use crate::codegen::program::common::*; +use crate::parser; +use crate::Program; +use heck::CamelCase; +use quote::quote; + +pub fn generate(program: &Program) -> proc_macro2::TokenStream { + let ctor_variant = match &program.state { + None => quote! {}, + Some(state) => { + let ctor_args: Vec = generate_ctor_typed_args(state) + .iter() + .map(|arg| { + format!("pub {}", parser::tts_to_string(&arg)) + .parse() + .unwrap() + }) + .collect(); + let strct = { + if ctor_args.is_empty() { + quote! { + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct New; + } + } else { + quote! { + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct New { + #(#ctor_args),* + } + } + } + }; + let sighash_arr = sighash_ctor(); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + quote! { + /// Instruction arguments to the `#[state]`'s `new` + /// constructor. + #strct + + impl anchor_lang::InstructionData for New { + fn data(&self) -> Vec { + let mut d = #sighash_tts.to_vec(); + d.append(&mut self.try_to_vec().expect("Should always serialize")); + d + } + } + } + } + }; + let state_method_variants: Vec = match &program.state { + None => vec![], + Some(state) => state + .impl_block_and_methods + .as_ref() + .map(|(_impl_block, methods)| { + methods + .iter() + .map(|method| { + let ix_name_camel: proc_macro2::TokenStream = method + .raw_method + .sig + .ident + .to_string() + .to_camel_case() + .parse() + .unwrap(); + let raw_args: Vec = method + .args + .iter() + .map(|arg| { + format!("pub {}", parser::tts_to_string(&arg.raw_arg)) + .parse() + .unwrap() + }) + .collect(); + + let ix_data_trait = { + let name = method.raw_method.sig.ident.to_string(); + let sighash_arr = sighash(SIGHASH_STATE_NAMESPACE, &name); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + quote! { + impl anchor_lang::InstructionData for #ix_name_camel { + fn data(&self) -> Vec { + let mut d = #sighash_tts.to_vec(); + d.append(&mut self.try_to_vec().expect("Should always serialize")); + d + } + } + } + }; + + // If no args, output a "unit" variant instead of a struct variant. + if method.args.is_empty() { + quote! { + /// Anchor generated instruction. + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct #ix_name_camel; + + #ix_data_trait + } + } else { + quote! { + /// Anchor generated instruction. + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct #ix_name_camel { + #(#raw_args),* + } + + #ix_data_trait + } + } + }) + .collect() + }) + .unwrap_or_default(), + }; + let variants: Vec = program + .ixs + .iter() + .map(|ix| { + let name = &ix.raw_method.sig.ident.to_string(); + let ix_name_camel = + proc_macro2::Ident::new(&name.to_camel_case(), ix.raw_method.sig.ident.span()); + let raw_args: Vec = ix + .args + .iter() + .map(|arg| { + format!("pub {}", parser::tts_to_string(&arg.raw_arg)) + .parse() + .unwrap() + }) + .collect(); + let ix_data_trait = { + let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + quote! { + impl anchor_lang::InstructionData for #ix_name_camel { + fn data(&self) -> Vec { + let mut d = #sighash_tts.to_vec(); + d.append(&mut self.try_to_vec().expect("Should always serialize")); + d + } + } + } + }; + // If no args, output a "unit" variant instead of a struct variant. + if ix.args.is_empty() { + quote! { + /// Instruction. + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct #ix_name_camel; + + #ix_data_trait + } + } else { + quote! { + /// Instruction. + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct #ix_name_camel { + #(#raw_args),* + } + + #ix_data_trait + } + } + }) + .collect(); + + quote! { + /// An Anchor generated module containing the program's set of + /// instructions, where each method handler in the `#[program]` mod is + /// associated with a struct defining the input arguments to the + /// method. These should be used directly, when one wants to serialize + /// Anchor instruction data, for example, when speciying + /// instructions on a client. + pub mod instruction { + use super::*; + + /// Instruction struct definitions for `#[state]` methods. + pub mod state { + use super::*; + + #ctor_variant + #(#state_method_variants)* + } + + #(#variants)* + } + } +} diff --git a/lang/syn/src/codegen/program/mod.rs b/lang/syn/src/codegen/program/mod.rs new file mode 100644 index 0000000000..48fae12e4a --- /dev/null +++ b/lang/syn/src/codegen/program/mod.rs @@ -0,0 +1,35 @@ +use crate::Program; +use quote::quote; + +mod accounts; +pub mod common; +mod cpi; +mod dispatch; +mod entry; +mod handlers; +mod instruction; + +pub fn generate(program: &Program) -> proc_macro2::TokenStream { + let mod_name = &program.name; + + let entry = entry::generate(program); + let dispatch = dispatch::generate(program); + let handlers = handlers::generate(program); + let user_defined_program = &program.program_mod; + let instruction = instruction::generate(program); + let cpi = cpi::generate(program); + let accounts = accounts::generate(program); + + quote! { + // TODO: remove once we allow segmented paths in `Accounts` structs. + use #mod_name::*; + + #entry + #dispatch + #handlers + #user_defined_program + #instruction + #cpi + #accounts + } +} diff --git a/lang/syn/src/parser/file.rs b/lang/syn/src/idl/file.rs similarity index 91% rename from lang/syn/src/parser/file.rs rename to lang/syn/src/idl/file.rs index b5562024d9..1e9b8e827f 100644 --- a/lang/syn/src/parser/file.rs +++ b/lang/syn/src/idl/file.rs @@ -1,6 +1,6 @@ use crate::idl::*; use crate::parser::{self, accounts, error, program}; -use crate::{AccountsStruct, StateIx}; +use crate::{AccountField, AccountsStruct, StateIx}; use anyhow::Result; use heck::MixedCase; use quote::ToTokens; @@ -21,7 +21,7 @@ pub fn parse(filename: impl AsRef) -> Result { let f = syn::parse_file(&src).expect("Unable to parse file"); - let p = program::parse(parse_program_mod(&f)); + let p = program::parse(parse_program_mod(&f))?; let accs = parse_account_derives(&f); @@ -52,7 +52,7 @@ pub fn parse(filename: impl AsRef) -> Result { .collect::>(); let accounts_strct = accs.get(&method.anchor_ident.to_string()).unwrap(); - let accounts = accounts_strct.idl_accounts(&accs); + let accounts = idl_accounts(accounts_strct, &accs); IdlStateMethod { name, args, @@ -91,7 +91,7 @@ pub fn parse(filename: impl AsRef) -> Result { }) .collect(); let accounts_strct = accs.get(&anchor_ident.to_string()).unwrap(); - let accounts = accounts_strct.idl_accounts(&accs); + let accounts = idl_accounts(&accounts_strct, &accs); IdlStateMethod { name, args, @@ -159,7 +159,7 @@ pub fn parse(filename: impl AsRef) -> Result { .collect::>(); // todo: don't unwrap let accounts_strct = accs.get(&ix.anchor_ident.to_string()).unwrap(); - let accounts = accounts_strct.idl_accounts(&accs); + let accounts = idl_accounts(accounts_strct, &accs); IdlIx { name: ix.ident.to_string().to_mixed_case(), accounts, @@ -345,7 +345,7 @@ fn parse_account_derives(f: &syn::File) -> HashMap { syn::Item::Struct(i_strct) => { for attr in &i_strct.attrs { if attr.tokens.to_string().contains(DERIVE_NAME) { - let strct = accounts::parse(i_strct); + let strct = accounts::parse(i_strct).expect("Code not parseable"); return Some((strct.ident.to_string(), strct)); } } @@ -439,3 +439,30 @@ fn to_idl_type(f: &syn::Field) -> IdlType { f.ty.to_tokens(&mut tts); tts.to_string().parse().unwrap() } + +fn idl_accounts( + accounts: &AccountsStruct, + global_accs: &HashMap, +) -> Vec { + accounts + .fields + .iter() + .map(|acc: &AccountField| match acc { + AccountField::CompositeField(comp_f) => { + let accs_strct = global_accs + .get(&comp_f.symbol) + .expect("Could not resolve Accounts symbol"); + let accounts = idl_accounts(accs_strct, global_accs); + IdlAccountItem::IdlAccounts(IdlAccounts { + name: comp_f.ident.to_string().to_mixed_case(), + accounts, + }) + } + AccountField::Field(acc) => IdlAccountItem::IdlAccount(IdlAccount { + name: acc.ident.to_string().to_mixed_case(), + is_mut: acc.constraints.is_mutable(), + is_signer: acc.constraints.is_signer(), + }), + }) + .collect::>() +} diff --git a/lang/syn/src/idl.rs b/lang/syn/src/idl/mod.rs similarity index 99% rename from lang/syn/src/idl.rs rename to lang/syn/src/idl/mod.rs index 2b48bd0e1a..6a519435ca 100644 --- a/lang/syn/src/idl.rs +++ b/lang/syn/src/idl/mod.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +pub mod file; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Idl { pub version: String, diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index ed5623fd43..25d74f8b95 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -1,12 +1,17 @@ -//! DSL syntax tokens. - -#[cfg(feature = "idl")] -use crate::idl::{IdlAccount, IdlAccountItem, IdlAccounts}; -use anyhow::Result; -#[cfg(feature = "idl")] -use heck::MixedCase; -use quote::quote; -use std::collections::HashMap; +use codegen::accounts as accounts_codegen; +use codegen::program as program_codegen; +use parser::accounts as accounts_parser; +use parser::program as program_parser; +use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; +use std::ops::Deref; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::{ + Expr, Generics, Ident, ImplItemMethod, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, LitInt, + LitStr, PatType, Token, +}; pub mod codegen; #[cfg(feature = "hash")] @@ -21,27 +26,45 @@ pub mod parser; pub struct Program { pub state: Option, pub ixs: Vec, - pub name: syn::Ident, - pub program_mod: syn::ItemMod, + pub name: Ident, + pub program_mod: ItemMod, +} + +impl Parse for Program { + fn parse(input: ParseStream) -> ParseResult { + let program_mod = ::parse(input)?; + program_parser::parse(program_mod) + } +} + +impl From<&Program> for TokenStream { + fn from(program: &Program) -> Self { + program_codegen::generate(program) + } +} + +impl ToTokens for Program { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend::(self.into()); + } } -// State struct singleton. #[derive(Debug)] pub struct State { pub name: String, - pub strct: syn::ItemStruct, - pub ctor_and_anchor: Option<(syn::ImplItemMethod, syn::Ident)>, - pub impl_block_and_methods: Option<(syn::ItemImpl, Vec)>, + pub strct: ItemStruct, + pub ctor_and_anchor: Option<(ImplItemMethod, Ident)>, + pub impl_block_and_methods: Option<(ItemImpl, Vec)>, pub interfaces: Option>, pub is_zero_copy: bool, } #[derive(Debug)] pub struct StateIx { - pub raw_method: syn::ImplItemMethod, - pub ident: syn::Ident, + pub raw_method: ImplItemMethod, + pub ident: Ident, pub args: Vec, - pub anchor_ident: syn::Ident, + pub anchor_ident: Ident, // True if there exists a &self on the method. pub has_receiver: bool, } @@ -54,31 +77,50 @@ pub struct StateInterface { #[derive(Debug)] pub struct Ix { - pub raw_method: syn::ItemFn, - pub ident: syn::Ident, + pub raw_method: ItemFn, + pub ident: Ident, pub args: Vec, // The ident for the struct deriving Accounts. - pub anchor_ident: syn::Ident, + pub anchor_ident: Ident, } #[derive(Debug)] pub struct IxArg { - pub name: proc_macro2::Ident, - pub raw_arg: syn::PatType, + pub name: Ident, + pub raw_arg: PatType, } #[derive(Debug)] pub struct AccountsStruct { // Name of the accounts struct. - pub ident: syn::Ident, + pub ident: Ident, // Generics + lifetimes on the accounts struct. - pub generics: syn::Generics, + pub generics: Generics, // Fields on the accounts struct. pub fields: Vec, } +impl Parse for AccountsStruct { + fn parse(input: ParseStream) -> ParseResult { + let strct = ::parse(input)?; + accounts_parser::parse(&strct) + } +} + +impl From<&AccountsStruct> for TokenStream { + fn from(accounts: &AccountsStruct) -> Self { + accounts_codegen::generate(accounts) + } +} + +impl ToTokens for AccountsStruct { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend::(self.into()); + } +} + impl AccountsStruct { - pub fn new(strct: syn::ItemStruct, fields: Vec) -> Self { + pub fn new(strct: ItemStruct, fields: Vec) -> Self { let ident = strct.ident.clone(); let generics = strct.generics; Self { @@ -87,159 +129,27 @@ impl AccountsStruct { fields, } } - - // Returns all program owned accounts in the Accounts struct. - // - // `global_accs` is given to "link" account types that are embedded - // in each other. - pub fn account_tys( - &self, - global_accs: &HashMap, - ) -> Result> { - let mut tys = vec![]; - for f in &self.fields { - match f { - AccountField::Field(f) => { - if let Ty::ProgramAccount(pty) = &f.ty { - tys.push(pty.account_ident.to_string()); - } - } - AccountField::AccountsStruct(comp_f) => { - let accs = global_accs.get(&comp_f.symbol).ok_or_else(|| { - anyhow::format_err!("Invalid account type: {}", comp_f.symbol) - })?; - tys.extend(accs.account_tys(global_accs)?); - } - } - } - Ok(tys) - } - - #[cfg(feature = "idl")] - pub fn idl_accounts( - &self, - global_accs: &HashMap, - ) -> Vec { - self.fields - .iter() - .map(|acc: &AccountField| match acc { - AccountField::AccountsStruct(comp_f) => { - let accs_strct = global_accs - .get(&comp_f.symbol) - .expect("Could not reslve Accounts symbol"); - let accounts = accs_strct.idl_accounts(global_accs); - IdlAccountItem::IdlAccounts(IdlAccounts { - name: comp_f.ident.to_string().to_mixed_case(), - accounts, - }) - } - AccountField::Field(acc) => IdlAccountItem::IdlAccount(IdlAccount { - name: acc.ident.to_string().to_mixed_case(), - is_mut: acc.is_mut, - is_signer: acc.is_signer, - }), - }) - .collect::>() - } } #[derive(Debug)] pub enum AccountField { - // Use a `String` instead of the `AccountsStruct` because all - // accounts structs aren't visible to a single derive macro. - // - // When we need the global context, we fill in the String with the - // appropriate values. See, `account_tys` as an example. - AccountsStruct(CompositeField), // Composite - Field(Field), // Primitive + Field(Field), + CompositeField(CompositeField), } #[derive(Debug)] -pub struct CompositeField { - pub ident: syn::Ident, - pub symbol: String, - pub constraints: Vec, - pub raw_field: syn::Field, +pub struct Field { + pub ident: Ident, + pub constraints: ConstraintGroup, + pub ty: Ty, } -// An account in the accounts struct. #[derive(Debug)] -pub struct Field { - pub ident: syn::Ident, - pub ty: Ty, - pub constraints: Vec, - pub is_mut: bool, - pub is_signer: bool, - pub is_init: bool, - // TODO: move associated out of the constraints and put into tis own - // field + struct. - // Used by the associated attribute only. - pub payer: Option, - // Used by the associated attribute only. - pub space: Option, - // Used by the associated attribute only. - pub associated_seeds: Vec, -} - -impl Field { - pub fn typed_ident(&self) -> proc_macro2::TokenStream { - let name = &self.ident; - - let ty = match &self.ty { - Ty::AccountInfo => quote! { AccountInfo }, - Ty::ProgramState(ty) => { - let account = &ty.account_ident; - quote! { - ProgramState<#account> - } - } - Ty::CpiState(ty) => { - let account = &ty.account_ident; - quote! { - CpiState<#account> - } - } - Ty::ProgramAccount(ty) => { - let account = &ty.account_ident; - quote! { - ProgramAccount<#account> - } - } - Ty::Loader(ty) => { - let account = &ty.account_ident; - quote! { - Loader<#account> - } - } - Ty::CpiAccount(ty) => { - let account = &ty.account_ident; - quote! { - CpiAccount<#account> - } - } - Ty::Sysvar(ty) => { - let account = match ty { - SysvarTy::Clock => quote! {Clock}, - SysvarTy::Rent => quote! {Rent}, - SysvarTy::EpochSchedule => quote! {EpochSchedule}, - SysvarTy::Fees => quote! {Fees}, - SysvarTy::RecentBlockhashes => quote! {RecentBlockhashes}, - SysvarTy::SlotHashes => quote! {SlotHashes}, - SysvarTy::SlotHistory => quote! {SlotHistory}, - SysvarTy::StakeHistory => quote! {StakeHistory}, - SysvarTy::Instructions => quote! {Instructions}, - SysvarTy::Rewards => quote! {Rewards}, - }; - quote! { - Sysvar<#account> - } - } - }; - - quote! { - #name: #ty - } - } +pub struct CompositeField { + pub ident: Ident, + pub constraints: ConstraintGroup, + pub symbol: String, + pub raw_field: syn::Field, } // A type of an account field. @@ -270,100 +180,228 @@ pub enum SysvarTy { #[derive(Debug, PartialEq)] pub struct ProgramStateTy { - pub account_ident: syn::Ident, + pub account_ident: Ident, } #[derive(Debug, PartialEq)] pub struct CpiStateTy { - pub account_ident: syn::Ident, + pub account_ident: Ident, } #[derive(Debug, PartialEq)] pub struct ProgramAccountTy { // The struct type of the account. - pub account_ident: syn::Ident, + pub account_ident: Ident, } #[derive(Debug, PartialEq)] pub struct CpiAccountTy { // The struct type of the account. - pub account_ident: syn::Ident, + pub account_ident: Ident, } #[derive(Debug, PartialEq)] pub struct LoaderTy { // The struct type of the account. - pub account_ident: syn::Ident, + pub account_ident: Ident, +} + +#[derive(Debug)] +pub struct Error { + pub name: String, + pub raw_enum: ItemEnum, + pub ident: Ident, + pub codes: Vec, } -// An access control constraint for an account. +#[derive(Debug)] +pub struct ErrorCode { + pub id: u32, + pub ident: Ident, + pub msg: Option, +} + +// All well formed constraints on a single `Accounts` field. +#[derive(Debug, Default, Clone)] +pub struct ConstraintGroup { + init: Option, + mutable: Option, + signer: Option, + owner: Option, + rent_exempt: Option, + seeds: Option, + executable: Option, + state: Option, + associated: Option, + belongs_to: Vec, + literal: Vec, + raw: Vec, +} + +impl ConstraintGroup { + pub fn is_init(&self) -> bool { + self.init.is_some() + } + + pub fn is_mutable(&self) -> bool { + self.mutable.is_some() + } + + pub fn is_signer(&self) -> bool { + self.signer.is_some() + } +} + +// A single account constraint *after* merging all tokens into a well formed +// constraint. Some constraints like "associated" are defined by multiple +// tokens, so a merging phase is required. #[derive(Debug)] pub enum Constraint { + Init(ConstraintInit), + Mut(ConstraintMut), Signer(ConstraintSigner), BelongsTo(ConstraintBelongsTo), Literal(ConstraintLiteral), + Raw(ConstraintRaw), Owner(ConstraintOwner), RentExempt(ConstraintRentExempt), Seeds(ConstraintSeeds), Executable(ConstraintExecutable), State(ConstraintState), - Associated(ConstraintAssociated), + AssociatedGroup(ConstraintAssociatedGroup), } +// Constraint token is a single keyword in a `#[account()]` attribute. #[derive(Debug)] -pub struct ConstraintBelongsTo { - pub join_target: proc_macro2::Ident, +pub enum ConstraintToken { + Init(Context), + Mut(Context), + Signer(Context), + BelongsTo(Context), + Literal(Context), + Raw(Context), + Owner(Context), + RentExempt(Context), + Seeds(Context), + Executable(Context), + State(Context), + AssociatedGroup(ConstraintAssociatedGroup), + Associated(Context), + AssociatedPayer(Context), + AssociatedSpace(Context), + AssociatedWith(Context), } -#[derive(Debug)] +impl Parse for ConstraintToken { + fn parse(stream: ParseStream) -> ParseResult { + accounts_parser::constraints::parse_token(stream) + } +} + +#[derive(Debug, Clone)] +pub struct ConstraintInit {} + +#[derive(Debug, Clone)] +pub struct ConstraintMut {} + +#[derive(Debug, Clone)] pub struct ConstraintSigner {} -#[derive(Debug)] +#[derive(Debug, Clone)] +pub struct ConstraintBelongsTo { + pub join_target: Ident, +} + +#[derive(Debug, Clone)] pub struct ConstraintLiteral { - pub tokens: proc_macro2::TokenStream, + pub lit: LitStr, } -#[derive(Debug)] +#[derive(Debug, Clone)] +pub struct ConstraintRaw { + pub raw: Expr, +} + +#[derive(Debug, Clone)] pub struct ConstraintOwner { - pub owner_target: proc_macro2::Ident, + pub owner_target: Ident, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ConstraintRentExempt { Enforce, Skip, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConstraintSeeds { - pub seeds: proc_macro2::Group, + pub seeds: Punctuated, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConstraintExecutable {} -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConstraintState { - pub program_target: proc_macro2::Ident, + pub program_target: Ident, +} + +#[derive(Debug, Clone)] +pub struct ConstraintAssociatedGroup { + pub is_init: bool, + pub associated_target: Ident, + pub associated_seeds: Vec, + pub payer: Option, + pub space: Option, } #[derive(Debug)] pub struct ConstraintAssociated { - pub associated_target: proc_macro2::Ident, - pub is_init: bool, + pub target: Ident, } #[derive(Debug)] -pub struct Error { - pub name: String, - pub raw_enum: syn::ItemEnum, - pub ident: syn::Ident, - pub codes: Vec, +pub struct ConstraintAssociatedPayer { + pub target: Ident, } #[derive(Debug)] -pub struct ErrorCode { - pub id: u32, - pub ident: syn::Ident, - pub msg: Option, +pub struct ConstraintAssociatedWith { + pub target: Ident, +} + +#[derive(Debug)] +pub struct ConstraintAssociatedSpace { + pub space: LitInt, +} + +// Syntaxt context object for preserving metadata about the inner item. +#[derive(Debug, Clone)] +pub struct Context { + span: Span, + inner: T, +} + +impl Context { + pub fn new(span: Span, inner: T) -> Self { + Self { span, inner } + } + + pub fn into_inner(self) -> T { + self.inner + } +} + +impl Deref for Context { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Spanned for Context { + fn span(&self) -> Span { + self.span + } } diff --git a/lang/syn/src/parser/accounts.rs b/lang/syn/src/parser/accounts.rs deleted file mode 100644 index 17aec3e879..0000000000 --- a/lang/syn/src/parser/accounts.rs +++ /dev/null @@ -1,429 +0,0 @@ -use crate::{ - AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAssociated, - ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, ConstraintOwner, - ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, CpiAccountTy, - CpiStateTy, Field, LoaderTy, ProgramAccountTy, ProgramStateTy, SysvarTy, Ty, -}; - -pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct { - let fields = match &strct.fields { - syn::Fields::Named(fields) => fields.named.iter().map(parse_account_field).collect(), - _ => panic!("invalid input"), - }; - AccountsStruct::new(strct.clone(), fields) -} - -fn parse_account_field(f: &syn::Field) -> AccountField { - let anchor_attr = parse_account_attr(f); - parse_field(f, anchor_attr) -} - -fn parse_account_attr(f: &syn::Field) -> Option<&syn::Attribute> { - let anchor_attrs: Vec<&syn::Attribute> = f - .attrs - .iter() - .filter(|attr| { - if attr.path.segments.len() != 1 { - return false; - } - if attr.path.segments[0].ident != "account" { - return false; - } - true - }) - .collect(); - match anchor_attrs.len() { - 0 => None, - 1 => Some(anchor_attrs[0]), - _ => panic!("Invalid syntax: please specify one account attribute."), - } -} - -fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField { - let ident = f.ident.clone().unwrap(); - let (constraints, is_mut, is_signer, is_init, payer, space, associated_seeds) = match anchor { - None => (vec![], false, false, false, None, None, Vec::new()), - Some(anchor) => parse_constraints(anchor), - }; - match is_field_primitive(f) { - true => { - let ty = parse_ty(f); - AccountField::Field(Field { - ident, - ty, - constraints, - is_mut, - is_signer, - is_init, - payer, - space, - associated_seeds, - }) - } - false => AccountField::AccountsStruct(CompositeField { - ident, - symbol: ident_string(f), - constraints, - raw_field: f.clone(), - }), - } -} - -fn is_field_primitive(f: &syn::Field) -> bool { - match ident_string(f).as_str() { - "ProgramState" | "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" - | "CpiState" | "Loader" => true, - _ => false, - } -} - -fn parse_ty(f: &syn::Field) -> Ty { - let path = match &f.ty { - syn::Type::Path(ty_path) => ty_path.path.clone(), - _ => panic!("invalid account syntax"), - }; - match ident_string(f).as_str() { - "ProgramState" => Ty::ProgramState(parse_program_state(&path)), - "CpiState" => Ty::CpiState(parse_cpi_state(&path)), - "ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)), - "CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)), - "Sysvar" => Ty::Sysvar(parse_sysvar(&path)), - "AccountInfo" => Ty::AccountInfo, - "Loader" => Ty::Loader(parse_program_account_zero_copy(&path)), - _ => panic!("invalid account type"), - } -} - -fn ident_string(f: &syn::Field) -> String { - let path = match &f.ty { - syn::Type::Path(ty_path) => ty_path.path.clone(), - _ => panic!("invalid account syntax"), - }; - // TODO: allow segmented paths. - assert!(path.segments.len() == 1); - let segments = &path.segments[0]; - segments.ident.to_string() -} - -fn parse_program_state(path: &syn::Path) -> ProgramStateTy { - let account_ident = parse_account(&path); - ProgramStateTy { account_ident } -} - -fn parse_cpi_state(path: &syn::Path) -> CpiStateTy { - let account_ident = parse_account(&path); - CpiStateTy { account_ident } -} - -fn parse_cpi_account(path: &syn::Path) -> CpiAccountTy { - let account_ident = parse_account(path); - CpiAccountTy { account_ident } -} - -fn parse_program_account(path: &syn::Path) -> ProgramAccountTy { - let account_ident = parse_account(path); - ProgramAccountTy { account_ident } -} - -fn parse_program_account_zero_copy(path: &syn::Path) -> LoaderTy { - let account_ident = parse_account(path); - LoaderTy { account_ident } -} - -fn parse_account(path: &syn::Path) -> syn::Ident { - let segments = &path.segments[0]; - match &segments.arguments { - syn::PathArguments::AngleBracketed(args) => { - // Expected: <'info, MyType>. - assert!(args.args.len() == 2); - match &args.args[1] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { - // TODO: allow segmented paths. - assert!(ty_path.path.segments.len() == 1); - let path_segment = &ty_path.path.segments[0]; - path_segment.ident.clone() - } - _ => panic!("Invalid ProgramAccount"), - } - } - _ => panic!("Invalid ProgramAccount"), - } -} - -fn parse_sysvar(path: &syn::Path) -> SysvarTy { - let segments = &path.segments[0]; - let account_ident = match &segments.arguments { - syn::PathArguments::AngleBracketed(args) => { - // Expected: <'info, MyType>. - assert!(args.args.len() == 2); - match &args.args[1] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { - // TODO: allow segmented paths. - assert!(ty_path.path.segments.len() == 1); - let path_segment = &ty_path.path.segments[0]; - path_segment.ident.clone() - } - _ => panic!("Invalid Sysvar"), - } - } - _ => panic!("Invalid Sysvar"), - }; - match account_ident.to_string().as_str() { - "Clock" => SysvarTy::Clock, - "Rent" => SysvarTy::Rent, - "EpochSchedule" => SysvarTy::EpochSchedule, - "Fees" => SysvarTy::Fees, - "RecentBlockhashes" => SysvarTy::RecentBlockhashes, - "SlotHashes" => SysvarTy::SlotHashes, - "SlotHistory" => SysvarTy::SlotHistory, - "StakeHistory" => SysvarTy::StakeHistory, - "Instructions" => SysvarTy::Instructions, - "Rewards" => SysvarTy::Rewards, - _ => panic!("Invalid Sysvar"), - } -} - -fn parse_constraints( - anchor: &syn::Attribute, -) -> ( - Vec, - bool, - bool, - bool, - Option, - Option, - Vec, -) { - let mut tts = anchor.tokens.clone().into_iter(); - let g_stream = match tts.next().expect("Must have a token group") { - proc_macro2::TokenTree::Group(g) => g.stream(), - _ => panic!("Invalid syntax"), - }; - - let mut is_init = false; - let mut is_mut = false; - let mut is_signer = false; - let mut constraints = vec![]; - let mut is_rent_exempt = None; - let mut payer = None; - let mut space = None; - let mut is_associated = false; - let mut associated_seeds = Vec::new(); - - let mut inner_tts = g_stream.into_iter(); - while let Some(token) = inner_tts.next() { - match token { - proc_macro2::TokenTree::Ident(ident) => match ident.to_string().as_str() { - "init" => { - is_init = true; - is_mut = true; - // If it's not specified, all program owned accounts default - // to being rent exempt. - if is_rent_exempt.is_none() { - is_rent_exempt = Some(true); - } - } - "mut" => { - is_mut = true; - } - "signer" => { - is_signer = true; - constraints.push(Constraint::Signer(ConstraintSigner {})); - } - "seeds" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let seeds = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Group(g) => g, - _ => panic!("invalid syntax"), - }; - constraints.push(Constraint::Seeds(ConstraintSeeds { seeds })) - } - "belongs_to" | "has_one" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let join_target = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }; - constraints.push(Constraint::BelongsTo(ConstraintBelongsTo { join_target })) - } - "owner" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let owner_target = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }; - constraints.push(Constraint::Owner(ConstraintOwner { owner_target })); - } - "rent_exempt" => { - match inner_tts.next() { - None => is_rent_exempt = Some(true), - Some(tkn) => { - match tkn { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let should_skip = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }; - match should_skip.to_string().as_str() { - "skip" => { - is_rent_exempt = Some(false); - }, - _ => panic!("invalid syntax: omit the rent_exempt attribute to enforce rent exemption"), - }; - } - }; - } - "executable" => { - constraints.push(Constraint::Executable(ConstraintExecutable {})); - } - "state" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let program_target = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }; - constraints.push(Constraint::State(ConstraintState { program_target })); - } - "associated" => { - is_associated = true; - is_mut = true; - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let associated_target = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }; - constraints.push(Constraint::Associated(ConstraintAssociated { - associated_target, - is_init, - })); - } - "with" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - associated_seeds.push(match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }); - } - "payer" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - let _payer = match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Ident(ident) => ident, - _ => panic!("invalid syntax"), - }; - payer = Some(_payer); - } - "space" => { - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Punct(punct) => { - assert!(punct.as_char() == '='); - punct - } - _ => panic!("invalid syntax"), - }; - match inner_tts.next().unwrap() { - proc_macro2::TokenTree::Literal(literal) => { - let tokens: proc_macro2::TokenStream = - literal.to_string().replace("\"", "").parse().unwrap(); - space = Some(tokens); - } - _ => panic!("invalid space"), - } - } - _ => { - panic!("invalid syntax"); - } - }, - proc_macro2::TokenTree::Punct(punct) => { - if punct.as_char() != ',' { - panic!("invalid syntax"); - } - } - proc_macro2::TokenTree::Literal(literal) => { - let tokens: proc_macro2::TokenStream = - literal.to_string().replace("\"", "").parse().unwrap(); - constraints.push(Constraint::Literal(ConstraintLiteral { tokens })); - } - _ => { - panic!("invalid syntax"); - } - } - } - - // If init, then tag the associated constraint as being part of init. - if is_init { - for c in &mut constraints { - if let Constraint::Associated(ConstraintAssociated { is_init, .. }) = c { - *is_init = true; - } - } - } - - // If `associated` is given, remove `init` since it's redundant. - if is_associated { - is_init = false; - } - - if let Some(is_re) = is_rent_exempt { - match is_re { - false => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)), - true => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Enforce)), - } - } - - ( - constraints, - is_mut, - is_signer, - is_init, - payer, - space, - associated_seeds, - ) -} diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs new file mode 100644 index 0000000000..20da4939be --- /dev/null +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -0,0 +1,385 @@ +use crate::{ + ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer, + ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintExecutable, + ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut, ConstraintOwner, + ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, + ConstraintToken, Context, +}; +use syn::ext::IdentExt; +use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::{bracketed, Expr, Ident, LitStr, Token}; + +pub fn parse(f: &syn::Field) -> ParseResult { + let mut constraints = ConstraintGroupBuilder::default(); + for attr in f.attrs.iter().filter(is_account) { + for c in attr.parse_args_with(Punctuated::::parse_terminated)? { + constraints.add(c)?; + } + } + constraints.build() +} + +pub fn is_account(attr: &&syn::Attribute) -> bool { + attr.path + .get_ident() + .map_or(false, |ident| ident == "account") +} + +// Parses a single constraint from a parse stream for `#[account()]`. +pub fn parse_token(stream: ParseStream) -> ParseResult { + let is_lit = stream.peek(LitStr); + if is_lit { + let lit: LitStr = stream.parse()?; + let c = ConstraintToken::Literal(Context::new(lit.span(), ConstraintLiteral { lit })); + return Ok(c); + } + + let ident = stream.call(Ident::parse_any)?; + let kw = ident.to_string(); + + let c = match kw.as_str() { + "init" => ConstraintToken::Init(Context::new(ident.span(), ConstraintInit {})), + "mut" => ConstraintToken::Mut(Context::new(ident.span(), ConstraintMut {})), + "signer" => ConstraintToken::Signer(Context::new(ident.span(), ConstraintSigner {})), + "executable" => { + ConstraintToken::Executable(Context::new(ident.span(), ConstraintExecutable {})) + } + _ => { + stream.parse::()?; + let span = ident.span().join(stream.span()).unwrap_or(ident.span()); + match kw.as_str() { + "belongs_to" | "has_one" => ConstraintToken::BelongsTo(Context::new( + span, + ConstraintBelongsTo { + join_target: stream.parse()?, + }, + )), + "owner" => ConstraintToken::Owner(Context::new( + span, + ConstraintOwner { + owner_target: stream.parse()?, + }, + )), + "rent_exempt" => ConstraintToken::RentExempt(Context::new( + span, + match stream.parse::()?.to_string().as_str() { + "skip" => ConstraintRentExempt::Skip, + "enforce" => ConstraintRentExempt::Enforce, + _ => { + return Err(ParseError::new( + span, + "rent_exempt must be either skip or enforce", + )) + } + }, + )), + "state" => ConstraintToken::State(Context::new( + span, + ConstraintState { + program_target: stream.parse()?, + }, + )), + "associated" => ConstraintToken::Associated(Context::new( + span, + ConstraintAssociated { + target: stream.parse()?, + }, + )), + "payer" => ConstraintToken::AssociatedPayer(Context::new( + span, + ConstraintAssociatedPayer { + target: stream.parse()?, + }, + )), + "with" => ConstraintToken::AssociatedWith(Context::new( + span, + ConstraintAssociatedWith { + target: stream.parse()?, + }, + )), + "space" => ConstraintToken::AssociatedSpace(Context::new( + span, + ConstraintAssociatedSpace { + space: stream.parse()?, + }, + )), + "seeds" => { + let seeds; + let bracket = bracketed!(seeds in stream); + ConstraintToken::Seeds(Context::new( + span.join(bracket.span).unwrap_or(span), + ConstraintSeeds { + seeds: seeds.parse_terminated(Expr::parse)?, + }, + )) + } + "constraint" => ConstraintToken::Raw(Context::new( + span, + ConstraintRaw { + raw: stream.parse()?, + }, + )), + _ => Err(ParseError::new(ident.span(), "Invalid attribute"))?, + } + } + }; + + Ok(c) +} + +#[derive(Default)] +pub struct ConstraintGroupBuilder { + pub init: Option>, + pub mutable: Option>, + pub signer: Option>, + pub belongs_to: Vec>, + pub literal: Vec>, + pub raw: Vec>, + pub owner: Option>, + pub rent_exempt: Option>, + pub seeds: Option>, + pub executable: Option>, + pub state: Option>, + pub associated: Option>, + pub associated_payer: Option>, + pub associated_space: Option>, + pub associated_with: Vec>, +} + +impl ConstraintGroupBuilder { + pub fn build(mut self) -> ParseResult { + // Init implies mutable and rent exempt. + if let Some(i) = &self.init { + match self.mutable { + Some(m) => { + return Err(ParseError::new( + m.span(), + "mut cannot be provided with init", + )) + } + None => self + .mutable + .replace(Context::new(i.span(), ConstraintMut {})), + }; + if self.rent_exempt.is_none() { + self.rent_exempt + .replace(Context::new(i.span(), ConstraintRentExempt::Enforce)); + } + } + + let ConstraintGroupBuilder { + init, + mutable, + signer, + belongs_to, + literal, + raw, + owner, + rent_exempt, + seeds, + executable, + state, + associated, + associated_payer, + associated_space, + associated_with, + } = self; + + // Converts Option> -> Option. + macro_rules! into_inner { + ($opt:ident) => { + $opt.map(|c| c.into_inner()) + }; + } + // Converts Vec> - Vec. + macro_rules! into_inner_vec { + ($opt:ident) => { + $opt.into_iter().map(|c| c.into_inner()).collect() + }; + } + + let is_init = init.is_some(); + Ok(ConstraintGroup { + init: into_inner!(init), + mutable: into_inner!(mutable), + signer: into_inner!(signer), + belongs_to: into_inner_vec!(belongs_to), + literal: into_inner_vec!(literal), + raw: into_inner_vec!(raw), + owner: into_inner!(owner), + rent_exempt: into_inner!(rent_exempt), + seeds: into_inner!(seeds), + executable: into_inner!(executable), + state: into_inner!(state), + associated: associated.map(|associated| ConstraintAssociatedGroup { + is_init, + associated_target: associated.target.clone(), + associated_seeds: associated_with.iter().map(|s| s.target.clone()).collect(), + payer: associated_payer.map(|p| p.target.clone()), + space: associated_space.map(|s| s.space.clone()), + }), + }) + } + + pub fn add(&mut self, c: ConstraintToken) -> ParseResult<()> { + match c { + ConstraintToken::Init(c) => self.add_init(c), + ConstraintToken::Mut(c) => self.add_mut(c), + ConstraintToken::Signer(c) => self.add_signer(c), + ConstraintToken::BelongsTo(c) => self.add_belongs_to(c), + ConstraintToken::Literal(c) => self.add_literal(c), + ConstraintToken::Raw(c) => self.add_raw(c), + ConstraintToken::Owner(c) => self.add_owner(c), + ConstraintToken::RentExempt(c) => self.add_rent_exempt(c), + ConstraintToken::Seeds(c) => self.add_seeds(c), + ConstraintToken::Executable(c) => self.add_executable(c), + ConstraintToken::State(c) => self.add_state(c), + ConstraintToken::Associated(c) => self.add_associated(c), + ConstraintToken::AssociatedPayer(c) => self.add_associated_payer(c), + ConstraintToken::AssociatedSpace(c) => self.add_associated_space(c), + ConstraintToken::AssociatedWith(c) => self.add_associated_with(c), + ConstraintToken::AssociatedGroup(_) => panic!("Invariant violation"), + } + } + + fn add_init(&mut self, c: Context) -> ParseResult<()> { + if self.init.is_some() { + return Err(ParseError::new(c.span(), "init already provided")); + } + self.init.replace(c); + Ok(()) + } + + fn add_mut(&mut self, c: Context) -> ParseResult<()> { + if self.mutable.is_some() { + return Err(ParseError::new(c.span(), "mut already provided")); + } + self.mutable.replace(c); + Ok(()) + } + + fn add_signer(&mut self, c: Context) -> ParseResult<()> { + if self.signer.is_some() { + return Err(ParseError::new(c.span(), "signer already provided")); + } + self.signer.replace(c); + Ok(()) + } + + fn add_belongs_to(&mut self, c: Context) -> ParseResult<()> { + if self + .belongs_to + .iter() + .filter(|item| item.join_target == c.join_target) + .count() + > 0 + { + return Err(ParseError::new( + c.span(), + "belongs_to target already provided", + )); + } + self.belongs_to.push(c); + Ok(()) + } + + fn add_literal(&mut self, c: Context) -> ParseResult<()> { + self.literal.push(c); + Ok(()) + } + + fn add_raw(&mut self, c: Context) -> ParseResult<()> { + self.raw.push(c); + Ok(()) + } + + fn add_owner(&mut self, c: Context) -> ParseResult<()> { + if self.owner.is_some() { + return Err(ParseError::new(c.span(), "owner already provided")); + } + self.owner.replace(c); + Ok(()) + } + + fn add_rent_exempt(&mut self, c: Context) -> ParseResult<()> { + if self.rent_exempt.is_some() { + return Err(ParseError::new(c.span(), "rent already provided")); + } + self.rent_exempt.replace(c); + Ok(()) + } + + fn add_seeds(&mut self, c: Context) -> ParseResult<()> { + if self.seeds.is_some() { + return Err(ParseError::new(c.span(), "seeds already provided")); + } + self.seeds.replace(c); + Ok(()) + } + + fn add_executable(&mut self, c: Context) -> ParseResult<()> { + if self.executable.is_some() { + return Err(ParseError::new(c.span(), "executable already provided")); + } + self.executable.replace(c); + Ok(()) + } + + fn add_state(&mut self, c: Context) -> ParseResult<()> { + if self.state.is_some() { + return Err(ParseError::new(c.span(), "state already provided")); + } + self.state.replace(c); + Ok(()) + } + + fn add_associated(&mut self, c: Context) -> ParseResult<()> { + if self.associated.is_some() { + return Err(ParseError::new(c.span(), "associated already provided")); + } + self.associated.replace(c); + Ok(()) + } + + fn add_associated_payer(&mut self, c: Context) -> ParseResult<()> { + if self.associated.is_none() { + return Err(ParseError::new( + c.span(), + "associated must be provided before payer", + )); + } + if self.associated_payer.is_some() { + return Err(ParseError::new(c.span(), "payer already provided")); + } + self.associated_payer.replace(c); + Ok(()) + } + + fn add_associated_space(&mut self, c: Context) -> ParseResult<()> { + if self.associated.is_none() { + return Err(ParseError::new( + c.span(), + "associated must be provided before space", + )); + } + if self.associated_space.is_some() { + return Err(ParseError::new(c.span(), "space already provided")); + } + self.associated_space.replace(c); + Ok(()) + } + + fn add_associated_with(&mut self, c: Context) -> ParseResult<()> { + if self.associated.is_none() { + return Err(ParseError::new( + c.span(), + "associated must be provided before with", + )); + } + self.associated_with.push(c); + Ok(()) + } +} diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs new file mode 100644 index 0000000000..dc156a68bd --- /dev/null +++ b/lang/syn/src/parser/accounts/mod.rs @@ -0,0 +1,218 @@ +use crate::{ + AccountField, AccountsStruct, CompositeField, CpiAccountTy, CpiStateTy, Field, LoaderTy, + ProgramAccountTy, ProgramStateTy, SysvarTy, Ty, +}; +use syn::parse::{Error as ParseError, Result as ParseResult}; +use syn::spanned::Spanned; + +pub mod constraints; + +pub fn parse(strct: &syn::ItemStruct) -> ParseResult { + let fields = match &strct.fields { + syn::Fields::Named(fields) => fields + .named + .iter() + .map(parse_account_field) + .collect::>>()?, + _ => { + return Err(ParseError::new_spanned( + &strct.fields, + "fields must be named", + )) + } + }; + Ok(AccountsStruct::new(strct.clone(), fields)) +} + +pub fn parse_account_field(f: &syn::Field) -> ParseResult { + let constraints = constraints::parse(f)?; + + let ident = f.ident.clone().unwrap(); + let account_field = match is_field_primitive(f)? { + true => { + let ty = parse_ty(f)?; + AccountField::Field(Field { + ident, + ty, + constraints, + }) + } + false => AccountField::CompositeField(CompositeField { + ident, + constraints, + symbol: ident_string(f)?, + raw_field: f.clone(), + }), + }; + Ok(account_field) +} + +fn is_field_primitive(f: &syn::Field) -> ParseResult { + let r = match ident_string(f)?.as_str() { + "ProgramState" | "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" + | "CpiState" | "Loader" => true, + _ => false, + }; + Ok(r) +} + +fn parse_ty(f: &syn::Field) -> ParseResult { + let path = match &f.ty { + syn::Type::Path(ty_path) => ty_path.path.clone(), + _ => return Err(ParseError::new(f.ty.span(), "invalid account type given")), + }; + let ty = match ident_string(f)?.as_str() { + "ProgramState" => Ty::ProgramState(parse_program_state(&path)?), + "CpiState" => Ty::CpiState(parse_cpi_state(&path)?), + "ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)?), + "CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)?), + "Sysvar" => Ty::Sysvar(parse_sysvar(&path)?), + "AccountInfo" => Ty::AccountInfo, + "Loader" => Ty::Loader(parse_program_account_zero_copy(&path)?), + _ => return Err(ParseError::new(f.ty.span(), "invalid account type given")), + }; + + Ok(ty) +} + +fn ident_string(f: &syn::Field) -> ParseResult { + let path = match &f.ty { + syn::Type::Path(ty_path) => ty_path.path.clone(), + _ => return Err(ParseError::new(f.ty.span(), "invalid type")), + }; + // TODO: allow segmented paths. + if path.segments.len() != 1 { + return Err(ParseError::new( + f.ty.span(), + "segmented paths are not currently allowed", + )); + } + + let segments = &path.segments[0]; + Ok(segments.ident.to_string()) +} + +fn parse_program_state(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(&path)?; + Ok(ProgramStateTy { account_ident }) +} + +fn parse_cpi_state(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(&path)?; + Ok(CpiStateTy { account_ident }) +} + +fn parse_cpi_account(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(path)?; + Ok(CpiAccountTy { account_ident }) +} + +fn parse_program_account(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(path)?; + Ok(ProgramAccountTy { account_ident }) +} + +fn parse_program_account_zero_copy(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(path)?; + Ok(LoaderTy { account_ident }) +} + +fn parse_account(path: &syn::Path) -> ParseResult { + let segments = &path.segments[0]; + match &segments.arguments { + syn::PathArguments::AngleBracketed(args) => { + // Expected: <'info, MyType>. + if args.args.len() != 2 { + return Err(ParseError::new( + args.args.span(), + "bracket arguments must be the lifetime and type", + )); + } + match &args.args[1] { + syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { + // TODO: allow segmented paths. + if ty_path.path.segments.len() != 1 { + return Err(ParseError::new( + ty_path.path.span(), + "segmented paths are not currently allowed", + )); + } + + let path_segment = &ty_path.path.segments[0]; + Ok(path_segment.ident.clone()) + } + _ => { + return Err(ParseError::new( + args.args[1].span(), + "first bracket argument must be a lifetime", + )) + } + } + } + _ => { + return Err(ParseError::new( + segments.arguments.span(), + "expected angle brackets with a lifetime and type", + )) + } + } +} + +fn parse_sysvar(path: &syn::Path) -> ParseResult { + let segments = &path.segments[0]; + let account_ident = match &segments.arguments { + syn::PathArguments::AngleBracketed(args) => { + // Expected: <'info, MyType>. + if args.args.len() != 2 { + return Err(ParseError::new( + args.args.span(), + "bracket arguments must be the lifetime and type", + )); + } + match &args.args[1] { + syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { + // TODO: allow segmented paths. + if ty_path.path.segments.len() != 1 { + return Err(ParseError::new( + ty_path.path.span(), + "segmented paths are not currently allowed", + )); + } + let path_segment = &ty_path.path.segments[0]; + path_segment.ident.clone() + } + _ => { + return Err(ParseError::new( + args.args[1].span(), + "first bracket argument must be a lifetime", + )) + } + } + } + _ => { + return Err(ParseError::new( + segments.arguments.span(), + "expected angle brackets with a lifetime and type", + )) + } + }; + let ty = match account_ident.to_string().as_str() { + "Clock" => SysvarTy::Clock, + "Rent" => SysvarTy::Rent, + "EpochSchedule" => SysvarTy::EpochSchedule, + "Fees" => SysvarTy::Fees, + "RecentBlockhashes" => SysvarTy::RecentBlockhashes, + "SlotHashes" => SysvarTy::SlotHashes, + "SlotHistory" => SysvarTy::SlotHistory, + "StakeHistory" => SysvarTy::StakeHistory, + "Instructions" => SysvarTy::Instructions, + "Rewards" => SysvarTy::Rewards, + _ => { + return Err(ParseError::new( + account_ident.span(), + "invalid sysvar provided", + )) + } + }; + Ok(ty) +} diff --git a/lang/syn/src/parser/mod.rs b/lang/syn/src/parser/mod.rs index 3a6712f9f6..118f45f346 100644 --- a/lang/syn/src/parser/mod.rs +++ b/lang/syn/src/parser/mod.rs @@ -1,7 +1,5 @@ pub mod accounts; pub mod error; -#[cfg(feature = "idl")] -pub mod file; pub mod program; pub fn tts_to_string(item: T) -> String { diff --git a/lang/syn/src/parser/program.rs b/lang/syn/src/parser/program.rs deleted file mode 100644 index b521cef92e..0000000000 --- a/lang/syn/src/parser/program.rs +++ /dev/null @@ -1,304 +0,0 @@ -use crate::parser; -use crate::{Ix, IxArg, Program, State, StateInterface, StateIx}; - -// Name of the attribute denoting a state struct. -const STATE_STRUCT_ATTRIBUTE: &str = "state"; - -// Reserved keyword for the constructor method. -const CTOR_METHOD_NAME: &str = "new"; - -pub fn parse(program_mod: syn::ItemMod) -> Program { - let mod_ident = &program_mod.ident; - let mod_content = &program_mod.content.as_ref().unwrap().1; - - // Parse program state. - let state: Option = { - // Parse `struct` marked with the `#[state]` attribute. - let strct: Option<(&syn::ItemStruct, bool)> = mod_content - .iter() - .filter_map(|item| match item { - syn::Item::Struct(item_strct) => { - let attrs = &item_strct.attrs; - if attrs.is_empty() { - return None; - } - let attr_label = attrs[0].path.get_ident().map(|i| i.to_string()); - if attr_label != Some(STATE_STRUCT_ATTRIBUTE.to_string()) { - return None; - } - let is_zero_copy = parser::tts_to_string(&attrs[0].tokens) == "(zero_copy)"; - Some((item_strct, is_zero_copy)) - } - _ => None, - }) - .next(); - // Parse `impl` block for the state struct. - let impl_block: Option = match strct { - None => None, - Some((strct, _)) => mod_content - .iter() - .filter_map(|item| match item { - syn::Item::Impl(item_impl) => { - let impl_ty_str = parser::tts_to_string(&item_impl.self_ty); - let strct_name = strct.ident.to_string(); - if item_impl.trait_.is_some() { - return None; - } - if strct_name != impl_ty_str { - return None; - } - Some(item_impl.clone()) - } - _ => None, - }) - .next(), - }; - // Parse ctor and the generic type in `Context`. - let ctor_and_anchor = match &impl_block { - None => None, - Some(impl_block) => { - impl_block - .items - .iter() - .filter_map(|item: &syn::ImplItem| match item { - syn::ImplItem::Method(m) => { - if m.sig.ident == CTOR_METHOD_NAME { - let (_, is_zero_copy) = strct.as_ref().unwrap(); - let ctx_arg = { - if *is_zero_copy { - // Second param is context. - let mut iter = m.sig.inputs.iter(); - iter.next().expect("First param must be receiver"); - iter.next().expect("Second param must be Context") - } else { - // First param is ctx. - m.sig.inputs.first().unwrap() - } - }; - match ctx_arg { - syn::FnArg::Receiver(_) => panic!("invalid ctor syntax"), - syn::FnArg::Typed(arg) => { - Some((m.clone(), extract_ident(&arg).clone())) - } - } - } else { - None - } - } - _ => None, - }) - .next() - } - }; - // Parse all methods in the above `impl` block. - let methods: Option> = impl_block.as_ref().map(|impl_block| { - impl_block - .items - .iter() - .filter(|item| match item { - syn::ImplItem::Method(m) => m.sig.ident.to_string() != CTOR_METHOD_NAME, - _ => false, - }) - .filter_map(|item: &syn::ImplItem| match item { - syn::ImplItem::Method(m) => match m.sig.inputs.first() { - None => None, - Some(arg) => match arg { - syn::FnArg::Typed(_) => None, - syn::FnArg::Receiver(_) => { - let mut args = m - .sig - .inputs - .iter() - .filter_map(|arg| match arg { - syn::FnArg::Receiver(_) => None, - syn::FnArg::Typed(arg) => Some(arg), - }) - .map(|raw_arg| { - let ident = match &*raw_arg.pat { - syn::Pat::Ident(ident) => &ident.ident, - _ => panic!("invalid syntax"), - }; - IxArg { - name: ident.clone(), - raw_arg: raw_arg.clone(), - } - }) - .collect::>(); - // Remove the Anchor accounts argument - let anchor = args.remove(0); - let anchor_ident = extract_ident(&anchor.raw_arg).clone(); - - Some(StateIx { - raw_method: m.clone(), - ident: m.sig.ident.clone(), - args, - anchor_ident, - has_receiver: true, - }) - } - }, - }, - _ => None, - }) - .collect() - }); - // Parse all trait implementations for the above `#[state]` struct. - let trait_impls: Option> = strct.map(|_strct| { - mod_content - .iter() - .filter_map(|item| match item { - syn::Item::Impl(item_impl) => { - let trait_name = match &item_impl.trait_ { - None => return None, - Some((_, path, _)) => path - .segments - .iter() - .next() - .expect("Must have one segment in a path") - .ident - .clone() - .to_string(), - }; - let methods = item_impl - .items - .iter() - .filter_map(|item: &syn::ImplItem| match item { - syn::ImplItem::Method(m) => match m.sig.inputs.first() { - None => None, - Some(_arg) => { - let mut has_receiver = false; - let mut args = m - .sig - .inputs - .iter() - .filter_map(|arg| match arg { - syn::FnArg::Receiver(_) => { - has_receiver = true; - None - } - syn::FnArg::Typed(arg) => Some(arg), - }) - .map(|raw_arg| { - let ident = match &*raw_arg.pat { - syn::Pat::Ident(ident) => &ident.ident, - _ => panic!("invalid syntax"), - }; - IxArg { - name: ident.clone(), - raw_arg: raw_arg.clone(), - } - }) - .collect::>(); - // Remove the Anchor accounts argument - let anchor = args.remove(0); - let anchor_ident = extract_ident(&anchor.raw_arg).clone(); - - Some(StateIx { - raw_method: m.clone(), - ident: m.sig.ident.clone(), - args, - anchor_ident, - has_receiver, - }) - } - }, - _ => None, - }) - .collect(); - Some(StateInterface { - trait_name, - methods, - }) - } - _ => None, - }) - .collect::>() - }); - // Put it all together. - strct.map(|(strct, is_zero_copy)| { - // Chop off the `#[state]` attribute. It's just a marker. - let mut strct = strct.clone(); - strct.attrs = vec![]; - - State { - name: strct.ident.to_string(), - strct, - interfaces: trait_impls, - impl_block_and_methods: impl_block.map(|impl_block| (impl_block, methods.unwrap())), - ctor_and_anchor, - is_zero_copy, - } - }) - }; - // Parse all non-state ix handlers. - let ixs: Vec = mod_content - .iter() - .filter_map(|item| match item { - syn::Item::Fn(item_fn) => Some(item_fn), - _ => None, - }) - .map(|method: &syn::ItemFn| { - let mut args: Vec = method - .sig - .inputs - .iter() - .map(|arg: &syn::FnArg| match arg { - syn::FnArg::Typed(arg) => { - let ident = match &*arg.pat { - syn::Pat::Ident(ident) => &ident.ident, - _ => panic!("invalid syntax"), - }; - IxArg { - name: ident.clone(), - raw_arg: arg.clone(), - } - } - _ => panic!("invalid syntax"), - }) - .collect(); - // Remove the Context argument - let anchor = args.remove(0); - let anchor_ident = extract_ident(&anchor.raw_arg).clone(); - - Ix { - raw_method: method.clone(), - ident: method.sig.ident.clone(), - args, - anchor_ident, - } - }) - .collect(); - - Program { - state, - ixs, - name: mod_ident.clone(), - program_mod, - } -} - -fn extract_ident(path_ty: &syn::PatType) -> &proc_macro2::Ident { - let p = match &*path_ty.ty { - syn::Type::Path(p) => &p.path, - _ => panic!("invalid syntax"), - }; - let segment = p.segments.first().unwrap(); - let generic_args = match &segment.arguments { - syn::PathArguments::AngleBracketed(args) => args, - _ => panic!("invalid syntax"), - }; - let generic_ty = generic_args - .args - .iter() - .filter_map(|arg| match arg { - syn::GenericArgument::Type(ty) => Some(ty), - _ => None, - }) - .next() - .unwrap(); - let path = match generic_ty { - syn::Type::Path(ty_path) => &ty_path.path, - _ => panic!("invalid syntax"), - }; - &path.segments[0].ident -} diff --git a/lang/syn/src/parser/program/instructions.rs b/lang/syn/src/parser/program/instructions.rs new file mode 100644 index 0000000000..374931be00 --- /dev/null +++ b/lang/syn/src/parser/program/instructions.rs @@ -0,0 +1,59 @@ +use crate::parser::program::ctx_accounts_ident; +use crate::{Ix, IxArg}; +use syn::parse::{Error as ParseError, Result as ParseResult}; +use syn::spanned::Spanned; + +// Parse all non-state ix handlers from the program mod definition. +pub fn parse(program_mod: &syn::ItemMod) -> ParseResult> { + let mod_content = &program_mod + .content + .as_ref() + .ok_or_else(|| ParseError::new(program_mod.span(), "program content not provided"))? + .1; + + mod_content + .iter() + .filter_map(|item| match item { + syn::Item::Fn(item_fn) => Some(item_fn), + _ => None, + }) + .map(|method: &syn::ItemFn| { + let mut args: Vec = method + .sig + .inputs + .iter() + .map(|arg: &syn::FnArg| match arg { + syn::FnArg::Typed(arg) => { + let ident = match &*arg.pat { + syn::Pat::Ident(ident) => &ident.ident, + _ => { + return Err(ParseError::new( + arg.pat.span(), + "expected argument name", + )) + } + }; + Ok(IxArg { + name: ident.clone(), + raw_arg: arg.clone(), + }) + } + syn::FnArg::Receiver(_) => Err(ParseError::new( + arg.span(), + "expected a typed argument not self", + )), + }) + .collect::>()?; + // Remove the Context argument + let anchor = args.remove(0); + let anchor_ident = ctx_accounts_ident(&anchor.raw_arg)?; + + Ok(Ix { + raw_method: method.clone(), + ident: method.sig.ident.clone(), + args, + anchor_ident, + }) + }) + .collect::>>() +} diff --git a/lang/syn/src/parser/program/mod.rs b/lang/syn/src/parser/program/mod.rs new file mode 100644 index 0000000000..117e93d980 --- /dev/null +++ b/lang/syn/src/parser/program/mod.rs @@ -0,0 +1,59 @@ +use crate::Program; +use syn::parse::{Error as ParseError, Result as ParseResult}; +use syn::spanned::Spanned; + +mod instructions; +mod state; + +pub fn parse(program_mod: syn::ItemMod) -> ParseResult { + let state = state::parse(&program_mod)?; + let ixs = instructions::parse(&program_mod)?; + + Ok(Program { + state, + ixs, + name: program_mod.ident.clone(), + program_mod, + }) +} + +fn ctx_accounts_ident(path_ty: &syn::PatType) -> ParseResult { + let p = match &*path_ty.ty { + syn::Type::Path(p) => &p.path, + _ => return Err(ParseError::new(path_ty.ty.span(), "invalid type")), + }; + let segment = p + .segments + .first() + .ok_or_else(|| ParseError::new(p.segments.span(), "expected generic arguments here"))?; + + let generic_args = match &segment.arguments { + syn::PathArguments::AngleBracketed(args) => args, + _ => { + return Err(ParseError::new( + segment.arguments.span(), + "expected generic arguments here", + )) + } + }; + let generic_ty = generic_args + .args + .iter() + .filter_map(|arg| match arg { + syn::GenericArgument::Type(ty) => Some(ty), + _ => None, + }) + .next() + .ok_or_else(|| ParseError::new(generic_args.span(), "expected Accounts type"))?; + + let path = match generic_ty { + syn::Type::Path(ty_path) => &ty_path.path, + _ => { + return Err(ParseError::new( + generic_ty.span(), + "expected Accounts struct type", + )) + } + }; + Ok(path.segments[0].ident.clone()) +} diff --git a/lang/syn/src/parser/program/state.rs b/lang/syn/src/parser/program/state.rs new file mode 100644 index 0000000000..34152878f8 --- /dev/null +++ b/lang/syn/src/parser/program/state.rs @@ -0,0 +1,312 @@ +use crate::parser; +use crate::parser::program::ctx_accounts_ident; +use crate::{IxArg, State, StateInterface, StateIx}; +use syn::parse::{Error as ParseError, Result as ParseResult}; +use syn::spanned::Spanned; + +// Name of the attribute denoting a state struct. +const STATE_STRUCT_ATTRIBUTE: &str = "state"; + +// Reserved keyword for the constructor method. +const CTOR_METHOD_NAME: &str = "new"; + +// Parse the state from the program mod definition. +pub fn parse(program_mod: &syn::ItemMod) -> ParseResult> { + let mod_content = &program_mod + .content + .as_ref() + .ok_or_else(|| ParseError::new(program_mod.span(), "program content not provided"))? + .1; + + // Parse `struct` marked with the `#[state]` attribute. + let strct: Option<(&syn::ItemStruct, bool)> = mod_content + .iter() + .filter_map(|item| match item { + syn::Item::Struct(item_strct) => { + let attrs = &item_strct.attrs; + if attrs.is_empty() { + return None; + } + let attr_label = attrs[0].path.get_ident().map(|i| i.to_string()); + if attr_label != Some(STATE_STRUCT_ATTRIBUTE.to_string()) { + return None; + } + let is_zero_copy = parser::tts_to_string(&attrs[0].tokens) == "(zero_copy)"; + Some((item_strct, is_zero_copy)) + } + _ => None, + }) + .next(); + + // Parse `impl` block for the state struct. + let impl_block: Option = match strct { + None => None, + Some((strct, _)) => mod_content + .iter() + .filter_map(|item| match item { + syn::Item::Impl(item_impl) => { + let impl_ty_str = parser::tts_to_string(&item_impl.self_ty); + let strct_name = strct.ident.to_string(); + if item_impl.trait_.is_some() { + return None; + } + if strct_name != impl_ty_str { + return None; + } + Some(item_impl.clone()) + } + _ => None, + }) + .next(), + }; + + // Parse ctor and the generic type in `Context`. + let ctor_and_anchor: Option<(syn::ImplItemMethod, syn::Ident)> = impl_block + .as_ref() + .map(|impl_block| { + let r: Option> = impl_block + .items + .iter() + .filter_map(|item: &syn::ImplItem| match item { + syn::ImplItem::Method(m) => match m.sig.ident.to_string() == CTOR_METHOD_NAME { + false => None, + true => Some(m), + }, + _ => None, + }) + .map(|m: &syn::ImplItemMethod| { + let (_, is_zero_copy) = strct + .as_ref() + .expect("impl_block exists therefore the struct exists"); + let ctx_arg = { + if *is_zero_copy { + // Second param is context. + let mut iter = m.sig.inputs.iter(); + match iter.next() { + None => { + return Err(ParseError::new( + m.sig.span(), + "first parameter must be &mut self", + )) + } + Some(arg) => match arg { + syn::FnArg::Receiver(r) => { + if r.mutability.is_none() { + return Err(ParseError::new( + m.sig.span(), + "first parameter must be &mut self", + )); + } + } + syn::FnArg::Typed(_) => { + return Err(ParseError::new( + m.sig.span(), + "first parameter must be &mut self", + )) + } + }, + }; + match iter.next() { + None => { + return Err(ParseError::new( + m.sig.span(), + "second parameter must be the Context", + )) + } + Some(ctx_arg) => match ctx_arg { + syn::FnArg::Receiver(_) => { + return Err(ParseError::new( + ctx_arg.span(), + "second parameter must be the Context", + )) + } + syn::FnArg::Typed(arg) => arg, + }, + } + } else { + match m.sig.inputs.first() { + None => { + return Err(ParseError::new( + m.sig.span(), + "first parameter must be the Context", + )) + } + Some(ctx_arg) => match ctx_arg { + syn::FnArg::Receiver(_) => { + return Err(ParseError::new( + ctx_arg.span(), + "second parameter must be the Context", + )) + } + syn::FnArg::Typed(arg) => arg, + }, + } + } + }; + Ok((m.clone(), ctx_accounts_ident(&ctx_arg)?)) + }) + .next(); + r.transpose() + }) + .transpose()? + .unwrap_or(None); + + // Parse all methods in the above `impl` block. + let methods: Option> = impl_block + .as_ref() + .map(|impl_block| { + impl_block + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Method(m) => match m.sig.ident.to_string() != CTOR_METHOD_NAME { + false => None, + true => Some(m), + }, + _ => None, + }) + .map(|m: &syn::ImplItemMethod| { + let mut args = m + .sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(arg) => Some(arg), + }) + .map(|raw_arg| { + let ident = match &*raw_arg.pat { + syn::Pat::Ident(ident) => &ident.ident, + _ => { + return Err(ParseError::new( + raw_arg.pat.span(), + "unexpected type argument", + )) + } + }; + Ok(IxArg { + name: ident.clone(), + raw_arg: raw_arg.clone(), + }) + }) + .collect::>>()?; + // Remove the Anchor accounts argument + let anchor = args.remove(0); + let anchor_ident = ctx_accounts_ident(&anchor.raw_arg)?; + + Ok(StateIx { + raw_method: m.clone(), + ident: m.sig.ident.clone(), + args, + anchor_ident, + has_receiver: true, + }) + }) + .collect::>>() + }) + .transpose()?; + + // Parse all trait implementations for the above `#[state]` struct. + let trait_impls: Option> = strct + .map(|_strct| { + mod_content + .iter() + .filter_map(|item| match item { + syn::Item::Impl(item_impl) => match &item_impl.trait_ { + None => None, + Some((_, path, _)) => { + let trait_name = path + .segments + .iter() + .next() + .expect("Must have one segment in a path") + .ident + .clone() + .to_string(); + Some((item_impl, trait_name)) + } + }, + _ => None, + }) + .map(|(item_impl, trait_name)| { + let methods = item_impl + .items + .iter() + .filter_map(|item: &syn::ImplItem| match item { + syn::ImplItem::Method(m) => Some(m), + _ => None, + }) + .map(|m: &syn::ImplItemMethod| { + match m.sig.inputs.first() { + None => { + return Err(ParseError::new( + m.sig.inputs.span(), + "state methods must have a self argument", + )) + } + Some(_arg) => { + let mut has_receiver = false; + let mut args = m + .sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => { + has_receiver = true; + None + } + syn::FnArg::Typed(arg) => Some(arg), + }) + .map(|raw_arg| { + let ident = match &*raw_arg.pat { + syn::Pat::Ident(ident) => &ident.ident, + _ => panic!("invalid syntax"), + }; + IxArg { + name: ident.clone(), + raw_arg: raw_arg.clone(), + } + }) + .collect::>(); + // Remove the Anchor accounts argument + let anchor = args.remove(0); + let anchor_ident = ctx_accounts_ident(&anchor.raw_arg)?; + + Ok(StateIx { + raw_method: m.clone(), + ident: m.sig.ident.clone(), + args, + anchor_ident, + has_receiver, + }) + } + } + }) + .collect::>>()?; + Ok(StateInterface { + trait_name, + methods, + }) + }) + .collect::>>() + }) + .transpose()?; + + Ok(strct.map(|(strct, is_zero_copy)| { + // Chop off the `#[state]` attribute. It's just a marker. + // + // TODO: instead of mutating the syntax, we should just implement + // a macro that does nothing. + let mut strct = strct.clone(); + strct.attrs = vec![]; + + State { + name: strct.ident.to_string(), + strct, + interfaces: trait_impls, + impl_block_and_methods: impl_block.map(|impl_block| (impl_block, methods.unwrap())), + ctor_and_anchor, + is_zero_copy, + } + })) +} diff --git a/spl/src/shmem.rs b/spl/src/shmem.rs index 8e7a6b8f50..d1fb5be08f 100644 --- a/spl/src/shmem.rs +++ b/spl/src/shmem.rs @@ -1,6 +1,7 @@ //! CPI API for interacting with the SPL shared memory //! [program](https://github.com/solana-labs/solana-program-library/tree/master/shared-memory). +use anchor_lang::ToAccountInfo; use anchor_lang::{Accounts, CpiContext}; use solana_program::account_info::AccountInfo; use solana_program::declare_id; @@ -42,8 +43,8 @@ pub struct Ret<'info> { #[derive(Accounts)] pub struct Shmem<'info> { // Shared memory account to write the return value into. - #[account(mut, "shmem.owner == shmem_program.key")] + #[account(mut, constraint = shmem.owner == shmem_program.key)] pub shmem: AccountInfo<'info>, - #[account("shmem_program.key == &ID")] + #[account(constraint = shmem_program.key == &ID)] pub shmem_program: AccountInfo<'info>, }