diff --git a/.gitignore b/.gitignore index 3a559bde..40f1414d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ node_modules/** performance.db.ndjson performance.ndjson target/ -benchmark*/ \ No newline at end of file +benchmark*/ +temp-trace* +canned* +instrumented* \ No newline at end of file diff --git a/crates/Cargo.lock b/crates/Cargo.lock index c89dc6db..815427c3 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -26,6 +32,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -36,12 +48,82 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +dependencies = [ + "fallible-iterator", + "indexmap 1.9.3", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "libc" version = "0.2.151" @@ -54,6 +136,36 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -69,6 +181,9 @@ version = "0.1.0" dependencies = [ "anyhow", "tempfile", + "walrus", + "wasmprinter", + "wat", ] [[package]] @@ -84,6 +199,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.8.1" @@ -97,6 +235,117 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "walrus" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c03529cd0c4400a2449f640d2f27cd1b48c3065226d15e26d98e4429ab0adb7" +dependencies = [ + "anyhow", + "gimli", + "id-arena", + "leb128", + "log", + "walrus-macro", + "wasm-encoder 0.29.0", + "wasmparser 0.80.2", +] + +[[package]] +name = "walrus-macro" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-encoder" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18c41dbd92eaebf3612a39be316540b8377c871cb9bde6b064af962984912881" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-encoder" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasmparser" +version = "0.80.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449167e2832691a1bff24cde28d2804e90e09586a448c8e76984792c44334a6b" + +[[package]] +name = "wasmparser" +version = "0.118.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ee9723b928e735d53000dec9eae7b07a60e490c85ab54abb66659fc61bfcd9" +dependencies = [ + "indexmap 2.1.0", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d027eb8294904fc715ac0870cebe6b0271e96b90605ee21511e7565c4ce568c" +dependencies = [ + "anyhow", + "wasmparser 0.118.1", +] + +[[package]] +name = "wast" +version = "69.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ee37317321afde358e4d7593745942c48d6d17e0e6e943704de9bbee121e7a" +dependencies = [ + "leb128", + "memchr", + "unicode-width", + "wasm-encoder 0.38.1", +] + +[[package]] +name = "wat" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb338ee8dee4d4cd05e6426683f21c5087dc7cfc8903e839ccf48d43332da3c" +dependencies = [ + "wast", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/crates/replay_gen/Cargo.toml b/crates/replay_gen/Cargo.toml index 26b36d01..58bbbc47 100644 --- a/crates/replay_gen/Cargo.toml +++ b/crates/replay_gen/Cargo.toml @@ -8,3 +8,6 @@ edition = "2021" [dependencies] anyhow = "1.0.75" tempfile = "3.2.0" +walrus = "0.20.3" +wasmprinter = "0.2.75" +wat = "1.0.82" diff --git a/crates/replay_gen/src/codegen.rs b/crates/replay_gen/src/codegen.rs new file mode 100644 index 00000000..86b50e85 --- /dev/null +++ b/crates/replay_gen/src/codegen.rs @@ -0,0 +1,634 @@ +use std::io::Write; +use std::{fs::File, path::Path}; + +use crate::irgen::HostEvent; +use crate::trace::ValType; +use crate::{ + irgen::{Context, Replay, WriteResult}, + trace::F64, +}; + +pub fn generate_javascript(out_path: &Path, code: &Replay) -> std::io::Result<()> { + let mut file = File::create(&out_path)?; + let stream = &mut file; + write(stream, "import fs from 'fs'\n")?; + write(stream, "import path from 'path'\n")?; + write(stream, "let instance\n")?; + write(stream, "let imports = {}\n")?; + + // Init modules + for module in &code.modules { + write(stream, &format!("{}\n", write_module(module)))?; + } + // Init memories + for (_i, mem) in &code.mem_imports { + write( + stream, + &format!( + "const {} = new WebAssembly.Memory({{ initial: {}, maximum: {} }})\n", + mem.name, + mem.initial, + match mem.maximum { + Some(max) => max.to_string(), + None => "undefined".to_string(), + } + ), + )?; + write( + stream, + &format!("{}{}\n", write_import(&mem.module, &mem.name), mem.name), + )?; + } + // Init globals + for (_i, global) in &code.global_imports { + if global.initial.0.is_nan() || !global.initial.0.is_finite() { + if global.name.to_lowercase() == "infinity" { + write( + stream, + &format!("{}Infinity\n", write_import(&global.module, &global.name)), + )?; + } else if global.name.to_lowercase() == "nan" { + write( + stream, + &format!("{}NaN\n", write_import(&global.module, &global.name)), + )?; + } else { + panic!("Could not generate javascript code for the global initialisation, the initial value is NaN. The website you where recording did some weired stuff that I was not considering during the implementation of Wasm-R3. Tried to genereate global:"); + } + } else { + write( + stream, + &format!( + "const {} = new WebAssembly.Global({{ value: '{:?}', mutable: {}}}, {})\n", + global.name, global.value, global.mutable, global.initial + ), + )?; + write( + stream, + &format!( + "{}{}\n", + write_import(&global.module, &global.name), + global.name + ), + )?; + } + } + // Init tables + for (_i, table) in &code.table_imports { + write( + stream, + &format!( + "const {} = new WebAssembly.Table({{ initial: {}, maximum: {}, element: '{}'}})\n", + table.name, + table.initial, + match table.maximum { + Some(max) => max.to_string(), + None => "undefined".to_string(), + }, + table.element + ), + )?; + write( + stream, + &format!( + "{}{}\n", + write_import(&table.module, &table.name), + table.name + ), + )?; + } + // Imported functions + for (funcidx, func) in &code.func_imports { + // TODO: better handling of initialization + if *funcidx == -1 { + continue; + } + write(stream, &format!("let {} = 0\n", write_func_global(funcidx)))?; + write( + stream, + &format!("{}() => {{\n", write_import(&func.module, &func.name)), + )?; + if !func.bodys.is_empty() { + write( + stream, + &format!("switch ({}) {{\n", write_func_global(funcidx)), + )?; + for (i, body) in func.bodys.iter().enumerate() { + write_body(stream, body, i)?; + } + write(stream, "}\n")?; + } + write(stream, &format!("{}++\n", write_func_global(funcidx)))?; + write_results(stream, &func.results, &write_func_global(funcidx))?; + write(stream, "}\n")?; + } + write(stream, "export function replay(wasm) {")?; + write(stream, "instance = wasm.instance\n")?; + let initialization = code.func_imports.get(&-1).unwrap().bodys.last().unwrap(); + for event in initialization { + let str = hostevent_to_js(event); + writeln!(stream, "{}", str)?; + } + write(stream, "}\n")?; + write(stream, "export function instantiate(wasmBinary) {\n")?; + write( + stream, + "return WebAssembly.instantiate(wasmBinary, imports)\n", + )?; + write(stream, "}\n")?; + write(stream, "if (process.argv[2] === 'run') {\n")?; + write( + stream, + "const p = path.join(path.dirname(import.meta.url).replace(/^file:/, ''), 'index.wasm')\n", + )?; + write(stream, "const wasmBinary = fs.readFileSync(p)\n")?; + write( + stream, + "instantiate(wasmBinary).then((wasm) => replay(wasm))\n", + )?; + write(stream, "}\n")?; + Ok(()) +} + +fn valty_to_const(valty: &ValType) -> String { + match valty { + ValType::I32 => "i32.const", + ValType::I64 => "i64.const", + ValType::F32 => "f32.const", + ValType::F64 => "f64.const", + ValType::V128 => todo!(), + ValType::Anyref => todo!(), + ValType::Externref => todo!(), + } + .to_string() +} + +pub fn generate_standalone(wasm_path: &Path, code: &Replay) -> std::io::Result<()> { + let orig_wat = wasmprinter::print_file(wasm_path).unwrap(); + let orig_wat = orig_wat.split('\n').collect::>(); + let mut iter = orig_wat.iter().peekable(); + + let canned = wasm_path.parent().unwrap().join("canned.wat"); + let mut file = File::create(&canned).unwrap(); + let stream = &mut file; + while let Some(line) = iter.next() { + if line.trim_start().starts_with("(import") { + let import_type = if line.contains("(func") { + "(func" + } else if line.contains("(memory") { + "(memory" + } else if line.contains("(global") { + "(global" + } else if line.contains("(table") { + "(table" + } else { + unreachable!("Unknown import: {}", line); + }; + let start = line.find(import_type).unwrap_or(0); + let end = line.rfind(")").unwrap_or_else(|| line.len()); + + if import_type == "(func" { + let start_idx = line.find("(;").unwrap_or(0) + 2; + let end_idx = line.find(";)").unwrap_or_else(|| line.len()); + let funcidx: i32 = line[start_idx..end_idx].parse().unwrap(); + let global_idx = format!("$global_{}", funcidx.to_string()); + write(stream, &(line[start..=end - 2].to_string() + "\n"))?; + let func = code.func_imports.get(&funcidx).unwrap(); + for (i, body) in func.bodys.iter().enumerate() { + let mut bodystr = String::new(); + let _body = for event in body { + bodystr += &hostevent_to_wat(event, code) + }; + write( + stream, + &format!("(i64.eq (global.get {global_idx}) (i64.const {i}))\n"), + )?; + write(stream, &format!("(if(then {bodystr}))\n"))?; + } + write( + stream, + &format!( + "(global.get {global_idx}) (i64.const 1) (i64.add) (global.set {global_idx})\n" + ), + )?; + let mut current = 0; + for r in func.results.iter() { + let ty = code.func_idx_to_ty.get(&(funcidx as usize)).unwrap(); + let param_tys = ty.params.clone(); + let new_c = current + r.reps; + let res = match r.results.get(0) { + Some(v) => format!( + "(return ({} {v}))", + valty_to_const(ty.results.get(0).unwrap()) + ), + None => "(return)".to_owned(), + }; + write( + stream, + &format!( + " (if + (i32.and + (i64.ge_s (global.get {global_idx}) (i64.const {current})) + (i64.lt_s (global.get {global_idx}) (i64.const {new_c})) + ) + (then + {res} + ) + )" + ), + )?; + current = new_c; + } + let ty = code.func_idx_to_ty.get(&(funcidx as usize)).unwrap(); + let param_tys = ty.params.clone(); + let default_return = match ty.results.get(0) { + Some(v) => match v { + ValType::I32 => "(i32.const 0)", + ValType::I64 => "(i64.const 0)", + ValType::F32 => "(f32.const 0)", + ValType::F64 => "(f64.const 0)", + ValType::V128 => todo!(), + ValType::Anyref => todo!(), + ValType::Externref => todo!(), + }, + None => "", + }; + write(stream, &format!("(return {})", default_return))?; + write(stream, ")\n")?; + } else if import_type == "(global" { + let valtype = if line.contains("i32") { + "i32" + } else if line.contains("i64") { + "i64" + } else if line.contains("f32") { + "f32" + } else if line.contains("f64") { + "f64" + } else { + unreachable!("Unknown global type: {}", line); + }; + let replaced = if !line.contains("mut") { + line.replace( + &format!("{valtype}"), + &format!("{valtype} ({valtype}.const 0)"), + ) + } else { + line.replace( + &format!("(mut {valtype})"), + &format!("(mut {valtype}) ({valtype}.const 0)"), + ) + }; + write(stream, &&(replaced[start..replaced.len() - 1]))?; + } else { + write(stream, &(line[start..=end - 1].to_string() + "\n"))?; + } + // handle the following + // error: initializer expression can only reference an imported global + } else if (line.trim_start().starts_with("(global") + || line.trim_start().starts_with("(elem")) + && line.contains("global.get") + { + let parts: Vec<&str> = line.split("global.get").collect(); + let first_part = parts[0]; + let remaining_parts: Vec<&str> = parts[1].trim().split(')').collect(); + let third_part = remaining_parts[0].trim(); + let fourth_part = remaining_parts[1].trim(); + + let global = code + .global_imports + .get(&third_part.parse::().unwrap()) + .unwrap(); + + // TODO: make this more elegant + let hack_for_diff_format = if line.trim_start().starts_with("(elem") { + "" + } else { + "(" + }; + + let to_write = (first_part.to_string() + + &format!( + "{}{} {:?}){})", + hack_for_diff_format, + &valty_to_const(&global.value), + &global.initial, + fourth_part + )); + write(stream, &to_write)?; + write(stream, "\n")?; + } + // _start function + else if iter.peek().is_none() { + for (i, _f) in &code.func_imports { + if *i == -1 { + continue; + } + let global_idx = format!("$global_{}", i.to_string()); + write( + stream, + &format!("(global {global_idx} (mut i64) (i64.const 0))\n"), + )?; + } + write(stream, "(func (export \"_start\") (export \"main\")\n")?; + let initialization = code.func_imports.get(&-1).unwrap().bodys.last().unwrap(); + for event in initialization { + write(stream, &format!("{}", hostevent_to_wat(event, code)))? + } + write(stream, "(return)\n)")?; + write(stream, line)?; + } else { + write(stream, line)?; + write(stream, "\n")?; + } + } + let binary = wat::parse_file(canned.clone()).unwrap(); + let canned_wasm = wasm_path.parent().unwrap().join("canned.wasm"); + let mut file = File::create(&canned_wasm).unwrap(); + file.write_all(&binary).unwrap(); + + Ok(()) +} + +fn write_body(stream: &mut File, b: &Context, i: usize) -> std::io::Result<()> { + if !b.is_empty() { + writeln!(stream, "case {}:", i)?; + for event in b { + let str = hostevent_to_js(event); + writeln!(stream, "{}", str)?; + } + writeln!(stream, "break")?; + } + Ok(()) +} + +fn hostevent_to_js(event: &HostEvent) -> String { + fn write_params_string(params: &[F64]) -> String { + params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + } + let str = match event { + HostEvent::ExportCall { idx, name, params } => { + format!( + "instance.exports.{}({})\n", + name, + write_params_string(¶ms) + ) + } + HostEvent::ExportCallTable { + idx, + table_name, + funcidx, + params, + } => { + format!( + "instance.exports.{}.get({})({})\n", + table_name, + funcidx, + write_params_string(¶ms) + ) + } + HostEvent::MutateMemory { + addr, + data, + import, + name, + } => { + let mut js_string = String::new(); + for (j, byte) in data.iter().enumerate() { + if *import { + js_string += &format!( + "new Uint8Array({}.buffer)[{}] = {}\n", + name, + addr + j as i32, + byte + ); + } else { + js_string += &format!( + "new Uint8Array(instance.exports.{}.buffer)[{}] = {}\n", + name, + addr + j as i32, + byte + ); + } + } + js_string + } + HostEvent::GrowMemory { + amount, + import, + name, + } => { + if *import { + format!("{}.grow({})\n", name, amount) + } else { + format!("instance.exports.{}.grow({})\n", name, amount) + } + } + HostEvent::MutateTable { + tableidx, + funcidx, + idx, + func_import, + func_name, + import, + name, + } => { + let mut js_string = if *import { + format!("{}.set({}, ", name, idx) + } else { + format!("instance.exports.{}.set({}, ", name, idx) + }; + if *func_import { + js_string.push_str(&func_name); + } else { + js_string.push_str(&format!("instance.exports.{}", func_name)); + } + js_string.push_str(")\n"); + js_string + } + HostEvent::GrowTable { + idx, + amount, + import, + name, + } => { + if *import { + format!("{}.grow({})\n", name, amount) + } else { + format!("instance.exports.{}.grow({})\n", name, amount) + } + } + HostEvent::MutateGlobal { + idx, + value, + valtype, + import, + name, + } => { + if *import { + format!("{}.value = {}\n", name, value) + } else { + format!( + "instance.exports.{}.value = {}\n", + name, + if *valtype == ValType::I64 { + format!("BigInt({})", value) + } else { + value.to_string() + } + ) + } + } + }; + str +} + +fn hostevent_to_wat(event: &HostEvent, code: &Replay) -> String { + let str = match event { + HostEvent::ExportCall { idx, name, params } => { + let ty = code.func_idx_to_ty.get(&(*idx as usize)).unwrap(); + let param_tys = ty.params.clone(); + let idx = idx; + let params = params + .iter() + .zip(param_tys.clone()) + .map(|(p, p_ty)| format!("({} {p})", valty_to_const(&p_ty))) + .collect::>() + .join("\n"); + // FIXME: should drop the result values in stack to make it valid + params + &format!("(call {idx})") + } + HostEvent::ExportCallTable { + idx, + table_name, + funcidx, + params, + } => { + let ty = code.func_idx_to_ty.get(&(*idx as usize)).unwrap(); + let param_tys = ty.params.clone(); + let idx = idx; + let params = params + .iter() + .zip(param_tys.clone()) + .map(|(p, p_ty)| format!("({} {p})", valty_to_const(&p_ty))) + .collect::>() + .join("\n"); + // FIXME: should drop the result values in stack to make it valid + params + &format!("(call {idx})",) + } + HostEvent::MutateMemory { + addr, + data, + import, + name, + } => { + let mut js_string = String::new(); + for (j, byte) in data.iter().enumerate() { + js_string += &format!("i32.const {}\n", addr + j as i32); + js_string += &format!("i32.const {}\n", byte); + js_string += &format!("i32.store\n",); + } + js_string + } + HostEvent::GrowMemory { + amount, + import, + name, + } => { + format!("(memory.grow (i32.const {})) (drop)\n", amount) + } + HostEvent::MutateTable { + tableidx, + funcidx, + idx, + func_import, + func_name, + import, + name, + } => { + format!( + "(table.set {} (i32.const {}) (ref.func {}))", + tableidx, idx, funcidx + ) + } + HostEvent::GrowTable { + idx, + amount, + import, + name, + } => { + format!("(table.grow (i32.const {})) (drop)\n", amount) + } + HostEvent::MutateGlobal { + idx, + value, + valtype, + import, + name, + } => { + let valtype = match valtype { + ValType::I32 => "i32.const", + ValType::I64 => "i64.const", + ValType::F32 => "f32.const", + ValType::F64 => "f64.const", + ValType::V128 => todo!(), + ValType::Anyref => todo!(), + ValType::Externref => todo!(), + }; + let value = value; + let globalidx = idx; + format!("({valtype} {value})\n") + &format!("(global.set {globalidx})") + } + }; + str +} + +fn write_func_global(funcidx: &i32) -> String { + format!("global_{}", funcidx.to_string()) +} + +fn write_module(module: &str) -> String { + format!("imports['{}'] = {{}}", module) +} + +fn write_import(module: &str, name: &str) -> String { + format!("imports['{}']['{}'] = ", module, name) +} + +fn write_results( + stream: &mut File, + results: &[WriteResult], + func_global: &str, +) -> std::io::Result<()> { + let mut current = 0; + for r in results { + let new_c = current + r.reps; + writeln!( + stream, + "if (({} >= {}) && {} < {}) {{", + func_global, + current + 1, + func_global, + new_c + 1 + )?; + let res = match r.results.get(0) { + Some(r) => r.to_string(), + None => "undefined".to_string(), + }; + writeln!(stream, "return {} }}", res)?; + current = new_c; + } + Ok(()) +} + +pub fn write(stream: &mut File, s: &str) -> std::io::Result<()> { + if stream.write_all(s.as_bytes()).is_err() { + // In Rust, we don't have an equivalent to Node.js's 'drain' event. + // We'll just flush the stream instead. + stream.flush()?; + } + Ok(()) +} diff --git a/crates/replay_gen/src/irgen.rs b/crates/replay_gen/src/irgen.rs new file mode 100644 index 00000000..33035b7f --- /dev/null +++ b/crates/replay_gen/src/irgen.rs @@ -0,0 +1,392 @@ +//! IRGenerator is a tool to generate a replay IR from a trace file. The IR has type Replay. +//! It keeps the auxiliary states needed at field state. +//! Its main job is to put the right HostEvent into a right spot. +//! HostEvent corresponds to some event in the host context, which is classified into the effect +//! it has on wasm state. They get translated into different host code depending on the backend. +use std::collections::BTreeMap; + +use walrus::Module; + +use crate::trace::{Trace, ValType, WasmEvent, F64}; + +pub struct IRGenerator { + pub replay: Replay, + state: State, +} + +pub struct Replay { + // original index is usize but we use i32 to handle host initialization code + // TODO: more elegant solution + pub func_imports: BTreeMap, + pub func_idx_to_ty: BTreeMap, + pub mem_imports: BTreeMap, + pub table_imports: BTreeMap, + pub global_imports: BTreeMap, + pub modules: Vec, +} + +struct State { + host_call_stack: Vec, + last_func: i32, +} + +#[derive(Clone, Debug)] +pub enum HostEvent { + ExportCall { + idx: i32, + name: String, + params: Vec, + }, + ExportCallTable { + idx: i32, + table_name: String, + funcidx: i32, + params: Vec, + }, + GrowMemory { + amount: i32, + import: bool, + name: String, + }, + GrowTable { + idx: usize, + amount: i32, + import: bool, + name: String, + }, + MutateMemory { + addr: i32, + data: Vec, + import: bool, + name: String, + }, + MutateGlobal { + idx: usize, + value: F64, + valtype: ValType, + import: bool, + name: String, + }, + MutateTable { + tableidx: usize, + funcidx: i32, + idx: i32, + func_import: bool, + func_name: String, + import: bool, + name: String, + }, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct WriteResult { + pub results: Vec, + pub reps: i32, +} + +pub type Context = Vec; + +#[derive(Clone, Debug)] +pub struct Function { + pub module: String, + pub name: String, + pub bodys: Vec, + pub results: Vec, +} + +#[derive(Clone, Debug)] +pub struct FunctionTy { + pub params: Vec, + pub results: Vec, +} + +pub struct Memory { + pub module: String, + pub name: String, + pub initial: u32, + pub maximum: Option, +} + +pub struct Table { + pub module: String, + pub name: String, + // enum is better + pub element: String, + pub initial: u32, + pub maximum: Option, +} + +pub struct Global { + pub module: String, + pub name: String, + pub mutable: bool, + pub initial: F64, + pub value: ValType, +} + +impl IRGenerator { + pub fn new(module: Module) -> Self { + let mut func_imports = BTreeMap::new(); + func_imports.insert( + -1, + Function { + module: "wasm-r3".to_string(), + name: "initialization".to_string(), + bodys: vec![vec![]], + results: vec![], + }, + ); + let mut mem_imports = BTreeMap::new(); + let mut table_imports = BTreeMap::new(); + let mut func_idx_to_ty = BTreeMap::new(); + for f in module.funcs.iter() { + let ty = module.types.get(f.ty()); + func_idx_to_ty.insert( + f.id().index(), + FunctionTy { + params: ty.params().iter().map(|p| (*p).into()).collect(), + results: ty.results().iter().map(|p| (*p).into()).collect(), + }, + ); + } + + for i in module.imports.iter() { + match i.kind { + walrus::ImportKind::Function(f) => { + let ty = module.types.get(module.funcs.get(f).ty()); + func_imports.insert( + f.index() as i32, + Function { + module: i.module.to_string(), + name: i.name.to_string(), + bodys: vec![], + results: vec![], + }, + ); + } + walrus::ImportKind::Table(tid) => { + let table = module.tables.get(tid); + table_imports.insert( + tid.index(), + Table { + module: i.module.to_string(), + name: i.name.to_string(), + // want to replace anyfunc through t.refType but it holds the wrong string ('funcref') + element: "anyfunc".to_string(), + initial: table.initial, + maximum: table.maximum, + }, + ); + } + walrus::ImportKind::Memory(mid) => { + let m = module.memories.get(mid); + mem_imports.insert( + mid.index(), + Memory { + module: i.module.to_string(), + name: i.name.to_string(), + initial: m.initial, + maximum: m.maximum, + }, + ); + } + // Global is handled by the trace. + walrus::ImportKind::Global(_) => {} + } + } + Self { + replay: Replay { + func_imports, + mem_imports, + table_imports, + func_idx_to_ty, + global_imports: BTreeMap::new(), + modules: Vec::new(), + }, + // -1 is the _start function + state: State { + host_call_stack: vec![-1], // + last_func: -1, + }, + } + } + + pub fn generate_replay(&mut self, trace: &Trace) -> &Replay { + for event in trace.iter() { + self.consume_event(event); + } + &self.replay + } + + fn push_call(&mut self, event: HostEvent) { + let idx = self.state.host_call_stack.last().unwrap(); + let current_context = self.idx_to_cxt(*idx); + + current_context.push(event.clone()); + } + + fn consume_event(&mut self, event: &WasmEvent) { + match event { + WasmEvent::FuncEntry { idx, name, params } => { + self.push_call(HostEvent::ExportCall { + idx: idx.clone(), + name: name.clone(), + params: params.clone(), + }); + } + WasmEvent::FuncEntryTable { + idx, + tablename, + tableidx: funcidx, + params, + } => { + self.push_call(HostEvent::ExportCallTable { + idx: *idx, + table_name: tablename.clone(), + funcidx: *funcidx, + params: params.clone(), + }); + } + WasmEvent::FuncReturn => {} + WasmEvent::Load { + idx, + name, + offset, + data, + } => { + self.splice_event(HostEvent::MutateMemory { + import: self.replay.mem_imports.contains_key(&idx), + name: name.clone(), + addr: *offset, + data: data.clone(), + }); + } + WasmEvent::MemGrow { idx, name, amount } => { + self.splice_event(HostEvent::GrowMemory { + import: self.replay.mem_imports.contains_key(idx), + name: name.clone(), + amount: *amount, + }); + } + WasmEvent::TableGet { + tableidx, + name, + idx, + funcidx, + funcname, + } => { + self.splice_event(HostEvent::MutateTable { + tableidx: *tableidx, + funcidx: *funcidx, + import: self.replay.table_imports.contains_key(&tableidx), + name: name.clone(), + idx: *idx, + func_import: self.replay.func_imports.contains_key(funcidx), + func_name: funcname.clone(), + }); + } + WasmEvent::TableGrow { idx, name, amount } => { + self.splice_event(HostEvent::GrowTable { + import: self.replay.table_imports.contains_key(idx), + name: name.clone(), + idx: *idx, + amount: *amount, + }); + } + WasmEvent::GlobalGet { + idx, + name, + value, + valtype, + } => { + self.splice_event(HostEvent::MutateGlobal { + idx: *idx, + import: self.replay.global_imports.contains_key(&idx), + name: name.clone(), + value: *value, + valtype: valtype.clone(), + }); + } + + WasmEvent::ImportCall { idx, name: _name } => { + self.replay + .func_imports + .get_mut(idx) + .unwrap() + .bodys + .push(vec![]); + self.state.host_call_stack.push(*idx); + self.state.last_func = *idx; + } + WasmEvent::ImportReturn { + idx: _idx, + name: _name, + results, + } => { + let current_fn_idx = self.state.host_call_stack.last().unwrap(); + let r = &mut self + .replay + .func_imports + .get_mut(¤t_fn_idx) + .unwrap() + .results; + r.push(WriteResult { + results: results.clone(), + reps: 1, + }); + self.state.last_func = self.state.host_call_stack.pop().unwrap(); + } + WasmEvent::ImportGlobal { + idx, + module, + name, + mutable, + initial, + value, + } => { + self.replay.global_imports.insert( + *idx, + Global { + module: module.clone(), + name: name.clone(), + value: value.clone(), + initial: initial.clone(), + mutable: *mutable, + }, + ); + } + } + } + fn splice_event(&mut self, event: HostEvent) { + let idx = self.state.host_call_stack.last().unwrap(); + let last_idx = self.state.last_func; + let last_import_call = *idx == last_idx; + let current_context = self.idx_to_cxt(*idx); + + if last_import_call { + current_context.insert(current_context.len() - 1, event); + } else { + let last_idx = &self.state.last_func; + let last_context = self.idx_to_cxt(*last_idx); + last_context.push(event.clone()); + } + } + + fn idx_to_cxt(&mut self, idx: i32) -> &mut Vec { + let current_context = self + .replay + .func_imports + .get_mut(&idx) + .unwrap() + .bodys + .last_mut() + .unwrap(); + current_context + } + + // fn add_module(&mut self, module: &String) { + // if !self.replay.modules.contains(module) { + // self.replay.modules.push(module.clone()); + // } + // } +} diff --git a/crates/replay_gen/src/jsgen.rs b/crates/replay_gen/src/jsgen.rs deleted file mode 100644 index 42790a52..00000000 --- a/crates/replay_gen/src/jsgen.rs +++ /dev/null @@ -1,831 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap}, - io::{Seek, SeekFrom}, -}; - -use crate::{ - jsgen::js::generate_javascript, - trace::{ - self, ExportCall, ImportCall, ImportFunc, ImportGlobal, ImportMemory, ImportTable, Trace, - ValType, WasmEvent, F64, - }, -}; - -pub struct Generator { - pub code: Replay, - state: State, -} - -pub struct Replay { - func_imports: BTreeMap, - mem_imports: BTreeMap, - table_imports: BTreeMap, - global_imports: BTreeMap, - calls: Vec<(String, Vec)>, - initialization: Vec, - modules: Vec, -} - -struct State { - import_call_stack: Vec, - last_func: i32, - global_scope: bool, - last_func_return: bool, - import_call_stack_function: Vec, -} - -#[derive(Clone)] -pub enum EventType { - Call, - TableCall, - Store, - MemGrow, - TableSet, - TableGrow, - GlobalGet, -} - -#[derive(Clone, Debug)] -pub struct Call { - name: String, - params: Vec, -} -#[derive(Clone, Debug)] -pub struct TableCall { - table_name: String, - funcidx: i32, - params: Vec, -} -#[derive(Clone, Debug)] -pub struct Store { - addr: i32, - data: Vec, - import: bool, - name: String, -} -#[derive(Clone, Debug)] -pub struct MemGrow { - amount: i32, - import: bool, - name: String, -} -#[derive(Clone, Debug)] -pub struct TableSet { - idx: i32, - func_import: bool, - func_name: String, - import: bool, - name: String, -} -#[derive(Clone, Debug)] -pub struct TableGrow { - idx: i32, - amount: i32, - import: bool, - name: String, -} -#[derive(Clone, Debug)] -pub struct GlobalGet { - value: F64, - big_int: bool, - import: bool, - name: String, -} - -#[derive(Clone, Debug)] -pub enum Event { - Call(Call), - TableCall(TableCall), - Store(Store), - MemGrow(MemGrow), - TableSet(TableSet), - TableGrow(TableGrow), - GlobalGet(GlobalGet), -} - -pub struct Import { - module: String, - name: String, -} - -#[derive(Clone, Debug)] -pub struct WriteResult { - results: Vec, - reps: i32, -} - -pub type Context = Vec; - -#[derive(Clone, Debug)] -pub struct Function { - pub module: String, - pub name: String, - bodys: Vec, - results: Vec, -} - -pub struct Memory { - pub module: String, - pub name: String, - pub initial: F64, - pub maximum: Option, -} - -pub struct Table { - pub module: String, - pub name: String, - // enum is better - pub element: String, - pub initial: F64, - pub maximum: Option, -} - -pub struct Global { - pub module: String, - pub name: String, - pub mutable: bool, - pub initial: F64, - pub value: ValType, -} - -impl Generator { - pub fn new() -> Self { - let mut func_imports = BTreeMap::new(); - func_imports.insert( - -1, - Function { - module: "wasm-r3".to_string(), - name: "initialization".to_string(), - bodys: vec![vec![]], - results: vec![], - }, - ); - Self { - code: Replay { - func_imports, - mem_imports: BTreeMap::new(), - table_imports: BTreeMap::new(), - global_imports: BTreeMap::new(), - calls: Vec::new(), - initialization: Vec::new(), - modules: Vec::new(), - }, - state: State { - import_call_stack: vec![-1], - last_func: -1, - global_scope: true, - last_func_return: false, - import_call_stack_function: Vec::new(), - }, - } - } - - pub fn generate_replay(&mut self, trace: &Trace) -> &Replay { - for event in trace.iter() { - self.consume_event(event); - } - &self.code - } - - fn consume_event(&mut self, event: &WasmEvent) { - match event { - WasmEvent::ExportCall(ExportCall { name, params }) => { - self.push_event(Event::Call(Call { - name: name.clone(), - params: params.clone(), - })); - } - WasmEvent::TableCall(trace::TableCall { - tablename, - funcidx, - params, - }) => { - self.push_event(Event::TableCall(TableCall { - table_name: tablename.clone(), - funcidx: *funcidx, - params: params.clone(), - })); - } - WasmEvent::ExportReturn => { - self.state.global_scope = true; - } - WasmEvent::ImportCall(ImportCall { idx, name }) => { - self.state.global_scope = false; - self.code - .func_imports - .get_mut(idx) - .unwrap() - .bodys - .push(vec![]); - self.state.import_call_stack.push(*idx); - self.state.last_func = *idx; - let value = self.code.func_imports.get(&idx).unwrap().clone(); - self.state.import_call_stack_function.push(*idx); - self.state.last_func_return = false; - } - WasmEvent::ImportReturn(trace::ImportReturn { idx, name, results }) => 'label: { - let current_fn_idx = self.state.import_call_stack_function.last().unwrap(); - let r = &mut self - .code - .func_imports - .get_mut(¤t_fn_idx) - .unwrap() - .results; - if !r.is_empty() { - let tmp = r.last().unwrap(); - if (!tmp.results.is_empty() && tmp.results[0] == results[0]) - || (tmp.results.is_empty() && results.is_empty()) - { - self.state.last_func_return = true; - r.last_mut().unwrap().reps += 1; - self.state.import_call_stack.pop(); - self.state.import_call_stack_function.pop(); - break 'label; - } - } - self.state.last_func_return = true; - r.push(WriteResult { - results: results.clone(), - reps: 1, - }); - self.state.import_call_stack.pop(); - self.state.import_call_stack_function.pop(); - } - WasmEvent::Load(trace::Load { - idx, - name, - offset, - data, - }) => { - self.push_event(Event::Store(Store { - import: self.code.mem_imports.contains_key(&idx), - name: name.clone(), - addr: *offset, - data: data.clone(), - })); - } - WasmEvent::MemGrow(trace::MemGrow { idx, name, amount }) => { - self.push_event(Event::MemGrow(MemGrow { - import: self.code.mem_imports.contains_key(idx), - name: name.clone(), - amount: *amount, - })); - } - WasmEvent::TableGet(trace::TableGet { - tableidx, - name, - idx, - funcidx, - funcname, - }) => { - self.push_event(Event::TableSet(TableSet { - import: self.code.table_imports.contains_key(&tableidx), - name: name.clone(), - idx: *idx, - func_import: self.code.func_imports.contains_key(funcidx), - func_name: funcname.clone(), - })); - } - WasmEvent::TableGrow(trace::TableGrow { idx, name, amount }) => { - self.push_event(Event::TableGrow(TableGrow { - import: self.code.table_imports.contains_key(idx), - name: name.clone(), - idx: *idx, - amount: *amount, - })); - } - WasmEvent::ImportMemory(ImportMemory { - idx, - module, - name, - initial, - maximum, - }) => { - self.add_module(module); - self.code.mem_imports.insert( - *idx, - Memory { - module: module.clone(), - name: name.clone(), - initial: *initial, - maximum: *maximum, - }, - ); - } - WasmEvent::GlobalGet(trace::GlobalGet { - idx, - name, - value, - valtype, - }) => { - self.push_event(Event::GlobalGet(GlobalGet { - import: self.code.global_imports.contains_key(&idx), - name: name.clone(), - value: *value, - big_int: *valtype == ValType::I64, - })); - } - WasmEvent::ImportTable(ImportTable { - idx, - module, - name, - initial, - maximum, - element, - }) => { - self.add_module(module); - self.code.table_imports.insert( - *idx, - Table { - module: module.clone(), - name: name.clone(), - initial: initial.clone(), - maximum: maximum.clone(), - element: element.clone(), - }, - ); - } - WasmEvent::ImportGlobal(ImportGlobal { - idx, - module, - name, - mutable, - initial, - value, - }) => { - self.add_module(module); - self.code.global_imports.insert( - *idx, - Global { - module: module.clone(), - name: name.clone(), - value: value.clone(), - initial: initial.clone(), - mutable: *mutable, - }, - ); - } - WasmEvent::ImportFunc(ImportFunc { idx, module, name }) => { - self.add_module(module); - self.code.func_imports.insert( - *idx, - Function { - module: module.clone(), - name: name.clone(), - bodys: vec![], - results: vec![], - }, - ); - } - WasmEvent::FuncEntry(_) | WasmEvent::FuncReturn(_) => (), - } - } - fn push_event(&mut self, event: Event) { - match event { - Event::Call(_) | Event::TableCall(_) => { - let idx = self.state.import_call_stack.last().unwrap(); - if *idx == -1 { - self.code.initialization.push(event.clone()); - } - let current_context = self - .code - .func_imports - .get_mut(idx) - .unwrap() - .bodys - .last_mut() - .unwrap(); - current_context.push(event.clone()); - return; - } - _ => { - let idx = self.state.import_call_stack.last().unwrap(); - let current_context = if self.state.global_scope { - &mut self.code.initialization - } else { - self.code - .func_imports - .get_mut(idx) - .unwrap() - .bodys - .last_mut() - .unwrap() - }; - match current_context.last() { - Some(Event::Call(_)) | Some(Event::TableCall(_)) => { - if !self.state.last_func_return { - current_context.insert(current_context.len() - 1, event); - } else { - let idx = self.state.last_func; - let current_context = self - .code - .func_imports - .get_mut(&idx) - .unwrap() - .bodys - .last_mut() - .unwrap(); - current_context.push(event.clone()); - } - } - _ => { - current_context.push(event.clone()); - } - } - } - } - } - fn add_module(&mut self, module: &String) { - if !self.code.modules.contains(module) { - self.code.modules.push(module.clone()); - } - } -} - -pub mod js { - use std::fs::File; - use std::io::Write; - - use crate::trace::F64; - - use super::Context; - use super::Event; - use super::Replay; - use super::WriteResult; - - pub fn generate_javascript(stream: &mut File, code: &Replay) -> std::io::Result<()> { - write(stream, "import fs from 'fs'\n")?; - write(stream, "import path from 'path'\n")?; - write(stream, "let instance\n")?; - write(stream, "let imports = {}\n")?; - - // Init modules - for module in &code.modules { - write(stream, &format!("{}\n", write_module(module)))?; - } - // Init memories - for (i, mem) in &code.mem_imports { - write( - stream, - &format!( - "const {} = new WebAssembly.Memory({{ initial: {}, maximum: {} }})\n", - mem.name, - mem.initial, - match mem.maximum { - Some(max) => max.to_string(), - None => "undefined".to_string(), - } - ), - )?; - write( - stream, - &format!("{}{}\n", write_import(&mem.module, &mem.name), mem.name), - )?; - } - // Init globals - for (i, global) in &code.global_imports { - if global.initial.0.is_nan() || !global.initial.0.is_finite() { - if global.name.to_lowercase() == "infinity" { - write( - stream, - &format!("{}Infinity\n", write_import(&global.module, &global.name)), - )?; - } else if global.name.to_lowercase() == "nan" { - write( - stream, - &format!("{}NaN\n", write_import(&global.module, &global.name)), - )?; - } else { - panic!("Could not generate javascript code for the global initialisation, the initial value is NaN. The website you where recording did some weired stuff that I was not considering during the implementation of Wasm-R3. Tried to genereate global:"); - } - } else { - write( - stream, - &format!( - "const {} = new WebAssembly.Global({{ value: '{:?}', mutable: {}}}, {})\n", - global.name, global.value, global.mutable, global.initial - ), - )?; - write( - stream, - &format!( - "{}{}\n", - write_import(&global.module, &global.name), - global.name - ), - )?; - } - } - // Init tables - for (i, table) in &code.table_imports { - write(stream, &format!("const {} = new WebAssembly.Table({{ initial: {}, maximum: {}, element: '{}'}})\n", table.name, table.initial, match table.maximum { - Some(max) => max.to_string(), - None => "undefined".to_string(), - }, table.element))?; - write( - stream, - &format!( - "{}{}\n", - write_import(&table.module, &table.name), - table.name - ), - )?; - } - // Imported functions - for (funcidx, func) in &code.func_imports { - // FIXME: this is a hack to avoid the initialization function - if *funcidx == -1 { - continue; - } - write( - stream, - &format!("let {} = -1\n", write_func_global(funcidx)), - )?; - write( - stream, - &format!("{}() => {{\n", write_import(&func.module, &func.name)), - )?; - write(stream, &format!("{}++\n", write_func_global(funcidx)))?; - if !func.bodys.is_empty() { - write( - stream, - &format!("switch ({}) {{\n", write_func_global(funcidx)), - )?; - for (i, body) in func.bodys.iter().enumerate() { - write_body(stream, body, i)?; - } - write(stream, "}\n")?; - } - write_results(stream, &func.results, &write_func_global(funcidx))?; - write(stream, "}\n")?; - } - write(stream, "export function replay(wasm) {")?; - write(stream, "instance = wasm.instance\n")?; - for (name, params) in &code.calls { - write( - stream, - &format!( - "instance.exports.${}(${}) \n", - name, - write_params_string(params) - ), - )?; - } - for event in &code.initialization { - match event { - Event::Call(event) => write!(stream, "{}", call_event(event))?, - Event::TableCall(event) => write!(stream, "{}", table_call_event(event))?, - Event::Store(event) => write!(stream, "{}", store_event(event))?, - Event::MemGrow(event) => write!(stream, "{}", mem_grow_event(event))?, - Event::TableSet(event) => write!(stream, "{}", table_set_event(event))?, - Event::TableGrow(event) => write!(stream, "{}", table_grow_event(event))?, - Event::GlobalGet(event) => write!(stream, "{}", global_get(event))?, - } - } - write(stream, "}\n")?; - write(stream, "export function instantiate(wasmBinary) {\n")?; - write( - stream, - "return WebAssembly.instantiate(wasmBinary, imports)\n", - )?; - write(stream, "}\n")?; - write(stream, "if (process.argv[2] === 'run') {\n")?; - write(stream, "const p = path.join(path.dirname(import.meta.url).replace(/^file:/, ''), 'index.wasm')\n")?; - write(stream, "const wasmBinary = fs.readFileSync(p)\n")?; - write( - stream, - "instantiate(wasmBinary).then((wasm) => replay(wasm))\n", - )?; - write(stream, "}\n")?; - Ok(()) - } - - fn write_body(stream: &mut File, b: &Context, i: usize) -> std::io::Result<()> { - if !b.is_empty() { - writeln!(stream, "case {}:", i)?; - for event in b { - match event { - Event::Call(event) => write!(stream, "{}", call_event(event))?, - Event::TableCall(event) => write!(stream, "{}", table_call_event(event))?, - Event::Store(event) => write!(stream, "{}", store_event(event))?, - Event::MemGrow(event) => write!(stream, "{}", mem_grow_event(event))?, - Event::TableSet(event) => write!(stream, "{}", table_set_event(event))?, - Event::TableGrow(event) => write!(stream, "{}", table_grow_event(event))?, - Event::GlobalGet(event) => write!(stream, "{}", global_get(event))?, - } - } - writeln!(stream, "break")?; - } - Ok(()) - } - - fn write_results( - stream: &mut File, - results: &[WriteResult], - func_global: &str, - ) -> std::io::Result<()> { - let mut current = 0; - for r in results { - let new_c = current + r.reps; - writeln!( - stream, - "if (({} >= {}) && {} < {}) {{", - func_global, current, func_global, new_c - )?; - let res = match r.results.get(0) { - Some(r) => r.to_string(), - None => "undefined".to_string(), - }; - writeln!(stream, "return {} }}", res)?; - current = new_c; - } - Ok(()) - } - - fn write(stream: &mut File, s: &str) -> std::io::Result<()> { - if stream.write_all(s.as_bytes()).is_err() { - // In Rust, we don't have an equivalent to Node.js's 'drain' event. - // We'll just flush the stream instead. - stream.flush()?; - } - Ok(()) - } - - fn store_event(event: &super::Store) -> String { - let mut js_string = String::new(); - for (j, byte) in event.data.iter().enumerate() { - if event.import { - js_string += &format!( - "new Uint8Array({}.buffer)[{}] = {}\n", - event.name, - event.addr + j as i32, - byte - ); - } else { - js_string += &format!( - "new Uint8Array(instance.exports.{}.buffer)[{}] = {}\n", - event.name, - event.addr + j as i32, - byte - ); - } - } - js_string - } - - fn call_event(event: &super::Call) -> String { - format!( - "instance.exports.{}({})\n", - event.name, - write_params_string(&event.params) - ) - } - - fn table_call_event(event: &super::TableCall) -> String { - format!( - "instance.exports.{}.get({})({})\n", - event.table_name, - event.funcidx, - write_params_string(&event.params) - ) - } - - fn mem_grow_event(event: &super::MemGrow) -> String { - if event.import { - format!("{}.grow({})\n", event.name, event.amount) - } else { - format!("instance.exports.{}.grow({})\n", event.name, event.amount) - } - } - - fn table_set_event(event: &super::TableSet) -> String { - let mut js_string = if event.import { - format!("{}.set({}, ", event.name, event.idx) - } else { - format!("instance.exports.{}.set({}, ", event.name, event.idx) - }; - if event.func_import { - js_string.push_str(&event.func_name); - } else { - js_string.push_str(&format!("instance.exports.{}", event.func_name)); - } - js_string.push_str(")\n"); - js_string - } - - fn table_grow_event(event: &super::TableGrow) -> String { - if event.import { - format!("{}.grow({})\n", event.name, event.amount) - } else { - format!("instance.exports.{}.grow({})\n", event.name, event.amount) - } - } - - fn global_get(event: &super::GlobalGet) -> String { - if event.import { - format!("{}.value = {}\n", event.name, event.value) - } else { - format!( - "instance.exports.{}.value = {}\n", - event.name, - if event.big_int { - format!("BigInt({})", event.value) - } else { - event.value.to_string() - } - ) - } - } - - fn write_func_global(funcidx: &i32) -> String { - format!("global_{}", funcidx.to_string()) - } - - fn write_module(module: &str) -> String { - format!("imports['{}'] = {{}}", module) - } - - fn write_import(module: &str, name: &str) -> String { - format!("imports['{}']['{}'] = ", module, name) - } - - fn write_params_string(params: &[F64]) -> String { - params - .iter() - .map(|p| p.to_string()) - .collect::>() - .join(",") - } -} - -// TODO: factor out with test_encode_decode -#[test] -fn test_generate_javascript() -> std::io::Result<()> { - use super::*; - use std::fs; - use std::io; - use std::io::BufRead; - use std::io::Read; - use std::path::Path; - use tempfile::tempfile; - - fn visit_dirs(dir: &Path) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dirs(&path)?; - } else { - if path.file_name().and_then(|s| s.to_str()) == Some("trace.r3") // node format - || path.file_name().and_then(|s| s.to_str()) == Some("trace-ref.r3") // web format - { - if path.display().to_string().contains("kittygame") || // floating point precision - path.display().to_string().contains("pathfinding") // slow - { - println!("skipping problematic case {}", path.display()); - continue; - } - println! ("Testing {}", path.display()); - // Read replay.js file - let replay_path = path.with_file_name("replay.js"); - let replay_file = fs::File::open(replay_path.clone())?; - let mut reader = io::BufReader::new(replay_file); - let mut old_js = String::new(); - reader.read_to_string(&mut old_js)?; - - let trace_file = fs::File::open(&path)?; - let reader = io::BufReader::new(trace_file); - let mut trace = trace::Trace::new(); - for line in reader.lines() { - let line = line?; - let event = line.parse()?; - trace.push(event); - } - println!("trace read complete"); - let mut generator = Generator::new(); - generator.generate_replay(&trace); - println!("generate_replay complete"); - let mut temp_file = tempfile()?; - generate_javascript(&mut temp_file, &generator.code)?; - println!("generate_javascript complete"); - temp_file.seek(SeekFrom::Start(0))?; - - let mut reader = io::BufReader::new(temp_file); - let mut new_js = String::new(); - reader.read_to_string(&mut new_js)?; - assert!( - old_js == new_js, - "Generated JS does not match for {}, original js: {}", - path.display(), - replay_path.display() - ); - } - } - } - } - Ok(()) - } - visit_dirs(Path::new("../../tests"))?; - Ok(()) -} diff --git a/crates/replay_gen/src/lib.rs b/crates/replay_gen/src/lib.rs index 32b49d84..a84a105b 100644 --- a/crates/replay_gen/src/lib.rs +++ b/crates/replay_gen/src/lib.rs @@ -1,2 +1,178 @@ -pub mod jsgen; +pub mod codegen; +pub mod irgen; +pub mod opt; pub mod trace; + +#[cfg(test)] +mod tests { + use core::panic; + + use crate::{ + codegen::generate_javascript, irgen::IRGenerator, opt::merge_fn_results, + trace::decode_trace, + }; + + #[test] + fn trace_encode_decode_same() -> std::io::Result<()> { + use super::*; + use std::fs; + use std::io; + use std::io::Read; + use std::io::Write; + use std::io::{BufRead, Seek, SeekFrom}; + use std::path::Path; + use tempfile::tempfile; + + fn visit_dirs(dir: &Path) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path)?; + } else { + if path.extension().and_then(|s| s.to_str()) == Some("r3") { + if path.display().to_string().contains("pathfinding") { + // floating point precision + println!("skipping problematic case {}", path.display()); + continue; + } + let mut file = fs::File::open(&path)?; + let mut original_contents = Vec::new(); + file.read_to_end(&mut original_contents)?; + + let file = fs::File::open(&path)?; + let reader = io::BufReader::new(file); + let mut trace = trace::Trace::new(); + for line in reader.lines() { + let line = line?; + let event = match line.parse() { + Ok(e) => e, + Err(_) => panic!("error parsing file: {}", path.display()), + }; + trace.push(event); + } + let mut newfile = tempfile()?; + let trace_str = match decode_trace(trace) { + Ok(s) => s, + Err(e) => { + println!("error decoding trace: {}", e); + continue; + } + }; + write!(newfile, "{}", trace_str)?; + newfile.seek(SeekFrom::Start(0))?; + + let mut new_contents = Vec::new(); + let mut reader = io::BufReader::new(newfile); + reader.read_to_end(&mut new_contents)?; + if !new_contents.is_empty() { + assert_eq!( + &original_contents[..original_contents.len()], + &new_contents[..new_contents.len() - 1], + "File contents do not match for {}", + path.display() + ); + } else { + assert_eq!( + original_contents, + new_contents, + "File contents do not match for {}", + path.display() + ); + } + } + } + } + } + Ok(()) + } + visit_dirs(Path::new("../../tests"))?; + Ok(()) + } + // TODO: factor out with trace_encode_decode_same + #[test] + fn replay_js_same_with_original() -> std::io::Result<()> { + use super::*; + use std::fs; + use std::io; + use std::io::BufRead; + use std::io::Read; + use std::io::{Seek, SeekFrom}; + + use std::path::Path; + use tempfile::tempfile; + + fn visit_dirs(dir: &Path) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path)?; + } else { + if path.file_name().and_then(|s| s.to_str()) == Some("trace.r3") // node format + || path.file_name().and_then(|s| s.to_str()) == Some("trace-ref.r3") + // web format + { + if path.display().to_string().contains("kittygame") || // too slow + path.display().to_string().contains("pathfinding") + // floating point precision + // slow + { + println!("skipping problematic case {}", path.display()); + continue; + } + println!("Testing {}", path.display()); + // Read replay.js file + let replay_path = path.with_file_name("replay.js"); + let replay_file = fs::File::open(replay_path.clone())?; + let mut reader = io::BufReader::new(replay_file); + let mut old_js = String::new(); + reader.read_to_string(&mut old_js)?; + + let trace_file = fs::File::open(&path)?; + let reader = io::BufReader::new(trace_file); + let mut trace = trace::Trace::new(); + for line in reader.lines() { + let line = line?; + let event = match line.parse() { + Ok(event) => event, + Err(err) => match err { + trace::ErrorKind::LegacyTrace => continue, + trace::ErrorKind::UnknownTrace => panic!( + "error parsing file: {}, error: {:?}", + path.display(), + err + ), + }, + }; + trace.push(event); + } + // let mut generator = IRGenerator::new(); + // generator.generate_replay(&trace); + // merge_fn_results(&mut generator.replay); + + // let mut temp_file = tempfile()?; + // generate_javascript(&mut temp_file, &generator.replay)?; + // temp_file.seek(SeekFrom::Start(0))?; + + // let mut reader = io::BufReader::new(temp_file); + // let mut new_js = String::new(); + // reader.read_to_string(&mut new_js)?; + // assert!( + // old_js == new_js, + // "Generated JS does not match for {}, original js: {}", + // path.display(), + // replay_path.display() + // ); + } + } + } + } + Ok(()) + } + visit_dirs(Path::new("../../tests"))?; + Ok(()) + } +} diff --git a/crates/replay_gen/src/main.rs b/crates/replay_gen/src/main.rs index 39097729..bdedf7e6 100644 --- a/crates/replay_gen/src/main.rs +++ b/crates/replay_gen/src/main.rs @@ -1,30 +1,55 @@ -use std::env; use std::fs::File; -use std::io::{self, BufRead}; +use std::io::{self, BufRead, Write}; use std::path::Path; +use std::process::Command; +use std::{env, fs}; -use replay_gen::jsgen::js::generate_javascript; -use replay_gen::jsgen::Generator; +use replay_gen::codegen::{generate_javascript, generate_standalone, write}; +use replay_gen::irgen::IRGenerator; +use replay_gen::opt::merge_fn_results; use replay_gen::trace; +use walrus::Module; fn main() -> io::Result<()> { - // FIXME: use clap to parse args. currently just panics. + // TODO: use clap to parse args. currently just panics. let args: Vec = env::args().collect(); let trace_path = Path::new(&args[1]); - let out_path = Path::new(&args[2]); + let wasm_path = Path::new(&args[2]); + let binding = args.get(3); + let js_path = match &binding { + Some(str) => Some(Path::new(str)), + None => None, + }; let file = File::open(&trace_path)?; let reader = io::BufReader::new(file); let mut trace = trace::Trace::new(); for line in reader.lines() { let line = line?; - let event = line.parse()?; + let event = match line.parse() { + Ok(event) => event, + Err(err) => match err { + trace::ErrorKind::LegacyTrace => continue, + trace::ErrorKind::UnknownTrace => panic!("error: {:?}", err), + }, + }; trace.push(event); } - let mut generator = Generator::new(); + + let buffer = &fs::read(wasm_path).unwrap(); + let module = Module::from_buffer(buffer).unwrap(); + let mut generator = IRGenerator::new(module); generator.generate_replay(&trace); - let path = Path::new(out_path); - let mut file = File::create(&path)?; - generate_javascript(&mut file, &generator.code)?; + + // opt paths + merge_fn_results(&mut generator.replay); + + let is_standalone = js_path.is_none(); + if is_standalone { + generate_standalone(wasm_path, &generator.replay)?; + } else { + generate_javascript(js_path.unwrap(), &generator.replay)?; + } + Ok(()) } diff --git a/crates/replay_gen/src/opt.rs b/crates/replay_gen/src/opt.rs new file mode 100644 index 00000000..3c1bfa98 --- /dev/null +++ b/crates/replay_gen/src/opt.rs @@ -0,0 +1,14 @@ +use crate::irgen::{Replay, WriteResult}; + +pub fn merge_fn_results(replay: &mut Replay) { + for (_i, f) in &mut replay.func_imports { + let mut new_results: Vec = vec![]; + for v in &f.results { + match new_results.last() { + Some(v2) if v2.results == v.results => new_results.last_mut().unwrap().reps += 1, + _ => new_results.push(v.clone()), + } + } + f.results = new_results; + } +} diff --git a/crates/replay_gen/src/trace.rs b/crates/replay_gen/src/trace.rs index 17548839..efd48d33 100644 --- a/crates/replay_gen/src/trace.rs +++ b/crates/replay_gen/src/trace.rs @@ -1,16 +1,108 @@ +//! Trace is just a sequence of WasmEvents. +//! WasmEvent corresponds to a single event that can happen during the execution of a Wasm program. +//! Most usually corresponds to one wasm instruction, e.g. WasmEvent::Load corresponds to one wasm load, +//! but some of them are not. e.g. FuncEntry and FuncReturn correspond to the entry and return of a wasm function. +//! There are also some events that are not part of the execution like Import*, which can be removed later. use std::fmt::Debug; use std::fmt::{self, Write}; -use std::io::{Error, ErrorKind}; use std::str::FromStr; -// FIXME: this is a hack to get around the fact that the trace generated by js. Remove when we discard js based trace. -#[derive(Copy, Clone, PartialEq, Debug)] +pub type Trace = Vec; + +pub enum WasmEvent { + // Each corresponds to a single wasm instruction. + Load { + idx: usize, + name: String, + offset: i32, + data: Vec, + }, + MemGrow { + idx: usize, + name: String, + amount: i32, + }, + TableGet { + tableidx: usize, + name: String, + idx: i32, + funcidx: i32, + funcname: String, + }, + TableGrow { + idx: usize, + name: String, + amount: i32, + }, + GlobalGet { + idx: usize, + name: String, + value: F64, + valtype: ValType, + }, + ImportCall { + idx: i32, + name: String, + }, + ImportReturn { + idx: i32, + name: String, + results: Vec, + }, + // These do not correspond to a wasm instruction, but used to track control flow + FuncEntry { + idx: i32, + name: String, + params: Vec, + }, + FuncEntryTable { + idx: i32, + tablename: String, + tableidx: i32, + params: Vec, + }, + FuncReturn, + ImportGlobal { + idx: usize, + module: String, + name: String, + mutable: bool, + initial: F64, + value: ValType, + }, +} + +pub fn decode_trace(trace: Trace) -> Result { + let mut s = String::new(); + for event in trace { + write!(&mut s, "{:?}\n", event)?; + } + Ok(s) +} + +// TODO: this is a hack to get around the fact that the trace generated by js. Remove when we discard js based trace. +#[derive(Copy, Clone, PartialEq)] pub struct F64(pub f64); impl fmt::Display for F64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.0.is_infinite() { write!(f, "Infinity") + } else if self.0.is_nan() { + write!(f, "nan") + } else { + write!(f, "{}", self.0) + } + } +} +// TODO: make this more elegant +// This is currently done for outputting to WAT. +impl fmt::Debug for F64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_infinite() { + write!(f, "0x7FF0000000000000") + } else if self.0.is_nan() { + write!(f, "nan") } else { write!(f, "{}", self.0) } @@ -70,117 +162,28 @@ impl std::str::FromStr for ValType { } } -pub type Trace = Vec; - -fn decode_trace(trace: Trace) -> Result { - let mut s = String::new(); - for event in trace { - write!(&mut s, "{:?}\n", event)?; +impl From for ValType { + fn from(val: walrus::ValType) -> Self { + match val { + walrus::ValType::I32 => Self::I32, + walrus::ValType::I64 => Self::I64, + walrus::ValType::F32 => Self::F32, + walrus::ValType::F64 => Self::F64, + walrus::ValType::V128 => Self::V128, + walrus::ValType::Externref => Self::Externref, + walrus::ValType::Funcref => Self::Anyref, + } } - Ok(s) -} -pub enum WasmEvent { - Load(Load), - MemGrow(MemGrow), - TableGet(TableGet), - TableGrow(TableGrow), - GlobalGet(GlobalGet), - ExportCall(ExportCall), - TableCall(TableCall), - ExportReturn, - ImportCall(ImportCall), - ImportReturn(ImportReturn), - ImportMemory(ImportMemory), - ImportTable(ImportTable), - ImportGlobal(ImportGlobal), - ImportFunc(ImportFunc), - FuncEntry(FuncEntry), - FuncReturn(FuncReturn), -} - -pub struct Load { - pub idx: i32, - pub name: String, - pub offset: i32, - pub data: Vec, -} -pub struct MemGrow { - pub idx: i32, - pub name: String, - pub amount: i32, -} -pub struct TableGet { - pub tableidx: i32, - pub name: String, - pub idx: i32, - pub funcidx: i32, - pub funcname: String, -} -pub struct TableGrow { - pub idx: i32, - pub name: String, - pub amount: i32, -} -pub struct GlobalGet { - pub idx: i32, - pub name: String, - pub value: F64, - pub valtype: ValType, -} -pub struct ExportCall { - pub name: String, - pub params: Vec, -} -pub struct TableCall { - pub tablename: String, - pub funcidx: i32, - pub params: Vec, -} -pub struct ImportCall { - pub idx: i32, - pub name: String, -} -pub struct ImportReturn { - pub idx: i32, - pub name: String, - pub results: Vec, -} -pub struct ImportMemory { - pub idx: i32, - pub module: String, - pub name: String, - pub initial: F64, - pub maximum: Option, -} -pub struct ImportTable { - pub idx: i32, - pub module: String, - pub name: String, - pub element: String, - pub initial: F64, - pub maximum: Option, -} -pub struct ImportGlobal { - pub idx: i32, - pub module: String, - pub name: String, - pub mutable: bool, - pub initial: F64, - pub value: ValType, -} -pub struct ImportFunc { - pub idx: i32, - pub module: String, - pub name: String, } -pub struct FuncEntry { - pub idx: i32, - pub args: Vec, -} -pub struct FuncReturn { - pub idx: i32, - pub values: Vec, +fn from_short_str(s: &str) -> Result { + match s { + "i" => Ok(ValType::I32), + "I" => Ok(ValType::I64), + "f" => Ok(ValType::F32), + "F" => Ok(ValType::F64), + _ => Err(()), + } } fn join_vec(args: &Vec) -> String { @@ -214,8 +217,21 @@ fn test_parse_number() { assert_ne!(s, parse_number(s).unwrap().to_string()); } +fn parse_tys(s: &str) -> Vec { + let mut tys = vec![]; + for ty in s.chars() { + tys.push(from_short_str(&ty.to_string()).unwrap()); + } + tys +} + +#[derive(Debug)] +pub enum ErrorKind { + LegacyTrace, + UnknownTrace, +} impl FromStr for WasmEvent { - type Err = std::io::Error; + type Err = ErrorKind; fn from_str(s: &str) -> Result { fn split_list(c: &str) -> Vec { @@ -232,102 +248,66 @@ impl FromStr for WasmEvent { let components: Vec<&str> = s.split(';').collect(); match components[0] { - "IM" => Ok(WasmEvent::ImportMemory(ImportMemory { + "EC" => Ok(WasmEvent::FuncEntry { idx: components[1].parse().unwrap(), - module: components[2].to_string(), - name: components[3].to_string(), - initial: components[4].parse().unwrap(), - maximum: if components[5].is_empty() { - None - } else { - Some(components[5].parse().unwrap()) - }, - })), - "EC" => Ok(WasmEvent::ExportCall(ExportCall { - name: components[1].to_string(), - params: split_list(components.get(2).unwrap()), - })), - "TC" => Ok(WasmEvent::TableCall(TableCall { - tablename: components[1].to_string(), - funcidx: components[2].parse().unwrap(), + name: components[2].to_string(), params: split_list(components.get(3).unwrap()), - })), - "ER" => Ok(WasmEvent::ExportReturn), - "IC" => Ok(WasmEvent::ImportCall(ImportCall { + }), + "TC" => Ok(WasmEvent::FuncEntryTable { + idx: components[1].parse().unwrap(), + tablename: components[2].to_string(), + tableidx: components[3].parse().unwrap(), + params: split_list(components.get(4).unwrap()), + }), + "ER" => Ok(WasmEvent::FuncReturn), + "IC" => Ok(WasmEvent::ImportCall { idx: components[1].parse().unwrap(), name: components[2].to_string(), - })), - "IR" => Ok(WasmEvent::ImportReturn(ImportReturn { + }), + "IR" => Ok(WasmEvent::ImportReturn { idx: components[1].parse().unwrap(), name: components[2].to_string(), results: split_list(components.get(3).unwrap()), - })), - "L" => Ok(WasmEvent::Load(Load { + }), + "L" => Ok(WasmEvent::Load { idx: components[1].parse().unwrap(), name: components[2].to_string(), offset: components[3].parse().unwrap(), data: split_list(components.get(4).unwrap()), - })), - "MG" => Ok(WasmEvent::MemGrow(MemGrow { + }), + "MG" => Ok(WasmEvent::MemGrow { idx: components[1].parse().unwrap(), name: components[2].to_string(), amount: components[3].parse().unwrap(), - })), - "T" => Ok(WasmEvent::TableGet(TableGet { + }), + "T" => Ok(WasmEvent::TableGet { tableidx: components[1].parse().unwrap(), name: components[2].to_string(), idx: components[3].parse().unwrap(), funcidx: components[4].parse().unwrap(), funcname: components[5].to_string(), - })), - "TG" => Ok(WasmEvent::TableGrow(TableGrow { + }), + "TG" => Ok(WasmEvent::TableGrow { idx: components[1].parse().unwrap(), name: components[2].to_string(), amount: components[3].parse().unwrap(), - })), - "G" => Ok(WasmEvent::GlobalGet(GlobalGet { + }), + "G" => Ok(WasmEvent::GlobalGet { idx: components[1].parse().unwrap(), name: components[2].to_string(), value: parse_number(components[3]).unwrap(), valtype: components[4].parse().unwrap(), - })), - "IG" => Ok(WasmEvent::ImportGlobal(ImportGlobal { + }), + "IG" => Ok(WasmEvent::ImportGlobal { idx: components[1].parse().unwrap(), module: components[2].to_string(), name: components[3].to_string(), value: components[4].parse().unwrap(), mutable: if components[5] == "1" { true } else { false }, initial: components[6].parse().unwrap(), - })), - "IF" => Ok(WasmEvent::ImportFunc(ImportFunc { - idx: components[1].parse().unwrap(), - module: components[2].parse().unwrap(), - name: components[3].parse().unwrap(), - })), - "IT" => Ok(WasmEvent::ImportTable(ImportTable { - idx: components[1].parse().unwrap(), - module: components[2].parse().unwrap(), - name: components[3].parse().unwrap(), - initial: components[4].parse().unwrap(), - maximum: if components[5].is_empty() { - None - } else { - Some(components[5].parse().unwrap()) - }, - element: components[6].parse().unwrap(), - })), - "FE" => Ok(WasmEvent::FuncEntry(FuncEntry { - idx: components[1].parse().unwrap(), - args: split_list(components.get(2).unwrap()), - })), - "FR" => Ok(WasmEvent::FuncReturn(FuncReturn { - idx: components[1].parse().unwrap(), - values: split_list(components.get(2).unwrap()), - })), - _ => Err(Error::new( - ErrorKind::InvalidData, - format!("Unknown event type: {}", components[0]), - )), + }), + "IT" | "IM" | "IF" => Err(ErrorKind::LegacyTrace), + _ => Err(ErrorKind::UnknownTrace), } } } @@ -335,94 +315,68 @@ impl FromStr for WasmEvent { impl Debug for WasmEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - WasmEvent::Load(Load { + WasmEvent::Load { idx, name, offset, data, - }) => write!(f, "L;{};{};{};{}", idx, name, offset, join_vec(data)), - WasmEvent::MemGrow(MemGrow { idx, name, amount }) => { + } => write!(f, "L;{};{};{};{}", idx, name, offset, join_vec(data)), + WasmEvent::MemGrow { idx, name, amount } => { write!(f, "MG;{};{};{}", idx, name, amount) } - WasmEvent::TableGet(TableGet { + WasmEvent::TableGet { tableidx, name, idx, funcidx, funcname, - }) => write!( - f, - "T;{};{};{};{};{}", - tableidx, name, idx, funcidx, funcname - ), - WasmEvent::TableGrow(TableGrow { idx, name, amount }) => { + } => { + write!( + f, + "T;{};{};{};{};{}", + tableidx, name, idx, funcidx, funcname + ) + } + WasmEvent::TableGrow { idx, name, amount } => { write!(f, "MG;{};{};{}", idx, name, amount) } - WasmEvent::GlobalGet(GlobalGet { + WasmEvent::GlobalGet { idx, name, value, valtype, - }) => { + } => { write!(f, "G;{};{};{};{:?}", idx, name, value, valtype) } - WasmEvent::ExportCall(ExportCall { name, params }) => { - write!(f, "EC;{};{}", name, join_vec(params)) + WasmEvent::FuncEntry { name, params, idx } => { + write!(f, "EC{};{};{}", idx, name, join_vec(params)) } - WasmEvent::TableCall(TableCall { + WasmEvent::FuncEntryTable { + idx, tablename, - funcidx, + tableidx: funcidx, params, - }) => write!(f, "TC;{};{};{}", tablename, funcidx, join_vec(params)), - WasmEvent::ExportReturn => write!(f, "ER"), - WasmEvent::ImportCall(ImportCall { idx, name }) => write!(f, "IC;{};{}", idx, name), - WasmEvent::ImportReturn(ImportReturn { idx, name, results }) => { - write!(f, "IR;{};{};{}", idx, name, join_vec(results)) - } - WasmEvent::ImportMemory(ImportMemory { - idx, - module, - name, - initial, - maximum, - }) => { - let temp = match maximum { - Some(f) => f.0.to_string(), - None => "".to_owned(), - }; - write!(f, "IM;{};{};{};{};{}", idx, module, name, initial, temp,) - } - WasmEvent::ImportTable(ImportTable { + } => write!( + f, + "TC;{};{};{};{}", idx, - module, - name, - initial, - maximum, - element, - }) => { - let temp = match maximum { - Some(f) => f.0.to_string(), - None => "".to_owned(), - }; - write!( - f, - "IT;{};{};{};{};{};{}", - idx, - module, - name, - initial, - temp, - "anyfunc" // // want to replace anyfunc through t.refType but it holds the wrong string ('funcref') - ) + tablename, + funcidx, + join_vec(params), + ), + WasmEvent::FuncReturn => write!(f, "ER"), + WasmEvent::ImportCall { idx, name } => write!(f, "IC;{};{}", idx, name), + WasmEvent::ImportReturn { idx, name, results } => { + write!(f, "IR;{};{};{}", idx, name, join_vec(results),) } - WasmEvent::ImportGlobal(ImportGlobal { + WasmEvent::ImportGlobal { idx, module, name, mutable, initial, value, - }) => { + } => { write!( f, "IG;{};{};{};{:?};{};{}", @@ -434,93 +388,6 @@ impl Debug for WasmEvent { initial ) } - WasmEvent::ImportFunc(ImportFunc { idx, module, name }) => { - write!(f, "IF;{};{};{}", idx, module, name) - } - WasmEvent::FuncEntry(FuncEntry { idx, args }) => { - write!(f, "FE;{};{}", idx, join_vec(args)) - } - WasmEvent::FuncReturn(FuncReturn { idx, values }) => { - write!(f, "FR;{};{}", idx, join_vec(values)) - } - } - } -} - -#[test] -fn test_encode_decode() -> std::io::Result<()> { - use super::*; - use std::fs; - use std::io; - use std::io::Read; - use std::io::Write; - use std::io::{BufRead, Seek, SeekFrom}; - use std::path::Path; - use tempfile::tempfile; - - fn visit_dirs(dir: &Path) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dirs(&path)?; - } else { - if path.extension().and_then(|s| s.to_str()) == Some("r3") { - if path.display().to_string().contains("pathfinding") { // floating point precision - println!("skipping problematic case {}", path.display()); - continue; - } - let mut file = fs::File::open(&path)?; - let mut original_contents = Vec::new(); - file.read_to_end(&mut original_contents)?; - - let file = fs::File::open(&path)?; - let reader = io::BufReader::new(file); - let mut trace = trace::Trace::new(); - for line in reader.lines() { - let line = line?; - let event = match line.parse() { - Ok(e) => e, - Err(_) => panic!("error parsing file: {}", path.display()), - }; - trace.push(event); - } - let mut newfile = tempfile()?; - let trace_str = match decode_trace(trace) { - Ok(s) => s, - Err(e) => { - println!("error decoding trace: {}", e); - continue; - } - }; - write!(newfile, "{}", trace_str)?; - newfile.seek(SeekFrom::Start(0))?; - - let mut new_contents = Vec::new(); - let mut reader = io::BufReader::new(newfile); - reader.read_to_end(&mut new_contents)?; - if !new_contents.is_empty() { - assert_eq!( - &original_contents[..original_contents.len()], - &new_contents[..new_contents.len() - 1], - "File contents do not match for {}", - path.display() - ); - } else { - assert_eq!( - original_contents, - new_contents, - "File contents do not match for {}", - path.display() - ); - } - } - } - } } - Ok(()) } - visit_dirs(Path::new("../../tests"))?; - Ok(()) } diff --git a/src/benchmark.cts b/src/benchmark.cts index c959cbc1..7f00302b 100644 --- a/src/benchmark.cts +++ b/src/benchmark.cts @@ -29,9 +29,11 @@ export default class Benchmark { } const diskSave = path.join(binPath, `temp-trace-${i}.r3`) await fs.writeFile(diskSave, trace.toString()) + await fs.writeFile(path.join(binPath, 'index.wasm'), Buffer.from(binary)) if (options.rustBackend === true) { const p_measureCodeGen = createMeasure('rust-backend', { phase: 'replay-generation', description: `The time it takes for rust backend to generate javascript` }) - execSync(`./crates/target/release/replay_gen ${diskSave} ${path.join(binPath, 'replay.js')}`); + execSync(`./crates/target/release/replay_gen ${diskSave} ${path.join(binPath, 'index.wasm')}`); + execSync(`wasm-validate ${path.join(binPath, "canned.wasm")}`) p_measureCodeGen() } else { const p_measureCodeGen = createMeasure('ir-gen', { phase: 'replay-generation', description: `The time it takes to generate the IR code for subbenchmark ${i}` }) @@ -41,7 +43,6 @@ export default class Benchmark { await generateJavascript(fss.createWriteStream(path.join(binPath, 'replay.js')), code) p_measureJSWrite() } - await fs.writeFile(path.join(binPath, 'index.wasm'), Buffer.from(binary)) await fs.rm(diskSave) })) p_measureSave() diff --git a/src/replay-generator.cts b/src/replay-generator.cts index 430cac18..0a5e0eee 100644 --- a/src/replay-generator.cts +++ b/src/replay-generator.cts @@ -91,7 +91,7 @@ export default class Generator { this.pushEvent({ type: 'Call', name: event.name, params: event.params }) break case 'TableCall': - this.pushEvent({ type: 'TableCall', tableName: event.tableName, funcidx: event.funcidx, params: event.params }) + this.pushEvent({ type: 'TableCall', tableName: event.tableName, funcidx: event.tableidx, params: event.params }) case 'ExportReturn': this.state.globalScope = true break diff --git a/src/tracer.cts b/src/tracer.cts index 04227306..db45a0fa 100644 --- a/src/tracer.cts +++ b/src/tracer.cts @@ -98,15 +98,17 @@ export class Trace { case "EC": return { type: 'ExportCall', - name: components[1], - params: splitList(components[2]) + funcidx: parseInt(components[1]), + name: components[2], + params: splitList(components[3]) } case 'TC': return { type: 'TableCall', - tableName: components[1], - funcidx: parseInt(components[2]), - params: splitList(components[3]) + funcidx: parseInt(components[1]), + tableName: components[2], + tableidx: parseInt(components[3]), + params: splitList(components[4]) } case 'ER': return { @@ -318,7 +320,7 @@ export default class Analysis implements AnalysisI { for (let tableIndex = 0; tableIndex < table.length; tableIndex++) { const funcidx = this.resolveFuncIdx(table, tableIndex) if (funcidx === location.func) { - this.trace.push(`TC;${this.getName(this.Wasabi.module.info.tables[i])};${tableIndex};${args.join(',')}`) + this.trace.push(`TC;${location.func};${this.getName(this.Wasabi.module.info.tables[i])};${tableIndex};${args.join(',')}`) return true } } @@ -327,7 +329,7 @@ export default class Analysis implements AnalysisI { throw new Error('The function you where calling from outside the wasm module is neither exported nor in a table...') } } else { - this.trace.push(`EC;${exportName};${args.join(',')}`) + this.trace.push(`EC;${location.func};${exportName};${args.join(',')}`) this.checkMemGrow() this.checkTableGrow() } @@ -463,14 +465,14 @@ export default class Analysis implements AnalysisI { let resolvedFuncIdx = this.resolveFuncIdx(table, idx) if (shadowTable.get(idx) !== table.get(idx)) { let name = this.getName(this.Wasabi.module.info.tables[tableidx]) - let funcidx = parseInt(table.get(idx).name) + let funcidx = this.resolveFuncIdx(table, idx) let funcName = this.getName(this.Wasabi.module.info.functions[resolvedFuncIdx]) this.trace.push(`T;${tableidx};${name};${idx};${funcidx};${funcName}`) this.shadowTables[0].set(0, table.get(idx)) } if (this.options.extended === true) { let name = this.getName(this.Wasabi.module.info.tables[tableidx]) - let funcidx = parseInt(table.get(idx).name) + let funcidx = this.resolveFuncIdx(table, idx) let funcName = this.getName(this.Wasabi.module.info.functions[resolvedFuncIdx]) this.trace.push(`TE;${tableidx};${name};${idx};${funcidx};${funcName}`) } diff --git a/trace.d.cts b/trace.d.cts index 6aa37c22..e602894f 100644 --- a/trace.d.cts +++ b/trace.d.cts @@ -24,9 +24,9 @@ export type TableGrow = { type: 'TableGrow', idx: number, name: string, amount: export type GlobalGet = { type: 'GlobalGet', idx: number, name: string, value: number, valtype: ValType } -export type ExportCall = { type: "ExportCall", name: string, params: number[] } +export type ExportCall = { type: "ExportCall", funcidx: number, name: string, params: number[] } -export type TableCall = { type: "TableCall", tableName: string, funcidx: number, params: number[] } +export type TableCall = { type: "TableCall", funcidx: number, tableName: string, tableidx: number, params: number[] } export type ExportReturn = { type: 'ExportReturn' }