From 0c37e1bc1ca2a811ef4779302ff477ec4f8fe49e Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 7 Jan 2026 21:18:45 +0000 Subject: [PATCH 01/10] Add script to generate spec test --- .ruff.toml | 1 + scripts/test/generate-atomic-spec-test.py | 257 ++++++++++++++++++++++ test/spec/relaxed-atomics.wast | 203 +++++------------ 3 files changed, 317 insertions(+), 144 deletions(-) create mode 100644 scripts/test/generate-atomic-spec-test.py diff --git a/.ruff.toml b/.ruff.toml index 69c92ac2bae..e6fd850fd43 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -31,6 +31,7 @@ ignore = [ "B011", # https://docs.astral.sh/ruff/rules/assert-false/ "B023", # https://docs.astral.sh/ruff/rules/function-uses-loop-variable/ "E501", # https://docs.astral.sh/ruff/rules/line-too-long/ + "E741", # https://docs.astral.sh/ruff/rules/ambiguous-variable-name/ "PERF401", # https://docs.astral.sh/ruff/rules/manual-list-comprehension/ "PLR0912", # https://docs.astral.sh/ruff/rules/too-many-branches/ "PLR0913", # https://docs.astral.sh/ruff/rules/too-many-arguments/ diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py new file mode 100644 index 00000000000..0f4da9cf71c --- /dev/null +++ b/scripts/test/generate-atomic-spec-test.py @@ -0,0 +1,257 @@ +import subprocess +import sys +import tempfile +from argparse import ArgumentParser +from collections.abc import Iterator +from pathlib import Path + +# Workaround for python <3.10, escape characters can't appear in f-strings. +# Although we require 3.10 in some places, the formatter complains without this. +newline = "\n" + +backslash = '\\' + + +def indent(s): + return "\n".join(f" {line}" if line else "" for line in s.split("\n")) + + +# skips None for convenience +def instruction(*args): + return f"({' '.join(arg for arg in args if arg is not None)})" + + +def atomic_instruction(op, memid, immediate, /, *args, drop): + if drop: + return f"(drop {instruction(op, memid, immediate, *args)})" + return instruction(op, memid, immediate, *args) + + +all_ops = [ + ("i32.atomic.load", "(i32.const 51)", True), + ("i32.atomic.store", "(i32.const 51) (i32.const 51)", False), +] + + +def func(memid, immediate, ops=all_ops): + return f'''(func ${immediate if immediate is not None else "no_immediate"}{"_with_memid" if memid is not None else "_without_memid"} +{indent(newline.join(atomic_instruction(op, memid, immediate, arg, drop=should_drop) for op, arg, should_drop in ops))} +)''' + + +def module(*statements): + return f'''(module +{newline.join(map(indent, statements))} +)''' + + +def module_binary(bin): + return f'''(module binary "{''.join(f'{backslash}{byte:02x}' for byte in bin)}")''' + + +def assert_invalid(module, reason): + return f'''(assert_invalid {module} "{reason}")''' + + +def generate_atomic_spec_test(): + # Declare two memories so we have control over whether the memory immediate is printed + # A memory immediate of 0 is allowed to be omitted. + return module( + "(memory 1 1 shared)", + "(memory 1 1 shared)", + "", + "\n\n".join([f'{func(memid, ordering)}' for memid in [None, "1"] for ordering in [None, "acqrel", "seqcst"]])) + + +def to_binary(wasm_as, wat: str) -> bytes: + with tempfile.NamedTemporaryFile(mode="w+") as input, tempfile.NamedTemporaryFile(mode="rb") as output: + input.write(wat) + input.seek(0) + + proc = subprocess.run([wasm_as, "--enable-multimemory", "--enable-threads", "--enable-relaxed-atomics", input.name, "-o", output.name], capture_output=True) + try: + proc.check_returncode() + except Exception: + print(proc.stderr.decode('utf-8'), end="", file=sys.stderr) + raise + + return output.read() + + +def findall(bytes, byte): + ix = -1 + while ((ix := bytes.find(byte, ix + 1)) != -1): + yield ix + + +def read_unsigned_leb(bytes, start): + """Returns (bytes read, value)""" + ret = 0 + for i, byte in enumerate(bytes[start:]): + ret |= (byte & ~(1 << 7)) << (7 * i) + if not byte & (1 << 7): + return i + 1, ret + raise ValueError("Unexpected end of input, continuation bit was set for the last byte.") + + +def to_unsigned_leb(num): + ret = bytearray() + + if num == 0: + ret = bytearray() + ret.append(0) + return ret + ret = bytearray() + while num > 0: + rem = num >> 7 + ret.append((num & 0x7F) | (bool(rem) << 7)) + + num = rem + return ret + + +def unsigned_leb_add(bytes: bytearray, start, add) -> int: + """Returns number of bytes added""" + l, decoded = read_unsigned_leb(bytes, start) + added = to_unsigned_leb(decoded + add) + + bytes[start:start + l] = added[:l] + + if len(added) > l: + for i, b in enumerate(added[l:], start=l): + bytes.insert(i, b) + + return len(added) - l + + +def unsigned_leb_subtract(bytes, start, sub): + l, decoded = read_unsigned_leb(bytes, start) + subbed = to_unsigned_leb(decoded - sub) + + bytes[start:start + len(subbed)] = subbed + + diff = l - len(subbed) + for _ in range(diff): + bytes.pop(start + len(subbed)) + + return -diff + + +def iterate_sections(bytes) -> Iterator[bytearray]: + bytes = bytes.removeprefix(b"\00asm\01\00\00\00") + start = 0 + while True: + read, size = read_unsigned_leb(bytes, start + 1) + + # section op + section size + body + yield bytearray(bytes[start:start + 1 + read + size]) + start += 1 + read + size + if start > len(bytes): + raise ValueError("not expected", start, len(bytes)) + elif start == len(bytes): + return + + +def iterate_functions(bytes) -> Iterator[bytearray]: + read, size = read_unsigned_leb(bytes, 1) + read2, size2 = read_unsigned_leb(bytes, 1 + read) + section_body = bytes[1 + read + read2:] + + start = 0 + while True: + read, size = read_unsigned_leb(section_body, start) + yield bytearray(section_body[start:start + read + size]) + start += read + size + if start > len(section_body): + raise ValueError("not expected", start, len(section_body)) + elif start == len(section_body): + return + + +def binary_tests(b: bytes) -> bytes: + updated_tests = [b"\00asm\01\00\00\00"] + + for section in iterate_sections(b): + if section[0] != 0x0a: + updated_tests.append(section) + continue + + bytes_read, size = read_unsigned_leb(section, 1) + _, func_count = read_unsigned_leb(section, 1 + bytes_read) + + updated_code_section = bytearray() + updated_code_section.append(0x0a) + updated_code_section += to_unsigned_leb(size) + + updated_code_section += to_unsigned_leb(func_count) + + section_bytes_added = 0 + for i, func in enumerate(iterate_functions(section)): + # TODO: this is wrong if the function size is 0xfe + ix = func.find(0xfe) + if ix == -1: + raise ValueError("Didn't find atomic operation") + if i not in (2, 5): + updated_code_section += func + continue + if func[ix + 2] & (1 << 5): + raise ValueError("Memory immediate was already set.") + func_bytes_added = 0 + for i in findall(func, 0xfe): + func[i + 2] |= (1 << 5) + + # ordering comes after mem idx + has_mem_idx = bool(func[i + 2] & (1 << 6)) + func.insert(i + 3 + has_mem_idx, 0x00) + + func_bytes_added += 1 + + # adding to the func byte size might have added a byte + section_bytes_added += unsigned_leb_add(func, 0, func_bytes_added) + section_bytes_added += func_bytes_added + + updated_code_section += func + + _ = unsigned_leb_add(updated_code_section, 1, section_bytes_added) + updated_tests.append(updated_code_section) + + return b''.join(updated_tests) + + +def failing_test(instruction, arg, /, memidx, drop): + """Module assertion that sets a memory ordering immediate for a non-atomic instruction""" + + func = f"(func ${''.join(filter(str.isalnum, instruction))} {atomic_instruction(instruction, memidx, 'acqrel', arg, drop=drop)})" + return assert_invalid(module("(memory 1 1 shared)", "", func), f"Can't set memory ordering for non-atomic {instruction}") + + +def drop_atomic(instruction): + first, atomic, last = instruction.partition(".atomic") + return first + last + + +def failing_tests(): + op, arg, should_drop = all_ops[0] + op = drop_atomic(op) + + return failing_test(op, arg, memidx=None, drop=should_drop) + + +def main(): + parser = ArgumentParser() + parser.add_argument("--wasm-as", default=Path("bin/wasm-as"), type=Path) + + args = parser.parse_args() + + wat = generate_atomic_spec_test() + bin = binary_tests(to_binary(args.wasm_as, wat)) + + print(wat) + print(module_binary(bin)) + print() + print(failing_tests()) + print() + + +if __name__ == "__main__": + main() diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index 1527fbcae2a..7c6aa8bcb80 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -1,147 +1,62 @@ (module - (memory $0 23 256 shared) - - (func $acqrel (result i32) - (i32.atomic.store acqrel (i32.const 0) (i32.const 0)) - (i32.atomic.load acqrel - (i32.const 1) - ) - ) - (func $seqcst (result i32) - (i32.atomic.store 0 seqcst (i32.const 0) (i32.const 0)) - ;; seqcst may be omitted for atomic loads, it's the default - (drop (i32.atomic.load seqcst - (i32.const 1) - )) - ;; allows memory index before memory ordering immediate - (i32.atomic.load 0 seqcst - (i32.const 1) - ) - ) -) - -(assert_malformed - (module quote - "(memory $0 23 256 shared)" - "(func $acqrel (result i32)" - " (i32.load acqrel" - " (i32.const 1)" - " ) " - ") " - ) - "Memory ordering can only be provided for atomic loads." -) - -;; Parses acquire-release immediate -;; (module -;; (memory $0 23 256 shared) -;; (func $acqrel -;; (i32.atomic.store (i32.const 0) (i32.const 0)) -;; ) -;; ) -(module binary - "\00asm\01\00\00\00" - "\01\04\01\60\00\00\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0d\01" ;; code section - "\0b\00" ;; func $acqrel - "\41\00\41\00" ;; (i32.const 0) (i32.const 0) - "\fe\17" ;; i32.atomic.store - "\22" ;; 2 | (1<<5): Alignment of 2 (32-bit store), with bit 5 set indicating that then next byte is a memory ordering - "\01" ;; acqrel ordering - "\00" ;; offset - "\0b" ;; end -) - -;; Parses optional seq-cst immediate -(module binary - "\00asm\01\00\00\00" - "\01\04\01\60\00\00\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0d\01" ;; code section - "\0b\00" ;; func $seqcst - "\41\00\41\00" ;; (i32.const 0) (i32.const 0) - "\fe\17" ;; i32.atomic.store - "\22" ;; 2 | (1<<5): Alignment of 2 (32-bit store), with bit 5 set indicating that then next byte is a memory ordering - "\00" ;; seqcst ordering - "\00" ;; offset - "\0b" ;; end -) - -;; Parses optional seq-cst immediate with memory idx -(module binary - "\00asm\01\00\00\00" - "\01\04\01\60\00\00\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0e\01" ;; code section - "\0c\00" ;; func $seqcst - "\41\00\41\00" ;; (i32.const 0) (i32.const 0) - "\fe\17" ;; i32.atomic.store - "\62" ;; 2 | (1<<5): Alignment of 2 (32-bit store), with bit 5 set indicating that then next byte is a memory ordering - "\00" ;; memory index - "\00" ;; seqcst ordering - "\00" ;; offset - "\0b" ;; end -) - -;; Parses acquire-release immediate -;; Equivalent to -;; (module -;; (memory $0 23 256 shared) -;; (func $load (result i32) -;; (i32.atomic.load acqrel -;; (i32.const 1) -;; ) -;; ) -;; ) -(module binary - "\00asm\01\00\00\00" ;; header + version - "\01\05\01\60\00\01\7f\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0b\01" ;; code section - ;; func $load body - "\09\00" ;; size + decl count - "\41\01" ;; i32.const 1 - "\fe\10" ;; i32.atomic.load - "\22" ;; 2 | (1<<5): Alignment of 2 (32-bit load), with bit 5 set indicating that the next byte is a memory ordering - "\01" ;; acqrel ordering - "\00" ;; offset - "\0b" ;; end + (memory 1 1 shared) + (memory 1 1 shared) + + (func $no_immediate_without_memid + (drop (i32.atomic.load (i32.const 51))) + (i32.atomic.store (i32.const 51) (i32.const 51)) + ) + + (func $acqrel_without_memid + (drop (i32.atomic.load acqrel (i32.const 51))) + (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) + ) + + (func $seqcst_without_memid + (drop (i32.atomic.load seqcst (i32.const 51))) + (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) + ) + + (func $no_immediate_with_memid + (drop (i32.atomic.load 1 (i32.const 51))) + (i32.atomic.store 1 (i32.const 51) (i32.const 51)) + ) + + (func $acqrel_with_memid + (drop (i32.atomic.load 1 acqrel (i32.const 51))) + (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) + ) + + (func $seqcst_with_memid + (drop (i32.atomic.load 1 seqcst (i32.const 51))) + (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) + ) ) - -;; parses acquire-release immediate after memory index (module binary - "\00asm\01\00\00\00" ;; header + version - "\01\05\01\60\00\01\7f\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0c\01" ;; code section - ;; func $load body - "\0a\00" ;; size + decl count - "\41\01" ;; i32.const 1 - "\fe\10" ;; i32.atomic.load - "\62" ;; 2 | (1<<5) | (1<<6): Alignment of 2 (32-bit load), with bit 5 set indicating that the next byte is a memory ordering - "\00" ;; memory index - "\01" ;; acqrel ordering - "\00" ;; offset - "\0b" ;; end -) - -;; Parses optional seqcst memory ordering for atomic loads -;; This isn't covered by round-trip tests because we omit it by default. -;; Equivalent to -;; (module -;; (memory $0 23 256 shared) -;; (func $load (result i32) -;; (i32.atomic.load seqcst -;; (i32.const 1) -;; ) -;; ) -;; ) -(module binary - "\00asm\01\00\00\00" ;; header + version - "\01\05\01\60\00\01\7f\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0b\01" ;; code section - ;; func $load body - "\09\00" ;; size + decl count - "\41\01" ;; i32.const 1 - "\fe\10" ;; i32.atomic.load - "\22" ;; 2 | (1<<5): Alignment of 2 (32-bit load), with bit 5 set indicating that the next byte is a memory ordering - "\00" ;; seqcst ordering - "\00" ;; offset - "\0b" ;; end -) + "\00\61\73\6d\01\00\00\00\01\04\01\60\00\00\03\07\06\00\00\00\00\00\00\05\07\02\03\01\01\03\01\01" ;; other sections + "\0a\7b\06" ;; code section + "\11\00" ;; func $no_immediate_without_memid + "\41\33\fe\10\02\00\1a" ;; drop (i32.atomic.load (i32.const 51)) + "\41\33\41\33\fe\17\02\00\0b" ;; (i32.atomic.store (i32.const 51) (i32.const 51)) + "\13\00" ;; func $acqrel_without_memid + "\41\33\fe\10\22\01\00\1a" ;; (drop (i32.atomic.load acqrel (i32.const 51))) + "\41\33\41\33\fe\17\22\01\00\0b" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) + "\13\00" ;; func $seqcst_without_memid + "\41\33\fe\10\22\00\00\1a" ;; (drop (i32.atomic.load seqcst (i32.const 51))) + "\41\33\41\33\fe\17\22\00\00\0b" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) + "\13\00" ;; func $no_immediate_with_memid + "\41\33\fe\10\42\01\00\1a" ;; (drop (i32.atomic.load 1 (i32.const 51))) + "\41\33\41\33\fe\17\42\01\00\0b" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) + "\15\00" ;; func $acqrel_with_memid + "\41\33\fe\10\62\01\01\00\1a" ;; (drop (i32.atomic.load 1 acqrel (i32.const 51))) + "\41\33\41\33\fe\17\62\01\01\00\0b" ;; (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) + "\15\00" ;; func $seqcst_with_memid + "\41\33\fe\10\62\01\00\00\1a" ;; (drop (i32.atomic.load 1 seqcst (i32.const 51))) + "\41\33\41\33\fe\17\62\01\00\00\0b" ;; (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) + ) + +(assert_invalid (module + (memory 1 1 shared) + + (func $i32load (drop (i32.load acqrel (i32.const 51)))) +) "Can't set memory ordering for non-atomic i32.load") From 4f65e35dcb7ee6f468417ef5f4efa343898dc61a Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 21 Jan 2026 00:51:36 +0000 Subject: [PATCH 02/10] Generate tests with binary templates, add more in-depth test --- scripts/test/generate-atomic-spec-test.py | 208 ++++++++-------------- test/spec/relaxed-atomics.wast | 50 ++++-- 2 files changed, 108 insertions(+), 150 deletions(-) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index 0f4da9cf71c..f349f7ac94b 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -1,9 +1,5 @@ -import subprocess -import sys -import tempfile -from argparse import ArgumentParser -from collections.abc import Iterator -from pathlib import Path +import itertools +from dataclasses import dataclass # Workaround for python <3.10, escape characters can't appear in f-strings. # Although we require 3.10 in some places, the formatter complains without this. @@ -53,7 +49,7 @@ def assert_invalid(module, reason): return f'''(assert_invalid {module} "{reason}")''' -def generate_atomic_spec_test(): +def text_test(): # Declare two memories so we have control over whether the memory immediate is printed # A memory immediate of 0 is allowed to be omitted. return module( @@ -63,37 +59,6 @@ def generate_atomic_spec_test(): "\n\n".join([f'{func(memid, ordering)}' for memid in [None, "1"] for ordering in [None, "acqrel", "seqcst"]])) -def to_binary(wasm_as, wat: str) -> bytes: - with tempfile.NamedTemporaryFile(mode="w+") as input, tempfile.NamedTemporaryFile(mode="rb") as output: - input.write(wat) - input.seek(0) - - proc = subprocess.run([wasm_as, "--enable-multimemory", "--enable-threads", "--enable-relaxed-atomics", input.name, "-o", output.name], capture_output=True) - try: - proc.check_returncode() - except Exception: - print(proc.stderr.decode('utf-8'), end="", file=sys.stderr) - raise - - return output.read() - - -def findall(bytes, byte): - ix = -1 - while ((ix := bytes.find(byte, ix + 1)) != -1): - yield ix - - -def read_unsigned_leb(bytes, start): - """Returns (bytes read, value)""" - ret = 0 - for i, byte in enumerate(bytes[start:]): - ret |= (byte & ~(1 << 7)) << (7 * i) - if not byte & (1 << 7): - return i + 1, ret - raise ValueError("Unexpected end of input, continuation bit was set for the last byte.") - - def to_unsigned_leb(num): ret = bytearray() @@ -110,112 +75,94 @@ def to_unsigned_leb(num): return ret -def unsigned_leb_add(bytes: bytearray, start, add) -> int: - """Returns number of bytes added""" - l, decoded = read_unsigned_leb(bytes, start) - added = to_unsigned_leb(decoded + add) - - bytes[start:start + l] = added[:l] - - if len(added) > l: - for i, b in enumerate(added[l:], start=l): - bytes.insert(i, b) +def bin_to_str(bin): + return ''.join(f'{backslash}{byte:02x}' for byte in bin) - return len(added) - l +@dataclass +class statement: + bin: bytes + text: str -def unsigned_leb_subtract(bytes, start, sub): - l, decoded = read_unsigned_leb(bytes, start) - subbed = to_unsigned_leb(decoded - sub) - bytes[start:start + len(subbed)] = subbed +@dataclass +class function: + body: [statement] + memidx: bytes + ordering: bytes - diff = l - len(subbed) - for _ in range(diff): - bytes.pop(start + len(subbed)) - return -diff +def normalize_spaces(s): + return " ".join(s.split()) -def iterate_sections(bytes) -> Iterator[bytearray]: - bytes = bytes.removeprefix(b"\00asm\01\00\00\00") - start = 0 - while True: - read, size = read_unsigned_leb(bytes, start + 1) +def binary_line(bin): + return f'"{bin_to_str(bin)}"\n' - # section op + section size + body - yield bytearray(bytes[start:start + 1 + read + size]) - start += 1 + read + size - if start > len(bytes): - raise ValueError("not expected", start, len(bytes)) - elif start == len(bytes): - return +def binary_test_example(): + return r'''(module binary + "\00asm\01\00\00\00" ;; header + version + "\01\05\01\60\00\01\7f\03\02\01\00\05\05\01\03\17\80\02" ;; other sections + "\0a\0c\01" ;; code section + "\0a\00" ;; func size + decl count + "\41\33" ;; i32.const 51 + "\fe\10" ;; i32.atomic.load + "\62" ;; 2 | (1<<5) | (1<<6): Alignment of 2 (32-bit load), with bit 5 set indicating that the next byte is a memory ordering + "\00" ;; memory index + "\01" ;; acqrel ordering + "\00" ;; offset + "\0b" ;; end +)''' -def iterate_functions(bytes) -> Iterator[bytearray]: - read, size = read_unsigned_leb(bytes, 1) - read2, size2 = read_unsigned_leb(bytes, 1 + read) - section_body = bytes[1 + read + read2:] - - start = 0 - while True: - read, size = read_unsigned_leb(section_body, start) - yield bytearray(section_body[start:start + read + size]) - start += read + size - if start > len(section_body): - raise ValueError("not expected", start, len(section_body)) - elif start == len(section_body): - return - - -def binary_tests(b: bytes) -> bytes: - updated_tests = [b"\00asm\01\00\00\00"] - - for section in iterate_sections(b): - if section[0] != 0x0a: - updated_tests.append(section) - continue - bytes_read, size = read_unsigned_leb(section, 1) - _, func_count = read_unsigned_leb(section, 1 + bytes_read) +def binary_tests(): - updated_code_section = bytearray() - updated_code_section.append(0x0a) - updated_code_section += to_unsigned_leb(size) + func_statements = [ + [b"\x41\x33\xfe\x10%(align)s%(memidx)s%(ordering)s\x00\x1a", "(drop (i32.atomic.load %(memidx)s %(ordering)s (i32.const 51)))"], + # TODO 0b ends the function + [b"\x41\x33\x41\x33\xfe\x17%(align)s%(memidx)s%(ordering)s\x00", "(i32.atomic.store %(memidx)s %(ordering)s (i32.const 51) (i32.const 51))"], + ] - updated_code_section += to_unsigned_leb(func_count) + # Each function ends with 0x0b. Add it to the last statement for simplicity. + func_statements[-1][0] += b'\x0b' - section_bytes_added = 0 - for i, func in enumerate(iterate_functions(section)): - # TODO: this is wrong if the function size is 0xfe - ix = func.find(0xfe) - if ix == -1: - raise ValueError("Didn't find atomic operation") - if i not in (2, 5): - updated_code_section += func - continue - if func[ix + 2] & (1 << 5): - raise ValueError("Memory immediate was already set.") - func_bytes_added = 0 - for i in findall(func, 0xfe): - func[i + 2] |= (1 << 5) + funcs: [function] = [] + for memidx, ordering in itertools.product([b'', b'\x01'], [b'', b'\x00', b'\x01']): + func = function([], memidx, ordering) + for bin_statement, str_statement in func_statements: + align = 2 | (bool(memidx) << 5) | (bool(ordering) << 6) + s = statement( + bin=bin_statement % {b'align': int.to_bytes(align), b'ordering': ordering, b'memidx': memidx}, + text=normalize_spaces(str_statement % {'ordering': ["seqcst", "acqrel"][ordering[0]] if ordering else '', 'memidx': "1" if memidx else ""})) - # ordering comes after mem idx - has_mem_idx = bool(func[i + 2] & (1 << 6)) - func.insert(i + 3 + has_mem_idx, 0x00) + func.body.append(s) + funcs.append(func) - func_bytes_added += 1 + # +1 for each function since we didn't count the local count byte yet, and +1 overall for the function count + section_size = sum(len(statement.bin) + 1 for func in funcs for statement in func.body) + 1 + code_section = bytearray(b"\x0a") + to_unsigned_leb(section_size) + to_unsigned_leb(len(funcs)) - # adding to the func byte size might have added a byte - section_bytes_added += unsigned_leb_add(func, 0, func_bytes_added) - section_bytes_added += func_bytes_added + '''(module + (memory 1 1 shared) + (memory 1 1 shared) + ) + ''' + module = b"\x00\x61\x73\x6d\x01\x00\x00\x00\x01\x04\01\x60\x00\x00\x03\x07\06\x00\x00\x00\x00\x00\x00\x05\07\x02\x03\x01\x01\x03\x01\x01" - updated_code_section += func + str_builder = [binary_line(module), f'"{bin_to_str(code_section)}" ;; code section\n'] - _ = unsigned_leb_add(updated_code_section, 1, section_bytes_added) - updated_tests.append(updated_code_section) + for func in funcs: + bin_size = sum(len(statement.bin) for statement in func.body) + # body size plus 1 byte for the number of locals (0) + func_bytes = to_unsigned_leb(bin_size + 1) + # number of locals, none in our case + func_bytes.append(0x00) + str_builder.append(f'"{bin_to_str(func_bytes)}" ;; func\n') + for stmt in func.body: + str_builder.append(f'"{bin_to_str(stmt.bin)}" ;; {stmt.text}\n') - return b''.join(updated_tests) + return f"(module binary\n{indent(''.join(str_builder))})" def failing_test(instruction, arg, /, memidx, drop): @@ -238,16 +185,11 @@ def failing_tests(): def main(): - parser = ArgumentParser() - parser.add_argument("--wasm-as", default=Path("bin/wasm-as"), type=Path) - - args = parser.parse_args() - - wat = generate_atomic_spec_test() - bin = binary_tests(to_binary(args.wasm_as, wat)) - - print(wat) - print(module_binary(bin)) + print(text_test()) + print() + print(binary_test_example()) + print() + print(binary_tests()) print() print(failing_tests()) print() diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index 7c6aa8bcb80..eb44ae14642 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -32,31 +32,47 @@ (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) ) ) + +(module binary + "\00asm\01\00\00\00" ;; header + version + "\01\05\01\60\00\01\7f\03\02\01\00\05\05\01\03\17\80\02" ;; other sections + "\0a\0c\01" ;; code section + "\0a\00" ;; func size + decl count + "\41\33" ;; i32.const 51 + "\fe\10" ;; i32.atomic.load + "\62" ;; 2 | (1<<5) | (1<<6): Alignment of 2 (32-bit load), with bit 5 set indicating that the next byte is a memory ordering + "\00" ;; memory index + "\01" ;; acqrel ordering + "\00" ;; offset + "\0b" ;; end +) + (module binary - "\00\61\73\6d\01\00\00\00\01\04\01\60\00\00\03\07\06\00\00\00\00\00\00\05\07\02\03\01\01\03\01\01" ;; other sections + "\00\61\73\6d\01\00\00\00\01\04\01\60\00\00\03\07\06\00\00\00\00\00\00\05\07\02\03\01\01\03\01\01" "\0a\7b\06" ;; code section - "\11\00" ;; func $no_immediate_without_memid - "\41\33\fe\10\02\00\1a" ;; drop (i32.atomic.load (i32.const 51)) + "\11\00" ;; func + "\41\33\fe\10\02\00\1a" ;; (drop (i32.atomic.load (i32.const 51))) "\41\33\41\33\fe\17\02\00\0b" ;; (i32.atomic.store (i32.const 51) (i32.const 51)) - "\13\00" ;; func $acqrel_without_memid - "\41\33\fe\10\22\01\00\1a" ;; (drop (i32.atomic.load acqrel (i32.const 51))) - "\41\33\41\33\fe\17\22\01\00\0b" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) - "\13\00" ;; func $seqcst_without_memid - "\41\33\fe\10\22\00\00\1a" ;; (drop (i32.atomic.load seqcst (i32.const 51))) - "\41\33\41\33\fe\17\22\00\00\0b" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) - "\13\00" ;; func $no_immediate_with_memid - "\41\33\fe\10\42\01\00\1a" ;; (drop (i32.atomic.load 1 (i32.const 51))) - "\41\33\41\33\fe\17\42\01\00\0b" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) - "\15\00" ;; func $acqrel_with_memid - "\41\33\fe\10\62\01\01\00\1a" ;; (drop (i32.atomic.load 1 acqrel (i32.const 51))) - "\41\33\41\33\fe\17\62\01\01\00\0b" ;; (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) - "\15\00" ;; func $seqcst_with_memid + "\13\00" ;; func + "\41\33\fe\10\42\00\00\1a" ;; (drop (i32.atomic.load seqcst (i32.const 51))) + "\41\33\41\33\fe\17\42\00\00\0b" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) + "\13\00" ;; func + "\41\33\fe\10\42\01\00\1a" ;; (drop (i32.atomic.load acqrel (i32.const 51))) + "\41\33\41\33\fe\17\42\01\00\0b" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) + "\13\00" ;; func + "\41\33\fe\10\22\01\00\1a" ;; (drop (i32.atomic.load 1 (i32.const 51))) + "\41\33\41\33\fe\17\22\01\00\0b" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) + "\15\00" ;; func "\41\33\fe\10\62\01\00\00\1a" ;; (drop (i32.atomic.load 1 seqcst (i32.const 51))) "\41\33\41\33\fe\17\62\01\00\00\0b" ;; (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) - ) + "\15\00" ;; func + "\41\33\fe\10\62\01\01\00\1a" ;; (drop (i32.atomic.load 1 acqrel (i32.const 51))) + "\41\33\41\33\fe\17\62\01\01\00\0b" ;; (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) +) (assert_invalid (module (memory 1 1 shared) (func $i32load (drop (i32.load acqrel (i32.const 51)))) ) "Can't set memory ordering for non-atomic i32.load") + From 83b23580a1aa230389c76499e53f8a28714eecc1 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 21 Jan 2026 22:07:41 +0000 Subject: [PATCH 03/10] Cleanup --- scripts/test/generate-atomic-spec-test.py | 68 +++++++++++------------ test/spec/relaxed-atomics.wast | 4 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index f349f7ac94b..05643bc6fc1 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -1,5 +1,5 @@ import itertools -from dataclasses import dataclass +from dataclasses import astuple, dataclass # Workaround for python <3.10, escape characters can't appear in f-strings. # Although we require 3.10 in some places, the formatter complains without this. @@ -8,6 +8,20 @@ backslash = '\\' +@dataclass +class instruction_test: + op: str + arg: str + should_drop: bool + bin: bytes + + +ALL_OPS = [ + instruction_test("i32.atomic.load", "(i32.const 51)", True, b"\x41\x33\xfe\x10%(align)s%(memidx)s%(ordering)s\x00\x1a"), + instruction_test("i32.atomic.store", "(i32.const 51) (i32.const 51)", False, b"\x41\x33\x41\x33\xfe\x17%(align)s%(memidx)s%(ordering)s\x00"), +] + + def indent(s): return "\n".join(f" {line}" if line else "" for line in s.split("\n")) @@ -17,21 +31,15 @@ def instruction(*args): return f"({' '.join(arg for arg in args if arg is not None)})" -def atomic_instruction(op, memid, immediate, /, *args, drop): +def atomic_instruction(op, memid, ordering, /, *args, drop): if drop: - return f"(drop {instruction(op, memid, immediate, *args)})" - return instruction(op, memid, immediate, *args) + return f"(drop {instruction(op, memid, ordering, *args)})" + return instruction(op, memid, ordering, *args) -all_ops = [ - ("i32.atomic.load", "(i32.const 51)", True), - ("i32.atomic.store", "(i32.const 51) (i32.const 51)", False), -] - - -def func(memid, immediate, ops=all_ops): - return f'''(func ${immediate if immediate is not None else "no_immediate"}{"_with_memid" if memid is not None else "_without_memid"} -{indent(newline.join(atomic_instruction(op, memid, immediate, arg, drop=should_drop) for op, arg, should_drop in ops))} +def func(memid, ordering): + return f'''(func ${ordering if ordering is not None else "no_ordering"}{"_with_memid" if memid is not None else "_without_memid"} +{indent(newline.join(atomic_instruction(op, memid, ordering, arg, drop=should_drop) for op, arg, should_drop, _ in map(astuple, ALL_OPS)))} )''' @@ -50,8 +58,8 @@ def assert_invalid(module, reason): def text_test(): - # Declare two memories so we have control over whether the memory immediate is printed - # A memory immediate of 0 is allowed to be omitted. + # Declare two memories so we have control over whether the memory idx is printed + # A memory idx of 0 is allowed to be omitted. return module( "(memory 1 1 shared)", "(memory 1 1 shared)", @@ -117,26 +125,18 @@ def binary_test_example(): def binary_tests(): - - func_statements = [ - [b"\x41\x33\xfe\x10%(align)s%(memidx)s%(ordering)s\x00\x1a", "(drop (i32.atomic.load %(memidx)s %(ordering)s (i32.const 51)))"], - # TODO 0b ends the function - [b"\x41\x33\x41\x33\xfe\x17%(align)s%(memidx)s%(ordering)s\x00", "(i32.atomic.store %(memidx)s %(ordering)s (i32.const 51) (i32.const 51))"], - ] - - # Each function ends with 0x0b. Add it to the last statement for simplicity. - func_statements[-1][0] += b'\x0b' - funcs: [function] = [] - for memidx, ordering in itertools.product([b'', b'\x01'], [b'', b'\x00', b'\x01']): + for (memidx_bytes, memidx), (ordering_bytes, ordering) in itertools.product([(b'', None), (b'\x01', "1")], [(b'', None), (b'\x00', "seqcst"), (b'\x01', "acqrel")]): func = function([], memidx, ordering) - for bin_statement, str_statement in func_statements: - align = 2 | (bool(memidx) << 5) | (bool(ordering) << 6) + for test_case in ALL_OPS: + align = 2 | (bool(memidx_bytes) << 5) | (bool(ordering_bytes) << 6) s = statement( - bin=bin_statement % {b'align': int.to_bytes(align), b'ordering': ordering, b'memidx': memidx}, - text=normalize_spaces(str_statement % {'ordering': ["seqcst", "acqrel"][ordering[0]] if ordering else '', 'memidx': "1" if memidx else ""})) - + bin=test_case.bin % {b'align': int.to_bytes(align), b'ordering': ordering_bytes, b'memidx': memidx_bytes}, + text=atomic_instruction(test_case.op, memidx, ordering, test_case.arg, drop=test_case.should_drop)) func.body.append(s) + + # Functions end with 0x0b. + func.body[-1].bin += b'\x0b' funcs.append(func) # +1 for each function since we didn't count the local count byte yet, and +1 overall for the function count @@ -178,10 +178,10 @@ def drop_atomic(instruction): def failing_tests(): - op, arg, should_drop = all_ops[0] - op = drop_atomic(op) + inst = ALL_OPS[0] + op = drop_atomic(inst.op) - return failing_test(op, arg, memidx=None, drop=should_drop) + return failing_test(op, inst.arg, memidx=None, drop=inst.should_drop) def main(): diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index eb44ae14642..0f7c32e3d98 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -2,7 +2,7 @@ (memory 1 1 shared) (memory 1 1 shared) - (func $no_immediate_without_memid + (func $no_ordering_without_memid (drop (i32.atomic.load (i32.const 51))) (i32.atomic.store (i32.const 51) (i32.const 51)) ) @@ -17,7 +17,7 @@ (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) ) - (func $no_immediate_with_memid + (func $no_ordering_with_memid (drop (i32.atomic.load 1 (i32.const 51))) (i32.atomic.store 1 (i32.const 51) (i32.const 51)) ) From cabf23699f36795eb714a46c33ae9cac5ce4bb63 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 21 Jan 2026 22:47:52 +0000 Subject: [PATCH 04/10] Fix section length calculation --- scripts/test/generate-atomic-spec-test.py | 34 +++++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index 05643bc6fc1..3eb0c64523f 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -139,18 +139,7 @@ def binary_tests(): func.body[-1].bin += b'\x0b' funcs.append(func) - # +1 for each function since we didn't count the local count byte yet, and +1 overall for the function count - section_size = sum(len(statement.bin) + 1 for func in funcs for statement in func.body) + 1 - code_section = bytearray(b"\x0a") + to_unsigned_leb(section_size) + to_unsigned_leb(len(funcs)) - - '''(module - (memory 1 1 shared) - (memory 1 1 shared) - ) - ''' - module = b"\x00\x61\x73\x6d\x01\x00\x00\x00\x01\x04\01\x60\x00\x00\x03\x07\06\x00\x00\x00\x00\x00\x00\x05\07\x02\x03\x01\x01\x03\x01\x01" - - str_builder = [binary_line(module), f'"{bin_to_str(code_section)}" ;; code section\n'] + str_builder = [] for func in funcs: bin_size = sum(len(statement.bin) for statement in func.body) @@ -162,6 +151,27 @@ def binary_tests(): for stmt in func.body: str_builder.append(f'"{bin_to_str(stmt.bin)}" ;; {stmt.text}\n') + section_size = ( + # function body size + sum(len(statement.bin) for func in funcs for statement in func.body) + + # function count byte + 1 + + # num locals per function (always 0) + len(funcs) + + # each function declares its size, add bytes for the LEB encoding of each function's size + sum(len(to_unsigned_leb(sum(len(statement.bin) for statement in func.body))) for func in funcs)) + + code_section = bytearray(b"\x0a") + to_unsigned_leb(section_size) + to_unsigned_leb(len(funcs)) + + '''(module + (memory 1 1 shared) + (memory 1 1 shared) + ) + ''' + module = b"\x00\x61\x73\x6d\x01\x00\x00\x00\x01\x04\01\x60\x00\x00\x03\x07\06\x00\x00\x00\x00\x00\x00\x05\07\x02\x03\x01\x01\x03\x01\x01" + + str_builder = [binary_line(module), f'"{bin_to_str(code_section)}" ;; code section\n'] + str_builder + return f"(module binary\n{indent(''.join(str_builder))})" From 39fa1461c82b8c499d5e45750d07cb7e26ade4e5 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 21 Jan 2026 23:02:32 +0000 Subject: [PATCH 05/10] Add some comments --- scripts/test/generate-atomic-spec-test.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index 3eb0c64523f..a75167d5d03 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -26,7 +26,6 @@ def indent(s): return "\n".join(f" {line}" if line else "" for line in s.split("\n")) -# skips None for convenience def instruction(*args): return f"({' '.join(arg for arg in args if arg is not None)})" @@ -38,6 +37,12 @@ def atomic_instruction(op, memid, ordering, /, *args, drop): def func(memid, ordering): + """Return a function testing ALL_OPS e.g. + (func $acqrel_without_memid + (drop (i32.atomic.load acqrel (i32.const 51))) + ... + ) + """ return f'''(func ${ordering if ordering is not None else "no_ordering"}{"_with_memid" if memid is not None else "_without_memid"} {indent(newline.join(atomic_instruction(op, memid, ordering, arg, drop=should_drop) for op, arg, should_drop, _ in map(astuple, ALL_OPS)))} )''' @@ -71,10 +76,8 @@ def to_unsigned_leb(num): ret = bytearray() if num == 0: - ret = bytearray() ret.append(0) return ret - ret = bytearray() while num > 0: rem = num >> 7 ret.append((num & 0x7F) | (bool(rem) << 7)) @@ -83,7 +86,8 @@ def to_unsigned_leb(num): return ret -def bin_to_str(bin): +def bin_to_str(bin: bytes) -> str: + """Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00""" return ''.join(f'{backslash}{byte:02x}' for byte in bin) @@ -125,6 +129,7 @@ def binary_test_example(): def binary_tests(): + """Return a (module binary ...) testing ALL_OPS""" funcs: [function] = [] for (memidx_bytes, memidx), (ordering_bytes, ordering) in itertools.product([(b'', None), (b'\x01', "1")], [(b'', None), (b'\x00', "seqcst"), (b'\x01', "acqrel")]): func = function([], memidx, ordering) From 65d951c11ba861de8261d26ad29e43101b6f1b10 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 21 Jan 2026 23:46:27 +0000 Subject: [PATCH 06/10] Fix mask for ordering and memidx --- scripts/test/generate-atomic-spec-test.py | 2 +- test/spec/relaxed-atomics.wast | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index a75167d5d03..e7781d6436b 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -134,7 +134,7 @@ def binary_tests(): for (memidx_bytes, memidx), (ordering_bytes, ordering) in itertools.product([(b'', None), (b'\x01', "1")], [(b'', None), (b'\x00', "seqcst"), (b'\x01', "acqrel")]): func = function([], memidx, ordering) for test_case in ALL_OPS: - align = 2 | (bool(memidx_bytes) << 5) | (bool(ordering_bytes) << 6) + align = 2 | (bool(ordering_bytes) << 5) | (bool(memidx_bytes) << 6) s = statement( bin=test_case.bin % {b'align': int.to_bytes(align), b'ordering': ordering_bytes, b'memidx': memidx_bytes}, text=atomic_instruction(test_case.op, memidx, ordering, test_case.arg, drop=test_case.should_drop)) diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index 0f7c32e3d98..6f20e0f4394 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -54,14 +54,14 @@ "\41\33\fe\10\02\00\1a" ;; (drop (i32.atomic.load (i32.const 51))) "\41\33\41\33\fe\17\02\00\0b" ;; (i32.atomic.store (i32.const 51) (i32.const 51)) "\13\00" ;; func - "\41\33\fe\10\42\00\00\1a" ;; (drop (i32.atomic.load seqcst (i32.const 51))) - "\41\33\41\33\fe\17\42\00\00\0b" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) + "\41\33\fe\10\22\00\00\1a" ;; (drop (i32.atomic.load seqcst (i32.const 51))) + "\41\33\41\33\fe\17\22\00\00\0b" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) "\13\00" ;; func - "\41\33\fe\10\42\01\00\1a" ;; (drop (i32.atomic.load acqrel (i32.const 51))) - "\41\33\41\33\fe\17\42\01\00\0b" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) + "\41\33\fe\10\22\01\00\1a" ;; (drop (i32.atomic.load acqrel (i32.const 51))) + "\41\33\41\33\fe\17\22\01\00\0b" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) "\13\00" ;; func - "\41\33\fe\10\22\01\00\1a" ;; (drop (i32.atomic.load 1 (i32.const 51))) - "\41\33\41\33\fe\17\22\01\00\0b" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) + "\41\33\fe\10\42\01\00\1a" ;; (drop (i32.atomic.load 1 (i32.const 51))) + "\41\33\41\33\fe\17\42\01\00\0b" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) "\15\00" ;; func "\41\33\fe\10\62\01\00\00\1a" ;; (drop (i32.atomic.load 1 seqcst (i32.const 51))) "\41\33\41\33\fe\17\62\01\00\00\0b" ;; (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) From e7182223d3df7c467e62db6adfb081a02adcce39 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 23 Jan 2026 00:15:12 +0000 Subject: [PATCH 07/10] Add text tests including correct handling of i32 and i64 memories --- scripts/test/generate-atomic-spec-test.py | 230 ++++++---------------- test/spec/relaxed-atomics.wast | 38 ++-- test/spec/relaxed-atomics2.wast | 49 +++++ 3 files changed, 129 insertions(+), 188 deletions(-) create mode 100644 test/spec/relaxed-atomics2.wast diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index e7781d6436b..f8c959d3293 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -1,213 +1,93 @@ -import itertools -from dataclasses import astuple, dataclass +import enum +from dataclasses import dataclass +from enum import Enum # Workaround for python <3.10, escape characters can't appear in f-strings. # Although we require 3.10 in some places, the formatter complains without this. newline = "\n" -backslash = '\\' - - -@dataclass -class instruction_test: - op: str - arg: str - should_drop: bool - bin: bytes - - -ALL_OPS = [ - instruction_test("i32.atomic.load", "(i32.const 51)", True, b"\x41\x33\xfe\x10%(align)s%(memidx)s%(ordering)s\x00\x1a"), - instruction_test("i32.atomic.store", "(i32.const 51) (i32.const 51)", False, b"\x41\x33\x41\x33\xfe\x17%(align)s%(memidx)s%(ordering)s\x00"), -] - def indent(s): return "\n".join(f" {line}" if line else "" for line in s.split("\n")) -def instruction(*args): - return f"({' '.join(arg for arg in args if arg is not None)})" - - -def atomic_instruction(op, memid, ordering, /, *args, drop): - if drop: - return f"(drop {instruction(op, memid, ordering, *args)})" - return instruction(op, memid, ordering, *args) - - -def func(memid, ordering): - """Return a function testing ALL_OPS e.g. - (func $acqrel_without_memid - (drop (i32.atomic.load acqrel (i32.const 51))) - ... - ) - """ - return f'''(func ${ordering if ordering is not None else "no_ordering"}{"_with_memid" if memid is not None else "_without_memid"} -{indent(newline.join(atomic_instruction(op, memid, ordering, arg, drop=should_drop) for op, arg, should_drop, _ in map(astuple, ALL_OPS)))} -)''' - - -def module(*statements): - return f'''(module -{newline.join(map(indent, statements))} -)''' - - -def module_binary(bin): - return f'''(module binary "{''.join(f'{backslash}{byte:02x}' for byte in bin)}")''' - - -def assert_invalid(module, reason): - return f'''(assert_invalid {module} "{reason}")''' - - -def text_test(): - # Declare two memories so we have control over whether the memory idx is printed - # A memory idx of 0 is allowed to be omitted. - return module( - "(memory 1 1 shared)", - "(memory 1 1 shared)", - "", - "\n\n".join([f'{func(memid, ordering)}' for memid in [None, "1"] for ordering in [None, "acqrel", "seqcst"]])) - +class ValueType(Enum): + i32 = enum.auto() + i64 = enum.auto() -def to_unsigned_leb(num): - ret = bytearray() - if num == 0: - ret.append(0) - return ret - while num > 0: - rem = num >> 7 - ret.append((num & 0x7F) | (bool(rem) << 7)) - - num = rem - return ret - - -def bin_to_str(bin: bytes) -> str: - """Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00""" - return ''.join(f'{backslash}{byte:02x}' for byte in bin) +class Ordering(Enum): + seqcst = 0 + acqrel = 1 @dataclass -class statement: - bin: bytes - text: str +class Template: + str: str + value_type: object + args: int -@dataclass -class function: - body: [statement] - memidx: bytes - ordering: bytes +templates = [ + Template(str="(drop (i32.atomic.load %(memarg)s%(args)s))", value_type=ValueType.i32, args=1), + Template(str="(drop (i64.atomic.load %(memarg)s%(args)s))", value_type=ValueType.i64, args=1), + Template(str="(i32.atomic.store %(memarg)s%(args)s)", value_type=ValueType.i32, args=2), + Template(str="(i64.atomic.store %(memarg)s%(args)s)", value_type=ValueType.i64, args=2), +] -def normalize_spaces(s): - return " ".join(s.split()) +def statement(template, mem_idx: str | None, ordering: Ordering | None): + """Return a statement exercising the op in `template` e.g. (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42))""" + memargs = [] + if mem_idx is not None: + memargs.append(mem_idx) + if ordering is not None: + memargs.append(ordering.name) + memarg_str = " ".join(memargs) + " " if memargs else "" + idx_type = ValueType.i64 if mem_idx == "1" else ValueType.i32 if mem_idx == "0" else ValueType.i32 -def binary_line(bin): - return f'"{bin_to_str(bin)}"\n' + # The first argument (the memory location) must match the memory that we're indexing. Other arguments match the op (e.g. i32 for i32.atomic.load). + args = [f"({idx_type.name}.const 42)"] + [f"({template.value_type.name}.const 42)" for _ in range(template.args - 1)] + return template.str % {"memarg": memarg_str, "args": " ".join(args)} -def binary_test_example(): - return r'''(module binary - "\00asm\01\00\00\00" ;; header + version - "\01\05\01\60\00\01\7f\03\02\01\00\05\05\01\03\17\80\02" ;; other sections - "\0a\0c\01" ;; code section - "\0a\00" ;; func size + decl count - "\41\33" ;; i32.const 51 - "\fe\10" ;; i32.atomic.load - "\62" ;; 2 | (1<<5) | (1<<6): Alignment of 2 (32-bit load), with bit 5 set indicating that the next byte is a memory ordering - "\00" ;; memory index - "\01" ;; acqrel ordering - "\00" ;; offset - "\0b" ;; end +def func(): + """Return a func exercising all ops in `templates` e.g. + (func $test-all-ops + (drop (i32.atomic.load (i32.const 42))) + (drop (i32.atomic.load acqrel (i32.const 42))) + ... + ) + """ + statements = [statement(template, mem_idx, ordering) for template in templates for mem_idx in [None, "0", "1"] for ordering in [None, Ordering.acqrel, Ordering.seqcst]] + return f'''(func $test-all-ops +{indent(newline.join(statements))} )''' -def binary_tests(): - """Return a (module binary ...) testing ALL_OPS""" - funcs: [function] = [] - for (memidx_bytes, memidx), (ordering_bytes, ordering) in itertools.product([(b'', None), (b'\x01', "1")], [(b'', None), (b'\x00', "seqcst"), (b'\x01', "acqrel")]): - func = function([], memidx, ordering) - for test_case in ALL_OPS: - align = 2 | (bool(ordering_bytes) << 5) | (bool(memidx_bytes) << 6) - s = statement( - bin=test_case.bin % {b'align': int.to_bytes(align), b'ordering': ordering_bytes, b'memidx': memidx_bytes}, - text=atomic_instruction(test_case.op, memidx, ordering, test_case.arg, drop=test_case.should_drop)) - func.body.append(s) - - # Functions end with 0x0b. - func.body[-1].bin += b'\x0b' - funcs.append(func) - - str_builder = [] - - for func in funcs: - bin_size = sum(len(statement.bin) for statement in func.body) - # body size plus 1 byte for the number of locals (0) - func_bytes = to_unsigned_leb(bin_size + 1) - # number of locals, none in our case - func_bytes.append(0x00) - str_builder.append(f'"{bin_to_str(func_bytes)}" ;; func\n') - for stmt in func.body: - str_builder.append(f'"{bin_to_str(stmt.bin)}" ;; {stmt.text}\n') - - section_size = ( - # function body size - sum(len(statement.bin) for func in funcs for statement in func.body) + - # function count byte - 1 + - # num locals per function (always 0) - len(funcs) + - # each function declares its size, add bytes for the LEB encoding of each function's size - sum(len(to_unsigned_leb(sum(len(statement.bin) for statement in func.body))) for func in funcs)) - - code_section = bytearray(b"\x0a") + to_unsigned_leb(section_size) + to_unsigned_leb(len(funcs)) - - '''(module - (memory 1 1 shared) - (memory 1 1 shared) - ) - ''' - module = b"\x00\x61\x73\x6d\x01\x00\x00\x00\x01\x04\01\x60\x00\x00\x03\x07\06\x00\x00\x00\x00\x00\x00\x05\07\x02\x03\x01\x01\x03\x01\x01" - - str_builder = [binary_line(module), f'"{bin_to_str(code_section)}" ;; code section\n'] + str_builder - - return f"(module binary\n{indent(''.join(str_builder))})" - - -def failing_test(instruction, arg, /, memidx, drop): - """Module assertion that sets a memory ordering immediate for a non-atomic instruction""" - - func = f"(func ${''.join(filter(str.isalnum, instruction))} {atomic_instruction(instruction, memidx, 'acqrel', arg, drop=drop)})" - return assert_invalid(module("(memory 1 1 shared)", "", func), f"Can't set memory ordering for non-atomic {instruction}") - +def text_test(): + """Return a (module ...) that exercises all ops in `templates`.""" + return f'''(module + (memory i32 1 1) + (memory i64 1 1) -def drop_atomic(instruction): - first, atomic, last = instruction.partition(".atomic") - return first + last +{indent(func())} +)''' -def failing_tests(): - inst = ALL_OPS[0] - op = drop_atomic(inst.op) +def invalid_text_test(): + return '''(assert_invalid (module + (memory 1 1 shared) - return failing_test(op, inst.arg, memidx=None, drop=inst.should_drop) + (func $i32load (drop (i32.load acqrel (i32.const 51)))) +) "Can't set memory ordering for non-atomic i32.load")''' def main(): print(text_test()) print() - print(binary_test_example()) - print() - print(binary_tests()) - print() - print(failing_tests()) - print() + print(invalid_text_test()) if __name__ == "__main__": diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index 6f20e0f4394..9ad4dcfaf27 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -5,31 +5,37 @@ (func $no_ordering_without_memid (drop (i32.atomic.load (i32.const 51))) (i32.atomic.store (i32.const 51) (i32.const 51)) + (drop (i32.atomic.rmw.add (i32.const 51) (i32.const 51))) ) (func $acqrel_without_memid (drop (i32.atomic.load acqrel (i32.const 51))) (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) + (drop (i32.atomic.rmw.add acqrel (i32.const 51) (i32.const 51))) ) (func $seqcst_without_memid (drop (i32.atomic.load seqcst (i32.const 51))) (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) + (drop (i32.atomic.rmw.add seqcst (i32.const 51) (i32.const 51))) ) (func $no_ordering_with_memid (drop (i32.atomic.load 1 (i32.const 51))) (i32.atomic.store 1 (i32.const 51) (i32.const 51)) + (drop (i32.atomic.rmw.add 1 (i32.const 51) (i32.const 51))) ) (func $acqrel_with_memid (drop (i32.atomic.load 1 acqrel (i32.const 51))) (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) + (drop (i32.atomic.rmw.add 1 acqrel (i32.const 51) (i32.const 51))) ) (func $seqcst_with_memid (drop (i32.atomic.load 1 seqcst (i32.const 51))) (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) + (drop (i32.atomic.rmw.add 1 seqcst (i32.const 51) (i32.const 51))) ) ) @@ -49,25 +55,31 @@ (module binary "\00\61\73\6d\01\00\00\00\01\04\01\60\00\00\03\07\06\00\00\00\00\00\00\05\07\02\03\01\01\03\01\01" - "\0a\7b\06" ;; code section - "\11\00" ;; func + "\0a\b8\01\06" ;; code section + "\1a\00" ;; func "\41\33\fe\10\02\00\1a" ;; (drop (i32.atomic.load (i32.const 51))) - "\41\33\41\33\fe\17\02\00\0b" ;; (i32.atomic.store (i32.const 51) (i32.const 51)) - "\13\00" ;; func + "\41\33\41\33\fe\17\02\00" ;; (i32.atomic.store (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\1e\02\00\1a\0b" ;; (drop (i32.atomic.rmw.add (i32.const 51) (i32.const 51))) + "\1d\00" ;; func "\41\33\fe\10\22\00\00\1a" ;; (drop (i32.atomic.load seqcst (i32.const 51))) - "\41\33\41\33\fe\17\22\00\00\0b" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) - "\13\00" ;; func + "\41\33\41\33\fe\17\22\00\00" ;; (i32.atomic.store seqcst (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\1e\22\00\00\1a\0b" ;; (drop (i32.atomic.rmw.add seqcst (i32.const 51) (i32.const 51))) + "\1d\00" ;; func "\41\33\fe\10\22\01\00\1a" ;; (drop (i32.atomic.load acqrel (i32.const 51))) - "\41\33\41\33\fe\17\22\01\00\0b" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) - "\13\00" ;; func + "\41\33\41\33\fe\17\22\01\00" ;; (i32.atomic.store acqrel (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\1e\22\11\00\1a\0b" ;; (drop (i32.atomic.rmw.add acqrel (i32.const 51) (i32.const 51))) + "\1d\00" ;; func "\41\33\fe\10\42\01\00\1a" ;; (drop (i32.atomic.load 1 (i32.const 51))) - "\41\33\41\33\fe\17\42\01\00\0b" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) - "\15\00" ;; func + "\41\33\41\33\fe\17\42\01\00" ;; (i32.atomic.store 1 (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\1e\42\01\00\1a\0b" ;; (drop (i32.atomic.rmw.add 1 (i32.const 51) (i32.const 51))) + "\20\00" ;; func "\41\33\fe\10\62\01\00\00\1a" ;; (drop (i32.atomic.load 1 seqcst (i32.const 51))) - "\41\33\41\33\fe\17\62\01\00\00\0b" ;; (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) - "\15\00" ;; func + "\41\33\41\33\fe\17\62\01\00\00" ;; (i32.atomic.store 1 seqcst (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\1e\62\01\00\00\1a\0b" ;; (drop (i32.atomic.rmw.add 1 seqcst (i32.const 51) (i32.const 51))) + "\20\00" ;; func "\41\33\fe\10\62\01\01\00\1a" ;; (drop (i32.atomic.load 1 acqrel (i32.const 51))) - "\41\33\41\33\fe\17\62\01\01\00\0b" ;; (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\17\62\01\01\00" ;; (i32.atomic.store 1 acqrel (i32.const 51) (i32.const 51)) + "\41\33\41\33\fe\1e\62\01\11\00\1a\0b" ;; (drop (i32.atomic.rmw.add 1 acqrel (i32.const 51) (i32.const 51))) ) (assert_invalid (module diff --git a/test/spec/relaxed-atomics2.wast b/test/spec/relaxed-atomics2.wast new file mode 100644 index 00000000000..4ca52f2ae99 --- /dev/null +++ b/test/spec/relaxed-atomics2.wast @@ -0,0 +1,49 @@ +(module + (memory i32 1 1) + (memory i64 1 1) + + (func $test-all-ops + (drop (i32.atomic.load (i32.const 42))) + (drop (i32.atomic.load acqrel (i32.const 42))) + (drop (i32.atomic.load seqcst (i32.const 42))) + (drop (i32.atomic.load 0 (i32.const 42))) + (drop (i32.atomic.load 0 acqrel (i32.const 42))) + (drop (i32.atomic.load 0 seqcst (i32.const 42))) + (drop (i32.atomic.load 1 (i64.const 42))) + (drop (i32.atomic.load 1 acqrel (i64.const 42))) + (drop (i32.atomic.load 1 seqcst (i64.const 42))) + (drop (i64.atomic.load (i32.const 42))) + (drop (i64.atomic.load acqrel (i32.const 42))) + (drop (i64.atomic.load seqcst (i32.const 42))) + (drop (i64.atomic.load 0 (i32.const 42))) + (drop (i64.atomic.load 0 acqrel (i32.const 42))) + (drop (i64.atomic.load 0 seqcst (i32.const 42))) + (drop (i64.atomic.load 1 (i64.const 42))) + (drop (i64.atomic.load 1 acqrel (i64.const 42))) + (drop (i64.atomic.load 1 seqcst (i64.const 42))) + (i32.atomic.store (i32.const 42) (i32.const 42)) + (i32.atomic.store acqrel (i32.const 42) (i32.const 42)) + (i32.atomic.store seqcst (i32.const 42) (i32.const 42)) + (i32.atomic.store 0 (i32.const 42) (i32.const 42)) + (i32.atomic.store 0 acqrel (i32.const 42) (i32.const 42)) + (i32.atomic.store 0 seqcst (i32.const 42) (i32.const 42)) + (i32.atomic.store 1 (i64.const 42) (i32.const 42)) + (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42)) + (i32.atomic.store 1 seqcst (i64.const 42) (i32.const 42)) + (i64.atomic.store (i32.const 42) (i64.const 42)) + (i64.atomic.store acqrel (i32.const 42) (i64.const 42)) + (i64.atomic.store seqcst (i32.const 42) (i64.const 42)) + (i64.atomic.store 0 (i32.const 42) (i64.const 42)) + (i64.atomic.store 0 acqrel (i32.const 42) (i64.const 42)) + (i64.atomic.store 0 seqcst (i32.const 42) (i64.const 42)) + (i64.atomic.store 1 (i64.const 42) (i64.const 42)) + (i64.atomic.store 1 acqrel (i64.const 42) (i64.const 42)) + (i64.atomic.store 1 seqcst (i64.const 42) (i64.const 42)) + ) +) + +(assert_invalid (module + (memory 1 1 shared) + + (func $i32load (drop (i32.load acqrel (i32.const 51)))) +) "Can't set memory ordering for non-atomic i32.load") From 5fd89b74755874d1e16d5a849fcc588e27879760 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 27 Jan 2026 19:39:21 +0000 Subject: [PATCH 08/10] Add comment --- test/spec/relaxed-atomics.wast | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index 9ad4dcfaf27..c5d7635aa8c 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -1,3 +1,5 @@ +;; TODO: replace this with the script generated by scripts/test/generate-atomic-spec-test.py + (module (memory 1 1 shared) (memory 1 1 shared) From 8b14835ec2f069a08c0b6a797a37182f9b5009a2 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 27 Jan 2026 19:50:36 +0000 Subject: [PATCH 09/10] Add comment to script output --- scripts/test/generate-atomic-spec-test.py | 4 +++- test/spec/relaxed-atomics2.wast | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index f8c959d3293..00d460881d9 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -61,7 +61,9 @@ def func(): ) """ statements = [statement(template, mem_idx, ordering) for template in templates for mem_idx in [None, "0", "1"] for ordering in [None, Ordering.acqrel, Ordering.seqcst]] - return f'''(func $test-all-ops + return f''';; Memory index must come before memory ordering if present. +;; Both immediates are optional; an ommitted memory ordering will be treated as seqcst. +(func $test-all-ops {indent(newline.join(statements))} )''' diff --git a/test/spec/relaxed-atomics2.wast b/test/spec/relaxed-atomics2.wast index 4ca52f2ae99..151b8693bd8 100644 --- a/test/spec/relaxed-atomics2.wast +++ b/test/spec/relaxed-atomics2.wast @@ -2,6 +2,8 @@ (memory i32 1 1) (memory i64 1 1) + ;; Memory index must come before memory ordering if present. + ;; Both immediates are optional; an ommitted memory ordering will be treated as seqcst. (func $test-all-ops (drop (i32.atomic.load (i32.const 42))) (drop (i32.atomic.load acqrel (i32.const 42))) From a7e04db4f2a7ec19d6cd1b5fd7831ad97cb72176 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 27 Jan 2026 23:29:43 +0000 Subject: [PATCH 10/10] Add comment that the test is auto-generated --- scripts/test/generate-atomic-spec-test.py | 2 ++ test/spec/relaxed-atomics2.wast | 2 ++ 2 files changed, 4 insertions(+) diff --git a/scripts/test/generate-atomic-spec-test.py b/scripts/test/generate-atomic-spec-test.py index 00d460881d9..f9da782eb71 100644 --- a/scripts/test/generate-atomic-spec-test.py +++ b/scripts/test/generate-atomic-spec-test.py @@ -87,6 +87,8 @@ def invalid_text_test(): def main(): + print(";; Generated by scripts/test/generate-atomic-spec-test.py. Do not edit manually.") + print() print(text_test()) print() print(invalid_text_test()) diff --git a/test/spec/relaxed-atomics2.wast b/test/spec/relaxed-atomics2.wast index 151b8693bd8..827c518b3b5 100644 --- a/test/spec/relaxed-atomics2.wast +++ b/test/spec/relaxed-atomics2.wast @@ -1,3 +1,5 @@ +;; Generated by scripts/test/generate-atomic-spec-test.py. Do not edit manually. + (module (memory i32 1 1) (memory i64 1 1)