hoa/compiler
Hoa\Compiler is a PHP library for building lexers and parsers from grammar definitions. It supports LL(1) and LR(1) analysis, AST manipulation, grammar validation, and compiler construction, making it easier to parse domain-specific languages and custom syntax.
Hoa Compiler is a grammar-driven lexer/parser generator. You write a .pp grammar, load it with Hoa\Compiler\Llk\Llk, parse input into a generic TreeNode AST, then walk that AST with visitors. It is archived and unmaintained, so pin versions and verify PHP compatibility before committing.
Install:
composer require hoa/compiler
Minimal grammar, e.g. resources/grammars/arithmetic.pp:
%skip \s+
%token number \d+
%token plus \+
%token times \*
%token lparen \(
%token rparen \)
expr:
term ( plus term )*
term:
factor ( times factor )*
factor:
number
| lparen expr rparen
Parse and dump the AST:
use Hoa\Compiler\Llk\Llk;
use Hoa\Compiler\Visitor\Dump;
use Hoa\File\Read;
$parser = Llk::load(new Read(__DIR__ . '/arithmetic.pp'));
$ast = $parser->parse('1 + 2 * (3 + 4)');
echo (new Dump())->visit($ast);
Look first at:
Hoa\Compiler\Llk\Llk::load() — entry point for loading a grammar.Hoa\Compiler\Llk\Parser — the parser object returned by load().Hoa\Compiler\Llk\TreeNode — the AST node type (getId(), getValue(), getChildren()).Hoa\Compiler\Visitor\Dump — the fastest way to understand the tree your grammar produces.%token, %skip, rule definitions, alternation, repetition, and optional groups.A good first use case is a small internal DSL or expression/config language: define the grammar, parse a fixture, dump the AST, then replace Dump with a visitor that validates or evaluates the tree.
Keep the parser as a long-lived service. Llk::load() reads and compiles the grammar each time it is called. Load it once per request/process and reuse it.
use Hoa\Compiler\Llk\Llk;
use Hoa\Compiler\Llk\Parser;
use Hoa\File\Read;
final class GrammarParserFactory
{
public function __invoke(): Parser
{
return Llk::load(new Read(resource_path('grammars/arithmetic.pp')));
}
}
In Laravel, bind it as a singleton in a service provider:
$this->app->singleton(Parser::class, function () {
return Llk::load(new Read(resource_path('grammars/arithmetic.pp')));
});
Under PHP-FPM this is per request; under Octane the singleton can persist for the worker lifetime, which is usually preferable.
Wrap parsing in a domain service. Do not let controllers or jobs call Llk directly. Translate parser exceptions into domain/validation errors at the boundary.
final class ExpressionParser
{
public function __construct(private Parser $parser) {}
public function parse(string $input): ExpressionAst
{
try {
$tree = $this->parser->parse($input);
} catch (\Hoa\Compiler\Exception $e) {
throw new InvalidExpression($e->getMessage(), previous: $e);
}
return new ExpressionAst($tree);
}
}
Use visitors for every AST operation. The AST is generic; avoid scattering getChild(2) calls throughout the codebase. Create one visitor per concern: validation, evaluation, pretty-printing, code generation, metrics collection.
use Hoa\Visitor\Element;
use Hoa\Visitor\Visit;
final class AstToArray implements Visit
{
public function visit(Element $element, &$handle = null, $eldnah = null)
{
$node = [
'id' => $element->getId(),
'value' => $element->getValue(),
'children' => [],
];
foreach ($element->getChildren() as $child) {
$node['children'][] = $this->visit($child, $handle, $eldnah);
}
return $node;
}
}
The visit() signature must match Hoa\Visitor\Visit; the reference parameters are part of the interface.
Normalize the tree shape in one place. Because nonterminals and tokens both appear as nodes, visitors often need to skip punctuation tokens (plus, lparen, etc.) or unwrap single-child wrapper rules. Put that logic in one traversal helper rather than repeating it.
Use grammar tests with AST snapshots. For each grammar rule, keep valid and invalid fixture inputs. Snapshot the Dump output for valid cases; assert exception type/message for invalid cases. This catches accidental grammar changes faster than end-to-end tests.
Design grammars for LL parsing. Refactor left-recursive rules into right-recursive or repetition forms. Keep token order deliberate: keywords before identifier patterns, punctuation before catch-all regexes.
Use %skip aggressively. Whitespace and comments should disappear at the lexer level. Otherwise every visitor must handle them.
Separate lexer concerns from parser concerns. If tokenization gets complicated, test the lexer indirectly through small parser fixtures. Hoa exposes the lexer/parser internals, but relying on internal APIs increases upgrade risk.
hoa/consistency, hoa/zformat, etc.). Pin exact versions and consider a maintained fork or an alternative parser generator if PHP 8 compatibility is required.%token identifier [a-zA-Z_]\w* can swallow keywords.%skip for whitespace. Otherwise spaces become AST nodes and visitors become noisy.expr: term ( plus term )* produces child nodes for plus as well as term. Visitors must either skip token nodes by ID or explicitly consume them.getChild(0) is fine for tiny grammars, but long-term code should switch on getId() and use named helper methods. Grammar refactors silently change child positions.Dump before writing visitors. Most confusion comes from guessing the AST shape. Dump a representative input first.Hoa\Compiler\Exception subclasses and translate them. Do not expose raw parser messages to end users.Llk::load() as a compilation step and cache the resulting parser object in the container. Rebuild it on deploy.Hoa\File\Read. Relative paths and stream wrappers can behave differently across CLI, queue workers, and web requests.resources/ or a dedicated grammars/ directory and load them explicitly.composer why hoa/consistency and avoid mixing with newer Hoa components unless you have tested the combination.[$element->getId(), $element->getValue()] for every node. This gives a compact trace without the full Dump output.How can I help you explore Laravel packages today?