diff --git a/src/exec/css_in_elm.rs b/src/exec/css_in_elm.rs new file mode 100644 index 0000000..6fd2a18 --- /dev/null +++ b/src/exec/css_in_elm.rs @@ -0,0 +1,290 @@ +use crate::exec::{fixtures, runtime}; +use crate::reporting::{CompilerError, InterpreterError, Problem}; +use deno_core::{v8, Extension, OpState}; +use elm_project_utils::{setup_generator_project, ElmPostProcessor, ElmResult}; +use os_pipe::dup_stderr; +use serde::{Deserialize, Serialize}; +use sqlx::sqlite::SqlitePool; +use sqlx::Row; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::fs; +use std::io; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::Arc; +use tokio; +use tracing::info_span; + +pub(crate) fn run( + debug: bool, + verbosity: u64, + elm_project_dir: PathBuf, + source_checksum: u64, + entrypoint: elmi::Global, + output: Option, +) -> Result<(), Problem> { + // TODO require that output type is a directory + + // step 6 create our private project + let packages = ["ThinkAlexandria/css-in-elm", "elm/json"]; + // 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-ce7993"); + 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::css_in_elm::generate(source_checksum, &entrypoint); + + 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() { + eprintln!("{}", String::from_utf8_lossy(&output.stderr)); + return Err(CompilerError::FailedBuildingFixture.into()); + } + } + Err(err) => { + eprintln!("{:?}", err); + return Err(CompilerError::FailedBuildingFixture.into()); + } + } + drop(timing_guard); + + 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 mut munger = ElmPostProcessor::new(); + let mut buffer = String::with_capacity(data.len()); + munger.run(&data, &mut buffer); + + // Mutate the buffer in place for the final fixup + let pattern = ";}(this));"; + match buffer.get((buffer.len() - pattern.len())..) { + Some(end) if end == pattern => { + buffer.truncate(buffer.len() - pattern.len()); + buffer.push_str(";}(globalThis));"); + } + _ => (), + } + // 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. + buffer.push_str( + r#" + + const { setTimeout } = globalThis.__bootstrap.timers; + Deno.core.setMacrotaskCallback(globalThis.__bootstrap.timers.handleTimerMacrotask); + globalThis.setTimeout = setTimeout; + + "#, + ); + + buffer.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. + buffer.push_str( + r#" + globalThis.runOnInput = function(route) {}; + + if (worker.ports.onFilesOutput) { + worker.ports.onFilesOutput.subscribe(function(result){ + Deno.core.opSync('op_starmelon_elm_css_files_output', result) + }); + }"#, + ); + + drop(timing_guard); + + let mut buffer_file = generator_dir.join("bin").join(&gen_module_name); + buffer_file.set_extension("js"); + let span = info_span!("file writes"); + let timing_guard = span.enter(); + std::fs::write(&buffer_file, buffer) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, buffer_file.clone()))?; + drop(timing_guard); + + let desired_route = entrypoint.0.module.clone().to_string(); + let foo = move |mut scope: deno_core::v8::HandleScope| -> Result<(), InterpreterError> { + Ok(()) + // I don't think we have to provide any input to kick the process off + + //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::::try_from(v8_value)?; + + //let this = v8::undefined(scope).into(); + + //let span = info_span!("dispatch v8 call"); + //let timing_guard = span.enter(); + //let arg1 = { + // let x = + // v8::String::new(scope, &desired_route).ok_or(InterpreterError::AllocationFailed)?; + // v8::Local::new(scope, x).into() + //}; + + //function.call(scope, this, &[arg1]); + //drop(timing_guard); + + //Ok(()) + }; + + // 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); + + let span = info_span!("register private api"); + let timing_guard = span.enter(); + + // Step 10 setup our custom ops for the javascript runtime + let mailbox: Arc>>> = Arc::new(RefCell::new(None)); + let mailbox_clone = Arc::clone(&mailbox); + + let mut extensions = vec![Extension::builder() + .ops(vec![op_starmelon_elm_css_files_output::decl()]) + .state(move |state| { + state.put(Arc::clone(&mailbox_clone)); + + Ok(()) + }) + .build()]; + + drop(timing_guard); + + // step 11 create a v8 isolate. + let span = info_span!("create v8 isolate"); + let timing_guard = span.enter(); + let (worker, main_module) = runtime::setup_worker(extensions, &buffer_file.to_string_lossy()) + .map_err(|err| InterpreterError::EventLoop(err))?; + drop(timing_guard); + + 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(files) => { + let base = match output { + None => { + std::env::current_dir().unwrap() + } + Some(base) => base, + }; + for FileDefinition { filename, content, success } in files.iter() { + if *success { + let outfile = base.join(&filename); + let mut f = fs::File::create(&outfile) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, outfile.clone()))?; + f.write_all(content.as_bytes()) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, outfile))?; + } else { + eprintln!("{} failed\n{}", filename, content) + } + } + }, + None => eprintln!("nothing in the mailbox"), + } + + Ok(()) +} + +type OutputMailbox = Arc>>>; + +#[derive(Deserialize, Debug)] +struct FileDefinition { + filename: String, + content: String, + success: bool, +} + +#[deno_core::op] +fn op_starmelon_elm_css_files_output( + state: &mut OpState, + msg: Vec, +) -> Result<(), deno_core::error::AnyError> { + let mailbox_clone = state.borrow::(); + if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { + mailbox.replace(msg); + } + Ok(()) +} diff --git a/src/exec/fixtures/css_in_elm.rs b/src/exec/fixtures/css_in_elm.rs new file mode 100644 index 0000000..7385eb5 --- /dev/null +++ b/src/exec/fixtures/css_in_elm.rs @@ -0,0 +1,69 @@ +use crate::exec::astrid_pages::OutputType; +use elm_quote::Tokens; +use genco::tokens::quoted; + +pub(crate) fn generate(source_checksum: u64, entrypoint: &elmi::Global) -> (String, String) { + let tokens: Tokens = genco::quote! { + import Css + import Css.File + import Platform + import Stylesheets + + -- START CUSTOMIZED PART + import #(&entrypoint.0.module) + + entrypoint = #(&entrypoint.0.module).#(&entrypoint.1) + -- END CUSTOMIZED PART + + -- MAIN + + main: Program () Model Never + main = + Platform.worker + { init = init + , update = update + , subscriptions = subscriptions + } + + -- PORTS + + port onFilesOutput : Css.File.CssFileStructure -> Cmd msg + + -- MODEL + + type alias Model = () + + init : () -> (Model, Cmd Never) + init flags = + ( (), onFilesOutput structure ) + + structure : Css.File.CssFileStructure + structure = + Css.File.toFileStructure <| + List.map + (#("\\")(fileName, stylesheets) -> + (fileName, Css.File.compile stylesheets) + ) + entrypoint + + -- UPDATE + + update : Never -> Model -> ((), Cmd Never) + update _ model = + ( model, Cmd.none ) + + -- SUBSCRIPTIONS + + subscriptions _ = Sub.none + }; + + let module_name = format!("Main{:020}", source_checksum); + + let source = format!( + "port module {} exposing (main)\n\n{}", + module_name, + tokens.to_file_string().unwrap(), + ); + + (module_name, source) +} diff --git a/src/exec/fixtures/mod.rs b/src/exec/fixtures/mod.rs index 81c4e15..3598e8a 100644 --- a/src/exec/fixtures/mod.rs +++ b/src/exec/fixtures/mod.rs @@ -1,3 +1,4 @@ pub mod astrid_pages; +pub mod css_in_elm; pub mod scripting; pub mod sqlite; diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 7b615d6..68e7419 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use tracing::info_span; mod astrid_pages; +mod css_in_elm; mod fixtures; mod scripting; @@ -58,6 +59,8 @@ pub(crate) fn exec( if is_astrid_pages_route(tipe) { ExecutionMode::AstridPagesRoute + } else if is_css_in_elm_stylesheet(tipe) { + ExecutionMode::CssInElm } else { let (input_type, output_type) = scripting::resolve_function_type(tipe)?; ExecutionMode::Scripting(input_type, output_type) @@ -98,11 +101,20 @@ pub(crate) fn exec( entrypoint, output, ), + ExecutionMode::CssInElm => css_in_elm::run( + debug, + verbosity, + elm_project_dir, + source_checksum, + entrypoint, + output, + ), } } enum ExecutionMode { AstridPagesRoute, + CssInElm, Scripting(Option, scripting::OutputType), } @@ -115,6 +127,40 @@ fn is_astrid_pages_route(tipe: &elmi::Type) -> bool { } } +fn is_css_in_elm_stylesheet(tipe: &elmi::Type) -> bool { + match tipe { + elmi::Type::TType(module_name, name, args) => { + if module_name == "elm/core/List" && name == "List" && args.len() == 1 { + match &args[0] { + elmi::Type::TTuple(a, b, None) => { + match &**a { + elmi::Type::TType(module_name, name, _) + if module_name == "elm/core/String" && name == "String" => (), + _ => return false, + } + match &**b { + elmi::Type::TType(module_name, name, args) + if module_name == "elm/core/List" && name == "List" && args.len() == 1 => + { + match &args[0] { + elmi::Type::TAlias(module_name, name, _, _) + if module_name == "ThinkAlexandria/css-in-elm/Css" && name == "Stylesheet" => return true, + _ => (), + } + } + _ => (), + } + } + _ => (), + } + } + + } + _ => (), + } + false +} + mod runtime { use crate::reporting::InterpreterError; use deno_core::error::{type_error, AnyError}; diff --git a/src/main.rs b/src/main.rs index c77d86e..0b3fd65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use std::time::Instant; use structopt::StructOpt; use tracing::info_span; +mod derive; mod elm; mod exec; mod reporting; @@ -70,6 +71,45 @@ fn main() { } => { transpile::transpile(file, debug, function, verbosity).unwrap(); } + Arguments::Derive(DeriveMacros::LiveTable { + file, + debug, + verbosity, + output, + timings, + }) => { + let start = Instant::now(); + let span = info_span!("derive livetable editor"); + let timing_guard = span.enter(); + let result = + derive::derive_livetable(file, debug, output, verbosity /*, sqlite */); + drop(timing_guard); + if let Err(problem) = result { + let span = info_span!("pretty print problem"); + let timing_guard = span.enter(); + eprintln!("{}", pretty(80, problem.to_doc())); + drop(timing_guard); + } + { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + handle.flush().unwrap(); + } + + if timings { + let mut report = timing_report.dump().unwrap(); + report.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap()); + + for (step, duration) in report.iter() { + eprintln!("[{:>10.3?}] {}", duration, step); + } + } + eprintln!( + "\t\x1b[1;92mFinished\x1b[0m [{}] in {:?}", + "unoptimized", + Instant::now() - start + ); + } } } @@ -131,6 +171,25 @@ enum Arguments { #[structopt(long)] debug: bool, }, + #[structopt(name = "derive")] + Derive(DeriveMacros), +} + +#[derive(Debug, StructOpt)] +pub enum DeriveMacros { + #[structopt(name = "livetable")] + LiveTable { + #[structopt(parse(from_os_str))] + file: PathBuf, + #[structopt(short = "v", parse(from_occurrences))] + verbosity: u64, + #[structopt(long)] + debug: bool, + #[structopt(long)] + output: Option, + #[structopt(long)] + timings: bool, + }, } pub mod generated {