Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Ternary Expression TypeNarrower for Nullables #14

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7efcfd3
TASK: Put domain logic inside constructor of UnionType
mhsdesign Apr 9, 2023
2ffe099
TASK: Simple Nullable Handling
mhsdesign Apr 9, 2023
6293145
TASK: Infer types in arms of ternary
mhsdesign Apr 9, 2023
2a08474
TASK: Revert 7efcfd3cc179443912b5e06d1b913efde3cb5739
mhsdesign Apr 9, 2023
5e4d6ce
TASK: Union test that all members are deduplicated
mhsdesign Apr 9, 2023
5c08a4e
TASK: Union test isNullable and withoutNullable
mhsdesign Apr 9, 2023
8f0ba11
TASK: UnionType rename to containsNull, withoutNull
mhsdesign Apr 21, 2023
8a34ee1
TASK: Introduce TernaryBranchScope
mhsdesign Apr 21, 2023
1b476e5
TASK: Type inference for null comparison in ternary
mhsdesign Apr 21, 2023
57ee20c
TASK: UnionType RequiresAtLeastOneMember
mhsdesign Apr 21, 2023
3487bd9
TASK: TernaryBranchScope introduce static factories and dont throw bo…
mhsdesign Apr 21, 2023
1348ac1
TASK: Make type inference in TernaryBranchScope more explicit
mhsdesign Apr 22, 2023
492e05c
TASK: Adjust naming of $nonNullable to $typeWithoutNull
mhsdesign Apr 22, 2023
0c7061f
TASK: Solve #7 rudimentary
mhsdesign Apr 22, 2023
22df4ba
TASK: Introduce TypeInferrer inspired by phpstan to support future ad…
mhsdesign Apr 22, 2023
0adb4c0
TASK: Cleanup InferredTypes and extract duplicated logic to TypeInfer…
mhsdesign Apr 23, 2023
f61b896
TASK: Remove `@phpstan-ignore-next-line` by asserting that an array i…
mhsdesign Apr 23, 2023
6a8bd00
Merge remote-tracking branch 'origin/main' into task/simpleNullableHa…
mhsdesign Apr 23, 2023
d835fa5
TASK: UnionType::getIterator use `yield from`
mhsdesign Apr 26, 2023
88c6fd6
TASK: Rename `Inferrer` to `Narrower` and apply further suggestions f…
mhsdesign Apr 26, 2023
e0a913a
TASK: `Narrower` handle boolean literal comparisons
mhsdesign Apr 26, 2023
aaf6c49
TASK: `Narrower` null comparison against any expression that resolves…
mhsdesign Apr 26, 2023
560c97d
TASK: Add `ExpressionTypeNarrowerTest`
mhsdesign Apr 26, 2023
d00a194
TASK: Don't narrow `nullableString === true` as string
mhsdesign Apr 26, 2023
530b155
TASK: Narrow `nullableString && true`
mhsdesign Apr 26, 2023
331cdda
TASK: Correct namespace
mhsdesign Apr 26, 2023
bd28d87
Merge remote-tracking branch 'origin' into task/simpleNullableHandling
mhsdesign Apr 29, 2023
1027e16
TASK: Adjust to BinaryOperationNode api change
mhsdesign Apr 29, 2023
884b895
TASK: Apply suggestions from code review
mhsdesign Apr 29, 2023
0121247
TASK: ExpressionTypeNarrower support UnaryOperationNode
mhsdesign Apr 29, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class TypeReferenceTranspiler
{
Expand All @@ -43,22 +45,41 @@ public function __construct(
public function transpile(TypeReferenceNode $typeReferenceNode): string
{
$type = $this->scope->resolveTypeReference($typeReferenceNode);
$phpTypeReference = match ($type::class) {

return match ($type::class) {
UnionType::class => $this->transpileUnionType($type, $typeReferenceNode),
default => $this->transpileNonUnionType($type, $typeReferenceNode)
};
}

private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typeReferenceNode): string
{
if (count($unionType) === 2 && $unionType->containsNull()) {
$typeWithoutNull = $unionType->withoutNull();
return $this->transpileNullableType($typeWithoutNull, $typeReferenceNode);
}

throw new \Exception('@TODO Transpilation of complex union types is not implemented');
}

private function transpileNonUnionType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string
{
return match ($type::class) {
NumberType::class => 'int|float',
StringType::class => 'string',
BooleanType::class => 'bool',
SlotType::class => $this->strategy->getPhpTypeReferenceForSlotType($type, $typeReferenceNode),
ComponentType::class => $this->strategy->getPhpTypeReferenceForComponentType($type, $typeReferenceNode),
EnumType::class => $this->strategy->getPhpTypeReferenceForEnumType($type, $typeReferenceNode),
StructType::class => $this->strategy->getPhpTypeReferenceForStructType($type, $typeReferenceNode),
UnionType::class => throw new \Exception("There is no such thing as nested unions, think again."),
default => $this->strategy->getPhpTypeReferenceForCustomType($type, $typeReferenceNode)
};
}

return $typeReferenceNode->isOptional
? match ($phpTypeReference) {
'int|float' => 'null|int|float',
default => '?' . $phpTypeReference
}
: $phpTypeReference;
private function transpileNullableType(TypeInterface $typeWithoutNull, TypeReferenceNode $typeReferenceNode): string
{
$phpTypeWithoutNull = $this->transpileNonUnionType($typeWithoutNull, $typeReferenceNode);
return (str_contains($phpTypeWithoutNull, '|') ? 'null|' : '?') . $phpTypeWithoutNull;
}
}
151 changes: 151 additions & 0 deletions src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

/**
* PackageFactory.ComponentEngine - Universal View Components for PHP
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression;

use PackageFactory\ComponentEngine\Definition\BinaryOperator;
use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode;
use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode;
use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode;
use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode;
use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\Truthiness;
use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;

/**
* This class handles the analysis of identifier types that are used in a condition
* and based on the requested branch: truthy or falsy, will predict the types a variable will have in the respective branch
* so it matches the expected runtime behaviour
*
* For example given this expression: `nullableString ? nullableString : "fallback"` based on the condition `nullableString`
* it will infer that in the truthy context nullableString is a string while in the falsy context it will infer that it is null.
* In the above case the ternary expression will resolve to a string.
*
* The structure is partially inspired by phpstan
* https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/TypeSpecifier.php#L111
*/
class ExpressionTypeNarrower
{
private function __construct(
private readonly ScopeInterface $scope,
private readonly Truthiness $assumedTruthiness
) {
}

public static function forTruthy(ScopeInterface $scope): self
{
return new self($scope, Truthiness::TRUTHY);
}

public static function forFalsy(ScopeInterface $scope): self
{
return new self($scope, Truthiness::FALSY);
}

public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode): NarrowedTypes
{
if ($expressionNode->root instanceof IdentifierNode) {
$type = $this->scope->lookupTypeFor($expressionNode->root->value);
if (!$type) {
return NarrowedTypes::empty();
}
// case `nullableString ? "nullableString is not null" : "nullableString is null"`
return NarrowedTypes::fromEntry($expressionNode->root->value, $this->assumedTruthiness->narrowType($type));
}

if (($binaryOperationNode = $expressionNode->root) instanceof BinaryOperationNode) {
$right = $binaryOperationNode->right;
$left = $binaryOperationNode->left;

if (
(($boolean = $right->root) instanceof BooleanLiteralNode
&& $other = $left // @phpstan-ignore-line
) || (($boolean = $left->root) instanceof BooleanLiteralNode
&& $other = $right // @phpstan-ignore-line
)
) {
switch ($binaryOperationNode->operator) {
case BinaryOperator::AND:
if ($boolean->value && $this->assumedTruthiness === Truthiness::TRUTHY) {
return $this->narrowTypesOfSymbolsIn($other);
}
break;
case BinaryOperator::EQUAL:
case BinaryOperator::NOT_EQUAL:
$contextBasedOnOperator = $this->assumedTruthiness->basedOnBinaryOperator($binaryOperationNode->operator);
assert($contextBasedOnOperator !== null);

if ($other->root instanceof IdentifierNode) {
return NarrowedTypes::empty();
}

$subNarrower = new self(
$this->scope,
$boolean->value ? $contextBasedOnOperator : $contextBasedOnOperator->negate()
);
return $subNarrower->narrowTypesOfSymbolsIn($other);
}

return NarrowedTypes::empty();
}

$expressionTypeResolver = (new ExpressionTypeResolver($this->scope));
if (
($expressionTypeResolver->resolveTypeOf($right)->is(NullType::get())
&& $other = $left // @phpstan-ignore-line
) || ($expressionTypeResolver->resolveTypeOf($left)->is(NullType::get())
&& $other = $right // @phpstan-ignore-line
)
) {
if (!$other->root instanceof IdentifierNode) {
return NarrowedTypes::empty();
}
$type = $this->scope->lookupTypeFor($other->root->value);
if (!$type) {
return NarrowedTypes::empty();
}

if (!$contextBasedOnOperator = $this->assumedTruthiness->basedOnBinaryOperator($binaryOperationNode->operator)) {
return NarrowedTypes::empty();
}

return NarrowedTypes::fromEntry(
$other->root->value,
$contextBasedOnOperator->negate()->narrowType($type)
);
}
}

if (($unaryOperationNode = $expressionNode->root) instanceof UnaryOperationNode) {
$subNarrower = new self(
$this->scope,
$this->assumedTruthiness->negate()
);
return $subNarrower->narrowTypesOfSymbolsIn($unaryOperationNode->argument);
}

return NarrowedTypes::empty();
}
}
62 changes: 62 additions & 0 deletions src/TypeSystem/Narrower/NarrowedTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* PackageFactory.ComponentEngine - Universal View Components for PHP
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace PackageFactory\ComponentEngine\TypeSystem\Narrower;

