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.