rhaiscript / rhai

Rhai - An embedded scripting language for Rust.
https://crates.io/crates/rhai
Apache License 2.0
3.87k stars 181 forks source link

Type hints #488

Open tamasfe opened 3 years ago

tamasfe commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

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!

erlend-sh commented 1 year ago

I am thinking along the lines of Python's type hints

On that note:

Timmmm commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

Or:

fn foo(a: bool) -> bool? {
   if a {
      return true;
   } else {
      return ();
   }
}
pyranota commented 1 year ago

As for this keyword, Rhai could use this:

fn foo(this: type1 | type2){
   // todo
}
Timmmm commented 1 year ago

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 commented 10 hours ago

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

https://github.com/microsoft/TypeScript/issues/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 commented 10 hours ago

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.