diff --git a/Cargo.toml b/Cargo.toml index cb790f0..50508d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2018" [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 = [] } diff --git a/src/main.rs b/src/main.rs index 9ae656e..d4090e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ extern crate naive_wadler_prettier as pretty; -use crate::reporting::{Problem, SetupError, TypeError}; +use crate::reporting::{CompilerError, InterpreterError, Problem, SetupError, TypeError}; use elmi::DataBinary; -use rusty_v8 as v8; +use pretty::pretty; 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; @@ -16,7 +15,6 @@ use std::sync::Arc; use std::time::Instant; use structopt::StructOpt; use tokio; -use uuid::Uuid; mod reporting; @@ -30,410 +28,361 @@ fn main() { debug, function, output, + verbosity, } => { - 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 - // TODO CompilerError:: - let data = std::fs::read(&file).unwrap(); - - let mut hasher = PortableHash::new(); - // TODO CompilerError::ElmJsonChecksumFailed(io::Error, PathBuf), - 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"), + if let Err(problem) = exec(file, debug, function, output, verbosity) { + println!("{}", pretty(80, problem.to_doc())); } } } - // 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 +} + +fn exec( + file: PathBuf, + debug: bool, + function: String, + output: Option, + verbosity: u64, +) -> Result<(), Problem> { + 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() { + // TODO report the stdout from the elm compiler + return Err(Problem::Wildcard("elm failed".into())); + } + } + Err(_) => { + return Err(Problem::Wildcard("elm failed".into())); + } + } + eprintln!("[{:?}] compiled {:?}", Instant::now() - start, file); + + // Step 2, find the elm.json and elm-stuff directory + let elm_project_dir = + find_project_root("elm.json", "./").map_err(CompilerError::MissingElmJson)?; + + let elm_cache_dir = elm_project_dir.join("elm-stuff").join("0.19.1"); + + if !elm_cache_dir.is_dir() { + return Err(CompilerError::MissingElmStuff(elm_cache_dir).into()); + } + + let data = std::fs::read(&file) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, file.clone()))?; + + 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 Err(CompilerError::CantParseModule(first_line.clone()).into()); + } + }; + if let Some(module_name) = tokens.next() { + module_name.to_string() + } else { + return Err(CompilerError::CantParseModule(first_line.clone()).into()); + } + } else { + return Err(CompilerError::EmptyModule.into()); + }; + + // step 3 find all the filepaths in the elm-stuff/0.19.1/* folder + let interfaces = load_interfaces(&elm_cache_dir)?; + + // 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; + + resolve_function_type(tipe)? + } + None => return Err(CompilerError::CantFindFunction(function).into()), + }, + None => return Err(CompilerError::MissingModuleTypeInformation(target_module).into()), + }; + eprintln!("[{:?}] resolved target function", Instant::now() - start); + + // step 6 create our private project + let generator_dir = setup_generator_project(elm_project_dir.clone())?; + + // 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) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?; + source_file + .write_all(source.as_bytes()) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?; + 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(CompilerError::FailedBuildingFixture.into()); + } + } + Err(_) => { + return Err(CompilerError::FailedBuildingFixture.into()); + } + } + 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) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, intermediate_file.clone()))?; + + // 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));"); + + 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) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, final_file.clone()))?; + + // 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()) + .map_err(|err| InterpreterError::EventLoop(err))?; + + 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 { runtime::xyz(worker, main_module).await })?; + 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) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?; + } + Some(Err(problem)) => { + println!("had a problem {}", problem); + } + None => println!("nothing in the mailbox"), + } + + Ok(()) +} + +fn load_interfaces(elm_cache_dir: &Path) -> Result, Problem> { + let mut interfaces = HashMap::new(); + + let entries = fs::read_dir(&elm_cache_dir) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, elm_cache_dir.to_path_buf()))?; + + for entry in entries { + let entry = entry.map_err(|io_err| { + CompilerError::ReadInputFailed(io_err, elm_cache_dir.to_path_buf()) + })?; + 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) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; + eprintln!("[{:?}] loaded {:?}", Instant::now() - start, path); + + let start = Instant::now(); + let (_remaining, i) = elmi::Interface::get(&data).map_err(|err| { + CompilerError::FailedElmiParse("todo elmi parsing".to_owned()) + })?; + + 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) } struct PortableHash(ahash::AHasher); @@ -485,6 +434,8 @@ enum Arguments { debug: bool, #[structopt(long)] output: Option, + #[structopt(short = "v", parse(from_occurrences))] + verbosity: u64, }, } @@ -525,7 +476,7 @@ fn resolve_function_type(tipe: &elmi::Type) -> Result<(Option, Output Err(TypeError::CantEvalType(tipe.clone())) } - elmi::Type::TType(module_name, name, args) => Err(TypeError::CantEvalCustomType), + elmi::Type::TType(_, _, _) => Err(TypeError::CantEvalCustomType), elmi::Type::TRecord(_, _) => Err(TypeError::CantEvalRecord), elmi::Type::TUnit => Err(TypeError::CantEvalUnit), elmi::Type::TTuple(_, _, _) => Err(TypeError::CantEvalTuple), @@ -575,7 +526,7 @@ fn resolve_input_type(tipe: &elmi::Type) -> Result { fn resolve_output_type(tipe: &elmi::Type) -> Result { match tipe { - elmi::Type::TType(module_name, name, args) => { + 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" { @@ -602,7 +553,8 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc"); if !our_temp_dir.exists() { - fs::create_dir(&our_temp_dir); + fs::create_dir(&our_temp_dir) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, our_temp_dir.clone()))?; } let source_dir = our_temp_dir.join("src"); let bin_dir = our_temp_dir.join("bin"); @@ -612,7 +564,10 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result // 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 elm_json_file = std::fs::File::open(&elm_json_path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, elm_json_path.clone()))?; + // TODO CompilerError::ElmJsonChecksumFailed(io::Error, PathBuf), + // ReadInputFailed(io::Error, PathBuf), let mut hasher = PortableHash::new(); std::io::copy(&mut elm_json_file, &mut hasher).unwrap(); @@ -621,41 +576,58 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result 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()) + let mut raw_checksum = std::fs::read(&checksum_file) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, checksum_file.clone()))?; + let existing_checksum = String::from_utf8_lossy(&raw_checksum) .parse::() - .unwrap(); + .map_err(move |_parse_error| { + raw_checksum.truncate(60); + CompilerError::CorruptedChecksum(String::from_utf8_lossy(&raw_checksum).to_string()) + })?; 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(); + let mut f = std::fs::File::create(&checksum_file) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, checksum_file.clone()))?; + ::write(&mut f, checksum.to_string().as_bytes()) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, checksum_file.clone()))?; drop(f); + let generator_elm_json_path = our_temp_dir.join("elm.json"); let mut generator_elm_json_file = - std::fs::File::create(&our_temp_dir.join("elm.json")).unwrap(); - elm_json_file.rewind().unwrap(); + std::fs::File::create(&generator_elm_json_path).map_err(|io_err| { + CompilerError::WriteOutputFailed(io_err, generator_elm_json_path.clone()) + })?; - let mut data = std::fs::read(&elm_json_path).unwrap(); + elm_json_file + .rewind() + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, elm_json_path.clone()))?; - let mut elm_json = serde_json::from_slice::(&mut data).unwrap(); - // We need to modify the elm.json's source_directories field + let mut data = std::fs::read(&elm_json_path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, elm_json_path.clone()))?; + + let mut elm_json = serde_json::from_slice::(&mut data) + .map_err(|err| CompilerError::FailedParseElmJson(err))?; - // 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(); + let dir_path = elm_project_dir.join(dirname); + let dir = canonicalize(&dir_path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, dir_path.clone()))?; new_src_directories.push(dir.to_string_lossy().to_string()); } - std::fs::create_dir(&source_dir).unwrap(); - std::fs::create_dir(&bin_dir).unwrap(); + std::fs::create_dir(&source_dir) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_dir.clone()))?; + std::fs::create_dir(&bin_dir) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, bin_dir.clone()))?; new_src_directories.push( canonicalize(our_temp_dir.join("src")) - .unwrap() + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, our_temp_dir.join("src")))? .to_string_lossy() .to_string(), ); @@ -664,7 +636,9 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result eprintln!("modified json {:?}", elm_json); - serde_json::to_writer(&mut generator_elm_json_file, &elm_json).unwrap(); + serde_json::to_writer(&mut generator_elm_json_file, &elm_json).map_err(|io_err| { + CompilerError::WriteElmJsonFailed(io_err, generator_elm_json_path.clone()) + })?; elm_install(&our_temp_dir, "ThinkAlexandria/elm-html-in-elm")?; elm_install(&our_temp_dir, "elm/json")?; @@ -773,25 +747,22 @@ fn generate_fixture>( } mod runtime { + use crate::reporting::InterpreterError; 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_core::{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") @@ -839,6 +810,54 @@ mod runtime { Ok((worker, main_module)) } + pub async fn xyz( + mut worker: MainWorker, + main_module: ModuleSpecifier, + ) -> Result<(), InterpreterError> { + let wait_for_inspector = false; + // step 10 load the module into our v8 isolate + worker.execute_module(&main_module).await?; + worker.run_event_loop(wait_for_inspector).await?; + + { + 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") + .ok_or(InterpreterError::AllocationFailed)?; + v8::Local::new(scope, x).into() + }; + let v8_value = global + .get(scope, runOnInput) + .ok_or(InterpreterError::ReferenceError)?; + + // 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)?; + + let this = v8::undefined(scope).into(); + + let arg1 = { + let x = v8::String::new(scope, &string_input) + .ok_or(InterpreterError::AllocationFailed)?; + 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?; + eprintln!("finished waiting on runtime"); + + Ok(()) + } + const SPECIFIER: &str = "file://$deno$/bundle.js"; struct EmbeddedModuleLoader(String); diff --git a/src/reporting.rs b/src/reporting.rs index c63d640..e21fe86 100644 --- a/src/reporting.rs +++ b/src/reporting.rs @@ -1,8 +1,11 @@ -use deno_core::error::{type_error, AnyError}; +use deno_core::error::AnyError; use elmi; -use pretty::{self, hang, hardline, hcat, hsep, sep, space_join, Doc}; +use pretty::{self, cyan, hang, hardline, hcat, hsep, sep, space_join, Doc}; use rusty_v8; +use serde_json; +use std::cmp::max; use std::io; +use std::path::Path; use std::path::PathBuf; #[derive(Debug)] @@ -11,6 +14,19 @@ pub enum Problem { BadCompiler(CompilerError), BadSetup(SetupError), BadRuntime(InterpreterError), + Wildcard(String), +} + +impl Problem { + pub fn to_doc(&self) -> Doc { + match self { + Problem::BadTypes(err) => err.to_doc(), + Problem::BadCompiler(err) => err.to_doc(), + Problem::BadSetup(err) => err.to_doc(), + Problem::BadRuntime(err) => err.to_doc(), + Problem::Wildcard(err) => Doc::text(err), + } + } } impl From for Problem { @@ -31,6 +47,12 @@ impl From for Problem { } } +impl From for Problem { + fn from(error: InterpreterError) -> Self { + Self::BadRuntime(error) + } +} + #[derive(Debug)] pub enum TypeError { CantEvalRecord, @@ -45,16 +67,21 @@ pub enum TypeError { EvalRequiresSingleArgument(elmi::Type), } -#[derive(Debug)] -pub enum SetupError { - InstallDependencyFailed, -} - #[derive(Debug)] pub enum CompilerError { + MissingElmJson(io::Error), + MissingElmStuff(PathBuf), + CantParseModule(String), // first line + EmptyModule, + MissingModuleTypeInformation(String), + CantFindFunction(String), + FailedBuildingFixture, ElmJsonChecksumFailed(io::Error, PathBuf), ReadInputFailed(io::Error, PathBuf), + CorruptedChecksum(String), WriteOutputFailed(io::Error, PathBuf), + WriteElmJsonFailed(serde_json::Error, PathBuf), + FailedParseElmJson(serde_json::Error), FailedElmiParse(String), } @@ -62,4 +89,68 @@ pub enum CompilerError { pub enum InterpreterError { Setup(rusty_v8::DataError), EventLoop(AnyError), + AllocationFailed, + ReferenceError, +} + +impl From for InterpreterError { + fn from(error: AnyError) -> Self { + Self::EventLoop(error) + } +} + +impl From for InterpreterError { + fn from(error: rusty_v8::DataError) -> Self { + Self::Setup(error) + } +} + +impl TypeError { + pub fn to_doc(&self) -> Doc { + Doc::text("type error") + } +} + +#[derive(Debug)] +pub enum SetupError { + InstallDependencyFailed, +} + +impl SetupError { + pub fn to_doc(&self) -> Doc { + Doc::text("setup error") + } +} + +impl CompilerError { + pub fn to_doc(&self) -> Doc { + Doc::text("compiler error") + } +} + +impl InterpreterError { + pub fn to_doc(&self) -> Doc { + Doc::text("interpreter error") + } +} + +fn to_message_bar(title: String, file_path: &Path) -> Doc { + let used_space = 4 + title.len() + 1 + file_path.to_string_lossy().len(); + + cyan(Doc::text(format!( + "-- {} {} {}", + title, + std::iter::repeat('-') + .take(max(1, 80 - used_space)) + .collect::(), + file_path.display(), + ))) +} + +pub mod doc { + use pretty::Doc; + + pub fn reflow>(paragraph: S) -> Doc { + pretty::fill_words(paragraph) + } }