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_*. - 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 0x80 to 0xFF |
| 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
| Horizontal Tab (U+0009) |
| Line Feed (U+000A) |
| Vertical Tab (U+000B) |
| Form Feed (U+000C) |
| Carriage Return (U+000D) |
| Space (U+0020) |
Whitespace separates tokens and improves the legibility of the source text. TypeLang does not distinguish between horizontal whitespace and line terminators, and a Comment stands wherever whitespace stands, save for the one place the presence of the whitespace itself is read (see Ignored Tokens).
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.
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 ::; . in ...); in every such case, the longest matching punctuator is taken.
4.3.2Names
A NameToken is the raw lexical unit that is later assembled, together with the namespace separator, into the syntactic Name production (see Names and Namespaces); the two are distinct non-terminals of, respectively, the lexical and the syntactic grammar. A NameToken MUST begin with a Letter or an underscore (_) and MAY continue with any Letter, Digit, underscore, or dash (-). The only difference from the PHP identifier grammar is that the dash character is additionally permitted in any non-leading position.
A source text is read as a sequence of bytes rather than of codepoints, and every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a Letter covers. A name MAY therefore be written in any script.
Проект\Тип
A NameToken is always the longest possible valid sequence; it MUST NOT be followed by a NameContinue character.
ExampleTypeName
Dashes are accepted in any non-leading position, which is what makes built-in type names such as non-empty-string and array-key expressible:
non-empty-string
The reserved words true, false and null are also valid as a fragment of a NameToken, provided the NameToken is not equal to one of those reserved words on its own (see Reserved Words):
true-type
Counter-example. A NameToken cannot begin with a Digit or a dash.
Counter Example № 142type
ParseException: Syntax error, unexpected "type"
4.3.3Reserved Words
| true | false | null | is | not |
The words true, false and null are literal keywords (see Literal Tokens), and is and not are the conditional operator keywords (see Conditional Types). Each is matched only when not immediately followed by a NameContinue character. The three literal keywords are matched case-insensitively, while is and not are matched the one way they are spelled.
Wherever the source contains one of these words, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. It remains an Identifier all the same (see Names and Namespaces), so a reserved word is shadowed rather than forbidden: it stands as a name wherever the token it is scanned as denotes no type of its own.
A literal keyword standing alone, in a position where a type is expected, is read as the LiteralType it denotes and not as the NamedType it would otherwise be (see Primary Types):
TrUe
Behind a namespace separator or another segment, where no literal is expected, the same word is a name segment. This makes \true a reference to a type literally named true, distinct from the true literal:
\true
The conditional operator keywords denote no type of their own, so nothing shadows them, and a bare is or not is an ordinary named type:
not
4.3.4Variable
A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken, save for the dash a variable cannot carry. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).
The special variable $this is recognised as a distinct token and additionally denotes the current object type when used as a primary type (see Primary Types).
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 Sign. A NegativeSign makes the denoted value negative; a leading + denotes the same value the absence of a Sign does. Underscores (DigitSeparator) MAY appear as visual separators; they carry no meaning and do not affect the denoted value. A DigitSeparator MUST stand between two digits, and therefore may neither lead a run of digits, nor trail one, nor stand beside another DigitSeparator.
Decimal. A decimal literal other than a lone 0 MUST NOT begin with a zero, which is the mark of an OctalIntLiteral.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
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 or, as octal has been written since long before that prefix, with a leading 0 alone; digits are 0 through 7 only. A 0123 denotes the same value a 0o123 does.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
0o42
04_23
Counter-example. A leading zero makes a literal an octal, so a digit outside the octal radix may not follow one.
Counter Example № 208
ParseException: Syntax error, unexpected "8"
Hexadecimal. Prefixed with 0x or 0X; digits are 0 through 9 and a through f, in either case.
| 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.
An OctalIntLiteral written without an OctalIndicator is the legacy octal spelling kept for compatibility with historical PHP source. It is read in the same radix (base 8) the prefixed spelling is, so 042 denotes the value 34.
A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the implementation’s platform integer type, the numeric value MUST be clamped to the nearest representable bound (the platform’s PHP_INT_MAX or PHP_INT_MIN) while the raw lexeme MUST be preserved unchanged.
4.4.4Float
A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a Sign.
Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both:
0.9
.9
1.
Scientific notation uses the case-insensitive e indicator followed by a decimal exponent, which takes a Sign of its own:
10e-2
10e+2
Every run of digits a float is written of accepts the DigitSeparator under the same rule an integer does, and, unlike a DecimalIntLiteral, MAY begin with a zero:
2_3.4_5e-6_7
Counter-example. A lone decimal point is not a valid float.
Counter Example № 4.
ParseException: Syntax error, unexpected "."
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. The two words are separate tokens, so whitespace and comments MAY stand between them the way they MAY stand between any other two tokens.
T is not string ? int : bool
Either the subject or the comparand MAY be a Variable instead of a Type, including in “Yoda” order, where the variable appears on the right:
$value is array ? non-empty-array : null
array is $value ? non-empty-array : null
$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[][]
The legacy list syntax accepts no key type between the brackets; for that, use offset access or the modern array<…> generic. A User[int] is a well-formed document all the same — an offset access type (see below) rather than a list, since int is a valid Type. It is the absence of a type between the brackets that selects the list reading:
User[int]
5.4.2Offset Access
A pair of square brackets enclosing a Type forms an offset access type, which denotes the type of the element addressed by that offset within the operand (typically a shape or an array).
T['offset']
T[U]
array{int, string}[0]
Because the offset is itself an arbitrary Type, any type — including objects, shapes, or conditional types — MAY be used as a key:
T<U>[object{key: int, ...}]
Counter-example. Each offset is enclosed in a single pair of brackets.
Counter Example № 6Collection[[Some]]
ParseException: Syntax error, unexpected "["
5.5Primary Types
A primary type is the tightest-binding form. It is one of: a parenthesized Type, the $this type, a literal type, a callable type (see Callable Types), or a named type.
A parenthesized type is used to override precedence; it denotes exactly the type it encloses:
(A | B) & C
The $this variable, when used as a primary type, denotes the current object type:
$this
* 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.true denotes the boolean literal and never a type named true. An is and a not denote no literal of their own, and nothing is therefore left to shadow the named type each of them is.5.6Names and Namespaces
A Name is a sequence of one or more Identifier segments joined by the namespace separator (\), mirroring PHP namespaces. A fully-qualified name additionally begins with a leading separator.
Example\Name
\Absolute\Type\Name
A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end.
Reserved words within names. Every reserved word — true, false, null, is and not (see Reserved Words) — MAY appear as an Identifier segment of a Name. In a qualified position — following a separator or another segment — such a word is a name segment and nothing besides. This makes \null a reference to a type named null, distinct from the null literal.
Counter-example. A name cannot end with a separator.
Counter Example № 7Example\Name\
ParseException: Syntax error, unexpected end of input
5.7Named Types
A named type is a Name optionally followed by either a list of template arguments (generics) or a shape body. A bare named type is the most common type of all:
Path\To\ExampleClass
This grammar imposes no restriction on which names are valid; int, string, list, non-empty-string, and any user-defined class name are all parsed identically as named types. Whether a name refers to a built-in type, a class, an interface, an enum, or a type alias is a semantic concern for the implementation and is outside the scope of this specification.
5.8Generic Types
A generic type supplies a named type with one or more template arguments, each of which is itself a Type or a Wildcard. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.
Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no such limits.
Path\To\ExampleClass<T, U>
iterable<int<0, max>, Collection<User>>
HashMap<Request, User,>
Counter-example. At least one argument is required, and a leading comma is not permitted.
Counter Example № 8example<>
ParseException: Syntax error, unexpected ">"
int<0, max> in iterable<int<0, max>, …>. This is distinct from a template parameter, which would be part of a type’s (hypothetical) declaration. TypeLang describes only use sites, and therefore only template arguments.5.8.1Template Argument Hints
A template argument MAY carry a single leading hint: a NameToken placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.
HashMap<array-key, covariant Request>
At the lexical level, Whitespace MUST stand directly behind the hint; this separation is what distinguishes a hint from the start of the argument’s own type, and a Comment does not stand in its place (see Ignored Tokens). Each argument may carry at most one hint.
Counter-example. A hint is a single NameToken. A second one reads as the bound of a template parameter, and a parameter list belongs to a callable, so the statement is refused where the ( it would go on with is missing.
Counter Example № 9HashMap<array-key, some covariant Request>
ParseException: Syntax error, unexpected end of input
Counter-example. A hint cannot be a reserved word: true, false, null, is and not are always scanned as the corresponding literal or operator token (see Reserved Words) and never as a NameToken.
Counter Example № 10HashMap<is Request>
ParseException: Syntax error, unexpected ">"
5.9Literal Types
A literal type denotes the singleton type containing exactly one value, or, in the case of a ConstantMask, a well-defined family of such singleton types. The boolean, null, integer, floating-point and string forms correspond directly to the literal tokens of the same name:
true
42
"Hello World"
A ClassConstant and a ConstantMask (see Constant Types, below) are likewise literal types, since each references a single PHP constant or a well-defined family of PHP constants. An unqualified reference to a global constant, by contrast, has no dedicated grammar of its own; it is described in Global Constants.
5.10Constant Types
TypeLang provides two dedicated grammar forms for referencing constants: a ClassConstant and a ConstantMask. Together with the naming convention for a single global constant described below, this specification uses the term constant type loosely to refer to all three collectively; only ClassConstant and ConstantMask, however, are formal non-terminals of the grammar.
true, false or null. Unlike a conventional type name, however, a constant is conventionally written without a dash.5.10.1Global Constants
Syntactically, an unqualified reference to a global constant is indistinguishable from, and MUST be parsed as, an ordinary named type (see Primary Types):
JSON_THROW_ON_ERROR
pcov\version
Whether a given NamedType denotes a global constant, a class, an interface, or something else entirely is a semantic concern that this specification leaves to the implementation.
5.10.2Class Constants
A reference to a class constant is a Name (the class), the :: separator, and an Identifier (the constant). The constant segment is a single identifier and cannot itself be namespaced.
ClassName::CONSTANT_NAME
Path\To\ClassName::ANOTHER_CONSTANT_NAME
Counter-example. The constant part may not contain a namespace separator.
Counter Example № 11ClassName::SOME\ANY
ParseException: Syntax error, unexpected "\"
5.10.3Constant Masks
A constant mask denotes a family of constants whose names are written of the same segments, in the same order, with a Wildcard standing wherever the name is left unsaid. A mask MUST contain at least one Wildcard, and its segments and wildcards alternate, so two wildcards in a row denote nothing a single one does not.
A global constant mask matches every global constant the segments describe:
JSON_*
The name of a global constant MAY be left unsaid from its very beginning, and the namespace it belongs to MAY be written in front of it. A Name standing in front of a MaskTail carries the namespace and, in its last segment, the leading segment of the mask; a Name separated from the mask by a \ is a namespace whole, and the mask then matches every constant that namespace holds:
*_SUFFIX
Path\To\JSON_*
Path\To\*
A class constant mask matches every constant of a class the segments describe; the leading segment MAY be omitted entirely, in which case the mask matches every constant of the class:
Path\To\ClassName::PREFIX_*
Path\To\ClassName::*_SUFFIX
Path\To\ClassName::PREFIX_*_SUFFIX
Path\To\ClassName::*
Counter-example. A global mask must have at least one segment of a name, whether it stands in front of a wildcard or behind it; a lone asterisk, which would match every constant there is, is not a type.
Counter Example № 12*
ParseException: Syntax error, unexpected end of input
Counter-example. Two wildcards in a row are not a mask.
Counter Example № 13Path\To\ClassName::PREFIX_**
ParseException: Syntax error, unexpected "*"
6Structural Types
This section defines the structural type forms — shapes and callables. Each of these forms builds on the named type grammar introduced in the previous section.
6.1Shape Types
A shape rigidly describes the individual elements of a composite value, such as the keys of an array or the properties of an object. A shape is written as a named type immediately followed by a brace-enclosed body; the name — commonly array, object, or list, but any name is accepted — describes the container, and the body describes its elements.
array{
a: First,
b: Second
}
Custom\Type{
id: int,
name: string
}
An empty shape body describes a container with no elements:
array{}
6.1.1Shape Fields
A shape field is either explicit (a key, a colon, and a value type) or implicit (a value type alone, whose key is assigned positionally). A shape key may be a bare identifier, an integer literal, a string literal, or a constant reference — that is, a ClassConstant or a ConstantMask.
:: or a trailing *.array{ name: First, count: Second }
array{ 1: First, 42: Second }
array{ "name-some": First, "escape\nchars": Second }
array{ First, Second }
array{ Path\To\ClassName::CONSTANT_NAME: First, JSON_*: Second }
Mixed keys are not permitted. A single shape MUST use either explicit keys throughout or implicit keys throughout.
Counter Example № 14array{ named: First, Second }
ParseException: Cannot mix explicit and implicit shape keys
Duplicate explicit keys are not permitted. No two explicit fields of the same shape may denote the same key, regardless of which of the five key forms each uses.
Counter Example № 15array{ 1: int, 2: int, 1: string }
ParseException: Duplicate key "1"
6.1.2Optional Fields
An explicit field MAY be marked optional by placing a question mark before the colon. An optional key (key?: Type) states that the field may be absent; this is distinct from an optional value (key: ?Type), which states that the field is always present but its value may be null.
array{ key?: Type }
array{ key: ?Type }
6.1.3Unsealed Shapes
By default, a shape is sealed: it describes its container exactly, and no additional elements are permitted. A trailing ellipsis (...) makes the shape unsealed, allowing elements beyond those listed.
array{ key: Type, ... }
An unsealed shape MAY also stand alone, describing a container constrained only by its (absent) field list:
array{ ... }
An unsealed marker MAY carry template arguments that describe the type of the additional elements — and, optionally, of their keys — using the same angle-bracket syntax as generics:
array{ user: User, ...<string, object> }
6.2Callable Types
A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — optionally followed by the template parameters it declares, then by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.
callable()
callable(): void
Closure(int<0, max>, callable(?C): mixed): void
6.2.1Template Parameters
| of | as |
A callable type MAY declare the template parameters it introduces, written as a <...> list between the name and the parameter list. Each parameter is an Identifier followed by as many bounds as are put on it.
A parameter accepts three kinds of limit, each of which MUST be written at most once. The two bounds MAY be written in either order, while a TemplateDefault MUST be written last: a bound standing behind it belongs to the Type of the default rather than to the parameter.
- An UpperBound narrows the parameter from above: the argument is to be a subtype of the given Type. Its two operators denote the same thing, and a conforming implementation MUST retain the one that was written.
- A LowerBound bounds the parameter from below: the argument is to be a supertype of the given Type.
- A TemplateDefault is the Type the parameter takes when no argument is supplied. It bounds nothing.
Each operator is matched the one way it is spelled, so an OF bounds nothing an of does.
callable<T>(T): T
callable<T of Some>(T): T
Closure<T of Some, U super Any, V = int>(T, U): V
callable<T of Some super Any = int>(T): void
Counter-example. A word that bounds nothing is not an operator.
Counter Example № 16callable<T whatever Some>(): void
ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super"
Counter-example. Neither is a word that is spelled the way an operator is spelled, but written in a case of its own.
Counter Example № 17callable<T OF Some>(): void
ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super"
Counter-example. Each kind of limit is written at most once.
Counter Example № 18callable<T of Some as Any>(): void
ParseException: Template parameter cannot have more than one upper bound
Counter-example. A bound does not stand behind a default.
Counter Example № 19callable<T = int of Some>(): void
ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself
A <...> list is written the same way whether it declares template parameters or supplies template arguments, and only the ( that follows a parameter list tells them apart. A list that no ( follows is therefore a list of template arguments, which describes no bounds.
Counter-example. Bounds belong to a callable alone.
Counter Example № 20Collection<T of Some>
ParseException: Syntax error, unexpected end of input
6.2.2Callable Parameters
A parameter is described by its type, optionally followed by the reference and the variadic markers, by a name and by the default marker — in that order and in no other. The type is the only required part.
callable(Type)
callable(Type $name)
Counter-example. A parameter given by name alone, without a type, is not permitted.
Counter Example № 21callable($name)
ParseException: Syntax error, unexpected ")"
Named Parameters. A name beginning with $ MAY follow the parameter’s type, permitting the argument to be passed by name, exactly as in PHP.
callable(A $a, B, C)
Output Parameters. An ampersand (&) placed after the parameter type marks the parameter as passed by reference (an output parameter).
callable(T&)
callable(T &$name)
Counter-example. The ampersand must follow the type; it must not precede it.
Counter Example № 22callable(&T)
ParseException: Syntax error, unexpected "&"
Optional Parameters. A trailing = marks a parameter as optional: the caller MAY omit the corresponding argument.
callable(T=)
callable(T &$name=)
Variadic Parameters. An ellipsis (...) placed after the parameter type marks the parameter as variadic. Where a parameter carries both markers, the ampersand comes first.
callable(T ...$name)
callable(T &...$name)
Counter-example. The ellipsis must follow the type; it must not precede it.
Counter Example № 23callable(...T)
ParseException: Syntax error, unexpected "..."
Counter-example. A variadic parameter is already optional and therefore must not additionally carry a default marker.
Counter Example № 24callable(T ...$name=)
ParseException: Cannot have variadic param with a default
AAppendix: Notation Conventions
This specification uses a number of notation conventions to describe the language grammar. This appendix explains those notations so as to avoid ambiguity, and is itself non-normative: it describes how to read the productions found elsewhere in this document, but introduces no new requirement of its own.
A.1Context-Free Grammar
A context-free grammar consists of a number of productions. Each production has an abstract symbol, called a non-terminal, as its left-hand side, and one or more sequences of non-terminal symbols and terminal characters as its right-hand side.
Starting from a single goal non-terminal (Type, for TypeLang), the grammar describes a language: the set of character sequences obtained by repeatedly replacing a non-terminal with one of its right-hand sides until only terminals remain.
Terminals are written in a monospace font, either as a specific character or sequence (for example | or is), or as prose describing a code point (for example "New Line (U+000A)").
A production with a single definition is written on one line:
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 0x80 to 0xFF |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Ignored Tokens
| Horizontal Tab (U+0009) |
| Line Feed (U+000A) |
| Vertical Tab (U+000B) |
| Form Feed (U+000C) |
| Carriage Return (U+000D) |
| Space (U+0020) |
| New Line (U+000A) | Carriage Return (U+000D) |
| // | # |
Lexical Tokens
| ? | | | & | * | , | : | = |
| ( | ) | [ | ] | { | } | |
| < | > | |||||
| :: | \ | ... |
| true | false | null | is | not |
Literal Tokens
| true | false | NameContinue |
| - | + |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 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
| of | as |
§Index
- BinaryDigit
- BinaryDigits
- BinaryIndicator
- BinaryIntLiteral
- BlockComment
- BlockCommentChar
- BoolLiteral
- CallableParameter
- CallableParameters
- CallableReturnType
- CallableType
- ClassConstant
- ClassConstantMask
- Comment
- CommentChar
- ConditionalOperand
- ConditionalOperator
- ConditionalSuffix
- ConditionalType
- ConstantMask
- DecimalIntLiteral
- Digit
- Digits
- DigitSeparator
- Document
- DoubleQuotedString
- DoubleStringChar
- EscapeSequence
- ExplicitField
- ExponentFloatLiteral
- ExponentIndicator
- ExponentPart
- Expression
- FloatLiteral
- FullyQualifiedName
- GlobalConstantMask
- HexDigit
- HexDigits
- HexEscape
- HexIndicator
- HexIntLiteral
- Identifier
- Ignored
- ImplicitField
- IntersectionType
- IntLiteral
- LeadingFloatLiteral
- LeadingMask
- Letter
- LineComment
- LineCommentStart
- LineTerminator
- ListSuffix
- LiteralType
- LogicalType
- LowerBound
- MaskTail
- Name
- NameContinue
- NamedType
- NameStart
- NameToken
- NegativeSign
- NonZeroDigit
- NullableType
- NullLiteral
- OctalDigit
- OctalDigits
- OctalIndicator
- OctalIntLiteral
- OffsetSuffix
- PostfixType
- PrimaryType
- Punctuator
- RelativeName
- ReservedWord
- ShapeBody
- ShapeField
- ShapeFieldList
- ShapeFields
- ShapeKey
- ShapeValue
- Sign
- SimpleEscape
- SingleQuotedString
- SingleStringChar
- SourceCharacter
- StringLiteral
- TemplateArgument
- TemplateArgumentHint
- TemplateArguments
- TemplateArgumentValue
- TemplateBound
- TemplateDefault
- TemplateParameter
- TemplateParameters
- ThisVariable
- Token
- TrailingFloatLiteral
- Type
- TypeSuffix
- UnaryType
- UnicodeEscape
- UnionType
- UnsealedShape
- UpperBound
- UpperBoundOperator
- Variable
- VariableContinue
- Whitespace
- Wildcard