Crystal -> JS Transpiler

There are a couple of issues around “Crystal for Javascript”, and some background chatter, but I am hoping to get some central discussion and momentum going around this topic.

Justification

Having a single language which is capable of writing code for backend, frontend, and mobile can simplify development, improve communication between user advocates and the developer, improve internal team cohesion, make on-boarding new developers easier, and improve long term maintenance costs. I know because we do this at my company using Ruby.

To show the power of this approach you might want to watch this short video: https://www.youtube.com/watch?v=GEe7hHIhyUs. The essential idea is that by sharing classes (with possibly different internal implementations) between client and server the client can access server objects transparently, essentially allowing the program to develop code as if on a single platform.

Currently the possible practical choices for a single isomorphic language are (to my knowledge) Ruby, Kotlin, Javascript, and perhaps Clojure.

Each of these choices has negatives, which Crystal, if it could also run in the browser would I believe solve.

Ruby is the closest to perfect, but because of Ruby’s runtime complexity the resulting code size when transpiled to JS is large, and the code runs slower than the JS equivalent. Even with this disadvantage it’s a great solution, and we use full stack ruby (meaning integrated code is running on client and server) on 3 fairly complex applications.

Kotlin is by design intent isomorphic so out of the box it does work well running everywhere, but the language remains verbose. The equivalent full stack application in Kotlin is about 6X the number of lines of code than the Ruby equivalent (using the https://hyperstack.org framework.) Also due to Kotlin’s lack of any real meta-programming capability, it can’t put together decent DSLs which are really needed to reduce the programmer workload.

Javascript (using Node.js on the backend) works, but is worse than Kotlin in terms of verbosity, and doesn’t offer Kotlin’s advantage of any kind of type checking. The only way JS is practical as a single full stack language is to add some kind of templating language to it like JSX. Even with JSX the equivalent JS/JSX full stack application is about 4X larger than the Ruby using Rails and the Hyperstack gem.

Clojure, is the least known by myself personally, but have been told by those using it, that it works great as a full stack isomorphic language. However I really don’t see the LISP syntax catching on widely anytime soon.

So what if we took Crystal (or realistically a rich subset) and made a JS transpiler like Opal provides for Ruby?

it would have all the pros demonstrated by Ruby, plus it would have a much smaller faster foot print in the Browser. The added (really a huge) benefit is that the Crystal Macro approach to meta programming / DSL creating, enables even cleaner DSLs to be created.

How to Do It?

Hopefully the justification will get some people interested in moving this conversation forward. The question then becomes how?

I would propose first that the potential power of Crystal-Everywhere ™ is so great, that it is worth at least initially developing a quick, possibly dirty, first implementation. It may not be as fast or as compact as a final solution, but if it gets people interested, there will be additional contributors to help speed things up.

With that in mind lets consider the options:

LLVM -> Emscripten: Long term this might be best, but its hard due to GC issues, and getting the end code to interoperate with JS.

LLVM -> WASM: Long term this might even be better, but in addition to the problems with the Emscripten route, it also has the problems associated with WASM (as in its not really ready for prime time on a number of fronts.)

AST (out of parser) -> javascript: This is the route (I believe) taken by the Crow project and it’s basically the same route taken by the Opal team. However I think its as hard (but for different reasons) than the above approaches. Possibly could be wrong on this, as I have not studied it in detail, but I am basing it on the fact that features like macros, are not implemented.

AST (before code gen) -> javascript. I have only spent a short time poking at the code, but my understanding (please correct me if wrong) is that semantic analysis, and macro expansion produces a final AST that will be fed to LLVM code gen. My proposal is that at this point you generate fairly clean JS code, that would be much more compact and performant than the Ruby/Opal transpiled code. Not as fast as WASM perhaps, but probably not much slower than the same code handwritten in JS.

So the final approach would be the one I would propose taking. Here are the details of how I would go about this.

Collapse Types that have different machine reps, but are semantically the same

In a JS world Ints are all the same, and while strong typing can still work, the resulting code will be the same regardless. Apply the same process to any other types like this that are simply different because of the machine representation.

Compile time errors for unimplementable features

With this approach certain features especially around low level machine access are not going to be possible in JS. Even without these features you still have a beautiful language. And because of macro expansions its easy to control server vs. client side use of these features in isomorphic code.

Map Crystal Objects to JS Objects

With the above caveats (plus perhaps a few others I have missed) a Crystal Object can be directly implemented as a JS Object.

Generate the Code!

I believe with the above rules about how data looks, code gen is pretty straight forward. There might be a few things to work around with closures, and parameter passing, but there are pretty code models that we can lift from the way Ruby/Opal handles this.

Your Turn!

I am of course over simplifying, but I just want to get some discussion and interest going on this.

10 Likes

Personally I’ve been thinking about this a lot lately and I love how you were able to put a lot of my thoughts into words. Here is my take on it:

Currently the Crystal compiler does one thing, it takes Crystal code, generates an AST from it, and converts it into machine code via LLVM IR. This is the same approach being taken by hundreds of languages, but I feel like Crystal is missing one thing. Many other languages also separate the compiler from the parser, allowing multiple compilers to be implemented. Nim, for example, can compile to C, Javascript, WASM, and more.

Ideally the same should be done with Crystal.

1 Like

I’m not sure its fundamental that Crystal only has one backend. I suspects its simply that ATM there IS only one backend. If that makes any sense? The fact that it generates a well defined AST, means plugging in a different code gen should be easy. I’m hoping from input from the core team here…

The backend could be gcc or generating C code and then compiling that. It’s just a matter of replacing codegen/... with a different implementation (or making it possible to support multiple backends). The language isn’t necessarily tied to LLVM. It’s just that we chose LLVM because it’s easy to work with and it’s the popular choice right now, one that gets improvements all the time.

1 Like

Is there an issue for this yet that you know of? Allowing support for multiple backends.

Imagine writing Crystal code and it generating pure C code, I would probably understand C better than trying to learn C by itself, LOL

But I would never do it, since I believe there is a global wide plague in the programming industry right now in terms of learning new languages. Stick with what you know and build something amazing. Programmers waste way too much time trying to be hip and learn new languages just because.

Also, this would be an absolute monstrosity to maintain and there are not enough lead devs in Crystal to work on this. IMHO, their time would be better spent with core Crystal related issues.

If/when Crystal becomes bigger, this might be an option later down the road. Since it could be maintained by more contributors.

This is the first time catmando has posted — let’s welcome them to our community!

Welcome!!

No. The reason is that I can’t imagine anyone wanting to do it. But if somebody wants to do it, go ahead (just note that the core team is working on changing the representation of unions to prepare them for multithreading, so porting that while also keeping track of current changes will be a huge pain). My advice is to wait until 1.0 for tackling these things.

1 Like

I’m not talking about making a new language, or complicating things. Just trying to get crystal to run in the browser.

This eliminates the need to learn multiple languages. Right now building a website is complicated largely because you need to learn JS, JSX, (or some other template language client side) Ruby (the most popular currently), ERB (or HAML or whatever) plus CSS.

The value proposition of Crystal is hugely increased if you could say “it works everywhere”. Otherwise its the same mess as above, just substituting Crystal for Ruby.

So I agree we need fewer, BUT better languages. Ruby is almost there, but Crystal is better.

As far as support goes, there are others (including myself) who would jump on to build and maintain the JS code gen. I’m just trying to get to a point of direction that everybody feels is worth supporting.

Building a website is really simple tbh. Frameworks and template languages are what makes it confusing and a convoluted mess. Not only that, but they add an immense amount of bloat. You just need a some HTML, JavaScript and CSS skills. If doing backend development, use nginx/kemal, etc.

I agree about your proposition of Crystal and wish it could be used everywhere. But that’s in a perfect world :innocent:

Right, why don’t all people just use English? The PL situation is different though, because most of them are designed with various goals in mind, making it unwise to wish for one tool for all the jobs. Having said that once we have WASM in widespread adoption there would probably be an utility in this backend.

we will have to disagree on that one :slight_smile:

Maybe… or maybe we strongly agree.

My business runs on on a series of cloud applications, that facilitate customer ordering, manage customer service, image processing, storage and retrieval, the factory floor, plus electron apps that control various machines.

I think I would have to have a lot bigger team if we had to maintain all that in vanilla JS on the front end.

But I agree current state of the world is a mess (as I described previously) hence we use a simple single framework everywhere for everything, written in Ruby, and using Rails on the backend.

I just think this solution would be greatly enhanced if I could repeat it in Crystal :slight_smile:

1 Like

I guess that is my point. I don’t want to wait for WASM, so that is why Crystal -> JS transpiler (like Ruby uses) seems the right solution now.

There is no need for separate languages between the front-end and back-end. We’ve proven Ruby works great as a unified language, but I think Crystal can take this farther, hence my interest.

I really recommend you try Elm. It’s perfect for the browser. It’s type-safe, it’s incredibly small, and it’s great for starting with a small app and evolving it into a big, complex one. The compiler helps you a lot.

Crystal -> JS is just one part of the problem. Then you need to bind to the DOM and all of the browser API.

Crystal is the wrong language for the front-end in my opinion. However much you would save on learning one more syntax you would lose tenfold fighting the semantics.

I will support / star your repo if you decide to go down this path, but I think there is not enough contributors yet to support this, and you will end up having to maintain everything and it will eat into your time. I don’t want to see that happen to a fellow Crystal dev. Time is gold.

1 Like

I hope you don’t mind the discussion… Why do you say that?

Maybe I am missing something here, but we are actively using Ruby for our Front end development. We have probably 100K SLOC of Ruby code on the FE, and that is just my company. Lots of other folks doing the same. Code is far easy to write, read and maintain than JS.

Crystal would give us all that, plus strong typing, smaller asset size, and faster. Plus the macro facility would make the DSL’s even nicer to use.

So i’m so interested to know why you wouldn’t want to do this :question:

2 Likes

This is why I would propose to use the same route that Opal did. By keeping things high level you get the bindings almost for free.

1 Like

Thanks so much for that reply!

1 Like

I’m guessing you look at this from “pragmatic” perspective, while I see a, well, ideological picture if you will. Crystal is build around a set of concepts that help with some things, but drink your blood otherwise. I mean it is turing-complete so you are able to do what you want, however given the wide range of available choices there are languanges that imo work better. Any languange makes you mind work in patterns that, again, help or hurt depending on how much its semantics reflect your problem. Front-end and back-end typically solve completely different problems, so they require different tools so that your mind works efficiently, so that the language makes it easy and fun solving your problem, not making you fight itself line by line.

The syntax is not the problem. Front and back developers think in different ways, so they need different tools. There are full-stack people, they are expensive and they still do not care much that they need to use different tools for different contexts.

I think you are solving a non-problem, trying to optimize the number of languages, but hey, it’s your business not mine.

I think after semantic analysis is done you could write a separate Codegen pass that generates JS code. You can start small (supporting some nodes) and slowly grow it.

1 Like