Writing custom macros locally#

Prerequisites#

This program includes the ability for you to write your own macros. Whilst these macros can be published to pypi, it is often easier to develop them locally.

First create a folder with the name of the macro that you would like to import. For example, if you were writing a set of macros called greet, you would create a folder called greet/. In that folder create a file called macros.json with this contents, and the templates replaced:

{
    "name": "{NAME}",
    "package": "{FOLDER_NAME}",
    "description": "{MACRO_DESCRIPTION}",
    "macros": [],
    "bootstrap": []
}

To create a macro, you will need to create a .py or .mpy file in the folder. If the file is a .mpy file, you will need to add it to the bootstrap array. Then add the macro to the macros array:

{
    "name": "Greeter",
    "package": "greet",
    "description": "A selection of greeting macros",
    "macros": [
        {
            "name": "Hello",
            "keyword": "hello",
            "file": "hello.mpy",
            "description": "Say hello to someone"
        }
    ],
    "bootstrap": ["hello.mpy"]
}

From here, we can start to write out parser. Whilst the makros package provides a number of helper libraries, it leaves the parsing up to you. Whilst you can use any parsing technique, this program has been developed to be familiar to anyone who has completed the fist interpreter inside of the Crafting interpreters book <https://craftinginterpreters.com/>.

The first part of a macro is the parser, which must extend the MacroParser class and have the name Parser. This function will be responsible for parsing your code. The example for this project will be fairly simple, but full blown parers, like the one used for namespaces or enums may be necessary for more complex projects.

from makros.macros.types import MacroParser, MacroTranslator
from makros.tokens import Tokens
import tokenize

class Parser(MacroParser):
    def parse(self, tokens: Tokens) -> any:
        token = tokens.advance()

        token_return = '""'

        if token.type == tokenize.STRING:
            token_return = token.string.replace("'", '"')

        if token.type == tokenize.NAME:
            token_return = token.string

        return token_return

This example will provide a string to the translator, but it is often necessary to send a more complex AST tree to the translator. Because it is just a string, our parsing code can be fairly neat:

class Translator(MacroTranslator):
    def translate(self, token_return: str) -> str:
        return "print(f'Hello, {" + token_return + "}')"

This is now a complete custom macro. You can import it in your project so long as the folder is within your project tree. You can then use it in your code:

macro import greet.hello
hello "World"

You can publish your macro to pypi and the makro command will index it by package name. Complete source code for this example is available on GitHub.

Helpful code snippets#

There are a number of generators (dubbed pyx) which can be used to generate code valid python code programmatically.

import makros.macro_creation.pyx as pyx

pyx.program(
    # Debug comment
    f'# Start of namespace {ast.identifier.string}',
    pyx.create_class(
        # Class name
        namespace_def_function,

        # Class body
        pyx.create_func(
            # Function name
            '__init__',

            # Function args
            'self',

            # Function body
            ast.body.visit(self)
        )
    ),

    # Create an instance of the class
    f'{ast.identifier.string} = {namespace_def_function}()',
    # Remove the original class, we don't need it anymore
    f'del {namespace_def_function}',
    # Debug comment
    f'# End of namespace {ast.identifier.string}'
)