PHP TypeLang Help

Combinators

Every named building block a tag's grammar is assembled from — a type, a variable, a URI, an email address — is a combinator: a small object that knows how to read exactly one piece of syntax and nothing else. Spec::rule(TypeCombinator::NAME, 'type') in @param's definition, for instance, refers to the combinator below by name.

/** * @template-covariant TResult of mixed = mixed */ interface CombinatorInterface { /** * @return TResult */ public function __invoke(Cursor $cursor): mixed; }

Given a Cursor positioned at the start of whatever text is still left to parse, a combinator either consumes the part that belongs to it and returns the value it read, or throws NoMatchException and leaves the cursor exactly where it found it. That second case matters: a combinator that partially consumes input before deciding it does not fit would corrupt whichever alternative the surrounding grammar tries next — see Spec::oneOf(), which relies on being able to roll back cleanly.

The Cursor itself offers the handful of reading operations most combinators need, so a combinator rarely has to touch its raw position by hand: peek() and read() for a fixed number of bytes, readWhile()/readUntil() for a run of (or up to) a set of characters, readWord() for the next whitespace-delimited word, readPhpIdentifier()/readPhpQualifiedName() for a name (with or without namespace separators), readLiteral() for an exact piece of text, and readRemainder() for whatever is left entirely.

Built-in Combinators

Fourteen combinators cover every built-in tag's grammar between them:

Name

Returns

Reads

Visibility

Visibility

Method or property visibility.

AuthorName

string

Everything up to an optional "<".

CallableType

TypeReference

A TypeLang callable type.

Description

DescriptionInterface

Everything left, recursively parsed for inline tags.

Email

string

An address, up to its closing ">".

Integer

int

A non-negative integer.

IssueName

string

Letters, digits, _, - and ..

Name

string

A single identifier.

Reference

CodeReference

A class, function, method, constant, property or variable.

Type

TypeReference

A full TypeLang type.

URI

UriReference

A well-formed word, per RFC 3986.

URL

UrlReference

A word that additionally carries a scheme.

Variable

string

A $name, without the leading $.

Version

string

A word beginning with a digit;
anything else is left for the description.

Writing One of Your Own

A combinator that reads one of a fixed set of keywords looks much like the built-in Access combinator above. This one reads a low/medium/high priority level:

enum Priority: string { case Low = 'low'; case Medium = 'medium'; case High = 'high'; } final readonly class PriorityCombinator implements CombinatorInterface { public const string NAME = 'Priority'; public function __invoke(Cursor $cursor): Priority { $priority = Priority::tryFrom($cursor->readWord()); if ($priority === null) { throw new NoMatchException('Expected a priority level'); } return $priority; } }

Three habits keep a combinator well-behaved inside a larger grammar:

  • Fail before consuming. Throw NoMatchException the moment the input is found not to fit, before reading anything that would belong to what comes next — a combinator that consumes first and validates afterward leaves the cursor somewhere a sibling alternative can no longer make sense of.

  • Consume only your own syntax. Whitespace meant to separate one grammar element from the next is handled by Spec::sequence(), not by the combinator itself; reading past it leaves nothing for the rest of the grammar to match.

  • Fail rather than guess. When more than one reading of the same input would be plausible, throwing and letting a different alternative in a surrounding Spec::oneOf() take over is safer than silently picking one.

Once written, a combinator with a NAME constant is used from a tag's $spec exactly like a built-in one — see Grammar. To make it available to the parser, register it on a platform.

07 July 2026