Imported from AndreLZGava/PHire-Script-Sandbox (
knowledge_base/phirescript/skills/write-emitter/SKILL.md). Install upstream withnpx skills add AndreLZGava/PHire-Script-Sandbox --skill write-emitter. Copyright stays with the author.
Skill: Write Emitter
Triggers
- "add an emitter", "write a NodeEmitter", "emit new node to PHP"
- "how is X emitted", "emitter for MyConstructNode"
- "EmitterDispatcher", "EmitContext", "NodeEmitter interface"
- New feature is parsed and bound but produces empty output or wrong PHP
When to Use
Use when a new AST node needs to generate PHP code. Every concrete Node subclass that can appear in a compiled program needs a corresponding NodeEmitter.
Repository Context
NodeEmitterinterface:src/Emitter/Base/NodeEmitter.phpNodeEmitterAbstract:src/Emitter/Base/NodeEmitterAbstract.phpEmitterDispatcher:src/Emitter/Base/EmitterDispatcher.phpEmitContext:src/Emitter/Base/EmitContext.phpPhpTypeResolver:src/Emitter/Base/Type/PhpTypeResolver.phpUseRegistry:src/Emitter/Base/UseRegistry.php- Emitter orchestrator:
src/Emitter.php(where dispatcher is built) - Existing emitters for reference:
src/Emitter/Declarations/ClassEmitter.php,src/Emitter/OOP/MethodEmitter.php
Key Patterns
NodeEmitter interface
interface NodeEmitter
{
public function supports(object $node, EmitContext $ctx): bool;
public function emit(object $node, EmitContext $ctx): string;
}
supports()→truewhen this emitter handles the given nodeemit()→ returns the PHP code string for this node
Minimal emitter
namespace PHireScript\Emitter\Declarations;
use PHireScript\Compiler\Parser\Ast\Nodes\Declarations\MyConstructNode;
use PHireScript\Emitter\Base\EmitContext;
use PHireScript\Emitter\Base\NodeEmitter;
class MyConstructEmitter implements NodeEmitter
{
public function supports(object $node, EmitContext $ctx): bool
{
return $node instanceof MyConstructNode;
}
public function emit(object $node, EmitContext $ctx): string
{
// Emit child nodes by delegating back to the dispatcher:
$body = $ctx->emitter->emit($node->body, $ctx);
return "// my construct: {$node->name}\n{$body}";
}
}
EmitContext API
class EmitContext {
public bool $dev; // dev mode flag from PHireScript.json
public UseRegistry $uses; // accumulates PHP use statements
public EmitterDispatcher $emitter; // dispatcher — use to emit child nodes
public bool $insideInterface;
public bool $insideClass;
public bool $insideMethod;
public bool $insideTrait;
// ... other flags
}
Emitting child nodes
Always delegate to $ctx->emitter->emit($childNode, $ctx) — never recurse directly.
This ensures the dispatcher routes correctly to the right emitter for each child type.
public function emit(object $node, EmitContext $ctx): string
{
$parts = [];
foreach ($node->body->statements as $statement) {
$parts[] = $ctx->emitter->emit($statement, $ctx);
}
return implode("\n", $parts);
}
Using PhpTypeResolver
Map PHireScript type names to PHP type syntax:
use PHireScript\Emitter\Base\Type\PhpTypeResolver;
$phpType = PhpTypeResolver::resolve($node->returnType, $ctx);
// PHireScript String → string
// PHireScript Email → string
// PHireScript Uuid → string
// PHireScript UserModel → UserModel (custom type = class name)
// PHireScript String|Null → string|null (null always last in PHP 8)
Registering PHP use statements
// Add a use statement to the accumulated header block:
$ctx->uses->add('App\\Models\\User');
// UseRegistry::render() emits all collected use statements as a sorted block
NodeEmitterAbstract helper
Extends NodeEmitter. Provides removeEndPunctuation(string $name): string for stripping ? or ! suffixes from method names.
Registering the emitter
Open src/Emitter.php and add your emitter to the array where EmitterDispatcher is built:
new EmitterDispatcher([
// ... existing emitters ...
new MyConstructEmitter(),
]);
Context flags in supports()
Some emitters are context-dependent (e.g., same Node type emits differently inside interface vs class):
public function supports(object $node, EmitContext $ctx): bool
{
return $node instanceof MethodDeclarationNode && $ctx->insideInterface;
}
The dispatcher tries context-dependent emitters first via linear scan, falling back to the fast-path cache for unambiguous node types.
Critical Rules
- Return a valid PHP string — the emitter output is parsed by nikic/php-parser; invalid PHP causes a FatalErrorException from within the processor, not from the emitter itself.
- Never mutate the node — emitters are read-only; all mutation happens in Binders.
- Delegate child nodes — use
$ctx->emitter->emit($child, $ctx)for children; never instantiate child emitters directly. - Register the emitter — if not registered in
Emitter.php, the node is silently emitted as empty string. supports()must not have side effects — it may be called multiple times during dispatcher warm-up.
Common Mistakes
- Emitting raw PHireScript type names instead of using
PhpTypeResolver→ invalid PHP types - Forgetting to register in
Emitter.php→ node produces no output, PHP file has missing constructs - Returning PHP with wrong indentation → nikic re-indents, but logic errors are not fixed
- Direct string concatenation of PHP that requires a use statement but doesn't register it →
undefined classat runtime
Validation Checklist
-
NodeEmitterinterface implemented (bothsupports()andemit()) - Child nodes emitted via
$ctx->emitter->emit($child, $ctx) - PHP types resolved via
PhpTypeResolver::resolve() -
usestatements accumulated via$ctx->uses->add() - Emitter registered in
src/Emitter.php - Emitter output is valid PHP (test with
php -lon the compiled file) - Sandbox test case passes
Examples
See: examples/