Print elm-syntax declarations as rust code.
To try it out, you can
run this script.
import Elm.Parser
import ElmSyntaxToRust
"""module Sample exposing (..)
plus2 : Float -> Float
plus2 n =
n + ([ 2.0 ] |> List.sum)
"""
|> Elm.Parser.parseToFile
|> Result.mapError (\_ -> "failed to parse elm source code")
|> Result.map
(\syntaxModule ->
[ syntaxModule ]
|> ElmSyntaxToRust.modules
|> .declarations
|> ElmSyntaxToRust.rustDeclarationsToModuleString
)
-->
Ok """...
pub fn sample_plus2<'a>(allocator: &'a Bump, n: f64) -> f64 {
std::ops::Add::add(n, elm::list_sum_float(elm::list(allocator, [2_f64])))
}
"""- not supported are
- ports that use non-json values like
port sendMessage : String -> Cmd msg, glsl,==on a generic value, mutually recursive phantom types elm/file,elm/http,elm/browser,elm-explorations/markdown,elm-explorations/webgl,elm-explorations/benchmark,elm/regex(nothing instd),elm-explorations/linear-algebra(std::simdonly available in nightly)Task,Process,Platform.Task,Platform.ProcessId,Platform.Router,Platform.sendToApp,Platform.sendToSelf,Random.generate,Time.now,Time.every,Time.here,Time.getZoneName,Bytes.getHostEndianness- extensible record types outside of module-level value/function declarations. For example, these declarations might not work:
Allowed is only record extension in module-level value/functions, annotated or not:
-- in variant value type Named rec = Named { rec | name : String } -- in let type, annotated or not let getName : { r | name : name } -> name
In the non-allowed cases listed above, we assume that you intended to use a regular record type with only the extension fields which can lead to rust compile errors if you actually pass in additional fields.userId : { u | name : String, server : Domain } -> String
- elm's
toLocale[Case]functions will just behave liketoCase - elm's
VirtualDom/Html/Svg.lazyNfunctions will still exist for compatibility but they will behave just like constructing them eagerly
- ports that use non-json values like
- dependencies cannot internally use the same module names as the transpiled project
- the resulting code might not be readable or even conventionally formatted and comments are not preserved
Please report any issues you notice <3
- it runs fast natively and as wasm
- it feels like a superset of elm which makes transpiling and "ffi" easier
- it's overall a very polished and active language with a good standard library and good tooling
An example can be found in example-hello-world/.
In your elm project, add cargo.toml
[package]
name = "your-project-name"
edition = "2024" # not lower
[dependencies]
bumpalo = "3.19.0"and a file src/main.rs that uses elm.rs:
mod elm
print(elm::your_module_your_function("yourInput"))where elm::your_module_your_function(firstArgument, secondArgument) is the transpiled elm function Your.Module.yourFunction firstArgument secondArgument. (If the value/function contains extensible records, search for elm::your_module_your_function_ with the underscore to see the different specialized options)
Run with
cargo runIf something unexpected happened, please report an issue.
In the transpiled code, you will find these types:
- elm
Bool(TrueorFalse) → rustbool(trueorfalse),Char('a') →char('a'),( Bool, Char )→( bool, char ) - elm
Ints will be of typei64. Create and match by appending_i64to any number literal or usingas i64 - elm
Floats will be of typef64. Create and match by appending_f64to any number literal or usingas f64 - elm
Strings (like"a") will be of typeelm::StringString. Create from literals or other string slices with (elm::StringString::One("a")). Match withyour_string if elm::string_equals_str(your_string, "some string") - elm
Array as (likeArray.fromList [ 'a' ]) will be of typeRc<Vec<A>>(aliaselm::ArrayArray<A>). Create new values with (std::rc::Rc::new(vec!['a'])). To match, use e.g.match array.as_slice() { [] => ..., [_, ..] => ... etc } - elm records like
{ y : Float, x : Float }will be of typeelm::GeneratedXY<f64, f64>with the fields sorted and can be constructed and matched withelm::GeneratedXY { x: _, y: _ }.record.xaccess also works - a transpiled elm app does not run itself.
An elm main
Platform.workerprogram type will literally just consist of fieldsinit,updateandsubscriptionswhere subscriptions/commands are returned as a list ofelm::PlatformSubSingle/elm::PlatformCmdSinglewith possible elm subscriptions/commands in a choice type. It's then your responsibility as "the platform" to perform effects, create events and manage the state. For an example see example-worker-blocking/ & example-worker-concurrent/
Most transpiled functions require a reference to an allocator to be passed as the first argument.
When the called function or an indirectly called function then creates a new List for example, it will use the given allocator.
bumpalo specifically is required because rusts allocator APIs are not stabilized, yet.
Also note that regular lifetime end + Drop stuff will still occur sometimes.
So overall, if you intend to let the transpiled code handle a memory-hungry long-running computation, it might run out of memory. Use it for classic arena-friendly loop steps like state → interface, request → response etc.
If you want elm to control the application state,
I recommend taking a look at /example-worker-blocking/,
specifically the struct ElmStatePersistent in src/main.rs.
You will typically have the transpiled type for the elm state which has a temporary lifetime bound to a given allocator and a separate custom "persistent" rust type
which only contains owned types – and conversion functions between the two.
- keep types closer to elm (and do not expand aliases everywhere which currently leads to large and hard to understand transpiled types)
- try and benchmark switching
Stringrepresentation fromOne &str | Append String StringtoRc<Vec<&str>>or&dyn Fn(String) -> Stringto avoid massive nesting = indirection = expensive memory lookup (+ alloc and dealloc but lesser so) - if lambda is called with a function, always inline that function
- possible optimization: make
JsonValuelazy at field values and Array level - your idea 👀