Benjamin-Dobell / IntelliJ-Luanalysis

Type-safe Lua IDE — IntelliJ IDEA plugin
Apache License 2.0
155 stars 22 forks source link

Top and Bottom Types Distinct from "any" #142

Open Feuermurmel opened 1 year ago

Feuermurmel commented 1 year ago

I hope I'm understanding the current intended semantics of any correctly. To my understanding, any is a type that is meant to be assignable to any other type and also from any other type. It is used in places where the real type isn't known or where it would be too cumbersome to write it down. Similar to any in TypeScript and Any in Python.

What I'm mainly missing is a Top type, i.e. a type that is assignable from any other type, but not vice-versa. This would be useful in cases where the type a type is not known and where the analyzer should check that every possible type is handled. This is what Python's object and TypeScript's unknown is.

And by extension, a Bottom type could be added, i.e. a type that is assignable to any other type, but not vice-versa. This would be useful in cases that are expected to never happen, e.g. as return type of a function that never returns or that always panics. This is what Python's Never (alias of NoReturn) and TypeScript's never is.

Benjamin-Dobell commented 1 year ago

Definitely on my list of things to do. unknown would be the highest priority. never would definitely be nice to have as well, but there's a whole heap of control flow stuff that's currently missing and without it never isn't all that useful.

There is at least kind of a never type for general use, but in the case of a function it implies either return (without a value) or the absence of return, which is subtlety different from a function that returns nil. You can also get at the void type implicitly. At runtime it'll actually be nil, but it's useful for us when writing code to have a distinction between something that's intentionally nil and meant to be nothing.

---@type string
local aString

local implicitVoid = --[[---@not string]] aString

---@type void
local explicitVoid

explicitVoid = implicitVoid
image
Feuermurmel commented 1 year ago

Ah, I see. So @not can be used for the same as TypeScript's Exclude. Somehow it's behaving a bit strange. It works in the example you have, with a separate variable of type string, but it does not work on a string literal:

image

And as you explained, void does not have the properties that one would expect from a never type:

image

Benjamin-Dobell commented 1 year ago

That first case is obviously incorrect and is something I'd like to fix at some point. For the most part it comes down to the fact we don't have intersection types, as you've noted elsewhere.

For that 2nd example though, what behaviour would you expect?

TypeScript will allow the assignment with never:

image

but I'd argue that's incorrect, or at least, less useful to developers.

That said, Luanalysis can be tricked into doing something similar:

---@type string
local aString

---@type void
local explicitVoid

aString = explicitVoid
aString = math.random() and "hi" or explicitVoid;
image

Basically if you X | void the result is X, which is the same behaviour as TypeScript and generally correct.

The difference is that Luanalysis forbids assignment of "nothing" to "something" which I'd argue is more useful. When we get into set theory (e.g. unions) it disappears though as it should.

Feuermurmel commented 1 year ago

Hey, it took a bit of time to come back to this and answer your questions. After opening the few issues on this project, I quickly decided to drop it and move to something else (TypeScriptToLua). So I'm not invested in these issues anymore, but I'll try to answer your questions.

For that 2nd example though, what behaviour would you expect?

It depends on what the intention for void is. Generally in the type systems I've seen it's one of two things:

With (a), allowing void to be used as a type is confusing, so ---@type void on a variable should be flagged as an error, which makes the question of assignment irrelevant. All usages of the return value of such e function should be flagged as an error.

With (b), the unit type should be assignable like any other type.

TypeScript will allow the assignment with never:

image

but I'd argue that's incorrect, or at least, less useful to developers.

as is an unchecked cast. IMHO the problem with the example code lies there. After that, all bets are off anyways. I'd argue the syntax for an unchecked cast is much too light in TypeScript and looks like something on-dangerous.

I've written a lot of code in langauges that treat the bottom type as a normal type (e.g. Scala) and in my experience, these types are so rare that they're never a problem. I wouldn't fret about it too much and start off with whatever the math tells you is correct. Adding more warnings/errors is always possible later (this is what TypeScript does a lot, first do it correct, and then make it more useful if the same mistakes are made repeatedly).

Basically if you X | void the result is X, which is the same behaviour as TypeScript and generally correct.

The difference is that Luanalysis forbids assignment of "nothing" to "something" which I'd argue is more useful. When we get into set theory (e.g. unions) it disappears though as it should.

This makes me assume that void should be considered a function annotation, in which case, yes, the assignment should not be possible, but a type X | void should not be possible ether (void being a function annotation and not a type).

Feuermurmel commented 1 year ago

On a personal note, the way this plugin treats any and void is the reason I abandoned this project in the end. It just gave me the feeling that these "types" weren't going the be treated consistently and that I won't be able to rely on my intuition, ultimately leading to more mistakes on my part.

I'd rather have a type systems that follows some mathematical reasoning, where I've built a lot of intuition from other languages, and then adds some helpful, additional safeguards on top.