PHP TypeLang Specification

Status of This Document

This document is a Working Draft. It is a work in progress and is subject to change at any time. Publication of this document does not imply endorsement of its contents by any particular organization. It is inappropriate to cite this document as other than a work in progress.

1Requirements Notation

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals, as shown here.

2Introduction

This is the specification for TypeLang, a language for describing the types of values in the PHP ecosystem. TypeLang is not a programming language and has no runtime: a TypeLang document is a single type expression that denotes a set of values, such as those commonly written inside PHP docblocks (phpdoc), attributes, or configuration.

The syntax of TypeLang is based on, and is intentionally compatible with, the type grammars popularized by the static analysis tools PHPStan and Psalm, while unifying and extending them into a single, formally described grammar.

This specification describes the syntax of the language — the lexical structure of the source text and the syntactic structure of the resulting abstract syntax tree (AST). It deliberately does not prescribe the semantics of types (whether a referenced type physically exists, whether a generic accepts the given number of arguments, or whether two types are compatible). Those concerns are left to the implementation that consumes a TypeLang document.

This specification is maintained by the PHP Type Language project.

3Overview

TypeLang is a language for describing the types of PHP values. The vast majority of PHP developers encounter such type descriptions on a daily basis inside docblocks:

/**
 * @param int[] $param Example array parameter
 *
 * @return non-empty-string An example return type
 *
 * @throws \OutOfBoundsException in case of something went wrong
 */
function example(array $param): string {}

There are many possible variations of tags (identifiers starting with the @ symbol) in docblocks; however, they all share one common feature: the use of a type declaration syntax. In the example above, the type of the @param tag is int[], the type of @return is non-empty-string, and the type of @throws is \OutOfBoundsException.

TypeLang describes exactly this syntax. A complete TypeLang document is a single type expression; it can be embedded in docblocks, attributes, configuration files, IDE tooling, serializers, validators, mappers, or anywhere else a precise description of a PHP value is required.

3.1Design Principles

The language is defined according to the following principles.

Syntax, not semantics. This specification defines only what is well-formed. A conforming implementation MUST NOT impose restrictions on type naming, on the number of generic arguments, on the existence of a referenced class or constant, or on the logical consistency of a type. For example, the document int<string, max> is syntactically valid even though no implementation is likely to accept it semantically. Validation of such concerns MAY be performed by the consuming implementation but is out of scope for this specification.

Superset compatibility. TypeLang aims to parse every type expression accepted by PHPStan or Psalm, and to resolve ambiguities and inconsistencies between them in favour of the most permissive well-defined interpretation.

A single root. Unlike a general-purpose language, a TypeLang document is not a list of statements. It consists of exactly one Type (see Types); a document contains no declarations, no imports, and no execution.

3.2Conformance

A conforming implementation of TypeLang MUST accept every document that this specification defines as valid, and MUST reject every document that this specification defines as invalid. A conforming implementation MAY additionally accept extensions to the grammar defined herein, provided that every document accepted by such an extension would otherwise be rejected by this specification; an implementation MUST NOT extend the grammar in a way that causes a document valid under this specification to be assigned a different syntactic structure.

This specification defines only syntax. Whether a given document is semantically meaningful (for example, whether a referenced class exists, or whether the number of supplied generic arguments matches the number expected by a referenced type) is a matter for the consuming implementation and is outside the scope of conformance to this specification.

The illustrative error messages that accompany counter-examples throughout this specification (see Examples and Counter-Examples) are non-normative. A conforming implementation is not required to reproduce their exact wording, but MUST reject the corresponding document.

3.3Document

A TypeLang Document consists of a single Type expression. The source text of a document, after removal of Ignored tokens, MUST be described in full by a single Type; any trailing tokens that cannot be consumed by Type constitute a syntax error.

The following is an example of a valid document combining several features of the language:

array{
    id: int<1, max>,
    name: non-empty-string,
    roles: list<App\Domain\Role>,
    parent?: ?self,
    ...
}

3.4Relationship to Other Tools

TypeLang is a strict syntactic superset of the common subset shared by PHPStan and Psalm. Notable extensions beyond what those tools accept include, among others:

  • Binary (0b1010), octal (0o42, legacy 042) and hexadecimal (0xDEAD) integer literals.
  • Escape, hexadecimal and Unicode sequences inside double-quoted string literals.
  • Global constant masks such as JSON_*.
  • A uniform conditional (ternary) type syntax that also permits comparison operators and Yoda-style operands.

A detailed feature-by-feature comparison is maintained alongside the project documentation and is out of scope for this specification.

3.5A Note on Examples

Throughout this document, code examples are presented in fenced blocks. Examples that denote valid documents are shown directly; examples that denote a syntax error are explicitly marked as counter-examples and are accompanied by the kind of error a conforming parser is expected to raise.

3.6Security and Implementation Considerations

TypeLang documents are frequently extracted from sources that an implementation does not fully trust, such as docblocks or attributes authored by third-party packages. Implementers SHOULD take the following into account.

Recursion depth. Several productions of this grammar are directly or indirectly recursive (for example, a parenthesized Type, a generic TemplateArgument, a shape field value, and a callable parameter or return type may each themselves contain an arbitrary Type). An implementation that evaluates such productions using unbounded native recursion MAY be vulnerable to stack exhaustion when parsing a maliciously or accidentally deeply nested document. An implementation SHOULD impose an implementation-defined limit on nesting depth and MUST, upon reaching that limit, reject the document rather than exhibit undefined behaviour.

