refactor: move some setup to elm-project-utils crate
This commit is contained in:
parent
6790eec12c
commit
d7981129eb
6 changed files with 791 additions and 821 deletions
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# I decided to use os_pipe becaues I want to pipe stdout of a subprocess into
|
# I decided to use os_pipe becaues I want to pipe stdout of a subprocess into
|
||||||
# stderr in real time. I want the outupt of the process to stderr and stdout
|
# stderr in real time. I want the output of the process to stderr and stdout
|
||||||
# show up in the original order. I looked an os_pipe uses some unsafe code to
|
# show up in the original order. I looked an os_pipe uses some unsafe code to
|
||||||
# duplicate file descriptors. I am unfamiliar with the correct way of sharing
|
# duplicate file descriptors. I am unfamiliar with the correct way of sharing
|
||||||
# the fds. Therefore I am going to trust that their unsafe code is necessary.
|
# the fds. Therefore I am going to trust that their unsafe code is necessary.
|
||||||
|
|
|
||||||
18
src/exec/astrid_pages.rs
Normal file
18
src/exec/astrid_pages.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use crate::reporting::{Problem};
|
||||||
|
use std::path::{PathBuf};
|
||||||
|
|
||||||
|
pub(crate) fn run(
|
||||||
|
_debug: bool,
|
||||||
|
_verbosity: u64,
|
||||||
|
_sqlite_path: Option<PathBuf>,
|
||||||
|
_elm_project_dir: PathBuf,
|
||||||
|
_source_checksum: u64,
|
||||||
|
_entrypoint: elmi::Global,
|
||||||
|
_output: Option<PathBuf>,
|
||||||
|
) -> Result<(), Problem> {
|
||||||
|
// step 6 create our private project
|
||||||
|
// let generator_dir = setup_generator_project(verbosity, elm_project_dir.clone())?;
|
||||||
|
|
||||||
|
eprintln!("todo implement astrid pages Route mode");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::exec::{InputType, OutputType};
|
use crate::exec::scripting::{InputType, OutputType};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
pub(crate) fn generate<S: AsRef<str>>(
|
pub(crate) fn generate<S: AsRef<str>>(
|
||||||
|
|
|
||||||
861
src/exec/mod.rs
861
src/exec/mod.rs
|
|
@ -1,41 +1,14 @@
|
||||||
use crate::elm;
|
use crate::elm;
|
||||||
use crate::reporting::{CompilerError, InterpreterError, Problem, SetupError, TypeError};
|
use crate::reporting::{CompilerError, Problem};
|
||||||
use crate::PortableHash;
|
use crate::PortableHash;
|
||||||
use deno_core::futures::StreamExt;
|
|
||||||
use elm_project_utils::ChecksumConstraint;
|
|
||||||
use os_pipe::dup_stderr;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::sqlite::SqlitePool;
|
|
||||||
use sqlx::Row;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::fs::{self, canonicalize};
|
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use tracing::{info_span};
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Instant;
|
|
||||||
use tokio;
|
|
||||||
use tracing::{info_span, Instrument};
|
|
||||||
|
|
||||||
|
mod astrid_pages;
|
||||||
mod fixtures;
|
mod fixtures;
|
||||||
|
mod scripting;
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(tag = "$")]
|
|
||||||
pub(crate) enum ElmResult<T, E> {
|
|
||||||
Ok { a: T },
|
|
||||||
Err { a: E },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> ElmResult<T, E> {
|
|
||||||
fn ok(a: T) -> Self {
|
|
||||||
Self::Ok { a }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn err(a: E) -> Self {
|
|
||||||
Self::Err { a }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn exec(
|
pub(crate) fn exec(
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
|
|
@ -86,12 +59,17 @@ pub(crate) fn exec(
|
||||||
// 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");
|
||||||
let timing_guard = span.enter();
|
let timing_guard = span.enter();
|
||||||
let (input_type, output_type) = match interfaces.get(&entrypoint.0) {
|
let mode = match interfaces.get(&entrypoint.0) {
|
||||||
Some(interface) => match interface.values.get(&entrypoint.1) {
|
Some(interface) => match interface.values.get(&entrypoint.1) {
|
||||||
Some(annotation) => {
|
Some(annotation) => {
|
||||||
let elmi::CannonicalAnnotation(_free_vars, tipe) = annotation;
|
let elmi::CannonicalAnnotation(_free_vars, tipe) = annotation;
|
||||||
|
|
||||||
resolve_function_type(tipe)?
|
if is_astrid_pages_route(tipe) {
|
||||||
|
ExecutionMode::AstridPagesRoute
|
||||||
|
} else {
|
||||||
|
let (input_type, output_type) = scripting::resolve_function_type(tipe)?;
|
||||||
|
ExecutionMode::Scripting(input_type, output_type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => return Err(CompilerError::BadImport(entrypoint).into()),
|
None => return Err(CompilerError::BadImport(entrypoint).into()),
|
||||||
},
|
},
|
||||||
|
|
@ -99,736 +77,46 @@ pub(crate) fn exec(
|
||||||
};
|
};
|
||||||
drop(timing_guard);
|
drop(timing_guard);
|
||||||
|
|
||||||
// step 6 create our private project
|
match mode {
|
||||||
let generator_dir = setup_generator_project(verbosity, elm_project_dir.clone())?;
|
ExecutionMode::Scripting(input_type, output_type) => scripting::run(
|
||||||
|
debug,
|
||||||
// step 7 create an Elm fixture file to run our function
|
verbosity,
|
||||||
let span = info_span!("create Elm fixture files");
|
sqlite_path,
|
||||||
let timing_guard = span.enter();
|
elm_project_dir,
|
||||||
let (gen_module_name, source) = fixtures::scripting::generate(
|
|
||||||
source_checksum,
|
source_checksum,
|
||||||
entrypoint.0.module.clone(),
|
entrypoint,
|
||||||
entrypoint.1.clone(),
|
|
||||||
input_type,
|
input_type,
|
||||||
output_type,
|
output_type,
|
||||||
);
|
input_source,
|
||||||
|
output,
|
||||||
let mut source_filename = generator_dir.join("src").join(&gen_module_name);
|
),
|
||||||
source_filename.set_extension("elm");
|
ExecutionMode::AstridPagesRoute => astrid_pages::run(
|
||||||
|
debug,
|
||||||
let span = info_span!("file writes");
|
verbosity,
|
||||||
let file_write_timing_guard = span.enter();
|
sqlite_path,
|
||||||
fs::File::create(&source_filename)
|
elm_project_dir,
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?
|
source_checksum,
|
||||||
.write_all(source.as_bytes())
|
entrypoint,
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?;
|
output,
|
||||||
drop(file_write_timing_guard);
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
// 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())
|
|
||||||
.stderr(Stdio::piped());
|
|
||||||
if verbosity < 1 {
|
|
||||||
command.stdout(Stdio::piped());
|
|
||||||
} else {
|
|
||||||
let pipe = dup_stderr()
|
|
||||||
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, "stdout".into()))?;
|
|
||||||
command.stdout(pipe);
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
command.arg("--debug");
|
|
||||||
}
|
|
||||||
command.arg(&source_filename);
|
|
||||||
|
|
||||||
let span = info_span!("elm make fixture file");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
match command.output() {
|
|
||||||
Ok(output) => {
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(CompilerError::FailedBuildingFixture.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
return Err(CompilerError::FailedBuildingFixture.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
// 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 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 span = info_span!("munge fixture javascript");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
let final_script = (|| {
|
|
||||||
let mut final_script = data
|
|
||||||
.replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)")
|
|
||||||
.replace(
|
|
||||||
"$elm$json$Json$Decode$fail('REPLACE_ME_WITH_BYTES_DECODER');",
|
|
||||||
r#" _Json_decodePrim(function(value) {
|
|
||||||
return (typeof value === 'object' && value instanceof DataView)
|
|
||||||
? $elm$core$Result$Ok(value)
|
|
||||||
: _Json_expecting('a DataView', value);
|
|
||||||
});"#,
|
|
||||||
)
|
|
||||||
.replace(";}(this));", ";}(globalThis));");
|
|
||||||
|
|
||||||
if sqlite_path.is_some() {
|
|
||||||
final_script = final_script
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$execute = function (query) {\n\treturn $author$project$Astrid$Query$dummyExecute;\n};",
|
|
||||||
include_str!("fixtures/sql-client-integration.js"),
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$fetch = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
|
||||||
"var $author$project$Astrid$Query$fetch = _Query_fetchAll;",
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$fetchOne = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
|
||||||
"var $author$project$Astrid$Query$fetchOne = _Query_fetchOne;",
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$map3 = F4(\n\tfunction (f, a, b, c) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
|
||||||
r#"var $author$project$Astrid$Query$map3 = _Query_map3;"#,
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$map2 = F3(\n\tfunction (f, a, b) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
|
||||||
r#"var $author$project$Astrid$Query$map2 = _Query_map2;"#,
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$map = F2(\n\tfunction (f, a) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
|
||||||
r#"var $author$project$Astrid$Query$map = _Query_map1;"#,
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"var $author$project$Astrid$Query$andThen = F2(\n\tfunction (f, q) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
|
||||||
r#"var $author$project$Astrid$Query$andThen = _Query_andThen;"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
// final_script.replace("var $author$project$Astrid$Query$run = ", "JSON.stringify(x)");
|
|
||||||
final_script.push_str("\n\n");
|
|
||||||
//final_script.push_str(r#"
|
|
||||||
// Deno.core.print(JSON.stringify(
|
|
||||||
// Deno.core.opSync(
|
|
||||||
// 'op_starmelon_batch_queries',
|
|
||||||
// [ [true, "select json_object('id', id, 'foo', foo) from foobar", []]
|
|
||||||
// , [false, "select json_object('id', id, 'foo', foo) from foobar", []]
|
|
||||||
// ]
|
|
||||||
// )
|
|
||||||
// ))
|
|
||||||
//"#);
|
|
||||||
}
|
|
||||||
|
|
||||||
final_script.push_str("\n\n");
|
|
||||||
// I think that when I set this script to be the main module, I am skipping the
|
|
||||||
// deno/runtime/js/99_main.js script that sets up a bunch of global variables. If I
|
|
||||||
// manually add the timer related code below then setTimeout works again.
|
|
||||||
// NB. there are 706 lines of setup code that add a bunch of apis to the global window
|
|
||||||
// scope. Figure out if I need to include all of them. For example, starmelon does not need
|
|
||||||
// to perform http calls right now, but I eventually want to.
|
|
||||||
final_script.push_str("const { setTimeout } = globalThis.__bootstrap.timers;\n");
|
|
||||||
final_script.push_str(
|
|
||||||
"Deno.core.setMacrotaskCallback(globalThis.__bootstrap.timers.handleTimerMacrotask);\n",
|
|
||||||
);
|
|
||||||
final_script.push_str("globalThis.setTimeout = setTimeout;\n");
|
|
||||||
|
|
||||||
final_script.push_str(&format!("var worker = Elm.{}.init();\n", &gen_module_name));
|
|
||||||
// add a shortcut for invoking the function so I don't have to traverse so many object
|
|
||||||
// lookups using the rust v8 API.
|
|
||||||
match input_type {
|
|
||||||
None => {
|
|
||||||
final_script.push_str(
|
|
||||||
"globalThis.runOnInput = function() { worker.ports.onInput.send(null) };\n",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(InputType::Value) => {
|
|
||||||
final_script
|
|
||||||
.push_str("globalThis.runOnInput = function(data) { worker.ports.onInput.send(JSON.parse(data)) };\n");
|
|
||||||
}
|
|
||||||
Some(InputType::String) => {
|
|
||||||
final_script.push_str(
|
|
||||||
"globalThis.runOnInput = function(data) { worker.ports.onInput.send(data) };",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(InputType::Bytes) => {
|
|
||||||
final_script.push_str(
|
|
||||||
r#"
|
|
||||||
globalThis.runOnInput = function(data) {
|
|
||||||
const dv = new DataView(data.buffer)
|
|
||||||
worker.ports.onInput.send(dv)
|
|
||||||
};"#,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final_script
|
|
||||||
})();
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
let mut final_file = generator_dir.join("bin").join(&gen_module_name);
|
|
||||||
final_file.set_extension("js");
|
|
||||||
let span = info_span!("file writes");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
std::fs::write(&final_file, final_script)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, final_file.clone()))?;
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
// Create a tokio runtime before registering ops so we can block on futures inside sync ops
|
|
||||||
let span = info_span!("create tokio runtime");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
let sys = tokio::runtime::Builder::new_current_thread()
|
|
||||||
// The default number of additional threads for running blocking FnOnce is 512.
|
|
||||||
.max_blocking_threads(1)
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
// step 10 create a v8 isolate. We need to register a different callback depending on
|
|
||||||
// the output type (string, or bytes)
|
|
||||||
|
|
||||||
let span = info_span!("create v8 isolate");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
let (mut worker, main_module) = runtime::setup_worker(&final_file.to_string_lossy())
|
|
||||||
.map_err(|err| InterpreterError::EventLoop(err))?;
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
let span = info_span!("register private api");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
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(())
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 10.B setup the sqlite database feature
|
|
||||||
let sql_background_thread_handle = if let Some(database_url) = sqlite_path {
|
|
||||||
// I want to construct the connection in the initial thread so I can tell if the connection
|
|
||||||
// failed
|
|
||||||
let db_pool = sys
|
|
||||||
.block_on(async { SqlitePool::connect(&database_url.to_string_lossy()).await })
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (worker_mailbox, rx) = std::sync::mpsc::channel::<(
|
|
||||||
oneshot::Sender<ElmResult<_, _>>,
|
|
||||||
Vec<(bool, String, Vec<String>)>,
|
|
||||||
)>();
|
|
||||||
|
|
||||||
let sql_worker_thread = std::thread::spawn(move || {
|
|
||||||
let worker = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
loop {
|
|
||||||
if let Ok((response, queries)) = rx.recv() {
|
|
||||||
// I am not sure if I should only work on one database task at a time, or
|
|
||||||
// submit as many takes as possible. Just spawning the future onto this
|
|
||||||
// exectutor does not seem to work, even though the docs say the thread pool
|
|
||||||
// will poll the future until it completes.
|
|
||||||
let db_pool_clone = db_pool.clone();
|
|
||||||
let span = info_span!("inside sql queries futures");
|
|
||||||
let f = async move {
|
|
||||||
let _start = Instant::now();
|
|
||||||
let db_pool = db_pool_clone;
|
|
||||||
let mut result: Vec<Vec<String>> = vec![];
|
|
||||||
for (fetch_all, sql, _args) in queries {
|
|
||||||
let mut acc = Vec::new();
|
|
||||||
if fetch_all {
|
|
||||||
let mut stream = sqlx::query(&sql).fetch(&db_pool);
|
|
||||||
loop {
|
|
||||||
match stream.next().await {
|
|
||||||
None => break,
|
|
||||||
Some(Ok(row)) => {
|
|
||||||
match row.try_get::<String, _>(0) {
|
|
||||||
Ok(s) => acc.push(s),
|
|
||||||
// TODO set an error flag before returning this one
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some(Err(err)) => {
|
|
||||||
eprintln!("got fetch_all sql error {:?}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push(acc);
|
|
||||||
} else {
|
|
||||||
let s = sqlx::query(&sql)
|
|
||||||
.fetch_one(&db_pool)
|
|
||||||
.await
|
|
||||||
.and_then(|row| row.try_get::<String, _>(0))
|
|
||||||
.unwrap();
|
|
||||||
result.push(vec![s]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.send(ElmResult::ok(result))
|
|
||||||
};
|
|
||||||
|
|
||||||
// I found it interesting that the runtime of the future from the viewpoint of
|
|
||||||
// tracing was around 230us for a trivial select 2 rows query, but walltime I
|
|
||||||
// measured was around 700us. So polling the future or waiting for file IO is
|
|
||||||
// more expensive than I thought.
|
|
||||||
worker.block_on(f.instrument(span)).unwrap();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let worker_mailbox_clone = worker_mailbox.clone();
|
|
||||||
|
|
||||||
worker.js_runtime.register_op(
|
|
||||||
"op_starmelon_batch_queries",
|
|
||||||
deno_core::op_sync(
|
|
||||||
move |_state, queries: Vec<(bool, String, Vec<String>)>, _: ()| {
|
|
||||||
let worker_mailbox = worker_mailbox_clone.clone();
|
|
||||||
let (sender, receiver) =
|
|
||||||
oneshot::channel::<ElmResult<Vec<Vec<String>>, String>>();
|
|
||||||
|
|
||||||
let span = info_span!("run sql");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
|
|
||||||
worker_mailbox.send((sender, queries)).unwrap();
|
|
||||||
let elm_result = receiver.recv().unwrap();
|
|
||||||
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
Ok(elm_result)
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
|
|
||||||
Some((worker_mailbox, sql_worker_thread))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.js_runtime.sync_ops_cache();
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
// 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 input = match input_type {
|
|
||||||
None => None,
|
|
||||||
Some(input_type) => {
|
|
||||||
let buffer = match input_source {
|
|
||||||
None => {
|
|
||||||
// read from stdin when input_source was not present
|
|
||||||
let mut stdin = io::stdin();
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
stdin.read_to_end(&mut buffer).unwrap();
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
Some(path) => {
|
|
||||||
let buffer = std::fs::read(&path)
|
|
||||||
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?;
|
|
||||||
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match input_type {
|
|
||||||
InputType::String => {
|
|
||||||
let s = String::from_utf8(buffer).map_err(|_| CompilerError::BadInput)?;
|
|
||||||
|
|
||||||
Some(ValidatedInput::String(s))
|
|
||||||
}
|
|
||||||
InputType::Bytes => Some(ValidatedInput::Bytes(buffer)),
|
|
||||||
InputType::Value => {
|
|
||||||
let _: serde_json::Value =
|
|
||||||
serde_json::from_slice(&buffer).map_err(|_| CompilerError::BadInput)?;
|
|
||||||
|
|
||||||
let s = String::from_utf8(buffer).map_err(|_| CompilerError::BadInput)?;
|
|
||||||
|
|
||||||
Some(ValidatedInput::Value(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let span = info_span!("eval javascript");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
sys.block_on(async move { runtime::xyz(worker, main_module, input).await })?;
|
|
||||||
drop(timing_guard);
|
|
||||||
|
|
||||||
// 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(buffer)) => match output {
|
|
||||||
None => {
|
|
||||||
io::stdout()
|
|
||||||
.write_all(&buffer)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?;
|
|
||||||
}
|
|
||||||
Some(filename) => {
|
|
||||||
let mut f = fs::File::create(&filename)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, filename))?;
|
|
||||||
|
|
||||||
f.write_all(&buffer)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Err(problem)) => {
|
|
||||||
println!("had a problem {}", problem);
|
|
||||||
}
|
|
||||||
None => println!("nothing in the mailbox"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((tx, thread_handle)) = sql_background_thread_handle {
|
|
||||||
drop(tx);
|
|
||||||
thread_handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum InputType {
|
|
||||||
Value,
|
|
||||||
String,
|
|
||||||
Bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ValidatedInput {
|
|
||||||
String(String),
|
|
||||||
Value(String),
|
|
||||||
Bytes(Vec<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum OutputType {
|
|
||||||
Html,
|
|
||||||
String,
|
|
||||||
Bytes,
|
|
||||||
Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_function_type(tipe: &elmi::Type) -> Result<(Option<InputType>, OutputType), 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
|
|
||||||
let output_type = resolve_output_type(&**b)?;
|
|
||||||
let input_type = resolve_input_type(&**a)?;
|
|
||||||
|
|
||||||
Ok((Some(input_type), output_type))
|
|
||||||
}
|
|
||||||
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((None, OutputType::String));
|
|
||||||
}
|
|
||||||
if module_name == "elm/bytes/Bytes" && name == "Bytes" {
|
|
||||||
return Ok((None, OutputType::String));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(TypeError::CantEvalType(tipe.clone()))
|
|
||||||
}
|
|
||||||
elmi::Type::TType(module_name, name, _args) => {
|
|
||||||
if module_name == "elm/virtual-dom/VirtualDom" && name == "Node" {
|
|
||||||
return Ok((None, OutputType::Html));
|
|
||||||
}
|
|
||||||
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
|
|
||||||
// ```
|
|
||||||
resolve_function_type(tipe)
|
|
||||||
}
|
|
||||||
elmi::AliasType::Holey(_) => return Err(TypeError::CantEvalHoleyAlias),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_input_type(tipe: &elmi::Type) -> Result<InputType, TypeError> {
|
enum ExecutionMode {
|
||||||
match tipe {
|
AstridPagesRoute,
|
||||||
elmi::Type::TLambda(_, _) => Err(TypeError::EvalRequiresSingleArgument(tipe.clone())),
|
Scripting(Option<scripting::InputType>, scripting::OutputType),
|
||||||
elmi::Type::TType(module_name, name, args) if args.is_empty() => {
|
|
||||||
if module_name == "elm/core/String" && name == "String" {
|
|
||||||
Ok(InputType::String)
|
|
||||||
} else if module_name == "elm/bytes/Bytes" && name == "Bytes" {
|
|
||||||
Ok(InputType::Bytes)
|
|
||||||
} else if module_name == "elm/json/Json.Encode" && name == "Value" {
|
|
||||||
Ok(InputType::Value)
|
|
||||||
} else {
|
|
||||||
Err(TypeError::InputTypeNotSupported(tipe.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elmi::Type::TAlias(_, _, _, ref alias) => match &**alias {
|
|
||||||
elmi::AliasType::Filled(tipe) => resolve_input_type(tipe),
|
|
||||||
elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias),
|
|
||||||
},
|
|
||||||
_ => Err(TypeError::OutputTypeNotSupported(tipe.clone())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_output_type(tipe: &elmi::Type) -> Result<OutputType, TypeError> {
|
fn is_astrid_pages_route(tipe: &elmi::Type) -> bool {
|
||||||
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" {
|
module_name == "author/project/Astrid.Pages" && name == "Route"
|
||||||
Ok(OutputType::String)
|
|
||||||
} else if module_name == "elm/bytes/Bytes" && name == "Bytes" {
|
|
||||||
Ok(OutputType::Bytes)
|
|
||||||
} else if module_name == "elm/json/Json.Encode" && name == "Value" {
|
|
||||||
Ok(OutputType::Value)
|
|
||||||
} else if module_name == "elm/virtual-dom/VirtualDom" && name == "Node" {
|
|
||||||
Ok(OutputType::Html)
|
|
||||||
} else {
|
|
||||||
Err(TypeError::OutputTypeNotSupported(tipe.clone()))
|
|
||||||
}
|
}
|
||||||
|
_ => false,
|
||||||
}
|
}
|
||||||
elmi::Type::TAlias(_, _, _, ref alias) => match &**alias {
|
|
||||||
elmi::AliasType::Filled(tipe) => resolve_output_type(tipe),
|
|
||||||
elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias),
|
|
||||||
},
|
|
||||||
_ => Err(TypeError::OutputTypeNotSupported(tipe.clone())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_generator_project(verbosity: u64, elm_project_dir: PathBuf) -> Result<PathBuf, Problem> {
|
|
||||||
// I added a couple of random bytes to the directory name to reduce the risk of
|
|
||||||
// collisions with other programs that also use elm-stuff for their scratch space
|
|
||||||
let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc");
|
|
||||||
|
|
||||||
if !our_temp_dir.exists() {
|
|
||||||
fs::create_dir(&our_temp_dir)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, our_temp_dir.clone()))?;
|
|
||||||
}
|
|
||||||
let source_dir = our_temp_dir.join("src");
|
|
||||||
let bin_dir = our_temp_dir.join("bin");
|
|
||||||
|
|
||||||
// Rather than attempt to modify the elm.json to use the extra libraries, I will invoke
|
|
||||||
// elm install. Because the elm install commands take 500ms for a no-op I decided to
|
|
||||||
// use a checksum file when the source elm.json changes
|
|
||||||
|
|
||||||
let elm_json_path = elm_project_dir.join("elm.json");
|
|
||||||
|
|
||||||
let mut constraint =
|
|
||||||
ChecksumConstraint::new(&elm_json_path, our_temp_dir.join("elm-json-checksum"))?;
|
|
||||||
|
|
||||||
if !constraint.dirty()? {
|
|
||||||
return Ok(our_temp_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut data = fs::read(&elm_json_path)
|
|
||||||
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, elm_json_path.clone()))?;
|
|
||||||
|
|
||||||
let mut elm_json = serde_json::from_slice::<ElmApplication>(&mut data)
|
|
||||||
.map_err(|err| CompilerError::FailedParseElmJson(err))?;
|
|
||||||
|
|
||||||
let mut new_src_directories = Vec::with_capacity(elm_json.source_directories.len() + 1);
|
|
||||||
for dirname in elm_json.source_directories.iter() {
|
|
||||||
let dir_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());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !source_dir.exists() {
|
|
||||||
std::fs::create_dir(&source_dir)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_dir.clone()))?;
|
|
||||||
}
|
|
||||||
if !bin_dir.exists() {
|
|
||||||
std::fs::create_dir(&bin_dir)
|
|
||||||
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, bin_dir.clone()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
new_src_directories.push(
|
|
||||||
canonicalize(our_temp_dir.join("src"))
|
|
||||||
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, our_temp_dir.join("src")))?
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
eprintln!("before json {:?}", elm_json);
|
|
||||||
elm_json.source_directories = new_src_directories;
|
|
||||||
|
|
||||||
eprintln!("modified json {:?}", elm_json);
|
|
||||||
|
|
||||||
let generator_elm_json_path = our_temp_dir.join("elm.json");
|
|
||||||
let mut generator_elm_json_file =
|
|
||||||
std::fs::File::create(&generator_elm_json_path).map_err(|io_err| {
|
|
||||||
CompilerError::WriteOutputFailed(io_err, generator_elm_json_path.clone())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
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(verbosity, &our_temp_dir, "ThinkAlexandria/elm-html-in-elm")?;
|
|
||||||
elm_install(verbosity, &our_temp_dir, "elm/json")?;
|
|
||||||
elm_install(verbosity, &our_temp_dir, "elm/bytes")?;
|
|
||||||
|
|
||||||
constraint.update()?;
|
|
||||||
|
|
||||||
Ok(our_temp_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct ElmApplication {
|
|
||||||
#[serde(rename = "source-directories")]
|
|
||||||
source_directories: Vec<String>,
|
|
||||||
#[serde(flatten)]
|
|
||||||
other_fields: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn elm_install<P: AsRef<Path>, S: AsRef<str>>(
|
|
||||||
verbosity: u64,
|
|
||||||
our_temp_dir: P,
|
|
||||||
package: S,
|
|
||||||
) -> Result<(), Problem> {
|
|
||||||
let span = info_span!("elm_install");
|
|
||||||
let _ = span.enter();
|
|
||||||
let mut command = Command::new("elm");
|
|
||||||
|
|
||||||
command
|
|
||||||
.arg("install")
|
|
||||||
.arg(package.as_ref())
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.current_dir(&our_temp_dir);
|
|
||||||
|
|
||||||
if verbosity < 1 {
|
|
||||||
command.stderr(Stdio::null());
|
|
||||||
command.stdout(Stdio::null());
|
|
||||||
} else {
|
|
||||||
let pipe = dup_stderr()
|
|
||||||
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, "stdout".into()))?;
|
|
||||||
command.stdout(pipe);
|
|
||||||
command.stderr(Stdio::piped());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut child = command.spawn().unwrap();
|
|
||||||
|
|
||||||
let child_stdin = child.stdin.as_mut().unwrap();
|
|
||||||
child_stdin.write_all(b"y\n").unwrap();
|
|
||||||
drop(child_stdin);
|
|
||||||
|
|
||||||
match child.wait() {
|
|
||||||
Ok(exit_status) => {
|
|
||||||
if !exit_status.success() {
|
|
||||||
return Err(SetupError::InstallDependencyFailed.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
return Err(SetupError::InstallDependencyFailed.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod runtime {
|
mod runtime {
|
||||||
use crate::exec::ValidatedInput;
|
|
||||||
use crate::reporting::InterpreterError;
|
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;
|
||||||
|
|
@ -839,9 +127,9 @@ mod runtime {
|
||||||
use deno_runtime::worker::WorkerOptions;
|
use deno_runtime::worker::WorkerOptions;
|
||||||
use deno_runtime::BootstrapOptions;
|
use deno_runtime::BootstrapOptions;
|
||||||
use deno_web::BlobStore;
|
use deno_web::BlobStore;
|
||||||
use rusty_v8 as v8;
|
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -901,11 +189,14 @@ mod runtime {
|
||||||
Ok((worker, main_module))
|
Ok((worker, main_module))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn xyz(
|
pub async fn xyz<F>(
|
||||||
mut worker: MainWorker,
|
mut worker: MainWorker,
|
||||||
main_module: ModuleSpecifier,
|
main_module: ModuleSpecifier,
|
||||||
input: Option<ValidatedInput>,
|
foo: F,
|
||||||
) -> Result<(), InterpreterError> {
|
) -> Result<(), InterpreterError>
|
||||||
|
where
|
||||||
|
F: FnOnce(rusty_v8::HandleScope) -> Result<(), InterpreterError>,
|
||||||
|
{
|
||||||
let wait_for_inspector = false;
|
let wait_for_inspector = false;
|
||||||
// step 10 load the module into our v8 isolate
|
// step 10 load the module into our v8 isolate
|
||||||
worker
|
worker
|
||||||
|
|
@ -917,67 +208,9 @@ mod runtime {
|
||||||
.instrument(info_span!("run v8 event loop"))
|
.instrument(info_span!("run v8 event loop"))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
{
|
let scope_outer = worker.js_runtime.handle_scope();
|
||||||
let scope = &mut worker.js_runtime.handle_scope();
|
|
||||||
let ctx = scope.get_current_context();
|
|
||||||
let global = ctx.global(scope);
|
|
||||||
|
|
||||||
let entrypoint = {
|
foo(scope_outer)?;
|
||||||
let x = v8::String::new(scope, "runOnInput")
|
|
||||||
.ok_or(InterpreterError::AllocationFailed)?;
|
|
||||||
v8::Local::new(scope, x).into()
|
|
||||||
};
|
|
||||||
let v8_value = global
|
|
||||||
.get(scope, entrypoint)
|
|
||||||
.ok_or(InterpreterError::ReferenceError)?;
|
|
||||||
|
|
||||||
// step 12 invoke the function
|
|
||||||
let function = v8::Local::<v8::Function>::try_from(v8_value)?;
|
|
||||||
|
|
||||||
let this = v8::undefined(scope).into();
|
|
||||||
|
|
||||||
let span = info_span!("dispatch v8 call");
|
|
||||||
let timing_guard = span.enter();
|
|
||||||
match input {
|
|
||||||
None => {
|
|
||||||
function.call(scope, this, &[]);
|
|
||||||
}
|
|
||||||
Some(ValidatedInput::String(s)) => {
|
|
||||||
let arg1 = {
|
|
||||||
let x =
|
|
||||||
v8::String::new(scope, &s).ok_or(InterpreterError::AllocationFailed)?;
|
|
||||||
v8::Local::new(scope, x).into()
|
|
||||||
};
|
|
||||||
|
|
||||||
function.call(scope, this, &[arg1]);
|
|
||||||
}
|
|
||||||
Some(ValidatedInput::Value(v)) => {
|
|
||||||
let arg1 = {
|
|
||||||
let x =
|
|
||||||
v8::String::new(scope, &v).ok_or(InterpreterError::AllocationFailed)?;
|
|
||||||
v8::Local::new(scope, x).into()
|
|
||||||
};
|
|
||||||
|
|
||||||
function.call(scope, this, &[arg1]);
|
|
||||||
}
|
|
||||||
Some(ValidatedInput::Bytes(data)) => {
|
|
||||||
let length = data.len();
|
|
||||||
|
|
||||||
let y = data.into_boxed_slice();
|
|
||||||
let k = v8::ArrayBuffer::new_backing_store_from_boxed_slice(y).make_shared();
|
|
||||||
let x = v8::ArrayBuffer::with_backing_store(scope, &k);
|
|
||||||
|
|
||||||
let arg1 = v8::Local::new(scope, x).into();
|
|
||||||
|
|
||||||
let c = v8::Uint8Array::new(scope, arg1, 0, length).unwrap();
|
|
||||||
|
|
||||||
let arg2 = v8::Local::new(scope, c).into();
|
|
||||||
|
|
||||||
function.call(scope, this, &[arg2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(timing_guard)
|
|
||||||
}
|
|
||||||
|
|
||||||
worker
|
worker
|
||||||
.run_event_loop(wait_for_inspector)
|
.run_event_loop(wait_for_inspector)
|
||||||
|
|
|
||||||
697
src/exec/scripting.rs
Normal file
697
src/exec/scripting.rs
Normal file
|
|
@ -0,0 +1,697 @@
|
||||||
|
use crate::reporting::{CompilerError, InterpreterError, Problem, TypeError};
|
||||||
|
use deno_core::futures::StreamExt;
|
||||||
|
use elm_project_utils::{setup_generator_project, ElmResult};
|
||||||
|
use os_pipe::dup_stderr;
|
||||||
|
use rusty_v8 as v8;
|
||||||
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
use sqlx::Row;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fs::{self};
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::path::{PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
use tokio;
|
||||||
|
use tracing::{info_span, Instrument};
|
||||||
|
use crate::exec::{fixtures, runtime};
|
||||||
|
|
||||||
|
pub(crate) fn run(
|
||||||
|
debug: bool,
|
||||||
|
verbosity: u64,
|
||||||
|
sqlite_path: Option<PathBuf>,
|
||||||
|
elm_project_dir: PathBuf,
|
||||||
|
source_checksum: u64,
|
||||||
|
entrypoint: elmi::Global,
|
||||||
|
input_type: Option<InputType>,
|
||||||
|
output_type: OutputType,
|
||||||
|
input_source: Option<PathBuf>,
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
) -> Result<(), Problem> {
|
||||||
|
// step 6 create our private project
|
||||||
|
let packages = ["ThinkAlexandria/elm-html-in-elm", "elm/json", "elm/bytes"];
|
||||||
|
// I added a couple of random bytes to the directory name to reduce the risk of
|
||||||
|
// collisions with other programs that also use elm-stuff for their scratch space
|
||||||
|
let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc");
|
||||||
|
let generator_dir =
|
||||||
|
setup_generator_project(verbosity, elm_project_dir.clone(), our_temp_dir, &packages)?;
|
||||||
|
|
||||||
|
// step 7 create an Elm fixture file to run our function
|
||||||
|
let span = info_span!("create Elm fixture files");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let (gen_module_name, source) = fixtures::scripting::generate(
|
||||||
|
source_checksum,
|
||||||
|
entrypoint.0.module.clone(),
|
||||||
|
entrypoint.1.clone(),
|
||||||
|
input_type,
|
||||||
|
output_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut source_filename = generator_dir.join("src").join(&gen_module_name);
|
||||||
|
source_filename.set_extension("elm");
|
||||||
|
|
||||||
|
let span = info_span!("file writes");
|
||||||
|
let file_write_timing_guard = span.enter();
|
||||||
|
fs::File::create(&source_filename)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?
|
||||||
|
.write_all(source.as_bytes())
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?;
|
||||||
|
drop(file_write_timing_guard);
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
if verbosity < 1 {
|
||||||
|
command.stdout(Stdio::piped());
|
||||||
|
} else {
|
||||||
|
let pipe = dup_stderr()
|
||||||
|
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, "stdout".into()))?;
|
||||||
|
command.stdout(pipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
command.arg("--debug");
|
||||||
|
}
|
||||||
|
command.arg(&source_filename);
|
||||||
|
|
||||||
|
let span = info_span!("elm make fixture file");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
match command.output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(CompilerError::FailedBuildingFixture.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(CompilerError::FailedBuildingFixture.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// 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 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 span = info_span!("munge fixture javascript");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let final_script = (|| {
|
||||||
|
let mut final_script = data
|
||||||
|
.replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)")
|
||||||
|
.replace(
|
||||||
|
"$elm$json$Json$Decode$fail('REPLACE_ME_WITH_BYTES_DECODER');",
|
||||||
|
r#" _Json_decodePrim(function(value) {
|
||||||
|
return (typeof value === 'object' && value instanceof DataView)
|
||||||
|
? $elm$core$Result$Ok(value)
|
||||||
|
: _Json_expecting('a DataView', value);
|
||||||
|
});"#,
|
||||||
|
)
|
||||||
|
.replace(";}(this));", ";}(globalThis));");
|
||||||
|
|
||||||
|
if sqlite_path.is_some() {
|
||||||
|
final_script = final_script
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$execute = function (query) {\n\treturn $author$project$Astrid$Query$dummyExecute;\n};",
|
||||||
|
include_str!("fixtures/sql-client-integration.js"),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$fetch = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
"var $author$project$Astrid$Query$fetch = _Query_fetchAll;",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$fetchOne = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
"var $author$project$Astrid$Query$fetchOne = _Query_fetchOne;",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map3 = F4(\n\tfunction (f, a, b, c) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map3 = _Query_map3;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map2 = F3(\n\tfunction (f, a, b) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map2 = _Query_map2;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map = F2(\n\tfunction (f, a) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map = _Query_map1;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$andThen = F2(\n\tfunction (f, q) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$andThen = _Query_andThen;"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
// final_script.replace("var $author$project$Astrid$Query$run = ", "JSON.stringify(x)");
|
||||||
|
final_script.push_str("\n\n");
|
||||||
|
//final_script.push_str(r#"
|
||||||
|
// Deno.core.print(JSON.stringify(
|
||||||
|
// Deno.core.opSync(
|
||||||
|
// 'op_starmelon_batch_queries',
|
||||||
|
// [ [true, "select json_object('id', id, 'foo', foo) from foobar", []]
|
||||||
|
// , [false, "select json_object('id', id, 'foo', foo) from foobar", []]
|
||||||
|
// ]
|
||||||
|
// )
|
||||||
|
// ))
|
||||||
|
//"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
final_script.push_str("\n\n");
|
||||||
|
// I think that when I set this script to be the main module, I am skipping the
|
||||||
|
// deno/runtime/js/99_main.js script that sets up a bunch of global variables. If I
|
||||||
|
// manually add the timer related code below then setTimeout works again.
|
||||||
|
// NB. there are 706 lines of setup code that add a bunch of apis to the global window
|
||||||
|
// scope. Figure out if I need to include all of them. For example, starmelon does not need
|
||||||
|
// to perform http calls right now, but I eventually want to.
|
||||||
|
final_script.push_str("const { setTimeout } = globalThis.__bootstrap.timers;\n");
|
||||||
|
final_script.push_str(
|
||||||
|
"Deno.core.setMacrotaskCallback(globalThis.__bootstrap.timers.handleTimerMacrotask);\n",
|
||||||
|
);
|
||||||
|
final_script.push_str("globalThis.setTimeout = setTimeout;\n");
|
||||||
|
|
||||||
|
final_script.push_str(&format!("var worker = Elm.{}.init();\n", &gen_module_name));
|
||||||
|
// add a shortcut for invoking the function so I don't have to traverse so many object
|
||||||
|
// lookups using the rust v8 API.
|
||||||
|
match input_type {
|
||||||
|
None => {
|
||||||
|
final_script.push_str(
|
||||||
|
"globalThis.runOnInput = function() { worker.ports.onInput.send(null) };\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(InputType::Value) => {
|
||||||
|
final_script
|
||||||
|
.push_str("globalThis.runOnInput = function(data) { worker.ports.onInput.send(JSON.parse(data)) };\n");
|
||||||
|
}
|
||||||
|
Some(InputType::String) => {
|
||||||
|
final_script.push_str(
|
||||||
|
"globalThis.runOnInput = function(data) { worker.ports.onInput.send(data) };",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(InputType::Bytes) => {
|
||||||
|
final_script.push_str(
|
||||||
|
r#"
|
||||||
|
globalThis.runOnInput = function(data) {
|
||||||
|
const dv = new DataView(data.buffer)
|
||||||
|
worker.ports.onInput.send(dv)
|
||||||
|
};"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final_script
|
||||||
|
})();
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
let mut final_file = generator_dir.join("bin").join(&gen_module_name);
|
||||||
|
final_file.set_extension("js");
|
||||||
|
let span = info_span!("file writes");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
std::fs::write(&final_file, final_script)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, final_file.clone()))?;
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// Create a tokio runtime before registering ops so we can block on futures inside sync ops
|
||||||
|
let span = info_span!("create tokio runtime");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let sys = tokio::runtime::Builder::new_current_thread()
|
||||||
|
// The default number of additional threads for running blocking FnOnce is 512.
|
||||||
|
.max_blocking_threads(1)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// step 10 create a v8 isolate. We need to register a different callback depending on
|
||||||
|
// the output type (string, or bytes)
|
||||||
|
|
||||||
|
let span = info_span!("create v8 isolate");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let (mut worker, main_module) = runtime::setup_worker(&final_file.to_string_lossy())
|
||||||
|
.map_err(|err| InterpreterError::EventLoop(err))?;
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
let span = info_span!("register private api");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
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(())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 10.B setup the sqlite database feature
|
||||||
|
let sql_background_thread_handle = if let Some(database_url) = sqlite_path {
|
||||||
|
// I want to construct the connection in the initial thread so I can tell if the connection
|
||||||
|
// failed
|
||||||
|
let db_pool = sys
|
||||||
|
.block_on(async { SqlitePool::connect(&database_url.to_string_lossy()).await })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (worker_mailbox, rx) = std::sync::mpsc::channel::<(
|
||||||
|
oneshot::Sender<ElmResult<_, _>>,
|
||||||
|
Vec<(bool, String, Vec<String>)>,
|
||||||
|
)>();
|
||||||
|
|
||||||
|
let sql_worker_thread = std::thread::spawn(move || {
|
||||||
|
let worker = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
if let Ok((response, queries)) = rx.recv() {
|
||||||
|
// I am not sure if I should only work on one database task at a time, or
|
||||||
|
// submit as many takes as possible. Just spawning the future onto this
|
||||||
|
// exectutor does not seem to work, even though the docs say the thread pool
|
||||||
|
// will poll the future until it completes.
|
||||||
|
let db_pool_clone = db_pool.clone();
|
||||||
|
let span = info_span!("inside sql queries futures");
|
||||||
|
let f = async move {
|
||||||
|
let _start = Instant::now();
|
||||||
|
let db_pool = db_pool_clone;
|
||||||
|
let mut result: Vec<Vec<String>> = vec![];
|
||||||
|
for (fetch_all, sql, _args) in queries {
|
||||||
|
let mut acc = Vec::new();
|
||||||
|
if fetch_all {
|
||||||
|
let mut stream = sqlx::query(&sql).fetch(&db_pool);
|
||||||
|
loop {
|
||||||
|
match stream.next().await {
|
||||||
|
None => break,
|
||||||
|
Some(Ok(row)) => {
|
||||||
|
match row.try_get::<String, _>(0) {
|
||||||
|
Ok(s) => acc.push(s),
|
||||||
|
// TODO set an error flag before returning this one
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(Err(err)) => {
|
||||||
|
eprintln!("got fetch_all sql error {:?}", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(acc);
|
||||||
|
} else {
|
||||||
|
let s = sqlx::query(&sql)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.and_then(|row| row.try_get::<String, _>(0))
|
||||||
|
.unwrap();
|
||||||
|
result.push(vec![s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.send(ElmResult::ok(result))
|
||||||
|
};
|
||||||
|
|
||||||
|
// I found it interesting that the runtime of the future from the viewpoint of
|
||||||
|
// tracing was around 230us for a trivial select 2 rows query, but walltime I
|
||||||
|
// measured was around 700us. So polling the future or waiting for file IO is
|
||||||
|
// more expensive than I thought.
|
||||||
|
worker.block_on(f.instrument(span)).unwrap();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let worker_mailbox_clone = worker_mailbox.clone();
|
||||||
|
|
||||||
|
worker.js_runtime.register_op(
|
||||||
|
"op_starmelon_batch_queries",
|
||||||
|
deno_core::op_sync(
|
||||||
|
move |_state, queries: Vec<(bool, String, Vec<String>)>, _: ()| {
|
||||||
|
let worker_mailbox = worker_mailbox_clone.clone();
|
||||||
|
let (sender, receiver) =
|
||||||
|
oneshot::channel::<ElmResult<Vec<Vec<String>>, String>>();
|
||||||
|
|
||||||
|
let span = info_span!("run sql");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
|
||||||
|
worker_mailbox.send((sender, queries)).unwrap();
|
||||||
|
let elm_result = receiver.recv().unwrap();
|
||||||
|
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
Ok(elm_result)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some((worker_mailbox, sql_worker_thread))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.js_runtime.sync_ops_cache();
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// 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 input = match input_type {
|
||||||
|
None => None,
|
||||||
|
Some(input_type) => {
|
||||||
|
let buffer = match input_source {
|
||||||
|
None => {
|
||||||
|
// read from stdin when input_source was not present
|
||||||
|
let mut stdin = io::stdin();
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
stdin.read_to_end(&mut buffer).unwrap();
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
Some(path) => {
|
||||||
|
let buffer = std::fs::read(&path)
|
||||||
|
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?;
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match input_type {
|
||||||
|
InputType::String => {
|
||||||
|
let s = String::from_utf8(buffer).map_err(|_| CompilerError::BadInput)?;
|
||||||
|
|
||||||
|
Some(ValidatedInput::String(s))
|
||||||
|
}
|
||||||
|
InputType::Bytes => Some(ValidatedInput::Bytes(buffer)),
|
||||||
|
InputType::Value => {
|
||||||
|
let _: serde_json::Value =
|
||||||
|
serde_json::from_slice(&buffer).map_err(|_| CompilerError::BadInput)?;
|
||||||
|
|
||||||
|
let s = String::from_utf8(buffer).map_err(|_| CompilerError::BadInput)?;
|
||||||
|
|
||||||
|
Some(ValidatedInput::Value(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let foo = move |mut scope: rusty_v8::HandleScope| -> Result<(), InterpreterError> {
|
||||||
|
let scope = &mut scope;
|
||||||
|
let ctx = scope.get_current_context();
|
||||||
|
let global = ctx.global(scope);
|
||||||
|
|
||||||
|
let entrypoint = {
|
||||||
|
let x =
|
||||||
|
v8::String::new(scope, "runOnInput").ok_or(InterpreterError::AllocationFailed)?;
|
||||||
|
v8::Local::new(scope, x).into()
|
||||||
|
};
|
||||||
|
let v8_value = global
|
||||||
|
.get(scope, entrypoint)
|
||||||
|
.ok_or(InterpreterError::ReferenceError)?;
|
||||||
|
|
||||||
|
// step 12 invoke the function
|
||||||
|
let function = v8::Local::<v8::Function>::try_from(v8_value)?;
|
||||||
|
|
||||||
|
let this = v8::undefined(scope).into();
|
||||||
|
|
||||||
|
let span = info_span!("dispatch v8 call");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
match input {
|
||||||
|
None => {
|
||||||
|
function.call(scope, this, &[]);
|
||||||
|
}
|
||||||
|
Some(ValidatedInput::String(s)) => {
|
||||||
|
let arg1 = {
|
||||||
|
let x = v8::String::new(scope, &s).ok_or(InterpreterError::AllocationFailed)?;
|
||||||
|
v8::Local::new(scope, x).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
function.call(scope, this, &[arg1]);
|
||||||
|
}
|
||||||
|
Some(ValidatedInput::Value(v)) => {
|
||||||
|
let arg1 = {
|
||||||
|
let x = v8::String::new(scope, &v).ok_or(InterpreterError::AllocationFailed)?;
|
||||||
|
v8::Local::new(scope, x).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
function.call(scope, this, &[arg1]);
|
||||||
|
}
|
||||||
|
Some(ValidatedInput::Bytes(data)) => {
|
||||||
|
let length = data.len();
|
||||||
|
|
||||||
|
let y = data.into_boxed_slice();
|
||||||
|
let k = v8::ArrayBuffer::new_backing_store_from_boxed_slice(y).make_shared();
|
||||||
|
let x = v8::ArrayBuffer::with_backing_store(scope, &k);
|
||||||
|
|
||||||
|
let arg1 = v8::Local::new(scope, x).into();
|
||||||
|
|
||||||
|
let c = v8::Uint8Array::new(scope, arg1, 0, length).unwrap();
|
||||||
|
|
||||||
|
let arg2 = v8::Local::new(scope, c).into();
|
||||||
|
|
||||||
|
function.call(scope, this, &[arg2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let span = info_span!("eval javascript");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
sys.block_on(async move { runtime::xyz(worker, main_module, foo).await })?;
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// 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(buffer)) => match output {
|
||||||
|
None => {
|
||||||
|
io::stdout()
|
||||||
|
.write_all(&buffer)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?;
|
||||||
|
}
|
||||||
|
Some(filename) => {
|
||||||
|
let mut f = fs::File::create(&filename)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, filename))?;
|
||||||
|
|
||||||
|
f.write_all(&buffer)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Err(problem)) => {
|
||||||
|
println!("had a problem {}", problem);
|
||||||
|
}
|
||||||
|
None => println!("nothing in the mailbox"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((tx, thread_handle)) = sql_background_thread_handle {
|
||||||
|
drop(tx);
|
||||||
|
thread_handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum InputType {
|
||||||
|
Value,
|
||||||
|
String,
|
||||||
|
Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ValidatedInput {
|
||||||
|
String(String),
|
||||||
|
Value(String),
|
||||||
|
Bytes(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum OutputType {
|
||||||
|
Html,
|
||||||
|
String,
|
||||||
|
Bytes,
|
||||||
|
Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_function_type(
|
||||||
|
tipe: &elmi::Type,
|
||||||
|
) -> Result<(Option<InputType>, OutputType), 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
|
||||||
|
let output_type = resolve_output_type(&**b)?;
|
||||||
|
let input_type = resolve_input_type(&**a)?;
|
||||||
|
|
||||||
|
Ok((Some(input_type), output_type))
|
||||||
|
}
|
||||||
|
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((None, OutputType::String));
|
||||||
|
}
|
||||||
|
if module_name == "elm/bytes/Bytes" && name == "Bytes" {
|
||||||
|
return Ok((None, OutputType::String));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(TypeError::CantEvalType(tipe.clone()))
|
||||||
|
}
|
||||||
|
elmi::Type::TType(module_name, name, _args) => {
|
||||||
|
if module_name == "elm/virtual-dom/VirtualDom" && name == "Node" {
|
||||||
|
return Ok((None, OutputType::Html));
|
||||||
|
}
|
||||||
|
eprintln!("found this type {:?} {:?}", module_name, name);
|
||||||
|
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
|
||||||
|
// ```
|
||||||
|
resolve_function_type(tipe)
|
||||||
|
}
|
||||||
|
elmi::AliasType::Holey(_) => return Err(TypeError::CantEvalHoleyAlias),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_input_type(tipe: &elmi::Type) -> Result<InputType, 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(InputType::String)
|
||||||
|
} else if module_name == "elm/bytes/Bytes" && name == "Bytes" {
|
||||||
|
Ok(InputType::Bytes)
|
||||||
|
} else if module_name == "elm/json/Json.Encode" && name == "Value" {
|
||||||
|
Ok(InputType::Value)
|
||||||
|
} else {
|
||||||
|
Err(TypeError::InputTypeNotSupported(tipe.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elmi::Type::TAlias(_, _, _, ref alias) => match &**alias {
|
||||||
|
elmi::AliasType::Filled(tipe) => resolve_input_type(tipe),
|
||||||
|
elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias),
|
||||||
|
},
|
||||||
|
_ => Err(TypeError::OutputTypeNotSupported(tipe.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_output_type(tipe: &elmi::Type) -> Result<OutputType, TypeError> {
|
||||||
|
match tipe {
|
||||||
|
elmi::Type::TType(module_name, name, _args) => {
|
||||||
|
if module_name == "elm/core/String" && name == "String" {
|
||||||
|
Ok(OutputType::String)
|
||||||
|
} else if module_name == "elm/bytes/Bytes" && name == "Bytes" {
|
||||||
|
Ok(OutputType::Bytes)
|
||||||
|
} else if module_name == "elm/json/Json.Encode" && name == "Value" {
|
||||||
|
Ok(OutputType::Value)
|
||||||
|
} else if module_name == "elm/virtual-dom/VirtualDom" && name == "Node" {
|
||||||
|
Ok(OutputType::Html)
|
||||||
|
} else {
|
||||||
|
Err(TypeError::OutputTypeNotSupported(tipe.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elmi::Type::TAlias(_, _, _, ref alias) => match &**alias {
|
||||||
|
elmi::AliasType::Filled(tipe) => resolve_output_type(tipe),
|
||||||
|
elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias),
|
||||||
|
},
|
||||||
|
_ => Err(TypeError::OutputTypeNotSupported(tipe.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::reporting::doc::reflow;
|
use crate::reporting::doc::reflow;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use elm_project_utils::RedoScriptError;
|
use elm_project_utils::{ProjectSetupError, RedoScriptError};
|
||||||
use elmi;
|
use elmi;
|
||||||
use pretty::{self, cyan, vcat, Doc};
|
use pretty::{self, cyan, vcat, Doc};
|
||||||
use rusty_v8;
|
use rusty_v8;
|
||||||
|
|
@ -70,6 +70,28 @@ impl From<elm_project_utils::RedoScriptError> for Problem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<elm_project_utils::ProjectSetupError> for Problem {
|
||||||
|
fn from(error: ProjectSetupError) -> Self {
|
||||||
|
match error {
|
||||||
|
ProjectSetupError::WriteElmJsonFailed { filename, source } => {
|
||||||
|
CompilerError::WriteElmJsonFailed(source, filename).into()
|
||||||
|
}
|
||||||
|
ProjectSetupError::WriteFailed { filename, source } => {
|
||||||
|
CompilerError::WriteOutputFailed(source, filename).into()
|
||||||
|
}
|
||||||
|
ProjectSetupError::ReadFailed { filename, source } => {
|
||||||
|
CompilerError::ReadInputFailed(source, filename).into()
|
||||||
|
}
|
||||||
|
ProjectSetupError::FailedParseElmJson { filename: _, source } => {
|
||||||
|
CompilerError::FailedParseElmJson(source).into()
|
||||||
|
}
|
||||||
|
ProjectSetupError::InstallDependencyFailed => {
|
||||||
|
SetupError::InstallDependencyFailed.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TypeError {
|
pub enum TypeError {
|
||||||
CantEvalRecord,
|
CantEvalRecord,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue