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, legacy042) 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).
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
TypeLang documents are interpreted from a source text, which is a sequence of SourceCharacter. Any Unicode code point may appear in the source text.
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.
| A | B | C | D | E | F | G | H | I | J | K | L | M |
| N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
| a | b | c | d | e | f | g | h | i | j | k | l | m |
| n | o | p | q | r | s | t | u | v | w | x | y | z |
| Any byte from U+0080 to U+00FF |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
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.
4.2.1White Space
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
| // | # |
| 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.
# 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
| ? | | | & | * | , | : | = |
| ( | ) | [ | ] | { | } | |
| < | > | <= | >= | |||
| :: | \ | ... | #[ |
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
| true | false | null | is |
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).
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
| true | false | NameContinue |
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.
| 0 | 1 |
0b1010_1101
Octal. Prefixed with 0o or 0O; digits are 0 through 7 only.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
0o42
Hexadecimal. Prefixed with 0x or 0X; digits are 0 through 9 and a through f, in either case.
| x | X |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| a | b | c | d | e | f | ||||
| A | B | C | D | E | F |
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 "."
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.
| n | r | t | v | e | f | $ | " | \ |
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).
\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:
- Conditional (ternary) types —
… is … ? … : … - Union types —
A | B - Intersection types —
A & B - Nullable prefix —
?A - Postfix list and offset access —
A[],A[K] - 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.
?A[] denotes a nullable list, ?(A[]), and not a list of nullables.5.2Conditional Types
| is | is 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.
$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
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
* 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
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 ">"
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.
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.
:: 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
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"
#[ 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:
A production with several alternative definitions is written as a list:
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”:
| | | & | ? |
is shorthand for
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.
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
| A | B | C | D | E | F | G | H | I | J | K | L | M |
| N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
| a | b | c | d | e | f | g | h | i | j | k | l | m |
| n | o | p | q | r | s | t | u | v | w | x | y | z |
| Any byte from U+0080 to U+00FF |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Ignored Tokens
| New Line (U+000A) | Carriage Return (U+000D) |
| // | # |
Lexical Tokens
| ? | | | & | * | , | : | = |
| ( | ) | [ | ] | { | } | |
| < | > | <= | >= | |||
| :: | \ | ... | #[ |
| true | false | null | is |
Literal Tokens
| true | false | NameContinue |
| 0 | 1 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| x | X |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| a | b | c | d | e | f | ||||
| A | B | C | D | E | F |
| n | r | t | v | e | f | $ | " | \ |
Types
| is | is not | >= | <= | < | > |
Logical Types
List and Offset Access Types
Primary Types
Names
Named and Generic Types
Literal and Constant Types
Shape Types
Callable Types
Attributes
§Index
- Attribute
- AttributeArgument
- AttributeArguments
- AttributeGroup
- AttributeGroups
- AttributeList
- BinaryDigit
- BinaryIndicator
- BinaryIntLiteral
- BlockComment
- BlockCommentChar
- BoolLiteral
- CallableParameter
- CallableParameterBody
- CallableParameters
- CallableReturnType
- CallableType
- ClassConstant
- Comment
- CommentChar
- ConditionalOperand
- ConditionalOperator
- ConditionalSuffix
- ConditionalType
- ConstantMask
- DecimalIntLiteral
- Digit
- DigitSeparator
- Document
- DoubleQuotedString
- DoubleStringChar
- EscapeSequence
- ExplicitField
- ExponentFloatLiteral
- ExponentIndicator
- ExponentPart
- Expression
- FloatLiteral
- FullyQualifiedName
- HexDigit
- HexEscape
- HexIndicator
- HexIntLiteral
- Identifier
- Ignored
- ImplicitField
- IntersectionType
- IntLiteral
- LeadingFloatLiteral
- Letter
- LineComment
- LineCommentStart
- LineTerminator
- ListSuffix
- LiteralType
- LogicalType
- Name
- NameContinue
- NamedType
- NameStart
- NameToken
- NegativeSign
- NullableType
- NullLiteral
- OctalDigit
- OctalIndicator
- OctalIntLiteral
- OffsetSuffix
- ParameterModifiers
- PostfixType
- PrimaryType
- Punctuator
- RelativeName
- ReservedWord
- ShapeBody
- ShapeField
- ShapeFieldList
- ShapeFields
- ShapeKey
- ShapeValue
- SimpleEscape
- SingleQuotedString
- SingleStringChar
- SourceCharacter
- StringLiteral
- TemplateArgument
- TemplateArgumentHint
- TemplateArguments
- TemplateArgumentType
- ThisVariable
- Token
- TrailingFloatLiteral
- Type
- TypeSuffix
- UnaryType
- UnicodeEscape
- UnionType
- UnsealedShape
- Variable
- Whitespace