diff options
Diffstat (limited to 'test/gen-spec-js.py')
-rwxr-xr-x | test/gen-spec-js.py | 606 |
1 files changed, 449 insertions, 157 deletions
diff --git a/test/gen-spec-js.py b/test/gen-spec-js.py index 45e72c3d..cd3f3d75 100755 --- a/test/gen-spec-js.py +++ b/test/gen-spec-js.py @@ -16,181 +16,473 @@ # import argparse +from collections import namedtuple import cStringIO import json import os +import re +import struct import sys -from utils import Error - -JS_HEADER = """\ -var passed = 0; -var failed = 0; -var quiet = false; - -""" - -MODULE_RUN = """\ -testModule%(module_index)s(); -""" - -MODULE_RUN_FOOTER = """\ -end(); - -""" - -MODULE_TEST_HEADER = """\ -function testModule%(module_index)s() { -""" - -MODULE_DATA_HEADER = """\ - var module = createModule([ -""" - -MODULE_DATA_FOOTER = """\ - ]); - -""" - -INVOKE_COMMAND = """\ - invoke(module, '%(name)s'); -""" - -ASSERT_RETURN_COMMAND = """\ - assertReturn(module, '%(name)s', '%(file)s', %(line)s); -""" - -ASSERT_TRAP_COMMAND = """\ - assertTrap(module, '%(name)s', '%(file)s', %(line)s); -""" - -COMMANDS = { - 'invoke': INVOKE_COMMAND, - 'assert_return': ASSERT_RETURN_COMMAND, - 'assert_return_nan': ASSERT_RETURN_COMMAND, - 'assert_trap': ASSERT_TRAP_COMMAND -} - -MODULE_TEST_FOOTER = """\ -} - -""" - -JS_FOOTER = """\ -function createModule(data) { - var u8a = new Uint8Array(data); - var ffi = {spectest: {print: print}}; - return Wasm.instantiateModule(u8a, ffi); -} - -function assertReturn(module, name, file, line) { - try { - var result = module.exports[name](); - } catch(e) { - print(file + ":" + line + ": " + name + " unexpectedly threw: " + e); - } - - if (result == 1) { - passed++; - } else { - print(file + ":" + line + ": " + name + " failed."); - failed++; - } -} - -function assertTrap(module, name, file, line) { - var threw = false; - try { - module.exports[name](); - } catch (e) { - threw = true; - } - - if (threw) { - passed++; - } else { - print(file + ":" + line + ": " + name + " failed, didn't throw"); - failed++; - } -} - -function invoke(module, name) { - try { - var invokeResult = module.exports[name](); - passed++; - } catch(e) { - print(name + " unexpectedly threw: " + e); - failed++; - } - - if (!quiet) - print(name + " = " + invokeResult); -} - -function end() { - if ((failed > 0) || !quiet) - print(passed + "/" + (passed + failed) + " tests passed."); -} -""" - -def ProcessJsonFile(json_file_path): - json_file_dir = os.path.dirname(json_file_path) - with open(json_file_path) as json_file: - json_data = json.load(json_file) - - output = cStringIO.StringIO() - output.write(JS_HEADER) - for module_index in range(len(json_data['modules'])): - output.write(MODULE_RUN % {'module_index': module_index}) - output.write(MODULE_RUN_FOOTER) - - for module_index, module in enumerate(json_data['modules']): - module_filepath = os.path.join(json_file_dir, module['filename']) - with open(module_filepath, 'rb') as wasm_file: - wasm_data = wasm_file.read() - - output.write(MODULE_TEST_HEADER % {'module_index': module_index}) - output.write(MODULE_DATA_HEADER) - WriteModuleBytes(output, wasm_data) - output.write(MODULE_DATA_FOOTER) - - for command_index, command in enumerate(module['commands']): - output.write(COMMANDS[command['type']] % command) - output.write(MODULE_TEST_FOOTER) - output.write(JS_FOOTER) - - return output.getvalue() - - -def WriteModuleBytes(output, module_data): - BYTES_PER_LINE = 16 - offset = 0 - while True: - line_data = module_data[offset:offset+BYTES_PER_LINE] - if not line_data: - break - output.write(' ') - output.write(', '.join('%3d' % ord(byte) for byte in line_data)) - output.write(',\n') - offset += BYTES_PER_LINE +from find_exe import GetWast2WasmExecutable, GetWasm2WastExecutable +from utils import ChangeDir, ChangeExt, Error, Executable +import utils + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + +F32_INF = 0x7f800000 +F32_NEG_INF = 0xff800000 +F32_NEG_ZERO = 0x80000000 +F32_SIGN_BIT = F32_NEG_ZERO +F32_SIG_MASK = 0x7fffff +F32_QUIET_NAN_TAG = 0x400000 +F64_INF = 0x7ff0000000000000L +F64_NEG_INF = 0xfff0000000000000L +F64_NEG_ZERO = 0x8000000000000000L +F64_SIGN_BIT = F64_NEG_ZERO +F64_SIG_MASK = 0xfffffffffffffL +F64_QUIET_NAN_TAG = 0x8000000000000L + +def I32ToJS(value): + # JavaScript will return all i32 values as signed. + if value >= 2**31: + value -= 2**32 + return str(value) + +def IsNaNF32(f32_bits): + return (F32_INF < f32_bits < F32_NEG_ZERO) or (f32_bits > F32_NEG_INF) + +def ReinterpretF32(f32_bits): + return struct.unpack('<f', struct.pack('<I', f32_bits))[0] + +def NaNF32ToString(f32_bits): + result = '-' if f32_bits & F32_SIGN_BIT else '' + result += 'nan' + sig = f32_bits & F32_SIG_MASK + if sig != F32_QUIET_NAN_TAG: + result += ':0x%x' % sig + return result + +def F32ToWasm(f32_bits): + if IsNaNF32(f32_bits): + return 'f32.const %s' % NaNF32ToString(f32_bits) + elif f32_bits == F32_INF: + return 'f32.const infinity' + elif f32_bits == F32_NEG_INF: + return 'f32.const -infinity' + else: + return 'f32.const %s' % float.hex(ReinterpretF32(f32_bits)) + +def F32ToJS(f32_bits): + assert not IsNaNF32(f32_bits) + if f32_bits == F32_INF: + return 'Infinity' + elif f32_bits == F32_NEG_INF: + return '-Infinity' + else: + return 'f32(%s)' % ReinterpretF32(f32_bits) + +def IsNaNF64(f64_bits): + return (F64_INF < f64_bits < F64_NEG_ZERO) or (f64_bits > F64_NEG_INF) + +def ReinterpretF64(f64_bits): + return struct.unpack('<d', struct.pack('<Q', f64_bits))[0] + +def NaNF64ToString(f64_bits): + result = '-' if f64_bits & F64_SIGN_BIT else '' + result += 'nan' + sig = f64_bits & F64_SIG_MASK + if sig != F64_QUIET_NAN_TAG: + result += ':0x%x' % sig + return result + +def F64ToWasm(f64_bits): + if IsNaNF64(f64_bits): + return 'f64.const %s' % NaNF64ToString(f64_bits) + elif f64_bits == F64_INF: + return 'f64.const infinity' + elif f64_bits == F64_NEG_INF: + return 'f64.const -infinity' + else: + return 'f64.const %s' % float.hex(ReinterpretF64(f64_bits)) + +def F64ToJS(f64_bits): + assert not IsNaNF64(f64_bits) + if f64_bits == F64_INF: + return 'Infinity' + elif f64_bits == F64_NEG_INF: + return '-Infinity' + else: + # Use repr to get full precision + return repr(ReinterpretF64(f64_bits)) + + +def UnescapeWasmString(s): + # Wast allows for more escape characters than this, but we assume that + # wasm2wast will only use the \xx escapes. + result = '' + i = 0 + while i < len(s): + c = s[i] + if c == '\\': + x = s[i+1:i+3] + if len(x) != 2: + raise Error('String with invalid escape: \"%s\"' % s) + result += chr(int(x, 16)) + i += 3 + else: + result += c + i += 1 + return result + +def EscapeJSString(s): + result = '' + for c in s: + if 32 <= ord(c) < 127 and c not in '"\\': + result += c + else: + result += '\\x%02x' % ord(c) + return result + + +def IsValidJSConstant(const): + type_ = const['type'] + if type_ == 'i32': + return True + elif type_ == 'i64': + return False + elif type_ == 'f32': + return not IsNaNF32(int(const['value'])) + elif type_ == 'f64': + return not IsNaNF64(long(const['value'])) + +def IsValidJSAction(action): + return all(IsValidJSConstant(x) for x in action.get('args', [])) + +def IsValidJSCommand(command): + type_ = command['type'] + action = command['action'] + if type_ == 'assert_return': + expected = command['expected'] + return (IsValidJSAction(action) and + all(IsValidJSConstant(x) for x in expected)) + elif type_ == 'assert_return_nan': + return IsValidJSAction(action) + elif type_ == 'assert_trap': + return IsValidJSAction(action) + + +def CollectInvalidModuleCommands(commands): + modules = [] + module_map = {} + for command in commands: + if command['type'] == 'module': + pair = (command, []) + modules.append(pair) + module_name = command.get('name') + if module_name: + module_map[module_name] = pair + elif command['type'] in ('assert_return', 'assert_return_nan', + 'assert_trap'): + if IsValidJSCommand(command): + continue + + action = command['action'] + module_name = action.get('module') + if module_name: + pair = module_map[module_name] + else: + pair = modules[-1] + pair[1].append(command) + return modules + + +class ModuleExtender(object): + def __init__(self, wast2wasm, wasm2wast, temp_dir): + self.wast2wasm = wast2wasm + self.wasm2wast = wasm2wast + self.temp_dir = temp_dir + self.lines = [] + self.exports = {} + + def Extend(self, wasm_path, commands): + wast_path = self._RunWasm2Wast(wasm_path) + with open(wast_path) as wast_file: + wast = wast_file.read() + + self.lines = [] + self.exports = self._GetExports(wast) + for i, command in enumerate(commands): + self._Command(i, command) + + wast = wast[:wast.rindex(')')] + '\n\n' + wast += '\n'.join(self.lines) + ')' + # print wast + with open(wast_path, 'w') as wast_file: + wast_file.write(wast) + return self._RunWast2Wasm(wast_path) + + def _Command(self, index, command): + command_type = command['type'] + new_field = 'assert_%d' % index + if command_type == 'assert_return': + self.lines.append('(func (export "%s")' % new_field) + self.lines.append('block') + self._Action(command['action']) + for expected in command['expected']: + self._Reinterpret(expected['type']) + self._Constant(expected) + self._Reinterpret(expected['type']) + self._Compare(expected['type']) + self.lines.extend(['i32.eqz', 'br_if 0']) + self.lines.extend(['return', 'end', 'unreachable', ')']) + elif command_type == 'assert_return_nan': + type_ = command['expected'][0]['type'] + self.lines.append('(func (export "%s")' % new_field) + self.lines.append('(local %s)' % type_) + self.lines.append('block') + self._Action(command['action']) + self.lines.append('tee_local 0') + self._Reinterpret(type_) + self.lines.append('get_local 0') + self._Reinterpret(type_) + self._Compare(type_) + self.lines.extend(['i32.eqz', 'br_if 0', 'return', 'end', + 'unreachable', ')']) + + # Change the command to assert_return, it won't return NaN anymore. + command['type'] = 'assert_return' + elif command_type == 'assert_trap': + self.lines.append('(func (export "%s")' % new_field) + self._Action(command['action']) + self.lines.extend(['br 0', ')']) + else: + raise Error('Unexpected command: %s' % command_type) + + # Update command to point to the new exported function. + command['action']['field'] = new_field + command['action']['args'] = [] + command['expected'] = [] + + def _GetExports(self, wast): + result = {} + pattern = r'^\s*\(export \"(.*?)\"\s*\((\w+) (\d+)' + for name, type_, index in re.findall(pattern, wast, re.MULTILINE): + result[UnescapeWasmString(name)] = (type_, index) + return result + + def _Action(self, action): + export = self.exports[action['field']] + if action['type'] == 'invoke': + for arg in action['args']: + self._Constant(arg) + self.lines.append('call %s' % export[1]) + elif action['type'] == 'get': + self.lines.append('get_global %s' % export[1]) + else: + raise Error('Unexpected action: %s' % action['type']) + + def _Reinterpret(self, type_): + self.lines.extend({'i32': [], + 'i64': [], + 'f32': ['i32.reinterpret/f32'], + 'f64': ['i64.reinterpret/f64']}[type_]) + + def _Compare(self, type_): + self.lines.append({'i32': 'i32.eq', + 'i64': 'i64.eq', + 'f32': 'i32.eq', + 'f64': 'i64.eq'}[type_]) + + def _Constant(self, const): + inst = None + type_ = const['type'] + if type_ == 'i32': + inst = 'i32.const %s' % const['value'] + elif type_ == 'i64': + inst = 'i64.const %s' % const['value'] + elif type_ == 'f32': + inst = F32ToWasm(int(const['value'])) + elif type_ == 'f64': + inst = F64ToWasm(long(const['value'])) + self.lines.append(inst) + + def _RunWasm2Wast(self, wasm_path): + wast_path = ChangeDir(ChangeExt(wasm_path, '.wast'), self.temp_dir) + self.wasm2wast.RunWithArgs(wasm_path, '-o', wast_path) + return wast_path + + def _RunWast2Wasm(self, wast_path): + wasm_path = ChangeDir(ChangeExt(wast_path, '.wasm'), self.temp_dir) + self.wast2wasm.RunWithArgs(wast_path, '-o', wasm_path) + return wasm_path + + +class JSWriter(object): + def __init__(self, base_dir, commands, out_file): + self.base_dir = base_dir + self.commands = commands + self.out_file = out_file + + def Write(self): + for command in self.commands: + self._WriteCommand(command) + + def _WriteCommand(self, command): + command_funcs = { + 'module': self._WriteModuleCommand, + 'action': self._WriteActionCommand, + 'register': self._WriteRegisterCommand, + 'assert_malformed': self._WriteAssertModuleCommand, + 'assert_invalid': self._WriteAssertModuleCommand, + 'assert_unlinkable': self._WriteAssertModuleCommand, + 'assert_uninstantiable': self._WriteAssertModuleCommand, + 'assert_return': self._WriteAssertReturnCommand, + 'assert_return_nan': self._WriteAssertActionCommand, + 'assert_trap': self._WriteAssertActionCommand, + } + + func = command_funcs.get(command['type']) + if func is None: + raise Error('Unexpected type: %s' % command['type']) + func(command) + + def _WriteModuleCommand(self, command): + if 'name' in command: + self.out_file.write('let %s = ' % command['name']) + self.out_file.write('$$ = instance("%s");\n' % + self._Module(command['filename'])) + + def _WriteActionCommand(self, command): + self.out_file.write('%s;\n' % self._Action(command['action'])) + + def _WriteRegisterCommand(self, command): + self.out_file.write('register("%s", %s)\n' % (command['as'], + command.get('name', '$$'))) + + def _WriteAssertModuleCommand(self, command): + filename = command.get('filename') + if filename: + self.out_file.write('%s("%s");\n' % (command['type'], + self._Module(filename))) + else: + # TODO(binji): this is only needed because assert_invalid doesn't write a + # module file currently. + self.out_file.write('// No filename for command: %s\n' % command) + + def _WriteAssertReturnCommand(self, command): + expected = command['expected'] + if len(expected) == 1: + self.out_file.write('assert_return(() => %s, %s);\n' % + (self._Action(command['action']), + self._ConstantList(expected))) + elif len(expected) == 0: + self._WriteAssertActionCommand(command) + else: + raise Error('Unexpected result with multiple values: %s' % expected) + + def _WriteAssertActionCommand(self, command): + self.out_file.write('%s(() => %s);\n' % (command['type'], + self._Action(command['action']))) + + def _Module(self, filename): + with open(os.path.join(self.base_dir, filename), 'rb') as wasm_file: + return ''.join('\\x%02x' % ord(c) for c in wasm_file.read()) + + def _Constant(self, const): + assert IsValidJSConstant(const), 'Invalid JS const: %s' % const + type_ = const['type'] + value = int(const['value']) + if type_ == 'i32': + return I32ToJS(value) + elif type_ == 'f32': + return F32ToJS(value) + elif type_ == 'f64': + return F64ToJS(value) + else: + assert False + + def _ConstantList(self, consts): + return ', '.join(self._Constant(const) for const in consts) + + def _Action(self, action): + type_ = action['type'] + if type_ not in ('invoke', 'get'): + raise Error('Unexpected action type: %s' % type_) + + args = '' + if type_ == 'invoke': + args = '(%s)' % self._ConstantList(action.get('args', [])) + + return '%s.exports["%s"]%s' % (action.get('module', '$$'), + EscapeJSString(action['field']), + args) def main(args): parser = argparse.ArgumentParser() parser.add_argument('-o', '--output', metavar='PATH', help='output file.') + parser.add_argument('-P', '--prefix', metavar='PATH', help='prefix file.', + default=os.path.join(SCRIPT_DIR, 'gen-spec-prefix.js')) + parser.add_argument('--wast2wasm', metavar='PATH', + help='set the wast2wasm executable to use.') + parser.add_argument('--wasm2wast', metavar='PATH', + help='set the wasm2wast executable to use.') + parser.add_argument('--temp-dir', metavar='PATH', + help='set the directory that temporary wasm/wast' + ' files are written.') + parser.add_argument('--no-error-cmdline', + help='don\'t display the subprocess\'s commandline when' + + ' an error occurs', dest='error_cmdline', + action='store_false') + parser.add_argument('-p', '--print-cmd', + help='print the commands that are run.', + action='store_true') parser.add_argument('file', help='spec json file.') options = parser.parse_args(args) - output_data = ProcessJsonFile(options.file) + wast2wasm = Executable(GetWast2WasmExecutable(options.wast2wasm), + error_cmdline=options.error_cmdline) + wasm2wast = Executable(GetWasm2WastExecutable(options.wasm2wast), + error_cmdline=options.error_cmdline) + + wast2wasm.verbose = options.print_cmd + wasm2wast.verbose = options.print_cmd + + with open(options.file) as json_file: + json_dir = os.path.dirname(options.file) + spec_json = json.load(json_file) + all_commands = spec_json['commands'] + + # modules is a list of pairs: [(module_command, [assert_command, ...]), ...] + modules = CollectInvalidModuleCommands(all_commands) + + with utils.TempDirectory(options.temp_dir, 'gen-spec-js-') as temp_dir: + extender = ModuleExtender(wast2wasm, wasm2wast, temp_dir) + for module_command, assert_commands in modules: + if assert_commands: + wasm_path = os.path.join(json_dir, module_command['filename']) + new_module_filename = extender.Extend(wasm_path, assert_commands) + module_command['filename'] = os.path.relpath(new_module_filename, + json_dir) + + output = cStringIO.StringIO() + if options.prefix: + with open(options.prefix) as prefix_file: + output.write(prefix_file.read()) + output.write('\n') + + JSWriter(json_dir, all_commands, output).Write() + if options.output: - output_file = open(options.output, 'w') + out_file = open(options.output, 'w') else: - output_file = sys.stdout + out_file = sys.stdout try: - output_file.write(output_data) + out_file.write(output.getvalue()) finally: - output_file.close() + out_file.close() return 0 |