From 0c37e1bc1ca2a811ef4779302ff477ec4f8fe49e Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 7 Jan 2026 21:18:45 +0000 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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))