Skip to content
This repository has been archived by the owner on Sep 1, 2023. It is now read-only.

[ DO NOT MERGE ] Write a quick and dirty PoC for RFC #51 #50

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/TypeAssert.hack
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ function classname_of<T>(classname<T> $expected, string $what): classname<T> {
return TypeSpec\classname($expected)->assertType($what);
}

function matches_type_structure<T>(TypeStructure<T> $ts, mixed $value): T {
return TypeSpec\__Private\from_type_structure($ts)->assertType($value);
function matches_type_structure<T>(
TypeStructure<T> $ts,
mixed $value,
?TypeSpec\TResolver $resolver = null,
): T {
return TypeSpec\__Private\from_type_structure($ts, $resolver)->assertType(
$value,
);
}

function matches<reify T>(mixed $value): T {
Expand Down
10 changes: 8 additions & 2 deletions src/TypeCoerce.hack
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ function arraykey(mixed $x): arraykey {
return TypeSpec\arraykey()->coerceType($x);
}

function match_type_structure<T>(TypeStructure<T> $ts, mixed $value): T {
return TypeSpec\__Private\from_type_structure($ts)->coerceType($value);
function match_type_structure<T>(
TypeStructure<T> $ts,
mixed $value,
?TypeSpec\TResolver $resolver = null,
): T {
return TypeSpec\__Private\from_type_structure($ts, $resolver)->coerceType(
$value,
);
}

function match<reify T>(mixed $value): T {
Expand Down
13 changes: 12 additions & 1 deletion src/TypeSpec.hack
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

namespace Facebook\TypeSpec;

use type Facebook\TypeAssert\UnsupportedTypeException;

type TResolver = (function(TypeStructure<mixed>): TypeSpec<mixed>);

abstract class TypeSpec<+T> {
private ?Trace $trace = null;

Expand Down Expand Up @@ -66,6 +70,12 @@ function darray<Tk as arraykey, Tv>(
return new __Private\DictLikeArraySpec('darray', $tsk, $tsv);
}

function throwing_resolver(): TResolver {
return (TypeStructure<mixed> $ts) ==> {
throw new UnsupportedTypeException(Shapes::at($ts, 'alias') as string);
};
}

function dict<Tk as arraykey, Tv>(
TypeSpec<Tk> $tsk,
TypeSpec<Tv> $tsv,
Expand Down Expand Up @@ -185,8 +195,9 @@ function varray_or_darray<Tv>(
return new __Private\VArrayOrDArraySpec($inner);
}

function of<reify T>(): TypeSpec<T> {
function of<reify T>(?TResolver $resolver = null): TypeSpec<T> {
return __Private\from_type_structure(
\HH\ReifiedGenerics\get_type_structure<T>(),
$resolver,
);
}
50 changes: 35 additions & 15 deletions src/TypeSpec/__Private/from_type_structure.hack
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(TypeStructure<T> $ts): TypeSpec<T> {
function from_type_structure<T>(
TypeStructure<T> $ts,
?TypeSpec\TResolver $resolver,
): TypeSpec<T> {
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<mixed> to TypeSpec<T> 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 */
Expand Down Expand Up @@ -62,7 +75,7 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
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:
Expand All @@ -75,13 +88,13 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
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');
Expand All @@ -90,42 +103,42 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
$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(
C\count($generics) === 2,
'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(
C\count($generics) === 1,
'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:
Expand All @@ -134,7 +147,7 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
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)
Expand All @@ -153,6 +166,7 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
$classname,
from_type_structure(
TypeAssert\not_null($ts['generic_types'] ?? null)[0],
$resolver,
),
);
case Map::class:
Expand All @@ -163,9 +177,11 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
$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:
Expand All @@ -176,6 +192,7 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
$classname,
from_type_structure(
TypeAssert\not_null($ts['generic_types'] ?? null)[0],
$resolver,
),
);
default:
Expand All @@ -191,9 +208,11 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
$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,
),
);
}
Expand All @@ -203,6 +222,7 @@ function from_type_structure<T>(TypeStructure<T> $ts): TypeSpec<T> {
$classname,
from_type_structure(
TypeAssert\not_null($ts['generic_types'] ?? null)[0],
$resolver,
),
);
}
Expand Down
50 changes: 50 additions & 0 deletions src/example/new_behavior.hack
Original file line number Diff line number Diff line change
@@ -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<dict<string, UserID>>
// @runtime-type TypeSpec<dict<string, int>>
$without_resolver = TypeSpec\of<dict<string, UserID>>();

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<dict<string, UserID>>(TypeSpec\throwing_resolver());
} catch (UnsupportedTypeException $e) {
echo $e->getMessage().\PHP_EOL;
}

// Or you provide a custom resolver.
// @typechecker-type TypeSpec<dict<string, UserID>>
// @runtime-type TypeSpec<dict<string, UserID>>
$with_custom_resolver = TypeSpec\of<dict<string, UserID>>(fun('my_resolver'));

echo '$with_custom_resolver: '.$with_custom_resolver->toString().\PHP_EOL;
}


function my_resolver<T>(TypeStructure<T> $ts): \Facebook\TypeSpec\TypeSpec<T> {
switch ($ts['alias']) {
case 'UserID':
/*HH_IGNORE_ERROR[4110] Unsafe generics*/
return MyTypeSpec\UserID();
default:
throw new UnsupportedTypeException($ts['alias'] as string);
}
}
70 changes: 70 additions & 0 deletions src/example/newtype.hack
Original file line number Diff line number Diff line change
@@ -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';
}
}
}
}
}
Loading