Skip to content

Commit

Permalink
Build POISON_MARKER and TANY_MARKER
Browse files Browse the repository at this point in the history
Summary:
We introduce two new type aliases to the `HH\FIXME` namespace such that

| Alias | Current Value | Sound Dynamic Value |
| -- | -- | -- |
| POISON_MARKER<T> | T | ~T |
| TANY_MARKER<T> | Tany | T |

> NOTE: in the examples below, commented out method declarations represent the code before migration.

`POISON_MARKER` will be used for hierarchy poisoning when an unenforceable type overrides an enforced one e.g. a closure

```
class C {
  // public function f(): nonnull {}
  public function f(): HH\FIXME\POISON_MARKER<nonnull> {}
}
class D extends C {
  public function f(): (function (): void) {
    /* HH_FIXME[4110] */
    return null;
  }
}

/**
 * POISON_MARKER will retain current behavior -- expression has type nonnull
 * but will become ~nonnull under sound dynamic
 */
function get_nonnull(C $c): nonnull {
  return $c->f(); // type hint violation, returning null
}
function test(): void {
  get_nonnull(new D());
}
```

`TANY_MARKER` will be used for migrating existing uses of Tany to desired types under sound dynamic without affecting current inference. This is why it points to `T` and not `~T` -- we want full manual control here. In some cases we may want a combination of TANY_MARKER and POISON_MARKER.
```
class C {
  // public function f(): int {
  public function f(): HH\FIXME\POISON_MARKER<arraykey> { // slightly weaken enforcement
    return 1;
  }
}
class D extends C {
  // public function f(): DECL_TANY {
  public function f(): HH\FIXME\TANY_MARKER<string> {
    return "hello";
  }
}
class E extends D {
  public function f(): string {
    return "hello";
  }
}
```

## Implementation
There is some sleight of hand here - despite outward appearance, the implementation of these aliases diverges.
- For `POISON_MARKER`, I've stripped off the alias for both type constraints and type structures, now matches like types in hint positions and almost matches for type structures (see test cases). The upshot is the runtime doesn't know about POISON_MARKER's existence.
- For `TANY_MARKER`, I strip off the type arguments to simulate how we do decl Tanys, but HHVM still needs to have the symbol name in scope to create the OF_GENERIC type structure, so it's declared in systemlib. See the `tany.php` example from D40516837 (9e8c173) for an analogue.

Reviewed By: andrewjkennedy

Differential Revision: D40371013

fbshipit-source-id: d2d9e4f4d57f6f88d1c31201d60d9c9e19656173
  • Loading branch information
