Skip to content

Commit 2d81e13

Browse files
test(jco): wast->js conversion logic
1 parent 0462214 commit 2d81e13

2 files changed

Lines changed: 218 additions & 30 deletions

File tree

crates/xtask/src/build/wast_fixtures.rs

Lines changed: 181 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
use anyhow::{Context as _, Result, bail, ensure};
12
use std::fs::{File, OpenOptions};
23
use std::io::{Read, Write};
34
use std::path::Path;
4-
5-
use anyhow::{Context as _, Result, ensure};
5+
use wast::core::WastRetCore;
66

77
/// Convert a single WAST file
88
fn convert_wast_file(
99
input_wast: &mut File,
10-
input_wast_path: &PathBuf,
10+
input_wast_path: &Path,
1111
output_wasm: &mut File,
1212
output_js: &mut File,
1313
) -> Result<()> {
@@ -23,11 +23,16 @@ fn convert_wast_file(
2323
)
2424
})?;
2525

26-
// TODO: write JS preamble
26+
// Start exported test function
2727
writeln!(
2828
output_js,
2929
r#"
30-
import {{ }} from "../";
30+
export async function runWastTest(args) {{
31+
if (!args) {{ throw new Error('missing args'); }}
32+
if (!args.instance) {{ throw new Error('missing loaded wasm instance'); }}
33+
if (!args.assert) {{ throw new Error('missing assert obj'); }}
34+
const {{ instance, assert }} = args;
35+
let res;
3136
"#
3237
)?;
3338

@@ -49,44 +54,182 @@ fn convert_wast_file(
4954
output_wasm.flush()?;
5055
}
5156
wast::WastDirective::ModuleDefinition(_) => {
52-
todo!("unsupported directive ModuleDefinition")
57+
bail!("unsupported directive ModuleDefinition")
5358
}
5459
wast::WastDirective::ModuleInstance { .. } => {
55-
todo!("unsupported directive ModuleInstance")
60+
bail!("unsupported directive ModuleInstance")
5661
}
5762
wast::WastDirective::AssertMalformed { .. } => {
58-
todo!("unsupported directive AssertMalformed")
63+
bail!("unsupported directive AssertMalformed")
5964
}
6065
wast::WastDirective::AssertInvalid { .. } => {
61-
todo!("unsupported directive AssertInvalid")
66+
bail!("unsupported directive AssertInvalid")
6267
}
6368
wast::WastDirective::Register { .. } => {
64-
todo!("unsupported directive Register")
69+
bail!("unsupported directive Register")
70+
}
71+
wast::WastDirective::Invoke(_) => bail!("unsupported directive Invoke"),
72+
wast::WastDirective::AssertTrap { .. } => bail!("unsupported directive AssertTrap"),
73+
wast::WastDirective::AssertReturn { exec, results, .. } => {
74+
ensure!(
75+
results.len() == 1,
76+
"assert return with multiple results not yet supported"
77+
);
78+
// TODO: we need to check asyncness for await or not
79+
let (export_name, args) = extract_export_fn(&exec)?;
80+
let check_expr = match results.first() {
81+
Some(ret) => {
82+
format!("assert.strictEqual(res, {});", wast_ret_to_js_param(ret)?)
83+
}
84+
None => "".into(),
85+
};
86+
writeln!(
87+
output_js,
88+
r#"
89+
res = await instance['{export_name}']({});
90+
{check_expr}
91+
"#,
92+
args_to_js_params(args)?,
93+
)?;
6594
}
66-
wast::WastDirective::Invoke(_) => todo!("unsupported directive Invoke"),
67-
wast::WastDirective::AssertTrap { .. } => todo!("unsupported directive AssertTrap"),
68-
wast::WastDirective::AssertReturn { .. } => todo!("unsupported directive AssertReturn"),
6995
wast::WastDirective::AssertExhaustion { .. } => {
70-
todo!("unsupported directive AssertExhaustion")
96+
bail!("unsupported directive AssertExhaustion")
7197
}
7298
wast::WastDirective::AssertUnlinkable { .. } => {
73-
todo!("unsupported directive AssertUnlinkable")
99+
bail!("unsupported directive AssertUnlinkable")
74100
}
75101
wast::WastDirective::AssertException { .. } => {
76-
todo!("unsupported directive AssertException")
102+
bail!("unsupported directive AssertException")
77103
}
78104
wast::WastDirective::AssertSuspension { .. } => {
79-
todo!("unsupported directive AssertSuspension")
105+
bail!("unsupported directive AssertSuspension")
80106
}
81-
wast::WastDirective::Thread(_) => todo!("unsupported directive Thread"),
82-
wast::WastDirective::Wait { .. } => todo!("unsupported directive Wait"),
107+
wast::WastDirective::Thread(_) => bail!("unsupported directive Thread"),
108+
wast::WastDirective::Wait { .. } => bail!("unsupported directive Wait"),
83109
}
84110
}
85111

