#smart-contracts #blockchain #ink #wasm #macro-derive

no-std pendzl_lang_codegen

pendzl codegeneration for macros in pendzl_lang_macro

19 releases (1 stable)

new 1.0.2 Nov 5, 2024
1.0.1-v1calls Aug 5, 2024
0.2.4 Mar 26, 2024
0.2.4-v1calls2 Jun 3, 2024
0.1.0-alpha2 Jan 24, 2024

#23 in #ink


Used in 6 crates (via pendzl_lang_macro)

MIT license

110KB
1.5K SLoC

1. Code Generation Module

This module contains the implementations of macros used for code generation in the library.

  • Contains:

    • Implementation of the codegen::implementation macro:
      • Implements the implementation macro, which injects default fn implementations (& overrides) of standard traits.
    • Boilerplate code for implementations injecting:
      • Every implementation of a trait (PSP22, PSP34, PSP22Metadata etc) is injected with the default implementation of the trait.
    • Implementation of the codegen::storage_item macro:
      • Implements functionality for the storage_item macro, handling storage-related code generation.
    • Implementation of the codegen::storage_field_getter_derive macro:
      • Contains the implementation for deriving the StorageFieldGetter trait for storage structures.

How does it work?

Macro Implementation: The generate Function

The generate function serves as the core entry point for the procedural macro, processing an ink! module, injecting default implementations for specified traits & handling overridden methods.

Parsing Attributes and Module Input

The function begins by parsing the attributes provided to the macro to determine which traits require default implementations. The attributes are expected to be a list of trait names, such as #[implementation(PSP22, PSP34)]. These are parsed using the syn::parse2::<AttributeArgs> function, which results in a vector of trait names (to_inject_default_impls_vec).

Identifying the Storage Struct

The storage struct is the central data structure for an ink! contract, representing the contract's state. The extract_storage_struct_name function iterates over the module's items to find the struct annotated with #[ink(storage)]. Once found, it extracts the struct's identifier (name) for later use in implementing traits.

Processing Overridden Methods

Users can provide custom implementations for specific trait methods by annotating functions with #[overrider(TraitName)]. The consume_overriders function searches for these annotated functions, extracts them, and removes them from the module's items to prevent duplication. It collects these overrides into an OverridenFnMap, mapping trait names to the corresponding overridden functions.

Injecting Trait Implementations

For each trait specified in the attributes (eg. for pendzl::implementation(PSP22, Pausable) would be PSP22 and Pausable), it calls the corresponding implementation function (e.g., impl_psp22, impl_pausable), passing impl_args as the context. These implementation functions generate the default implementations of the traits, modify the module's items, and handle any necessary imports. Note that all of the impl_XYZ functions follow the same pattern - injecting default implementations for internal and public traits & potentially injecting required imports. Such boilerplate is necessary for simplicity and further DX down the line.

Cleaning Up Imports

Pendzl's codegen removes unnecessary base trait imports when extended traits are present to avoiding potential conflicts or redundancy. This optimization ensures that the generated code includes only the necessary imports. It also auto injects StorageFieldGetter trait and any overridden trait implementations collected earlier.

Trait Implementation Functions

following section talks about the contents of implenentation.rs file

The Pendzl codegen provides several functions to generate default implementations for specific traits, such as impl_psp22, impl_pausable, impl_psp22_vault, impl_psp34, impl_psp34_burnable, impl_ownable, impl_vesting, impl_set_code_hash... and more.

. Each of these functions follows a similar pattern:

  1. Context Retrieval: Accesses the storage struct name and other necessary context from impl_args.
  2. Implementation Preparation: Constructs default implementations for internal traits and public traits, defining methods that either provide default behavior or delegate to internal methods.
  3. Override Handling: Calls override_functions to apply any user-provided overrides to the trait implementations.
  4. Import Management: Adds necessary imports to impl_args.imports, ensuring all required types and traits are available in the generated code.
  5. Updating Module Items: Appends the constructed implementations to impl_args.items.

Example: Implementing the PSP22 Trait

The impl_psp22 function injects the default implementation of the PSP22 trait into the module. It constructs the internal default implementation (PSP22InternalDefaultImpl), the internal trait implementation (PSP22Internal), the public default implementation (PSP22DefaultImpl), and the public trait implementation (PSP22). Each of these implementations defines methods that provide default behaviors or delegate to internal methods.

The function then calls override_functions to handle any overrides provided by the user for both PSP22Internal and PSP22. It manages necessary imports, such as pendzl::contracts::psp22::*, and appends the implementations to the module items.

Storage Item Handling

The storage_item function implements the #[pendzl::storage_item] macro, preparing structs or enums to be part of the contract's storage. It allows the use of the #[lazy] attribute to mark fields that should be lazily loaded and wrapped in ::ink::storage::Lazy. The macro also generates constant storage keys for every mapping or lazy field, following recommendations from ink!'s storage layout documentation.

Dependencies

~8MB
~159K SLoC