Input length. This specification does not limit the length of a Name, the number of TemplateArguments, the number of ShapeFields, or the overall length of a document. An implementation intended for use with untrusted input SHOULD impose implementation-defined limits on these quantities.

Numeric ranges. As described in Integer, an integer literal’s value is clamped to the representable range of the implementation’s platform integer while the raw lexeme is preserved. Implementations that convert a FloatLiteral to a native floating-point representation are subject to the usual limitations of that representation (precision loss, rounding, infinities) and SHOULD document their behaviour in such cases.

4Lexical Grammar

A TypeLang document is defined by a syntactic grammar whose terminal symbols are tokens (indivisible lexical units). Tokens are themselves defined by a lexical grammar that matches patterns of source characters. Throughout this document, syntactic grammar productions are distinguished by a single colon :, while lexical grammar productions are distinguished by a double colon :: (see Appendix A).

The source text of a TypeLang document MUST be a sequence of SourceCharacter. That sequence is first scanned, from left to right, into a sequence of Token and Ignored lexical units. The resulting token sequence, once every Ignored unit is discarded, MUST then be described in full by a single Type syntactic production (see Types).

Note See Appendix A for further information about the lexical and syntactic grammar and the other notational conventions used throughout this document.
Lexical Analysis

The source text is scanned by repeatedly taking the next longest possible sequence of code points permitted by the lexical grammar productions as the next token (a “maximal munch” longest-match discipline). Where this rule alone would be ambiguous — that is, where two or more productions match a sequence of equal length starting at the same position — the production order given in this section SHALL be authoritative: an earlier production is preferred over a later one.

4.1Source Text

SourceCharacter
Any Unicode code point

TypeLang documents are interpreted from a source text, which is a sequence of SourceCharacter. Any Unicode code point may appear in the source text.

Note This specification describes the lexical grammar in terms of Unicode code points. The reference implementation, in common with most PHP source tooling, instead operates on a byte string that is assumed (but not required) to be UTF-8 encoded. Within the ASCII range (U+0000 through U+007F), a byte and the code point it encodes coincide, and this specification’s rules apply identically whether stated in terms of bytes or of code points. For the non-ASCII range, this specification treats every byte from U+0080 through U+00FF individually as a Letter (see below) without decoding it; this happens to accept identifiers containing correctly encoded multi-byte UTF-8 sequences, but it does not itself validate that a source text is well-formed UTF-8.

For the purposes of the lexical grammar, a letter is any of the ASCII characters a through z and A through Z, together with every byte in the range U+0080 through U+00FF.

Letter
ABCDEFGHIJKLM
NOPQRSTUVWXYZ
abcdefghijklm
nopqrstuvwxyz
Any byte from 0x80 to 0xFF
Digit
0123456789

4.2Ignored Tokens

Ignored tokens improve readability and separate lexical tokens, but are otherwise insignificant. Any amount of Ignored MAY appear before and after every lexical Token.

Note Although Ignored between two tokens is never itself significant, the presence of Whitespace standing directly behind a NameToken is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where Ignored is observable to the syntactic grammar, and the one place a Comment does not stand for the whitespace it is written among: a comment MAY follow that whitespace, but MUST NOT stand in its stead.

4.2.1White Space

Whitespace
Horizontal Tab (U+0009)
Line Feed (U+000A)
Vertical Tab (U+000B)
Form Feed (U+000C)
Carriage Return (U+000D)
Space (U+0020)

Whitespace separates tokens and improves the legibility of the source text. TypeLang does not distinguish between horizontal whitespace and line terminators, and a Comment stands wherever whitespace stands, save for the one place the presence of the whitespace itself is read (see Ignored Tokens).

Note The six characters above are the whole of it. A source text is read as a sequence of bytes (see Source Text), so a character that parts words in a script of its own — a no-break space (U+00A0), say — is no whitespace here: every byte it is written of falls in the range a Letter covers, and it is read as a part of the name it stands in.

4.2.2Comments

LineTerminator
New Line (U+000A)Carriage Return (U+000D)

A TypeLang source document MAY contain comments. A line comment begins with either the // or # marker and continues up to, but not including, the next LineTerminator (or the end of the source, whichever occurs first). A block comment begins with /* and continues up to and including the next */.

Comments are Ignored and have no bearing on the meaning of a document.

4.3Lexical Tokens

A TypeLang document is composed of several kinds of indivisible lexical tokens, defined here by patterns of source characters. Lexical tokens MAY be separated by Ignored tokens and are used as the terminal symbols of the syntactic grammar.

4.3.1Punctuators

Punctuator
?|&*,:=
()[]{}
<>
::\...

TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, : and ::; . in ...); in every such case, the longest matching punctuator is taken.

4.3.2Names

A NameToken is the raw lexical unit that is later assembled, together with the namespace separator, into the syntactic Name production (see Names and Namespaces); the two are distinct non-terminals of, respectively, the lexical and the syntactic grammar. A NameToken MUST begin with a Letter or an underscore (_) and MAY continue with any Letter, Digit, underscore, or dash (-). The only difference from the PHP identifier grammar is that the dash character is additionally permitted in any non-leading position.

A source text is read as a sequence of bytes rather than of codepoints, and every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a Letter covers. A name MAY therefore be written in any script.

Проект\Тип

A NameToken is always the longest possible valid sequence; it MUST NOT be followed by a NameContinue character.

ExampleTypeName

Dashes are accepted in any non-leading position, which is what makes built-in type names such as non-empty-string and array-key expressible:

non-empty-string

The reserved words true, false and null are also valid as a fragment of a NameToken, provided the NameToken is not equal to one of those reserved words on its own (see Reserved Words):

true-type

