-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
TypeScript compiler in Rust #5432
Comments
I'm not sure how feasible to integrate with deno, but tsc have the incremental flag to speedup re runs, or spin up language server and keep it alive |
Thanks @dsherret. Yes it might speed up big projects where there are incremental changes. Once we get to the point where we are only type checking in the compiler (I guess it really becomes the "type checker" at that point) we would want to see if incremental compiles would help. |
I think there's a possibility that perf gains here would be substantial, for two reasons. First, tsc is a compiler geared towards interactive IDE/LSP server use-case. My experience as an author of such IDE tooling tells me that this has non-trivial perf implications (pervasive lazyness, lossless-ish syntax trees, and the tax of additional complexity). By shifting focus to batch compilation, it should be possible to simplify and speed-up things on the architecture level. Second, while type-checking can't really benefit from something like simd, there's still a lot that can be gained by using a language with full control over data layout. Type checking is mostly pointer chasing and cache invalidation, and having tightly packed arena-allocated data structures helps a lot. HashMap performance is also super important. Sorbet, the typechecker for Ruby, was written in C++ for these reasons (some info 1, 2). The fact that type-checking could be made faster at significant dev cost, of course doesn't necessary mean that it should. |
@matklad thanks for the feedback and thoughts. Your second point is really interesting. There might be some data structures, or operations that are simply not performant. Because of the legacy of the compiler, evolving over a language that has moved a lot, and not specifically optimised for the engine it primarily runs in (v8), there could actually be a lot of ground to be gained in making sure the performance of the data structures is optimised. TypeScript core team, for lots of valid reasons, is never going to move off of building the compiler in TypeScript. Therefore my biggest fear is the while getting 100% syntax parity is "easy" getting 100% type checking parity would be hard or impossible. That is why I am hopeful that the long road to get there means we may never get to the final destination. @95th that is effectively what we are talking about here. |
esbuild can be used for transpiling typescript without type checking. |
@znck esbuild is built in Go and is not designed to be embeddable... It offers no advantages over swc which is built in Rust and which we have already integrated in Deno. |
Go is an implementation detail and shouldn't matter. Quoting @evanw, esbuild's author:
The real issue IMHO is that esbuild doesn't (currently) support decorators - so that's a show stopper. I also recently learned about sucrase, which seems substantially faster than |
@elektronik2k5 it matters if you are thinking of leveraging it. Why port something to Rust when we already have something that does exactly the same thing, written in Rust? That just doesn't make sense. Plus esbuild's focus is on being a bundler, Deno's need is for single file asynchronous transforms. Sucrase's what it is not would likely not be suitable based on its own statements: https://github.com/alangpierce/sucrase#what-sucrase-is-not. In particular it does not transform decorators, saying it needs to be supported by your runtime, but there are no runtimes that support decorators. It also says itself that it isn't intended for production use. swc's metric comparison there is sort of obscured, because of the way swc is being used. We have yet to really baseline what swc could do. swc is significantly faster when it is targeting more modern versions of JavaScript then when it is targetting older version. The benchmark's on Sucrase's website don't really indicate what the configuration of swc was, nor what the inputs were. Coupled with the fact that all those stats are some obfuscated by using the Node.js APIs to orchestrate everything. Because we have already indicated swc, and aren't going to get rid of it any time soon, and we don't have any strong indication that it is going to be a performance barrier, it doesn't make sense to pursue Sucrase. Also, all of these would only solve bullet point two, which is important but clearly not the whole picture. None of these solutions do type checking. They all just do type stripping and transforms. The bigger fish to fry is always how to improve the speed of the type checking. |
As I mention in this issue #6173 why not to give the possibility to the user to decide for their transpiler? Of course having by default a super efficient TS transpiler build-in inside Deno would be a huge advantage and would maybe be the reason of people switching from Node to Deno. But giving the possibility to give the choice to user to decide for their transpiler is an easy move and would also open the door to other programming language like dart, coffescript...
|
Rewriting typescript in rust can truly improve its speed and memory consumption but (a big one) separating deno typescript from main typescript development line can cause many problems:
All of those, at the end, cause deno to use an outdated and much bug-proof version of TypeScript, and I think it does not worth unless TypeScript maintainers decide to port entire language to Rust or any low level language. But (another big one)We can use tools like AssemblyScript,or any other similar tools to Compile/Convert latest versions of TypeScript's compiler with some little modifications to WebAssembly version and use its higher performance version. Yes, I know, AssemblyScript is so young and ..., but we can put/redirect any required affords from TypeScript->Rust rewrite process to help these projects. At last, yes Rust conversions are good if they don't have costs and problems, like V8 engine in rust, windows kernel in rust, Linux complete rewrite in rust, but are they possible for any projects that use these libraries? (I think it's not possible even for their main maintainer, unless use a progressive work to convert like what Mozilla team did in Firefox) |
AssemblyScript (and Web Assembly) have come up a number times. Let's make it clear, it is not suitable. There are fundamental differences between how JavaScript and WebAssembly work. JavaScript is a garbage collected language, WebAssembly is a memory allocated language, like Rust and C++ is, where the code itself manages the language. AssemblyScript makes it clear on their GitHub page: "Definitely not a TypeScript to WebAssembly compiler". It is similar to TypeScript, which makes it easier for those familiar with TypeScript to write Web Assembly, but it is not a general purpose TypeScript/JavaScript to WebAssembly compiler, which is what would be needed. Why isn't there a general purpose JavaScript to WebAssembly compiler? Because that is effectively v8 or any other JavaScript engine, which provides all the other infrastructure needed, like a highly tuned garbage collector, to be able to deal with general purpose JavaScript. Even if there was a general purpose JavaScript/TypeScript to WebAssembly compiler, it wouldn't speed up the code, for two reasons. First, Web Assembly is an abstraction on top of another language. In the case of v8, C++. C++ is abstracting WebAssembly from the underlying CPU, still morphing the op codes to something the native architecture of the CPU understands. The second is that v8 is really really really really good at optimising JavaScript, a lot better than some other compiler could be, and types don't significantly help with runtime performance of JavaScript (Google experimented with this a while ago, it effectively didn't work). AssemblyScript is great, if you are a TypeScript developer, and you need to move a hot path out of JavaScript/TypeScript to WebAssembly. It is a lot lower overhead for you then having to learn Rust or C++. Which is an awesome thing. AssemblyScript is a really cool project. So if we accept that converting the TypeScript compiler to WebAssembly is impossible and wouldn't work, why not move some of the hot paths AssemblyScript/WebAssembly? In Deno, we simply wouldn't do that. Why? Because as state before, WebAssembly is still an abstraction on top of another language, it is "cheap" and "easy" for us to right things in Rust, and native compiled Rust will always run faster than Rust to WebAssembly. Working around the limitations of AssemblyScript, for Deno, wouldn't make it any easier. The wider community shouldn't worry, people are talking to each other. Since Deno was started there has been communication between the Deno team and the TypeScript core team. Like all groups of people, we don't always see eye-to-eye, that doesn't mean we can't have productive conversations about how to improve the things for everyone. |
First of all, I know and said that everything in Rust is Fast (compair to JIT langs, but not even every nodeJS task!, suggest to watch Ashley Williams - How I Convinced the World's Largest Package Manager to Use Rust, and So Can You! from npm team) and Secure (memory-wise). It's a fact, an obvious one. I see deno, as alternative to nodejs mess in future, but like every other thing in this world it has its own limited budget, eager followers, volunteer maintainers, ... . if deno maintainers spend big chunk of these resources where are not much important (compare to others), denos fate will be like WeWork and other prodigal ideas, platforms and services. One of TypeScript compiler problems is it's boot time and JIT speed, we can (with some modification, maybe automatic ones) convert TypeScript compiler source code to make it compatible with tools like AssemblyScript then compile it to WASM or even find some tricks to use LLVM and generate native binaries for top used platforms (like Windows, Linux, MacOS, ...) to speed this part up and use main typescript compiler on other not supported platforms. This conversion is fast (maybe some weeks to setup tools, develop solutions and CI/CD tasks and ...) and already available. It costs very low. Even if we work together and reach our Ideal purpose (some day, full functional TypeScript Compiler in Rust), then we can switch to that better solution, till then we have our good temporary solution to TypeScript compiler's problem. |
@HKhademian you are not understanding...
I am glad you know what is and isn't important for us.
This cannot be done, and even if it could it would run slower then it currently does.
Great, I await your proof of concept. |
Regardless of the WASM thing, I think @HKhademian has a point about the maintainability of such a solution, which is why I've watched this issue for some time somewhat worried about this Rust TS compiler plan. @kitsonk is the plan to rely on pulling in TS' automated test suites to ease tracking advancements in TS, or is there some other strategy long term? |
Apologies for the double post but I do want to also say that provided the TS specification is sufficiently detailed, it ultimately isn't inconceivable that multiple implementations of the language can be handled-- after all, it is commonly a sign of language maturity (and an insurance policy) when there are multiple distinct compiler implementations. But it will be a large effort. I do think it will be necessary to decouple this effort from Deno so you can get contributors who just want a faster TS compiler without losing some decent level of type checking. |
Which I also extensively made as the last bullet point of my original post, and might even be unnecessary.
If we end up at that point, ensuring the language doesn't become fractured would be a goal. |
I didn't decide anything for you, I said deno can work without faster Typescript compiler. so it has less importance than fixing some bugs, issues and even add new features. I love deno idea and it's existence, I love to help it and anything I said was in direction of my thoughts for its good. |
If type checking was ported to Rust then what's the point of keeping it in sync with TypeScript? I would understand it, if there was a huge ecosystem (like npm) that needs to be supported, then it would be valuable to be compatible with all of that old TypeScript code, but that's not the case. If Deno had it's own type checker, I wouldn't care if it supports code already written in TypeScript, because most of that code is written for Node anyways. It could reuse existing TypeScript syntax (or even Flow, Hegel or a custom one), but it wouldn't be a huge problem if it worked differently than one of the existing type checkers. Even the current approach of using slightly modified tsc already isn't compatible and needs editor plugins to work properly. Also, having a custom type checker would be a great opportunity to create something that is more strict and correct by default. TypeScript has had a lot o strict options added and had to make a lot of compromises for backward compatibility, so it's not that great at all. |
No, there already is a lot of Typescript code out there and I think a lot of us want to be able to use it both on Node and Deno. If Deno were to become (even more) incompatible to tsc, I would opt to simply compile/transform my TS code to Deno-compatible JavaScript and ship that for use with deno. Honestly, it already looks like the better path given the performance and compatibility headaches that already exist. |
Compatibility with old ecosystems is the biggest reason TypeScript is popular. And the are a lot of code. Most of which doesn't even care if it's written for node or browser. |
@HKhademian Deno's compile time is not a problem if you are talking about the production environment where most of the time we don't care about the start-up time. But, when it comes to development, it is a major developer experience killer. I am working on an HTML5 game and was using Deno at the beginning because Deno has It costs 20s to compile while my game was just several thousands lines of code. The speed of iteration was killed by this slow compile time. I switched to Node and setup a I am not sure if Deno has incremental compilation enabled for TSC or if that is even possible for Deno. However, I believe if even Deno team doesn't implement TSC in Rust, there are places where TSC can be faster in JS implementations or Deno's internal pipeline. |
@CreatCodeBuild completely agree with you. If developer experience is not good, people will not use Deno. Right now, Deno is still in a very early stage and I would not recommand it to anybody in a professional project. For the moment, we still have to wait till we get reasonable performance and flexibility. But at least, we could help to build the eco-system by creating new modules, compatible with Deno. In another thread, we point out that one of the main problem of TSC is the types checking of the code. Would be great if we could just deactivate this type check when we are in dev mode. Also another point that you mention, is that in node many people use babel because it is much much faster than TSC. So would be really great to give the possibility to decide for our own transpiler. Of course by default, it would be TSC but if someone want to tune it, he could just mention to deno to use something else (for example to use babel). And doing this, would also open lot of room to developers, we could then use a different module loader than the native one from v8 and therefor use nodejs module. |
Maybe it's a silly idea but could we just convert TS code to JS by removing all type-chekings and type-system and see the bare ts code as JS one (which it is), feed it to V8 and skip typescript compiler? We could use This idea (maybe it's silly) came from where I saw deno can run JS like TS codes, if we don't have any problems with JS code, so by fast convert (not compile or analyse) it to JS, we can use JS's benefits. |
@HKhademian this has been discussed before. See #5436 |
@HKhademian to add to @dsherret is specifically discussed as a step (second one!) in the approach in the original post that we are working towards. 😕 |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Can't Deno fire off two threads, one running SWC stripping types and emitting into a temporary file, the other thread running TSC --no-emit for performance, then, depending on the result of TSC, running the emit file from SWC, or returning with a non-zero exit status? Would that even help? |
That's also how https://github.com/rsms/estrella works (except it's using esbuild instead of SWC). |
In that case, if it's viable, based on that benchmark,
we could cut out the 15% and the 24%, reducing the overall time by roughly 40%. |
Yes. |
@kitsonk In that case, what does "bind" refer to from that benchmark? |
Is this good new for JavaScript/TypeScript?
Read more: https://rome.tools/blog/2021/09/21/rome-will-be-rewritten-in-rust |
@Maxvien the core teams between both projects are in communication. There are certainly things that we can collaborate on that are mutual interest. Having more JavaScript/TypeScript projects focused on Rust certainly will help things. One thing we both agreed on is that we are a ways away from full type checking TypeScript in Rust, though Rome has some interesting ideas at ways of chipping away at the problem, as well as we are keeping an eye on what is happening with swc's work on type checking in Rust as well. |
Looks like the SWC doesn't have any intention of releasing their type-checker under an OSS license (and development has stalled on it), instead intending to monetize the software under a proprietary license to fund it's development. swc-project/swc#571 (comment) So far SWC -> TSC AST conversion seems to be the avenue that would be fastest to gets some speedups. Is there a separate tracking issue for looking into that and/or has someone already done some evaluation of the work required for that. |
In the context of the discussion: I’m porting tsc to Go - DongYoon Kang |
@kitsonk Sorry if I lag behind the discussion or bring up something uninformed, but just to ask; isn't the long-term (v2) aim for Deno transpiling the deno_ast to rust ast and skipping v8? Making typescript generate rust and therefore effectively making the deno runtime a typescript abstraction over rust would simplify the architecture no? Probably a ludicrous idea. For one thing that, in this hypothetical situation, you would have to drop support for js. But are there really many people who started with deno from a js angle instead of ts? |
@GerbenRampaart We are not dropping JS support (who said that?). Secondly it is not possible to "compile" TS to Rust. TS does not require sound static typing, while Rust does. The type systems and allocation primitives of Rust and JS are too different to transpile one to the other. User code will continue to be executed in V8 for the foreseeable future. |
@lucacasonato Thanks for the answer, did not realise the type systems were that different. No one said deno is dropping js support, I implied it would be a consequence of a ts => rust transpiler, that's all. (edit: I re-read my original spit-ball and see how you could read that the deno team said js support would be dropped, so I made it clearer, thanks.) |
stc, a WIP typechecker for TS in Rust, was just open-sourced. It's by the author of swc. |
There is another TS compiler in Rust, based on the OXC project. https://github.com/kaleidawave/ezno Please... don't start a third one. These projects are great, utilize one from these, contribute there, because we do better with 1 fully compliant implementation than 3 partially compliant ones. |
FYI: This thread is a bit older. There are no plans on our side here at Deno to write a TypeScript type checker at the moment. |
Won't implement. Too hard. |
Just to follow-up, in Deno v1.0 type checking was indeed a bottleneck, but that's no longer the case. There have been significant changes to Deno's TypeScript support that reduces the need for a separate TypeScript implementation in a more performant language like Rust.
Overall, in my opinion, it's best to align as much as possible with the official implementation of TypeScript. Creating a new implementation is a full time job for a large team of developers. |
This is long, but please read before you comment. If you are interested in following this, use the subscribe button to the right instead of just adding a comment.
Since the Deno v1 announcement there has been a lot of community interest in the TypeScript compiler in Rust that was mentioned. This issue is intended to provide information on that. First a few points:
tsc
to maximise performance.The Problem
We need a bit of context of what problem we are really trying to solve here. Type checking, especially with a structural type system built on top of a weakly typed underlying language, is expensive. As TypeScript as a language has grown, and the type checking become more and more advanced and safe, means that it is increasingly "expensive" to do this type of checking. Deno treats TypeScript as a first class language. At the moment Deno also always runs TypeScript through the TypeScript compiler and treats any type errors as it would treat a JavaScript runtime error.
This is "expensive". If you look at Deno benchmarks you will see that there is currently a cost of about 10x just spinning up the compiler to get some TypeScript transpiled into JavaScript.
We have tried a few things to improve that, but there is still a big cost, and we want to try to narrow that gap significantly. For other things in Deno we have had really good experience in moving to Rust things we used to do in JavaScript. There are several reasons for the performance improvement, but the most compelling is that it is just plain easier to get performant code in Rust. If you resort to non-obvious structures in JavaScript, you can often get lots of performance from v8, but it is really really really hard work. Our text encoder is an example. We started with a spec complaint implementation, following the structures laid out in the IDL. It was abysmal performance. We then implemented a fairly "magical" non-obvious implementation in JavaScript/TypeScript which dramatically increased performance, but that wasn't even enough, we got eve more performance moving that to Rust, with even the overhead of "copying" that data in and out of the JS sandbox is faster than what we could get out of heavily optimised JavaScript. Could we have gotten more out of our JS implementation... maybe... but it just gets too hard.
How does it work today?
I realise a lot of folks might not understand well where we are at today, so some background context is helpful. It sort of works like this...
How the CLI is put together
typescript.js
(tsc
if you will) and our infrastructure code to manage it."Running" TypeScript code
deno run
), the Deno CLI will take the module passed and see if it is in the Deno cache, or if it needs to be fetched. If it is in the cache and it is a local module, it checks if the source has been modified or the--reload
flag is true, and if it is remote, just if the--reload
flag is true. If the transpiled version of the modules is in the cache and "valid" Deno will just load it (see below).ts.preProcessFile()
plus other logic to determine the dependencies of the root file. As those dependencies are identified, the compiler "ops" to Rust to resolve and fetch those modules.X-TypeScript-Types
header to identify those files. The fetching logic will resolve all of that before returning the type definition to the compiler instead of the JavaScript file..d.ts
files for JavaScript files based where appropriate.Loading JavaScript into the isolate
The Approach
Ok, so what do we do about it. The discussion I have had on the subject with various people have always been about an evolutionary approach. Also this approach is what I personally feel is right for Deno. Solving everyone's problems is tough, but if solving Deno's problems helps out everyone else 🥳.
Here are the major items that I think we need to tackle, and likely in the order presented here:
ts.preProcessFile()
has been super useful, there are a few bugs with it that have been long outstanding, and with our added logic of supporting type substitutions it is just better/easier/more testable to have all of that in Rust. It will speed up things little bit from not having to "op" back and forth sending individual files.This is discussed in Slow TypeScript compile time #4323 but likely needs its own issue to deliver the feature (which I will take care of).This is covered in Not Type Checking TypeScript #5436. Some of the conversations I have had with people is that making this the default might actually make more sense. Something we should discuss. Not type checking TypeScript would certainly speed things up, and especially when you are developing, and saving type checking for those special occasions, like a pre-commit hook.deno fmt
(along with the excellent dprint built on top of it, which is how we became aware of swc in the first place) anddeno doc
. SWC, like most ECMAScript parsers, produces a estree like AST structure. The TypeScript AST is significantly different than that... it is far more aligned to a Roslyn AST. It would be naïve of me (or anyone) to think that it would be trivial to transform swc's AST to a TypeScript one, but it maybe worth the effort, or at least exploring a simple transform to see if we can generate an AST (TS SourceFile) that TypeScript can consume. If this did work, we could use the TypeScript compiler to just do type checking. (It was pointed out that a reference implementation of estree like AST to TS AST exists in typescript-eslint)The text was updated successfully, but these errors were encountered: