Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
63 changes: 36 additions & 27 deletions src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode;
use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext;
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;
Expand All @@ -38,75 +38,84 @@
* 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 is not null" : "nullableString is null"` 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 a null
* 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
{
public function __construct(
private readonly ScopeInterface $scope
private function __construct(
private readonly ScopeInterface $scope,
private readonly Truthiness $assumedTruthiness
) {
}

public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarrowerContext $context): NarrowedTypes
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, $context->narrowType($type));
return NarrowedTypes::fromEntry($expressionNode->root->value, $this->assumedTruthiness->narrowType($type));
}

if (($binaryOperationNode = $expressionNode->root) instanceof BinaryOperationNode) {
// todo we currently only work with two operands
if (count($binaryOperationNode->operands->rest) !== 1) {
return NarrowedTypes::empty();
}
$first = $binaryOperationNode->operands->first;
$second = $binaryOperationNode->operands->rest[0];
$right = $binaryOperationNode->right;
$left = $binaryOperationNode->left;

if (
(($boolean = $first->root) instanceof BooleanLiteralNode
&& $other = $second // @phpstan-ignore-line
) || (($boolean = $second->root) instanceof BooleanLiteralNode
&& $other = $first // @phpstan-ignore-line
(($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 && $context === TypeNarrowerContext::TRUTHY) {
return $this->narrowTypesOfSymbolsIn($other, $context);
if ($boolean->value && $this->assumedTruthiness === Truthiness::TRUTHY) {
return $this->narrowTypesOfSymbolsIn($other);
}
break;
case BinaryOperator::EQUAL:
case BinaryOperator::NOT_EQUAL:
$contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator);
$contextBasedOnOperator = $this->assumedTruthiness->basedOnBinaryOperator($binaryOperationNode->operator);
assert($contextBasedOnOperator !== null);

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

return $this->narrowTypesOfSymbolsIn(
$other,
$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($first)->is(NullType::get())
&& $other = $second // @phpstan-ignore-line
) || ($expressionTypeResolver->resolveTypeOf($second)->is(NullType::get())
&& $other = $first // @phpstan-ignore-line
($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) {
Expand All @@ -117,7 +126,7 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro
return NarrowedTypes::empty();
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

enum TypeNarrowerContext
enum Truthiness
{
case TRUTHY;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@
use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression\ExpressionTypeNarrower;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class TernaryBranchScope implements ScopeInterface
{
private function __construct(
public function __construct(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because one should use the static factories - but I guess this will make unit testing harder … for now fine imo? I like having private constructors at times …

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you've made it public 🧐

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misread the diff 😂 its the 4 day in berlin 😅😂

private readonly NarrowedTypes $narrowedTypes,
private readonly ScopeInterface $parentScope
) {
Expand All @@ -41,15 +40,15 @@ private function __construct(
public static function forTruthyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self
{
return new self(
(new ExpressionTypeNarrower($parentScope))->narrowTypesOfSymbolsIn($conditionNode, TypeNarrowerContext::TRUTHY),
ExpressionTypeNarrower::forTruthy($parentScope)->narrowTypesOfSymbolsIn($conditionNode),
$parentScope
);
}

public static function forFalsyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self
{
return new self(
(new ExpressionTypeNarrower($parentScope))->narrowTypesOfSymbolsIn($conditionNode, TypeNarrowerContext::FALSY),
ExpressionTypeNarrower::forFalsy($parentScope)->narrowTypesOfSymbolsIn($conditionNode),
$parentScope
);
}
Comment thread
grebaldi marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression\ExpressionTypeNarrower;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes;
use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
Expand Down Expand Up @@ -85,7 +84,7 @@ public function narrowedExpressionsExamples(): mixed
'FALSY:: nullableString && true' => [
'nullableString && true',
NarrowedTypes::empty(),
TypeNarrowerContext::FALSY
false
],

'nullableString === true' => [
Expand All @@ -102,18 +101,20 @@ public function narrowedExpressionsExamples(): mixed
public function narrowedExpressions(
string $expressionAsString,
NarrowedTypes $expectedTypes,
TypeNarrowerContext $context = TypeNarrowerContext::TRUTHY
bool $truthiness = true
): void {
$expressionTypeNarrower = new ExpressionTypeNarrower(
scope: new DummyScope([
'nullableString' => UnionType::of(StringType::get(), NullType::get()),
'variableOfTypeNull' => NullType::get()
])
);
$scope = new DummyScope([
'nullableString' => UnionType::of(StringType::get(), NullType::get()),
'variableOfTypeNull' => NullType::get()
]);

$expressionTypeNarrower = $truthiness
? ExpressionTypeNarrower::forTruthy($scope)
: ExpressionTypeNarrower::forFalsy($scope);

$expressionNode = ExpressionNode::fromString($expressionAsString);

$actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode, $context);
$actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode);

$this->assertEqualsCanonicalizing(
$expectedTypes->toArray(),
Expand Down