-
Notifications
You must be signed in to change notification settings - Fork 1
WIP: Ternary Expression TypeNarrower for Nullables #14
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
Draft
mhsdesign
wants to merge
30
commits into
main
Choose a base branch
from
task/simpleNullableHandling
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 6 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 2ffe099
TASK: Simple Nullable Handling
mhsdesign 6293145
TASK: Infer types in arms of ternary
mhsdesign 2a08474
TASK: Revert 7efcfd3cc179443912b5e06d1b913efde3cb5739
mhsdesign 5e4d6ce
TASK: Union test that all members are deduplicated
mhsdesign 5c08a4e
TASK: Union test isNullable and withoutNullable
mhsdesign 8f0ba11
TASK: UnionType rename to containsNull, withoutNull
mhsdesign 8a34ee1
TASK: Introduce TernaryBranchScope
mhsdesign 1b476e5
TASK: Type inference for null comparison in ternary
mhsdesign 57ee20c
TASK: UnionType RequiresAtLeastOneMember
mhsdesign 3487bd9
TASK: TernaryBranchScope introduce static factories and dont throw bo…
mhsdesign 1348ac1
TASK: Make type inference in TernaryBranchScope more explicit
mhsdesign 492e05c
TASK: Adjust naming of $nonNullable to $typeWithoutNull
mhsdesign 0c7061f
TASK: Solve #7 rudimentary
mhsdesign 22df4ba
TASK: Introduce TypeInferrer inspired by phpstan to support future ad…
mhsdesign 0adb4c0
TASK: Cleanup InferredTypes and extract duplicated logic to TypeInfer…
mhsdesign f61b896
TASK: Remove `@phpstan-ignore-next-line` by asserting that an array i…
mhsdesign 6a8bd00
Merge remote-tracking branch 'origin/main' into task/simpleNullableHa…
mhsdesign d835fa5
TASK: UnionType::getIterator use `yield from`
mhsdesign 88c6fd6
TASK: Rename `Inferrer` to `Narrower` and apply further suggestions f…
mhsdesign e0a913a
TASK: `Narrower` handle boolean literal comparisons
mhsdesign aaf6c49
TASK: `Narrower` null comparison against any expression that resolves…
mhsdesign 560c97d
TASK: Add `ExpressionTypeNarrowerTest`
mhsdesign d00a194
TASK: Don't narrow `nullableString === true` as string
mhsdesign 530b155
TASK: Narrow `nullableString && true`
mhsdesign 331cdda
TASK: Correct namespace
mhsdesign bd28d87
Merge remote-tracking branch 'origin' into task/simpleNullableHandling
mhsdesign 1027e16
TASK: Adjust to BinaryOperationNode api change
mhsdesign 884b895
TASK: Apply suggestions from code review
mhsdesign 0121247
TASK: ExpressionTypeNarrower support UnaryOperationNode
mhsdesign File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?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\Scope\ShallowScope; | ||
|
||
use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; | ||
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; | ||
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; | ||
|
||
final class ShallowScope implements ScopeInterface | ||
{ | ||
public function __construct( | ||
private readonly string $overriddenName, | ||
private readonly TypeInterface $overriddenType, | ||
private readonly ScopeInterface $parentScope | ||
) { | ||
} | ||
|
||
public function lookupTypeFor(string $name): ?TypeInterface | ||
{ | ||
if ($this->overriddenName === $name) { | ||
return $this->overriddenType; | ||
} | ||
|
||
return $this->parentScope->lookupTypeFor($name); | ||
} | ||
|
||
public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface | ||
{ | ||
return $this->parentScope->resolveTypeReference($typeReferenceNode); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It pains me to object to this, because I really, really like the idea. Yet, I think the cost of introducing
ShallowScope
for this is too high.My reasoning here goes as follows:
The problem that the code above attempts to solve is that of deduction of a non-nullable subtype from a nullable union type given its falsiness has been determined for a ternary expression branch. The conditions in line 57 further narrow the specificity of the problem thusly:
I would say that this is a very specific case. (Indeed a tad too specific, but more on that later)
Now, to make the deduction visible to the ternary branches, their respective type resolvers are instantiated each with a
ShallowScope
instance that overrides the type that the identifier in the ternary condition points to.I would say that
ShallowScope
is a very general abstraction.The point of the scope-system is to give total control over the mode of type resolution to specific language constructs that in the mental model of the reader/writer intuitively invoke the notion of "scope". Conceptually, I would say this prohibits the existence of more abstract scope implementations that would have no correspondence to said mental model.
Fortunately, this could be remedied rather easily, by renaming
ShallowScope
toTernaryBranchScope
. (Since in the above scenario ternary branches would carry deduced type information, I find it absolutely fair to say that there is such a thing as aTernaryBranchScope
)This leaves us with the problem of specificity introduced by the condition in line 57. Take these two examples:
(1)
(2)
In both cases, reader's intuition would be able to tell that
a
is not null in the true branch. But due to the limitation through the condition in line 57, the type system would acknowledge this in case (1) but not in case (2). I would argue that this runs counter reader's intuition, who would expect this behavior to be consistent.The more I think about it, the more this deduction problem sounds like a job for the aforementioned
TernaryBranchScope
:)TernaryBranchScope
may very well be aware of the entire expression of the ternary condition. Then if asked for the type of an identifier, it could then and there deduce a narrower type.So, to conclude this: I'd like to suggest the introduction of
TernaryBranchScope
(as a replacement forShallowScope
) in the scope of this PR. It should take on the task of deduction as I've described it, but for now it would be fine to keep the current limitations on that algorithm and leave a TODO-comment, so it may be refined later.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented :D But now im too tired to type things down that i encountered, see you tomorrow.
(And thanks for the tipp with TernaryBranchScope works like a charm and even better ^^)
Also i cant thank you enough for your most precise code reviews and comments - they are extremely helpful and well guiding me! Thank you really so much!!!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes youre right, my implementation is only limited for super simple things like
foo ? foo : ""
i thought about how we can i implement a universal solution, which also handles any thinkable more complex case, but it seems like a hard challenge
for example given
foo
is a nullable string.those expressions will be resolved as of now correctly:
but here we loose it:
I checked all the examples with typescript, and it does a good job (unsurprisingly). I even went as far as trying to have a look at the ts codebase for some inspiration, but i couldnt even find the right unit tests to start with xD
Some of the above are surely not a real world use case but its a simple demonstration how easy it is to confuse the implemented type resolution (or type inference?).
I think i need to find out while resolving the condition part, which variable participated and how it affected the truthyness. Im not really sure if this would be the right approach in the long run and if it can be easily implemented.
But for now im also quite happy with my current implementation as a start.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found the place where phpstan does it:
https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/MutatingScope.php#L1616
interestingly phpstan's code is quite clever but not clever to not fail for the bait
(foo ? foo : foo) ? foo : ""
:https://phpstan.org/r/3b012ae8-4dd9-4901-95c0-45587f70134e
also psalm fails for the bait
https://psalm.dev/r/c536f4dab4
but psalm correctly works with your example:
and i couldnt find another way (except the ternary thing) to confuse the type resolution - its really good.
with 22df4ba
i kind of stole the basic idea and structure of phpstan and it should be capable to implement in the future more complex inference - but first i need to build support for negation
!foo
it came just to my mind that it will be fun to implement type inference for nullable struct members ...
foo.bar ? 1 : 0
or evenfoo.bar?.buz ? 1 : 0