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

Type hints #488

Open
tamasfe opened this issue Nov 15, 2021 · 15 comments
Open

Type hints #488

tamasfe opened this issue Nov 15, 2021 · 15 comments

Comments

@tamasfe
Copy link
Member

tamasfe commented Nov 15, 2021

I'm in the middle of writing the LSP server I mentioned in #268, and in order to provide more useful information (such as field completions), some kind of type system is essential. Right now I'm planning HM-style type inference and external type definitions for modules just like it is in TypeScript's .d.ts files, but allowing users to define types for function signatures and let/const bindings inline would be very useful.

Would it be possible (acceptable) to add optional type hint syntax in Rhai? I am thinking along the lines of Python's type hints, or a much simpler version of TypeScript's type system.

Type hints would only serve the users and static analyzers, and they could be completely stripped when a script is compiled.

I don't have exact fleshed-out proposal for this yet as I will have to experiment more in the LSP project first, but I would like to see something like this supported in Rhai in the future.

@schungx
Copy link
Collaborator

schungx commented Nov 15, 2021

I think something like TS's .d.ts will be great for this purpose. The user simply loads a .d.whatever within the search path (e.g. same folder as the Rhai scripts) which includes signatures for all functions and modules registered into the Engine (possibly with documentation). Possibly definition of external types also.

Beware that any function call is likely to come up with multiple hits due to overloading; unless your server is very smart, it is probably not feasible to determine statically what the types of the arguments are, and so it may hit more than one version of the same function.

Type hints are possible but do you really want to go there? Implementing an entire type system in an LSP is not a trivial task... You'd probably do well enough just by providing standard features like "go to definition" for variables and functions, detection of dead code and/or unused variables, etc.

@tamasfe
Copy link
Member Author

tamasfe commented Nov 15, 2021

I think something like TS's .d.ts will be great for this purpose. The user simply loads a .d.whatever within the search path (e.g. same folder as the Rhai scripts) which includes signatures for all functions and modules registered into the Engine (possibly with documentation). Possibly definition of external types also.

Yes, this is something I'm definitely planning to do, inline type hint proposal would be a follow up based on it.

Beware that any function call is likely to come up with multiple hits due to overloading; unless your server is very smart, it is probably not feasible to determine statically what the types of the arguments are, and so it may hit more than one version of the same function.

I'm aware of this, I'm currently looking into ways to handle it in HM properly. Determining the types of this is quite interesting as well.

Type hints are possible but do you really want to go there? Implementing an entire type system in an LSP is not a trivial task... You'd probably do well enough just by providing standard features like "go to definition" for variables and functions, detection of dead code and/or unused variables, etc.

I would definitely like to have some kind of type system because I'm so used to it as a developer and I hate it when an IDE goes "welp, you have this ... something that come from ... somewhere... good luck!". As for the LSP, I think this is the next major step as implementing function signature help or field completion without types while possible, feels really hackish and fragile.

So my motivation is definitely there, whether I'll have the time and dedication to implement all of this is a good question though.

@schungx
Copy link
Collaborator

schungx commented Nov 15, 2021

My suggestion is to first have an LSP with standard functionalities. Have it work nice and well.

Then start adding new things to it, such as type hints...

Typing for dynamic scripting languages is a highly non-trivial matter, and I think you should avoid chewing off too large a piece. Better nibble at it bite by bite...

@Timmmm
Copy link

Timmmm commented Jan 30, 2022

I think you're going to want type hints eventually anyway, just because it makes programming so much more tractable. Basically every dynamically typed language that gains any kind of popularity has to add them eventually.

You might not want to bother implementing them yet but I think it would be worth sketching out the syntax so you can add them later without issue. E.g. Typescript has some awkward syntax with function types due to the => syntax, and Python has issues with forward declaring types.

Implementing an entire type system in an LSP is not a trivial task

I agree. It should arguably be part of the compiler. You can't really avoid the work somewhere though.

Beware that any function call is likely to come up with multiple hits due to overloading; unless your server is very smart, it is probably not feasible to determine statically what the types of the arguments are, and so it may hit more than one version of the same function.

Typescript has the same problem and it's not really an issue because the type annotations say what type the arguments are.

One final thing - you'll need to support type hints in the file (not just in .d.ts) because a) it's a much better experience and b) you can't annotate stuff in function bodies (local variables) otherwise.

