AST Overview #
Sugar parses templates into an abstract syntax tree (AST), runs compiler passes, and then generates PHP output. Understanding the AST makes it easier to debug, extend, and build custom passes.
Pipeline At A Glance #
- Parser builds the AST.
- Compiler passes transform and enrich the tree.
- Code generator emits PHP.
Common passes include directive extraction, component expansion, and context analysis for escaping.
Node Catalog #
DocumentNode #
The root of a template. It holds the top-level children nodes and is the entry point for traversal.
Example:
<h1>Hello</h1>
DocumentNode
ElementNode(tag="h1")
TextNode("Hello")
ElementNode #
Represents an HTML element with tag, attributes, children, and selfClosing. When dynamicTag is set, the tag name is rendered from a runtime expression.
dynamicTag is set by the s:tag directive and lets the compiler emit a runtime tag name instead of a literal one. This keeps the AST stable (still an ElementNode) while allowing the opening and closing tags to be generated from an expression.
Example:
<div s:tag="$tagName">Content</div>
ElementNode(tag="div", dynamicTag="$tagName")
TextNode("Content")
Example:
<a href="/profile">Profile</a>
ElementNode(tag="a", attributes=[href="/profile"])
TextNode("Profile")
FragmentNode #
Wrapperless node used for <s-template> blocks. It renders only its children, accepts only s:* attributes, and can be self-closing for directive-only markup.
FragmentNode is also produced by ElementRoutingPass when a <s-NAME> tag is resolved to an element-claiming directive. The element attributes are synthesized as directive attributes on the fragment, which DirectiveExtractionPass then processes using the standard nesting rules.
Example:
<s-template s:if="$show">
<p>Visible</p>
</s-template>
FragmentNode(attributes=[s:if])
ElementNode(tag="p")
TextNode("Visible")
Example (element-claiming directive, after ElementRoutingPass):
<s-youtube src="$id" s:if="$show" />
FragmentNode(attributes=[s:youtube="$id", s:if="$show"])
TextNode #
Static text content. No escaping logic lives here; it is emitted directly by the code generator.
Example:
Welcome back
TextNode("Welcome back")
OutputNode #
Dynamic output expression. It stores:
-
expression: PHP expression to evaluate -
escape: whether escaping is enabled -
context: output context used by the escaper -
pipes: optional pipe transformations
Example:
<?= $userName ?>
OutputNode(expression="$userName", escape=true, context=HTML)
RawPhpNode #
Raw PHP code captured from the template. It is passed through without modification.
Example:
<?php $count = count($items); ?>
RawPhpNode("$count = count($items);")
RawBodyNode #
Verbatim content preserved from s:raw regions. The parser does not interpret the contents, and the compiler emits them as-is.
Example:
<div s:raw>
<?php echo $notParsed; ?>
<span><?= $stillRaw ?></span>
</div>
RawBodyNode("<?php echo $notParsed; ?>\n <span><?= $stillRaw ?></span>")
DirectiveNode #
Structural directives like s:if, s:foreach, or s:while. These nodes wrap child nodes and may carry paired siblings (for directives like forelse).
Example:
<p s:if="$show">Hello</p>
DirectiveNode(name="if", expression="$show")
ElementNode(tag="p")
TextNode("Hello")
ComponentNode #
Represents a custom element invocation (<s-NAME>). It holds element attributes and slot children.
The parser always produces a ComponentNode for <s-NAME> tags. What happens next depends on the directive registry:
-
If
NAMEis a directive that implementsElementClaimingDirectiveInterface,ElementRoutingPassconverts theComponentNodeinto aFragmentNodewith directive attributes before directive extraction runs. -
Otherwise,
ComponentExpansionPassreplaces it with the component template AST.
Example:
<s-button class="primary">Save</s-button>
ComponentNode(name="button", attributes=[class="primary"])
TextNode("Save")
RuntimeCallNode #
A runtime call that returns output. Used when a template needs a dynamic runtime call (for example, dynamic component rendering).
Example:
RuntimeCallNode(callableExpression="$__sugar->renderComponent", arguments=["$name", "$bindings"])
AttributeNode #
Represents a single attribute name and its AttributeValue. The value can be boolean, static, output, or mixed parts.
Example:
<input disabled>
AttributeNode(name="disabled", value=boolean)
AttributeValue Shapes #
AttributeValue normalizes attribute values into one of four shapes:
-
Boolean: presence-only attributes like
disabled - Static: literal strings from markup
-
Output: a single
OutputNode -
Parts: interleaved strings and
OutputNodevalues
Examples:
<input disabled>
<a href="/profile">
<a href="<?= $url ?>">
<div class="btn <?= $state ?>">
disabled -> boolean
href -> static("/profile")
href -> output(OutputNode("$url"))
class -> parts(["btn ", OutputNode("$state")])
Use AttributeValue::from() to normalize legacy shapes (including null for boolean attributes). toParts() returns a parts list for rendering, or null for boolean attributes.
Output Context And Escaping #
OutputNode carries an OutputContext enum that tells the escaper how to render the value. The context analysis pass sets the correct context for output nodes in element bodies and attributes.
Traversal Notes #
All nodes carry source line and column, and the base Node tracks the originating template path for better diagnostics.
Some nodes implement sibling navigation helpers (for example DocumentNode, ElementNode, ComponentNode, and DirectiveNode). Use them when writing passes that depend on node order or need adjacent context.