112+
// Close out the function
113+
writeln!(output_js, "}}",)?;
114+
86115
output_js.flush()?;
87116
Ok(())
88117
}
89118

119+
/// Generate a list of JS params
120+
fn args_to_js_params(args: &[wast::WastArg<'_>]) -> Result<String> {
121+
args.iter()
122+
.map(|arg| match arg {
123+
wast::WastArg::Core(v) => core_val_to_js_param(v),
124+
wast::WastArg::Component(v) => cm_val_to_js_param(v),
125+
_ => bail!("unsupported wast arg"),
126+
})
127+
.collect::<Result<Vec<String>>>()
128+
.map(|s| s.join(","))
129+
}
130+
131+
/// Convert a Wast core value to a JS value
132+
fn core_val_to_js_param(wast_arg: &wast::core::WastArgCore<'_>) -> Result<String> {
133+
match wast_arg {
134+
wast::core::WastArgCore::I32(v) => Ok(format!("{v}")),
135+
wast::core::WastArgCore::I64(v) => Ok(format!("{v}")),
136+
wast::core::WastArgCore::F32(v) => Ok(format!("{:.8}", f32::from_bits(v.bits))),
137+
wast::core::WastArgCore::F64(v) => Ok(format!("{:.8}", f64::from_bits(v.bits))),
138+
wast::core::WastArgCore::V128(v) => Ok(format!("{}", i128::from_le_bytes(v.to_le_bytes()))),
139+
wast::core::WastArgCore::RefNull(_) => bail!("refs unsupported core args"),
140+
wast::core::WastArgCore::RefExtern(_) => bail!("refs unsupported core args"),
141+
wast::core::WastArgCore::RefHost(_) => bail!("refs unsupported core args"),
142+
}
143+
}
144+
145+
/// Convert a Wast return value to a JS value
146+
fn wast_ret_to_js_param(wast_ret: &wast::WastRet<'_>) -> Result<String> {
147+
match wast_ret {
148+
wast::WastRet::Core(wast_ret_core) => wast_ret_core_val_to_js_param(wast_ret_core),
149+
wast::WastRet::Component(wast_val) => cm_val_to_js_param(wast_val),
150+
_ => bail!("unsupported wast ret"),
151+
}
152+
}
153+
154+
/// Convert a Wast CM value to a JS value
155+
fn wast_ret_core_val_to_js_param(wast_ret_core: &WastRetCore<'_>) -> Result<String> {
156+
match wast_ret_core {
157+
WastRetCore::I32(_) => bail!("WastRetCore::I32 not yet supported"),
158+
WastRetCore::I64(_) => bail!("WastRetCore::I64 not yet supported"),
159+
WastRetCore::F32(_nan_pattern) => bail!("WastRetCore::F32 not yet supported"),
160+
WastRetCore::F64(_nan_pattern) => bail!("WastRetCore::F64 not yet supported"),
161+
WastRetCore::V128(_v128_pattern) => bail!("WastRetCore::V128 not yet supported"),
162+
WastRetCore::RefNull(_heap_type) => bail!("WastRetCore::RefNull not yet supported"),
163+
WastRetCore::RefExtern(_) => bail!("WastRetCore::RefExtern not yet supported"),
164+
WastRetCore::RefHost(_) => bail!("WastRetCore::RefHost not yet supported"),
165+
WastRetCore::RefFunc(_index) => bail!("WastRetCore::RefFunc not yet supported"),
166+
WastRetCore::RefAny => bail!("WastRetCore::RefAny not yet supported"),
167+
WastRetCore::RefEq => bail!("WastRetCore::RefEq not yet supported"),
168+
WastRetCore::RefArray => bail!("WastRetCore::RefArray not yet supported"),
169+
WastRetCore::RefStruct => bail!("WastRetCore::RefStruct not yet supported"),
170+
WastRetCore::RefI31 => bail!("WastRetCore::RefI31 not yet supported"),
171+
WastRetCore::RefI31Shared => bail!("WastRetCore::RefI31Shared not yet supported"),
172+
WastRetCore::Either(_wast_ret_cores) => bail!("WastRetCore::Either not yet supported"),
173+
}
174+
}
175+
176+
/// Convert a Wast CM value to a JS value
177+
fn cm_val_to_js_param(wast_val: &wast::component::WastVal<'_>) -> Result<String> {
178+
match wast_val {
179+
wast::component::WastVal::Bool(v) => Ok(format!("{v}")),
180+
wast::component::WastVal::U8(v) => Ok(format!("{v}")),
181+
wast::component::WastVal::S8(v) => Ok(format!("{v}")),
182+
wast::component::WastVal::U16(v) => Ok(format!("{v}")),
183+
wast::component::WastVal::S16(v) => Ok(format!("{v}")),
184+
wast::component::WastVal::U32(v) => Ok(format!("{v}")),
185+
wast::component::WastVal::S32(v) => Ok(format!("{v}")),
186+
wast::component::WastVal::U64(v) => Ok(format!("{v}")),
187+
wast::component::WastVal::S64(v) => Ok(format!("{v}")),
188+
wast::component::WastVal::F32(v) => Ok(format!("{:.8}", f32::from_bits(v.bits))),
189+
wast::component::WastVal::F64(v) => Ok(format!("{:.8}", f64::from_bits(v.bits))),
190+
wast::component::WastVal::Char(v) => Ok(format!("'{v}'")),
191+
wast::component::WastVal::String(s) => Ok(format!("'{s}'")),
192+
wast::component::WastVal::List(vals) | wast::component::WastVal::Tuple(vals) => vals
193+
.iter()
194+
.map(|v| cm_val_to_js_param(v))
195+
.collect::<Result<Vec<String>>>()
196+
.map(|parts| parts.join(","))
197+
.map(|v| format!("[{v}]")),
198+
wast::component::WastVal::Record(items) => items
199+
.iter()
200+
.map(|(k, v)| cm_val_to_js_param(v).map(|v| format!("{k}: {v}")))
201+
.collect::<Result<Vec<String>>>()
202+
.map(|parts| parts.join(","))
203+
.map(|v| format!("{{{v}}}")),
204+
wast::component::WastVal::Variant(tag, wast_val) => match wast_val {
205+
Some(v) => cm_val_to_js_param(v).map(|v| format!("{{ tag: '{tag}', val: {v} }}")),
206+
None => Ok(format!("{{ tag: '{tag}', val: null }}")),
207+
},
208+
wast::component::WastVal::Enum(v) => Ok(format!("{{ tag: '{v}' }}")),
209+
wast::component::WastVal::Option(wast_val) => match wast_val {
210+
Some(v) => cm_val_to_js_param(v),
211+
None => Ok("null".into()),
212+
},
213+
wast::component::WastVal::Result(wast_val) => match wast_val {
214+
Ok(v) => match v {
215+
Some(v) => cm_val_to_js_param(v),
216+
None => Ok("{{ tag: 'ok', val: null }}".into()),
217+
},
218+
Err(e) => match e {
219+
Some(v) => cm_val_to_js_param(v),
220+
None => Ok("{{ tag: 'err', val: null }}".into()),
221+
},
222+
},
223+
wast::component::WastVal::Flags(items) => Ok(format!(
224+
"{{{}}}",
225+
items
226+
.iter()
227+
.map(|k| format!("{k}: true"))
228+
.collect::<Vec<String>>()
229+
.join(",")
230+
)),
231+
}
232+
}
90233
/// Build WAST tests that can be used to test p3 host compliance
91234
pub(crate) fn run(wast_path: &Path) -> Result<()> {
92235
let wast_path = wast_path.canonicalize().with_context(|| {
@@ -106,7 +249,6 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> {
106249
output_wasm_path.add_extension("wasm");
107250
let mut output_wasm = OpenOptions::new()
108251
.write(true)
109-
.create_new(true)
110252
.truncate(true)
111253
.open(&output_wasm_path)
112254
.with_context(|| {
@@ -120,7 +262,6 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> {
120262
output_js_path.add_extension("js");
121263
let mut output_js = OpenOptions::new()
122264
.write(true)
123-
.create_new(true)
124265
.truncate(true)
125266
.open(output_js_path)
126267
.with_context(|| format!("failed to open output JS file @ [{}]", wast_path.display()))?;
@@ -129,3 +270,22 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> {
129270

130271
Ok(())
131272
}
273+
274+
/// Extract the export function from an Exec, along with it's results
275+
fn extract_export_fn<'a>(
276+
exec: &'a wast::WastExecute,
277+
) -> Result<(&'a str, &'a [wast::WastArg<'a>])> {
278+
match exec {
279+
wast::WastExecute::Invoke(wast::WastInvoke {
280+
module, name, args, ..
281+
}) => {
282+
ensure!(
283+
module.is_none(),
284+
"wast invocations with modules not yet supported"
285+
);
286+
Ok((*name, args))
287+
}
288+
wast::WastExecute::Wat(_) => bail!("unsupported wast execute type WastExecute::Wat"),
289+
wast::WastExecute::Get { .. } => bail!("unsupported wast execute type WastExecute::Get"),
290+
}
291+
}

packages/jco/test/p3/ported/component-model/wast.js

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { join, relative } from "node:path";
1+
import { join, relative, basename } from "node:path";
22
import { opendir } from "node:fs/promises";
33
import { spawn } from "node:child_process";
44

55
import { suite, test, assert, beforeAll } from "vitest";
66

77
import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js";
8-
import { fileExists } from "../../../helpers.js";
8+
import { fileExists, setupAsyncTest } from "../../../helpers.js";
99

1010
// Relative paths to tests that should be skipped
1111
const TESTS_TO_SKIP = new Set([
@@ -28,10 +28,12 @@ suite("component-model", async () => {
2828

2929
const wastPath = join(dirent.parentPath, dirent.name);
3030
const wastRelPath = relative(COMPONENT_MODEL_FIXTURES_WAST_DIR, wastPath);
31-
if (TESTS_TO_SKIP.has(wastRelPath)) { continue; }
31+
if (TESTS_TO_SKIP.has(wastRelPath)) {
32+
continue;
33+
}
3234

33-
const wasmPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.wasm`);
34-
const scriptPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.js`);
35+
const wasmPath = join(dirent.parentPath, `${dirent.name}.wasm`);
36+
const scriptPath = join(dirent.parentPath, `${dirent.name}.js`);
3537
metadata.push({
3638
wastRelPath,
3739
wastPath,
@@ -40,7 +42,7 @@ suite("component-model", async () => {
4042
});
4143
}
4244

43-
metadata = metadata.slice(0,1);
45+
metadata = metadata.slice(0, 1);
4446

4547
beforeAll(async () => {
4648
for (const { wastPath } of metadata) {
@@ -49,7 +51,7 @@ suite("component-model", async () => {
4951
stdio: "inherit",
5052
shell: true,
5153
});
52-
await new Promise(resolve => fixtureBuild.on("exit", resolve));
54+
await new Promise((resolve) => fixtureBuild.on("exit", resolve));
5355
}
5456
});
5557

@@ -58,8 +60,34 @@ suite("component-model", async () => {
5860
t(wastRelPath, async () => {
5961
assert(await fileExists(wasmPath), `missing generated wasm component @ [${wasmPath}]`);
6062
assert(await fileExists(scriptPath), `missing generated script @ [${scriptPath}]`);
61-
// TODO: convert WAST test to WAST + executable JS
62-
assert.strictEqual(true, true);
63+
64+
let cleanup;
65+
try {
66+
const setup = await setupAsyncTest({
67+
asyncMode: "jspi",
68+
component: {
69+
name: basename(wastRelPath).replace(".wast.wasm", ""),
70+
path: wasmPath,
71+
},
72+
jco: {
73+
transpile: {
74+
extraArgs: {
75+
minify: false,
76+
},
77+
},
78+
},
79+
});
80+
cleanup = setup.cleanup;
81+
const instance = setup.instance;
82+
83+
const mod = await import(scriptPath);
84+
await mod.runWastTest({
85+
instance,
86+
assert,
87+
});
88+
} finally {
89+
await cleanup?.();
90+
}
6391
});
6492
}
6593
});

0 commit comments

Comments
 (0)