From f7aff7585d95113a473dd96a0b88347d6c291c0e Mon Sep 17 00:00:00 2001 From: YetAnotherMinion Date: Wed, 15 Sep 2021 06:07:28 +0100 Subject: [PATCH] feat: unix adaptor for elm called starmelon Introduce starmelon, a program for executing elm functions with input from files and writing the output back to files. Support evaluating 4 types of values and 12 types of functions. ```elm x : String x : Bytes x : VirtualDom.Node msg x : Json.Encode.Value f : String -> String f : String -> Bytes f : String -> VirtualDom.Node msg f : String -> Json.Encode.Value f : Bytes -> String f : Bytes -> Bytes f : Bytes -> VirtualDom.Node msg f : Bytes -> Json.Encode.Value f : Json.Encode.Value -> String f : Json.Encode.Value -> Bytes f : Json.Encode.Value -> VirtualDom.Node msg f : Json.Encode.Value -> Json.Encode.Value ``` My target use case for starmelon is generating html files. It is nice to be able to write parameterized framgents of markup in multiple modules and then compose them into a final value. I also have in mind attempting to replace helm (kubernetes pacakge manager) because I hate how error prone its string based templating language is. --- Cargo.toml | 24 + examples/single-page/Cargo.toml | 11 + examples/single-page/elm.json | 25 + examples/single-page/src/Foo/Main.elm | 14 + examples/single-page/src/Main.elm | 19 + examples/single-page/src/lib.rs | 6 + examples/single-page/starmelon | 1 + src/main.rs | 1256 +++++++++++++++++++++++++ 8 files changed, 1356 insertions(+) create mode 100644 Cargo.toml create mode 100644 examples/single-page/Cargo.toml create mode 100644 examples/single-page/elm.json create mode 100644 examples/single-page/src/Foo/Main.elm create mode 100644 examples/single-page/src/Main.elm create mode 100644 examples/single-page/src/lib.rs create mode 120000 examples/single-page/starmelon create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2af5904 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "starmelon" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +structopt = { version = "0.3" } +elmi = { path = "../../../infra/rust-elmi" } +uuid = { version = "0.8", features = [ "v4" ] } +ahash = "0.7" +serde = { version = "1.0", features = [ "derive" ] } +serde_json = { version ="1.0", features = [] } + +# All of these are required for deno's javascript runtime. We need to keep the +# same versions as other projects in our cargo workspace. Multiple different +# versions of rust_v8 seem to break its build script. +deno_runtime = "0.21.0" +tokio = { version = "1.6", features = ["full"] } +deno_core = "0.95.0" +deno_web = "0.44" +rusty_v8 = "0.25.3" +futures = "0.3.15" diff --git a/examples/single-page/Cargo.toml b/examples/single-page/Cargo.toml new file mode 100644 index 0000000..80d6301 --- /dev/null +++ b/examples/single-page/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "single-page" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = [ "dylib" ] + +[dependencies] + diff --git a/examples/single-page/elm.json b/examples/single-page/elm.json new file mode 100644 index 0000000..e664a5e --- /dev/null +++ b/examples/single-page/elm.json @@ -0,0 +1,25 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/svg": "1.0.1" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/examples/single-page/src/Foo/Main.elm b/examples/single-page/src/Foo/Main.elm new file mode 100644 index 0000000..66240d4 --- /dev/null +++ b/examples/single-page/src/Foo/Main.elm @@ -0,0 +1,14 @@ +module Foo.Main exposing (view) + +import Html exposing (Html, div, text) +import Array exposing (Array) + +type alias Model = + { a : Int + , b : Array Int + } + +view : Model -> Html msg +view model = + div [] + [ text "Hello world" ] diff --git a/examples/single-page/src/Main.elm b/examples/single-page/src/Main.elm new file mode 100644 index 0000000..798f184 --- /dev/null +++ b/examples/single-page/src/Main.elm @@ -0,0 +1,19 @@ +module Main exposing (view, view2) + +import Html exposing (Html, div, text) +import Svg exposing (Svg, svg) +import Array exposing (Array) + +type alias Model = + { a : Int + , b : Array Int + } + +view2 : String -> Svg msg +view2 model = + svg [] [] + +view : String -> Html msg +view model = + div [] + [ text <| "Hello world" ++ model ] diff --git a/examples/single-page/src/lib.rs b/examples/single-page/src/lib.rs new file mode 100644 index 0000000..48745b1 --- /dev/null +++ b/examples/single-page/src/lib.rs @@ -0,0 +1,6 @@ +#![crate_type = "dylib"] + +#[no_mangle] +pub fn foobar() -> i32 { + 42 +} diff --git a/examples/single-page/starmelon b/examples/single-page/starmelon new file mode 120000 index 0000000..11f0862 --- /dev/null +++ b/examples/single-page/starmelon @@ -0,0 +1 @@ +../../../../../target/debug/starmelon \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..01bfb0b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,1256 @@ +use elmi::DataBinary; +use rusty_v8 as v8; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::Display; +use std::fs::{self, canonicalize, metadata}; +use std::hash::Hasher; +use std::io::{self, BufRead, Error, ErrorKind, Seek, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::Arc; +use std::time::Instant; +use structopt::StructOpt; +use tokio; +use uuid::Uuid; + +fn main() { + let args = Arguments::from_args(); + + match args { + // Arguments::Make { file, debug, output } => { + Arguments::Exec { + file, + debug, + function, + output, + } => { + let mut command = Command::new("elm"); + command + .arg("make") + .arg("--output") + .arg("/dev/null") + .stdin(Stdio::null()); + + if debug { + command.arg("--debug"); + } + command.arg(&file); + + let start = Instant::now(); + match command.status() { + Ok(exit_status) => { + if !exit_status.success() { + return; + } + } + Err(_) => { + return; + } + } + eprintln!("[{:?}] compiled {:?}", Instant::now() - start, file); + + // Step 2, find the elm.json and elm-stuff directory + let elm_project_dir = match find_project_root("elm.json", "./") { + Ok(p) => p, + Err(_) => { + return; + } + }; + + let elm_cache_dir = elm_project_dir.join("elm-stuff").join("0.19.1"); + + if !elm_cache_dir.is_dir() { + return; + } + + // TODO remove this unwrap + let data = std::fs::read(&file).unwrap(); + + let mut hasher = PortableHash::new(); + hasher.write_all(&data).unwrap(); + // also include the function name in the checksum so users can run multiple functions + // from the same file but still get caching if the file does not change. + hasher.write_all(function.as_bytes()).unwrap(); + let source_checksum = hasher.finish(); + + // step 2.5 get the module name out of the file. + let target_module = if let Some(Ok(first_line)) = data.lines().next() { + let mut tokens = first_line.split_whitespace(); + match tokens.next() { + Some(token) if token == "port" => { + tokens.next(); + } + Some(token) if token == "module" => (), + Some(token) if token == "effect" => { + tokens.next(); + } + _ => return, + }; + if let Some(module_name) = tokens.next() { + module_name.to_string() + } else { + return; + } + } else { + return; + }; + + // step 3 find all the filepaths in the elm-stuff/0.19.1/* folder + let interfaces = (|| -> io::Result> { + let mut interfaces = HashMap::new(); + + for entry in fs::read_dir(&elm_cache_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + match path.extension() { + Some(ext) if ext == "elmi" => { + // step 4 load all the modules + let start = Instant::now(); + let data = std::fs::read(&path).unwrap(); + eprintln!("[{:?}] loaded {:?}", Instant::now() - start, path); + + let start = Instant::now(); + let (_remaining, i) = elmi::Interface::get(&data).unwrap(); + + if let Some(stem) = path.file_stem() { + // in theory the module name of the interface can be determined by + // the filename in elm 0.19.0 and 0.19.1 + let module_name: String = stem + .to_string_lossy() + .split('-') + .collect::>() + .join("."); + eprintln!( + "[{:?}] parsed {:?}", + Instant::now() - start, + module_name + ); + interfaces.insert(module_name, i); + } + } + _ => (), + } + } + } + + Ok(interfaces) + })() + .unwrap(); + + // Step 5, check for the desired function + let start = Instant::now(); + let (input_type, output_type) = match interfaces.get(&target_module) { + Some(interface) => match interface.values.get(&elmi::Name::from(&function)) { + Some(annotation) => { + let elmi::CannonicalAnnotation(_free_vars, tipe) = annotation; + match resolve_function_type(tipe) { + Ok(x) => x, + Err(problem) => { + println!("problem {:?}", problem); + return; + } + } + } + None => return, + }, + None => return, + }; + eprintln!("[{:?}] resolved target function", Instant::now() - start); + + // step 6 create our private project + let generator_dir = setup_generator_project(elm_project_dir.clone()).unwrap(); + + // step 7 create an Elm fixture file to run our function + let start = Instant::now(); + let (gen_module_name, source) = generate_fixture( + source_checksum, + target_module, + function, + input_type, + output_type, + ); + + let mut source_filename = generator_dir.join("src").join(&gen_module_name); + source_filename.set_extension("elm"); + + let mut source_file = fs::File::create(&source_filename).unwrap(); + source_file.write_all(source.as_bytes()).unwrap(); + drop(source_file); + eprintln!( + "[{:?}] created generator Elm program", + Instant::now() - start + ); + + // step 8 compile the fixture + let mut intermediate_file = generator_dir.join("obj").join(&gen_module_name); + intermediate_file.set_extension("js"); + + let mut command = Command::new("elm"); + + command + .arg("make") + .arg("--output") + .arg(&intermediate_file) + .current_dir(&generator_dir) + .stdin(Stdio::null()); + + if debug { + command.arg("--debug"); + } + command.arg(&source_filename); + + let start = Instant::now(); + match command.status() { + Ok(exit_status) => { + if !exit_status.success() { + return; + } + } + Err(_) => { + return; + } + } + eprintln!( + "[{:?}] compiled {:?}", + Instant::now() - start, + intermediate_file + ); + + // Step 9 fixup the compiled script to run in Deno + + let data = fs::read_to_string(&intermediate_file).unwrap(); + + // TODO figure out how to replace multiple substrings in a single pass. One neat trick + // might be to allocate enough space in our starting buffer to write the new code by + // having a really large replace block. For example if we are replacing a string + // `"REPLACE_ME" + "abc" * 2000` we have over 6k bytes to write out the new code. We + // will have to do some extra book keeping to make sure the buffer space is big enough + // for the replacement code. + let mut final_script = data + .replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)") + .replace(";}(this));", ";}(globalThis));"); + + // TODO figure out if I need the print helper + //window.print = function(msg) { + // Deno.core.print(msg); + //} + + final_script.push_str("\n\n"); + final_script.push_str(&format!("var worker = Elm.{}.init();\n", gen_module_name)); + // add a short cut for invoking the function so I don't have to traverse so many object + // lookups using the rust v8 API. + final_script.push_str( + "globalThis.runOnInput = function(data) { worker.ports.onInput.send(data) };\n", + ); + + match output_type { + OutputType::Value => { + final_script.push_str( + r#" + worker.ports.onOutput.subscribe(function(output){ + if (output.ctor === "Ok") { + const json = JSON.stringify(output.a); + Deno.core.opSync('op_starmelon_string_output', output); + } else { + Deno.core.opSync('op_starmelon_problem', output.a); + } + }); + "#, + ); + } + OutputType::Bytes => { + final_script.push_str( + r#" + // Elm will send a DataView + worker.ports.onOutput.subscribe(function(output){ + if (output.ctor === "Ok") { + const ui8 = new Uint8Array(output.a.buffer); + Deno.core.opSync('op_starmelon_bytes_output', ui8); + } else { + Deno.core.opSync('op_starmelon_problem', output.a); + } + }); + "#, + ); + } + OutputType::String | OutputType::Html => { + final_script.push_str( + r#" + worker.ports.onOutput.subscribe(function(output){ + if (output.ctor === "Ok") { + Deno.core.opSync('op_starmelon_string_output', output.a); + } else { + Deno.core.opSync('op_starmelon_problem', output.a); + } + }); + "#, + ); + } + } + + let mut final_file = generator_dir.join("bin").join(&gen_module_name); + final_file.set_extension("js"); + std::fs::write(&final_file, final_script).unwrap(); + + // step 10 create a v8 isolate. We need to register a different callback depending on + // the output type (string, or bytes) + + let (mut worker, main_module) = + runtime::setup_worker(&final_file.to_string_lossy()).unwrap(); + + let mailbox: Arc, String>>>> = + Arc::new(RefCell::new(None)); + + let mailbox_clone = Arc::clone(&mailbox); + worker.js_runtime.register_op( + "op_starmelon_bytes_output", + deno_core::op_sync(move |_state, msg: deno_core::ZeroCopyBuf, _: ()| { + let slice: &[u8] = &msg; + eprintln!("got message from v8 runtime {:?}", slice.to_owned()); + if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { + mailbox.replace(Ok(slice.to_owned())); + } + Ok(()) + }), + ); + + let mailbox_clone = Arc::clone(&mailbox); + worker.js_runtime.register_op( + "op_starmelon_string_output", + deno_core::op_sync(move |_state, msg: String, _: ()| { + if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { + mailbox.replace(Ok(msg.into_bytes())); + } + Ok(()) + }), + ); + + let mailbox_clone = Arc::clone(&mailbox); + worker.js_runtime.register_op( + "op_starmelon_problem", + deno_core::op_sync(move |_state, msg: String, _: ()| { + eprintln!("got problem from v8 runtime {:?}", &msg); + if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { + mailbox.replace(Err(msg)); + } + Ok(()) + }), + ); + + worker.js_runtime.sync_ops_cache(); + + let start = Instant::now(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + // step 10 load the module into our v8 isolate + worker.execute_module(&main_module).await.unwrap(); + //worker.execute_script("somelocation", r#"runOnInput("hello from v8");"#); + let wait_for_inspector = false; + worker.run_event_loop(wait_for_inspector).await.unwrap(); + + { + let scope = &mut worker.js_runtime.handle_scope(); + let ctx = scope.get_current_context(); + let global = ctx.global(scope); + + let runOnInput = { + let x = v8::String::new(scope, "runOnInput").unwrap(); + v8::Local::new(scope, x).into() + }; + let v8_value = global.get(scope, runOnInput).unwrap(); + + // step 11 marshal the input into the v8 isolate. If we are reading from an + // input file load that, if we are reading from stdin read that. + let string_input = "Foo bar zap".to_owned(); + + // step 12 invoke the function + let function = v8::Local::::try_from(v8_value).unwrap(); + + let this = v8::undefined(scope).into(); + + let arg1 = { + let x = v8::String::new(scope, &string_input).unwrap(); + v8::Local::new(scope, x).into() + }; + + let start = Instant::now(); + function.call(scope, this, &[arg1]); + eprintln!("\tcall dispatched {:?}", Instant::now() - start); + } + worker.run_event_loop(wait_for_inspector).await.unwrap(); + eprintln!("finished waiting on runtime"); + }); + eprintln!("[{:?}] eval javascript", Instant::now() - start); + + // step 13 receive the callback + // If I understood which combination of run_event_loop was required to execute + // the javascript to completion then we should have something in our mailbox by + // now. Another way to do this would be with an Arc. This will panic if the + // mailbox is currently borrowed + match mailbox.replace(None) { + Some(Ok(output)) => { + io::stdout().write_all(&output).unwrap(); + } + Some(Err(problem)) => { + println!("had a problem {}", problem); + } + None => println!("nothing in the mailbox"), + } + } + } + // step 6 generate the rust code type definitions, using the built in types when possible. + // step 7 assume someone already wrote the rust code to construct that type and expose it as dylib symbol in + // lib.rs as fn init() -> T + // + // step 8 run cargo build + // + // step 9 generate a PrivateMain worker using the view function + // + // step 10 load that private main into a v8 isolate + // + // step 11 load the dynamic library produced + // + // step 12 invoke the dynamic library function to get the data + // + // step 13 serialize the value + // + // step 14 put that value into the memory of the v8 + // - parse json value from string and run a generated decoder + // - eval a script that invokes the construtor functions directly + // + // step 15 invoke the wrapped view function + // + // step 16 take the string output and write it to a file if provided, or stdout +} + +struct PortableHash(ahash::AHasher); + +impl PortableHash { + fn new() -> Self { + // We need constant keys to get the same checksup every time we run the program. + Self(ahash::AHasher::new_with_keys(1, 2)) + } +} + +impl std::io::Write for PortableHash { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.write(bytes); + Ok(bytes.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl ::core::hash::Hasher for PortableHash { + fn write(&mut self, bytes: &[u8]) { + self.0.write(bytes); + } + + fn finish(&self) -> u64 { + self.0.finish() + } +} + +#[derive(Debug, StructOpt)] +enum Arguments { + //Make { + // #[structopt(parse(from_os_str))] + // file: PathBuf, + // #[structopt(long)] + // debug: bool, + // #[structopt(long)] + // output: Option, + //}, + Exec { + #[structopt(parse(from_os_str))] + file: PathBuf, + + function: String, + #[structopt(long)] + debug: bool, + #[structopt(long)] + output: Option, + }, +} + +#[derive(Debug, Copy, Clone)] +enum InputType { + Value, + String, + Bytes, +} + +#[derive(Debug, Copy, Clone)] +enum OutputType { + Html, + String, + Bytes, + Value, +} + +fn resolve_function_type(tipe: &elmi::Type) -> Result<(Option, OutputType), Problem> { + match tipe { + elmi::Type::TLambda(a, b) => { + // We want to check the output types first because this is where we will figure out if + // there is more than one argument + let output_type = resolve_output_type(&**b)?; + let input_type = resolve_input_type(&**a)?; + + Ok((Some(input_type), output_type)) + } + elmi::Type::TVar(_) => Err(Problem::CantEvalGeneric), + elmi::Type::TType(module_name, name, args) if args.is_empty() => { + // If our function returns a primitive type + if module_name == "elm/core/String" && name == "String" { + return Ok((None, OutputType::String)); + } + if module_name == "elm/bytes/Bytes" && name == "Bytes" { + return Ok((None, OutputType::String)); + } + + Err(Problem::CantEvalType(tipe.clone())) + } + elmi::Type::TType(module_name, name, args) => Err(Problem::CantEvalCustomType), + elmi::Type::TRecord(_, _) => Err(Problem::CantEvalRecord), + elmi::Type::TUnit => Err(Problem::CantEvalUnit), + elmi::Type::TTuple(_, _, _) => Err(Problem::CantEvalTuple), + elmi::Type::TAlias(_, _, _, ref alias) => { + match &**alias { + elmi::AliasType::Filled(tipe) => { + // I think the recursion is limited to a single step. I have not tested what + // the CannonicalAnnotation would look like for a doubly indirect alias, for + // example for `view` below + // ```elm + // type alias Foo = Int + // type alias Bar = String + // + // type alias Zap = Foo -> Bar + // + // view : Zap + // ``` + resolve_function_type(tipe) + } + elmi::AliasType::Holey(_) => return Err(Problem::CantEvalHoleyAlias), + } + } + } +} + +fn resolve_input_type(tipe: &elmi::Type) -> Result { + match tipe { + elmi::Type::TLambda(_, _) => Err(Problem::EvalRequiresSingleArgument(tipe.clone())), + elmi::Type::TType(module_name, name, args) if args.is_empty() => { + if module_name == "elm/core/String" && name == "String" { + Ok(InputType::String) + } else if module_name == "elm/bytes/Bytes" && name == "Bytes" { + Ok(InputType::Bytes) + } else if module_name == "elm/json/Json.Encode" && name == "Value" { + Ok(InputType::Value) + } else { + Err(Problem::InputTypeNotSupported(tipe.clone())) + } + } + elmi::Type::TAlias(_, _, _, ref alias) => match &**alias { + elmi::AliasType::Filled(tipe) => resolve_input_type(tipe), + elmi::AliasType::Holey(_) => Err(Problem::CantEvalHoleyAlias), + }, + _ => Err(Problem::OutputTypeNotSupported(tipe.clone())), + } +} + +fn resolve_output_type(tipe: &elmi::Type) -> Result { + match tipe { + elmi::Type::TType(module_name, name, args) => { + if module_name == "elm/core/String" && name == "String" { + Ok(OutputType::String) + } else if module_name == "elm/bytes/Bytes" && name == "Bytes" { + Ok(OutputType::Bytes) + } else if module_name == "elm/json/Json.Encode" && name == "Value" { + Ok(OutputType::Value) + } else if module_name == "elm/virtual-dom/VirtualDom" && name == "Node" { + Ok(OutputType::Html) + } else { + Err(Problem::OutputTypeNotSupported(tipe.clone())) + } + } + elmi::Type::TAlias(_, _, _, ref alias) => match &**alias { + elmi::AliasType::Filled(tipe) => resolve_output_type(tipe), + elmi::AliasType::Holey(_) => Err(Problem::CantEvalHoleyAlias), + }, + _ => Err(Problem::OutputTypeNotSupported(tipe.clone())), + } +} + +#[derive(Debug, Clone)] +enum Problem { + CantEvalRecord, + CantEvalUnit, + CantEvalCustomType, + CantEvalHoleyAlias, + CantEvalTuple, + CantEvalGeneric, + CantEvalType(elmi::Type), + InputTypeNotSupported(elmi::Type), + OutputTypeNotSupported(elmi::Type), + EvalRequiresSingleArgument(elmi::Type), + + InstallDependencyFailed, +} + +fn setup_generator_project(elm_project_dir: PathBuf) -> Result { + // I added a couple of random bytes to the directory name to reduce the risk of + // collisions with other programs that also use elm-stuff for their scratch space + let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc"); + + if !our_temp_dir.exists() { + fs::create_dir(&our_temp_dir); + } + let source_dir = our_temp_dir.join("src"); + let bin_dir = our_temp_dir.join("bin"); + + // Rather than attempt to modify the elm.json to use the extra libraries, I will invoke + // elm install. Because the elm install commands take 500ms for a no-op I decided to + // cache + + let elm_json_path = elm_project_dir.join("elm.json"); + let mut elm_json_file = std::fs::File::open(&elm_json_path).unwrap(); + + let mut hasher = PortableHash::new(); + std::io::copy(&mut elm_json_file, &mut hasher).unwrap(); + let checksum: u64 = hasher.finish(); + println!("elm.json checksum {}", checksum); + + let checksum_file = our_temp_dir.join("elm-json-checksum"); + if checksum_file.exists() { + let existing_checksum = String::from_utf8_lossy(&std::fs::read(&checksum_file).unwrap()) + .parse::() + .unwrap(); + + if existing_checksum == checksum { + return Ok(our_temp_dir); + } + } + + let mut f = std::fs::File::create(checksum_file).unwrap(); + ::write(&mut f, checksum.to_string().as_bytes()).unwrap(); + drop(f); + + let mut generator_elm_json_file = + std::fs::File::create(&our_temp_dir.join("elm.json")).unwrap(); + elm_json_file.rewind().unwrap(); + + let mut data = std::fs::read(&elm_json_path).unwrap(); + + let mut elm_json = serde_json::from_slice::(&mut data).unwrap(); + // We need to modify the elm.json's source_directories field + + // + let mut new_src_directories = Vec::with_capacity(elm_json.source_directories.len() + 1); + for dirname in elm_json.source_directories.iter() { + let dir = canonicalize(elm_project_dir.join(dirname)).unwrap(); + new_src_directories.push(dir.to_string_lossy().to_string()); + } + + std::fs::create_dir(&source_dir).unwrap(); + std::fs::create_dir(&bin_dir).unwrap(); + + new_src_directories.push( + canonicalize(our_temp_dir.join("src")) + .unwrap() + .to_string_lossy() + .to_string(), + ); + eprintln!("before json {:?}", elm_json); + elm_json.source_directories = new_src_directories; + + eprintln!("modified json {:?}", elm_json); + + serde_json::to_writer(&mut generator_elm_json_file, &elm_json).unwrap(); + + elm_install(&our_temp_dir, "ThinkAlexandria/elm-html-in-elm")?; + elm_install(&our_temp_dir, "elm/json")?; + elm_install(&our_temp_dir, "elm/bytes")?; + + Ok(our_temp_dir) +} + +#[derive(Debug, Serialize, Deserialize)] +struct ElmApplication { + #[serde(rename = "source-directories")] + source_directories: Vec, + #[serde(flatten)] + other_fields: serde_json::Value, +} + +fn elm_install, S: AsRef>(our_temp_dir: P, package: S) -> Result<(), Problem> { + let mut child = Command::new("elm") + .arg("install") + .arg(package.as_ref()) + .stdin(Stdio::piped()) + .current_dir(&our_temp_dir) + .spawn() + .unwrap(); + + let child_stdin = child.stdin.as_mut().unwrap(); + child_stdin.write_all(b"y\n").unwrap(); + drop(child_stdin); + + match child.wait() { + Ok(exit_status) => { + if !exit_status.success() { + return Err(Problem::InstallDependencyFailed); + } + } + Err(_) => { + return Err(Problem::InstallDependencyFailed); + } + } + + Ok(()) +} + +fn generate_fixture>( + source_checksum: u64, + target_module: S, + target_function: S, + input: Option, + output: OutputType, +) -> (String, String) { + // TODO update this size with a better estimate once I know how large the completed file is + // likely to be. + let mut buffer = String::with_capacity(4096); + + let module_name = format!("Gen{:020}", source_checksum); + + use std::fmt::Write; + write!(buffer, "port module {} exposing (main)\n", &module_name).unwrap(); + buffer.push_str(GENERATOR_IMPORTS); + buffer.push_str("-- START CUSTOMIZED PART\n"); + write!(buffer, "import {}\n\n", target_module.as_ref()).unwrap(); + + // if the input type is none then we don't have to generate an apply, and the port input type + // is () + match output { + OutputType::String => buffer.push_str("encodeOutput = encodeString\n"), + OutputType::Value => buffer.push_str("encodeOutput = encodeJson\n"), + OutputType::Bytes => buffer.push_str("encodeOutput = encodeBytes\n"), + OutputType::Html => buffer.push_str("encodeOutput = encodeHtml\n"), + } + + match input { + None => { + buffer.push_str(&zero_arg_apply( + target_module.as_ref(), + target_function.as_ref(), + )); + } + Some(InputType::Value) => { + buffer.push_str("decodeInput = decodeValue\n"); + buffer.push_str(&one_arg_apply( + target_module.as_ref(), + target_function.as_ref(), + )); + } + Some(InputType::Bytes) => { + buffer.push_str("decodeInput = decodeBytes\n"); + buffer.push_str(&one_arg_apply( + target_module.as_ref(), + target_function.as_ref(), + )); + } + Some(InputType::String) => { + buffer.push_str("decodeInput = decodeString\n"); + buffer.push_str(&one_arg_apply( + target_module.as_ref(), + target_function.as_ref(), + )); + } + } + buffer.push_str("-- END CUSTOMIZED PART\n"); + buffer.push_str(GENERATOR_BODY); + + (module_name, buffer) +} + +mod runtime { + use deno_core::error::{type_error, AnyError}; + use deno_core::futures::FutureExt; + use deno_core::{op_sync, resolve_url, FsModuleLoader, ModuleLoader, ModuleSpecifier, OpState}; + use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; + use deno_runtime::permissions::Permissions; + use deno_runtime::worker::MainWorker; + use deno_runtime::worker::WorkerOptions; + use deno_web::BlobStore; + use futures::future::lazy; + use rusty_v8 as v8; + use std::cell::RefCell; + use std::convert::TryFrom; + use std::path::Path; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::Arc; + use std::task::Context; + use std::time::Instant; + use tokio::sync::mpsc::unbounded_channel; + + fn get_error_class_name(e: &AnyError) -> &'static str { + deno_runtime::errors::get_error_class_name(e).unwrap_or("Error") + } + + pub fn setup_worker(path_str: &str) -> Result<(MainWorker, ModuleSpecifier), AnyError> { + //let module_loader = Rc::new(EmbeddedModuleLoader("void".to_owned())); + let module_loader = Rc::new(FsModuleLoader); + let create_web_worker_cb = Arc::new(|_| { + todo!("Web workers are not supported in the example"); + }); + + let options = WorkerOptions { + apply_source_maps: false, + args: vec![], + debug_flag: false, + unstable: false, + ca_data: None, + user_agent: "hello_runtime".to_string(), + seed: None, + js_error_create_fn: None, + create_web_worker_cb, + maybe_inspector_server: None, + should_break_on_first_statement: false, + module_loader, + runtime_version: "x".to_string(), + ts_version: "x".to_string(), + no_color: false, + get_error_class_fn: Some(&get_error_class_name), + location: None, + origin_storage_dir: None, + blob_store: BlobStore::default(), + broadcast_channel: InMemoryBroadcastChannel::default(), + shared_array_buffer_store: None, + }; + + let main_module = deno_core::resolve_path(path_str)?; + // note: resolve_url is just calling url::Url::parse and mapping the error type + // let main_module = resolve_url(SPECIFIER)?; + let permissions = Permissions::allow_all(); + + let mut worker = MainWorker::from_options(main_module.clone(), permissions, &options); + worker.bootstrap(&options); + + Ok((worker, main_module)) + } + + const SPECIFIER: &str = "file://$deno$/bundle.js"; + + struct EmbeddedModuleLoader(String); + + impl ModuleLoader for EmbeddedModuleLoader { + fn resolve( + &self, + _op_state: Rc>, + specifier: &str, + _referrer: &str, + _is_main: bool, + ) -> Result { + if let Ok(module_specifier) = resolve_url(specifier) { + //if get_source_from_data_url(&module_specifier).is_ok() + // || specifier == SPECIFIER + //{ + return Ok(module_specifier); + //} + } + Err(type_error( + "Self-contained binaries don't support module loading", + )) + } + + fn load( + &self, + _op_state: Rc>, + module_specifier: &ModuleSpecifier, + _maybe_referrer: Option, + _is_dynamic: bool, + ) -> Pin> { + let module_specifier = module_specifier.clone(); + //let is_data_uri = get_source_from_data_url(&module_specifier).ok(); + //let code = if let Some((ref source, _)) = is_data_uri { + // source.to_string() + //} else { + let code = self.0.to_string(); + //}; + async move { + //if is_data_uri.is_none() && module_specifier.to_string() != SPECIFIER { + // return Err(type_error( + // "Self-contained binaries don't support module loading", + // )); + //} + + Ok(deno_core::ModuleSource { + code, + module_url_specified: module_specifier.to_string(), + module_url_found: module_specifier.to_string(), + }) + } + .boxed_local() + } + } +} + +pub mod generated { + pub mod types { + pub trait Literal { + fn literal(&self, writer: &mut W) -> std::io::Result<()>; + } + + pub mod array { + use super::Literal; + use std::io::{self, Write}; + + pub struct Array(Vec); + + impl Literal for Array { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "$elm$core$Array$fromList(\n")?; + write!(writer, "\t$elm$core$List$fromArray([\n")?; + for item in self.0.iter() { + Literal::literal(item, writer)?; + write!(writer, ",\n")?; + } + write!(writer, "\t])\n")?; + write!(writer, ")") + } + } + } + + pub mod basics { + use super::Literal; + use std::io::{self, Write}; + + pub type Int = f64; + pub type Float = f64; + + impl Literal for f64 { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "{}", self) + } + } + } + + pub mod char { + use super::Literal; + use std::io::{self, Write}; + + pub type Char = char; + + impl Literal for Char { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "'{}'", self) + } + } + } + + pub mod dict { + use super::Literal; + use std::io::{self, Write}; + + pub type Dict = std::collections::HashMap; + + impl Literal for Dict { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "$elm$core$Dict$fromList(\n")?; + write!(writer, "\t$elm$core$List$fromArray([\n")?; + for (key, value) in self.iter() { + write!(writer, "(")?; + Literal::literal(key, writer)?; + write!(writer, ", ")?; + Literal::literal(value, writer)?; + write!(writer, "),\n")?; + } + write!(writer, "\t])\n")?; + write!(writer, ")") + } + } + } + + pub mod list { + use super::Literal; + use std::io::{self, Write}; + + pub type List = Vec; + + impl Literal for List { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "$elm$core$List$fromArray([\n")?; + for item in self.iter() { + Literal::literal(item, writer)?; + write!(writer, ",\n")?; + } + write!(writer, "\t])") + } + } + } + + pub mod maybe { + use super::Literal; + use std::io::{self, Write}; + + pub type Maybe = Option; + + impl Literal for Maybe { + fn literal(&self, writer: &mut W) -> io::Result<()> { + if let Some(inner) = self { + write!(writer, "$elm$core$Maybe$Just(")?; + Literal::literal(inner, writer)?; + write!(writer, ")") + } else { + write!(writer, "$elm$core$Maybe$Nothing") + } + } + } + } + + pub mod string { + use super::Literal; + use std::io::{self, Write}; + + pub type String = std::string::String; + + impl Literal for String { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "{:?}", self) + } + } + } + + pub mod tuple { + use super::Literal; + use std::io::{self, Write}; + + pub fn pair(a: A, b: B) -> (A, B) { + (a, b) + } + + impl Literal for (A, B) { + fn literal(&self, writer: &mut W) -> io::Result<()> { + write!(writer, "_Utils_Tuple2(")?; + Literal::literal(&self.0, writer)?; + write!(writer, ", ")?; + Literal::literal(&self.1, writer)?; + write!(writer, ")\n") + } + } + } + } +} + +/// Look for the cargo.toml file that we are building to determine the root directory of the project +/// (so that we can locate the source files) +fn find_project_root>(filename: S, search: S) -> io::Result { + if !search.as_ref().is_dir() { + return Err(Error::new( + ErrorKind::InvalidInput, + format!("`{}` is not a directory", search.as_ref().display()), + )); + } + let mut path_buf = canonicalize(&search)?; + + let error: io::Error = Error::new( + ErrorKind::NotFound, + format!( + "could not find `{}` in `{:?}` or any parent directory", + filename.as_ref().display(), + path_buf + ), + ); + + loop { + let test_file = path_buf.as_path().join(&filename); + match metadata(test_file) { + Ok(_) => return Ok(path_buf), + Err(_) => (), + } + match path_buf.as_path().parent() { + Some(_) => (), + None => return Err(error), + } + path_buf.pop(); + } +} + +// port module Generator exposing (main) +const GENERATOR_IMPORTS: &'static str = r#" +import Bytes exposing (Bytes) +import ElmHtml.InternalTypes exposing (decodeElmHtml) +import ElmHtml.ToString exposing (nodeToStringWithOptions, defaultFormatOptions) +import Html exposing (Html) +import Json.Decode as D +import Json.Encode as E +import Platform +"#; + +fn zero_arg_apply(target_module: S, function: S) -> String { + format!( + r#" + +apply : E.Value -> Cmd Msg +apply _ = + {target_module}.{function} + |> encodeOutput + |> onOutput +"#, + target_module = target_module, + function = function, + ) +} + +// The user just has to alias the decodeInput and encodeOutput +fn one_arg_apply(target_module: S, function: S) -> String { + format!( + r#" + +apply : E.Value -> Cmd Msg +apply value = + case decodeInput value of + Ok input -> + input + |> {target_module}.{function} + |> encodeOutput + |> onOutput + + Err error -> + error + |> D.errorToString + |> encodeFailure + |> onOutput +"#, + target_module = target_module, + function = function, + ) +} + +const GENERATOR_BODY: &'static str = r#" +-- MAIN + +main = + Platform.worker + { init = init + , update = update + , subscriptions = subscriptions + } + +-- PORTS + +port onInput : (E.Value -> msg) -> Sub msg +port onOutput : E.Value -> Cmd msg + +-- MODEL + + +type alias Model = () + +init : () -> (Model, Cmd Msg) +init _ = + ((), Cmd.none) + +-- UPDATE + +type Msg + = Input E.Value + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Input value -> + let + cmd = + apply value + in + (model, cmd) + +-- SUBSCRIPTIONS + +subscriptions : Model -> Sub Msg +subscriptions _ = + onInput Input + +-- DECODERS + +decodeBytes : E.Value -> Result D.Error Bytes +decodeBytes value = + D.decodeValue decodeBytesHelp value + +decodeBytesHelp : D.Decoder Bytes +decodeBytesHelp = + D.fail "REPLACE_ME_WITH_BYTES_DECODER" + +decodeString : E.Value -> Result D.Error String +decodeString value = + D.decodeValue D.string value + +-- ENCODERS + +encodeFailure : String -> E.Value +encodeFailure msg = + E.object + [ ("ctor", E.string "Err") + , ("a", E.string msg) + ] + +encodeSuccess : E.Value -> E.Value +encodeSuccess value = + E.object + [ ("ctor", E.string "Ok") + , ("a", value) + ] + +encodeJson : E.Value -> E.Value +encodeJson = + encodeSuccess + +encodeBytes : Bytes -> E.Value +encodeBytes bytes = + bytes + |> encodeBytesHelp + |> encodeSuccess + +encodeBytesHelp : Bytes -> E.Value +encodeBytesHelp bytes = + E.string "REPLACE_ME_WITH_BYTES_ENCODER" + +encodeString : String -> E.Value +encodeString s = + encodeSuccess (E.string s) + +encodeHtml : Html msg -> E.Value +encodeHtml node = + let + options = + { defaultFormatOptions | newLines = True, indent = 2 } + in + case + D.decodeString + (decodeElmHtml (\taggers eventHandler -> D.succeed ())) + (asJsonString node) + of + Ok elmHtml -> + elmHtml + |> nodeToStringWithOptions options + |> encodeString + + Err error -> + error + |> D.errorToString + |> encodeFailure + + +asJsonString : Html msg -> String +asJsonString x = "REPLACE_ME_WITH_JSON_STRINGIFY" +"#;