From 8354d51ecb70d5658131619349f26293fd5adc15 Mon Sep 17 00:00:00 2001 From: YetAnotherMinion Date: Sat, 11 Dec 2021 18:28:04 +0000 Subject: [PATCH] feat: prove elm object cache is complete --- Cargo.toml | 1 + examples/single-page/src/Main.elm | 9 +- src/elm.rs | 301 ++++++++++++++++++++++++++++++ src/main.rs | 167 ++--------------- src/reporting.rs | 2 +- src/timings.rs | 6 +- src/transpile.rs | 254 +++++++++++++++++++++++++ 7 files changed, 583 insertions(+), 157 deletions(-) create mode 100644 src/elm.rs diff --git a/Cargo.toml b/Cargo.toml index a2a5d2f..613dc77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ structopt = { version = "0.3" } elm-project-utils = { path = "../../../infra/rust-elm-project-utils" } tracing = { version = "0.1", features = [] } rustc-hash = "1.1" +home = "0.5" # Required to transpile view functions to Rust genco = "0.15" diff --git a/examples/single-page/src/Main.elm b/examples/single-page/src/Main.elm index 7c82fa7..d038c27 100644 --- a/examples/single-page/src/Main.elm +++ b/examples/single-page/src/Main.elm @@ -12,15 +12,16 @@ type alias Model = , b : Array Int } -view2 : String -> Svg msg -view2 model = - svg [] [] - view : String -> Html msg view model = div [] [ text <| "Hello world" ++ model ] +view2 : String -> Svg msg +view2 model = + svg [] [] + + view3: Bytes -> Html msg view3 model = case diff --git a/src/elm.rs b/src/elm.rs new file mode 100644 index 0000000..693960c --- /dev/null +++ b/src/elm.rs @@ -0,0 +1,301 @@ +use crate::reporting::{CompilerError, Problem}; +use elmi::DataBinary; +use std::collections::HashMap; +use std::fs::{self, canonicalize, metadata}; +use std::io::{self, BufRead, Error, ErrorKind, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command}; +use tracing::info_span; + +pub fn make(file: &Path, debug: bool, verbosity: u64) -> Result<(), Problem> { + let mut command = Command::new("elm"); + command.arg("make").arg("--output").arg("/dev/null"); + + if debug { + command.arg("--debug"); + } + command.arg(file); + + let span = info_span!("elm make target file"); + let timing_guard = span.enter(); + match command.output() { + Ok(output) => { + if !output.status.success() { + io::stderr().write_all(&output.stderr).unwrap(); + io::stderr().write_all(&output.stdout).unwrap(); + return Err(CompilerError::FailedBuildingFixture.into()); + } + if verbosity > 0 { + io::stderr().write_all(&output.stderr).unwrap(); + } + } + Err(_io_err) => { + // TODO handle this one + return Err(Problem::Wildcard("elm failed".into())); + } + } + drop(timing_guard); + + Ok(()) +} + +pub 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 parse_span = info_span!("parse elmi"); + let _parse_guard = parse_span.enter(); + + let load_span = info_span!("file reads"); + let load_guard = load_span.enter(); + let data = std::fs::read(&path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; + drop(load_guard); + + 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("."); + interfaces.insert(module_name, i); + } + } + _ => (), + } + } + } + + Ok(interfaces) +} + +pub fn load_objects(elm_cache_dir: &Path) -> Result, Problem> { + let mut objects = 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.file_name() { + Some(file_name) if file_name == "o.dat" => { + let data = std::fs::read(&path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; + + let (_remaining, opt_global_graph) = elmi::OptGlobalGraph::get(&data).map_err(|_err| { + CompilerError::FailedElmiParse("todo elmi parsing".to_owned()) + })?; + + objects.extend(opt_global_graph.nodes.into_iter()); + } + _ => {}, + } + match path.extension() { + Some(ext) if ext == "elmo" => { + // step 4 load all the modules + let parse_span = info_span!("parse elmo"); + let _parse_guard = parse_span.enter(); + + let load_span = info_span!("file reads"); + let load_guard = load_span.enter(); + let data = std::fs::read(&path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; + drop(load_guard); + + let (_remaining, opt_local_graph) = elmi::OptLocalGraph::get(&data).map_err(|_err| { + CompilerError::FailedElmiParse("todo elmi parsing".to_owned()) + })?; + + objects.extend(opt_local_graph.nodes.into_iter()); + } + _ => (), + } + } + } + + Ok(objects) +} + +pub struct SymbolLoader { + project_cache_directory: PathBuf, + global_cache_directory: PathBuf, + symbol_cache: HashMap, +} + +impl SymbolLoader { + pub fn try_load(&mut self, symbol: elmi::Global) -> Result { + if self.symbol_cache.contains_key(&symbol) { + return Ok(true); + } + + let elmi::Global( + elmi::ModuleNameCanonical { + ref package, + ref module, + }, + ref _identifier, + ) = symbol; + + if *package == elmi::PackageName::new("author", "project") { + let filename = module.0.split('.').collect::>().join("-"); + let mut path = self.project_cache_directory.join(filename); + path.set_extension(".elmo"); + } else { + let mut path = self.global_cache_directory.clone(); + path.push(&package.author); + path.push(&package.project); + // TODO figure out the version required. + //path.push(version); + path.push("artifacts.dat"); + } + + Ok(false) + } +} + +//pub fn load_symbol_table(elm_cache_dir: &Path) -> Result, Problem> { +// // given a global symbol, calculate the folder that contains the cache file. +// let (_remaining, i) = OptLocalGraph::get(&data).unwrap(); +// +// 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 parse_span = info_span!("parse elmi"); +// let _parse_guard = parse_span.enter(); +// +// let load_span = info_span!("file reads"); +// let load_guard = load_span.enter(); +// let data = std::fs::read(&path) +// .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; +// drop(load_guard); +// +// 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("."); +// interfaces.insert(module_name, i); +// } +// } +// _ => (), +// } +// } +// } +// +// Ok(interfaces) +//} + +/// 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) +pub 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(); + } +} + +pub fn get_elm_home() -> io::Result { + match std::env::var("ELM_HOME") { + Ok(elm_home) => Ok(PathBuf::from(elm_home)), + Err(_) => { + if let Some(mut dir) = home::home_dir() { + dir.push(".elm"); + Ok(dir) + } else { + Err(std::io::Error::new( + ErrorKind::NotFound, + "ELM_HOME not set and home directory could not be calculated by `home` crate", + )) + } + } + } +} + +pub fn parse_module_name(data: &[u8]) -> Result { + 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() { + Ok(elmi::Name(module_name.to_string())) + } else { + Err(CompilerError::CantParseModule(first_line.clone()).into()) + } + } else { + Err(CompilerError::EmptyModule.into()) + } +} diff --git a/src/main.rs b/src/main.rs index 77664ae..7cd9ae3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,18 +7,18 @@ use os_pipe::dup_stderr; use pretty::pretty; use serde::{Deserialize, Serialize}; use std::cell::RefCell; -use std::collections::HashMap; -use std::fs::{self, canonicalize, metadata}; +use std::fs::{self, canonicalize}; use std::hash::Hasher; -use std::io::{self, BufRead, Error, ErrorKind, Read, Write}; +use std::io::{self, Read, 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 tracing::{info_span, span, Instrument}; +use tracing::info_span; +mod elm; mod fixture; mod reporting; mod timings; @@ -75,8 +75,9 @@ fn main() { file, function, verbosity, + debug, } => { - println!("todo transpile the code") + transpile::transpile(file, debug, function, verbosity).unwrap(); } } } @@ -91,41 +92,11 @@ fn exec( ) -> Result<(), Problem> { // Our first elm make call is where we build the users program. There is a pretty good chance // this won't work. - let mut command = Command::new("elm"); - command.arg("make").arg("--output").arg("/dev/null"); - //.stdin(Stdio::null()); - //.stdout(Stdio::null()); - // I am using the default inherit stderr behavior here. - //.stderr(Stdio::piped()); - - if debug { - command.arg("--debug"); - } - command.arg(&file); - - let span = info_span!("elm make target file"); - let timing_guard = span.enter(); - match command.output() { - Ok(output) => { - if !output.status.success() { - io::stderr().write_all(&output.stderr).unwrap(); - io::stderr().write_all(&output.stdout).unwrap(); - return Err(CompilerError::FailedBuildingFixture.into()); - } - if verbosity > 0 { - io::stderr().write_all(&output.stderr).unwrap(); - } - } - Err(_io_err) => { - // TODO handle this one - return Err(Problem::Wildcard("elm failed".into())); - } - } - drop(timing_guard); + elm::make(&file, debug, verbosity)?; // Step 2, find the elm.json and elm-stuff directory let elm_project_dir = - find_project_root("elm.json", "./").map_err(CompilerError::MissingElmJson)?; + elm::find_project_root("elm.json", "./").map_err(CompilerError::MissingElmJson)?; let elm_cache_dir = elm_project_dir.join("elm-stuff").join("0.19.1"); @@ -144,31 +115,10 @@ fn exec( 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()); - }; + let elmi::Name(target_module) = elm::parse_module_name(&data)?; // step 3 find all the filepaths in the elm-stuff/0.19.1/* folder - let interfaces = load_interfaces(&elm_cache_dir)?; + let interfaces = elm::load_interfaces(&elm_cache_dir)?; // Step 5, check for the desired function let span = info_span!("resolved target function"); @@ -260,7 +210,7 @@ fn exec( // `"REPLACE_ME" + "abc" * 2000` we would 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 = (|| { + let final_script = (|| { let mut final_script = data .replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)") //.replace("setTimeout", "(function(x, y) { globalThis.__bootstrap.timers.setTimeout(x, y)})") @@ -455,7 +405,7 @@ fn exec( } }; - let start = Instant::now(); + let _start = Instant::now(); let span = info_span!("create tokio runtime"); let timing_guard = span.enter(); let sys = tokio::runtime::Builder::new_current_thread() @@ -498,55 +448,6 @@ fn exec( 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 parse_span = info_span!("parse elmi"); - let parse_guard = parse_span.enter(); - - let load_span = info_span!("file reads"); - let load_guard = load_span.enter(); - let data = std::fs::read(&path) - .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; - drop(load_guard); - - 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("."); - interfaces.insert(module_name, i); - } - } - _ => (), - } - } - } - - Ok(interfaces) -} - struct PortableHash(ahash::AHasher); impl PortableHash { @@ -606,6 +507,8 @@ enum Arguments { function: String, #[structopt(short = "v", parse(from_occurrences))] verbosity: u64, + #[structopt(long)] + debug: bool, }, } @@ -859,7 +762,7 @@ mod runtime { use crate::ValidatedInput; use deno_core::error::{type_error, AnyError}; use deno_core::futures::FutureExt; - use deno_core::{resolve_url, FsModuleLoader, ModuleLoader, ModuleSpecifier, OpState}; + use deno_core::{resolve_url, FsModuleLoader, ModuleLoader, ModuleSpecifier}; use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; use deno_runtime::permissions::Permissions; use deno_runtime::worker::MainWorker; @@ -867,7 +770,7 @@ mod runtime { use deno_runtime::BootstrapOptions; use deno_web::BlobStore; use rusty_v8 as v8; - use std::cell::RefCell; + use std::convert::TryFrom; use std::pin::Pin; use std::rc::Rc; @@ -922,7 +825,7 @@ mod runtime { // let main_module = resolve_url(SPECIFIER)?; let permissions = Permissions::allow_all(); - let mut worker = MainWorker::from_options(main_module.clone(), permissions, options); + let worker = MainWorker::from_options(main_module.clone(), permissions, options); //worker.bootstrap(&options); Ok((worker, main_module)) @@ -963,7 +866,7 @@ mod runtime { let this = v8::undefined(scope).into(); - let start = Instant::now(); + let _start = Instant::now(); let span = info_span!("dispatch v8 call"); let timing_guard = span.enter(); match input { @@ -1215,37 +1118,3 @@ pub mod generated { } } } - -/// 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(); - } -} diff --git a/src/reporting.rs b/src/reporting.rs index 1862809..1e1be96 100644 --- a/src/reporting.rs +++ b/src/reporting.rs @@ -179,7 +179,7 @@ impl CompilerError { impl InterpreterError { pub fn to_doc(&self) -> Doc { - let mut title = "COMPILER ERROR"; + let title = "COMPILER ERROR"; use InterpreterError::*; let message = match self { Setup(data_error) => Doc::text(format!("{:?}", data_error)), diff --git a/src/timings.rs b/src/timings.rs index 4f79e12..3ab297c 100644 --- a/src/timings.rs +++ b/src/timings.rs @@ -12,9 +12,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::thread::{self, ThreadId}; use std::time::{Duration, Instant}; -use tracing::info_span; + use tracing::span::{Attributes, Record}; -use tracing::{Event, Id, Metadata, Subscriber}; +use tracing::{Event, Id, Metadata}; // // Each step will be described with a less than 100 char string. Steps will overlap. @@ -82,7 +82,7 @@ impl ReportHandle { } impl tracing::Subscriber for Timings { - fn enabled(&self, metadata: &Metadata<'_>) -> bool { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { true } diff --git a/src/transpile.rs b/src/transpile.rs index 8b13789..10a4b40 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -1 +1,255 @@ +use crate::reporting::{CompilerError, Problem, TypeError}; +use elmi::DataBinary; +use std::collections::HashMap; +use std::path::{PathBuf}; +use tracing::info_span; +use crate::elm; +pub fn transpile( + file: PathBuf, + debug: bool, + function: String, + //output: Option, + verbosity: u64, +) -> Result<(), Problem> { + // Our first elm make call is where we build the users program. There is a pretty good chance + // this won't work. + elm::make(&file, debug, verbosity)?; + + // step 2 find the elm artifacts cache directory just like with exec + let elm_project_dir = + elm::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 elmi::Name(target_module) = elm::parse_module_name(&data)?; + + // Side note: the transpile function really should be taking a list of functions in modules and + // transpiling the entire forest of dependencies. This would allow avoiding kernel javascript + // used in the project. So I would want to reduce the tuples of file function into a set of + // files to load. Then recursively load all dependencies. + + // step 3 find all the filepaths in the elm-stuff/0.19.1/* folder + let interfaces = elm::load_interfaces(&elm_cache_dir)?; + + // Step 4, check for the desired functions have types that we can compile. + let span = info_span!("resolved target function"); + let timing_guard = span.enter(); + match interfaces.get(&target_module) { + Some(interface) => match interface.values.get(&elmi::Name::from(&function)) { + Some(annotation) => { + let elmi::CannonicalAnnotation(_free_vars, tipe) = annotation; + + validate_function_type(tipe)? + } + None => return Err(CompilerError::BadImport(target_module, function).into()), + }, + None => return Err(CompilerError::MissingModuleTypeInformation(target_module).into()), + }; + drop(timing_guard); + + // all the symbols in author/project will be found in the elm_cache_dir, while the rest will be + // found in the elm_home_dir + + // Step 5, recursively load all the symbols from the ~/.elm stuff artifacts.dat + println!("ok the function was acceptable, ready to build symbol table"); + + let objects = elm::load_objects(&elm_cache_dir)?; + + let entrypoint = elmi::Global( + elmi::ModuleNameCanonical { + package: elmi::PackageName::new("author", "project"), + module: elmi::Name(target_module.clone()), + }, + elmi::Name(function.clone()), + ); + + println!("the artifacts has the symbol {}", objects.contains_key(&entrypoint)); + //let visited: HashSet = HashSet::new(); + for (_key, node) in objects.iter() { + //println!("key {}", key); + match node { + elmi::Node::Define(_expr, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + } + elmi::Node::DefineTailFunc(_, _, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + } + elmi::Node::Ctor(_, _) => { + } + elmi::Node::Enum(_) => { + } + elmi::Node::Box => { + } + elmi::Node::Link(dep) => { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + elmi::Node::Cycle(_, _, _, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + } + elmi::Node::Manager(_) => (), + elmi::Node::Kernel(_, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + } + elmi::Node::PortIncoming(_, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + } + elmi::Node::PortOutgoing(_, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { + println!("could not find dep {}", dep); + } + } + } + } + } + + let _symbol_table: HashMap = HashMap::new(); + + // step 6, start generating rust code using a tree visitor on each of the entry points. + // Accumulate the contents of each rust module in map. + // + // step 6a decide on the name mangling rules for elm indentifiers to rust identifiers. I can't + // use the $ in identifiers. Maybe I could use Z. Not sure why I can't use underscore. + // author_project__module_name__identifier? + // + // step 6b figure out how to handle partial apply, one approach requires me to track the arity + // of each function call and generate a custom closure for each partial apply. + + // step 7 write out each of the generated rust modulues. + Ok(()) +} + +fn validate_function_type(tipe: &elmi::Type) -> Result<(), TypeError> { + 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. I only want to accept functions with a single + // argument to keep the complexity down while I figure out how to map over to rust. I + // am specifically worried about implementing partial application in a performant way. + // Nested partial application worries me. + validate_output_type(&**b)?; + validate_input_type(&**a)?; + Ok(()) + } + elmi::Type::TVar(_) => Err(TypeError::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(()); + } + if module_name == "elm/bytes/Bytes" && name == "Bytes" { + return Ok(()); + } + + Err(TypeError::CantEvalType(tipe.clone())) + } + elmi::Type::TType(_, _, _) => Err(TypeError::CantEvalCustomType), + elmi::Type::TRecord(_, _) => Err(TypeError::CantEvalRecord), + elmi::Type::TUnit => Err(TypeError::CantEvalUnit), + elmi::Type::TTuple(_, _, _) => Err(TypeError::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 + // ``` + validate_function_type(tipe) + } + elmi::AliasType::Holey(_) => return Err(TypeError::CantEvalHoleyAlias), + } + } + } +} + +fn validate_input_type(tipe: &elmi::Type) -> Result<(), TypeError> { + match tipe { + elmi::Type::TLambda(_, _) => Err(TypeError::EvalRequiresSingleArgument(tipe.clone())), + elmi::Type::TType(module_name, name, args) if args.is_empty() => { + if module_name == "elm/core/String" && name == "String" { + Ok(()) + } else if module_name == "elm/bytes/Bytes" && name == "Bytes" { + Ok(()) + } else if module_name == "elm/json/Json.Encode" && name == "Value" { + Ok(()) + } else { + Err(TypeError::InputTypeNotSupported(tipe.clone())) + } + } + elmi::Type::TAlias(_, _, _, ref alias) => match &**alias { + elmi::AliasType::Filled(tipe) => validate_input_type(tipe), + elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias), + }, + _ => Err(TypeError::OutputTypeNotSupported(tipe.clone())), + } +} + +fn validate_output_type(tipe: &elmi::Type) -> Result<(), TypeError> { + match tipe { + elmi::Type::TType(module_name, name, _args) => { + if module_name == "elm/core/String" && name == "String" { + Ok(()) + } else if module_name == "elm/bytes/Bytes" && name == "Bytes" { + Ok(()) + } else if module_name == "elm/json/Json.Encode" && name == "Value" { + Ok(()) + } else if module_name == "elm/virtual-dom/VirtualDom" && name == "Node" { + Ok(()) + } else { + Err(TypeError::OutputTypeNotSupported(tipe.clone())) + } + } + elmi::Type::TAlias(_, _, _, ref alias) => match &**alias { + elmi::AliasType::Filled(tipe) => validate_output_type(tipe), + elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias), + }, + _ => Err(TypeError::OutputTypeNotSupported(tipe.clone())), + } +} + +// Figure out how to do structural types. If I could name mangle all the functions I could write +// them out in the same namespace as lambdas which would avoid the structural typing problem if the +// lambda was used by one type. Monomorphism. But if the lambda is used by multiple types then I +// would either need to narrow the type into a tuple at the call site, generate a specialize struct +// and borrow all the children. +// +// +// The question is should I use Rc with immutable datastructures? Or should I try to statically +// analyse copies and use mutable state when possible.