Ps: great project. I've used it in a little prototype tool as a programmable config file and so far it works well!

@Timmmm
Copy link

Timmmm commented Apr 8, 2023

That second link is quite interesting. I don't think I've seen a language with gradual soundness before. Though a 3.7% performance increase in Python is about as bad a result as I could imagine.

I think Dart is probably the most interesting language to take inspiration from when it comes to gradual typing and soundness, since Dart 1 was gradually typed and unsound, and Dart 2 is now fully statically typed and sound. The soundness does actually cause some big inconveniences compared to e.g. Typescript which is not sound.

Those were caused by language choices that they can't change now so if Rhai wants to do sound static typing then it's definitely worth doing as soon as possible.

@schungx
Copy link
Collaborator

schungx commented Apr 8, 2023

Well most of these languages do not assume to work closely with another, compiled language, so their needs are more pronounced as they must be reasonably self-contained.

Rhai can cheat out by moving most typing needs to Rust...

So there is not a definite indication how useful strong typing will be...

@pyranota
Copy link

pyranota commented Oct 4, 2023

I also think that this idea of using type hint in Rhai is very good.
On the one hand, you dont really need it, because of rust types, but on the other hand, Rhai can be used as standalone scripting/modding language for projects written in rust.
For example for game engines. Real-life example, is Godot. You can code with gdscript, c++ etc. You could use gdscript for only dynamic typed scripts and c++ for statically, but would you?
In most scenarios you dont really need the speed of c++ for game logic, but you still need you code to be typed, it makes everything more readable and much more safe. Thats why gdscript implements typing. Thats what i think

@schungx
Copy link
Collaborator

schungx commented Oct 5, 2023

Well, it is actually quite easy to put typing into Rhai... typing removes the possible number of states the program can be in, therefore it is a restriction and thus easy to implement.

The hairy thing is not to restrict too much. For a dynamic language, one would expect to inter-mix different types (for example, returning () for not-found is a good example... the value and () would be two distinct types).

Therefore, for useful typing, you need some form of union typing like TypeScript, and possibly some form of algebra to express the allowed set of types. Thus this typing system, which is distinct from Rhai by the way, is the problematic part.

@pyranota
Copy link

pyranota commented Oct 5, 2023

Therefore, for useful typing, you need some form of union typing like TypeScript

Yeeeahh... Thats the good idea. For example:

fn foo(a: bool) -> () | bool{
   if a {
      return true;
   } else {
      return ();
   }
}

@schungx
Copy link
Collaborator

schungx commented Oct 5, 2023

Or:

fn foo(a: bool) -> bool? {
   if a {
      return true;
   } else {
      return ();
   }
}

@pyranota
Copy link

pyranota commented Oct 5, 2023

As for this keyword, Rhai could use this:

fn foo(this: type1 | type2){
   // todo
}

@Timmmm
Copy link

Timmmm commented Oct 5, 2023

Yeah I think basically copying Typescript is probably the way to go, including any, unknown, as hoc sum types, interfaces, generics, etc. You can probably stop short of some of the more advanced features.

The only thing I would really change from Typescript is that discriminated unions are very tedious to define, but fixing that properly requires it to be an actual language feature rather than just type annotations.

@cactusdualcore
Copy link

The TS type system is an absolute mess and turing complete.

microsoft/TypeScript#14833

And I absolutely despise Typescript's as operator, because type casting in dynamic languages is horrible.

function always_returns_number(): number | string {
  return 2;
}

// Cast is necessary, because it's a `string | number`.
// It requires more typing and is still no better than JS because you can always do `as unknown as T`.
// And the worst part for me, it's hard to see in a noisy code environment.
const x: number = always_returns_number() as number;

Most types for Rust interfaces should be known from the .d.* files and a lot of bottom-up interference might be able to help with types from there.

I prefer an approach similar to the below, because it's much less difficult to miss the reason why x is a number when
parsing this code visually.

fn always_returns_number(): number | string {
  return 2;
}

let x = always_returns_number();
if typeof(x) == "number" {
  // x must be a number
}

There probably is an even better approach.

@Timmmm
Copy link

Timmmm commented Dec 2, 2024

turing complete

It's a very common misconception but Turing completeness is irrelevant in most contexts, including this one. There's no real benefit to making a type system not Turing complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants