Skip to content

Commit

Permalink
lang: Add declare_program! macro (#2857)
Browse files Browse the repository at this point in the history
  • Loading branch information
acheroncrypto authored Mar 25, 2024
1 parent 4393d73 commit 0f60909
Show file tree
Hide file tree
Showing 30 changed files with 1,419 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- cli: Add `--no-idl` flag to the `build` command ([#2847](https://github.com/coral-xyz/anchor/pull/2847)).
- cli: Add priority fees to idl commands ([#2845](https://github.com/coral-xyz/anchor/pull/2845)).
- ts: Add `prepend` option to MethodBuilder `preInstructions` method ([#2863](https://github.com/coral-xyz/anchor/pull/2863)).
- lang: Add `declare_program!` macro ([#2857](https://github.com/coral-xyz/anchor/pull/2857)).

### Fixes

Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion lang/attribute/program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ idl-build = ["anchor-syn/idl-build"]
interface-instructions = ["anchor-syn/interface-instructions"]

[dependencies]
anchor-syn = { path = "../../syn", version = "0.29.0" }
anchor-syn = { path = "../../syn", version = "0.29.0", features = ["idl-types"] }
anyhow = "1"
bs58 = "0.5"
heck = "0.3"
proc-macro2 = "1"
quote = "1"
serde_json = "1"
syn = { version = "1", features = ["full"] }
360 changes: 360 additions & 0 deletions lang/attribute/program/src/declare_program/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
use anchor_syn::idl::types::{
Idl, IdlArrayLen, IdlDefinedFields, IdlField, IdlGenericArg, IdlRepr, IdlSerialization,
IdlType, IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy,
};
use quote::{format_ident, quote};

/// This function should ideally return the absolute path to the declared program's id but because
/// `proc_macro2::Span::call_site().source_file().path()` is behind an unstable feature flag, we
/// are not able to reliably decide where the definition is.
pub fn get_canonical_program_id() -> proc_macro2::TokenStream {
quote! { super::__ID }
}

pub fn gen_docs(docs: &[String]) -> proc_macro2::TokenStream {
let docs = docs
.iter()
.map(|doc| format!("{}{doc}", if doc.is_empty() { "" } else { " " }))
.map(|doc| quote! { #[doc = #doc] });
quote! { #(#docs)* }
}

pub fn gen_discriminator(disc: &[u8]) -> proc_macro2::TokenStream {
quote! { [#(#disc), *] }
}

pub fn gen_accounts_common(idl: &Idl, prefix: &str) -> proc_macro2::TokenStream {
let re_exports = idl
.instructions
.iter()
.map(|ix| format_ident!("__{}_accounts_{}", prefix, ix.name))
.map(|ident| quote! { pub use super::internal::#ident::*; });

quote! {
pub mod accounts {
#(#re_exports)*
}
}
}

pub fn convert_idl_type_to_syn_type(ty: &IdlType) -> syn::Type {
syn::parse_str(&convert_idl_type_to_str(ty)).unwrap()
}

// TODO: Impl `ToString` for `IdlType`
pub fn convert_idl_type_to_str(ty: &IdlType) -> String {
match ty {
IdlType::Bool => "bool".into(),
IdlType::U8 => "u8".into(),
IdlType::I8 => "i8".into(),
IdlType::U16 => "u16".into(),
IdlType::I16 => "i16".into(),
IdlType::U32 => "u32".into(),
IdlType::I32 => "i32".into(),
IdlType::F32 => "f32".into(),
IdlType::U64 => "u64".into(),
IdlType::I64 => "i64".into(),
IdlType::F64 => "f64".into(),
IdlType::U128 => "u128".into(),
IdlType::I128 => "i128".into(),
IdlType::U256 => "u256".into(),
IdlType::I256 => "i256".into(),
IdlType::Bytes => "bytes".into(),
IdlType::String => "String".into(),
IdlType::Pubkey => "Pubkey".into(),
IdlType::Option(ty) => format!("Option<{}>", convert_idl_type_to_str(ty)),
IdlType::Vec(ty) => format!("Vec<{}>", convert_idl_type_to_str(ty)),
IdlType::Array(ty, len) => format!(
"[{}; {}]",
convert_idl_type_to_str(ty),
match len {
IdlArrayLen::Generic(len) => len.into(),
IdlArrayLen::Value(len) => len.to_string(),
}
),
IdlType::Defined { name, generics } => generics
.iter()
.map(|generic| match generic {
IdlGenericArg::Type { ty } => convert_idl_type_to_str(ty),
IdlGenericArg::Const { value } => value.into(),
})
.reduce(|mut acc, cur| {
if !acc.is_empty() {
acc.push(',');
}
acc.push_str(&cur);
acc
})
.map(|generics| format!("{name}<{generics}>"))
.unwrap_or(name.into()),
IdlType::Generic(ty) => ty.into(),
}
}

pub fn convert_idl_type_def_to_ts(
ty_def: &IdlTypeDef,
ty_defs: &[IdlTypeDef],
) -> proc_macro2::TokenStream {
let name = format_ident!("{}", ty_def.name);
let docs = gen_docs(&ty_def.docs);

let generics = {
let generics = ty_def
.generics
.iter()
.map(|generic| match generic {
IdlTypeDefGeneric::Type { name } => format_ident!("{name}"),
IdlTypeDefGeneric::Const { name, ty } => format_ident!("{name}: {ty}"),
})
.collect::<Vec<_>>();
if generics.is_empty() {
quote!()
} else {
quote!(<#(#generics,)*>)
}
};

let attrs = {
let debug_attr = quote!(#[derive(Debug)]);

let default_attr = can_derive_default(ty_def, ty_defs)
.then(|| quote!(#[derive(Default)]))
.unwrap_or_default();

let ser_attr = match &ty_def.serialization {
IdlSerialization::Borsh => quote!(#[derive(AnchorSerialize, AnchorDeserialize)]),
IdlSerialization::Bytemuck => quote!(#[zero_copy]),
IdlSerialization::BytemuckUnsafe => quote!(#[zero_copy(unsafe)]),
_ => unimplemented!("{:?}", ty_def.serialization),
};

let clone_attr = matches!(ty_def.serialization, IdlSerialization::Borsh)
.then(|| quote!(#[derive(Clone)]))
.unwrap_or_default();

let copy_attr = matches!(ty_def.serialization, IdlSerialization::Borsh)
.then(|| can_derive_copy(ty_def, ty_defs).then(|| quote!(#[derive(Copy)])))
.flatten()
.unwrap_or_default();

quote! {
#debug_attr
#default_attr
#ser_attr
#clone_attr
#copy_attr
}
};

let repr = if let Some(repr) = &ty_def.repr {
let kind = match repr {
IdlRepr::Rust(_) => "Rust",
IdlRepr::C(_) => "C",
IdlRepr::Transparent => "transparent",
};
let kind = format_ident!("{kind}");

let modifier = match repr {
IdlRepr::Rust(modifier) | IdlRepr::C(modifier) => {
let packed = modifier.packed.then(|| quote!(packed)).unwrap_or_default();
let align = modifier
.align
.map(|align| quote!(align(#align)))
.unwrap_or_default();

if packed.is_empty() {
align
} else if align.is_empty() {
packed
} else {
quote! { #packed, #align }
}
}
_ => quote!(),
};
let modifier = if modifier.is_empty() {
modifier
} else {
quote! { , #modifier }
};

quote! { #[repr(#kind #modifier)] }
} else {
quote!()
};

let ty = match &ty_def.ty {
IdlTypeDefTy::Struct { fields } => {
let declare_struct = quote! { pub struct #name #generics };
handle_defined_fields(
fields.as_ref(),
|| quote! { #declare_struct; },
|fields| {
let fields = fields.iter().map(|field| {
let name = format_ident!("{}", field.name);
let ty = convert_idl_type_to_syn_type(&field.ty);
quote! { pub #name : #ty }
});
quote! {
#declare_struct {
#(#fields,)*
}
}
},
|tys| {
let tys = tys.iter().map(convert_idl_type_to_syn_type);
quote! {
#declare_struct (#(#tys,)*);
}
},
)
}
IdlTypeDefTy::Enum { variants } => {
let variants = variants.iter().map(|variant| {
let variant_name = format_ident!("{}", variant.name);
handle_defined_fields(
variant.fields.as_ref(),
|| quote! { #variant_name },
|fields| {
let fields = fields.iter().map(|field| {
let name = format_ident!("{}", field.name);
let ty = convert_idl_type_to_syn_type(&field.ty);
quote! { #name : #ty }
});
quote! {
#variant_name {
#(#fields,)*
}
}
},
|tys| {
let tys = tys.iter().map(convert_idl_type_to_syn_type);
quote! {
#variant_name (#(#tys,)*)
}
},
)
});

quote! {
pub enum #name #generics {
#(#variants,)*
}
}
}
IdlTypeDefTy::Type { alias } => {
let alias = convert_idl_type_to_syn_type(alias);
quote! { pub type #name = #alias; }
}
};

quote! {
#docs
#attrs
#repr
#ty
}
}

fn can_derive_copy(ty_def: &IdlTypeDef, ty_defs: &[IdlTypeDef]) -> bool {
match &ty_def.ty {
IdlTypeDefTy::Struct { fields } => {
can_derive_common(fields.as_ref(), ty_defs, can_derive_copy_ty)
}
IdlTypeDefTy::Enum { variants } => variants
.iter()
.all(|variant| can_derive_common(variant.fields.as_ref(), ty_defs, can_derive_copy_ty)),
IdlTypeDefTy::Type { alias } => can_derive_copy_ty(alias, ty_defs),
}
}

fn can_derive_default(ty_def: &IdlTypeDef, ty_defs: &[IdlTypeDef]) -> bool {
match &ty_def.ty {
IdlTypeDefTy::Struct { fields } => {
can_derive_common(fields.as_ref(), ty_defs, can_derive_default_ty)
}
// TODO: Consider storing the default enum variant in IDL
IdlTypeDefTy::Enum { .. } => false,
IdlTypeDefTy::Type { alias } => can_derive_default_ty(alias, ty_defs),
}
}

fn can_derive_copy_ty(ty: &IdlType, ty_defs: &[IdlTypeDef]) -> bool {
match ty {
IdlType::Option(inner) => can_derive_copy_ty(inner, ty_defs),
IdlType::Array(inner, len) => {
if !can_derive_copy_ty(inner, ty_defs) {
return false;
}

match len {
IdlArrayLen::Value(_) => true,
IdlArrayLen::Generic(_) => false,
}
}
IdlType::Defined { name, .. } => ty_defs
.iter()
.find(|ty_def| &ty_def.name == name)
.map(|ty_def| can_derive_copy(ty_def, ty_defs))
.expect("Type def must exist"),
IdlType::Bytes | IdlType::String | IdlType::Vec(_) | IdlType::Generic(_) => false,
_ => true,
}
}

fn can_derive_default_ty(ty: &IdlType, ty_defs: &[IdlTypeDef]) -> bool {
match ty {
IdlType::Option(inner) => can_derive_default_ty(inner, ty_defs),
IdlType::Vec(inner) => can_derive_default_ty(inner, ty_defs),
IdlType::Array(inner, len) => {
if !can_derive_default_ty(inner, ty_defs) {
return false;
}

match len {
IdlArrayLen::Value(len) => *len <= 32,
IdlArrayLen::Generic(_) => false,
}
}
IdlType::Defined { name, .. } => ty_defs
.iter()
.find(|ty_def| &ty_def.name == name)
.map(|ty_def| can_derive_default(ty_def, ty_defs))
.expect("Type def must exist"),
IdlType::Generic(_) => false,
_ => true,
}
}

fn can_derive_common(
fields: Option<&IdlDefinedFields>,
ty_defs: &[IdlTypeDef],
can_derive_ty: fn(&IdlType, &[IdlTypeDef]) -> bool,
) -> bool {
handle_defined_fields(
fields,
|| true,
|fields| {
fields
.iter()
.map(|field| &field.ty)
.all(|ty| can_derive_ty(ty, ty_defs))
},
|tys| tys.iter().all(|ty| can_derive_ty(ty, ty_defs)),
)
}

fn handle_defined_fields<R>(
fields: Option<&IdlDefinedFields>,
unit_cb: impl Fn() -> R,
named_cb: impl Fn(&[IdlField]) -> R,
tuple_cb: impl Fn(&[IdlType]) -> R,
) -> R {
match fields {
Some(fields) => match fields {
IdlDefinedFields::Named(fields) => named_cb(fields),
IdlDefinedFields::Tuple(tys) => tuple_cb(tys),
},
_ => unit_cb(),
}
}
Loading

0 comments on commit 0f60909

Please sign in to comment.