From 4f9cb175bb403b74eca1f33285357fdeb63e2cc6 Mon Sep 17 00:00:00 2001 From: Lexidor Digital <31805625+Lexidor@users.noreply.github.com> Date: Wed, 20 May 2020 18:51:11 +0200 Subject: [PATCH] Write a quick and dirty PoC for RFC #51 --- src/TypeAssert.hack | 10 ++- src/TypeCoerce.hack | 10 ++- src/TypeSpec.hack | 13 +++- .../__Private/from_type_structure.hack | 50 +++++++++---- src/example/new_behavior.hack | 50 +++++++++++++ src/example/newtype.hack | 70 +++++++++++++++++++ src/example/old_behavior.hack | 47 +++++++++++++ tests/ShapeSpecTest.hack | 1 + 8 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 src/example/new_behavior.hack create mode 100644 src/example/newtype.hack create mode 100644 src/example/old_behavior.hack diff --git a/src/TypeAssert.hack b/src/TypeAssert.hack index a579f43..c22f655 100644 --- a/src/TypeAssert.hack +++ b/src/TypeAssert.hack @@ -55,8 +55,14 @@ function classname_of(classname $expected, string $what): classname { return TypeSpec\classname($expected)->assertType($what); } -function matches_type_structure(TypeStructure $ts, mixed $value): T { - return TypeSpec\__Private\from_type_structure($ts)->assertType($value); +function matches_type_structure( + TypeStructure $ts, + mixed $value, + ?TypeSpec\TResolver $resolver = null, +): T { + return TypeSpec\__Private\from_type_structure($ts, $resolver)->assertType( + $value, + ); } function matches(mixed $value): T { diff --git a/src/TypeCoerce.hack b/src/TypeCoerce.hack index 47be0d0..2be40a3 100644 --- a/src/TypeCoerce.hack +++ b/src/TypeCoerce.hack @@ -40,8 +40,14 @@ function arraykey(mixed $x): arraykey { return TypeSpec\arraykey()->coerceType($x); } -function match_type_structure(TypeStructure $ts, mixed $value): T { - return TypeSpec\__Private\from_type_structure($ts)->coerceType($value); +function match_type_structure( + TypeStructure $ts, + mixed $value, + ?TypeSpec\TResolver $resolver = null, +): T { + return TypeSpec\__Private\from_type_structure($ts, $resolver)->coerceType( + $value, + ); } function match(mixed $value): T { diff --git a/src/TypeSpec.hack b/src/TypeSpec.hack index 4a10616..d5735ca 100644 --- a/src/TypeSpec.hack +++ b/src/TypeSpec.hack @@ -10,6 +10,10 @@ namespace Facebook\TypeSpec; +use type Facebook\TypeAssert\UnsupportedTypeException; + +type TResolver = (function(TypeStructure): TypeSpec); + abstract class TypeSpec<+T> { private ?Trace $trace = null; @@ -66,6 +70,12 @@ function darray( return new __Private\DictLikeArraySpec('darray', $tsk, $tsv); } +function throwing_resolver(): TResolver { + return (TypeStructure $ts) ==> { + throw new UnsupportedTypeException(Shapes::at($ts, 'alias') as string); + }; +} + function dict( TypeSpec $tsk, TypeSpec $tsv, @@ -185,8 +195,9 @@ function varray_or_darray( return new __Private\VArrayOrDArraySpec($inner); } -function of(): TypeSpec { +function of(?TResolver $resolver = null): TypeSpec { return __Private\from_type_structure( \HH\ReifiedGenerics\get_type_structure(), + $resolver, ); } diff --git a/src/TypeSpec/__Private/from_type_structure.hack b/src/TypeSpec/__Private/from_type_structure.hack index 1a8962e..0389f88 100644 --- a/src/TypeSpec/__Private/from_type_structure.hack +++ b/src/TypeSpec/__Private/from_type_structure.hack @@ -15,16 +15,29 @@ use type Facebook\TypeSpec\TypeSpec; use namespace HH\Lib\{C, Dict, Vec}; use namespace Facebook\{TypeAssert, TypeSpec}; -function from_type_structure(TypeStructure $ts): TypeSpec { +function from_type_structure( + TypeStructure $ts, + ?TypeSpec\TResolver $resolver, +): TypeSpec { if ($ts['optional_shape_field'] ?? false) { $ts['optional_shape_field'] = false; /* HH_IGNORE_ERROR[4110] */ - return new OptionalSpec(from_type_structure($ts)); + return new OptionalSpec(from_type_structure($ts, $resolver)); } if ($ts['nullable'] ?? false) { $ts['nullable'] = false; /* HH_IGNORE_ERROR[4110] */ - return new NullableSpec(from_type_structure($ts)); + return new NullableSpec(from_type_structure($ts, $resolver)); + } + + if (Shapes::keyExists($ts, 'opaque') && $ts['opaque']) { + if ($resolver) { + /* HH_IGNORE_ERROR[4110] TypeSpec to TypeSpec unsafe cast*/ + return $resolver($ts); + } else { + // Either warn and fallback to validating the runtime type + // or throw an unsupported type exception. + } } /* HH_IGNORE_ERROR[4022] exhaustive + default */ @@ -62,7 +75,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { return new TupleSpec( Vec\map( TypeAssert\not_null($ts['elem_types']), - $elem ==> from_type_structure($elem), + $elem ==> from_type_structure($elem, $resolver), ), ); case TypeStructureKind::OF_FUNCTION: @@ -75,13 +88,13 @@ function from_type_structure(TypeStructure $ts): TypeSpec { return new UntypedArraySpec(); case 1: /* HH_IGNORE_ERROR[4110] */ - return new VecLikeArraySpec('array', from_type_structure($generics[0])); + return new VecLikeArraySpec('array', from_type_structure($generics[0], $resolver)); case 2: /* HH_IGNORE_ERROR[4110] */ return new DictLikeArraySpec( 'array', - from_type_structure($generics[0]), - from_type_structure($generics[1]), + from_type_structure($generics[0], $resolver), + from_type_structure($generics[1], $resolver), ); default: invariant_violation('OF_ARRAY with > 2 generics'); @@ -90,7 +103,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { $generics = $ts['generic_types'] as nonnull; invariant(C\count($generics) === 1, 'got varray with multiple generics'); /* HH_IGNORE_ERROR[4110] */ - return TypeSpec\varray(from_type_structure($generics[0])); + return TypeSpec\varray(from_type_structure($generics[0], $resolver)); case TypeStructureKind::OF_DARRAY: $generics = $ts['generic_types'] as nonnull; invariant( @@ -98,7 +111,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { 'darrays must have exactly 2 generics', ); /* HH_IGNORE_ERROR[4110] */ - return TypeSpec\darray(from_type_structure($generics[0]), from_type_structure($generics[1])); + return TypeSpec\darray(from_type_structure($generics[0], $resolver), from_type_structure($generics[1], $resolver)); case TypeStructureKind::OF_VARRAY_OR_DARRAY: $generics = $ts['generic_types'] as nonnull; invariant( @@ -106,26 +119,26 @@ function from_type_structure(TypeStructure $ts): TypeSpec { 'got varray_or_darray with multiple generics', ); /* HH_IGNORE_ERROR[4110] */ - return TypeSpec\varray_or_darray(from_type_structure($generics[0])); + return TypeSpec\varray_or_darray(from_type_structure($generics[0], $resolver)); case TypeStructureKind::OF_DICT: $generics = TypeAssert\not_null($ts['generic_types']); invariant(C\count($generics) === 2, 'dicts must have 2 generics'); /* HH_IGNORE_ERROR[4110] */ return TypeSpec\dict( - from_type_structure($generics[0]), - from_type_structure($generics[1]), + from_type_structure($generics[0], $resolver), + from_type_structure($generics[1], $resolver), ); case TypeStructureKind::OF_KEYSET: $generics = TypeAssert\not_null($ts['generic_types']); invariant(C\count($generics) === 1, 'keysets must have 1 generic'); /* HH_IGNORE_ERROR[4110] */ - return TypeSpec\keyset(from_type_structure($generics[0])); + return TypeSpec\keyset(from_type_structure($generics[0], $resolver)); case TypeStructureKind::OF_VEC: $generics = TypeAssert\not_null($ts['generic_types']); invariant(C\count($generics) === 1, 'vecs must have 1 generic'); /* HH_IGNORE_ERROR[4110] */ - return TypeSpec\vec(from_type_structure($generics[0])); + return TypeSpec\vec(from_type_structure($generics[0], $resolver)); case TypeStructureKind::OF_GENERIC: throw new UnsupportedTypeException('OF_GENERIC'); case TypeStructureKind::OF_SHAPE: @@ -134,7 +147,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { return new ShapeSpec( Dict\pull_with_key( $fields, - ($_k, $field_ts) ==> from_type_structure($field_ts), + ($_k, $field_ts) ==> from_type_structure($field_ts, $resolver), ($k, $_v) ==> $k, ), ($ts['allows_unknown_fields'] ?? false) @@ -153,6 +166,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { $classname, from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[0], + $resolver, ), ); case Map::class: @@ -163,9 +177,11 @@ function from_type_structure(TypeStructure $ts): TypeSpec { $classname, from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[0], + $resolver, ), from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[1], + $resolver, ), ); case Set::class: @@ -176,6 +192,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { $classname, from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[0], + $resolver, ), ); default: @@ -191,9 +208,11 @@ function from_type_structure(TypeStructure $ts): TypeSpec { $classname, from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[0], + $resolver, ), from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[1], + $resolver, ), ); } @@ -203,6 +222,7 @@ function from_type_structure(TypeStructure $ts): TypeSpec { $classname, from_type_structure( TypeAssert\not_null($ts['generic_types'] ?? null)[0], + $resolver, ), ); } diff --git a/src/example/new_behavior.hack b/src/example/new_behavior.hack new file mode 100644 index 0000000..dc09d6c --- /dev/null +++ b/src/example/new_behavior.hack @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016, Fred Emmott + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +use namespace Facebook\TypeSpec; + +use type Facebook\TypeAssert\UnsupportedTypeException; + +<<__EntryPoint>> +function new_behavior(): void { + require_once __DIR__.'/../../vendor/hh_autoload.hh'; + + // It is possible to keep the backward compatible behavior by not passing a resolver. + // @typechecker-type TypeSpec> + // @runtime-type TypeSpec> + $without_resolver = TypeSpec\of>(); + + echo '$without_resolver: '.$without_resolver->toString().\PHP_EOL; + + // You can opt-in to strict behavior without providing your own custom types. + try { + $_throws = TypeSpec\of>(TypeSpec\throwing_resolver()); + } catch (UnsupportedTypeException $e) { + echo $e->getMessage().\PHP_EOL; + } + + // Or you provide a custom resolver. + // @typechecker-type TypeSpec> + // @runtime-type TypeSpec> + $with_custom_resolver = TypeSpec\of>(fun('my_resolver')); + + echo '$with_custom_resolver: '.$with_custom_resolver->toString().\PHP_EOL; +} + + +function my_resolver(TypeStructure $ts): \Facebook\TypeSpec\TypeSpec { + switch ($ts['alias']) { + case 'UserID': + /*HH_IGNORE_ERROR[4110] Unsafe generics*/ + return MyTypeSpec\UserID(); + default: + throw new UnsupportedTypeException($ts['alias'] as string); + } +} diff --git a/src/example/newtype.hack b/src/example/newtype.hack new file mode 100644 index 0000000..982dfd3 --- /dev/null +++ b/src/example/newtype.hack @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016, Fred Emmott + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// Imaginary application code +namespace { + use type Facebook\TypeSpec\TypeSpec; + newtype UserID = int; + + // Advice from: Hack and HHVM: Programming Productivity Without Breaking Things + // I can recommend, it is a good read and a timemachine to the early days of Hack. + function is_user_id(int $user_id): bool { + return $user_id > (2 ** 48); + } + + namespace MyTypeSpec { + use type Facebook\TypeAssert\{ + IncorrectTypeException, + TypeCoercionException, + }; + + /*HHAST_IGNORE_ERROR[CamelCasedMethodsUnderscoredFunctions] This is the name of the type*/ + function UserID(): TypeSpec<\UserID> { + return new _Private\UserIDSpec(); + } + + namespace _Private { + + final class UserIDSpec extends TypeSpec<\UserID> { + + <<__Override>> + public function assertType(mixed $value): \UserID { + if (!$value is int || !\is_user_id($value)) { + throw IncorrectTypeException::withValue( + $this->getTrace(), + $this->toString(), + $value, + ); + } + return $value; + } + + // This is NoCoercionSpecTrait, but that is in the _Private namespace. + <<__Override>> + public function coerceType(mixed $value): \UserID { + try { + return $this->assertType($value); + } catch (IncorrectTypeException $e) { + throw TypeCoercionException::withValue( + $this->getTrace(), + $e->getExpectedType(), + $value, + ); + } + } + + <<__Override>> + public function toString(): string { + return 'UserID'; + } + } + } + } +} diff --git a/src/example/old_behavior.hack b/src/example/old_behavior.hack new file mode 100644 index 0000000..4699f8d --- /dev/null +++ b/src/example/old_behavior.hack @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, Fred Emmott + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +use namespace Facebook\TypeSpec; + +use type Facebook\TypeAssert\IncorrectTypeException; + +<<__EntryPoint>> +function old_behavior(): void { + require_once __DIR__.'/../../vendor/hh_autoload.hh'; + + // @type TypeSpec> + $_1 = TypeSpec\dict(TypeSpec\string(), TypeSpec\int()); + + // @type TypeSpec> + $with_manual_typespec = TypeSpec\dict(TypeSpec\string(), MyTypeSpec\UserID()); + echo '$with_manual_typespec: '.$with_manual_typespec->toString().\PHP_EOL; + + + // @typechecker-type TypeSpec> + // @runtime-type TypeSpec> + $with_typespec_of = TypeSpec\of>(); + echo '$with_typespec_of: '.$with_typespec_of->toString().\PHP_EOL; + + + // @typechecker-type UserID + // @runtime-type int (not validated) + $user_id = $with_typespec_of->assertType(dict['string' => 1])['string']; + + // TypeAssert collapsed UserID to int and my invariance on UserID is silently violated. + echo "Is my \$user_id a UserID?\n"; + \var_dump(\is_user_id($user_id as int)); + + try { + $_throws = $with_manual_typespec->assertType(dict['string' => 1]); + } catch (IncorrectTypeException $e) { + echo "\nThe manual typespec validated my invariant\n"; + echo $e->getMessage(); + } +} diff --git a/tests/ShapeSpecTest.hack b/tests/ShapeSpecTest.hack index a8c49c2..42e084a 100644 --- a/tests/ShapeSpecTest.hack +++ b/tests/ShapeSpecTest.hack @@ -39,6 +39,7 @@ final class ShapeSpecTest extends TypeSpecTest { public function getTypeSpec(): TypeSpec { return TypeSpec\__Private\from_type_structure( type_structure(self::class, 'TShapeA'), + null, ); }