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:
- 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.
- Expand Macros: Process a given string of Python code, identifying and replacing macro calls with their expanded forms.
- 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)ormacro_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
astmodule 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
MacroEngineshould 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 theMacroEngineitself.
Expected Behavior:
When MacroEngine.expand(code_string) is called, it should:
- Parse the
code_string. - Traverse the parsed code structure.
- Identify macro calls.
- 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.
- Unparse the modified code structure back into a string.
When MacroEngine.execute(code_string) is called, it should:
- Call
expandto get the macro-free code. - 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
MacroEngineshould not modify the global or local scope of theMacroEngineclass itself during execution of expanded code.
Notes
- Using Python's
astmodule (Abstract Syntax Trees) is highly recommended for this challenge. It provides a structured way to parse, manipulate, and generate Python code. You'll likely needast.parse,ast.NodeVisitororast.NodeTransformer, andast.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 managedglobals()andlocals()dictionaries.