HHVM 4.14.0
HHVM 4.14 is released! As 4.8 has long term support, it remains supported, as do 3.30 and 4.9-4.13.
Highlights
- The typechecker is now able to track intersections - i.e. when a variable is known to have multiple supertypes, not one of multiple.
instanceof
no longer refines types; useis
expressions instead.hhast-migrate --instanceof-is
can be used to automatically convert expressions of the form$x instanceof SomeType
to expressions of the form$x is SomeType
,$x is SomeType<_>
,$x is SomeType<_, _>
etc. If the RHS of aninstanceof
is not a classname in sourcecode, but rather an expression, HHAST4.14.4 and up will migrate to a\is_a()
call. This does not refine the variable on the LHS for the typechecker.- Regexp literals can now nest brace-like delimiters; for example,
re"(())"
is now handled correctly. - Improved error messages, autocompletion, and fixed several crashes in the VSDebug server implementation.
- Improved namespace support for autocompletion.
Breaking Changes
instanceof
no longer refines types.$this
is no longer allowed in PHP-style static closures, e.g.$x = static function () { $this->foo(); }
.- the fatal errors for subclassing or using
new
onClosure
are now case-insensitive to match the runtime behavior. DateTime::COOKIE
andDATE_COOKIE
now use the correct format for cookies. The old value is available asDATE_RFC850
. This replaces full day-of-week (e.g. ‘Monday’) with 3-letter abbreviations (e.g. ‘Mon’).$shape['somefield'] ?? null
is now a type error if'somefield'
is known not to exist; it is still permitted if'somefield'
/may/ exist.
Future Changes
- visibility modifiers for class constants are currently banned by the type checker, but permitted-and-ignored by the runtime. We expect them to be permitted and enforced by both the typechecker and runtime in a future release.
Intersection Types
The typechecker is now able to track multiple type constraints for the same variable, creating intersection types; they can not be declared in source code.
For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
function f(Foo $x): void {
// $x is Foo
if ($x is IBar) {
// before: $x is IBar
// now: $x is Foo & IBar (intersection)
expects_an_IBar($x); // correctly works in both
expects_a_Foo($x); // no longer an error!
}
// before: $x is Foo | IBar (union)
// now: $x is Foo
expects_an_IBar($x); // correctly an error in both
expects_a_Foo($x); // no longer an error!
}
As well as allowing more valid code to be understood by the typechecker, this can exposed via hover type information and via error messages.
An expression is of type (A & B)
if it is both of type A
and of type
B
, which is the case inside the if statement above. Intersections are in some
ways similar to type *unions *(denoted by |), which allow tracking types through
control flow and are also internal to the typechecker (they can not be declared
in source code). Having both intersections and unions allows us to greatly
improve type refinement in Hack, leading to a more intuitive experience.
As part of this work, tracking for unions has also been improved:
1
2
3
4
5
6
7
8
function f(mixed $x): void {
if ($x is int || $x is string) {
// before: $x is mixed
// now: $x is int | string
expects_an_arraykey($x); // no longer an error!
}
// both: $x is mixed
}
Previously, it was common to work around this limitation with temporary variables or type assertions:
1
2
3
4
5
6
7
8
function f(Foo $x): void {
$x_for_hack = $x; // now unnecessary
if ($x_for_hack is IBar) {
expects_an_IBar($x_for_hack);
expects_a_Foo($x);
}
expects_a_Foo($x as Foo); // "as" no longer needed
}
These workarounds can now be removed:
1
2
3
4
5
6
7
function f(Foo $x): void {
if ($x is IBar) {
expects_an_IBar($x);
expects_a_Foo($x);
}
expects_a_Foo($x);
}
intersections vs unions
The difference between type /intersections/ and type /unions/ is essentially the same as the one between the logical ‘AND’ and the logical ‘OR’.
Something is an intersection type (I1 & I2)
if it is both I1
and I2
.
Intersections are obtained with refinements:
1
2
3
4
5
function f(I1 $x): void {
if ($x is I2) {
// here $x is both an I1 and I2, and gets type (I1 & I2)
}
}
Something is a union type (A | B)
if it is either A
or B
; the
type system does not know which one it actually is.
1
2
3
4
5
6
7
8
9
10
11
12
if (something()) {
$x = new A();
} else {
$x = new B();
}
// here $x is either A or B and has type (A | B)
$v = vec[new A(), new B()];
$y = $v[$i]
// $y is either A or B and has type (A | B)
instanceof
Refinement
In previous versions of Hack/HHVM, expressions of the form $x instanceof Foo
would inform the typechecker that $x
is of type Foo
; this is no longer the
case, replaced by is
and as
expressions. For example:
1
2
3
4
5
6
7
8
9
10
11
class Foo {}
function takes_foo(Foo $_): void {}
function do_stuff(mixed $in): void {
if ($in instanceof Foo) {
takes_foo($in); // valid in 4.13, but not in 4.14
}
if ($in is Foo) {
takes_foo($in); // valid in 3.28+, including 4.14
}
}
is
expressions require matching generics; for example:
1
2
3
4
5
6
7
8
9
10
class NoGenerics {}
class OneGeneric<T> {}
class TwoGenerics<Ta, Tb> {}
function do_stuff(mixed $in, classname<SomeType> $what): void {
if ($in is NoGenerics) { }
if ($in is OneGeneric<_>) { }
if ($in is TwoGenerics<_, _>) { }
if (is_a($in, $what)){ /*$in is still mixed here*/ }
}
hhast-migrate --instanceof-is
can be used in
HHAST 4.13 to
automatically convert these expressions.
The old behavior of instanceof
can also be disabled on 4.13 by setting
disable_instanceof_refinement=true
in .hhconfig
.