WASM is a vm and JS is a language.
Are you asking about the pro and con of a transpiler over a vm solution?
For one thing BEAM is the Erlang VM which Elixir and many other languages run on. Why would anybody want to implement it in Javascript? BEAM does a lot of crazy things, including a preemptive scheduler. Also BEAM GC isn't some generic GC, there's tons of crazy work going on with it to work with the Actor concurrency model.
I'm talking about compiling to WASM vs. compiling to JS. Calling something a VM or a language is mostly a matter of convention. x86 machine code can also be viewed as a language, and compilers certainly don't care. A compiler translates code in one language into code in another (there is no precise definition for "transpile", which is why compiler people hate that word; colloquially "transpile" just means that the compilation's target language is considered to be at a similar abstraction level to the source).
> Why would anybody want to implement it in Javascript?
Why would anybody want to implement it in WASM? If it's so that it could run in the browser, then compiling BEAM bytecode to JS would achieve the same goal, be easier to write and would likely result in a smaller and faster deliverable.
> Also BEAM GC isn't some generic GC, there's tons of crazy work going on with it to work with the Actor concurrency model.
There's nothing crazy going on in BEAM. As far as VMs go, it's near the bottom in terms of sophistication.
BEAM GC isn't very crazy, it (like a lot of other parts of BEAM) is delightfully simple. Each process gets it's own stack and heap, and because if immutability, any references are always to older things, so BEAM GC is based on copy collectors: copy everything that's reachable from the current stack frame, then copy and adjust the references; when you're done throw away the old heap/stack. It's a smidge more complicated because BEAM uses two generations for the collection, and has reference counted binaries that are shared among all processes and require cleanup; but all of that is pretty straightforward too. Because of the language design, there's no need for advanced GC like in other languages; but if you transpiled to other languages, their GC would work fine.
If you want to push towards transcode, that's probably the most likely alternative. It's shorter too.
The GC aspect is a real issue though.
For example, suppose we want to make an implementation of elixir that runs on the browser. The most straightforward way to do this in a semantics-preserving way is to implement a BEAM virtual machine in Javascript, and use it to run untyped BEAM bytecode. The problem is that if we do this the Javascript JIT is only able to see see the control flow and the variables for the BEAM interpreter loop itself, and it isn't able to to peek into the "meta level" to reason about the control flow and the variables of the elixir program. As a result, we should expect that the Javascript JIT will be no faster than an AOT-compiled bytecode interpreter. In order to produce good code in this case we would need to use a JIT engine that understands and expects the code pattern of an interpreter loop, such as the one used in the PyPy JIT for Python.
The other approach you could try to take would be to compile compile the Elixir code more directly into Javascript, so that Elixir functions becomes Javascript functions, Elixir variables become Javascript variables, Elixir loops become Javascript loops, and Elixir objects become Javascript objects. This way, the Javascript JIT will have a much better chance of generating good code. But the problem you run into now is how to do this compilation while preserving the semantics of the original language. If the original language isn't similar to Javascript then preforming such a translation can be quite challenging, and the end result might be some weird-looking Javascript that won't necessarily run very fast either.
The caveat is that this reasoning is only thinking in terms of raw speed of executing the bytecodes. Both the WASM and JS versions will still run the elixir bytecodes in "dynamically typed speed", as the JS JIT is not able to optimize at the "elixir level". Things get a bit more complicated if you add the garbage collector to the mix though because the WASM version can't reuse the highly-efficient Javascript GC.
One major factor is the special semantics of execution in Erlang VM compared to most other languages. Of note are:
* Guaranteed tail calls. There is no other way of doing a loop in Erlang other than recursing, so this point is really important. Although I can certainly imagine making this work with a JS target, we would have to perform certain code transformations that would most likely make the JS JIT a lot less effective.
* Preemption of Processes. Processes are the name of the lightweight actors used in the Erlang execution model. Each process is only allowed to run for a certain timeslice before execution has to be preempted. Although, again, I could imagine implementing this in a similar way as I would implement guaranteed tail calls, this would fully break up the control flow of a function, further disadvantaging the JS JIT.
* The semantics of the language term system. While this is probably one of the smallest issues for a JS target, it would still require us to implement and frequently call into relatively complex term operations that are implemented on top of the JS object model. I can't imagine this interacting well with the above two points in terms of performance.
* Concurrency/message passing model. One of our goals is exploring writing more fully featured applications in Erlang and then running it in the browser. As such, having the same concurrency model as the BEAM is critical. Using webworkers and utilizing shared memory through SharedMemoryBuffer (reenabled in chrome, let's hope firefox gets somewhere with this soon too), we believe makes it possible to implement things in a way that behaves very similar to the BEAM.
We want to invest heavily in the compiler in order to generate the best possible WASM code, which, given we play our cards right in codegen, should be further optimizable by the WASM engine. This involves inferring types and generating specialized code. Since most of Erlangs BIFs (built in functions) accept a much narrower array of types than what a lot of other dynamic languages do, we can draw relatively good type information from that.
This above is all an answer specific to the WASM/JS question, but doesn't even touch on the further goals of the project. While WASM is our primary focus at the moment, we also support native targets.
As for compilation to JS, ElixirScript (https://github.com/elixirscript/elixirscript) is prior art here. I am not involved in this project, so I can't really answer to what design decisions they made, but it is interesting to compare with regardless.
Again, time will obviously tell how well all of this will work out, but from what we have seen thus far, our approach is fairly solid.
Native targets are another matter, of course.
Anyway, it would be interesting to see results. Beating BEAM's performance is not hard.
Thank you for your answer.
Interesting observation. The hype I've heard around Elixir and Phoenix makes it sound like BEAM's performance is amazing. But maybe that's only in comparison to Ruby (specifically the C Ruby interpreter).
The JIT enhancement and the new language features which shipped in various JS engines in last 3 years is commendable.