Skip to content

Commit

Permalink
Implement compile subcommand.
Browse files Browse the repository at this point in the history
Compile JS to bytecode and compress with zstd using the embedded
dictionary.

There is no compatibility guarantee of lrt files, they can be executed
by the llrt version that created them.
  • Loading branch information
krk committed Dec 9, 2023
1 parent 9cef90c commit d10661f
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 38 deletions.
37 changes: 3 additions & 34 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ use std::{
use std::io::Write;

use jwalk::WalkDir;
use rquickjs::{
loader::{Loader, Resolver},
module::ModuleData,
Context, Ctx, Module, Runtime,
};
use rquickjs::{Context, Module, Runtime};

const BUNDLE_DIR: &str = "bundle";

Expand All @@ -32,33 +28,7 @@ macro_rules! rerun_if_changed {
};
}

struct DummyLoader;

impl Loader for DummyLoader {
fn load(&mut self, _ctx: &Ctx<'_>, name: &str) -> rquickjs::Result<ModuleData> {
Ok(ModuleData::source(name, ""))
}
}

struct DummyResolver;

impl Resolver for DummyResolver {
fn resolve(&mut self, _ctx: &Ctx<'_>, _base: &str, name: &str) -> rquickjs::Result<String> {
Ok(name.into())
}
}

fn human_file_size(size: usize) -> String {
let fsize = size as f64;
let i = if size == 0 {
0
} else {
(fsize.log2() / 1024f64.log2()).floor() as i32
};
let size = fsize / 1024f64.powi(i);
let units = ["B", "kB", "MB", "GB", "TB", "PB"];
format!("{:.3} {}", size, units[i as usize])
}
include!("src/compiler-common.rs");

