summaryrefslogtreecommitdiffstats
path: root/js/src/jit-test/etc/wasm/generate-spectests/wast2js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /js/src/jit-test/etc/wasm/generate-spectests/wast2js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jit-test/etc/wasm/generate-spectests/wast2js')
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/Cargo.toml11
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/README.md3
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/convert.rs720
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/harness.js448
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/lib.rs18
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/license.js14
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/main.rs40
-rw-r--r--js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/out.rs188
8 files changed, 1442 insertions, 0 deletions
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/Cargo.toml b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/Cargo.toml
new file mode 100644
index 0000000000..b55ae458df
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "wast2js"
+version = "0.1.0"
+authors = ["Ryan Hunt <rhunt@eqrion.net>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.19"
+wast = { path = "../../../../../../../../wasm-tools/crates/wast" }
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/README.md b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/README.md
new file mode 100644
index 0000000000..c06210bb04
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/README.md
@@ -0,0 +1,3 @@
+# wast2js
+
+Mozilla specific converter from `.wast` to `.js` for SpiderMonkey jit-tests.
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/convert.rs b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/convert.rs
new file mode 100644
index 0000000000..888b049036
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/convert.rs
@@ -0,0 +1,720 @@
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use anyhow::{bail, Context as _, Result};
+use std::fmt::Write;
+use std::path::Path;
+use std::str;
+
+use super::out::*;
+
+const HARNESS: &'static str = include_str!("./harness.js");
+const LICENSE: &'static str = include_str!("./license.js");
+
+pub fn harness() -> String {
+ HARNESS.to_string()
+}
+
+pub fn convert<P: AsRef<Path>>(path: P, wast: &str) -> Result<String> {
+ let filename = path.as_ref();
+ let adjust_wast = |mut err: wast::Error| {
+ err.set_path(filename);
+ err.set_text(wast);
+ err
+ };
+
+ let mut lexer = wast::lexer::Lexer::new(wast);
+ // The 'names.wast' spec test has confusable unicode -- disable detection.
+ lexer.allow_confusing_unicode(filename.ends_with("names.wast"));
+ let buf = wast::parser::ParseBuffer::new_with_lexer(lexer).map_err(adjust_wast)?;
+ let ast = wast::parser::parse::<wast::Wast>(&buf).map_err(adjust_wast)?;
+
+ let mut out = String::new();
+
+ writeln!(&mut out, "{}", LICENSE)?;
+ writeln!(&mut out, "// {}", filename.display())?;
+
+ let mut current_instance: Option<usize> = None;
+
+ for directive in ast.directives {
+ let sp = directive.span();
+ let (line, col) = sp.linecol_in(wast);
+ writeln!(&mut out, "")?;
+ convert_directive(
+ directive,
+ &mut current_instance,
+ filename,
+ line,
+ col,
+ wast,
+ &mut out,
+ )
+ .with_context(|| {
+ format!(
+ "failed to convert directive on {}:{}:{}",
+ filename.display(),
+ line + 1,
+ col
+ )
+ })?;
+ }
+
+ Ok(out)
+}
+
+fn convert_directive(
+ directive: wast::WastDirective,
+ current_instance: &mut Option<usize>,
+ filename: &Path,
+ line: usize,
+ col: usize,
+ wast: &str,
+ out: &mut String,
+) -> Result<()> {
+ use wast::WastDirective::*;
+
+ if col == 1 {
+ writeln!(out, "// {}:{}", filename.display(), line + 1)?;
+ } else {
+ writeln!(out, "// {}:{}:{}", filename.display(), line + 1, col)?;
+ }
+ match directive {
+ Wat(wast::QuoteWat::Wat(wast::Wat::Module(module))) => {
+ let next_instance = current_instance.map(|x| x + 1).unwrap_or(0);
+ let module_text = module_to_js_string(&module, wast)?;
+
+ writeln!(
+ out,
+ "let ${} = instantiate(`{}`);",
+ next_instance, module_text
+ )?;
+ if let Some(id) = module.id {
+ writeln!(
+ out,
+ "register(${}, {});",
+ next_instance,
+ format!("`{}`", escape_template_name_string(id.name()))
+ )?;
+ }
+
+ *current_instance = Some(next_instance);
+ }
+ Wat(wast::QuoteWat::QuoteModule(_, source)) => {
+ let next_instance = current_instance.map(|x| x + 1).unwrap_or(0);
+ let module_text = quote_module_to_js_string(source)?;
+
+ writeln!(
+ out,
+ "let ${} = instantiate(`{}`);",
+ next_instance, module_text
+ )?;
+
+ *current_instance = Some(next_instance);
+ }
+ Wat(_) => {
+ write!(out, "// unsupported quote wat")?;
+ }
+ Register {
+ span: _,
+ name,
+ module,
+ } => {
+ let instanceish = module
+ .map(|x| format!("`{}`", escape_template_name_string(x.name())))
+ .unwrap_or_else(|| format!("${}", current_instance.unwrap()));
+
+ writeln!(
+ out,
+ "register({}, `{}`);",
+ instanceish,
+ escape_template_name_string(name)
+ )?;
+ }
+ Invoke(i) => {
+ let invoke_node = invoke_to_js(current_instance, i)?;
+ writeln!(out, "{};", invoke_node.output(0))?;
+ }
+ AssertReturn {
+ span: _,
+ exec,
+ results,
+ } => {
+ let exec_node = execute_to_js(current_instance, exec, wast)?;
+ let expected_node = to_js_value_array(&results, assert_expression_to_js_value)?;
+ writeln!(
+ out,
+ "{};",
+ JSNode::Assert {
+ name: "assert_return".to_string(),
+ exec: exec_node,
+ expected: expected_node,
+ }
+ .output(0),
+ )?;
+ }
+ AssertTrap {
+ span: _,
+ exec,
+ message,
+ } => {
+ let exec_node = execute_to_js(current_instance, exec, wast)?;
+ let expected_node = Box::new(JSNode::Raw(format!(
+ "`{}`",
+ escape_template_name_string(message)
+ )));
+ writeln!(
+ out,
+ "{};",
+ JSNode::Assert {
+ name: "assert_trap".to_string(),
+ exec: exec_node,
+ expected: expected_node,
+ }
+ .output(0),
+ )?;
+ }
+ AssertExhaustion {
+ span: _,
+ call,
+ message,
+ } => {
+ let exec_node = invoke_to_js(current_instance, call)?;
+ let expected_node = Box::new(JSNode::Raw(format!(
+ "`{}`",
+ escape_template_name_string(message)
+ )));
+ writeln!(
+ out,
+ "{};",
+ JSNode::Assert {
+ name: "assert_exhaustion".to_string(),
+ exec: exec_node,
+ expected: expected_node,
+ }
+ .output(0),
+ )?;
+ }
+ AssertInvalid {
+ span: _,
+ module,
+ message,
+ } => {
+ let text = match module {
+ wast::QuoteWat::Wat(wast::Wat::Module(m)) => module_to_js_string(&m, wast)?,
+ wast::QuoteWat::QuoteModule(_, source) => quote_module_to_js_string(source)?,
+ other => bail!("unsupported {:?} in assert_invalid", other),
+ };
+ let exec = Box::new(JSNode::Raw(format!("instantiate(`{}`)", text)));
+ let expected_node = Box::new(JSNode::Raw(format!(
+ "`{}`",
+ escape_template_name_string(message)
+ )));
+ writeln!(
+ out,
+ "{};",
+ JSNode::Assert {
+ name: "assert_invalid".to_string(),
+ exec: exec,
+ expected: expected_node,
+ }
+ .output(0),
+ )?;
+ }
+ AssertMalformed {
+ module,
+ span: _,
+ message,
+ } => {
+ let text = match module {
+ wast::QuoteWat::Wat(wast::Wat::Module(m)) => module_to_js_string(&m, wast)?,
+ wast::QuoteWat::QuoteModule(_, source) => quote_module_to_js_string(source)?,
+ other => bail!("unsupported {:?} in assert_malformed", other),
+ };
+ let exec = Box::new(JSNode::Raw(format!("instantiate(`{}`)", text)));
+ let expected_node = Box::new(JSNode::Raw(format!(
+ "`{}`",
+ escape_template_name_string(message)
+ )));
+ writeln!(
+ out,
+ "{};",
+ JSNode::Assert {
+ name: "assert_malformed".to_string(),
+ exec: exec,
+ expected: expected_node,
+ }
+ .output(0),
+ )?;
+ }
+ AssertUnlinkable {
+ span: _,
+ module,
+ message,
+ } => {
+ let text = match module {
+ wast::Wat::Module(module) => module_to_js_string(&module, wast)?,
+ other => bail!("unsupported {:?} in assert_unlinkable", other),
+ };
+ let exec = Box::new(JSNode::Raw(format!("instantiate(`{}`)", text)));
+ let expected_node = Box::new(JSNode::Raw(format!(
+ "`{}`",
+ escape_template_name_string(message)
+ )));
+ writeln!(
+ out,
+ "{};",
+ JSNode::Assert {
+ name: "assert_unlinkable".to_string(),
+ exec: exec,
+ expected: expected_node,
+ }
+ .output(0),
+ )?;
+ }
+ AssertException { span: _, exec } => {
+ // This assert doesn't have a second parameter, so we don't bother
+ // formatting it using an Assert node.
+ let exec_node = execute_to_js(current_instance, exec, wast)?;
+ writeln!(out, "assert_exception(() => {});", exec_node.output(0))?;
+ }
+ Thread(..) => unimplemented!(),
+ Wait { .. } => unimplemented!(),
+ }
+
+ Ok(())
+}
+
+fn escape_template_string(text: &str, escape_ascii_lf_tab: bool) -> String {
+ let mut escaped = String::new();
+ for c in text.chars() {
+ match c {
+ '$' => escaped.push_str("$$"),
+ '\\' => escaped.push_str("\\\\"),
+ '`' => escaped.push_str("\\`"),
+ c if c.is_ascii_control() && escape_ascii_lf_tab && c != '\n' && c != '\t' => {
+ escaped.push_str(&format!("\\x{:02x}", c as u32))
+ }
+ c if !c.is_ascii() => escaped.push_str(&c.escape_unicode().to_string()),
+ c => escaped.push(c),
+ }
+ }
+ escaped
+}
+
+// Escape a module for use in a JS template string.
+fn escape_template_module_string(text: &str) -> String {
+ // Modules may have comments, where line-feeds must be passed through as-is
+ // to preserve their meaning
+ escape_template_string(text, false)
+}
+
+// Escape a name for use in a JS template string.
+fn escape_template_name_string(text: &str) -> String {
+ // Names don't care about line-feeds or tabs, just need equality
+ escape_template_string(text, true)
+}
+
+fn span_to_offset(span: wast::token::Span, text: &str) -> Result<usize> {
+ let (span_line, span_col) = span.linecol_in(text);
+ let mut cur = 0;
+ // Use split_terminator instead of lines so that if there is a `\r`,
+ // it is included in the offset calculation. The `+1` values below
+ // account for the `\n`.
+ for (i, line) in text.split_terminator('\n').enumerate() {
+ if span_line == i {
+ assert!(span_col < line.len());
+ return Ok(cur + span_col);
+ }
+ cur += line.len() + 1;
+ }
+ bail!("invalid line/col");
+}
+
+fn closed_module(module: &str) -> Result<&str> {
+ enum State {
+ Module,
+ QStr,
+ EscapeQStr,
+ }
+
+ let mut i = 0;
+ let mut level = 1;
+ let mut state = State::Module;
+
+ let mut chars = module.chars();
+ while level != 0 {
+ let next = match chars.next() {
+ Some(next) => next,
+ None => bail!("was unable to close module"),
+ };
+ match state {
+ State::Module => match next {
+ '(' => level += 1,
+ ')' => level -= 1,
+ '"' => state = State::QStr,
+ _ => {}
+ },
+ State::QStr => match next {
+ '"' => state = State::Module,
+ '\\' => state = State::EscapeQStr,
+ _ => {}
+ },
+ State::EscapeQStr => match next {
+ _ => state = State::QStr,
+ },
+ }
+ i += next.len_utf8();
+ }
+ return Ok(&module[0..i]);
+}
+
+fn module_to_js_string(module: &wast::core::Module, wast: &str) -> Result<String> {
+ let offset = span_to_offset(module.span, wast)?;
+ let opened_module = &wast[offset..];
+ if !opened_module.starts_with("module") {
+ return Ok(escape_template_module_string(opened_module));
+ }
+ Ok(escape_template_module_string(&format!(
+ "({}",
+ closed_module(opened_module)?
+ )))
+}
+
+fn quote_module_to_js_string(quotes: Vec<(wast::token::Span, &[u8])>) -> Result<String> {
+ let mut text = String::new();
+ for (_, src) in quotes {
+ text.push_str(str::from_utf8(src)?);
+ text.push_str(" ");
+ }
+ let escaped = escape_template_module_string(&text);
+ Ok(escaped)
+}
+
+fn invoke_to_js(current_instance: &Option<usize>, i: wast::WastInvoke) -> Result<Box<JSNode>> {
+ let instanceish = i
+ .module
+ .map(|x| format!("`{}`", escape_template_name_string(x.name())))
+ .unwrap_or_else(|| format!("${}", current_instance.unwrap()));
+ let body = to_js_value_array(&i.args, arg_to_js_value)?;
+
+ Ok(Box::new(JSNode::Invoke {
+ instance: instanceish,
+ name: escape_template_name_string(i.name),
+ body: body,
+ }))
+}
+
+fn execute_to_js(
+ current_instance: &Option<usize>,
+ exec: wast::WastExecute,
+ wast: &str,
+) -> Result<Box<JSNode>> {
+ match exec {
+ wast::WastExecute::Invoke(invoke) => invoke_to_js(current_instance, invoke),
+ wast::WastExecute::Wat(module) => {
+ let text = match module {
+ wast::Wat::Module(module) => module_to_js_string(&module, wast)?,
+ other => bail!("unsupported {:?} at execute_to_js", other),
+ };
+ Ok(Box::new(JSNode::Raw(format!("instantiate(`{}`)", text))))
+ }
+ wast::WastExecute::Get { module, global } => {
+ let instanceish = module
+ .map(|x| format!("`{}`", escape_template_name_string(x.name())))
+ .unwrap_or_else(|| format!("${}", current_instance.unwrap()));
+ Ok(Box::new(JSNode::Raw(format!(
+ "get({}, `{}`)",
+ instanceish, global
+ ))))
+ }
+ }
+}
+
+fn to_js_value_array<T, F: Fn(&T) -> Result<String>>(vals: &[T], func: F) -> Result<Box<JSNode>> {
+ let mut value_nodes: Vec<Box<JSNode>> = vec![];
+ for val in vals {
+ let converted_value = (func)(val)?;
+ value_nodes.push(Box::new(JSNode::Raw(converted_value)));
+ }
+
+ Ok(Box::new(JSNode::Array(value_nodes)))
+}
+
+fn to_js_value_array_string<T, F: Fn(&T) -> Result<String>>(vals: &[T], func: F) -> Result<String> {
+ let array = to_js_value_array(vals, func)?;
+ Ok(array.output(NOWRAP))
+}
+
+fn f32_needs_bits(a: f32) -> bool {
+ if a.is_infinite() {
+ return false;
+ }
+ return a.is_nan()
+ || ((a as f64) as f32).to_bits() != a.to_bits()
+ || (format!("{:.}", a).parse::<f64>().unwrap() as f32).to_bits() != a.to_bits();
+}
+
+fn f64_needs_bits(a: f64) -> bool {
+ return a.is_nan();
+}
+
+fn f32x4_needs_bits(a: &[wast::token::Float32; 4]) -> bool {
+ a.iter().any(|x| {
+ let as_f32 = f32::from_bits(x.bits);
+ f32_needs_bits(as_f32)
+ })
+}
+
+fn f64x2_needs_bits(a: &[wast::token::Float64; 2]) -> bool {
+ a.iter().any(|x| {
+ let as_f64 = f64::from_bits(x.bits);
+ f64_needs_bits(as_f64)
+ })
+}
+
+fn f32_to_js_value(val: f32) -> String {
+ if val == f32::INFINITY {
+ format!("Infinity")
+ } else if val == f32::NEG_INFINITY {
+ format!("-Infinity")
+ } else if val.is_sign_negative() && val == 0f32 {
+ format!("-0")
+ } else {
+ format!("{:.}", val)
+ }
+}
+
+fn f64_to_js_value(val: f64) -> String {
+ if val == f64::INFINITY {
+ format!("Infinity")
+ } else if val == f64::NEG_INFINITY {
+ format!("-Infinity")
+ } else if val.is_sign_negative() && val == 0f64 {
+ format!("-0")
+ } else {
+ format!("{:.}", val)
+ }
+}
+
+fn float32_to_js_value(val: &wast::token::Float32) -> String {
+ let as_f32 = f32::from_bits(val.bits);
+ if f32_needs_bits(as_f32) {
+ format!(
+ "bytes(\"f32\", {})",
+ to_js_value_array_string(&val.bits.to_le_bytes(), |x| Ok(format!("0x{:x}", x)))
+ .unwrap(),
+ )
+ } else {
+ format!("value(\"f32\", {})", f32_to_js_value(as_f32))
+ }
+}
+
+fn float64_to_js_value(val: &wast::token::Float64) -> String {
+ let as_f64 = f64::from_bits(val.bits);
+ if f64_needs_bits(as_f64) {
+ format!(
+ "bytes(\"f64\", {})",
+ to_js_value_array_string(&val.bits.to_le_bytes(), |x| Ok(format!("0x{:x}", x)))
+ .unwrap(),
+ )
+ } else {
+ format!("value(\"f64\", {})", f64_to_js_value(as_f64))
+ }
+}
+
+fn f32_pattern_to_js_value(pattern: &wast::core::NanPattern<wast::token::Float32>) -> String {
+ use wast::core::NanPattern::*;
+ match pattern {
+ CanonicalNan => format!("`canonical_nan`"),
+ ArithmeticNan => format!("`arithmetic_nan`"),
+ Value(x) => float32_to_js_value(x),
+ }
+}
+
+fn f64_pattern_to_js_value(pattern: &wast::core::NanPattern<wast::token::Float64>) -> String {
+ use wast::core::NanPattern::*;
+ match pattern {
+ CanonicalNan => format!("`canonical_nan`"),
+ ArithmeticNan => format!("`arithmetic_nan`"),
+ Value(x) => float64_to_js_value(x),
+ }
+}
+
+fn return_value_to_js_value(v: &wast::core::WastRetCore<'_>) -> Result<String> {
+ use wast::core::WastRetCore::*;
+ Ok(match v {
+ I32(x) => format!("value(\"i32\", {})", x),
+ I64(x) => format!("value(\"i64\", {}n)", x),
+ F32(x) => f32_pattern_to_js_value(x),
+ F64(x) => f64_pattern_to_js_value(x),
+ RefNull(x) => match x {
+ Some(wast::core::HeapType::Any) => format!("value('anyref', null)"),
+ Some(wast::core::HeapType::Exn) => format!("value('exnref', null)"),
+ Some(wast::core::HeapType::Eq) => format!("value('eqref', null)"),
+ Some(wast::core::HeapType::Array) => format!("value('arrayref', null)"),
+ Some(wast::core::HeapType::Struct) => format!("value('structref', null)"),
+ Some(wast::core::HeapType::I31) => format!("value('i31ref', null)"),
+ Some(wast::core::HeapType::None) => format!("value('nullref', null)"),
+ Some(wast::core::HeapType::Func) => format!("value('anyfunc', null)"),
+ Some(wast::core::HeapType::NoFunc) => format!("value('nullfuncref', null)"),
+ Some(wast::core::HeapType::Extern) => format!("value('externref', null)"),
+ Some(wast::core::HeapType::NoExtern) => format!("value('nullexternref', null)"),
+ None => "null".to_string(),
+ other => bail!(
+ "couldn't convert ref.null {:?} to a js assertion value",
+ other
+ ),
+ },
+ RefFunc(_) => format!("new RefWithType('funcref')"),
+ RefExtern(x) => match x {
+ Some(v) => format!("new ExternRefResult({})", v),
+ None => format!("new RefWithType('externref')"),
+ },
+ RefHost(x) => format!("new HostRefResult({})", x),
+ RefAny => format!("new RefWithType('anyref')"),
+ RefEq => format!("new RefWithType('eqref')"),
+ RefArray => format!("new RefWithType('arrayref')"),
+ RefStruct => format!("new RefWithType('structref')"),
+ RefI31 => format!("new RefWithType('i31ref')"),
+ V128(x) => {
+ use wast::core::V128Pattern::*;
+ match x {
+ I8x16(elements) => format!(
+ "i8x16({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}", x)))?,
+ ),
+ I16x8(elements) => format!(
+ "i16x8({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}", x)))?,
+ ),
+ I32x4(elements) => format!(
+ "i32x4({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}", x)))?,
+ ),
+ I64x2(elements) => format!(
+ "i64x2({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}n", x)))?,
+ ),
+ F32x4(elements) => {
+ let elements: Vec<String> = elements
+ .iter()
+ .map(|x| f32_pattern_to_js_value(x))
+ .collect();
+ let elements = strings_to_raws(elements);
+ JSNode::Call {
+ name: "new F32x4Pattern".to_string(),
+ args: elements,
+ }
+ .output(0)
+ }
+ F64x2(elements) => {
+ let elements: Vec<String> = elements
+ .iter()
+ .map(|x| f64_pattern_to_js_value(x))
+ .collect();
+ let elements = strings_to_raws(elements);
+ JSNode::Call {
+ name: "new F64x2Pattern".to_string(),
+ args: elements,
+ }
+ .output(0)
+ }
+ }
+ }
+ Either(v) => {
+ let args = v
+ .iter()
+ .map(|v| return_value_to_js_value(v).unwrap())
+ .collect();
+ let args = strings_to_raws(args);
+ JSNode::Call {
+ name: "either".to_string(),
+ args: args,
+ }
+ .output(0)
+ }
+ other => bail!("couldn't convert Core({:?}) to a js assertion value", other),
+ })
+}
+
+fn assert_expression_to_js_value(v: &wast::WastRet<'_>) -> Result<String> {
+ use wast::WastRet::*;
+
+ Ok(match &v {
+ Core(x) => return_value_to_js_value(x)?,
+ other => bail!("couldn't convert {:?} to a js assertion value", other),
+ })
+}
+
+fn arg_to_js_value(v: &wast::WastArg<'_>) -> Result<String> {
+ use wast::core::WastArgCore::*;
+ use wast::WastArg::Core;
+
+ Ok(match &v {
+ Core(I32(x)) => format!("{}", *x),
+ Core(I64(x)) => format!("{}n", *x),
+ Core(F32(x)) => float32_to_js_value(x),
+ Core(F64(x)) => float64_to_js_value(x),
+ Core(V128(x)) => {
+ use wast::core::V128Const::*;
+ match x {
+ I8x16(elements) => format!(
+ "i8x16({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}", x)))?,
+ ),
+ I16x8(elements) => format!(
+ "i16x8({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}", x)))?,
+ ),
+ I32x4(elements) => format!(
+ "i32x4({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}", x)))?,
+ ),
+ I64x2(elements) => format!(
+ "i64x2({})",
+ to_js_value_array_string(elements, |x| Ok(format!("0x{:x}n", x)))?,
+ ),
+ F32x4(elements) => {
+ if f32x4_needs_bits(elements) {
+ let bytes =
+ to_js_value_array(&x.to_le_bytes(), |x| Ok(format!("0x{:x}", x)))?;
+ format!("bytes('v128', {})", bytes.output(0))
+ } else {
+ let vals = to_js_value_array(elements, |x| {
+ Ok(f32_to_js_value(f32::from_bits(x.bits)))
+ })?;
+ format!("f32x4({})", vals.output(0))
+ }
+ }
+ F64x2(elements) => {
+ if f64x2_needs_bits(elements) {
+ let bytes =
+ to_js_value_array(&x.to_le_bytes(), |x| Ok(format!("0x{:x}", x)))?;
+ format!("bytes('v128', {})", bytes.output(0))
+ } else {
+ let vals = to_js_value_array(elements, |x| {
+ Ok(f64_to_js_value(f64::from_bits(x.bits)))
+ })?;
+ format!("f64x2({})", vals.output(0))
+ }
+ }
+ }
+ }
+ Core(RefNull(_)) => format!("null"),
+ Core(RefExtern(x)) => format!("externref({})", x),
+ Core(RefHost(x)) => format!("hostref({})", x),
+ other => bail!("couldn't convert {:?} to a js value", other),
+ })
+}
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/harness.js b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/harness.js
new file mode 100644
index 0000000000..a96781e8ed
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/harness.js
@@ -0,0 +1,448 @@
+"use strict";
+
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+if (!wasmIsSupported()) {
+ quit();
+}
+
+function bytes(type, bytes) {
+ var typedBuffer = new Uint8Array(bytes);
+ return wasmGlobalFromArrayBuffer(type, typedBuffer.buffer);
+}
+function value(type, value) {
+ return new WebAssembly.Global({
+ value: type,
+ mutable: false,
+ }, value);
+}
+
+function i8x16(elements) {
+ let typedBuffer = new Uint8Array(elements);
+ return wasmGlobalFromArrayBuffer("v128", typedBuffer.buffer);
+}
+function i16x8(elements) {
+ let typedBuffer = new Uint16Array(elements);
+ return wasmGlobalFromArrayBuffer("v128", typedBuffer.buffer);
+}
+function i32x4(elements) {
+ let typedBuffer = new Uint32Array(elements);
+ return wasmGlobalFromArrayBuffer("v128", typedBuffer.buffer);
+}
+function i64x2(elements) {
+ let typedBuffer = new BigUint64Array(elements);
+ return wasmGlobalFromArrayBuffer("v128", typedBuffer.buffer);
+}
+function f32x4(elements) {
+ let typedBuffer = new Float32Array(elements);
+ return wasmGlobalFromArrayBuffer("v128", typedBuffer.buffer);
+}
+function f64x2(elements) {
+ let typedBuffer = new Float64Array(elements);
+ return wasmGlobalFromArrayBuffer("v128", typedBuffer.buffer);
+}
+
+function either(...arr) {
+ return new EitherVariants(arr);
+}
+
+class F32x4Pattern {
+ constructor(x, y, z, w) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.w = w;
+ }
+}
+
+class F64x2Pattern {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+}
+
+class RefWithType {
+ constructor(type) {
+ this.type = type;
+ }
+
+ formatExpected() {
+ return `RefWithType(${this.type})`;
+ }
+
+ test(refGlobal) {
+ try {
+ new WebAssembly.Global({value: this.type}, refGlobal.value);
+ return true;
+ } catch (err) {
+ assertEq(err instanceof TypeError, true, `wrong type of error when creating global: ${err}`);
+ assertEq(!!err.message.match(/can only pass/), true, `wrong type of error when creating global: ${err}`);
+ return false;
+ }
+ }
+}
+
+// ref.extern values created by spec tests will be JS objects of the form
+// { [externsym]: <number> }. Other externref values are possible to observe
+// if extern.convert_any is used.
+let externsym = Symbol("externref");
+function externref(s) {
+ return { [externsym]: s };
+}
+function is_externref(x) {
+ return (x !== null && externsym in x) ? 1 : 0;
+}
+function is_funcref(x) {
+ return typeof x === "function" ? 1 : 0;
+}
+function eq_externref(x, y) {
+ return x === y ? 1 : 0;
+}
+function eq_funcref(x, y) {
+ return x === y ? 1 : 0;
+}
+
+class ExternRefResult {
+ constructor(n) {
+ this.n = n;
+ }
+
+ formatExpected() {
+ return `ref.extern ${this.n}`;
+ }
+
+ test(global) {
+ // the global's value can either be an externref or just a plain old JS number
+ let result = global.value;
+ if (typeof global.value === "object" && externsym in global.value) {
+ result = global.value[externsym];
+ }
+ return result === this.n;
+ }
+}
+
+// ref.host values created by spectests will be whatever the JS API does to
+// convert the given value to anyref. It should implicitly be like any.convert_extern.
+function hostref(v) {
+ if (!wasmGcEnabled()) {
+ throw new Error("ref.host only works when wasm GC is enabled");
+ }
+
+ const { internalizeNum } = new WebAssembly.Instance(
+ new WebAssembly.Module(wasmTextToBinary(`(module
+ (func (import "test" "coerce") (param i32) (result anyref))
+ (func (export "internalizeNum") (param i32) (result anyref)
+ (call 0 (local.get 0))
+ )
+ )`)),
+ { "test": { "coerce": x => x } },
+ ).exports;
+ return internalizeNum(v);
+}
+
+class HostRefResult {
+ constructor(n) {
+ this.n = n;
+ }
+
+ formatExpected() {
+ return `ref.host ${this.n}`;
+ }
+
+ test(externrefGlobal) {
+ assertEq(externsym in externrefGlobal.value, true, `HostRefResult only works with externref inputs`);
+ return externrefGlobal.value[externsym] === this.n;
+ }
+}
+
+let spectest = {
+ externref: externref,
+ is_externref: is_externref,
+ is_funcref: is_funcref,
+ eq_externref: eq_externref,
+ eq_funcref: eq_funcref,
+ print: console.log.bind(console),
+ print_i32: console.log.bind(console),
+ print_i32_f32: console.log.bind(console),
+ print_f64_f64: console.log.bind(console),
+ print_f32: console.log.bind(console),
+ print_f64: console.log.bind(console),
+ global_i32: 666,
+ global_i64: 666n,
+ global_f32: 666,
+ global_f64: 666,
+ table: new WebAssembly.Table({
+ initial: 10,
+ maximum: 20,
+ element: "anyfunc",
+ }),
+ memory: new WebAssembly.Memory({ initial: 1, maximum: 2 }),
+};
+
+let linkage = {
+ spectest,
+};
+
+function getInstance(instanceish) {
+ if (typeof instanceish === "string") {
+ assertEq(
+ instanceish in linkage,
+ true,
+ `'${instanceish}'' must be registered`,
+ );
+ return linkage[instanceish];
+ }
+ return instanceish;
+}
+
+function instantiate(source) {
+ let bytecode = wasmTextToBinary(source);
+ let module = new WebAssembly.Module(bytecode);
+ let instance = new WebAssembly.Instance(module, linkage);
+ return instance.exports;
+}
+
+function register(instanceish, name) {
+ linkage[name] = getInstance(instanceish);
+}
+
+function invoke(instanceish, field, params) {
+ let func = getInstance(instanceish)[field];
+ assertEq(func instanceof Function, true, "expected a function");
+ return wasmLosslessInvoke(func, ...params);
+}
+
+function get(instanceish, field) {
+ let global = getInstance(instanceish)[field];
+ assertEq(
+ global instanceof WebAssembly.Global,
+ true,
+ "expected a WebAssembly.Global",
+ );
+ return global;
+}
+
+function assert_trap(thunk, message) {
+ try {
+ thunk();
+ throw new Error("expected trap");
+ } catch (err) {
+ if (err instanceof WebAssembly.RuntimeError) {
+ return;
+ }
+ throw err;
+ }
+}
+
+let StackOverflow;
+try {
+ (function f() {
+ 1 + f();
+ })();
+} catch (e) {
+ StackOverflow = e.constructor;
+}
+function assert_exhaustion(thunk, message) {
+ try {
+ thunk();
+ assertEq("normal return", "exhaustion");
+ } catch (err) {
+ assertEq(
+ err instanceof StackOverflow,
+ true,
+ "expected exhaustion",
+ );
+ }
+}
+
+function assert_invalid(thunk, message) {
+ try {
+ thunk();
+ assertEq("valid module", "invalid module");
+ } catch (err) {
+ assertEq(
+ err instanceof WebAssembly.LinkError ||
+ err instanceof WebAssembly.CompileError,
+ true,
+ "expected an invalid module",
+ );
+ }
+}
+
+function assert_unlinkable(thunk, message) {
+ try {
+ thunk();
+ assertEq(true, false, "expected an unlinkable module");
+ } catch (err) {
+ assertEq(
+ err instanceof WebAssembly.LinkError ||
+ err instanceof WebAssembly.CompileError,
+ true,
+ "expected an unlinkable module",
+ );
+ }
+}
+
+function assert_malformed(thunk, message) {
+ try {
+ thunk();
+ assertEq("valid module", "malformed module");
+ } catch (err) {
+ assertEq(
+ err instanceof TypeError ||
+ err instanceof SyntaxError ||
+ err instanceof WebAssembly.CompileError ||
+ err instanceof WebAssembly.LinkError,
+ true,
+ `expected a malformed module`,
+ );
+ }
+}
+
+function assert_exception(thunk) {
+ let thrown = false;
+ try {
+ thunk();
+ } catch (err) {
+ thrown = true;
+ }
+ assertEq(thrown, true, "expected an exception to be thrown");
+}
+
+function assert_return(thunk, expected) {
+ let results = thunk();
+
+ if (results === undefined) {
+ results = [];
+ } else if (!Array.isArray(results)) {
+ results = [results];
+ }
+ if (!Array.isArray(expected)) {
+ expected = [expected];
+ }
+
+ if (!compareResults(results, expected)) {
+ let got = results.map((x) => formatResult(x)).join(", ");
+ let wanted = expected.map((x) => formatExpected(x)).join(", ");
+ assertEq(
+ `[${got}]`,
+ `[${wanted}]`,
+ );
+ assertEq(true, false, `${got} !== ${wanted}`);
+ }
+}
+
+function formatResult(result) {
+ if (typeof (result) === "object") {
+ return wasmGlobalToString(result);
+ } else {
+ return `${result}`;
+ }
+}
+
+function formatExpected(expected) {
+ if (
+ expected === `f32_canonical_nan` ||
+ expected === `f32_arithmetic_nan` ||
+ expected === `f64_canonical_nan` ||
+ expected === `f64_arithmetic_nan`
+ ) {
+ return expected;
+ } else if (expected instanceof F32x4Pattern) {
+ return `f32x4(${formatExpected(expected.x)}, ${
+ formatExpected(expected.y)
+ }, ${formatExpected(expected.z)}, ${formatExpected(expected.w)})`;
+ } else if (expected instanceof F64x2Pattern) {
+ return `f64x2(${formatExpected(expected.x)}, ${
+ formatExpected(expected.y)
+ })`;
+ } else if (expected instanceof EitherVariants) {
+ return expected.formatExpected();
+ } else if (expected instanceof RefWithType) {
+ return expected.formatExpected();
+ } else if (expected instanceof ExternRefResult) {
+ return expected.formatExpected();
+ } else if (expected instanceof HostRefResult) {
+ return expected.formatExpected();
+ } else if (typeof (expected) === "object") {
+ return wasmGlobalToString(expected);
+ } else {
+ throw new Error("unknown expected result");
+ }
+}
+
+class EitherVariants {
+ constructor(arr) {
+ this.arr = arr;
+ }
+ matches(v) {
+ return this.arr.some((e) => compareResult(v, e));
+ }
+ formatExpected() {
+ return `either(${this.arr.map(formatExpected).join(", ")})`;
+ }
+}
+
+function compareResults(results, expected) {
+ if (results.length !== expected.length) {
+ return false;
+ }
+ for (let i in results) {
+ if (expected[i] instanceof EitherVariants) {
+ return expected[i].matches(results[i]);
+ }
+ if (!compareResult(results[i], expected[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function compareResult(result, expected) {
+ if (
+ expected === `canonical_nan` ||
+ expected === `arithmetic_nan`
+ ) {
+ return wasmGlobalIsNaN(result, expected);
+ } else if (expected === null) {
+ return result.value === null;
+ } else if (expected instanceof F32x4Pattern) {
+ return compareResult(
+ wasmGlobalExtractLane(result, "f32x4", 0),
+ expected.x,
+ ) &&
+ compareResult(wasmGlobalExtractLane(result, "f32x4", 1), expected.y) &&
+ compareResult(wasmGlobalExtractLane(result, "f32x4", 2), expected.z) &&
+ compareResult(wasmGlobalExtractLane(result, "f32x4", 3), expected.w);
+ } else if (expected instanceof F64x2Pattern) {
+ return compareResult(
+ wasmGlobalExtractLane(result, "f64x2", 0),
+ expected.x,
+ ) &&
+ compareResult(wasmGlobalExtractLane(result, "f64x2", 1), expected.y);
+ } else if (expected instanceof RefWithType) {
+ return expected.test(result);
+ } else if (expected instanceof ExternRefResult) {
+ return expected.test(result);
+ } else if (expected instanceof HostRefResult) {
+ return expected.test(result);
+ } else if (typeof (expected) === "object") {
+ return wasmGlobalsEqual(result, expected);
+ } else {
+ throw new Error("unknown expected result");
+ }
+}
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/lib.rs b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/lib.rs
new file mode 100644
index 0000000000..b62bcbfaea
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/lib.rs
@@ -0,0 +1,18 @@
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+mod convert;
+mod out;
+pub use convert::*;
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/license.js b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/license.js
new file mode 100644
index 0000000000..12d51bd702
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/license.js
@@ -0,0 +1,14 @@
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/main.rs b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/main.rs
new file mode 100644
index 0000000000..d5d1118e7e
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/main.rs
@@ -0,0 +1,40 @@
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use anyhow::{Context as _, Result};
+use std::env;
+use std::path::Path;
+
+mod convert;
+mod out;
+
+fn main() -> Result<()> {
+ let files = env::args().collect::<Vec<String>>();
+ for path in &files[1..] {
+ let source =
+ std::fs::read_to_string(path).with_context(|| format!("failed to read `{}`", path))?;
+
+ let mut full_script = String::new();
+ full_script.push_str(&convert::harness());
+ full_script.push_str(&convert::convert(path, &source)?);
+
+ std::fs::write(
+ Path::new(path).with_extension("js").file_name().unwrap(),
+ &full_script,
+ )?;
+ }
+
+ Ok(())
+}
diff --git a/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/out.rs b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/out.rs
new file mode 100644
index 0000000000..fef1b82ade
--- /dev/null
+++ b/js/src/jit-test/etc/wasm/generate-spectests/wast2js/src/out.rs
@@ -0,0 +1,188 @@
+/* Copyright 2022 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// This module has a bare-bones "syntax tree" that helps us output more readable JS code in our
+/// tests. This is a lot cheaper and faster than running a full JS formatter on the generated code.
+/// The tree only has node types for things that matter to formatting; most things in the output
+/// use the Raw type, which is just a plain old string.
+
+pub const LINE_WIDTH: usize = 80; // the default line width to try to meet (will not be met perfectly)
+pub const NOWRAP: usize = 1000; // use for line_remaining when you don't want to wrap text
+
+pub enum JSNode {
+ Assert {
+ name: String,
+ exec: Box<JSNode>,
+ expected: Box<JSNode>,
+ },
+ Invoke {
+ instance: String,
+ name: String,
+ body: Box<JSNode>,
+ },
+ Call {
+ name: String,
+ args: Vec<Box<JSNode>>,
+ },
+ Array(Vec<Box<JSNode>>),
+ Raw(String),
+}
+
+impl JSNode {
+ /// Converts a node to JavaScript code. line_remaining is the number of characters remaining on
+ /// the line, used for line-wrapping purposes; if zero, the default LINE_WIDTH will be used.
+ pub fn output(&self, line_remaining: usize) -> String {
+ let line_remaining = if line_remaining == 0 {
+ LINE_WIDTH
+ } else {
+ line_remaining
+ };
+ match self {
+ Self::Assert {
+ name,
+ exec,
+ expected,
+ } => {
+ if self.len() > line_remaining {
+ format!(
+ "{}(\n{}\n)",
+ name,
+ indent(format!(
+ "() => {},\n{},",
+ exec.output(line_remaining - 8),
+ expected.output(line_remaining - 2),
+ )),
+ )
+ } else {
+ format!(
+ "{}(() => {}, {})",
+ name,
+ exec.output(NOWRAP),
+ expected.output(NOWRAP),
+ )
+ }
+ }
+ Self::Invoke {
+ instance,
+ name,
+ body,
+ } => {
+ let len_before_body = "invoke".len() + instance.len() + name.len();
+ if len_before_body > line_remaining {
+ format!(
+ "invoke({},\n{}\n)",
+ instance,
+ indent(format!("`{}`,\n{},", name, body.output(line_remaining - 2),)),
+ )
+ } else {
+ let body_string = body.output(line_remaining - len_before_body);
+ format!("invoke({}, `{}`, {})", instance, name, body_string)
+ }
+ }
+ Self::Call { name, args } => {
+ if self.len() > line_remaining {
+ let arg_strings: Vec<String> = args
+ .iter()
+ .map(|arg| arg.output(line_remaining - 2))
+ .collect();
+ format!("{}(\n{},\n)", name, indent(arg_strings.join(",\n")))
+ } else {
+ let arg_strings: Vec<String> =
+ args.iter().map(|arg| arg.output(NOWRAP)).collect();
+ format!("{}({})", name, arg_strings.join(", "))
+ }
+ }
+ Self::Array(values) => {
+ if self.len() > line_remaining {
+ let value_strings = output_nodes(&values, 70);
+ format!("[\n{},\n]", indent(value_strings.join(",\n")))
+ } else {
+ let value_strings = output_nodes(&values, 80);
+ format!("[{}]", value_strings.join(", "))
+ }
+ }
+ Self::Raw(val) => val.to_string(),
+ }
+ }
+
+ /// A rough estimate of the string length of the node. Used as a heuristic to know when we
+ /// should wrap text.
+ fn len(&self) -> usize {
+ match self {
+ Self::Assert {
+ name,
+ exec,
+ expected,
+ } => name.len() + exec.len() + expected.len(),
+ Self::Invoke {
+ instance,
+ name,
+ body,
+ } => instance.len() + name.len() + body.len(),
+ Self::Call { name, args } => {
+ let mut args_len: usize = 0;
+ for node in args {
+ args_len += node.len() + ", ".len();
+ }
+ name.len() + args_len
+ }
+ Self::Array(nodes) => {
+ let mut content_len: usize = 0;
+ for node in nodes {
+ content_len += node.len() + ", ".len();
+ }
+ content_len
+ }
+ Self::Raw(s) => s.len(),
+ }
+ }
+}
+
+pub fn output_nodes(nodes: &Vec<Box<JSNode>>, line_width_per_node: usize) -> Vec<String> {
+ nodes
+ .iter()
+ .map(|node| node.output(line_width_per_node))
+ .collect()
+}
+
+pub fn strings_to_raws(strs: Vec<String>) -> Vec<Box<JSNode>> {
+ let mut res: Vec<Box<JSNode>> = vec![];
+ for s in strs {
+ res.push(Box::new(JSNode::Raw(s)));
+ }
+ res
+}
+
+fn indent(s: String) -> String {
+ let mut result = String::new();
+ let mut do_indent = true;
+ for (i, line) in s.lines().enumerate() {
+ if i > 0 {
+ result.push('\n');
+ }
+ if line.chars().any(|c| !c.is_whitespace()) {
+ if do_indent {
+ result.push_str(" ");
+ }
+ result.push_str(line);
+
+ // An odd number of backticks in the line means we are entering or exiting a raw string.
+ if line.matches("`").count() % 2 == 1 {
+ do_indent = !do_indent
+ }
+ }
+ }
+ result
+}