vassilmladenov authored and facebook-github-bot committed Oct 25, 2022
1 parent 8159883 commit bb5e5d6
Show file tree
Hide file tree
Showing 17 changed files with 198 additions and 0 deletions.
5 changes: 5 additions & 0 deletions hphp/hack/hhi/soundness.hhi
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ function UNSAFE_CAST<<<__Explicit>> Tin, <<__Explicit>> Tout>(Tin $t, ?\HH\Forma
* ```
*/
function UNSAFE_NONNULL_CAST<T as nonnull>(?T $t, ?\HH\FormatString<nothing> $msg = null)[]: T;

/* Acts as Tany under current semantics, and T under sound dynamic */
type TANY_MARKER<T> = T;
/* Acts as T under current semantics, and ~T under sound dynamic */
type POISON_MARKER<T> = T;
15 changes: 15 additions & 0 deletions hphp/hack/src/hackc/emitter/emit_type_constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use hhbc::TypedValue;
use hhbc_string_utils as string_utils;
use hhvm_types_ffi::ffi::TypeStructureKind;
use naming_special_names_rust::classes;
use naming_special_names_rust::typehints;
use options::Options;
use oxidized::aast;
use oxidized::aast_defs;
Expand Down Expand Up @@ -287,6 +288,20 @@ fn hint_to_type_constant_list<'arena>(
]);
}
}
let hints = match &hints[..] {
[h] if name == typehints::POISON_MARKER => {
return hint_to_type_constant_list(
alloc,
opts,
tparams,
targ_map,
type_refinement_in_hint,
h,
);
}
[_h] if name == typehints::TANY_MARKER => <&[Hint]>::default(),
_ => hints,
};
let (classname, s_res) = resolve_classname(alloc, tparams, name.to_owned());
let mut r = bumpalo::vec![in alloc];
if s_res.eq_ignore_ascii_case("tuple") || s_res.eq_ignore_ascii_case("shape") {
Expand Down
6 changes: 6 additions & 0 deletions hphp/hack/src/hackc/emitter/emit_type_hint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ fn hint_to_type_constraint<'arena>(
_ => hint_to_type_constraint(alloc, kind, tparams, false, &hs[0]),
};
}
[h] if s == typehints::POISON_MARKER => {
return hint_to_type_constraint(alloc, kind, tparams, false, h);
}
[_h] if s == typehints::TANY_MARKER => {
return Ok(Constraint::default());
}
_ => {}
};
type_application_helper(alloc, tparams, kind, s)?
Expand Down
6 changes: 6 additions & 0 deletions hphp/hack/src/naming/naming_special_names.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,12 @@ module HH = struct
let contains = "\\HH\\Lib\\C\\contains"

let contains_key = "\\HH\\Lib\\C\\contains_key"

module FIXME = struct
let tTanyMarker = "\\HH\\FIXME\\TANY_MARKER"

let tPoisonMarker = "\\HH\\FIXME\\POISON_MARKER"
end
end

module Shapes = struct
Expand Down
3 changes: 3 additions & 0 deletions hphp/hack/src/naming/naming_special_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,9 @@ pub mod typehints {

pub const HH_SUPPORTDYN: &str = "\\HH\\supportdyn";

pub const TANY_MARKER: &str = "\\HH\\FIXME\\TANY_MARKER";
pub const POISON_MARKER: &str = "\\HH\\FIXME\\POISON_MARKER";

pub const WILDCARD: &str = "_";

lazy_static! {
Expand Down
15 changes: 15 additions & 0 deletions hphp/hack/src/typing/typing_phase.ml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@ let rec localize ~(ety_env : expand_env) env (dty : decl_ty) =
when String.equal x Naming_special_names.FB.cIncorrectType
&& Env.is_typedef env x ->
localize ~ety_env env (mk (get_reason dty, Tlike arg))
| Tapply ((_, x), [arg])
when String.equal x Naming_special_names.HH.FIXME.tTanyMarker ->
if TypecheckerOptions.enable_sound_dynamic (Env.get_tcopt env) then
localize ~ety_env env arg
else
((env, None), mk (r, Typing_utils.tany env))
| Tapply ((_, x), [arg])
when String.equal x Naming_special_names.HH.FIXME.tPoisonMarker ->
let decl_ty =
if TypecheckerOptions.enable_sound_dynamic (Env.get_tcopt env) then
mk (get_reason dty, Tlike arg)
else
arg
in
localize ~ety_env env decl_ty
| Tapply (((_p, cid) as cls), argl) ->
begin
match Env.get_class_or_typedef env cid with
Expand Down
13 changes: 13 additions & 0 deletions hphp/hack/test/sound_dynamic/typing/marker_types.good.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?hh

function f(
HH\FIXME\TANY_MARKER<int> $tany,
HH\FIXME\POISON_MARKER<string> $poison,
vec<HH\FIXME\TANY_MARKER<float>> $vec_float,
vec<HH\FIXME\TANY_MARKER<HH\FIXME\POISON_MARKER<float>>> $vec_like_float,
): void {
hh_show($tany);
hh_show($poison);
hh_show($vec_float);
hh_show($vec_like_float);
}
9 changes: 9 additions & 0 deletions hphp/hack/test/sound_dynamic/typing/marker_types.good.php.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
File "marker_types.good.php", line 9, characters 3-16:
int
File "marker_types.good.php", line 10, characters 3-18:
~string
File "marker_types.good.php", line 11, characters 3-21:
vec<float>
File "marker_types.good.php", line 12, characters 3-26:
vec<~float>
No errors
13 changes: 13 additions & 0 deletions hphp/hack/test/typecheck/sound_dynamic_marker_types.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?hh

function f(
HH\FIXME\TANY_MARKER<int> $tany,
HH\FIXME\POISON_MARKER<string> $poison,
vec<HH\FIXME\TANY_MARKER<float>> $vec_tany,
vec<HH\FIXME\TANY_MARKER<HH\FIXME\POISON_MARKER<float>>> $vec_tany2,
): void {
hh_show($tany);
hh_show($poison);
hh_show($vec_tany);
hh_show($vec_tany2);
}
9 changes: 9 additions & 0 deletions hphp/hack/test/typecheck/sound_dynamic_marker_types.php.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
File "sound_dynamic_marker_types.php", line 9, characters 3-16:
_
File "sound_dynamic_marker_types.php", line 10, characters 3-18:
string
File "sound_dynamic_marker_types.php", line 11, characters 3-20:
vec<_>
File "sound_dynamic_marker_types.php", line 12, characters 3-21:
vec<_>
No errors
6 changes: 6 additions & 0 deletions hphp/hack/test/typecheck/sound_dynamic_marker_types_is_as.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?hh

function f(): void {
3 as HH\FIXME\TANY_MARKER<int>;
4 as HH\FIXME\POISON_MARKER<string>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "sound_dynamic_marker_types_is_as.php", line 4, characters 8-32:
Invalid `as` expression hint (Typing[4195])
File "sound_dynamic_marker_types_is_as.php", line 4, characters 8-32:
The `as` operator cannot be used with a type with generics, because generics are erased at runtime
File "sound_dynamic_marker_types_is_as.php", line 5, characters 8-37:
Invalid `as` expression hint (Typing[4195])
File "sound_dynamic_marker_types_is_as.php", line 5, characters 8-37:
The `as` operator cannot be used with a type with generics, because generics are erased at runtime
4 changes: 4 additions & 0 deletions hphp/runtime/ext/hh/ext_hh.php
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,7 @@ function reflection_class_is_interface(


}

namespace HH\FIXME {
type TANY_MARKER<T> = T;
}
29 changes: 29 additions & 0 deletions hphp/test/slow/sound_dynamic/poison_int.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?hh

function f<reify T>(<<__Soft>> T $t): void {
echo "\$t: $t [reified]\n";
}
function f_poison(<<__Soft>> HH\FIXME\POISON_MARKER<int> $poison): void {
echo "\$poison: $poison\n";
}

class Typeconsts {
const type Tpoison = HH\FIXME\POISON_MARKER<int>;
}

<<__EntryPoint>>
function main(): void {
printf("\n===== HH\FIXME\POISON_MARKER<int> =====\n");
f_poison(4);
f_poison("not poison int");
f<HH\FIXME\POISON_MARKER<int>>(4);
f<HH\FIXME\POISON_MARKER<int>>("not poison int");

printf("\n===== Typeconsts::Tpoison =====\n");
f<Typeconsts::Tpoison>(4);
f<Typeconsts::Tpoison>("not poison int [typeconst]");

$rt = new ReflectionClass(Typeconsts::class)->getTypeConstant("Tpoison");
printf("Type structure kind: %d\n", $rt->getTypeStructure()["kind"]);
printf("Assigned type text: %s\n", $rt->getAssignedTypeText() ?? "<missing>");
}
17 changes: 17 additions & 0 deletions hphp/test/slow/sound_dynamic/poison_int.php.expectf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
===== HH\FIXME\POISON_MARKER<int> =====
$poison: 4

Warning: Argument 1 to f_poison() must be of type @int, string given in %s/test/slow/sound_dynamic/poison_int.php on line 18
$poison: not poison int
$t: 4 [reified]

Warning: Argument 1 passed to f() must be an instance of @int, string given in %s/test/slow/sound_dynamic/poison_int.php on line 5
$t: not poison int [reified]

===== Typeconsts::Tpoison =====
$t: 4 [reified]

Warning: Argument 1 passed to f() must be an instance of @int, string given in %s/test/slow/sound_dynamic/poison_int.php on line 5
$t: not poison int [typeconst] [reified]
Type structure kind: 1
Assigned type text: HH\int
29 changes: 29 additions & 0 deletions hphp/test/slow/sound_dynamic/tany_int.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?hh

function f<reify T>(<<__Soft>> T $t): void {
echo "\$t: $t [reified]\n";
}
function f_tany_int(<<__Soft>> HH\FIXME\TANY_MARKER<int> $tany_int): void {
echo "\$tany_int: $tany_int\n";
}

class Typeconsts {
const type Tany_int = HH\FIXME\TANY_MARKER<int>;
}

<<__EntryPoint>>
function main(): void {
printf("\n===== HH\FIXME\TANY_MARKER<int> =====\n");
f_tany_int(4);
f_tany_int("not Tany int");
f<HH\FIXME\TANY_MARKER<int>>(4);
f<HH\FIXME\TANY_MARKER<int>>("not Tany int");

printf("\n===== Typeconsts::Tany_int =====\n");
f<Typeconsts::Tany_int>(4);
f<Typeconsts::Tany_int>("not Tany int [typeconst]");

$rt = new ReflectionClass(Typeconsts::class)->getTypeConstant("Tany_int");
printf("Type structure kind: %d\n", $rt->getTypeStructure()["kind"]);
printf("Assigned type text: %s\n", $rt->getAssignedTypeText() ?? "<missing>");
}
11 changes: 11 additions & 0 deletions hphp/test/slow/sound_dynamic/tany_int.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
===== HH\FIXME\TANY_MARKER<int> =====
$tany_int: 4
$tany_int: not Tany int
$t: 4 [reified]
$t: not Tany int [reified]

===== Typeconsts::Tany_int =====
$t: 4 [reified]
$t: not Tany int [typeconst] [reified]
Type structure kind: 13
Assigned type text: HH\FIXME\TANY_MARKER

0 comments on commit bb5e5d6

Please sign in to comment.