generate-libwasm-spec-test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. #!/usr/bin/env python3
  2. import struct
  3. from sys import argv, stderr
  4. from os import path
  5. from string import whitespace
  6. import re
  7. import math
  8. from tempfile import NamedTemporaryFile
  9. from subprocess import call
  10. import json
  11. atom_end = set('()"' + whitespace)
  12. def parse(sexp):
  13. sexp = re.sub(r'(?m)\(;.*;\)', '', re.sub(r'(;;.*)', '', sexp))
  14. stack, i, length = [[]], 0, len(sexp)
  15. while i < length:
  16. c = sexp[i]
  17. kind = type(stack[-1])
  18. if kind == list:
  19. if c == '(':
  20. stack.append([])
  21. elif c == ')':
  22. stack[-2].append(stack.pop())
  23. elif c == '"':
  24. stack.append('')
  25. elif c in whitespace:
  26. pass
  27. else:
  28. stack.append((c,))
  29. elif kind == str:
  30. if c == '"':
  31. stack[-2].append(stack.pop())
  32. elif c == '\\':
  33. i += 1
  34. if sexp[i] != '"':
  35. stack[-1] += '\\'
  36. stack[-1] += sexp[i]
  37. else:
  38. stack[-1] += c
  39. elif kind == tuple:
  40. if c in atom_end:
  41. atom = stack.pop()
  42. stack[-1].append(atom)
  43. continue
  44. else:
  45. stack[-1] = ((stack[-1][0] + c),)
  46. i += 1
  47. return stack.pop()
  48. def parse_typed_value(ast):
  49. types = {
  50. 'i32.const': 'i32',
  51. 'i64.const': 'i64',
  52. 'f32.const': 'float',
  53. 'f64.const': 'double',
  54. }
  55. if len(ast) == 2 and ast[0][0] in types:
  56. return {"type": types[ast[0][0]], "value": ast[1][0]}
  57. return {"type": "error"}
  58. def generate_module_source_for_compilation(entries):
  59. s = '('
  60. for entry in entries:
  61. if type(entry) == tuple and len(entry) == 1 and type(entry[0]) == str:
  62. s += entry[0] + ' '
  63. elif type(entry) == str:
  64. s += json.dumps(entry).replace('\\\\', '\\') + ' '
  65. elif type(entry) == list:
  66. s += generate_module_source_for_compilation(entry)
  67. else:
  68. raise Exception("wat? I dunno how to pretty print " + str(type(entry)))
  69. while s.endswith(' '):
  70. s = s[:len(s) - 1]
  71. return s + ')'
  72. def generate_binary_source(chunks):
  73. res = b''
  74. for chunk in chunks:
  75. i = 0
  76. while i < len(chunk):
  77. c = chunk[i]
  78. if c == '\\':
  79. res += bytes.fromhex(chunk[i + 1: i + 3])
  80. i += 3
  81. continue
  82. res += c.encode('utf-8')
  83. i += 1
  84. return res
  85. named_modules = {}
  86. named_modules_inverse = {}
  87. registered_modules = {}
  88. module_output_path: str
  89. def generate_module(ast):
  90. # (module ...)
  91. name = None
  92. mode = 'ast' # binary, quote
  93. start_index = 1
  94. if len(ast) > 1:
  95. if isinstance(ast[1], tuple) and isinstance(ast[1][0], str) and ast[1][0].startswith('$'):
  96. name = ast[1][0]
  97. if len(ast) > 2:
  98. if isinstance(ast[2], tuple) and ast[2][0] in ('binary', 'quote'):
  99. mode = ast[2][0]
  100. start_index = 3
  101. else:
  102. start_index = 2
  103. elif isinstance(ast[1][0], str):
  104. mode = ast[1][0]
  105. start_index = 2
  106. result = {
  107. 'ast': lambda: ('parse', generate_module_source_for_compilation(ast)),
  108. 'binary': lambda: ('literal', generate_binary_source(ast[start_index:])),
  109. # FIXME: Make this work when we have a WAT parser
  110. 'quote': lambda: ('literal', ast[start_index]),
  111. }[mode]()
  112. return {
  113. 'module': result,
  114. 'name': name
  115. }
  116. def generate(ast):
  117. global named_modules, named_modules_inverse, registered_modules
  118. if type(ast) != list:
  119. return []
  120. tests = []
  121. for entry in ast:
  122. if len(entry) > 0 and entry[0] == ('module',):
  123. gen = generate_module(entry)
  124. module, name = gen['module'], gen['name']
  125. tests.append({
  126. "module": module,
  127. "tests": []
  128. })
  129. if name is not None:
  130. named_modules[name] = len(tests) - 1
  131. named_modules_inverse[len(tests) - 1] = (name, None)
  132. elif entry[0] == ('assert_unlinkable',):
  133. # (assert_unlinkable module message)
  134. if len(entry) < 2 or not isinstance(entry[1], list) or entry[1][0] != ('module',):
  135. print(f"Invalid argument to assert_unlinkable: {entry[1]}", file=stderr)
  136. continue
  137. result = generate_module(entry[1])
  138. tests.append({
  139. 'module': None,
  140. 'tests': [{
  141. "kind": "unlinkable",
  142. "module": result['module'],
  143. }]
  144. })
  145. elif len(entry) in [2, 3] and entry[0][0].startswith('assert_'):
  146. if entry[1][0] == ('invoke',):
  147. arg, name, module = 0, None, None
  148. if isinstance(entry[1][1], str):
  149. name = entry[1][1]
  150. else:
  151. name = entry[1][2]
  152. module = named_modules[entry[1][1][0]]
  153. arg = 1
  154. tests[-1]["tests"].append({
  155. "kind": entry[0][0][len('assert_'):],
  156. "function": {
  157. "module": module,
  158. "name": name,
  159. "args": list(parse_typed_value(x) for x in entry[1][arg + 2:])
  160. },
  161. "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None
  162. })
  163. elif entry[1][0] == ('get',):
  164. arg, name, module = 0, None, None
  165. if isinstance(entry[1][1], str):
  166. name = entry[1][1]
  167. else:
  168. name = entry[1][2]
  169. module = named_modules[entry[1][1][0]]
  170. arg = 1
  171. tests[-1]["tests"].append({
  172. "kind": entry[0][0][len('assert_'):],
  173. "get": {
  174. "name": name,
  175. "module": module,
  176. },
  177. "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None
  178. })
  179. else:
  180. if not len(tests):
  181. tests.append({
  182. "module": ('literal', b""),
  183. "tests": []
  184. })
  185. tests[-1]["tests"].append({
  186. "kind": "testgen_fail",
  187. "function": {
  188. "module": None,
  189. "name": "<unknown>",
  190. "args": []
  191. },
  192. "reason": f"Unknown assertion {entry[0][0][len('assert_'):]}"
  193. })
  194. elif len(entry) >= 2 and entry[0][0] == 'invoke':
  195. # toplevel invoke :shrug:
  196. arg, name, module = 0, None, None
  197. if not isinstance(entry[1], str) and isinstance(entry[1][1], str):
  198. name = entry[1][1]
  199. elif isinstance(entry[1], str):
  200. name = entry[1]
  201. else:
  202. name = entry[1][2]
  203. module = named_modules[entry[1][1][0]]
  204. arg = 1
  205. tests[-1]["tests"].append({
  206. "kind": "ignore",
  207. "function": {
  208. "module": module,
  209. "name": name,
  210. "args": list(parse_typed_value(x) for x in entry[1][arg + 2:])
  211. },
  212. "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None
  213. })
  214. elif len(entry) > 1 and entry[0][0] == 'register':
  215. if len(entry) == 3:
  216. registered_modules[entry[1]] = named_modules[entry[2][0]]
  217. x = named_modules_inverse[named_modules[entry[2][0]]]
  218. named_modules_inverse[named_modules[entry[2][0]]] = (x[0], entry[1])
  219. else:
  220. index = len(tests) - 1
  221. registered_modules[entry[1]] = index
  222. named_modules_inverse[index] = (":" + entry[1], entry[1])
  223. else:
  224. if not len(tests):
  225. tests.append({
  226. "module": ('literal', b""),
  227. "tests": []
  228. })
  229. tests[-1]["tests"].append({
  230. "kind": "testgen_fail",
  231. "function": {
  232. "module": None,
  233. "name": "<unknown>",
  234. "args": []
  235. },
  236. "reason": f"Unknown command {entry[0][0]}"
  237. })
  238. return tests
  239. def genarg(spec):
  240. if spec['type'] == 'error':
  241. return '0'
  242. def gen():
  243. x = spec['value']
  244. if spec['type'] in ('i32', 'i64'):
  245. if x.startswith('0x'):
  246. if spec['type'] == 'i32':
  247. # cast back to i32 to get the correct sign
  248. return str(struct.unpack('>i', struct.pack('>Q', int(x, 16))[4:])[0])
  249. # cast back to i64 to get the correct sign
  250. return str(struct.unpack('>q', struct.pack('>Q', int(x, 16)))[0]) + 'n'
  251. if spec['type'] == 'i64':
  252. # Make a bigint instead, since `double' cannot fit all i64 values.
  253. return x + 'n'
  254. return x
  255. if x == 'nan':
  256. return 'NaN'
  257. if x == '-nan':
  258. return '-NaN'
  259. try:
  260. x = float(x)
  261. if math.isnan(x):
  262. # FIXME: This is going to mess up the different kinds of nan
  263. return '-NaN' if math.copysign(1.0, x) < 0 else 'NaN'
  264. if math.isinf(x):
  265. return 'Infinity' if x > 0 else '-Infinity'
  266. return x
  267. except ValueError:
  268. try:
  269. x = float.fromhex(x)
  270. if math.isnan(x):
  271. # FIXME: This is going to mess up the different kinds of nan
  272. return '-NaN' if math.copysign(1.0, x) < 0 else 'NaN'
  273. if math.isinf(x):
  274. return 'Infinity' if x > 0 else '-Infinity'
  275. return x
  276. except ValueError:
  277. try:
  278. x = int(x, 0)
  279. return x
  280. except ValueError:
  281. return x
  282. x = gen()
  283. if isinstance(x, str):
  284. if x.startswith('nan'):
  285. return 'NaN'
  286. if x.startswith('-nan'):
  287. return '-NaN'
  288. return x
  289. return str(x)
  290. all_names_in_main = {}
  291. def genresult(ident, entry, index):
  292. expectation = f'expect().fail("Unknown result structure " + {json.dumps(entry)})'
  293. if "function" in entry:
  294. tmodule = 'module'
  295. if entry['function']['module'] is not None:
  296. tmodule = f'namedModules[{json.dumps(named_modules_inverse[entry["function"]["module"]][0])}]'
  297. expectation = (
  298. f'{tmodule}.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])})'
  299. )
  300. elif "get" in entry:
  301. expectation = f'module.getExport({ident})'
  302. if entry['kind'] == 'return':
  303. return (
  304. f'let {ident}_result = {expectation};\n ' +
  305. (f'expect({ident}_result).toBe({genarg(entry["result"])})\n ' if entry["result"] is not None else '')
  306. )
  307. if entry['kind'] == 'trap':
  308. return (
  309. f'expect(() => {expectation}).toThrow(TypeError, "Execution trapped");\n '
  310. )
  311. if entry['kind'] == 'ignore':
  312. return expectation
  313. if entry['kind'] == 'unlinkable':
  314. name = f'mod-{ident}-{index}.wasm'
  315. outpath = path.join(module_output_path, name)
  316. if not compile_wasm_source(entry['module'], outpath):
  317. return 'throw new Error("Module compilation failed");'
  318. return (
  319. f' expect(() => {{\n'
  320. f' let content = readBinaryWasmFile("Fixtures/SpecTests/{name}");\n'
  321. f' parseWebAssemblyModule(content, globalImportObject);\n'
  322. f' }}).toThrow(TypeError, "Linking failed");'
  323. )
  324. if entry['kind'] == 'testgen_fail':
  325. return f'throw Exception("Test Generator Failure: " + {json.dumps(entry["reason"])});\n '
  326. return f'throw Exception("(Test Generator) Unknown test kind {entry["kind"]}");\n '
  327. raw_test_number = 0
  328. def gentest(entry, main_name):
  329. global raw_test_number
  330. isfunction = 'function' in entry
  331. name: str
  332. isempty = False
  333. if isfunction or 'get' in entry:
  334. name = json.dumps((entry["function"] if isfunction else entry["get"])["name"])[1:-1]
  335. else:
  336. isempty = True
  337. name = str(f"_inline_test_{raw_test_number}")
  338. raw_test_number += 1
  339. if type(name) != str:
  340. print("Unsupported test case (call to", name, ")", file=stderr)
  341. return '\n '
  342. ident = '_' + re.sub("[^a-zA-Z_0-9]", "_", name)
  343. count = all_names_in_main.get(name, 0)
  344. all_names_in_main[name] = count + 1
  345. test_name = f'execution of {main_name}: {name} (instance {count})'
  346. tmodule = 'module'
  347. if not isempty:
  348. key = "function" if "function" in entry else "get"
  349. if entry[key]['module'] is not None:
  350. tmodule = f'namedModules[{json.dumps(named_modules_inverse[entry[key]["module"]][0])}]'
  351. source = (
  352. f'test({json.dumps(test_name)}, () => {{\n' +
  353. (
  354. f'let {ident} = {tmodule}.getExport({json.dumps(name)});\n '
  355. f'expect({ident}).not.toBeUndefined();\n '
  356. if not isempty else ''
  357. ) +
  358. f'{genresult(ident, entry, count)}'
  359. '});\n\n '
  360. )
  361. return source
  362. def gen_parse_module(name, index):
  363. export_string = ''
  364. if index in named_modules_inverse:
  365. entry = named_modules_inverse[index]
  366. export_string += f'namedModules[{json.dumps(entry[0])}] = module;\n '
  367. if entry[1]:
  368. export_string += f'globalImportObject[{json.dumps(entry[1])}] = module;\n '
  369. return (
  370. f'let content = readBinaryWasmFile("Fixtures/SpecTests/{name}.wasm");\n '
  371. f'const module = parseWebAssemblyModule(content, globalImportObject)\n '
  372. f'{export_string}\n '
  373. )
  374. def nth(a, x, y=None):
  375. if y:
  376. return a[x:y]
  377. return a[x]
  378. def compile_wasm_source(mod, outpath):
  379. if not mod:
  380. return True
  381. if mod[0] == 'literal':
  382. with open(outpath, 'wb+') as f:
  383. f.write(mod[1])
  384. return True
  385. elif mod[0] == 'parse':
  386. with NamedTemporaryFile("w+") as temp:
  387. temp.write(mod[1])
  388. temp.flush()
  389. rc = call(["wat2wasm", temp.name, "-o", outpath])
  390. return rc == 0
  391. return False
  392. def main():
  393. global module_output_path
  394. with open(argv[1]) as f:
  395. sexp = f.read()
  396. name = argv[2]
  397. module_output_path = argv[3]
  398. ast = parse(sexp)
  399. print('let globalImportObject = {};')
  400. print('let namedModules = {};\n')
  401. for index, description in enumerate(generate(ast)):
  402. testname = f'{name}_{index}'
  403. outpath = path.join(module_output_path, f'{testname}.wasm')
  404. mod = description["module"]
  405. if not compile_wasm_source(mod, outpath):
  406. print("Failed to compile", name, "module index", index, "skipping that test", file=stderr)
  407. continue
  408. sep = ""
  409. print(f'''describe({json.dumps(testname)}, () => {{
  410. {gen_parse_module(testname, index) if mod else ''}
  411. {sep.join(gentest(x, testname) for x in description["tests"])}
  412. }});
  413. ''')
  414. if __name__ == "__main__":
  415. main()