feat: prove elm object cache is complete
This commit is contained in:
parent
0a5e25e4f0
commit
8354d51ecb
7 changed files with 583 additions and 157 deletions
|
|
@ -21,6 +21,7 @@ structopt = { version = "0.3" }
|
||||||
elm-project-utils = { path = "../../../infra/rust-elm-project-utils" }
|
elm-project-utils = { path = "../../../infra/rust-elm-project-utils" }
|
||||||
tracing = { version = "0.1", features = [] }
|
tracing = { version = "0.1", features = [] }
|
||||||
rustc-hash = "1.1"
|
rustc-hash = "1.1"
|
||||||
|
home = "0.5"
|
||||||
|
|
||||||
# Required to transpile view functions to Rust
|
# Required to transpile view functions to Rust
|
||||||
genco = "0.15"
|
genco = "0.15"
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,16 @@ type alias Model =
|
||||||
, b : Array Int
|
, b : Array Int
|
||||||
}
|
}
|
||||||
|
|
||||||
view2 : String -> Svg msg
|
|
||||||
view2 model =
|
|
||||||
svg [] []
|
|
||||||
|
|
||||||
view : String -> Html msg
|
view : String -> Html msg
|
||||||
view model =
|
view model =
|
||||||
div []
|
div []
|
||||||
[ text <| "Hello world" ++ model ]
|
[ text <| "Hello world" ++ model ]
|
||||||
|
|
||||||
|
view2 : String -> Svg msg
|
||||||
|
view2 model =
|
||||||
|
svg [] []
|
||||||
|
|
||||||
|
|
||||||
view3: Bytes -> Html msg
|
view3: Bytes -> Html msg
|
||||||
view3 model =
|
view3 model =
|
||||||
case
|
case
|
||||||
|
|
|
||||||
301
src/elm.rs
Normal file
301
src/elm.rs
Normal file
|
|
@ -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<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 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::<Vec<&str>>()
|
||||||
|
.join(".");
|
||||||
|
interfaces.insert(module_name, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(interfaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_objects(elm_cache_dir: &Path) -> Result<HashMap<elmi::Global, elmi::Node>, 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<elmi::Global, elmi::Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolLoader {
|
||||||
|
pub fn try_load(&mut self, symbol: elmi::Global) -> Result<bool, Problem> {
|
||||||
|
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::<Vec<&str>>().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<HashMap<String, elmi::Interface>, 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::<Vec<&str>>()
|
||||||
|
// .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<S: AsRef<Path>>(filename: S, search: S) -> io::Result<PathBuf> {
|
||||||
|
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<PathBuf> {
|
||||||
|
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<elmi::Name, CompilerError> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/main.rs
167
src/main.rs
|
|
@ -7,18 +7,18 @@ use os_pipe::dup_stderr;
|
||||||
use pretty::pretty;
|
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::fs::{self, canonicalize};
|
||||||
use std::fs::{self, canonicalize, metadata};
|
|
||||||
use std::hash::Hasher;
|
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::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use tokio;
|
use tokio;
|
||||||
use tracing::{info_span, span, Instrument};
|
use tracing::info_span;
|
||||||
|
|
||||||
|
mod elm;
|
||||||
mod fixture;
|
mod fixture;
|
||||||
mod reporting;
|
mod reporting;
|
||||||
mod timings;
|
mod timings;
|
||||||
|
|
@ -75,8 +75,9 @@ fn main() {
|
||||||
file,
|
file,
|
||||||
function,
|
function,
|
||||||
verbosity,
|
verbosity,
|
||||||
|
debug,
|
||||||
} => {
|
} => {
|
||||||
println!("todo transpile the code")
|
transpile::transpile(file, debug, function, verbosity).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,41 +92,11 @@ fn exec(
|
||||||
) -> Result<(), Problem> {
|
) -> Result<(), Problem> {
|
||||||
// Our first elm make call is where we build the users program. There is a pretty good chance
|
// Our first elm make call is where we build the users program. There is a pretty good chance
|
||||||
// this won't work.
|
// this won't work.
|
||||||
let mut command = Command::new("elm");
|
elm::make(&file, debug, verbosity)?;
|
||||||
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);
|
|
||||||
|
|
||||||
// Step 2, find the elm.json and elm-stuff directory
|
// Step 2, find the elm.json and elm-stuff directory
|
||||||
let elm_project_dir =
|
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");
|
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();
|
let source_checksum = hasher.finish();
|
||||||
|
|
||||||
// step 2.5 get the module name out of the file.
|
// step 2.5 get the module name out of the file.
|
||||||
let target_module = if let Some(Ok(first_line)) = data.lines().next() {
|
let elmi::Name(target_module) = elm::parse_module_name(&data)?;
|
||||||
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
|
// 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
|
// Step 5, check for the desired function
|
||||||
let span = info_span!("resolved target 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
|
// `"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
|
// will have to do some extra book keeping to make sure the buffer space is big enough
|
||||||
// for the replacement code.
|
// for the replacement code.
|
||||||
let mut final_script = (|| {
|
let final_script = (|| {
|
||||||
let mut final_script = data
|
let mut final_script = data
|
||||||
.replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)")
|
.replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)")
|
||||||
//.replace("setTimeout", "(function(x, y) { globalThis.__bootstrap.timers.setTimeout(x, y)})")
|
//.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 span = info_span!("create tokio runtime");
|
||||||
let timing_guard = span.enter();
|
let timing_guard = span.enter();
|
||||||
let sys = tokio::runtime::Builder::new_current_thread()
|
let sys = tokio::runtime::Builder::new_current_thread()
|
||||||
|
|
@ -498,55 +448,6 @@ fn exec(
|
||||||
Ok(())
|
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 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::<Vec<&str>>()
|
|
||||||
.join(".");
|
|
||||||
interfaces.insert(module_name, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(interfaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PortableHash(ahash::AHasher);
|
struct PortableHash(ahash::AHasher);
|
||||||
|
|
||||||
impl PortableHash {
|
impl PortableHash {
|
||||||
|
|
@ -606,6 +507,8 @@ enum Arguments {
|
||||||
function: String,
|
function: String,
|
||||||
#[structopt(short = "v", parse(from_occurrences))]
|
#[structopt(short = "v", parse(from_occurrences))]
|
||||||
verbosity: u64,
|
verbosity: u64,
|
||||||
|
#[structopt(long)]
|
||||||
|
debug: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -859,7 +762,7 @@ mod runtime {
|
||||||
use crate::ValidatedInput;
|
use crate::ValidatedInput;
|
||||||
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::{resolve_url, FsModuleLoader, ModuleLoader, ModuleSpecifier, OpState};
|
use deno_core::{resolve_url, FsModuleLoader, ModuleLoader, ModuleSpecifier};
|
||||||
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;
|
||||||
|
|
@ -867,7 +770,7 @@ mod runtime {
|
||||||
use deno_runtime::BootstrapOptions;
|
use deno_runtime::BootstrapOptions;
|
||||||
use deno_web::BlobStore;
|
use deno_web::BlobStore;
|
||||||
use rusty_v8 as v8;
|
use rusty_v8 as v8;
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
@ -922,7 +825,7 @@ mod runtime {
|
||||||
// let main_module = resolve_url(SPECIFIER)?;
|
// let main_module = resolve_url(SPECIFIER)?;
|
||||||
let permissions = Permissions::allow_all();
|
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);
|
//worker.bootstrap(&options);
|
||||||
|
|
||||||
Ok((worker, main_module))
|
Ok((worker, main_module))
|
||||||
|
|
@ -963,7 +866,7 @@ mod runtime {
|
||||||
|
|
||||||
let this = v8::undefined(scope).into();
|
let this = v8::undefined(scope).into();
|
||||||
|
|
||||||
let start = Instant::now();
|
let _start = Instant::now();
|
||||||
let span = info_span!("dispatch v8 call");
|
let span = info_span!("dispatch v8 call");
|
||||||
let timing_guard = span.enter();
|
let timing_guard = span.enter();
|
||||||
match input {
|
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<S: AsRef<Path>>(filename: S, search: S) -> io::Result<PathBuf> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ impl CompilerError {
|
||||||
|
|
||||||
impl InterpreterError {
|
impl InterpreterError {
|
||||||
pub fn to_doc(&self) -> Doc {
|
pub fn to_doc(&self) -> Doc {
|
||||||
let mut title = "COMPILER ERROR";
|
let title = "COMPILER ERROR";
|
||||||
use InterpreterError::*;
|
use InterpreterError::*;
|
||||||
let message = match self {
|
let message = match self {
|
||||||
Setup(data_error) => Doc::text(format!("{:?}", data_error)),
|
Setup(data_error) => Doc::text(format!("{:?}", data_error)),
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::thread::{self, ThreadId};
|
use std::thread::{self, ThreadId};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tracing::info_span;
|
|
||||||
use tracing::span::{Attributes, Record};
|
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.
|
// 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 {
|
impl tracing::Subscriber for Timings {
|
||||||
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
|
fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
254
src/transpile.rs
254
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<PathBuf>,
|
||||||
|
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<Global> = 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<elmi::Global, elmi::Node> = 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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue