Search by

vanderlee / comprehend

vanderlee

PHP parser combinators with ABNF authoring, immutable syntax trees and explicit effects

Package info

github.com/vanderlee/Comprehend

pkg:composer/vanderlee/comprehend

Statistics

Installs: 45

Dependents: 1

Suggesters: 0

Stars: 9

Open Issues: 0

2.0.2 2026-10-24 10:32 UTC

README

Comprehend logo

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.

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.