#[tokio::main]
async fn main() -> StdResult<(), Box<dyn Error>> {
Expand Down Expand Up @@ -174,8 +144,7 @@ fn compress_bytecode(bundles_dir: &str, source_files: Vec<String>) -> io::Result

let file_count = source_files.len();
let mut dictionary_filenames = source_files.clone();
let mut dictionary_file_set: HashSet<String> =
HashSet::from_iter(dictionary_filenames.clone().into_iter());
let mut dictionary_file_set: HashSet<String> = HashSet::from_iter(dictionary_filenames.clone());

let mut cmd = Command::new("zstd");
cmd.args([
Expand Down
35 changes: 35 additions & 0 deletions src/compiler-common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Shared with build.rs and compiler.rs

use rquickjs::{
loader::{Loader, Resolver},
module::ModuleData,
Ctx,
};

struct DummyLoader;

impl Loader for DummyLoader {
fn load(&mut self, _ctx: &Ctx<'_>, name: &str) -> rquickjs::Result<ModuleData> {
Ok(ModuleData::source(name, ""))
}
}

struct DummyResolver;

impl Resolver for DummyResolver {
fn resolve(&mut self, _ctx: &Ctx<'_>, _base: &str, name: &str) -> rquickjs::Result<String> {
Ok(name.into())
}
}

fn human_file_size(size: usize) -> String {
let fsize = size as f64;
let i = if size == 0 {
0
} else {
(fsize.log2() / 1024f64.log2()).floor() as i32
};
let size = fsize / 1024f64.powi(i);
let units = ["B", "kB", "MB", "GB", "TB", "PB"];
format!("{:.3} {}", size, units[i as usize])
}
65 changes: 65 additions & 0 deletions src/compiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::{fs, io, path::Path};

use rquickjs::{Context, Module, Runtime};
use tracing::trace;
use zstd::bulk::Compressor;

use crate::vm::COMPRESSION_DICT;

include!("compiler-common.rs");

fn compress_module(bytes: &[u8]) -> io::Result<Vec<u8>> {
let mut compressor = Compressor::with_dictionary(22, COMPRESSION_DICT)?;
let raw: Vec<u8> = compressor.compress(bytes)?;
let uncompressed_len = bytes.len() as u32;

let mut compressed = Vec::with_capacity(4);
compressed.extend_from_slice(&uncompressed_len.to_le_bytes());
compressed.extend_from_slice(&raw);

Ok(compressed)
}

pub async fn compile_file(input_filename: &Path, output_filename: &Path) -> Result<(), String> {
let resolver: (DummyResolver,) = (DummyResolver,);
let loader = (DummyLoader,);

let rt = Runtime::new().unwrap();
rt.set_loader(resolver, loader);
let ctx = Context::full(&rt).unwrap();

let mut total_bytes: usize = 0;
let mut compressed_bytes: usize = 0;
let mut js_bytes: usize = 0;

ctx.with(|ctx| {
let source = fs::read_to_string(input_filename)
.unwrap_or_else(|_| panic!("Unable to load: {}", input_filename.to_string_lossy()));
js_bytes = source.len();

let module_name = input_filename
.with_extension("")
.to_string_lossy()
.to_string();

trace!("Compiling module: {}", module_name);

let module = unsafe { Module::unsafe_declare(ctx.clone(), module_name, source).unwrap() };
let bytes = module.write_object(false).unwrap();
let filename = output_filename.to_string_lossy().to_string();
let compressed = compress_module(&bytes).unwrap();
fs::write(filename, &compressed).unwrap();

total_bytes += bytes.len();
compressed_bytes += compressed.len();
});

trace!("JS size: {}", human_file_size(js_bytes));
trace!("Bytecode size: {}", human_file_size(total_bytes));
trace!(
"Compressed bytecode size: {}",
human_file_size(compressed_bytes)
);

Ok(())
}
36 changes: 33 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod allocator;
mod buffer;
mod bytearray_buffer;
mod child_process;
mod compiler;
mod console;
mod crypto;
mod encoding;
Expand All @@ -33,7 +34,7 @@ use rquickjs::{AsyncContext, Module};
use std::{
env::{self},
error::Error,
path::Path,
path::{Path, PathBuf},
process::exit,
time::Instant,
};
Expand All @@ -44,6 +45,7 @@ use util::{get_basename_ext_name, get_js_path, JS_EXTENSIONS};
use vm::Vm;

use crate::{
compiler::compile_file,
console::ENV_LLRT_CONSOLE_NEWLINE_RETURN,
process::{get_arch, get_platform},
util::walk_directory,
Expand Down Expand Up @@ -89,14 +91,20 @@ Usage:
llrt -v | --version
llrt -h | --help
llrt -e | --eval <source>
llrt compile input.js [output.lrt]
llrt test <test_args>
Options:
-v, --version Print version information
-h, --help Print this help message
-e, --eval Evaluate the provided source code
compile Compile JS to bytecode and compress it with zstd:
if [output.lrt] is omitted, <input>.lrt is used.
lrt file is expected to be executed by the llrt version
that created it
test Run tests with provided arguments:
<test_args> -d <directory> <test-filter>"#
<test_args> -d <directory> <test-filter>
"#
);
}

Expand Down Expand Up @@ -140,6 +148,28 @@ async fn start_cli(context: &AsyncContext) {
}
return;
}
"compile" => {
if let Some(filename) = args.get(i + 1) {
let output_filename = if let Some(arg) = args.get(i + 2) {
arg.to_string()
} else {
let mut buf = PathBuf::from(filename);
buf.set_extension("lrt");
buf.to_string_lossy().to_string()
};

let filename = Path::new(filename);
let output_filename = Path::new(&output_filename);
if let Err(error) = compile_file(filename, output_filename).await {
eprintln!("{error}");
exit(1);
}
return;
} else {
eprintln!("compile: input filename is required.");
exit(1);
}
}
_ => {}
}

Expand Down
2 changes: 1 addition & 1 deletion src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub fn uncompressed_size(input: &[u8]) -> StdResult<(usize, &[u8]), io::Error> {
Ok((uncompressed_size, rest))
}

static COMPRESSION_DICT: &[u8] = include_bytes!("../bundle/compression.dict");
pub(crate) static COMPRESSION_DICT: &[u8] = include_bytes!("../bundle/compression.dict");

static DECOMPRESSOR_DICT: Lazy<DecoderDictionary> =
Lazy::new(|| DecoderDictionary::copy(COMPRESSION_DICT));
Expand Down

0 comments on commit d10661f

Please sign in to comment.