Hone logo
Hone
Problems

Python Macro System

This challenge asks you to implement a simplified macro system in Python. Macros are powerful tools that allow you to transform code at compile-time or before execution. Building this system will deepen your understanding of code generation, metaprogramming, and how Python itself can be extended.

Problem Description

You need to create a Python class, let's call it MacroEngine, that can:

  1. Define Macros: Register functions that act as macros. These macro functions will receive the "code" they are meant to process as input and return the transformed code.
  2. Expand Macros: Process a given string of Python code, identifying and replacing macro calls with their expanded forms.
  3. Execute Expanded Code: Safely execute the Python code after all macros have been expanded.

Key Requirements:

  • Macro Definition: A macro should be a Python function. When a macro is invoked, it receives a representation of the code to be transformed (e.g., an AST node or a string). It must return a representation of the code that replaces the macro call.
  • Macro Invocation Syntax: For simplicity, let's define macro invocation as a specific function call syntax. For example, macro_name(arg1, arg2) or macro_name("string_arg"). The macro name should be easily distinguishable from regular function calls. We'll use a prefix, say @macro_. So, a macro call would look like @macro_name(...).
  • Code Representation: For this challenge, you can choose to work with code as either:
    • Strings: Parse and manipulate the code as raw strings. This is simpler but more fragile.
    • Abstract Syntax Trees (ASTs): Use Python's built-in ast module to parse code into a tree structure, manipulate the tree, and then unparse it back into code. This is more robust and generally preferred for metaprogramming.
  • Expansion Process: Macros should be expanded recursively. If a macro expands to code that itself contains macro calls, those should also be expanded.
  • Execution: The MacroEngine should have a method to execute the final, macro-expanded code. This execution should happen in a safe environment, potentially without polluting the global namespace of the MacroEngine itself.

Expected Behavior:

When MacroEngine.expand(code_string) is called, it should:

  1. Parse the code_string.
  2. Traverse the parsed code structure.
  3. Identify macro calls.
  4. For each macro call:
    • Retrieve the registered macro function.
    • Call the macro function with the appropriate arguments/code representation.
    • Replace the macro call with the returned expanded code.
  5. Unparse the modified code structure back into a string.

When MacroEngine.execute(code_string) is called, it should:

  1. Call expand to get the macro-free code.
  2. Execute this expanded code.

Edge Cases:

  • Macros that expand to nothing (empty code).
  • Macros that are not defined.
  • Recursive macro definitions (should be handled carefully, perhaps with a recursion depth limit).
  • Macros that generate invalid Python syntax.
  • Macros that take arguments that are complex expressions.

Examples

Example 1: Simple Macro for Printing

Let's imagine a macro @macro_print_me that takes an argument and wraps it in a print() call.

Input Code String:

@macro_print_me(hello_world)

Macro Definition:

def print_me_macro(arg_representation):
    # Assume arg_representation is an AST node for 'hello_world'
    # We want to return an AST node for print(hello_world)
    return ast.Call(
        func=ast.Name(id='print', ctx=ast.Load()),
        args=[arg_representation],
        keywords=[]
    )

Expected Output Code String (after expansion):

print(hello_world)

Example 2: Macro for Variable Declaration/Assignment

Consider a macro @macro_assign_constant that defines a constant variable.

Input Code String:

@macro_assign_constant(MY_VALUE, 100)

Macro Definition:

def assign_constant_macro(name_node, value_node):
    # Assume name_node is AST for MY_VALUE, value_node is AST for 100
    # We want to return an AST node for MY_VALUE = 100
    return ast.Assign(
        targets=[name_node],
        value=value_node
    )

Expected Output Code String (after expansion):

MY_VALUE = 100

Example 3: Macro with Conditional Expansion

A macro that conditionally includes code.

Input Code String:

@macro_if_debug("print('Debug mode enabled!')")

Macro Definition (assuming the macro engine has a DEBUG flag):

def if_debug_macro(code_to_include_str):
    if self.DEBUG: # Assume self.DEBUG is True for this example
        # Parse and return the code string as AST nodes
        return ast.parse(code_to_include_str).body
    else:
        return [] # Return an empty list of statements

Expected Output Code String (if DEBUG is True):

print('Debug mode enabled!')

Expected Output Code String (if DEBUG is False): (Empty string, as nothing is generated)

Constraints

  • The input code to be processed will be a valid Python string.
  • Macro names will strictly follow the pattern @macro_ followed by a valid Python identifier (e.g., @macro_my_func).
  • Macro arguments will be parsed and passed to the macro function. The exact representation (string, AST node) is up to your implementation choice, but AST is recommended for robustness.
  • The macro expansion depth should not exceed 100 levels to prevent infinite recursion.
  • The MacroEngine should not modify the global or local scope of the MacroEngine class itself during execution of expanded code.

Notes

  • Using Python's ast module (Abstract Syntax Trees) is highly recommended for this challenge. It provides a structured way to parse, manipulate, and generate Python code. You'll likely need ast.parse, ast.NodeVisitor or ast.NodeTransformer, and ast.unparse (available in Python 3.9+; for older versions, you might need a third-party library or a simpler string-based approach).
  • Consider how you will map the @macro_name(...) syntax to function calls. This will involve parsing the code structure to identify these specific patterns.
  • Think about how macro arguments are represented. Are they strings? AST nodes? How does a macro function receive them?
  • Handling the return value of a macro is crucial. What format should it be in, and how does the engine replace the original macro call with it?
  • For safe execution, consider using exec() with carefully managed globals() and locals() dictionaries.
Loading editor...
python