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_*.
  • Attributes (#[...]) on template arguments, shape fields and callable parameters.
  • 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 U+0080 to U+00FF
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 separating whitespace 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 the presence of separating whitespace is observable to the syntactic grammar.

4.2.1White Space

Whitespace
Any Unicode whitespace character

Whitespace separates tokens and improves the legibility of the source text. It includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed (U+000A) and carriage return (U+000D) characters. TypeLang does not distinguish between horizontal whitespace and line terminators, except within the multi-word is not operator (see Conditional Types).

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.

Note The # line comment marker and the #[ attribute marker share a leading character. Because lexical analysis prefers the longest match, the sequence #[ is always scanned as the start of an attribute (see Attributes) rather than as the start of a comment.

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 <=; : and ::; . in ...; # 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 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
truefalsenullis

The words true, false and null are literal keywords (see Literal Tokens), and is is the conditional operator keyword (see Conditional Types). All four are matched case-insensitively, and only when not immediately followed by a NameContinue character.

When the source contains one of these words standing alone, in a position where a type is expected, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. Consequently, a bare reserved word cannot be used as a type name.

Counter-example. A bare reserved word is scanned as a literal, not a type name.

Counter Example № 2TrUe

A reserved word MAY, however, appear as an Identifier inside a qualified Name — that is, when it is preceded by a namespace separator or another identifier (see Names and Namespaces). For example, \true references a type literally named true, whereas the bare true is the boolean literal.

4.3.4Variable

A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken. 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

An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a NegativeSign. Within the digits of any integer literal, underscores (DigitSeparator) MAY appear freely as visual separators; they carry no meaning and do not affect the denoted value.

Decimal.
1_000_000

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

0b1010_1101

Octal. Prefixed with 0o or 0O; digits are 0 through 7 only.

OctalDigit
01234567
0o42

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

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.

A DecimalIntLiteral whose first digit is 0 and which is longer than a single character is interpreted in the legacy octal radix (base 8), for compatibility with historical PHP source. Thus, 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 NegativeSign.

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 an optionally negative decimal exponent:

10e-2

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

ConditionalOperator
isis not>=<=<>

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. At the lexical level, the two words MUST be separated only by horizontal whitespace and are scanned as a single token; no line terminator may appear between them.

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

In addition to is and is not, the relational operators <, >, <= and >= MAY be used as the condition operator. This is an extension beyond what PHPStan and Psalm accept; see Relationship to Other Tools.

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[][]

Counter-example. The legacy list syntax does not accept a key type between the brackets; for that, use offset access or the modern array<…> generic.

Counter Example № 6User[int]

This document, however, parses User[int] successfully — as an offset access type (see below) rather than a list — because int is a valid Type. It is the absence of a type between the brackets that selects the list interpretation.

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 № 7Collection[[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.

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. The reserved words true, false, null and is (see Reserved Words) MAY appear as an Identifier segment of a Name. In isolation, such a word is scanned as a literal or operator and so cannot stand as a bare type name; but in a qualified position — following a separator or another segment — it denotes a name segment. 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 № 8Example\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. 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 № 9example<>
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: an Identifier 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, the hint and the type it modifies MUST be separated by whitespace; this separation is what distinguishes a hint from the start of the argument’s own type (see Ignored Tokens). Each argument may carry at most one hint.

Counter-example. A hint must be a single valid identifier.

Counter Example № 10HashMap<array-key, some covariant Request>
ParseException: Syntax error, unexpected "Request"

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

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

5.8.2Template Argument Attributes

Each template argument MAY additionally be prefixed with one or more attribute groups, providing metadata for the argument.

HashMap<#[name("key")] T, #[name("value")] U>

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 № 12ClassName::SOME\ANY
ParseException: Syntax error, unexpected "\"

5.10.3Constant Masks

A constant mask denotes a family of constants whose names share a common prefix. A mask MUST terminate with an asterisk (*).

A global constant mask matches every global constant beginning with the prefix:

JSON_*

A class constant mask matches every constant of a class whose name begins with the prefix; the prefix MAY be omitted entirely, in which case the mask matches every constant of the class:

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

Counter-example. A global mask must have a prefix; a lone asterisk is not a type.

Counter Example № 13*
ParseException: Syntax error, unexpected "*"

Counter-example. The asterisk must be the final character of the mask.

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

6Structural Types

This section defines the structural type forms — shapes and callables — and the attribute syntax shared by several constructs. 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 № 15array{ 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 № 16array{ 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.1.4Shape Field Attributes

Each shape field MAY be prefixed with one or more attribute groups:

App\Domain\User{
    #[name("user_name")]
    userName: non-empty-string,
    #[skip_when_empty]
    friends: list<App\Domain\User>,
    ...
}

6.2Callable Types

A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — followed 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.1Callable Parameters

ParameterModifiers
&...opt
...&opt

A parameter is described by its type, optionally followed by a name. A parameter MAY also be given by name alone, without a type.

callable(Type)
callable(Type $name)
callable($name)

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 № 17callable(&T)
ParseException: Syntax error, unexpected "T"

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 (...) marks a parameter as variadic. It MAY be written either before the type or after it, immediately before the name.

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

Counter-example. The ellipsis may appear in only one position; a parameter that is variadic in both the prefix and suffix positions is an error.

Counter Example № 18callable(...T...)
ParseException: Either prefix or postfix variadic syntax should be used, but not both

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

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

6.2.2Callable Parameter Attributes

Each callable parameter MAY be prefixed with one or more attribute groups:

Example\Functor(#[type<int8>] int $a): void

6.3Attributes

An attribute attaches arbitrary, implementation-defined metadata to the construct it precedes. The syntax mirrors PHP attributes: each attribute is a name optionally followed by a parenthesized list of arguments, and each argument is itself an arbitrary Type — including, recursively, a type that carries attributes of its own.

Attributes are written in groups delimited by #[ and ]. A construct MAY carry several attributes within one group, separated by commas, and several groups in sequence. Attributes may be attached to a template argument, a shape field, or a callable parameter.

A single attribute with one argument:

Example\Functor(#[type<int8>] int $a): void

Several attributes within one group:

HashMap<#[name("key"), out] T>

Several groups in sequence:

array{
    #[serialize("onSerialize")]
    #[deserialize("onDeserialize")]
    test?: App\Domain\User,
}

Counter-example. An attribute name must be a valid Name; a literal is not permitted in that position.

Counter Example № 20Collection<#[42] User>
ParseException: Syntax error, unexpected "42"
Note Because the #[ attribute marker is scanned as a single token by the lexical grammar, it is never confused with the # line-comment marker.

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 U+0080 to U+00FF
Digit
0123456789
Ignored Tokens
Whitespace
Any Unicode whitespace character
LineTerminator
New Line (U+000A)Carriage Return (U+000D)
Lexical Tokens
Punctuator
?|&*,:=
()[]{}
<><=>=
::\...#[
ReservedWord
truefalsenullis
Literal Tokens
OctalDigit
01234567
HexDigit
0123456789
abcdef
ABCDEF
SimpleEscape
nrtvef$"\
Types
ConditionalOperator
isis not>=<=<>
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
ParameterModifiers
&...opt
...&opt
Attributes

§Index

  1. Attribute
  2. AttributeArgument
  3. AttributeArguments
  4. AttributeGroup
  5. AttributeGroups
  6. AttributeList
  7. BinaryDigit
  8. BinaryIndicator
  9. BinaryIntLiteral
  10. BlockComment
  11. BlockCommentChar
  12. BoolLiteral
  13. CallableParameter
  14. CallableParameterBody
  15. CallableParameters
  16. CallableReturnType
  17. CallableType
  18. ClassConstant
  19. Comment
  20. CommentChar
  21. ConditionalOperand
  22. ConditionalOperator
  23. ConditionalSuffix
  24. ConditionalType
  25. ConstantMask
  26. DecimalIntLiteral
  27. Digit
  28. DigitSeparator
  29. Document
  30. DoubleQuotedString
  31. DoubleStringChar
  32. EscapeSequence
  33. ExplicitField
  34. ExponentFloatLiteral
  35. ExponentIndicator
  36. ExponentPart
  37. Expression
  38. FloatLiteral
  39. FullyQualifiedName
  40. HexDigit
  41. HexEscape
  42. HexIndicator
  43. HexIntLiteral
  44. Identifier
  45. Ignored
  46. ImplicitField
  47. IntersectionType
  48. IntLiteral
  49. LeadingFloatLiteral
  50. Letter
  51. LineComment
  52. LineCommentStart
  53. LineTerminator
  54. ListSuffix
  55. LiteralType
  56. LogicalType
  57. Name
  58. NameContinue
  59. NamedType
  60. NameStart
  61. NameToken
  62. NegativeSign
  63. NullableType
  64. NullLiteral
  65. OctalDigit
  66. OctalIndicator
  67. OctalIntLiteral
  68. OffsetSuffix
  69. ParameterModifiers
  70. PostfixType
  71. PrimaryType
  72. Punctuator
  73. RelativeName
  74. ReservedWord
  75. ShapeBody
  76. ShapeField
  77. ShapeFieldList
  78. ShapeFields
  79. ShapeKey
  80. ShapeValue
  81. SimpleEscape
  82. SingleQuotedString
  83. SingleStringChar
  84. SourceCharacter
  85. StringLiteral
  86. TemplateArgument
  87. TemplateArgumentHint
  88. TemplateArguments
  89. TemplateArgumentType
  90. ThisVariable
  91. Token
  92. TrailingFloatLiteral
  93. Type
  94. TypeSuffix
  95. UnaryType
  96. UnicodeEscape
  97. UnionType
  98. UnsealedShape
  99. Variable
  100. Whitespace
  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
      2. 5.8.2Template Argument Attributes
    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
      4. 6.1.4Shape Field Attributes
    2. 6.2Callable Types
      1. 6.2.1Callable Parameters
      2. 6.2.2Callable Parameter Attributes
    3. 6.3Attributes
  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