vanderlee / comprehend
PHP parser combinators with ABNF authoring, immutable syntax trees and explicit effects
Requires
- php: >=8.2.0
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- pestphp/pest: ^2.36.1
- phpmd/phpmd: 2.6.0
- phpstan/phpstan: ^2.1
- squizlabs/php_codesniffer: ^4.0
Suggests
- ext-dom: Required only for CST/AST XML serialization via toXml().
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-24 10:36:56 UTC
README
An interpreted parser-combinator framework for PHP 8.2+, with named grammars, reusable RFC rules, explicit sources, immutable concrete trees and named ASTs. MIT licensed. Requires JSON and mbstring. DOM is optional, needed only for XML output.
Quick start
Parse dice notation such as 2d6+3: two six-sided dice with a +3 modifier.
This builds a roll description; it does not roll the dice. The bundled Numbers
and Extraction extensions load automatically on first use.
require 'vendor/autoload.php'; use Vanderlee\Comprehend\Comprehend as C; use Vanderlee\Comprehend\Extensions\Numbers\SignPolicy; $modifier = C::integerValue(-100, 100, allowLeadingZeros: true, sign: SignPolicy::REQUIRED) ->optionalValue(default: 0); $roll = C::collect( C::integerValue(1, 100)->thenIgnore('d'), C::integerValue(2, 1000)->named('sides'), $modifier, )->map(static fn(array $parts): array => [ 'dice' => $parts[0], 'sides' => $parts[1], 'modifier' => $parts[2], ]); $result = $roll->parse('2d6+3'); if ($result->ok) { $value = $result->output(); // ['dice' => 2, 'sides' => 6, 'modifier' => 3] echo $result->rawText(); // '2d6+3' (original text) }
thenIgnore('d') recognizes the delimiter while keeping the dice count's value.
collect() gathers the three values and map() gives them descriptive keys.
1d20 defaults the modifier to zero; 2d6-3 produces a modifier of -3.
Counts range from 1–100, sides from 2–1000, and modifiers from -100 to 100.
allowLeadingZeros: true permits signed zero and leading zeros in the modifier.
The count and lowercase d are required; malformed input and trailing text fail.
ok is a short alias for success; output() returns value() (original text
unless mapped). Failed results yield null output. Value access stays lazy and
does not validate or commit effects. Use rawText() whenever you need the source text.
For spaced syntax, pass an optional outer spacing policy for one facade call:
$result = $roll->parse('2d6 +3', spacing: C::text(' ')->many()); if ($result->success) { echo $result->ast()->firstNamed('sides')->rawText(); // '6' }
Per-call spacing applies between items; it does not trim leading/trailing text.
The modifier's sign and digits remain contiguous, as in +3.
Local spacing() and lexeme() directives take precedence. The grammar remains
unchanged. Run php docs/examples/quick-start.php for checked examples.
matchPrefix(Source, int $byteOffset = 0) allows remaining input.
parseAll(Source, int $byteOffset = 0) requires the entire remainder to match.
Both return ParseResult; every public offset and span uses bytes. Choose
Utf8Source for validated Unicode code-point recognition or ByteSource for
arbitrary octets. The facade's parse() requires complete input; parseAll()
and matchPrefix() also accept strings as validated UTF-8 and freeze the current
grammar automatically. Facades whose entire graph declares the
ReusableSnapshot capability reuse their prepared snapshot after the first call.
Built-ins qualify by exact class; custom subclasses must explicitly opt in.
Deferred rules, definitions and other custom parsers still refresh; freeze those
explicitly for stable repeated parsing.
FrozenGrammar accepts strings too. Facade composition treats string arguments as Text literals.
Object constructors still take explicit parsers; arrays are not grammar syntax.
Source, syntax and meaning
One runtime serves object grammars, Rulesets and RFC imports. Each recognition
creates a ParseSession. The immutable CST retains selected syntax, trivia and
exact source spans. name() marks semantic structure for ast(), whose raw text
always comes from source. Typed mapValue() mappings and named result collections
are lazy and cached per recognition. onCommit() effects receive ParseResult and
run only after explicit commit() and successful semantic validation.
use Vanderlee\Comprehend\Result\ParseResult; $number = C::text('42') ->named('number') ->mapValue(fn (ParseResult $r): int => (int) $r->rawText()) ->validateSemantic(fn (ParseResult $r): bool => $r->value() > 0); $result = $number->parse('42'); $result->value(); // 42, cached $result->ast(); // named source structure $result->commit(); // validates, then commits selected effects once
Spacing is explicit: spacing(?Parser) configures intervening trivia;
trivia() boundary parsers recognize leading/trailing material; lexeme()
suppresses automatic spacing within a subtree. CST and AST serialize directly
to array, JSON and XML. There is no separate Token output model.
Named grammar authoring
Use Ruleset::define(string, Comprehend|Parser|Definition|callable), explicit
facade-returning rule(string, ...$arguments) lookup and root(string|Comprehend|Parser) selection.
ref() remains the low-level Parser reference. Deferred references permit forward and
recursive definitions; an undefined name raises a grammar error. Factories must
return Comprehend values or low-level Parser instances. Definition generators
accept both, and build() returns a facade for direct composition and parsing.
Use Stub::setParser(Parser) only for low-level recursive object grammars.
freeze() resolves and snapshots a graph, rejecting known left recursion and
nullable repetition. The immutable Comprehend facade composes the same object
parsers without mutating reused containers. Ruleset constructor entries, definitions
and roots accept facade values directly and detach them internally at registration.
See Grammar authoring
and its checked arithmetic/RFC examples. Mutable builders remain available;
changes affect future snapshots, while frozen grammars remain independent.
Execution limits
Seekable file sources retain stable disk snapshots with bounded read pages and
incremental UTF-8 validation. Optional ParseLimits bound cooperative depth and
work. See File input and measured performance
for regex limits, source lifetime and tree-retention costs.
Choice selects the first success by default; explicit first/longest/shortest policies are available. Repetition is greedy. A later failure does not retry an earlier successful choice or shrink a repeat. Nullable repetitions throw rather than loop. Left recursion is unsupported; no memoization or linear-time guarantee is provided. Parsers are interpreted combinators, not an LR parse-table engine. The documented ABNF subset compiles to the same graph; arbitrary BNF/EBNF and generalized ambiguity handling are unsupported. Sources may be in memory or seekable disk snapshots. General regexes allocate full subjects; diagnostic line indexing scans the source in chunks and retains line offsets.
Documentation and verification
Using Laravel? The separate comprehend-laravel package provides grammar-based validation rules. See the Laravel integration guide for installation and usage; the Comprehend core remains framework independent.
Browse the documentation homepage for installation, tutorials and the complete documentation navigation. See website contribution instructions to preview and build the GitHub Pages site locally.
The 2.0.0 breaking-release guide covers caller changes, supported PHP versions and the gated publication workflow.
- Tutorial, builder, reference
- Consumer guide for AI agents: recommended API, value composition and failure reporting
- Parse application values: domain objects, optional fields and nested lists
- Sources and custom parser contract
- Consumption, diagnostics, trivia
- Recovery extension: optional synchronization, preserved invalid regions and multiple diagnostics
- Separated lists: bounds, boundary separators and lazy item values
- Delimiters and value selection:
between(),thenIgnore()andignoreThen() - Bundled extensions: loading and available vocabularies
- ASCII identifiers and keywords: token syntax and keyword boundaries
- Writing syntax extensions: custom factories and fluent operations
- Discovery and capability providers: manifests and provider contracts
- Expression builder extension: precedence, associativity and lazy reductions
- CST, AST, serialization
- Semantic values and lifecycle
- RFC libraries, contributing
- Canonical runtime and removal inventory
- Extraction comparison: reproducible application-value extraction measurements
For application values, map() transforms an existing value, collect() builds
an ordered list of operand values, and valueChoice() forwards the selected
alternative's value. These projections remain lazy and cached while retaining
the original syntax. See the value composition example.
Use manyValues(), someValues() or bounded repeatValues() for lists of item
values, and optionalValue(default: ...) for optional fields. See the
repeated and optional values example.
PHPDoc generics preserve mapping and parsing value types without PHPStan extensions
through composition and parsing; see static value types.
Install with Composer, then run:
composer check-platform-reqs php vendor/bin/pest --do-not-cache-result php tools/check-canonical.php php docs/examples/execution.php
CI runs these checks on PHP 8.2–8.5, with an experimental PHP 8.6 job. This is a breaking redesign: old matching entry points, offset/result adapters, magic rule dispatch, coercion, automatic callbacks and Token projections are removed. See the breaking changes. No backwards-compatibility window is provided.
