generate-libwasm-spec-test.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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. import array
  12. atom_end = set('()"' + whitespace)
  13. def parse(sexp):
  14. sexp = re.sub(r'(?m)\(;.*;\)', '', re.sub(r'(;;.*)', '', sexp))
  15. stack, i, length = [[]], 0, len(sexp)
  16. while i < length:
  17. c = sexp[i]
  18. kind = type(stack[-1])
  19. if kind == list:
  20. if c == '(':
  21. stack.append([])
  22. elif c == ')':
  23. stack[-2].append(stack.pop())
  24. elif c == '"':
  25. stack.append('')
  26. elif c in whitespace:
  27. pass
  28. else:
  29. stack.append((c,))
  30. elif kind == str:
  31. if c == '"':
  32. stack[-2].append(stack.pop())
  33. elif c == '\\':
  34. i += 1
  35. if sexp[i] != '"':
  36. stack[-1] += '\\'
  37. stack[-1] += sexp[i]
  38. else:
  39. stack[-1] += c
  40. elif kind == tuple:
  41. if c in atom_end:
  42. atom = stack.pop()
  43. stack[-1].append(atom)
  44. continue
  45. else:
  46. stack[-1] = ((stack[-1][0] + c),)
  47. i += 1
  48. return stack.pop()
  49. class TestGenerationError(Exception):
  50. def __init__(self, message):
  51. self.msg = message
  52. def parse_typed_value(ast):
  53. types = {
  54. 'i32.const': 'i32',
  55. 'i64.const': 'i64',
  56. 'f32.const': 'float',
  57. 'f64.const': 'double',
  58. 'v128.const': 'bigint',
  59. }
  60. v128_sizes = {
  61. 'i8x16': 1,
  62. 'i16x8': 2,
  63. 'i32x4': 4,
  64. 'i64x2': 8,
  65. 'f32x4': 4,
  66. 'f64x2': 8,
  67. }
  68. v128_format_names = {
  69. 'i8x16': 'b',
  70. 'i16x8': 'h',
  71. 'i32x4': 'i',
  72. 'i64x2': 'q',
  73. 'f32x4': 'f',
  74. 'f64x2': 'd',
  75. }
  76. v128_format_names_unsigned = {
  77. 'i8x16': 'B',
  78. 'i16x8': 'H',
  79. 'i32x4': 'I',
  80. 'i64x2': 'Q',
  81. }
  82. def parse_v128_chunk(num, type) -> array:
  83. negative = 1
  84. if num.startswith('-'):
  85. negative = -1
  86. num = num[1:]
  87. elif num.startswith('+'):
  88. num = num[1:]
  89. # wtf spec test, split your wast tests already
  90. while num.startswith('0') and not num.startswith('0x'):
  91. num = num[1:]
  92. if num == '':
  93. num = '0'
  94. if type.startswith('f'):
  95. def generate():
  96. if num == 'nan:canonical':
  97. return float.fromhex('0x7fc00000')
  98. if num == 'nan:arithmetic':
  99. return float.fromhex('0x7ff00000')
  100. if num == 'nan:signaling':
  101. return float.fromhex('0x7ff80000')
  102. if num.startswith('nan:'):
  103. # FIXME: I have no idea if this is actually correct :P
  104. rest = num[4:]
  105. return float.fromhex('0x7ff80000') + int(rest, base=16)
  106. if num.lower() == 'infinity':
  107. return float.fromhex('0x7ff00000') * negative
  108. try:
  109. return float(num) * negative
  110. except ValueError:
  111. return float.fromhex(num) * negative
  112. value = generate()
  113. return struct.pack(f'={v128_format_names[type]}', value)
  114. value = negative * int(num.replace('_', ''), base=0)
  115. try:
  116. return struct.pack(f'={v128_format_names[type]}', value)
  117. except struct.error:
  118. # The test format uses signed and unsigned values interchangeably, this is probably an unsigned value.
  119. return struct.pack(f'={v128_format_names_unsigned[type]}', value)
  120. if len(ast) >= 2 and ast[0][0] in types:
  121. if ast[0][0] == 'v128.const':
  122. value = array.array('b')
  123. for i, num in enumerate(ast[2:]):
  124. size = v128_sizes[ast[1][0]]
  125. s = len(value)
  126. value.frombytes(parse_v128_chunk(num[0], ast[1][0]))
  127. assert len(value) - s == size, f'Expected {size} bytes, got {len(value) - s} bytes'
  128. return {
  129. 'type': types[ast[0][0]],
  130. 'value': value.tobytes().hex()
  131. }
  132. return {"type": types[ast[0][0]], "value": ast[1][0]}
  133. return {"type": "error"}
  134. def generate_module_source_for_compilation(entries):
  135. s = '('
  136. for entry in entries:
  137. if type(entry) is tuple and len(entry) == 1 and type(entry[0]) is str:
  138. s += entry[0] + ' '
  139. elif type(entry) is str:
  140. s += json.dumps(entry).replace('\\\\', '\\') + ' '
  141. elif type(entry) is list:
  142. s += generate_module_source_for_compilation(entry)
  143. else:
  144. raise Exception("wat? I dunno how to pretty print " + str(type(entry)))
  145. while s.endswith(' '):
  146. s = s[:len(s) - 1]
  147. return s + ')'
  148. def generate_binary_source(chunks):
  149. res = b''
  150. for chunk in chunks:
  151. i = 0
  152. while i < len(chunk):
  153. c = chunk[i]
  154. if c == '\\':
  155. res += bytes.fromhex(chunk[i + 1: i + 3])
  156. i += 3
  157. continue
  158. res += c.encode('utf-8')
  159. i += 1
  160. return res
  161. named_modules = {}
  162. named_modules_inverse = {}
  163. registered_modules = {}
  164. module_output_path: str
  165. def generate_module(ast):
  166. # (module ...)
  167. name = None
  168. mode = 'ast' # binary, quote
  169. start_index = 1
  170. if len(ast) > 1:
  171. if isinstance(ast[1], tuple) and isinstance(ast[1][0], str) and ast[1][0].startswith('$'):
  172. name = ast[1][0]
  173. if len(ast) > 2:
  174. if isinstance(ast[2], tuple) and ast[2][0] in ('binary', 'quote'):
  175. mode = ast[2][0]
  176. start_index = 3
  177. else:
  178. start_index = 2
  179. elif isinstance(ast[1][0], str):
  180. mode = ast[1][0]
  181. start_index = 2
  182. result = {
  183. 'ast': lambda: ('parse', generate_module_source_for_compilation(ast)),
  184. 'binary': lambda: ('literal', generate_binary_source(ast[start_index:])),
  185. # FIXME: Make this work when we have a WAT parser
  186. 'quote': lambda: ('literal', ast[start_index]),
  187. }[mode]()
  188. return {
  189. 'module': result,
  190. 'name': name
  191. }
  192. def generate(ast):
  193. global named_modules, named_modules_inverse, registered_modules
  194. if type(ast) is not list:
  195. return []
  196. tests = []
  197. for entry in ast:
  198. if len(entry) > 0 and entry[0] == ('module',):
  199. gen = generate_module(entry)
  200. module, name = gen['module'], gen['name']
  201. tests.append({
  202. "module": module,
  203. "tests": []
  204. })
  205. if name is not None:
  206. named_modules[name] = len(tests) - 1
  207. named_modules_inverse[len(tests) - 1] = (name, None)
  208. elif entry[0] == ('assert_unlinkable',):
  209. # (assert_unlinkable module message)
  210. if len(entry) < 2 or not isinstance(entry[1], list) or entry[1][0] != ('module',):
  211. print(f"Invalid argument to assert_unlinkable: {entry[1]}", file=stderr)
  212. continue
  213. result = generate_module(entry[1])
  214. tests.append({
  215. 'module': None,
  216. 'tests': [{
  217. "kind": "unlinkable",
  218. "module": result['module'],
  219. }]
  220. })
  221. elif entry[0] in (('assert_malformed',), ('assert_invalid',)):
  222. # (assert_malformed/invalid module message)
  223. if len(entry) < 2 or not isinstance(entry[1], list) or entry[1][0] != ('module',):
  224. print(f"Invalid argument to assert_malformed: {entry[1]}", file=stderr)
  225. continue
  226. result = generate_module(entry[1])
  227. kind = entry[0][0][len('assert_'):]
  228. tests.append({
  229. 'module': None,
  230. 'kind': kind,
  231. 'tests': [{
  232. "kind": kind,
  233. "module": result['module'],
  234. }]
  235. })
  236. elif len(entry) in [2, 3] and entry[0][0].startswith('assert_'):
  237. if entry[1][0] == ('invoke',):
  238. arg, name, module = 0, None, None
  239. if isinstance(entry[1][1], str):
  240. name = entry[1][1]
  241. else:
  242. name = entry[1][2]
  243. module = named_modules[entry[1][1][0]]
  244. arg = 1
  245. kind = entry[0][0][len('assert_'):]
  246. tests[-1]["tests"].append({
  247. "kind": kind,
  248. "function": {
  249. "module": module,
  250. "name": name,
  251. "args": list(parse_typed_value(x) for x in entry[1][arg + 2:])
  252. },
  253. "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg and kind != 'exhaustion' else None
  254. })
  255. elif entry[1][0] == ('get',):
  256. arg, name, module = 0, None, None
  257. if isinstance(entry[1][1], str):
  258. name = entry[1][1]
  259. else:
  260. name = entry[1][2]
  261. module = named_modules[entry[1][1][0]]
  262. arg = 1
  263. tests[-1]["tests"].append({
  264. "kind": entry[0][0][len('assert_'):],
  265. "get": {
  266. "name": name,
  267. "module": module,
  268. },
  269. "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None
  270. })
  271. else:
  272. if not len(tests):
  273. tests.append({
  274. "module": ('literal', b""),
  275. "tests": []
  276. })
  277. tests[-1]["tests"].append({
  278. "kind": "testgen_fail",
  279. "function": {
  280. "module": None,
  281. "name": "<unknown>",
  282. "args": []
  283. },
  284. "reason": f"Unknown assertion {entry[0][0][len('assert_'):]}"
  285. })
  286. elif len(entry) >= 2 and entry[0][0] == 'invoke':
  287. # toplevel invoke :shrug:
  288. arg, name, module = 0, None, None
  289. if not isinstance(entry[1], str) and isinstance(entry[1][1], str):
  290. name = entry[1][1]
  291. elif isinstance(entry[1], str):
  292. name = entry[1]
  293. else:
  294. name = entry[1][2]
  295. module = named_modules[entry[1][1][0]]
  296. arg = 1
  297. tests[-1]["tests"].append({
  298. "kind": "ignore",
  299. "function": {
  300. "module": module,
  301. "name": name,
  302. "args": list(parse_typed_value(x) for x in entry[1][arg + 2:])
  303. },
  304. "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None
  305. })
  306. elif len(entry) > 1 and entry[0][0] == 'register':
  307. if len(entry) == 3:
  308. registered_modules[entry[1]] = named_modules[entry[2][0]]
  309. x = named_modules_inverse[named_modules[entry[2][0]]]
  310. named_modules_inverse[named_modules[entry[2][0]]] = (x[0], entry[1])
  311. else:
  312. index = len(tests) - 1
  313. registered_modules[entry[1]] = index
  314. named_modules_inverse[index] = (":" + entry[1], entry[1])
  315. else:
  316. if not len(tests):
  317. tests.append({
  318. "module": ('literal', b""),
  319. "tests": []
  320. })
  321. tests[-1]["tests"].append({
  322. "kind": "testgen_fail",
  323. "function": {
  324. "module": None,
  325. "name": "<unknown>",
  326. "args": []
  327. },
  328. "reason": f"Unknown command {entry[0][0]}"
  329. })
  330. return tests
  331. def genarg(spec):
  332. if spec['type'] == 'error':
  333. return '0'
  334. def gen():
  335. x = spec['value']
  336. if spec['type'] == 'bigint':
  337. return f"0x{x}n"
  338. if spec['type'] in ('i32', 'i64'):
  339. if x.startswith('0x'):
  340. if spec['type'] == 'i32':
  341. # cast back to i32 to get the correct sign
  342. return str(struct.unpack('>i', struct.pack('>Q', int(x, 16))[4:])[0])
  343. # cast back to i64 to get the correct sign
  344. return str(struct.unpack('>q', struct.pack('>Q', int(x, 16)))[0]) + 'n'
  345. if spec['type'] == 'i64':
  346. # Make a bigint instead, since `double' cannot fit all i64 values.
  347. if x.startswith('0'):
  348. x = x.lstrip('0')
  349. if x == '':
  350. x = '0'
  351. return x + 'n'
  352. return x
  353. if x == 'nan':
  354. return 'NaN'
  355. if x == '-nan':
  356. return '-NaN'
  357. try:
  358. x = float(x)
  359. if math.isnan(x):
  360. # FIXME: This is going to mess up the different kinds of nan
  361. return '-NaN' if math.copysign(1.0, x) < 0 else 'NaN'
  362. if math.isinf(x):
  363. return 'Infinity' if x > 0 else '-Infinity'
  364. return x
  365. except ValueError:
  366. try:
  367. x = float.fromhex(x)
  368. if math.isnan(x):
  369. # FIXME: This is going to mess up the different kinds of nan
  370. return '-NaN' if math.copysign(1.0, x) < 0 else 'NaN'
  371. if math.isinf(x):
  372. return 'Infinity' if x > 0 else '-Infinity'
  373. return x
  374. except ValueError:
  375. try:
  376. x = int(x, 0)
  377. return x
  378. except ValueError:
  379. return x
  380. x = gen()
  381. if isinstance(x, str):
  382. if x.startswith('nan'):
  383. return 'NaN'
  384. if x.startswith('-nan'):
  385. return '-NaN'
  386. return x
  387. return str(x)
  388. all_names_in_main = {}
  389. def genresult(ident, entry, index):
  390. expectation = None
  391. if "function" in entry:
  392. tmodule = 'module'
  393. if entry['function']['module'] is not None:
  394. tmodule = f'namedModules[{json.dumps(named_modules_inverse[entry["function"]["module"]][0])}]'
  395. expectation = (
  396. f'{tmodule}.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])})'
  397. )
  398. elif "get" in entry:
  399. expectation = f'module.getExport({ident})'
  400. if entry['kind'] == 'return':
  401. return (
  402. f'let {ident}_result = {expectation};\n ' +
  403. (f'expect({ident}_result).toBe({genarg(entry["result"])})\n ' if entry["result"] is not None else '')
  404. )
  405. if entry['kind'] == 'ignore':
  406. return expectation
  407. if entry['kind'] == 'unlinkable':
  408. name = f'mod-{ident}-{index}.wasm'
  409. outpath = path.join(module_output_path, name)
  410. if not compile_wasm_source(entry['module'], outpath):
  411. return 'throw new Error("Module compilation failed");'
  412. return (
  413. f' expect(() => {{\n'
  414. f' let content = readBinaryWasmFile("Fixtures/SpecTests/{name}");\n'
  415. f' parseWebAssemblyModule(content, globalImportObject);\n'
  416. f' }}).toThrow(TypeError, "Linking failed");'
  417. )
  418. if entry['kind'] in ('exhaustion', 'trap', 'invalid'):
  419. return (
  420. f'expect(() => {expectation}.toThrow(TypeError, "Execution trapped"));\n '
  421. )
  422. if entry['kind'] == 'malformed':
  423. return ''
  424. if entry['kind'] == 'testgen_fail':
  425. raise TestGenerationError(entry["reason"])
  426. if not expectation:
  427. raise TestGenerationError(f"Unknown test result structure in {json.dumps(entry)}")
  428. return expectation
  429. raw_test_number = 0
  430. def gentest(entry, main_name):
  431. global raw_test_number
  432. isfunction = 'function' in entry
  433. name: str
  434. isempty = False
  435. if isfunction or 'get' in entry:
  436. name = json.dumps((entry["function"] if isfunction else entry["get"])["name"])[1:-1]
  437. else:
  438. isempty = True
  439. name = str(f"_inline_test_{raw_test_number}")
  440. raw_test_number += 1
  441. if type(name) is not str:
  442. print("Unsupported test case (call to", name, ")", file=stderr)
  443. return '\n '
  444. ident = '_' + re.sub("[^a-zA-Z_0-9]", "_", name)
  445. count = all_names_in_main.get(name, 0)
  446. all_names_in_main[name] = count + 1
  447. test_name = f'execution of {main_name}: {name} (instance {count})'
  448. tmodule = 'module'
  449. if not isempty:
  450. key = "function" if "function" in entry else "get"
  451. if entry[key]['module'] is not None:
  452. tmodule = f'namedModules[{json.dumps(named_modules_inverse[entry[key]["module"]][0])}]'
  453. test = "_test"
  454. try:
  455. result = genresult(ident, entry, count)
  456. except TestGenerationError as e:
  457. test = f"/* {e.msg} */ _test.skip"
  458. result = ""
  459. return (
  460. f'{test}({json.dumps(test_name)}, () => {{\n' +
  461. (
  462. f'let {ident} = {tmodule}.getExport({json.dumps(name)});\n '
  463. f'expect({ident}).not.toBeUndefined();\n '
  464. if not isempty else ''
  465. ) +
  466. f'{result}'
  467. '});\n\n '
  468. )
  469. def gen_parse_module(name, index):
  470. export_string = ''
  471. if index in named_modules_inverse:
  472. entry = named_modules_inverse[index]
  473. export_string += f'namedModules[{json.dumps(entry[0])}] = module;\n '
  474. if entry[1]:
  475. export_string += f'globalImportObject[{json.dumps(entry[1])}] = module;\n '
  476. return (
  477. 'let content, module;\n '
  478. 'try {\n '
  479. f'content = readBinaryWasmFile("Fixtures/SpecTests/{name}.wasm");\n '
  480. f'module = parseWebAssemblyModule(content, globalImportObject)\n '
  481. '} catch(e) { _test("parse", () => expect().fail(e)); _test = test.skip; _test.skip = test.skip; }\n '
  482. f'{export_string}\n '
  483. )
  484. def nth(a, x, y=None):
  485. if y:
  486. return a[x:y]
  487. return a[x]
  488. def compile_wasm_source(mod, outpath):
  489. if not mod:
  490. return True
  491. if mod[0] == 'literal':
  492. with open(outpath, 'wb+') as f:
  493. f.write(mod[1])
  494. return True
  495. elif mod[0] == 'parse':
  496. with NamedTemporaryFile("w+") as temp:
  497. temp.write(mod[1])
  498. temp.flush()
  499. rc = call(["wat2wasm", "--enable-all", "--no-check", temp.name, "-o", outpath])
  500. return rc == 0
  501. return False
  502. def main():
  503. global module_output_path
  504. with open(argv[1]) as f:
  505. sexp = f.read()
  506. name = argv[2]
  507. module_output_path = argv[3]
  508. ast = parse(sexp)
  509. print('let globalImportObject = {};')
  510. print('let namedModules = {};\n')
  511. for index, description in enumerate(generate(ast)):
  512. testname = f'{name}_{index}'
  513. outpath = path.join(module_output_path, f'{testname}.wasm')
  514. mod = description["module"]
  515. if not compile_wasm_source(mod, outpath) and ('kind' not in description or description["kind"] != "malformed"):
  516. print("Failed to compile", name, "module index", index, "skipping that test", file=stderr)
  517. continue
  518. sep = ""
  519. print(f'''describe({json.dumps(testname)}, () => {{
  520. let _test = test;
  521. {gen_parse_module(testname, index) if mod else ''}
  522. {sep.join(gentest(x, testname) for x in description["tests"])}
  523. }});
  524. ''')
  525. if __name__ == "__main__":
  526. main()