Counter-example. A NameToken cannot begin with a Digit or a dash.

Counter Example № 142type
ParseException: Syntax error, unexpected "type"

4.3.3Reserved Words

ReservedWord
truefalsenullisnot

The words true, false and null are literal keywords (see Literal Tokens), and is and not are the conditional operator keywords (see Conditional Types). Each is matched only when not immediately followed by a NameContinue character. The three literal keywords are matched case-insensitively, while is and not are matched the one way they are spelled.

Wherever the source contains one of these words, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. It remains an Identifier all the same (see Names and Namespaces), so a reserved word is shadowed rather than forbidden: it stands as a name wherever the token it is scanned as denotes no type of its own.

A literal keyword standing alone, in a position where a type is expected, is read as the LiteralType it denotes and not as the NamedType it would otherwise be (see Primary Types):

TrUe

Behind a namespace separator or another segment, where no literal is expected, the same word is a name segment. This makes \true a reference to a type literally named true, distinct from the true literal:

\true

The conditional operator keywords denote no type of their own, so nothing shadows them, and a bare is or not is an ordinary named type:

not

4.3.4Variable

A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken, save for the dash a variable cannot carry. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).

The special variable $this is recognised as a distinct token and additionally denotes the current object type when used as a primary type (see Primary Types).

Note The leading $ distinguishes variables from names at the lexical level, so no reserved-word restriction applies to the part of a Variable token that follows the $.

4.4Literal Tokens

A literal denotes a single, specific PHP value. The lexical grammar recognises boolean, null, integer, floating-point and string literals.

4.4.1Boolean

The case-insensitive words true and false denote the two values of the PHP bool type. Case is not significant: true, TRUE and TruE all denote the same value.

4.4.2Null

The case-insensitive word null denotes the PHP null value. As with booleans, case is not significant.

4.4.3Integer

Sign
-+

An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a Sign. A NegativeSign makes the denoted value negative; a leading + denotes the same value the absence of a Sign does. Underscores (DigitSeparator) MAY appear as visual separators; they carry no meaning and do not affect the denoted value. A DigitSeparator MUST stand between two digits, and therefore may neither lead a run of digits, nor trail one, nor stand beside another DigitSeparator.

Digits
Digit(DigitSeparator?Digit)*

Decimal. A decimal literal other than a lone 0 MUST NOT begin with a zero, which is the mark of an OctalIntLiteral.

DecimalIntLiteral
SignoptNonZeroDigit(DigitSeparator?Digit)*
Signopt0
NonZeroDigit
123456789
1_000_000

Binary. Prefixed with 0b or 0B; digits are 0 and 1 only.

0b1010_1101

Octal. Prefixed with 0o or 0O or, as octal has been written since long before that prefix, with a leading 0 alone; digits are 0 through 7 only. A 0123 denotes the same value a 0o123 does.

OctalDigit
01234567
0o42
04_23

Counter-example. A leading zero makes a literal an octal, so a digit outside the octal radix may not follow one.

Counter Example № 208
ParseException: Syntax error, unexpected "8"

Hexadecimal. Prefixed with 0x or 0X; digits are 0 through 9 and a through f, in either case.

HexDigits
HexDigit(DigitSeparator?HexDigit)*
HexDigit
0123456789
abcdef
ABCDEF
0xDEAD_BEEF

Counter-example. A radix may only contain digits valid for that radix.

Counter Example № 30b101042
ParseException: Syntax error, unexpected "42"
Static Semantics.

An OctalIntLiteral written without an OctalIndicator is the legacy octal spelling kept for compatibility with historical PHP source. It is read in the same radix (base 8) the prefixed spelling is, so 042 denotes the value 34.

A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the implementation’s platform integer type, the numeric value MUST be clamped to the nearest representable bound (the platform’s PHP_INT_MAX or PHP_INT_MIN) while the raw lexeme MUST be preserved unchanged.

4.4.4Float

A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a Sign.

Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both:

0.9
.9
1.

Scientific notation uses the case-insensitive e indicator followed by a decimal exponent, which takes a Sign of its own:

10e-2
10e+2

Every run of digits a float is written of accepts the DigitSeparator under the same rule an integer does, and, unlike a DecimalIntLiteral, MAY begin with a zero:

2_3.4_5e-6_7

Counter-example. A lone decimal point is not a valid float.

Counter Example № 4.
ParseException: Syntax error, unexpected "."
Note Because the lexer takes the longest match, the source 1.23 is always a single FloatLiteral and never the two tokens 1.2 and 3.

4.4.5String

