|
| 1 | +Creating Compiler Passes |
| 2 | +************************ |
| 3 | + |
| 4 | +.[perex] |
| 5 | +Compiler passes provide a powerful mechanism to analyze and modify Latte templates *after* they have been parsed into an Abstract Syntax Tree (AST) and *before* the final PHP code is generated. This allows for advanced template manipulation, optimizations, security checks (like the Sandbox), and collecting template insights. This guide will walk you through creating your own compiler passes. |
| 6 | + |
| 7 | + |
| 8 | +What is a Compiler Pass? |
| 9 | +======================== |
| 10 | + |
| 11 | +To understand the role of compiler passes, see [Latte's compilation pipeline |custom-tags#Understanding the Compilation Process]. As you can see, compiler passes operate at a crucial stage, allowing deep intervention between the initial parsing and the final code output. |
| 12 | + |
| 13 | +At its core, a compiler pass is simply a **PHP callable** (like a function, a static method, or an instance method) that accepts one argument: the root node of the template's AST, which is always an instance of `Latte\Compiler\Nodes\TemplateNode`. |
| 14 | + |
| 15 | +The primary goal of a compiler pass is usually one or both of the following: |
| 16 | + |
| 17 | +- Analysis: To walk through the AST and gather information about the template (e.g., find all defined blocks, check for specific tag usage, ensure certain security constraints are met). |
| 18 | +- Modification: To change the AST structure or node properties (e.g., automatically add HTML attributes, optimize certain tag combinations, replace deprecated tags with new ones, implement sandboxing rules). |
| 19 | + |
| 20 | + |
| 21 | +Registration |
| 22 | +============ |
| 23 | + |
| 24 | +Compiler passes are registered via an [Extension's|extending-latte#getPasses()] `getPasses()` method. This method returns an associative array where keys are unique names for the passes (used internally and for ordering) and values are the PHP callables implementing the pass logic. |
| 25 | + |
| 26 | +```php |
| 27 | +use Latte\Compiler\Nodes\TemplateNode; |
| 28 | +use Latte\Extension; |
| 29 | + |
| 30 | +class MyExtension extends Extension |
| 31 | +{ |
| 32 | + public function getPasses(): array |
| 33 | + { |
| 34 | + return [ |
| 35 | + 'modificationPass' => $this->modifyTemplateAst(...), |
| 36 | + // ... other passes ... |
| 37 | + ]; |
| 38 | + } |
| 39 | + |
| 40 | + public function modifyTemplateAst(TemplateNode $templateNode): void |
| 41 | + { |
| 42 | + // Implementation... |
| 43 | + } |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +Passes registered by Latte's core extensions and your custom extensions run sequentially. The order can be important, especially if one pass relies on the results or modifications of another. Latte provides a helper mechanism to control this order if needed; see the documentation for [`Extension::getPasses()` |extending-latte#getPasses()] for details. |
| 48 | + |
| 49 | + |
| 50 | +Traversing the AST with `NodeTraverser` |
| 51 | +======================================= |
| 52 | + |
| 53 | +Manually writing recursive functions to walk through the complex AST structure is tedious and error-prone. Latte provides a dedicated tool for this: [api:Latte\Compiler\NodeTraverser]. This class implements the [visitor pattern |https://en.wikipedia.org/wiki/Visitor_pattern], making AST traversal systematic and manageable. |
| 54 | + |
| 55 | +The basic usage involves creating an instance of `NodeTraverser` and calling its `traverse()` method, passing the root AST node and one or two "visitor" callables: |
| 56 | + |
| 57 | +```php |
| 58 | +use Latte\Compiler\Node; |
| 59 | +use Latte\Compiler\NodeTraverser; |
| 60 | +use Latte\Compiler\Nodes; |
| 61 | + |
| 62 | +$traverser = new NodeTraverser; |
| 63 | + |
| 64 | +$traverser->traverse( |
| 65 | + $templateNode, |
| 66 | + |
| 67 | + // 'enter' visitor: Called when entering a node (before its children) |
| 68 | + enter: function (Node $node) { |
| 69 | + echo "Entering node of type: " . $node::class . "\n"; |
| 70 | + // You can inspect the node here |
| 71 | + if ($node instanceof Nodes\TextNode) { |
| 72 | + // echo "Found text: " . $node->content . "\n"; |
| 73 | + } |
| 74 | + }, |
| 75 | + |
| 76 | + // 'leave' visitor: Called when leaving a node (after its children) |
| 77 | + leave: function (Node $node) { |
| 78 | + echo "Leaving node of type: " . $node::class . "\n"; |
| 79 | + // You might perform actions here after children have been processed |
| 80 | + } |
| 81 | +); |
| 82 | +``` |
| 83 | + |
| 84 | +You can provide only the `enter` visitor, only the `leave` visitor, or both, depending on your needs. |
| 85 | + |
| 86 | +**`enter(Node $node)`:** This function is executed for each node **before** the traverser visits any of that node's children. It's useful for: |
| 87 | + |
| 88 | +- Collecting information as you descend the tree. |
| 89 | +- Making decisions *before* processing children (like deciding to skip them, see [#Optimizing Traversal]). |
| 90 | +- Potentially modifying the node before children are visited (less common). |
| 91 | + |
| 92 | +**`leave(Node $node)`:** This function is executed for each node **after** all of its children (and their entire subtrees) have been fully visited (both entered and left). It's the most common place for: |
| 93 | + |
| 94 | +Both `enter` and `leave` visitors can optionally return a value to influence the traversal process. Returning `null` (or nothing) continues traversal normally, returning a `Node` instance replaces the current node, and returning special constants like `NodeTraverser::RemoveNode` or `NodeTraverser::StopTraversal` modifies the flow, as explained in the following sections. |
| 95 | + |
| 96 | + |
| 97 | +How Traversal Works |
| 98 | +------------------- |
| 99 | + |
| 100 | +The `NodeTraverser` internally uses the `getIterator()` method that every `Node` class must implement (as discussed in [Creating Custom Tags |custom-tags#Implementing getIterator() for Subnodes]). It iterates over the children yielded by `getIterator()`, recursively calls `traverse()` on them, ensuring that the `enter` and `leave` visitors are called in the correct depth-first order for every node in the tree accessible via iterators. This highlights again why a correctly implemented `getIterator()` in your custom tag nodes is absolutely essential for compiler passes to function correctly. |
| 101 | + |
| 102 | +Let's write a simple pass that counts how many times the `{do}` tag (represented by `Latte\Essential\Nodes\DoNode`) is used in the template. |
| 103 | + |
| 104 | +```php |
| 105 | +use Latte\Compiler\Node; |
| 106 | +use Latte\Compiler\NodeTraverser; |
| 107 | +use Latte\Compiler\Nodes\TemplateNode; |
| 108 | +use Latte\Essential\Nodes\DoNode; |
| 109 | + |
| 110 | +function countDoTags(TemplateNode $templateNode): void |
| 111 | +{ |
| 112 | + $count = 0; |
| 113 | + $traverser = new NodeTraverser; |
| 114 | + $traverser->traverse( |
| 115 | + $templateNode, |
| 116 | + enter: function (Node $node) use (&$count): void { |
| 117 | + if ($node instanceof DoNode) { |
| 118 | + $count++; |
| 119 | + } |
| 120 | + } |
| 121 | + // 'leave' visitor is not needed for this task |
| 122 | + ); |
| 123 | + |
| 124 | + echo "Found {do} tag $count times.\n"; |
| 125 | +} |
| 126 | + |
| 127 | +$latte = new Latte\Engine; |
| 128 | +$ast = $latte->parse($templateSource); |
| 129 | +countDoTags($ast); |
| 130 | +``` |
| 131 | + |
| 132 | +In this example, we only needed the `enter` visitor to check the type of each node encountered. |
| 133 | + |
| 134 | +Next, we'll explore how to use these visitors to actually modify the AST. |
0 commit comments