gluon-lang / gluon

A static, type inferred and embeddable language written in Rust.
https://gluon-lang.org
MIT License
3.22k stars 146 forks source link

Support for WebAssembly/WASM #418

Open newtack opened 6 years ago

newtack commented 6 years ago

I want to use gluon in WebAssembly. Is this possible now (doubtful since I saw that threads are used and WASM doesn't support threads yet) and if not what would need to change to make it support WASM?

Marwes commented 6 years ago

That's going to be a hard thing to do :) .

Yep, gluon threads (not to be confused with OS threads, gluon's are more like coroutines so perhaps I should rename them). If you omit threads there are still a lot of other problems.

There is probably more hard things to solve than the points I outlined above. If I think of any I will update the list.

Given all the hurdles and the fact that most problems to be solved do not benefit gluon as an embeddable language I am not looking to implement a WASM backed (or any other assembly-like) backend. It would certainly be an interesting project though so I am open to helping out if anyone is interested in attempting this (or making a LLVM backend for that matter, that might be even more useful). While I have only been listing problems so far there is one thing that should help a lot when writing a backend. The compiler provides an extremely small Intermediate Representation (IR) as output after all typechecking is done which should be very easy to translate into WASM, LLVM-IR, assembly etc https://github.com/gluon-lang/gluon/blob/483dfef2cc1e2ff58383713d26430bd39245c979/vm/src/core/mod.rs#L57-L101 . So it is just the runtime that is problematic 🙄

newtack commented 6 years ago

Thanks for your fast response.

Storyyeller commented 6 years ago

Regarding monomorphization, I don't understand what problems WASM presents that wouldn't already be an issue with Rust. When compiling to native Rust binaries, you already have to monomorphize everything. How is WASM different?

Marwes commented 6 years ago

@Storyyeller A function like this

let test f b c: (forall a . a -> a) -> b -> c  -> () =
    f b
    f c
    f ()

is impossible to write in Rust but you can do it in gluon. If you think about it, the argument f is a function that needs to be able to take ANY type. The only general way to handle this is to make sure that all values have the same representation which would basically make all gluon values be a tagged union (and this is indeed the same representation that the current interpreter uses).

https://github.com/gluon-lang/gluon/blob/0909139a2a80389a56d6546e921aef9e52abf9cc/vm/src/value.rs#L296-L335

This uniform representation means some extra work when generating WASM/LLVM-IR/etc but it shouldn't be excessively bad. Having this extra tag might also make it possible to skip out on generating stack-maps for the garbage collector since this means that all values know if they are heap allocated or not.

That said, even though tagged values makes this possible, there is a tradeoff. To tag each value we either need an extra integer for each value store the tag (which costs memory and some speed), or we need to pack the tag into the value itself, sacrificing (fast) i64/u64 (no cost in memory but maybe a slightly larger speed loss).

Marwes commented 6 years ago

Thinking about it, these kinds of functions that can't be monomorphized should be pretty uncommon. It might be possible to just return an error if a function that can't be monomorphized is encountered (to start with) and still be able to compile most real-world gluon code.

If/when it becomes necessary to compile these functions one could then generate extra code to tag and untag any values passed to and from these function. It makes the compiler more complex but it should be doable.

Storyyeller commented 6 years ago

I understand that higher ranked types can't be efficiently compiled to native code. My question is why this is more of a problem for WASM than it is for Rust. It seems to me like the issues should be the same either way.

Marwes commented 6 years ago

The problem here is the same for WASM/Rust/LLVM-IR/assembly . The only reason it is not a problem for Rust is that that Rust's type system do not allow these kinds of functions to be written. If Rust's type system where extended to support it then it would have the same problem.

(Rust RFC for Higher-ranked-types https://github.com/rust-lang/rfcs/issues/1481 )

Storyyeller commented 6 years ago

I know that. I meant why is it not a problem when you're running the Gluon VM in Rust? Surely running Gluon in Rust and running it in WASM should be equivalent?

Marwes commented 6 years ago

@Storyyeller Are you talking about compiling the gluon interpreter that is now written in Rust into WASM using rustc's WASM support or are you talking about compiling gluon code into WASM?

If it is the former then I don't think there is anything preventing that. The only platform specific code that is needed is for the REPL (or possibly in one of gluon's dependencies such as tokio).

What I wrote about above has only been about the latter, ie adding another back end which is capable emitting WASM (or LLVM-IR etc) from the gluon compiler itself instead of the custom bytecode that the current interpreter uses https://github.com/gluon-lang/gluon/blob/0909139a2a80389a56d6546e921aef9e52abf9cc/vm/src/types.rs#L17-L117

Storyyeller commented 6 years ago

Oh, I thought you were talking about the first one. Sorry for the confusion.

Marwes commented 6 years ago

@Storyyeller No worries 😆 .

Out of curiosity I tried compiling gluon with WASM. Currently it stops compilation due to https://crates.io/crates/iovec not having an implementation for WASM which is needed for tokio-core. There are probably more platform specific things in tokio-core (mio for instance) even if that is fixed however so the best way to let gluon compile to WASM (using rustc/cargo) would be to make tokio-core optional which should be possible at the cost of not being able to run all async code (only the futures crate may be used).

Marwes commented 6 years ago

@Storyyeller I made tokio_core optional now so gluon actually compiles to WASM now (about 3.5 Mb). Still needs some boilerplate to deal with the pointers used in the exported C API but in theory everything should work.

https://travis-ci.org/gluon-lang/gluon/builds/340155165

Zireael07 commented 4 years ago

Any news?

Marwes commented 4 years ago

Not really, you can still compile the rust code in the gluon crate to WASM https://travis-ci.org/gluon-lang/gluon/jobs/340155173 and run the interpreter in WASM.

For compiling gluon code directly to WASM I did a small investigation on using https://github.com/bytecodealliance/cranelift to JIT compile but i didn't take it further than compiling functions that only operate directly on integers/floats (no closures, records, indirect function calls etc etc).

Boscop commented 2 years ago

@Marwes It's not working, I'm getting this runtime error when using gluon in wasm. Any idea why? :)

image

gluon = { version = "0.18", default-features = false, features = ["random"] }

nightly-2022-01-31 Using trunk to build.

EDIT: I get the same error when not using the random feature.

Boscop commented 2 years ago

Also, another aspect of Wasm support would be that if a gluon script prints to stdout/stderr, it won't work. Is there a way to get all printed output from the script through a std channel or something? So that the host can display this output in appropriate ways (e.g. as part of the wasm UI, or log it to the browser console, or send it to the backend and print it to stdout/stderr there). How to do this? :)

Zireael07 commented 2 years ago

What I do in my own (non-gluon) project is this: `// A macro to provide println!(..)-style syntax for console.log logging.

[macro_export]

macro_rules! log { ( $( $t:tt ) ) => { web_sys::console::log_1(&format!( $( $t ) ).into()) } } ` This means all log! prints to web console.

Boscop commented 2 years ago

@Zireael07 Sure, but it can't be used to redirect the output of gluon scripts.

lilac commented 1 week ago

@Storyyeller A function like this

let test f b c: (forall a . a -> a) -> b -> c  -> () =
    f b
    f c
    f ()

is impossible to write in Rust but you can do it in gluon. If you think about it, the argument f is a function that needs to be able to take ANY type. The only general way to handle this is to make sure that all values have the same representation which would basically make all gluon values be a tagged union (and this is indeed the same representation that the current interpreter uses).

https://github.com/gluon-lang/gluon/blob/0909139a2a80389a56d6546e921aef9e52abf9cc/vm/src/value.rs#L296-L335

This uniform representation means some extra work when generating WASM/LLVM-IR/etc but it shouldn't be excessively bad. Having this extra tag might also make it possible to skip out on generating stack-maps for the garbage collector since this means that all values know if they are heap allocated or not.

That said, even though tagged values makes this possible, there is a tradeoff. To tag each value we either need an extra integer for each value store the tag (which costs memory and some speed), or we need to pack the tag into the value itself, sacrificing (fast) i64/u64 (no cost in memory but maybe a slightly larger speed loss).

The OCaml's runtime also has a uniform value representation, so it seems gluon program can be compiled to OCaml's IR. If so then we can easily build a gluon compiler and interpreter, by utilizing an IR like Malfunction, which is actually a thin abstraction of OCaml compiler's lambda IR. What do you think?