A string literal denotes a value of the PHP string type. Strings are delimited by single (') or double (") quotes. A delimiter, and the backslash itself, may be escaped by a preceding backslash.

'I am a single-quoted string with an escaped \' quote'
"I am a double-quoted string with an escaped \" quote"

The two forms differ in how they treat backslash escapes. A single-quoted string MUST be interpreted verbatim: the only meaningful escapes are \' and \\, and every other backslash MUST be preserved literally. A double-quoted string MUST interpret the full set of EscapeSequence productions described below.

Escape Sequences.
SimpleEscape
nrtvef$"\

Inside a double-quoted string, the following simple escape sequences are recognised, mirroring the PHP string syntax:

Sequence Produces
\n line feed (U+000A)
\r carriage return (U+000D)
\t horizontal tab (U+0009)
\v vertical tab (U+000B)
\e escape (U+001B)
\f form feed (U+000C)
\$ dollar sign (U+0024)
\" double quote (U+0022)
\\ reverse solidus (U+005C)

A HexEscape (\x followed by one or two hexadecimal digits) denotes the character with that byte value; for example, "\x41" denotes "A".

A UnicodeEscape (\u{...}) denotes the Unicode code point named by the hexadecimal value within the braces, emitted as its UTF-8 representation; the braces are REQUIRED. For example, "\u{E9}" denotes "é" (LATIN SMALL LETTER E WITH ACUTE, U+00E9).

Note None of the escape, hexadecimal, or Unicode sequences are interpreted inside a single-quoted string; there, \x41, \u{E9} and \n each denote those exact source characters, verbatim.

5Types

A TypeLang document is a single Type. This section defines the syntactic grammar of types: how the tokens produced by the Lexical Grammar are assembled into an abstract syntax tree.

The root non-terminal of every document is Type, which expands to a single Expression.

5.1Type Precedence

A Type is parsed as an operator-precedence hierarchy. From the loosest-binding construct to the tightest, the levels are:

  1. Conditional (ternary) types — … is … ? … : …
  2. Union types — A | B
  3. Intersection types — A & B
  4. Nullable prefix — ?A
  5. Postfix list and offset access — A[], A[K]
  6. Primary types — names, literals, callables, shapes, and parenthesized types

Each level is defined in terms of the next-tighter level, so that, for example, intersection binds more tightly than union and A & B | C is parsed as (A & B) | C. Parentheses (see Primary Types) MAY be used to override this precedence.

Note The nullable prefix binds more loosely than the postfix suffixes, so ?A[] denotes a nullable list, ?(A[]), and not a list of nullables.

5.2Conditional Types

A conditional type (also called a ternary type) selects between two types based on a comparison between a subject and a comparand. It is written as the subject, an operator, the comparand, a ?, the type chosen when the condition holds, a :, and the type chosen otherwise.

T is string ? int : bool

The negative-equality operator is spelled is not. The two words are separate tokens, so whitespace and comments MAY stand between them the way they MAY stand between any other two tokens.

T is not string ? int : bool

Either the subject or the comparand MAY be a Variable instead of a Type, including in “Yoda” order, where the variable appears on the right:

$value is array ? non-empty-array : null
array is $value ? non-empty-array : null
Note A bare Variable is only a valid type when it carries a ConditionalSuffix. A Variable MUST NOT otherwise stand alone as a type; the only variable-like token that may appear as a primary type on its own is $this (see Primary Types).

5.3Logical Types

TypeLang, like PHP, supports two composite (logical) type constructors — union and intersection — together with a nullable shorthand.

5.3.1Union Types

A union type is a sequence of two or more member types separated by the pipe character (|). It denotes a value that satisfies at least one of its members.

A | B | C
Note A UnionType with a single member denotes that member directly; no union node is produced unless at least two members are present.

5.3.2Intersection Types

An intersection type is a sequence of two or more member types separated by the ampersand character (&). It denotes a value that simultaneously satisfies every member.

A & B & C

Because intersection binds more tightly than union, the two constructors may be combined to express disjunctive and conjunctive normal forms, with parentheses used for the opposite grouping:

(A & B) | C
(A | B) & C

5.3.3Nullable Types

A nullable type is written with a leading question mark (?) and is shorthand for the union of its operand with null; that is, ?T denotes the same set of values as T | null.

?Example

Counter-example. The question mark must precede the type; a trailing question mark is not a nullable type.

Counter Example № 5Example?
ParseException: Syntax error, unexpected "?"

5.4List and Offset Access Types

A PrimaryType MAY be followed by zero or more postfix suffixes, applied left to right.

5.4.1List Syntax

An empty pair of square brackets ([]) forms the legacy list (array) syntax. It is equivalent to wrapping the operand in a list, and may be repeated to describe nested lists.

User[]
User[][]

The legacy list syntax accepts no key type between the brackets; for that, use offset access or the modern array<…> generic. A User[int] is a well-formed document all the same — an offset access type (see below) rather than a list, since int is a valid Type. It is the absence of a type between the brackets that selects the list reading:

User[int]

5.4.2Offset Access

A pair of square brackets enclosing a Type forms an offset access type, which denotes the type of the element addressed by that offset within the operand (typically a shape or an array).

T['offset']
T[U]
array{int, string}[0]

Because the offset is itself an arbitrary Type, any type — including objects, shapes, or conditional types — MAY be used as a key:

T<U>[object{key: int, ...}]

Counter-example. Each offset is enclosed in a single pair of brackets.

Counter Example № 6Collection[[Some]]
ParseException: Syntax error, unexpected "["

5.5Primary Types

A primary type is the tightest-binding form. It is one of: a parenthesized Type, the $this type, a literal type, a callable type (see Callable Types), or a named type.

A parenthesized type is used to override precedence; it denotes exactly the type it encloses:

(A | B) & C

The $this variable, when used as a primary type, denotes the current object type:

$this
Note Each of CallableType, NamedType, and the ClassConstant and ConstantMask forms of LiteralType begins with a Name. Once a complete Name has been recognized, these four are distinguished by the single token that immediately follows it: * selects a constant mask; :: selects a class constant; ( selects a callable type; < or { selects a generic or shape named type; any other following token (or the end of the document) leaves a plain named type.
Note A bare ReservedWord is likewise both a LiteralType and, through Identifier, a Name of a single segment (see Reserved Words). Where one source could be read as either, the LiteralType is the one taken, so a true denotes the boolean literal and never a type named true. An is and a not denote no literal of their own, and nothing is therefore left to shadow the named type each of them is.

5.6Names and Namespaces

A Name is a sequence of one or more Identifier segments joined by the namespace separator (\), mirroring PHP namespaces. A fully-qualified name additionally begins with a leading separator.

Example\Name
\Absolute\Type\Name

A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end.

Reserved words within names. Every reserved word — true, false, null, is and not (see Reserved Words) — MAY appear as an Identifier segment of a Name. In a qualified position — following a separator or another segment — such a word is a name segment and nothing besides. This makes \null a reference to a type named null, distinct from the null literal.

Counter-example. A name cannot end with a separator.

Counter Example № 7Example\Name\
ParseException: Syntax error, unexpected end of input

5.7Named Types

NamedType
Name(TemplateArguments|ShapeFields)?

A named type is a Name optionally followed by either a list of template arguments (generics) or a shape body. A bare named type is the most common type of all:

Path\To\ExampleClass

This grammar imposes no restriction on which names are valid; int, string, list, non-empty-string, and any user-defined class name are all parsed identically as named types. Whether a name refers to a built-in type, a class, an interface, an enum, or a type alias is a semantic concern for the implementation and is outside the scope of this specification.

5.8Generic Types

A generic type supplies a named type with one or more template arguments, each of which is itself a Type or a Wildcard. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.

Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no such limits.

Path\To\ExampleClass<T, U>
iterable<int<0, max>, Collection<User>>
HashMap<Request, User,>

Counter-example. At least one argument is required, and a leading comma is not permitted.

Counter Example № 8example<>
ParseException: Syntax error, unexpected ">"
Note Throughout this document, the term template argument refers to a value supplied at a use site, such as the int<0, max> in iterable<int<0, max>, …>. This is distinct from a template parameter, which would be part of a type’s (hypothetical) declaration. TypeLang describes only use sites, and therefore only template arguments.

5.8.1Template Argument Hints

A template argument MAY carry a single leading hint: a NameToken placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.

HashMap<array-key, covariant Request>

At the lexical level, Whitespace MUST stand directly behind the hint; this separation is what distinguishes a hint from the start of the argument’s own type, and a Comment does not stand in its place (see Ignored Tokens). Each argument may carry at most one hint.

Counter-example. A hint is a single NameToken. A second one reads as the bound of a template parameter, and a parameter list belongs to a callable, so the statement is refused where the ( it would go on with is missing.

Counter Example № 9HashMap<array-key, some covariant Request>
ParseException: Syntax error, unexpected end of input

Counter-example. A hint cannot be a reserved word: true, false, null, is and not are always scanned as the corresponding literal or operator token (see Reserved Words) and never as a NameToken.

Counter Example № 10HashMap<is Request>
ParseException: Syntax error, unexpected ">"

5.9Literal Types

A literal type denotes the singleton type containing exactly one value, or, in the case of a ConstantMask, a well-defined family of such singleton types. The boolean, null, integer, floating-point and string forms correspond directly to the literal tokens of the same name:

true
42
"Hello World"

A ClassConstant and a ConstantMask (see Constant Types, below) are likewise literal types, since each references a single PHP constant or a well-defined family of PHP constants. An unqualified reference to a global constant, by contrast, has no dedicated grammar of its own; it is described in Global Constants.

5.10Constant Types

TypeLang provides two dedicated grammar forms for referencing constants: a ClassConstant and a ConstantMask. Together with the naming convention for a single global constant described below, this specification uses the term constant type loosely to refer to all three collectively; only ClassConstant and ConstantMask, however, are formal non-terminals of the grammar.

Note Because the grammar of a class or global constant name is identical to that of a Name, constant names share its restrictions: they may not be one of the bare reserved words true, false or null. Unlike a conventional type name, however, a constant is conventionally written without a dash.

5.10.1Global Constants

Syntactically, an unqualified reference to a global constant is indistinguishable from, and MUST be parsed as, an ordinary named type (see Primary Types):

JSON_THROW_ON_ERROR
pcov\version

Whether a given NamedType denotes a global constant, a class, an interface, or something else entirely is a semantic concern that this specification leaves to the implementation.

5.10.2Class Constants

A reference to a class constant is a Name (the class), the :: separator, and an Identifier (the constant). The constant segment is a single identifier and cannot itself be namespaced.

ClassName::CONSTANT_NAME
Path\To\ClassName::ANOTHER_CONSTANT_NAME

Counter-example. The constant part may not contain a namespace separator.

Counter Example № 11ClassName::SOME\ANY
ParseException: Syntax error, unexpected "\"

5.10.3Constant Masks

A constant mask denotes a family of constants whose names are written of the same segments, in the same order, with a Wildcard standing wherever the name is left unsaid. A mask MUST contain at least one Wildcard, and its segments and wildcards alternate, so two wildcards in a row denote nothing a single one does not.

A global constant mask matches every global constant the segments describe:

JSON_*

The name of a global constant MAY be left unsaid from its very beginning, and the namespace it belongs to MAY be written in front of it. A Name standing in front of a MaskTail carries the namespace and, in its last segment, the leading segment of the mask; a Name separated from the mask by a \ is a namespace whole, and the mask then matches every constant that namespace holds:

*_SUFFIX
Path\To\JSON_*
Path\To\*

A class constant mask matches every constant of a class the segments describe; the leading segment MAY be omitted entirely, in which case the mask matches every constant of the class:

Path\To\ClassName::PREFIX_*
Path\To\ClassName::*_SUFFIX
Path\To\ClassName::PREFIX_*_SUFFIX
Path\To\ClassName::*

Counter-example. A global mask must have at least one segment of a name, whether it stands in front of a wildcard or behind it; a lone asterisk, which would match every constant there is, is not a type.

Counter Example № 12*
ParseException: Syntax error, unexpected end of input

Counter-example. Two wildcards in a row are not a mask.

Counter Example № 13Path\To\ClassName::PREFIX_**
ParseException: Syntax error, unexpected "*"

6Structural Types

This section defines the structural type forms — shapes and callables. Each of these forms builds on the named type grammar introduced in the previous section.

6.1Shape Types

A shape rigidly describes the individual elements of a composite value, such as the keys of an array or the properties of an object. A shape is written as a named type immediately followed by a brace-enclosed body; the name — commonly array, object, or list, but any name is accepted — describes the container, and the body describes its elements.

array{
    a: First,
    b: Second
}
Custom\Type{
    id: int,
    name: string
}

An empty shape body describes a container with no elements:

array{}

6.1.1Shape Fields

A shape field is either explicit (a key, a colon, and a value type) or implicit (a value type alone, whose key is assigned positionally). A shape key may be a bare identifier, an integer literal, a string literal, or a constant reference — that is, a ClassConstant or a ConstantMask.

Note An Identifier, a ClassConstant and a ConstantMask may each begin with the same Name. As in Primary Types, these are distinguished by what follows: a bare Identifier key is one not followed by :: or a trailing *.
array{ name: First, count: Second }
array{ 1: First, 42: Second }
array{ "name-some": First, "escape\nchars": Second }
array{ First, Second }
array{ Path\To\ClassName::CONSTANT_NAME: First, JSON_*: Second }

Mixed keys are not permitted. A single shape MUST use either explicit keys throughout or implicit keys throughout.

Counter Example № 14array{ named: First, Second }
ParseException: Cannot mix explicit and implicit shape keys

Duplicate explicit keys are not permitted. No two explicit fields of the same shape may denote the same key, regardless of which of the five key forms each uses.

Counter Example № 15array{ 1: int, 2: int, 1: string }
ParseException: Duplicate key "1"

6.1.2Optional Fields

An explicit field MAY be marked optional by placing a question mark before the colon. An optional key (key?: Type) states that the field may be absent; this is distinct from an optional value (key: ?Type), which states that the field is always present but its value may be null.

array{ key?: Type }
array{ key: ?Type }

6.1.3Unsealed Shapes

By default, a shape is sealed: it describes its container exactly, and no additional elements are permitted. A trailing ellipsis (...) makes the shape unsealed, allowing elements beyond those listed.

array{ key: Type, ... }

An unsealed shape MAY also stand alone, describing a container constrained only by its (absent) field list:

array{ ... }

An unsealed marker MAY carry template arguments that describe the type of the additional elements — and, optionally, of their keys — using the same angle-bracket syntax as generics:

array{ user: User, ...<string, object> }

6.2Callable Types

A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — optionally followed by the template parameters it declares, then by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.

callable()
callable(): void
Closure(int<0, max>, callable(?C): mixed): void

6.2.1Template Parameters

A callable type MAY declare the template parameters it introduces, written as a <...> list between the name and the parameter list. Each parameter is an Identifier followed by as many bounds as are put on it.

A parameter accepts three kinds of limit, each of which MUST be written at most once. The two bounds MAY be written in either order, while a TemplateDefault MUST be written last: a bound standing behind it belongs to the Type of the default rather than to the parameter.

  • An UpperBound narrows the parameter from above: the argument is to be a subtype of the given Type. Its two operators denote the same thing, and a conforming implementation MUST retain the one that was written.
  • A LowerBound bounds the parameter from below: the argument is to be a supertype of the given Type.
  • A TemplateDefault is the Type the parameter takes when no argument is supplied. It bounds nothing.

Each operator is matched the one way it is spelled, so an OF bounds nothing an of does.

callable<T>(T): T
callable<T of Some>(T): T
Closure<T of Some, U super Any, V = int>(T, U): V
callable<T of Some super Any = int>(T): void

Counter-example. A word that bounds nothing is not an operator.

Counter Example № 16callable<T whatever Some>(): void
ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super"

Counter-example. Neither is a word that is spelled the way an operator is spelled, but written in a case of its own.

Counter Example № 17callable<T OF Some>(): void
ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super"

Counter-example. Each kind of limit is written at most once.

Counter Example № 18callable<T of Some as Any>(): void
ParseException: Template parameter cannot have more than one upper bound

Counter-example. A bound does not stand behind a default.

Counter Example № 19callable<T = int of Some>(): void
ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself

A <...> list is written the same way whether it declares template parameters or supplies template arguments, and only the ( that follows a parameter list tells them apart. A list that no ( follows is therefore a list of template arguments, which describes no bounds.

Counter-example. Bounds belong to a callable alone.

Counter Example № 20Collection<T of Some>
ParseException: Syntax error, unexpected end of input

6.2.2Callable Parameters

A parameter is described by its type, optionally followed by the reference and the variadic markers, by a name and by the default marker — in that order and in no other. The type is the only required part.

callable(Type)
callable(Type $name)

Counter-example. A parameter given by name alone, without a type, is not permitted.

Counter Example № 21callable($name)
ParseException: Syntax error, unexpected ")"

Named Parameters. A name beginning with $ MAY follow the parameter’s type, permitting the argument to be passed by name, exactly as in PHP.

callable(A $a, B, C)

Output Parameters. An ampersand (&) placed after the parameter type marks the parameter as passed by reference (an output parameter).

callable(T&)
callable(T &$name)

Counter-example. The ampersand must follow the type; it must not precede it.

Counter Example № 22callable(&T)
ParseException: Syntax error, unexpected "&"

Optional Parameters. A trailing = marks a parameter as optional: the caller MAY omit the corresponding argument.

callable(T=)
callable(T &$name=)

Variadic Parameters. An ellipsis (...) placed after the parameter type marks the parameter as variadic. Where a parameter carries both markers, the ampersand comes first.

callable(T ...$name)
callable(T &...$name)

Counter-example. The ellipsis must follow the type; it must not precede it.

Counter Example № 23callable(...T)
ParseException: Syntax error, unexpected "..."

Counter-example. A variadic parameter is already optional and therefore must not additionally carry a default marker.

Counter Example № 24callable(T ...$name=)
ParseException: Cannot have variadic param with a default

AAppendix: Notation Conventions

This specification uses a number of notation conventions to describe the language grammar. This appendix explains those notations so as to avoid ambiguity, and is itself non-normative: it describes how to read the productions found elsewhere in this document, but introduces no new requirement of its own.

A.1Context-Free Grammar

A context-free grammar consists of a number of productions. Each production has an abstract symbol, called a non-terminal, as its left-hand side, and one or more sequences of non-terminal symbols and terminal characters as its right-hand side.

Starting from a single goal non-terminal (Type, for TypeLang), the grammar describes a language: the set of character sequences obtained by repeatedly replacing a non-terminal with one of its right-hand sides until only terminals remain.

Terminals are written in a monospace font, either as a specific character or sequence (for example | or is), or as prose describing a code point (for example "New Line (U+000A)").

A production with a single definition is written on one line:

NonTerminalWithSingleDefinition
NonTerminalterminal

A production with several alternative definitions is written as a list:

NonTerminalWithManyDefinitions
OtherNonTerminalterminal
terminal

A definition may refer to itself to describe a repetitive sequence:

A.2Lexical and Syntactic Grammar

TypeLang is defined by two grammars. The lexical grammar matches patterns of source characters into tokens; the syntactic grammar matches patterns of tokens into the abstract syntax tree.

A lexical grammar production is distinguished by a double colon ::. No Ignored characters may appear between the terminals of a lexical production.

A syntactic grammar production is distinguished by a single colon :. Ignored tokens may appear before or after any terminal token of a syntactic production.

A.3Grammar Notation

one of. A production whose alternatives are each a single terminal may be written compactly with the phrase “one of”:

Operator
|&?

is shorthand for

Operator
|
&
?

Optionality. A subscript-style suffix ? denotes an optional symbol: one sequence including it, and one excluding it.

is shorthand for

Lists. A suffix * denotes zero or more repetitions of a symbol; a suffix + denotes one or more. For example, Identifierlist matches a non-empty run of Identifier.

Constraints (but not). The phrase “but not” excludes certain expansions that would otherwise be permitted.

NonReserved
Nametruefalsenull

means that a NonReserved may be any Name except those three sequences.

Lookahead Restrictions. A restriction of the form [lookahead != X] states that the production must not be followed by X. Lookahead restrictions remove ambiguity and, together with longest-match scanning, ensure a single valid lexical analysis. For example:

makes explicit that a NameToken is always the longest possible sequence, and cannot be followed by another NameContinue character.

Overlapping Alternatives. The order in which a production’s alternatives are written carries no meaning by itself. Some alternatives, however, cannot be told apart by their first symbol alone — for example, ClassConstant, ConstantMask, CallableType and NamedType (see Primary Types) all begin with a Name. In every such case, this specification identifies, either through an explicit lookahead restriction or through an accompanying Note, the further input that determines which alternative applies at that position — typically the single token that follows a fully recognized Name or Identifier. The grammar described by this specification is unambiguous in the sense that, once that further input is consulted, at most one alternative can match at any position. This specification prescribes neither a parsing algorithm nor an implementation strategy; it is satisfied by any implementation — whether based on recursive descent, an LL(k) or LALR parser generator, a parsing expression grammar, or any other technique — that accepts exactly the documents, and performs exactly the disambiguation, described herein.

A.4Grammar Semantics

Some productions are accompanied by a Static Semantics description, which explains how a conforming implementation is to interpret the matched source beyond merely accepting it — for example, how the radix of an integer literal is determined, or how an out-of-range value is clamped. Static semantics never alter which documents are accepted; they only describe the value or node that a valid document denotes.

A.5Examples and Counter-Examples

Code blocks in this document illustrate the grammar. A block presented without qualification denotes a valid document. A block explicitly introduced as a counter-example denotes an invalid document, and is typically followed by the kind of error a conforming implementation is expected to raise. Such error messages are illustrative and non-normative; their exact wording is not prescribed by this specification, and an implementation MAY report a different message, provided that it rejects the document (see Conformance).

BAppendix: Grammar Summary

This appendix consolidates every grammar production defined in this specification. Lexical productions (double colon ::) appear first, followed by syntactic productions (single colon :). It is provided for convenience and is non-normative; in the event of any discrepancy between this appendix and the body of this specification, the body governs.

Source Text
SourceCharacter
Any Unicode code point
Letter
ABCDEFGHIJKLM
NOPQRSTUVWXYZ
abcdefghijklm
nopqrstuvwxyz
Any byte from 0x80 to 0xFF
Digit
0123456789
Ignored Tokens
Whitespace
Horizontal Tab (U+0009)
Line Feed (U+000A)
Vertical Tab (U+000B)
Form Feed (U+000C)
Carriage Return (U+000D)
Space (U+0020)
LineTerminator
New Line (U+000A)Carriage Return (U+000D)
Lexical Tokens
Punctuator
?|&*,:=
()[]{}
<>
::\...
ReservedWord
truefalsenullisnot
Literal Tokens
Sign
-+
Digits
Digit(DigitSeparator?Digit)*
DecimalIntLiteral
SignoptNonZeroDigit(DigitSeparator?Digit)*
Signopt0
NonZeroDigit
123456789
OctalDigit
01234567
HexDigits
HexDigit(DigitSeparator?HexDigit)*
HexDigit
0123456789
abcdef
ABCDEF
SimpleEscape
nrtvef$"\
Types
Logical Types
List and Offset Access Types
Primary Types
Names
Named and Generic Types
NamedType
Name(TemplateArguments|ShapeFields)?
Literal and Constant Types
Shape Types
Callable Types

§Index

  1. BinaryDigit
  2. BinaryDigits
  3. BinaryIndicator
  4. BinaryIntLiteral
  5. BlockComment
  6. BlockCommentChar
  7. BoolLiteral
  8. CallableParameter
  9. CallableParameters
  10. CallableReturnType
  11. CallableType
  12. ClassConstant
  13. ClassConstantMask
  14. Comment
  15. CommentChar
  16. ConditionalOperand
  17. ConditionalOperator
  18. ConditionalSuffix
  19. ConditionalType
  20. ConstantMask
  21. DecimalIntLiteral
  22. Digit
  23. Digits
  24. DigitSeparator
  25. Document
  26. DoubleQuotedString
  27. DoubleStringChar
  28. EscapeSequence
  29. ExplicitField
  30. ExponentFloatLiteral
  31. ExponentIndicator
  32. ExponentPart
  33. Expression
  34. FloatLiteral
  35. FullyQualifiedName
  36. GlobalConstantMask
  37. HexDigit
  38. HexDigits
  39. HexEscape
  40. HexIndicator
  41. HexIntLiteral
  42. Identifier
  43. Ignored
  44. ImplicitField
  45. IntersectionType
  46. IntLiteral
  47. LeadingFloatLiteral
  48. LeadingMask
  49. Letter
  50. LineComment
  51. LineCommentStart
  52. LineTerminator
  53. ListSuffix
  54. LiteralType
  55. LogicalType
  56. LowerBound
  57. MaskTail
  58. Name
  59. NameContinue
  60. NamedType
  61. NameStart
  62. NameToken
  63. NegativeSign
  64. NonZeroDigit
  65. NullableType
  66. NullLiteral
  67. OctalDigit
  68. OctalDigits
  69. OctalIndicator
  70. OctalIntLiteral
  71. OffsetSuffix
  72. PostfixType
  73. PrimaryType
  74. Punctuator
  75. RelativeName
  76. ReservedWord
  77. ShapeBody
  78. ShapeField
  79. ShapeFieldList
  80. ShapeFields
  81. ShapeKey
  82. ShapeValue
  83. Sign
  84. SimpleEscape
  85. SingleQuotedString
  86. SingleStringChar
  87. SourceCharacter
  88. StringLiteral
  89. TemplateArgument
  90. TemplateArgumentHint
  91. TemplateArguments
  92. TemplateArgumentValue
  93. TemplateBound
  94. TemplateDefault
  95. TemplateParameter
  96. TemplateParameters
  97. ThisVariable
  98. Token
  99. TrailingFloatLiteral
  100. Type
  101. TypeSuffix
  102. UnaryType
  103. UnicodeEscape
  104. UnionType
  105. UnsealedShape
  106. UpperBound
  107. UpperBoundOperator
  108. Variable
  109. VariableContinue
  110. Whitespace
  111. Wildcard
  1. 1Requirements Notation
  2. 2Introduction
  3. 3Overview
    1. 3.1Design Principles
    2. 3.2Conformance
    3. 3.3Document
    4. 3.4Relationship to Other Tools
    5. 3.5A Note on Examples
    6. 3.6Security and Implementation Considerations
  4. 4Lexical Grammar
    1. 4.1Source Text
    2. 4.2Ignored Tokens
      1. 4.2.1White Space
      2. 4.2.2Comments
    3. 4.3Lexical Tokens
      1. 4.3.1Punctuators
      2. 4.3.2Names
      3. 4.3.3Reserved Words
      4. 4.3.4Variable
    4. 4.4Literal Tokens
      1. 4.4.1Boolean
      2. 4.4.2Null
      3. 4.4.3Integer
      4. 4.4.4Float
      5. 4.4.5String
  5. 5Types
    1. 5.1Type Precedence
    2. 5.2Conditional Types
    3. 5.3Logical Types
      1. 5.3.1Union Types
      2. 5.3.2Intersection Types
      3. 5.3.3Nullable Types
    4. 5.4List and Offset Access Types
      1. 5.4.1List Syntax
      2. 5.4.2Offset Access
    5. 5.5Primary Types
    6. 5.6Names and Namespaces
    7. 5.7Named Types
    8. 5.8Generic Types
      1. 5.8.1Template Argument Hints
    9. 5.9Literal Types
    10. 5.10Constant Types
      1. 5.10.1Global Constants
      2. 5.10.2Class Constants
      3. 5.10.3Constant Masks
  6. 6Structural Types
    1. 6.1Shape Types
      1. 6.1.1Shape Fields
      2. 6.1.2Optional Fields
      3. 6.1.3Unsealed Shapes
    2. 6.2Callable Types
      1. 6.2.1Template Parameters
      2. 6.2.2Callable Parameters
  7. AAppendix: Notation Conventions
    1. A.1Context-Free Grammar
    2. A.2Lexical and Syntactic Grammar
    3. A.3Grammar Notation
    4. A.4Grammar Semantics
    5. A.5Examples and Counter-Examples
  8. BAppendix: Grammar Summary
  9. §Index