-
Notifications
You must be signed in to change notification settings - Fork 30
Improve type inference for member access in unions and intersections #461
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
base: main
Are you sure you want to change the base?
Conversation
Things to consider: This change makes access to union members stricter. This is good for cases when union contains types that aren't safe to index ( local x --- @type { a: integer } | { b: integer }
if x.a ~= nil then --> Warning: no field named `a`.
-- ...
end To make this case better, we can save information about reason for inference failure in P.S. should we rename |
|
The current system is not prepared for the occurrence of "never." As I explained before, I cannot determine the specific types of A and B during infer_type, so I cannot reasonably simplify type inference. |
I'm not sure I understand where the problem is. I mean, Sure, if a user spreads declaration of a class over multiple files, they might have an issue. But I don't think it's a common thing to do. |
Maybe you can give an example, when thing will go wrong? |
When diagnosing, all information is indeed determined, but during inference, everything is uncertain. For example, when dealing with A & B, a field 'x' of A might be defined in a place that I haven't analyzed yet or cannot analyze, so I don't consider 'x' to be a field of A. However, at this point, B may have already been analyzed and 'x' has been identified. Now, when we try to resolve (A & B).x, it becomes never. However, once the field 'x' of A is later inferred, (A & B).x is no longer never. |
But what happens if we try to resolve |
No matter whether we assume A.x exists or not, any subsequent inference that depends on it will be incorrect. This issue does not exist in TypeScript, because in TypeScript, type definitions are not split across files—all types and their members are already determined, as it can rely on imports to establish a dependency graph and completely trust this dependency relationship. This is not possible in Lua. |
I think the current PR can be kept for now until I make a reasonable refactor and assumptions someday. At the moment, I still have many stack overflow and performance issues to resolve. |
My point is, if we calculate type of a field during inference stage, and we don't have all information about it, we will get an incorrect result. Doesn't matter if this field is in a union or in a simple table. So I don't see the reason we can't merge this PR now, and fix this issue later. Besides, current implementation for union works like intersection, so maybe it's better to fix it before too much users depend on it? |
Here's some preliminary thoughts on this issue.
|
This is unacceptable. There are many large-scale projects using EmmyLua, often with more than 5,000+ files, and I've even seen single You can try refactoring the analysis algorithm; cppcxy doesn't have enough time to make the changes, so don't count on it happening this year at least. |
@xuhuanzy do you have a link to such examples? If I'll take on any refactorings, I'd like to see if my changes slow down or break anything. |
Most large projects like this are usually internal code within various companies, so they cannot be shared. This is also why it is difficult for us to solve these problems. |
I am working on EmmyLuaLs/emmylua_dap for zed , intellij and vscode, I need to consider these PRs after that. |
|
Alright, I've tried to come up with a test that would fail for such case, and I couldn't. The pass architecture from my third suggestion is exactly how things are implemented now. All in all, I don't see why my PR can't be merged. |
I'm still not very familiar with code base, so comments are welcome.
Fix #454.
This behavior is consistent with modern type theory. In mathematics, 'type' is defined as a set of all possible values that a variable can have. Therefore, a union of types would include all possible values for left and right types. An intersection of types would only include values that can be assigned to both left and right types. Hence, we come to rules for member access:
Note for unions. If one of the types does not contain a member, we shouldn't allow accessing it:
Note for intersections. If one of the types does not contain a member, we won't be able to construct such type, therefore member access is safe: