From fb34d1e0b19c8ebd3e731f21fdd10d259e0ed4d2 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Thu, 22 Aug 2024 18:03:51 -0400 Subject: [PATCH 1/5] Add `-C` flag to build command --- crates/cli/src/commands.rs | 121 +++++- crates/cli/src/main.rs | 34 +- crates/cli/tests/common/mod.rs | 11 + crates/cli/tests/dynamic_linking_test.rs | 205 +++++----- crates/cli/tests/integration_test.rs | 455 +++++++++++++---------- crates/runner/src/lib.rs | 98 ++++- 6 files changed, 607 insertions(+), 317 deletions(-) create mode 100644 crates/cli/tests/common/mod.rs diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 41731eb9..7254af49 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -1,5 +1,6 @@ +use anyhow::{anyhow, bail}; use clap::{Parser, Subcommand}; -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; #[derive(Debug, Parser)] #[command( @@ -27,10 +28,10 @@ pub enum Command { /// /// Use the `build` command instead. #[command(arg_required_else_help = true)] - Compile(CompileAndBuildCommandOpts), + Compile(CompileCommandOpts), /// Generates WebAssembly from a JavaScript source. #[command(arg_required_else_help = true)] - Build(CompileAndBuildCommandOpts), + Build(BuildCommandOpts), /// Emits the provider binary that is required to run dynamically /// linked WebAssembly modules. EmitProvider(EmitProviderCommandOpts), @@ -44,7 +45,7 @@ impl Command { } #[derive(Debug, Parser)] -pub struct CompileAndBuildCommandOpts { +pub struct CompileCommandOpts { #[arg(value_name = "INPUT", required = true)] /// Path of the JavaScript input file. pub input: PathBuf, @@ -73,9 +74,121 @@ pub struct CompileAndBuildCommandOpts { pub no_source_compression: bool, } +#[derive(Debug, Parser)] +pub struct BuildCommandOpts { + #[arg(value_name = "INPUT", required = true)] + /// Path of the JavaScript input file. + pub input: PathBuf, + + #[arg(short, default_value = "index.wasm")] + /// Desired path of the WebAssembly output file. + pub output: PathBuf, + + #[arg( + short = 'C', + long_help = "Available codegen options: +-C dynamic[=y|n] -- Creates a smaller module that requires a dynamically linked QuickJS provider Wasm module to execute (see `emit-provider` command). +-C wit=path -- Optional path to WIT file describing exported functions. Only supports function exports with no arguments and no return values. +-C wit-world=val -- Optional WIT world name for WIT file. Must be specified if WIT is file path is specified. +-C no-source-compression[=y|n] -- Disable source code compression, which reduces compile time at the expense of generating larger WebAssembly files. + " + )] + /// Codegen options. + pub codegen: Vec, +} + #[derive(Debug, Parser)] pub struct EmitProviderCommandOpts { #[structopt(short, long)] /// Output path for the provider binary (default is stdout). pub out: Option, } + +#[derive(Clone, Debug, Parser)] +pub struct CodegenOptionGroup { + /// Creates a smaller module that requires a dynamically linked QuickJS provider Wasm + /// module to execute (see `emit-provider` command). + pub dynamic: bool, + /// Optional path to WIT file describing exported functions. + /// Only supports function exports with no arguments and no return values. + pub wit: Option, + /// Optional path to WIT file describing exported functions. + /// Only supports function exports with no arguments and no return values. + pub wit_world: Option, + /// Disable source code compression, which reduces compile time at the expense of generating larger WebAssembly files. + pub no_source_compression: bool, +} + +#[derive(Clone, Debug)] +pub enum CodegenOption { + /// Creates a smaller module that requires a dynamically linked QuickJS provider Wasm + /// module to execute (see `emit-provider` command). + Dynamic(bool), + /// Optional path to WIT file describing exported functions. + /// Only supports function exports with no arguments and no return values. + Wit(PathBuf), + /// Optional path to WIT file describing exported functions. + /// Only supports function exports with no arguments and no return values. + WitWorld(String), + /// Disable source code compression, which reduces compile time at the expense of generating larger WebAssembly files. + NoSourceCompression(bool), +} + +impl FromStr for CodegenOption { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + let mut parts = s.splitn(2, '='); + let key = parts.next().ok_or_else(|| anyhow!("Invalid codegen key"))?; + let value = parts.next(); + let option = match key { + "dynamic" => Self::Dynamic(match value { + None => true, + Some("y") => true, + Some("n") => false, + _ => bail!("Invalid value for dynamic"), + }), + "wit" => Self::Wit(PathBuf::from( + value.ok_or_else(|| anyhow!("Must provide value for wit"))?, + )), + "wit-world" => Self::WitWorld( + value + .ok_or_else(|| anyhow!("Must provide value for wit-world"))? + .to_string(), + ), + "no-source-compression" => Self::NoSourceCompression(match value { + None => true, + Some("y") => true, + Some("n") => false, + _ => bail!("Invalid value for no-source-compression"), + }), + _ => bail!("Invalid codegen key"), + }; + Ok(option) + } +} + +impl From> for CodegenOptionGroup { + fn from(value: Vec) -> Self { + let mut dynamic = false; + let mut wit = None; + let mut wit_world = None; + let mut no_source_compression = false; + + for option in value { + match option { + CodegenOption::Dynamic(enabled) => dynamic = enabled, + CodegenOption::Wit(path) => wit = Some(path), + CodegenOption::WitWorld(world) => wit_world = Some(world), + CodegenOption::NoSourceCompression(enabled) => no_source_compression = enabled, + } + } + + Self { + dynamic, + wit, + wit_world, + no_source_compression, + } + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c6ab73ae..e3f4bf23 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -9,6 +9,7 @@ use crate::commands::{Cli, Command, EmitProviderCommandOpts}; use anyhow::Result; use clap::Parser; use codegen::CodeGenBuilder; +use commands::CodegenOptionGroup; use js::JS; use std::fs; use std::fs::File; @@ -19,10 +20,9 @@ fn main() -> Result<()> { match &args.command { Command::EmitProvider(opts) => emit_provider(opts), - c @ Command::Compile(opts) | c @ Command::Build(opts) => { - if c.is_compile() { - eprintln!( - r#" + Command::Compile(opts) => { + eprintln!( + r#" The `compile` command will be deprecated in the next major release of the CLI (v4.0.0) @@ -31,8 +31,7 @@ fn main() -> Result<()> { Use the `build` command instead. "# - ); - } + ); let js = JS::from_file(&opts.input)?; let mut builder = CodeGenBuilder::new(); @@ -52,6 +51,29 @@ fn main() -> Result<()> { let wasm = gen.generate(&js)?; + fs::write(&opts.output, wasm)?; + Ok(()) + } + Command::Build(opts) => { + let js = JS::from_file(&opts.input)?; + let codegen: CodegenOptionGroup = opts.codegen.clone().into(); + let mut builder = CodeGenBuilder::new(); + builder + .wit_opts(WitOptions::from_tuple(( + codegen.wit.clone(), + codegen.wit_world.clone(), + ))?) + .source_compression(!codegen.no_source_compression) + .provider_version("2"); + + let mut gen = if codegen.dynamic { + builder.build::()? + } else { + builder.build::()? + }; + + let wasm = gen.generate(&js)?; + fs::write(&opts.output, wasm)?; Ok(()) } diff --git a/crates/cli/tests/common/mod.rs b/crates/cli/tests/common/mod.rs new file mode 100644 index 00000000..a5cbded8 --- /dev/null +++ b/crates/cli/tests/common/mod.rs @@ -0,0 +1,11 @@ +use anyhow::Result; +use javy_runner::Builder; + +pub fn run_with_compile_and_build(test: F) -> Result<()> +where + F: Fn(&mut Builder) -> Result<()>, +{ + test(Builder::default().use_compile())?; + test(&mut Builder::default())?; + Ok(()) +} diff --git a/crates/cli/tests/dynamic_linking_test.rs b/crates/cli/tests/dynamic_linking_test.rs index a827c477..09880c80 100644 --- a/crates/cli/tests/dynamic_linking_test.rs +++ b/crates/cli/tests/dynamic_linking_test.rs @@ -3,134 +3,153 @@ use javy_runner::Builder; use std::path::{Path, PathBuf}; use std::str; +mod common; +use common::run_with_compile_and_build; + static ROOT: &str = env!("CARGO_MANIFEST_DIR"); static BIN: &str = env!("CARGO_BIN_EXE_javy"); #[test] pub fn test_dynamic_linking() -> Result<()> { - let mut runner = Builder::default() - .root(root()) - .bin(BIN) - .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - let (_, logs, _) = runner.exec(&[])?; - assert_eq!("42\n", String::from_utf8(logs)?); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(root()) + .bin(BIN) + .input("console.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + let (_, logs, _) = runner.exec(&[])?; + assert_eq!("42\n", String::from_utf8(logs)?); + Ok(()) + }) } #[test] pub fn test_dynamic_linking_with_func() -> Result<()> { - let mut runner = Builder::default() - .root(root()) - .bin(BIN) - .input("linking-with-func.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .wit("linking-with-func.wit") - .world("foo-test") - .build()?; - - let (_, logs, _) = runner.exec_func("foo-bar", &[])?; - - assert_eq!("Toplevel\nIn foo\n", String::from_utf8(logs)?); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(root()) + .bin(BIN) + .input("linking-with-func.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .wit("linking-with-func.wit") + .world("foo-test") + .build()?; + + let (_, logs, _) = runner.exec_func("foo-bar", &[])?; + + assert_eq!("Toplevel\nIn foo\n", String::from_utf8(logs)?); + Ok(()) + }) } #[test] pub fn test_dynamic_linking_with_func_without_flag() -> Result<()> { - let mut runner = Builder::default() - .root(root()) - .bin(BIN) - .input("linking-with-func-without-flag.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - let res = runner.exec_func("foo", &[]); - - assert_eq!( - "failed to find function export `foo`", - res.err().unwrap().to_string() - ); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(root()) + .bin(BIN) + .input("linking-with-func-without-flag.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + let res = runner.exec_func("foo", &[]); + + assert_eq!( + "failed to find function export `foo`", + res.err().unwrap().to_string() + ); + Ok(()) + }) } #[test] fn test_errors_in_exported_functions_are_correctly_reported() -> Result<()> { - let mut runner = Builder::default() - .root(root()) - .bin(BIN) - .input("errors-in-exported-functions.js") - .wit("errors-in-exported-functions.wit") - .world("foo-test") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - let res = runner.exec_func("foo", &[]); - - assert!(res - .err() - .unwrap() - .to_string() - .contains("error while executing")); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(root()) + .bin(BIN) + .input("errors-in-exported-functions.js") + .wit("errors-in-exported-functions.wit") + .world("foo-test") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + let res = runner.exec_func("foo", &[]); + + assert!(res + .err() + .unwrap() + .to_string() + .contains("error while executing")); + Ok(()) + }) } #[test] // If you need to change this test, then you've likely made a breaking change. pub fn check_for_new_imports() -> Result<()> { - let runner = Builder::default() - .root(root()) - .bin(BIN) - .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - runner.assert_known_base_imports() + run_with_compile_and_build(|builder| { + let runner = builder + .root(root()) + .bin(BIN) + .input("console.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + runner.assert_known_base_imports() + }) } #[test] // If you need to change this test, then you've likely made a breaking change. pub fn check_for_new_imports_for_exports() -> Result<()> { - let runner = Builder::default() - .root(root()) - .bin(BIN) - .input("linking-with-func.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .wit("linking-with-func.wit") - .world("foo-test") - .build()?; - - runner.assert_known_named_function_imports() + run_with_compile_and_build(|builder| { + let runner = builder + .root(root()) + .bin(BIN) + .input("linking-with-func.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .wit("linking-with-func.wit") + .world("foo-test") + .build()?; + + runner.assert_known_named_function_imports() + }) } #[test] pub fn test_dynamic_linking_with_arrow_fn() -> Result<()> { - let mut runner = Builder::default() - .root(root()) - .bin(BIN) - .input("linking-arrow-func.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .wit("linking-arrow-func.wit") - .world("exported-arrow") - .build()?; - - let (_, logs, _) = runner.exec_func("default", &[])?; - - assert_eq!("42\n", String::from_utf8(logs)?); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(root()) + .bin(BIN) + .input("linking-arrow-func.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .wit("linking-arrow-func.wit") + .world("exported-arrow") + .build()?; + + let (_, logs, _) = runner.exec_func("default", &[])?; + + assert_eq!("42\n", String::from_utf8(logs)?); + Ok(()) + }) } #[test] fn test_producers_section_present() -> Result<()> { - let runner = Builder::default() - .root(root()) - .bin(BIN) - .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - runner.assert_producers() + run_with_compile_and_build(|builder| { + let runner = builder + .root(root()) + .bin(BIN) + .input("console.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + runner.assert_producers() + }) } #[test] diff --git a/crates/cli/tests/integration_test.rs b/crates/cli/tests/integration_test.rs index 1c3a42b5..6a697963 100644 --- a/crates/cli/tests/integration_test.rs +++ b/crates/cli/tests/integration_test.rs @@ -1,129 +1,148 @@ use anyhow::Result; -use javy_runner::{Builder, Runner, RunnerError}; +use javy_runner::{Runner, RunnerError}; use std::path::PathBuf; use std::str; +mod common; +use common::run_with_compile_and_build; + static BIN: &str = env!("CARGO_BIN_EXE_javy"); static ROOT: &str = env!("CARGO_MANIFEST_DIR"); #[test] fn test_identity() -> Result<()> { - let mut runner = Builder::default().root(sample_scripts()).bin(BIN).build()?; - - let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 42); - assert_eq!(42, output); - assert_fuel_consumed_within_threshold(47_773, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder.root(sample_scripts()).bin(BIN).build()?; + + let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 42); + assert_eq!(42, output); + assert_fuel_consumed_within_threshold(47_773, fuel_consumed); + Ok(()) + }) } #[test] fn test_fib() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("fib.js") - .build()?; - - let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 5); - assert_eq!(8, output); - assert_fuel_consumed_within_threshold(66_007, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("fib.js") + .build()?; + + let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 5); + assert_eq!(8, output); + assert_fuel_consumed_within_threshold(66_007, fuel_consumed); + Ok(()) + }) } #[test] fn test_recursive_fib() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("recursive-fib.js") - .build()?; - - let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 5); - assert_eq!(8, output); - assert_fuel_consumed_within_threshold(69_306, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("recursive-fib.js") + .build()?; + + let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 5); + assert_eq!(8, output); + assert_fuel_consumed_within_threshold(69_306, fuel_consumed); + Ok(()) + }) } #[test] fn test_str() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("str.js") - .build()?; - - let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); - assert_eq!("world".as_bytes(), output); - assert_fuel_consumed_within_threshold(142_849, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("str.js") + .build()?; + + let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); + assert_eq!("world".as_bytes(), output); + assert_fuel_consumed_within_threshold(142_849, fuel_consumed); + Ok(()) + }) } #[test] fn test_encoding() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("text-encoding.js") - .build()?; - - let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); - assert_eq!("el".as_bytes(), output); - assert_fuel_consumed_within_threshold(258_197, fuel_consumed); - - let (output, _, _) = run(&mut runner, "invalid".as_bytes()); - assert_eq!("true".as_bytes(), output); - - let (output, _, _) = run(&mut runner, "invalid_fatal".as_bytes()); - assert_eq!("The encoded data was not valid utf-8".as_bytes(), output); - - let (output, _, _) = run(&mut runner, "test".as_bytes()); - assert_eq!("test2".as_bytes(), output); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("text-encoding.js") + .build()?; + + let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); + assert_eq!("el".as_bytes(), output); + assert_fuel_consumed_within_threshold(258_197, fuel_consumed); + + let (output, _, _) = run(&mut runner, "invalid".as_bytes()); + assert_eq!("true".as_bytes(), output); + + let (output, _, _) = run(&mut runner, "invalid_fatal".as_bytes()); + assert_eq!("The encoded data was not valid utf-8".as_bytes(), output); + + let (output, _, _) = run(&mut runner, "test".as_bytes()); + assert_eq!("test2".as_bytes(), output); + Ok(()) + }) } #[test] fn test_logging() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("logging.js") - .build()?; - - let (_output, logs, fuel_consumed) = run(&mut runner, &[]); - assert_eq!( - "hello world from console.log\nhello world from console.error\n", - logs.as_str(), - ); - assert_fuel_consumed_within_threshold(34169, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("logging.js") + .build()?; + + let (_output, logs, fuel_consumed) = run(&mut runner, &[]); + assert_eq!( + "hello world from console.log\nhello world from console.error\n", + logs.as_str(), + ); + assert_fuel_consumed_within_threshold(34169, fuel_consumed); + Ok(()) + }) } #[test] fn test_readme_script() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("readme.js") - .build()?; - - let (output, _, fuel_consumed) = run(&mut runner, r#"{ "n": 2, "bar": "baz" }"#.as_bytes()); - assert_eq!(r#"{"foo":3,"newBar":"baz!"}"#.as_bytes(), output); - assert_fuel_consumed_within_threshold(270_919, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("readme.js") + .build()?; + + let (output, _, fuel_consumed) = run(&mut runner, r#"{ "n": 2, "bar": "baz" }"#.as_bytes()); + assert_eq!(r#"{"foo":3,"newBar":"baz!"}"#.as_bytes(), output); + assert_fuel_consumed_within_threshold(270_919, fuel_consumed); + Ok(()) + }) } #[cfg(feature = "experimental_event_loop")] #[test] fn test_promises() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("promise.js") - .build()?; - - let (output, _, _) = run(&mut runner, &[]); - assert_eq!("\"foo\"\"bar\"".as_bytes(), output); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("promise.js") + .build()?; + + let (output, _, _) = run(&mut runner, &[]); + assert_eq!("\"foo\"\"bar\"".as_bytes(), output); + Ok(()) + }) } #[cfg(not(feature = "experimental_event_loop"))] @@ -131,166 +150,190 @@ fn test_promises() -> Result<()> { fn test_promises() -> Result<()> { use javy_runner::RunnerError; - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("promise.js") - .build()?; - let res = runner.exec(&[]); - let err = res.err().unwrap().downcast::().unwrap(); - assert!(str::from_utf8(&err.stderr) - .unwrap() - .contains("Pending jobs in the event queue.")); - - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("promise.js") + .build()?; + let res = runner.exec(&[]); + let err = res.err().unwrap().downcast::().unwrap(); + assert!(str::from_utf8(&err.stderr) + .unwrap() + .contains("Pending jobs in the event queue.")); + + Ok(()) + }) } #[cfg(feature = "experimental_event_loop")] #[test] fn test_promise_top_level_await() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("top-level-await.js") - .build()?; - let (out, _, _) = run(&mut runner, &[]); - - assert_eq!("bar", String::from_utf8(out)?); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("top-level-await.js") + .build()?; + let (out, _, _) = run(&mut runner, &[]); + + assert_eq!("bar", String::from_utf8(out)?); + Ok(()) + }) } #[test] fn test_exported_functions() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("exported-fn.js") - .wit("exported-fn.wit") - .world("exported-fn") - .build()?; - let (_, logs, fuel_consumed) = run_fn(&mut runner, "foo", &[]); - assert_eq!("Hello from top-level\nHello from foo\n", logs); - assert_fuel_consumed_within_threshold(80023, fuel_consumed); - let (_, logs, _) = run_fn(&mut runner, "foo-bar", &[]); - assert_eq!("Hello from top-level\nHello from fooBar\n", logs); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("exported-fn.js") + .wit("exported-fn.wit") + .world("exported-fn") + .build()?; + let (_, logs, fuel_consumed) = run_fn(&mut runner, "foo", &[]); + assert_eq!("Hello from top-level\nHello from foo\n", logs); + assert_fuel_consumed_within_threshold(80023, fuel_consumed); + let (_, logs, _) = run_fn(&mut runner, "foo-bar", &[]); + assert_eq!("Hello from top-level\nHello from fooBar\n", logs); + Ok(()) + }) } #[cfg(feature = "experimental_event_loop")] #[test] fn test_exported_promises() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("exported-promise-fn.js") - .wit("exported-promise-fn.wit") - .world("exported-promise-fn") - .build()?; - let (_, logs, _) = run_fn(&mut runner, "foo", &[]); - assert_eq!("Top-level\ninside foo\n", logs); - Ok(()) + use clap::builder; + + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("exported-promise-fn.js") + .wit("exported-promise-fn.wit") + .world("exported-promise-fn") + .build()?; + let (_, logs, _) = run_fn(&mut runner, "foo", &[]); + assert_eq!("Top-level\ninside foo\n", logs); + Ok(()) + }) } #[test] fn test_exported_functions_without_flag() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("exported-fn.js") - .build()?; - let res = runner.exec_func("foo", &[]); - assert_eq!( - "failed to find function export `foo`", - res.err().unwrap().to_string() - ); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("exported-fn.js") + .build()?; + let res = runner.exec_func("foo", &[]); + assert_eq!( + "failed to find function export `foo`", + res.err().unwrap().to_string() + ); + Ok(()) + }) } #[test] fn test_exported_function_without_semicolons() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("exported-fn-no-semicolon.js") - .wit("exported-fn-no-semicolon.wit") - .world("exported-fn") - .build()?; - run_fn(&mut runner, "foo", &[]); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("exported-fn-no-semicolon.js") + .wit("exported-fn-no-semicolon.wit") + .world("exported-fn") + .build()?; + run_fn(&mut runner, "foo", &[]); + Ok(()) + }) } #[test] fn test_producers_section_present() -> Result<()> { - let runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("readme.js") - .build()?; - - runner.assert_producers() + run_with_compile_and_build(|builder| { + let runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("readme.js") + .build()?; + + runner.assert_producers() + }) } #[test] fn test_error_handling() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("error.js") - .build()?; - let result = runner.exec(&[]); - let err = result.err().unwrap().downcast::().unwrap(); - - let expected_log_output = "Error:2:9 error\n at error (function.mjs:2:9)\n at (function.mjs:5:1)\n\n"; - - assert_eq!(expected_log_output, str::from_utf8(&err.stderr).unwrap()); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .root(sample_scripts()) + .bin(BIN) + .input("error.js") + .build()?; + let result = runner.exec(&[]); + let err = result.err().unwrap().downcast::().unwrap(); + + let expected_log_output = "Error:2:9 error\n at error (function.mjs:2:9)\n at (function.mjs:5:1)\n\n"; + + assert_eq!(expected_log_output, str::from_utf8(&err.stderr).unwrap()); + Ok(()) + }) } #[test] fn test_same_module_outputs_different_random_result() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("random.js") - .build()?; - let (output, _, _) = runner.exec(&[]).unwrap(); - let (output2, _, _) = runner.exec(&[]).unwrap(); - // In theory these could be equal with a correct implementation but it's very unlikely. - assert!(output != output2); - // Don't check fuel consumed because fuel consumed can be different from run to run. See - // https://github.com/bytecodealliance/javy/issues/401 for investigating the cause. - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("random.js") + .build()?; + let (output, _, _) = runner.exec(&[]).unwrap(); + let (output2, _, _) = runner.exec(&[]).unwrap(); + // In theory these could be equal with a correct implementation but it's very unlikely. + assert!(output != output2); + // Don't check fuel consumed because fuel consumed can be different from run to run. See + // https://github.com/bytecodealliance/javy/issues/401 for investigating the cause. + Ok(()) + }) } #[test] fn test_exported_default_arrow_fn() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("exported-default-arrow-fn.js") - .wit("exported-default-arrow-fn.wit") - .world("exported-arrow") - .build()?; - - let (_, logs, fuel_consumed) = run_fn(&mut runner, "default", &[]); - assert_eq!(logs, "42\n"); - assert_fuel_consumed_within_threshold(76706, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("exported-default-arrow-fn.js") + .wit("exported-default-arrow-fn.wit") + .world("exported-arrow") + .build()?; + + let (_, logs, fuel_consumed) = run_fn(&mut runner, "default", &[]); + assert_eq!(logs, "42\n"); + assert_fuel_consumed_within_threshold(76706, fuel_consumed); + Ok(()) + }) } #[test] fn test_exported_default_fn() -> Result<()> { - let mut runner = Builder::default() - .bin(BIN) - .root(sample_scripts()) - .input("exported-default-fn.js") - .wit("exported-default-fn.wit") - .world("exported-default") - .build()?; - let (_, logs, fuel_consumed) = run_fn(&mut runner, "default", &[]); - assert_eq!(logs, "42\n"); - assert_fuel_consumed_within_threshold(77909, fuel_consumed); - Ok(()) + run_with_compile_and_build(|builder| { + let mut runner = builder + .bin(BIN) + .root(sample_scripts()) + .input("exported-default-fn.js") + .wit("exported-default-fn.wit") + .world("exported-default") + .build()?; + let (_, logs, fuel_consumed) = run_fn(&mut runner, "default", &[]); + assert_eq!(logs, "42\n"); + assert_fuel_consumed_within_threshold(77909, fuel_consumed); + Ok(()) + }) } fn sample_scripts() -> PathBuf { diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs index 6042862d..4b0305a8 100644 --- a/crates/runner/src/lib.rs +++ b/crates/runner/src/lib.rs @@ -13,6 +13,7 @@ use wasmtime::{ AsContextMut, Config, Engine, ExternType, Instance, Linker, Module, OptLevel, Store, }; +#[derive(Clone)] pub struct Builder { /// The JS source. input: PathBuf, @@ -27,6 +28,8 @@ pub struct Builder { built: bool, /// Preload the module at path, using the given instance name. preload: Option<(String, PathBuf)>, + /// Whether to use the `compile` or `build` command. + use_compile: bool, } impl Default for Builder { @@ -39,6 +42,7 @@ impl Default for Builder { root: Default::default(), built: false, preload: None, + use_compile: false, } } } @@ -74,6 +78,11 @@ impl Builder { self } + pub fn use_compile(&mut self) -> &mut Self { + self.use_compile = true; + self + } + pub fn build(&mut self) -> Result { if self.built { bail!("Builder already used to build a runner") @@ -93,14 +102,19 @@ impl Builder { root, built: _, preload, + use_compile, } = std::mem::take(self); self.built = true; - if let Some(preload) = preload { - Runner::build_dynamic(bin_path, root, input, wit, world, preload) + if use_compile { + if let Some(preload) = preload { + Runner::compile_dynamic(bin_path, root, input, wit, world, preload) + } else { + Runner::compile_static(bin_path, root, input, wit, world) + } } else { - Runner::build_static(bin_path, root, input, wit, world) + Runner::build(bin_path, root, input, wit, world, preload) } } } @@ -156,7 +170,46 @@ impl StoreContext { } impl Runner { - fn build_static( + fn build( + bin: String, + root: PathBuf, + source: impl AsRef, + wit: Option, + world: Option, + preload: Option<(String, PathBuf)>, + ) -> Result { + // This directory is unique and will automatically get deleted + // when `tempdir` goes out of scope. + let tempdir = tempfile::tempdir()?; + let wasm_file = Self::out_wasm(&tempdir); + let js_file = root.join(source); + let wit_file = wit.map(|p| root.join(p)); + + let args = Self::build_args(&js_file, &wasm_file, &wit_file, &world, preload.is_some()); + + Self::exec_command(bin, root, args)?; + + let wasm = fs::read(&wasm_file)?; + + let engine = Self::setup_engine(); + let linker = Self::setup_linker(&engine)?; + + let preload = preload + .map(|(name, path)| { + let module = fs::read(path)?; + Ok::<(String, Vec), anyhow::Error>((name, module)) + }) + .transpose()?; + + Ok(Self { + wasm, + linker, + initial_fuel: u64::MAX, + preload, + }) + } + + fn compile_static( bin: String, root: PathBuf, source: impl AsRef, @@ -170,7 +223,7 @@ impl Runner { let js_file = root.join(source); let wit_file = wit.map(|p| root.join(p)); - let args = Self::base_build_args(&js_file, &wasm_file, &wit_file, &world); + let args = Self::base_compile_args(&js_file, &wasm_file, &wit_file, &world); Self::exec_command(bin, root, args)?; @@ -187,7 +240,7 @@ impl Runner { }) } - pub fn build_dynamic( + pub fn compile_dynamic( bin: String, root: PathBuf, source: impl AsRef, @@ -200,7 +253,7 @@ impl Runner { let js_file = root.join(source); let wit_file = wit.map(|p| root.join(p)); - let mut args = Self::base_build_args(&js_file, &wasm_file, &wit_file, &world); + let mut args = Self::base_compile_args(&js_file, &wasm_file, &wit_file, &world); args.push("-d".to_string()); Self::exec_command(bin, root, args)?; @@ -314,7 +367,36 @@ impl Runner { file } - fn base_build_args( + fn build_args( + input: &Path, + out: &Path, + wit: &Option, + world: &Option, + dynamic: bool, + ) -> Vec { + let mut args = vec![ + "build".to_string(), + input.to_str().unwrap().to_string(), + "-o".to_string(), + out.to_str().unwrap().to_string(), + ]; + + if let (Some(wit), Some(world)) = (wit, world) { + args.push("-C".to_string()); + args.push(format!("wit={}", wit.to_str().unwrap())); + args.push("-C".to_string()); + args.push(format!("wit-world={world}")); + } + + if dynamic { + args.push("-C".to_string()); + args.push("dynamic".to_string()); + } + + args + } + + fn base_compile_args( input: &Path, out: &Path, wit: &Option, From ef95b47b52591b8398d2dcb5e2bcfe1d332c096e Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 23 Aug 2024 14:55:01 -0400 Subject: [PATCH 2/5] no-source-compression -> source-compression --- crates/cli/src/commands.rs | 20 ++++++++++---------- crates/cli/src/main.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 7254af49..d580af57 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -90,7 +90,7 @@ pub struct BuildCommandOpts { -C dynamic[=y|n] -- Creates a smaller module that requires a dynamically linked QuickJS provider Wasm module to execute (see `emit-provider` command). -C wit=path -- Optional path to WIT file describing exported functions. Only supports function exports with no arguments and no return values. -C wit-world=val -- Optional WIT world name for WIT file. Must be specified if WIT is file path is specified. --C no-source-compression[=y|n] -- Disable source code compression, which reduces compile time at the expense of generating larger WebAssembly files. +-C source-compression[=y|n] -- Enable source code compression, which generates smaller WebAssembly files at the cost of increased compile time. Defaults to enabled. " )] /// Codegen options. @@ -115,8 +115,8 @@ pub struct CodegenOptionGroup { /// Optional path to WIT file describing exported functions. /// Only supports function exports with no arguments and no return values. pub wit_world: Option, - /// Disable source code compression, which reduces compile time at the expense of generating larger WebAssembly files. - pub no_source_compression: bool, + /// Enable source code compression, which generates smaller WebAssembly files at the cost of increased compile time. Defaults to enabled. + pub source_compression: bool, } #[derive(Clone, Debug)] @@ -130,8 +130,8 @@ pub enum CodegenOption { /// Optional path to WIT file describing exported functions. /// Only supports function exports with no arguments and no return values. WitWorld(String), - /// Disable source code compression, which reduces compile time at the expense of generating larger WebAssembly files. - NoSourceCompression(bool), + /// Enable source code compression, which generates smaller WebAssembly files at the cost of increased compile time. Defaults to enabled. + SourceCompression(bool), } impl FromStr for CodegenOption { @@ -156,11 +156,11 @@ impl FromStr for CodegenOption { .ok_or_else(|| anyhow!("Must provide value for wit-world"))? .to_string(), ), - "no-source-compression" => Self::NoSourceCompression(match value { + "source-compression" => Self::SourceCompression(match value { None => true, Some("y") => true, Some("n") => false, - _ => bail!("Invalid value for no-source-compression"), + _ => bail!("Invalid value for source-compression"), }), _ => bail!("Invalid codegen key"), }; @@ -173,14 +173,14 @@ impl From> for CodegenOptionGroup { let mut dynamic = false; let mut wit = None; let mut wit_world = None; - let mut no_source_compression = false; + let mut source_compression = true; for option in value { match option { CodegenOption::Dynamic(enabled) => dynamic = enabled, CodegenOption::Wit(path) => wit = Some(path), CodegenOption::WitWorld(world) => wit_world = Some(world), - CodegenOption::NoSourceCompression(enabled) => no_source_compression = enabled, + CodegenOption::SourceCompression(enabled) => source_compression = enabled, } } @@ -188,7 +188,7 @@ impl From> for CodegenOptionGroup { dynamic, wit, wit_world, - no_source_compression, + source_compression, } } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e3f4bf23..0b1a6a7c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -63,7 +63,7 @@ fn main() -> Result<()> { codegen.wit.clone(), codegen.wit_world.clone(), ))?) - .source_compression(!codegen.no_source_compression) + .source_compression(codegen.source_compression) .provider_version("2"); let mut gen = if codegen.dynamic { From 9c6804a9a8658fbb19069d3bd6b595f05130b8bc Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 23 Aug 2024 15:02:46 -0400 Subject: [PATCH 3/5] Use enum instead of bool for command in tests --- crates/cli/tests/common/mod.rs | 6 +++--- crates/runner/src/lib.rs | 31 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/crates/cli/tests/common/mod.rs b/crates/cli/tests/common/mod.rs index a5cbded8..7a9351e3 100644 --- a/crates/cli/tests/common/mod.rs +++ b/crates/cli/tests/common/mod.rs @@ -1,11 +1,11 @@ use anyhow::Result; -use javy_runner::Builder; +use javy_runner::{Builder, JavyCommand}; pub fn run_with_compile_and_build(test: F) -> Result<()> where F: Fn(&mut Builder) -> Result<()>, { - test(Builder::default().use_compile())?; - test(&mut Builder::default())?; + test(Builder::default().command(JavyCommand::Compile))?; + test(Builder::default().command(JavyCommand::Build))?; Ok(()) } diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs index 4b0305a8..b6ac3fcc 100644 --- a/crates/runner/src/lib.rs +++ b/crates/runner/src/lib.rs @@ -13,6 +13,12 @@ use wasmtime::{ AsContextMut, Config, Engine, ExternType, Instance, Linker, Module, OptLevel, Store, }; +#[derive(Clone)] +pub enum JavyCommand { + Build, + Compile, +} + #[derive(Clone)] pub struct Builder { /// The JS source. @@ -29,7 +35,7 @@ pub struct Builder { /// Preload the module at path, using the given instance name. preload: Option<(String, PathBuf)>, /// Whether to use the `compile` or `build` command. - use_compile: bool, + command: JavyCommand, } impl Default for Builder { @@ -42,7 +48,7 @@ impl Default for Builder { root: Default::default(), built: false, preload: None, - use_compile: false, + command: JavyCommand::Build, } } } @@ -78,8 +84,8 @@ impl Builder { self } - pub fn use_compile(&mut self) -> &mut Self { - self.use_compile = true; + pub fn command(&mut self, command: JavyCommand) -> &mut Self { + self.command = command; self } @@ -102,19 +108,20 @@ impl Builder { root, built: _, preload, - use_compile, + command, } = std::mem::take(self); self.built = true; - if use_compile { - if let Some(preload) = preload { - Runner::compile_dynamic(bin_path, root, input, wit, world, preload) - } else { - Runner::compile_static(bin_path, root, input, wit, world) + match command { + JavyCommand::Compile => { + if let Some(preload) = preload { + Runner::compile_dynamic(bin_path, root, input, wit, world, preload) + } else { + Runner::compile_static(bin_path, root, input, wit, world) + } } - } else { - Runner::build(bin_path, root, input, wit, world, preload) + JavyCommand::Build => Runner::build(bin_path, root, input, wit, world, preload), } } } From a22907f6797fe798e35aa22b35cc73f1aeaf7d7f Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 23 Aug 2024 15:13:42 -0400 Subject: [PATCH 4/5] Use WitOptions in CodegenOptionsGroup --- crates/cli/src/commands.rs | 24 +++++++++++------------- crates/cli/src/main.rs | 7 ++----- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index d580af57..4a429998 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -2,6 +2,8 @@ use anyhow::{anyhow, bail}; use clap::{Parser, Subcommand}; use std::{path::PathBuf, str::FromStr}; +use crate::codegen::WitOptions; + #[derive(Debug, Parser)] #[command( name = "javy", @@ -104,17 +106,12 @@ pub struct EmitProviderCommandOpts { pub out: Option, } -#[derive(Clone, Debug, Parser)] pub struct CodegenOptionGroup { /// Creates a smaller module that requires a dynamically linked QuickJS provider Wasm /// module to execute (see `emit-provider` command). pub dynamic: bool, - /// Optional path to WIT file describing exported functions. - /// Only supports function exports with no arguments and no return values. - pub wit: Option, - /// Optional path to WIT file describing exported functions. - /// Only supports function exports with no arguments and no return values. - pub wit_world: Option, + /// The WIT options. + pub wit: WitOptions, /// Enable source code compression, which generates smaller WebAssembly files at the cost of increased compile time. Defaults to enabled. pub source_compression: bool, } @@ -168,8 +165,10 @@ impl FromStr for CodegenOption { } } -impl From> for CodegenOptionGroup { - fn from(value: Vec) -> Self { +impl TryFrom> for CodegenOptionGroup { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> Result { let mut dynamic = false; let mut wit = None; let mut wit_world = None; @@ -184,11 +183,10 @@ impl From> for CodegenOptionGroup { } } - Self { + Ok(Self { dynamic, - wit, - wit_world, + wit: WitOptions::from_tuple((wit, wit_world))?, source_compression, - } + }) } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 0b1a6a7c..1c2f1149 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -56,13 +56,10 @@ fn main() -> Result<()> { } Command::Build(opts) => { let js = JS::from_file(&opts.input)?; - let codegen: CodegenOptionGroup = opts.codegen.clone().into(); + let codegen: CodegenOptionGroup = opts.codegen.clone().try_into()?; let mut builder = CodeGenBuilder::new(); builder - .wit_opts(WitOptions::from_tuple(( - codegen.wit.clone(), - codegen.wit_world.clone(), - ))?) + .wit_opts(codegen.wit) .source_compression(codegen.source_compression) .provider_version("2"); From ed34c1b07e9b030b729a949e135af9442f8b2031 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 23 Aug 2024 15:24:46 -0400 Subject: [PATCH 5/5] Make the linter happy --- crates/cli/src/commands.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 4a429998..79628b6a 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -39,13 +39,6 @@ pub enum Command { EmitProvider(EmitProviderCommandOpts), } -impl Command { - /// Returns true if it is [`Command::Compile`]. - pub fn is_compile(&self) -> bool { - matches!(self, Command::Compile(_)) - } -} - #[derive(Debug, Parser)] pub struct CompileCommandOpts { #[arg(value_name = "INPUT", required = true)]