Upgrade Guide
Upgrading To 4.0 From 3.x
Version 4.0 is a rewrite. Almost every public API changed, so this is a porting guide rather than a list of renames. The good news: the grammar files mostly survive, and that is where the bulk of the work usually lives.
PHP 8.4 Required
Likelihood Of Impact: High
Phplrt 4.0 requires PHP 8.4. The API uses property hooks, asymmetric
visibility and new in initializers throughout - which is also why so much
of it looks different.
Getters Became Properties
Likelihood Of Impact: High
Everything that was getX() is now a property. This is the single most
common change you will hit.
// 3.x
$token->getName();
$token->getOffset();
$token->getValue();
$token->getBytes();
$source->getContents();
$source->getPathname();
// 4.x
$token->name;
$token->offset;
$token->value;
$token->size;
$source->content;
$source->pathname;
Tokens Are Identified By Number
Likelihood Of Impact: High
In 3.x a token was addressed by its name. In 4.x it has an int $id, and the
name is optional metadata for error messages.
// 3.x
if ($token->getName() === 'T_DIGIT') { /* ... */ }
// 4.x
if ($token->id === MyParser::T_DIGIT) { /* ... */ }
Generated parsers expose the ids as class constants, so you do not have to track the numbers yourself.
Channels Replaced "Skipped Tokens"
Likelihood Of Impact: Medium
The lexer no longer takes a list of names to skip. Every token now carries a channel, and the parser reads the default one.
// 3.x
$lexer = new Lexer($tokens, ['T_WHITESPACE']);
// 4.x
$builder->addPattern('\s++', 'T_WHITESPACE')
->hide();
Unlike skipping, a hidden token is still produced - so you can look at comments and whitespace when you need them, and custom channels let you keep a token out of the grammar without throwing it away.
The Lexer Is Built, Not Configured
Likelihood Of Impact: High
Phplrt\Lexer\Lexer no longer takes a map of names and patterns. It takes a
single compiled regular expression and a set of tables - which you get from
LexerBuilder.
// 3.x
$lexer = new Lexer([
'T_DIGIT' => '\d+',
'T_PLUS' => '\+',
]);
// 4.x
use Phplrt\Lexer\Builder\LexerBuilder;
$builder = new LexerBuilder();
$builder->addPattern('\d++', 'T_DIGIT');
$builder->addValue('+', 'T_PLUS');
$lexer = $builder->build()
->toLexer();
The append(), prepend(), prependMany() and skip() methods are gone;
addPattern(), addValue() and hide() cover the same ground. Building is
also where patterns are validated, so a broken regex is reported with the
token that owns it instead of failing at match time.
The Parser Takes Explicit Arguments
Likelihood Of Impact: High
The Parser::CONFIG_* options are gone, replaced by constructor parameters:
// 3.x
$parser = new Parser($lexer, $grammar, [
Parser::CONFIG_INITIAL_RULE => 'expression',
Parser::CONFIG_AST_BUILDER => new MyBuilder(),
]);
// 4.x
$parser = new Parser(
lexer: $lexer,
grammar: $grammar,
initial: 0,
reducers: [0 => $callback],
);
Rules are keyed by integers rather than by name, and they refer to each other by index. In practice you do not write this array by hand - see the parser builder.
Checking And Trailing Tokens Became One Method
Likelihood Of Impact: Medium
3.x answered "is this valid?" with check(), and read a source the grammar
does not describe in full through a setting, picking up where it stopped from
the parser afterwards. Both are analyze() now, and it returns what it found
rather than keeping it:
// 3.x
$parser->check($source); // true or false
$parser = new Parser(..., [Parser::CONFIG_ALLOW_TRAILING_TOKENS => true]);
$parser->parse($source);
$context = $parser->getLastExecutionContext();
$context->buffer->current(); // where the parser stopped
// 4.x
use Phplrt\Parser\Analysis\Mode;
use Phplrt\Parser\Analysis\Result\SuccessfulResult;
$parser->analyze($source, Mode::SyntaxCheck) instanceof SuccessfulResult; // true or false
$result = $parser->analyze($source);
$result->value; // what the fragment reduced to
$result->token; // where the parser stopped
$result->diagnostics; // and what stands in the way
Two things changed beyond the names. Nothing is kept on the parser between
calls, so a parser is safe to share and to call from several places at once.
And where the reading stopped is now told for every source, not only for the
ones that fail: a valid source is a SuccessfulResult, a source the grammar
reads in part is a PartialResult, and one it cannot read at all is a
FailureResult.
See Analysing A Source.
BuilderInterface Became Per-Rule Reducers
Likelihood Of Impact: High
The single AST builder with a switch overrule names is gone. Each rule now
carries its own reducer.
// 3.x
class MyBuilder implements BuilderInterface
{
public function build(Context $ctx, $children)
{
switch ($ctx->getState()) {
case 'Number': return new NumberNode($children);
case 'Sum': return new SumNode($children);
}
return null;
}
}
// 4.x - in the grammar file
Number -> { return new \NumberNode($offset, $children->value); }
: <T_DIGIT> ;
Sum -> { return new \SumNode($children); }
: Number() ::T_PLUS:: Number() ;
// 4.x - or through the builder
$number->setReducer(static fn(Context $ctx, mixed $children): NumberNode
=> new NumberNode($children)
);
The reducer signature is callable(Context $ctx, mixed $children): mixed,
same as before, but $ctx->getState() is now $ctx->rule and holds an
integer.
Returning null still means "leave the children alone".
Grammar Rule Classes
Likelihood Of Impact: Medium
The rules moved from Phplrt\Parser\Grammar (3.2) - same namespace, but they
are now readonly value objects that reference other rules by integer id,
and Lexeme takes a token id instead of a name.
// 3.x
new Lexeme('T_DIGIT');
new Lexeme('T_WHITESPACE', false);
new Repetition($ruleId, 0, \INF);
// 4.x
new Lexeme(MyLexer::T_DIGIT);
new Lexeme(MyLexer::T_WHITESPACE, false);
new Repetition($ruleId, 0, \INF);
reduce() is gone from the rule classes: matching is done by the parser's
internal tracer, and the rules are pure data. This is what made the lookahead
tables and code generation possible.
The Buffer Package Is Gone
Likelihood Of Impact: Low
phplrt/buffer was merged into phplrt/parser as an internal detail
(Phplrt\Parser\Internal\Buffer). The parser no longer exposes a buffer, and
you do not choose one.
The Position and Visitor Packages Are Gone
Likelihood Of Impact: Medium
phplrt/position and phplrt/visitor have been removed.
Positions: the exception component computes lines and columns when it renders an error, which was the main use for it. See Error Reporting.
Visitors: 4.x does not prescribe an AST shape, so it cannot prescribe a way to walk one. Your nodes are your own classes; walk them however suits them.
Sources Are Constructed Directly
Likelihood Of Impact: Medium
The static factory methods on File are gone.
// 3.x
File::fromPathname('/app/x.txt');
File::fromSources('2 + 2');
// 4.x
new File('/app/x.txt');
new Source('2 + 2');
new VirtualFile('x.txt', '2 + 2'); // a string with a name
SourceFactory is there if you need the "figure out what this is" behaviour.
Code Generation Is Back
Likelihood Of Impact: Low
3.0 removed generation of a full PHP class in favour of a config array. 4.0 brings the class back, and it now includes the lexer, the token constants and the reducers as real methods:
new Compiler()
->load(new File(__DIR__ . '/grammar.pp3'))
->generate()
->withNamespaceName('App\Parser')
->withClassName('LanguageParser')
->save(__DIR__ . '/LanguageParser.php');
See Code Generation.
The .pp Format Is No Longer Read
Likelihood Of Impact: Medium
Grammars written in the original Hoa-style .pp format are not supported.
A .pp file is still recognized by its extension, so you get a clear error
rather than a confusing parse failure.
Rewrite the grammar in one of the formats that are read - see Grammar Syntax.
Grammar Files: What To Check
Likelihood Of Impact: Medium
A grammar file keeps its extension and keeps being read the way it was, so most of them compile unchanged. What no longer works:
The old pragmas are no longer supported. Unification and the error levels are gone; the corresponding behaviour is either the default now or is configured in PHP. Which settings a grammar may carry is listed under Settings.
$file and $state are no longer declared in a reducer. Use $source and
$rule, which is an int. See PHP in a Grammar.
Left recursion is now rejected at build time. It never worked at runtime either, but 3.x would let you compile it. Rewrite as a repetition:
// ✘ rejected
Expression : Expression() ::T_PLUS:: Number() ;
// ✔
Expression : Number() (::T_PLUS:: Number())* ;
Reducers returning null mean "no result" and pass the children through.
If a rule of yours legitimately produces null, wrap it.
Reducers returning arrays are flattened into the rule above. If you relied on nesting, return an object instead. See Results and Reducers.
Package Names
Likelihood Of Impact: Low
| 3.x | 4.x |
|---|---|
phplrt/lexer |
phplrt/lexer (+ phplrt/lexer-builder) |
phplrt/parser |
phplrt/parser (+ phplrt/parser-builder) |
phplrt/compiler |
phplrt/compiler |
phplrt/source |
phplrt/source |
phplrt/exception |
phplrt/exception |
phplrt/buffer |
removed - merged into phplrt/parser |
phplrt/position |
removed |
phplrt/visitor |
removed |
phplrt/ast-contracts |
removed - 4.x does not define a node shape |
Upgrading To 3.2 From 3.1
Grammar Package Removed
Likelihood Of Impact: Medium
The phplrt/grammar and phplrt/grammar-contracts has been removed. All
classes and interfaces associated with this package have been moved inside
the phplrt/parser package.
- All classes
Phplrt\Grammar\*has been renamed toPhplrt\Parser\Grammar\*. - All interfaces
Phplrt\Contracts\Grammar\*has been renamed toPhplrt\Parser\Grammar\*.
Buffer Package Has Been Moved
Likelihood Of Impact: Medium
- Interface
Phplrt\Contracts\Lexer\BufferInterfacehas been moved intoPhplrt\Buffer\BufferInterface. - All classes
Phplrt\Lexer\Buffer\*has been moved intoPhplrt\Buffer\*.