blob: 2a3502a90078c97b4da83cd5d51abaa156756adb [file] [log] [blame]
open Types
open Ast
open Script
open Source
(* Harness *)
let harness =
{|
'use strict';
let externrefs = {};
let externsym = Symbol("externref");
function externref(s) {
if (! (s in externrefs)) externrefs[s] = {[externsym]: s};
return externrefs[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;
}
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_i64: 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 handler = {
get(target, prop) {
return (prop in target) ? target[prop] : {};
}
};
let registry = new Proxy({spectest}, handler);
function register(name, instance) {
registry[name] = instance.exports;
return instance;
}
function module(bytes, valid = true) {
let buffer = new ArrayBuffer(bytes.length);
let view = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; ++i) {
view[i] = bytes.charCodeAt(i);
}
let validated;
try {
validated = WebAssembly.validate(buffer);
} catch (e) {
throw new Error("Wasm validate throws");
}
if (validated !== valid) {
let error;
skip: if (valid) {
try {
new WebAssembly.Module(buffer);
} catch (e) {
error = ": " + e;
break skip;
}
throw new Error("Wasm compile does not throw, although validation failed");
} else {
error = " expected";
}
throw new Error("Wasm validate failure" + error);
}
return new WebAssembly.Module(buffer);
}
function instance(bytes, imports = registry) {
return new WebAssembly.Instance(module(bytes), imports);
}
function call(instance, name, args) {
return instance.exports[name](...args);
}
function get(instance, name) {
let global = instance.exports[name];
if (global instanceof WebAssembly.Global) return global.value;
throw new Error("Wasm global expected");
}
function set(instance, name, arg) {
let global = instance.exports[name];
if (global instanceof WebAssembly.Global) {
try {
global.value = arg; return;
} catch (e) {}
}
throw new Error("Wasm mutable global expected");
}
function run(action) {
action();
}
function assert_malformed(bytes) {
try { module(bytes, false) } catch (e) {
if (e instanceof WebAssembly.CompileError) return;
throw new Error("Wasm decoding failure expected, got: " + e);
}
throw new Error("Wasm decoding failure expected");
}
function assert_invalid(bytes) {
try { module(bytes, false) } catch (e) {
if (e instanceof WebAssembly.CompileError) return;
throw new Error("Wasm validation failure expected, got: " + e);
}
throw new Error("Wasm validation failure expected");
}
function assert_unlinkable(bytes) {
let mod = module(bytes);
try { new WebAssembly.Instance(mod, registry) } catch (e) {
if (e instanceof WebAssembly.LinkError) return;
throw new Error("Wasm linking failure expected, got: " + e);
}
throw new Error("Wasm linking failure expected");
}
function assert_uninstantiable(bytes) {
let mod = module(bytes);
try { new WebAssembly.Instance(mod, registry) } catch (e) {
if (e instanceof WebAssembly.RuntimeError) return;
throw new Error("Wasm trap failure expected, got: " + e);
}
throw new Error("Wasm trap expected");
}
function assert_trap(action) {
try { action() } catch (e) {
if (e instanceof WebAssembly.RuntimeError) return;
throw new Error("Wasm trap expected, got: " + e);
}
throw new Error("Wasm trap expected");
}
let StackOverflow;
try { (function f() { 1 + f() })() } catch (e) { StackOverflow = e.constructor }
function assert_exhaustion(action) {
try { action() } catch (e) {
if (e instanceof StackOverflow) return;
throw new Error("Wasm resource exhaustion expected, got: " + e);
}
throw new Error("Wasm resource exhaustion expected");
}
function assert_return(action, ...expected) {
let actual = action();
if (actual === undefined) {
actual = [];
} else if (!Array.isArray(actual)) {
actual = [actual];
}
if (actual.length !== expected.length) {
throw new Error(expected.length + " value(s) expected, got " + actual.length);
}
for (let i = 0; i < actual.length; ++i) {
switch (expected[i]) {
case "nan:canonical":
case "nan:arithmetic":
case "nan:any":
// Note that JS can't reliably distinguish different NaN values,
// so there's no good way to test that it's a canonical NaN.
if (!Number.isNaN(actual[i])) {
throw new Error("Wasm return value NaN expected, got " + actual[i]);
};
return;
case "ref.func":
if (typeof actual[i] !== "function") {
throw new Error("Wasm function return value expected, got " + actual[i]);
};
return;
case "ref.extern":
if (actual[i] === null) {
throw new Error("Wasm reference return value expected, got " + actual[i]);
};
return;
default:
if (!Object.is(actual[i], expected[i])) {
throw new Error("Wasm return value " + expected[i] + " expected, got " + actual[i]);
};
}
}
}
|}
(* Errors & Tracing *)
module Error = Error.Make ()
exception Error = Error.Error
exception UnsupportedByJs
let js_val_type = function
| NumType _ -> ()
| VecType _ -> raise UnsupportedByJs
| RefType _ -> ()
let js_global_type = function
| GlobalType (t, _mut) -> js_val_type t
let js_func_type = function
| FuncType (ts1, ts2) -> List.iter js_val_type (ts1 @ ts2)
(* Context *)
module NameMap = Map.Make(struct type t = Ast.name let compare = compare end)
module Map = Map.Make(String)
type exports = extern_type NameMap.t
type modules = {mutable env : exports Map.t; mutable current : int}
let exports m : exports =
List.fold_left
(fun map exp -> NameMap.add exp.it.name (export_type m exp) map)
NameMap.empty m.it.exports
let modules () : modules = {env = Map.empty; current = 0}
let current_var (mods : modules) = "$" ^ string_of_int mods.current
let var_opt (mods : modules) = function
| None -> current_var mods
| Some x -> x.it
let bind (mods : modules) x_opt m =
let exports = exports m in
mods.current <- mods.current + 1;
mods.env <- Map.add (var_opt mods x_opt) exports mods.env;
if x_opt <> None then mods.env <- Map.add (current_var mods) exports mods.env
let lookup (mods : modules) x_opt name at =
let exports =
try Map.find (var_opt mods x_opt) mods.env with Not_found ->
Error.error at
(if x_opt = None then "no module defined within script"
else "unknown module " ^ var_opt mods x_opt ^ " within script")
in try NameMap.find name exports with Not_found ->
Error.error at ("unknown export \"" ^
string_of_name name ^ "\" within module")
let lookup_func (mods : modules) x_opt name at =
match lookup mods x_opt name at with
| ExternFuncType ft -> ft
| _ ->
Error.error at ("export \"" ^
string_of_name name ^ "\" is not a function")
let lookup_global (mods : modules) x_opt name at =
match lookup mods x_opt name at with
| ExternGlobalType gt -> gt
| _ ->
Error.error at ("export \"" ^
string_of_name name ^ "\" is not a global")
(* Dependencies for Wasm wrappers *)
type deps =
{ mutable dtypes : func_type list;
mutable descs : (import_desc' * int32) NameMap.t Map.t;
func_idx : int32 ref;
global_idx : int32 ref;
}
let new_deps () =
{ dtypes = []; descs = Map.empty;
func_idx = ref 0l; global_idx = ref 0l }
let dep deps x name idxr idesc =
let nmap =
match Map.find_opt x deps.descs with
| Some nmap -> nmap
| None -> NameMap.empty
in
match NameMap.find_opt name nmap with
| Some (_, idx) -> idx
| None ->
let idx = !idxr in
deps.descs <-
Map.add x (NameMap.add name (idesc, idx) nmap) deps.descs;
idxr := Int32.add idx 1l;
idx
let dep_type deps ft =
match Lib.List.index_of ft deps.dtypes with
| Some i -> Int32.of_int i
| None ->
let idx = Lib.List32.length deps.dtypes in
deps.dtypes <- deps.dtypes @ [ft];
idx
let dep_global deps x name gt =
dep deps x name deps.global_idx (GlobalImport gt)
let dep_func deps x name ft =
dep deps x name deps.func_idx
(FuncImport (dep_type deps ft @@ Source.no_region))
let dep_spectest deps name ft =
dep_func deps "spectest" (Utf8.decode name) ft
let dep_spectest_externref deps =
dep_spectest deps "externref"
(FuncType ([NumType I32Type], [RefType ExternRefType]))
let dep_spectest_is_externref deps =
dep_spectest deps "is_externref"
(FuncType ([RefType ExternRefType], [NumType I32Type]))
let dep_spectest_is_funcref deps =
dep_spectest deps "is_funcref"
(FuncType ([RefType FuncRefType], [NumType I32Type]))
let dep_spectest_eq_externref deps =
dep_spectest deps "eq_externref"
(FuncType ([RefType ExternRefType; RefType ExternRefType], [NumType I32Type]))
let _dep_spectest_eq_funcref deps =
dep_spectest deps "eq_funcref"
(FuncType ([RefType FuncRefType; RefType FuncRefType], [NumType I32Type]))
(* Script conversion to Wasm wrappers *)
let eq_of = function
| I32Type -> Values.I32 I32Op.Eq
| I64Type -> Values.I64 I64Op.Eq
| F32Type -> Values.F32 F32Op.Eq
| F64Type -> Values.F64 F64Op.Eq
let and_of = function
| I32Type | F32Type -> Values.I32 I32Op.And
| I64Type | F64Type -> Values.I64 I64Op.And
let reinterpret_of = function
| I32Type -> I32Type, Nop
| I64Type -> I64Type, Nop
| F32Type -> I32Type, Convert (Values.I32 I32Op.ReinterpretFloat)
| F64Type -> I64Type, Convert (Values.I64 I64Op.ReinterpretFloat)
let canonical_nan_of = function
| I32Type | F32Type -> Values.I32 (F32.to_bits F32.pos_nan)
| I64Type | F64Type -> Values.I64 (F64.to_bits F64.pos_nan)
let abs_mask_of = function
| I32Type | F32Type -> Values.I32 Int32.max_int
| I64Type | F64Type -> Values.I64 Int64.max_int
let nan_bitmask_of = function
| CanonicalNan -> abs_mask_of
| ArithmeticNan -> canonical_nan_of
let wasm_literal deps lit : instr list =
match lit.it with
| Values.Num n -> [Const (n @@ lit.at) @@ lit.at]
| Values.Vec s -> [VecConst (s @@ lit.at) @@ lit.at]
| Values.Ref (Values.NullRef t) -> [RefNull t @@ lit.at]
| Values.Ref (ExternRef n) ->
let externref_idx = dep_spectest_externref deps in
[ Const (Values.I32 n @@ lit.at) @@ lit.at;
Call (externref_idx @@ lit.at) @@ lit.at;
]
| Values.Ref _ -> assert false
let rec wasm_action mods deps act : instr list * value_type list =
match act.it with
| Invoke (x_opt, name, args) ->
let FuncType (_, ts2) as ft = lookup_func mods x_opt name act.at in
let idx = dep_func deps (var_opt mods x_opt) name ft in
List.concat_map (wasm_argument mods deps) args @
[Call (idx @@ act.at) @@ act.at], ts2
| Get (x_opt, name) ->
let GlobalType (t, _) as gt = lookup_global mods x_opt name act.at in
let idx = dep_global deps (var_opt mods x_opt) name gt in
[GlobalGet (idx @@ act.at) @@ act.at], [t]
| Set (x_opt, name, arg) ->
let GlobalType (t, _) as gt = lookup_global mods x_opt name act.at in
let idx = dep_global deps (var_opt mods x_opt) name gt in
wasm_argument mods deps arg @
[GlobalSet (idx @@ act.at) @@ act.at], []
and wasm_argument mods deps arg : instr list =
match arg.it with
| LiteralArg lit -> wasm_literal deps lit
| ActionArg act -> fst (wasm_action mods deps act)
let wasm_result deps res : instr list =
let at = res.at in
match res.it with
| NumResult (NumPat {it = num; at = at'}) ->
let t', reinterpret = reinterpret_of (Values.type_of_num num) in
[ reinterpret @@ at;
Const (num @@ at') @@ at;
reinterpret @@ at;
Compare (eq_of t') @@ at;
Test (Values.I32 I32Op.Eqz) @@ at;
BrIf (0l @@ at) @@ at;
]
| NumResult (NanPat nanop) ->
let open Values in
let nan = match nanop.it with F32 n | F64 n -> n | I32 _ | I64 _ -> . in
let t = Values.type_of_num nanop.it in
let t', reinterpret = reinterpret_of t in
[ reinterpret @@ at;
Const (nan_bitmask_of nan t' @@ at) @@ at;
Binary (and_of t') @@ at;
Const (canonical_nan_of t' @@ at) @@ at;
Compare (eq_of t') @@ at;
Test (Values.I32 I32Op.Eqz) @@ at;
BrIf (0l @@ at) @@ at;
]
| VecResult (VecPat (Values.V128 (shape, pats))) ->
let open Values in
(* VecResult is a list of NumPat or LitPat. For float shapes, we can have a mix of literals
* and NaNs. For NaNs, we need to mask it and compare with a canonical NaN. To simplify
* comparison, we build masks even for literals (will just be all set), collect them into
* a v128, then compare the entire 128 bits.
*)
let mask_and_canonical = function
| NumPat {it = I32 _ as i; _} -> I32 (Int32.minus_one), i
| NumPat {it = I64 _ as i; _} -> I64 (Int64.minus_one), i
| NumPat {it = F32 f; _} ->
I32 (Int32.minus_one), I32 (I32_convert.reinterpret_f32 f)
| NumPat {it = F64 f; _} ->
I64 (Int64.minus_one), I64 (I64_convert.reinterpret_f64 f)
| NanPat {it = F32 nan; _} ->
nan_bitmask_of nan I32Type, canonical_nan_of I32Type
| NanPat {it = F64 nan; _} ->
nan_bitmask_of nan I64Type, canonical_nan_of I64Type
| _ -> .
in
let masks, canons = List.split (List.map (fun p -> mask_and_canonical p) pats) in
let all_ones = V128.I32x4.of_lanes (List.init 4 (fun _ -> Int32.minus_one)) in
let mask, expected =
match shape with
| V128.I8x16 () ->
all_ones, V128.I8x16.of_lanes (List.map (I32Num.of_num 0) canons)
| V128.I16x8 () ->
all_ones, V128.I16x8.of_lanes (List.map (I32Num.of_num 0) canons)
| V128.I32x4 () ->
all_ones, V128.I32x4.of_lanes (List.map (I32Num.of_num 0) canons)
| V128.I64x2 () ->
all_ones, V128.I64x2.of_lanes (List.map (I64Num.of_num 0) canons)
| V128.F32x4 () ->
V128.I32x4.of_lanes (List.map (I32Num.of_num 0) masks),
V128.I32x4.of_lanes (List.map (I32Num.of_num 0) canons)
| V128.F64x2 () ->
V128.I64x2.of_lanes (List.map (I64Num.of_num 0) masks),
V128.I64x2.of_lanes (List.map (I64Num.of_num 0) canons)
in
[ VecConst (V128 mask @@ at) @@ at;
VecBinaryBits (V128 V128Op.And) @@ at;
VecConst (V128 expected @@ at) @@ at;
VecCompare (V128 (V128.I8x16 V128Op.Eq)) @@ at;
(* If all lanes are non-zero, then they are equal *)
VecTest (V128 (V128.I8x16 V128Op.AllTrue)) @@ at;
Test (I32 I32Op.Eqz) @@ at;
BrIf (0l @@ at) @@ at;
]
| RefResult (RefPat {it = Values.NullRef t; _}) ->
[ RefIsNull @@ at;
Test (Values.I32 I32Op.Eqz) @@ at;
BrIf (0l @@ at) @@ at;
]
| RefResult (RefPat {it = ExternRef n; _}) ->
let externref_idx = dep_spectest_externref deps in
let eq_externref_idx = dep_spectest_eq_externref deps in
[ Const (Values.I32 n @@ at) @@ at;
Call (externref_idx @@ at) @@ at;
Call (eq_externref_idx @@ at) @@ at;
Test (Values.I32 I32Op.Eqz) @@ at;
BrIf (0l @@ at) @@ at;
]
| RefResult (RefPat _) ->
assert false
| RefResult (RefTypePat t) ->
let is_ref_idx =
match t with
| FuncRefType -> dep_spectest_is_funcref deps
| ExternRefType -> dep_spectest_is_externref deps
in
[ Call (is_ref_idx @@ at) @@ at;
Test (Values.I32 I32Op.Eqz) @@ at;
BrIf (0l @@ at) @@ at;
]
let wasm_assertion mods deps ass : instr list * value_type list =
match ass.it with
| AssertReturn (act, ress) ->
[ Block (ValBlockType None,
fst (wasm_action mods deps act) @
List.concat_map (wasm_result deps) (List.rev ress) @
[Return @@ ass.at]
) @@ ass.at;
Unreachable @@ ass.at;
], []
| _ -> assert false
let wasm_module mk_code mods phrase : string =
let at = phrase.at in
let deps = new_deps () in
let code, ts = mk_code mods deps phrase in
let ts' = try List.iter js_val_type ts; ts with UnsupportedByJs -> [] in
let ftype = dep_type deps (FuncType ([], ts')) @@ at in
let types = List.map (fun ft -> ft @@ at) deps.dtypes in
let imports =
Map.bindings deps.descs |>
List.concat_map (fun (x, nmap) ->
NameMap.bindings nmap |>
List.map (fun (item_name, (idesc', idx)) ->
let idesc = idesc' @@ at in
idx, {module_name = Utf8.decode x; item_name; idesc} @@ at
)
) |>
List.sort compare |> List.map snd |> List.sort compare
in
let edesc = FuncExport (!(deps.func_idx) @@ at) @@ at in
let exports = [{name = Utf8.decode "run"; edesc} @@ at] in
let body = code @ (if ts = ts' then [] else [Return @@ at]) in
let funcs = [{ftype; locals = []; body} @@ at] in
let m = {empty_module with types; funcs; imports; exports} @@ at in
Encode.encode m
(* Script conversion to plain JS *)
let add_hex_char buf c = Printf.bprintf buf "\\x%02x" (Char.code c)
let add_char buf c =
if c < '\x20' || c >= '\x7f' then
add_hex_char buf c
else begin
if c = '\"' || c = '\\' then Buffer.add_char buf '\\';
Buffer.add_char buf c
end
let add_unicode_char buf uc =
if uc < 0x20 || uc >= 0x7f then
Printf.bprintf buf "\\u{%02x}" uc
else
add_char buf (Char.chr uc)
let js_string_with iter add_char s =
let buf = Buffer.create 256 in
Buffer.add_char buf '\"';
iter (add_char buf) s;
Buffer.add_char buf '\"';
Buffer.contents buf
let js_bytes = js_string_with String.iter add_hex_char
let js_name = js_string_with List.iter add_unicode_char
let js_float z =
match Printf.sprintf "%.17g" z with
| "nan" | "-nan" -> raise UnsupportedByJs
| "inf" -> "Infinity"
| "-inf" -> "-Infinity"
| s -> s
let js_num n =
let open Values in
match n with
| I32 i -> I32.to_string_s i
| I64 i -> I64.to_string_s i ^ "n"
| F32 z -> js_float (F32.to_float z)
| F64 z -> js_float (F64.to_float z)
let js_vec v =
raise UnsupportedByJs
let js_ref r =
let open Values in
match r with
| NullRef _ -> "null"
| ExternRef n -> "externref(" ^ Int32.to_string n ^ ")"
| _ -> raise UnsupportedByJs
let js_literal lit =
let open Values in
match lit.it with
| Num n -> js_num n
| Vec v -> js_vec v
| Ref r -> js_ref r
let js_num_pat = function
| NumPat num ->
if num = num then js_num num.it else raise UnsupportedByJs (* NaN *)
| NanPat nanop -> raise UnsupportedByJs
let js_vec_pat = function
| VecPat _ -> raise UnsupportedByJs
let js_ref_pat = function
| RefPat r -> js_ref r.it
| RefTypePat t -> "\"ref." ^ string_of_refed_type t ^ "\""
let js_result res =
match res.it with
| NumResult np -> js_num_pat np
| VecResult vp -> js_vec_pat vp
| RefResult rp -> js_ref_pat rp
let rec js_definition def =
match def.it with
| Textual m -> js_bytes (Encode.encode m)
| Encoded (_, bs) -> js_bytes bs
| Quoted (_, s) ->
try js_definition (Parse.string_to_module s) with Parse.Syntax _ ->
js_bytes "<malformed quote>"
let rec js_action mods act =
match act.it with
| Invoke (x_opt, name, args) ->
js_func_type (lookup_func mods x_opt name act.at);
"call(" ^ var_opt mods x_opt ^ ", " ^ js_name name ^ ", " ^
"[" ^ String.concat ", " (List.map (js_argument mods) args) ^
"].flat())"
| Get (x_opt, name) ->
js_global_type (lookup_global mods x_opt name act.at);
"get(" ^ var_opt mods x_opt ^ ", " ^ js_name name ^ ")"
| Set (x_opt, name, arg) ->
js_global_type (lookup_global mods x_opt name act.at);
"set(" ^ var_opt mods x_opt ^ ", " ^ js_name name ^ ", " ^
js_argument mods arg ^ ")"
and js_argument mods arg =
match arg.it with
| LiteralArg lit -> js_literal lit
| ActionArg act -> js_action mods act
let js_run_wasm bs =
"call(instance(" ^ js_bytes bs ^ ", registry), \"run\", [])"
let js_or_wasm_action mods act =
try js_action mods act
with UnsupportedByJs -> js_run_wasm (wasm_module wasm_action mods act)
let js_assertion mods ass =
match ass.it with
| AssertMalformed (def, _) ->
"assert_malformed(" ^ js_definition def ^ ")"
| AssertInvalid (def, _) ->
"assert_invalid(" ^ js_definition def ^ ")"
| AssertUnlinkable (def, _) ->
"assert_unlinkable(" ^ js_definition def ^ ")"
| AssertUninstantiable (def, _) ->
"assert_uninstantiable(" ^ js_definition def ^ ")"
| AssertReturn (act, ress) ->
(try
let js_ress = List.map js_result ress in
"assert_return(() => " ^ js_or_wasm_action mods act ^
String.concat ", " ("" :: js_ress) ^ ")"
with UnsupportedByJs ->
js_run_wasm (wasm_module wasm_assertion mods ass)
)
| AssertTrap (act, _) ->
"assert_trap(() => " ^ js_or_wasm_action mods act ^ ")"
| AssertExhaustion (act, _) ->
"assert_exhaustion(() => " ^ js_or_wasm_action mods act ^ ")"
let js_command mods cmd =
"\n// " ^ Filename.basename cmd.at.left.file ^
":" ^ string_of_int cmd.at.left.line ^ "\n" ^
match cmd.it with
| Module (x_opt, def) ->
let rec unquote def =
match def.it with
| Textual m -> m
| Encoded (_, bs) -> Decode.decode "binary" bs
| Quoted (_, s) -> unquote (Parse.string_to_module s)
in
bind mods x_opt (unquote def);
let xi = current_var mods in
"let " ^ current_var mods ^
" = register(\"" ^ xi ^ "\", instance(" ^ js_definition def ^ "));\n" ^
( if x_opt = None then "" else
let x = var_opt mods x_opt in
"let " ^ x ^ " = register(\"" ^ x ^ "\", " ^ xi ^ ");\n"
)
| Register (name, x_opt) ->
"register(" ^ js_name name ^ ", " ^ var_opt mods x_opt ^ ");\n"
| Action act ->
js_or_wasm_action mods act ^ ";\n"
| Assertion ass ->
js_assertion mods ass ^ ";\n"
| Meta _ -> assert false
let of_script scr =
(if !Flags.harness then harness else "") ^
String.concat "" (List.map (js_command (modules ())) scr)