refactor: replace unwraps with errors

This commit is contained in:
YetAnotherMinion 2021-09-19 08:03:01 +01:00 committed by nobody
commit 287e59c566
Signed by: GrocerPublishAgent
GPG key ID: D460CD54A9E3AB86
3 changed files with 545 additions and 436 deletions

View file

@ -8,7 +8,6 @@ edition = "2018"
[dependencies] [dependencies]
structopt = { version = "0.3" } structopt = { version = "0.3" }
elmi = { path = "../../../infra/rust-elmi" } elmi = { path = "../../../infra/rust-elmi" }
uuid = { version = "0.8", features = [ "v4" ] }
ahash = "0.7" ahash = "0.7"
serde = { version = "1.0", features = [ "derive" ] } serde = { version = "1.0", features = [ "derive" ] }
serde_json = { version ="1.0", features = [] } serde_json = { version ="1.0", features = [] }

View file

@ -1,11 +1,10 @@
extern crate naive_wadler_prettier as pretty; 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 elmi::DataBinary;
use rusty_v8 as v8; use pretty::pretty;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display; use std::fmt::Display;
use std::fs::{self, canonicalize, metadata}; use std::fs::{self, canonicalize, metadata};
use std::hash::Hasher; use std::hash::Hasher;
@ -16,7 +15,6 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use structopt::StructOpt; use structopt::StructOpt;
use tokio; use tokio;
use uuid::Uuid;
mod reporting; mod reporting;
@ -30,410 +28,361 @@ fn main() {
debug, debug,
function, function,
output, output,
verbosity,
} => { } => {
let mut command = Command::new("elm"); if let Err(problem) = exec(file, debug, function, output, verbosity) {
command println!("{}", pretty(80, problem.to_doc()));
.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<HashMap<String, elmi::Interface>> {
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::<Vec<&str>>()
.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<RefCell<Option<Result<Vec<u8>, 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::<v8::Function>::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 fn exec(
// file: PathBuf,
// step 8 run cargo build debug: bool,
// function: String,
// step 9 generate a PrivateMain worker using the view function output: Option<PathBuf>,
// verbosity: u64,
// step 10 load that private main into a v8 isolate ) -> Result<(), Problem> {
// let mut command = Command::new("elm");
// step 11 load the dynamic library produced command
// .arg("make")
// step 12 invoke the dynamic library function to get the data .arg("--output")
// .arg("/dev/null")
// step 13 serialize the value .stdin(Stdio::null());
//
// step 14 put that value into the memory of the v8 if debug {
// - parse json value from string and run a generated decoder command.arg("--debug");
// - eval a script that invokes the construtor functions directly }
// command.arg(&file);
// step 15 invoke the wrapped view function
// let start = Instant::now();
// step 16 take the string output and write it to a file if provided, or stdout 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<RefCell<Option<Result<Vec<u8>, 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<HashMap<String, elmi::Interface>, 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::<Vec<&str>>()
.join(".");
eprintln!("[{:?}] parsed {:?}", Instant::now() - start, module_name);
interfaces.insert(module_name, i);
}
}
_ => (),
}
}
}
Ok(interfaces)
} }
struct PortableHash(ahash::AHasher); struct PortableHash(ahash::AHasher);
@ -485,6 +434,8 @@ enum Arguments {
debug: bool, debug: bool,
#[structopt(long)] #[structopt(long)]
output: Option<PathBuf>, output: Option<PathBuf>,
#[structopt(short = "v", parse(from_occurrences))]
verbosity: u64,
}, },
} }
@ -525,7 +476,7 @@ fn resolve_function_type(tipe: &elmi::Type) -> Result<(Option<InputType>, Output
Err(TypeError::CantEvalType(tipe.clone())) 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::TRecord(_, _) => Err(TypeError::CantEvalRecord),
elmi::Type::TUnit => Err(TypeError::CantEvalUnit), elmi::Type::TUnit => Err(TypeError::CantEvalUnit),
elmi::Type::TTuple(_, _, _) => Err(TypeError::CantEvalTuple), elmi::Type::TTuple(_, _, _) => Err(TypeError::CantEvalTuple),
@ -575,7 +526,7 @@ fn resolve_input_type(tipe: &elmi::Type) -> Result<InputType, TypeError> {
fn resolve_output_type(tipe: &elmi::Type) -> Result<OutputType, TypeError> { fn resolve_output_type(tipe: &elmi::Type) -> Result<OutputType, TypeError> {
match tipe { match tipe {
elmi::Type::TType(module_name, name, args) => { elmi::Type::TType(module_name, name, _args) => {
if module_name == "elm/core/String" && name == "String" { if module_name == "elm/core/String" && name == "String" {
Ok(OutputType::String) Ok(OutputType::String)
} else if module_name == "elm/bytes/Bytes" && name == "Bytes" { } else if module_name == "elm/bytes/Bytes" && name == "Bytes" {
@ -602,7 +553,8 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result<PathBuf, Problem>
let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc"); let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc");
if !our_temp_dir.exists() { 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 source_dir = our_temp_dir.join("src");
let bin_dir = our_temp_dir.join("bin"); let bin_dir = our_temp_dir.join("bin");
@ -612,7 +564,10 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result<PathBuf, Problem>
// cache // cache
let elm_json_path = elm_project_dir.join("elm.json"); 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(); let mut hasher = PortableHash::new();
std::io::copy(&mut elm_json_file, &mut hasher).unwrap(); std::io::copy(&mut elm_json_file, &mut hasher).unwrap();
@ -621,41 +576,58 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result<PathBuf, Problem>
let checksum_file = our_temp_dir.join("elm-json-checksum"); let checksum_file = our_temp_dir.join("elm-json-checksum");
if checksum_file.exists() { 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::<u64>() .parse::<u64>()
.unwrap(); .map_err(move |_parse_error| {
raw_checksum.truncate(60);
CompilerError::CorruptedChecksum(String::from_utf8_lossy(&raw_checksum).to_string())
})?;
if existing_checksum == checksum { if existing_checksum == checksum {
return Ok(our_temp_dir); return Ok(our_temp_dir);
} }
} }
let mut f = std::fs::File::create(checksum_file).unwrap(); let mut f = std::fs::File::create(&checksum_file)
<std::fs::File as std::io::Write>::write(&mut f, checksum.to_string().as_bytes()).unwrap(); .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, checksum_file.clone()))?;
<std::fs::File as std::io::Write>::write(&mut f, checksum.to_string().as_bytes())
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, checksum_file.clone()))?;
drop(f); drop(f);
let generator_elm_json_path = our_temp_dir.join("elm.json");
let mut generator_elm_json_file = let mut generator_elm_json_file =
std::fs::File::create(&our_temp_dir.join("elm.json")).unwrap(); std::fs::File::create(&generator_elm_json_path).map_err(|io_err| {
elm_json_file.rewind().unwrap(); 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::<ElmApplication>(&mut data).unwrap(); let mut data = std::fs::read(&elm_json_path)
// We need to modify the elm.json's source_directories field .map_err(|io_err| CompilerError::ReadInputFailed(io_err, elm_json_path.clone()))?;
let mut elm_json = serde_json::from_slice::<ElmApplication>(&mut data)
.map_err(|err| CompilerError::FailedParseElmJson(err))?;
//
let mut new_src_directories = Vec::with_capacity(elm_json.source_directories.len() + 1); let mut new_src_directories = Vec::with_capacity(elm_json.source_directories.len() + 1);
for dirname in elm_json.source_directories.iter() { 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()); new_src_directories.push(dir.to_string_lossy().to_string());
} }
std::fs::create_dir(&source_dir).unwrap(); std::fs::create_dir(&source_dir)
std::fs::create_dir(&bin_dir).unwrap(); .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( new_src_directories.push(
canonicalize(our_temp_dir.join("src")) 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_lossy()
.to_string(), .to_string(),
); );
@ -664,7 +636,9 @@ fn setup_generator_project(elm_project_dir: PathBuf) -> Result<PathBuf, Problem>
eprintln!("modified json {:?}", elm_json); 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, "ThinkAlexandria/elm-html-in-elm")?;
elm_install(&our_temp_dir, "elm/json")?; elm_install(&our_temp_dir, "elm/json")?;
@ -773,25 +747,22 @@ fn generate_fixture<S: AsRef<str>>(
} }
mod runtime { mod runtime {
use crate::reporting::InterpreterError;
use deno_core::error::{type_error, AnyError}; use deno_core::error::{type_error, AnyError};
use deno_core::futures::FutureExt; 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::deno_broadcast_channel::InMemoryBroadcastChannel;
use deno_runtime::permissions::Permissions; use deno_runtime::permissions::Permissions;
use deno_runtime::worker::MainWorker; use deno_runtime::worker::MainWorker;
use deno_runtime::worker::WorkerOptions; use deno_runtime::worker::WorkerOptions;
use deno_web::BlobStore; use deno_web::BlobStore;
use futures::future::lazy;
use rusty_v8 as v8; use rusty_v8 as v8;
use std::cell::RefCell; use std::cell::RefCell;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::path::Path;
use std::pin::Pin; use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::task::Context;
use std::time::Instant; use std::time::Instant;
use tokio::sync::mpsc::unbounded_channel;
fn get_error_class_name(e: &AnyError) -> &'static str { fn get_error_class_name(e: &AnyError) -> &'static str {
deno_runtime::errors::get_error_class_name(e).unwrap_or("Error") deno_runtime::errors::get_error_class_name(e).unwrap_or("Error")
@ -839,6 +810,54 @@ mod runtime {
Ok((worker, main_module)) 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::<v8::Function>::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"; const SPECIFIER: &str = "file://$deno$/bundle.js";
struct EmbeddedModuleLoader(String); struct EmbeddedModuleLoader(String);

View file

@ -1,8 +1,11 @@
use deno_core::error::{type_error, AnyError}; use deno_core::error::AnyError;
use elmi; 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 rusty_v8;
use serde_json;
use std::cmp::max;
use std::io; use std::io;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug)] #[derive(Debug)]
@ -11,6 +14,19 @@ pub enum Problem {
BadCompiler(CompilerError), BadCompiler(CompilerError),
BadSetup(SetupError), BadSetup(SetupError),
BadRuntime(InterpreterError), 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<TypeError> for Problem { impl From<TypeError> for Problem {
@ -31,6 +47,12 @@ impl From<SetupError> for Problem {
} }
} }
impl From<InterpreterError> for Problem {
fn from(error: InterpreterError) -> Self {
Self::BadRuntime(error)
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum TypeError { pub enum TypeError {
CantEvalRecord, CantEvalRecord,
@ -45,16 +67,21 @@ pub enum TypeError {
EvalRequiresSingleArgument(elmi::Type), EvalRequiresSingleArgument(elmi::Type),
} }
#[derive(Debug)]
pub enum SetupError {
InstallDependencyFailed,
}
#[derive(Debug)] #[derive(Debug)]
pub enum CompilerError { pub enum CompilerError {
MissingElmJson(io::Error),
MissingElmStuff(PathBuf),
CantParseModule(String), // first line
EmptyModule,
MissingModuleTypeInformation(String),
CantFindFunction(String),
FailedBuildingFixture,
ElmJsonChecksumFailed(io::Error, PathBuf), ElmJsonChecksumFailed(io::Error, PathBuf),
ReadInputFailed(io::Error, PathBuf), ReadInputFailed(io::Error, PathBuf),
CorruptedChecksum(String),
WriteOutputFailed(io::Error, PathBuf), WriteOutputFailed(io::Error, PathBuf),
WriteElmJsonFailed(serde_json::Error, PathBuf),
FailedParseElmJson(serde_json::Error),
FailedElmiParse(String), FailedElmiParse(String),
} }
@ -62,4 +89,68 @@ pub enum CompilerError {
pub enum InterpreterError { pub enum InterpreterError {
Setup(rusty_v8::DataError), Setup(rusty_v8::DataError),
EventLoop(AnyError), EventLoop(AnyError),
AllocationFailed,
ReferenceError,
}
impl From<AnyError> for InterpreterError {
fn from(error: AnyError) -> Self {
Self::EventLoop(error)
}
}
impl From<rusty_v8::DataError> 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::<String>(),
file_path.display(),
)))
}
pub mod doc {
use pretty::Doc;
pub fn reflow<S: AsRef<str>>(paragraph: S) -> Doc {
pretty::fill_words(paragraph)
}
} }