use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

class NarrowedTypes
{
/**
* Map of identifierName to the corresponding inferred type
* @var array<string,TypeInterface>
*/
private readonly array $types;

private function __construct(
TypeInterface ...$types
) {
/** @var array<string,TypeInterface> $types */
$this->types = $types;
}

public static function empty(): self
{
return new self();
}

public static function fromEntry(string $identifierName, TypeInterface $type): self
{
return new self(...[$identifierName => $type]);
}

public function getType(string $identifierName): ?TypeInterface
{
return $this->types[$identifierName] ?? null;
}

/** @return array<string,TypeInterface> */
public function toArray(): array
{
return $this->types;
}
}
63 changes: 63 additions & 0 deletions src/TypeSystem/Narrower/Truthiness.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/**
* PackageFactory.ComponentEngine - Universal View Components for PHP
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace PackageFactory\ComponentEngine\TypeSystem\Narrower;

use PackageFactory\ComponentEngine\Definition\BinaryOperator;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

enum Truthiness
{
case TRUTHY;

case FALSY;

public function negate(): self
{
return match ($this) {
self::TRUTHY => self::FALSY,
self::FALSY => self::TRUTHY
};
}

public function basedOnBinaryOperator(BinaryOperator $operator): ?self
{
return match ($operator) {
BinaryOperator::EQUAL => $this,
BinaryOperator::NOT_EQUAL => $this->negate(),
default => null,
};
}

public function narrowType(TypeInterface $type): TypeInterface
{
if (!$type instanceof UnionType || !$type->containsNull()) {
return $type;
}
return match ($this) {
self::TRUTHY => $type->withoutNull(),
self::FALSY => NullType::get()
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode;
use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode;
use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver;
use PackageFactory\ComponentEngine\TypeSystem\Scope\TernaryBranchScope\TernaryBranchScope;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;
Expand All @@ -38,20 +39,29 @@ public function __construct(

public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeInterface
{
$expressionTypeResolver = new ExpressionTypeResolver(
scope: $this->scope
$trueExpressionTypeResolver = new ExpressionTypeResolver(
scope: TernaryBranchScope::forTruthyBranch(
$ternaryOperationNode->condition,
$this->scope
)
);
$conditionNode = $ternaryOperationNode->condition->root;

if ($conditionNode instanceof BooleanLiteralNode) {
return $conditionNode->value
? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true)
: $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false);
$falseExpressionTypeResolver = new ExpressionTypeResolver(
scope: TernaryBranchScope::forFalsyBranch(
$ternaryOperationNode->condition,
$this->scope
)
);

if ($ternaryOperationNode->condition->root instanceof BooleanLiteralNode) {
return $ternaryOperationNode->condition->root->value
? $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true)
: $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false);
}

return UnionType::of(
$expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
$expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)
$trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
$falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)
);
}
}
Loading