diff --git a/scripts/update_lit_checks.py b/scripts/update_lit_checks.py index 0e6801e663c..f53fe6b1f36 100755 --- a/scripts/update_lit_checks.py +++ b/scripts/update_lit_checks.py @@ -51,11 +51,6 @@ # segments included in the output. ITEM_NAME = r'\$[^\s()]*|\$"[^"]*"|declare' -# FIXME: This does not handle nested string contents. For example, -# (data (i32.const 10) "hello(") -# will look unterminated, due to the '(' inside the string. As a result, the -# code below will consider more elements after the |data| to be part of it, -# until it sees enough closing ')' symbols. ITEM_RE = re.compile(r'(?:^\s*\(rec\s*)?(^\s*)\((' + ALL_ITEMS + r')\s+(' + ITEM_NAME + ').*$', re.MULTILINE) @@ -140,9 +135,20 @@ def find_end(module, start): # open parenthesis at `start`. assert module[start] == '(' depth = 1 + in_string = False + escaped = False for end in range(start + 1, len(module)): if depth == 0: break + if in_string: + if escaped: + escaped = False + elif module[end] == '\\': + escaped = True + elif module[end] == '"': + in_string = False + elif module[end] == '"': + in_string = True elif module[end] == '(': depth += 1 elif module[end] == ')': @@ -150,6 +156,7 @@ def find_end(module, start): return end + def find_annotations(module, start): # Search backward to find the start of the line containing the first of the # annotations preceding `start`, if any. diff --git a/test/unit/test_update_lit_checks_strings.py b/test/unit/test_update_lit_checks_strings.py new file mode 100755 index 00000000000..99a528b796c --- /dev/null +++ b/test/unit/test_update_lit_checks_strings.py @@ -0,0 +1,41 @@ +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from scripts.test import shared + +from . import utils + + +class UpdateLitChecksStringsTest(utils.BinaryenTestCase): + def test_parentheses_in_strings(self): + # The updater must not treat data bytes as WAT delimiters. Include + # escaped quotes and backslashes so the following function stays separate. + strings = ['hello', 'hello(', 'hello)', 'hello(()', r'hello\"(', r'hello\\('] + with tempfile.TemporaryDirectory() as temp: + for string in strings: + with self.subTest(string=string): + test = Path(temp) / 'input.wat' + test.write_text(';; RUN: wasm-opt %s -S -o - | filecheck %s\n' + '(module\n' + ' (memory $mem 1)\n' + f' (data $data (i32.const 0) "{string}")\n' + ' (func $after (result i32) (i32.const 42))\n' + ')\n') + command = [sys.executable, + os.path.join(shared.options.binaryen_root, 'scripts', 'update_lit_checks.py'), + '--binaryen-bin', shared.options.binaryen_bin, + '--force', '--all-items', '--dry-run', str(test)] + result = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + lines = result.stdout.splitlines() + # Both items get their own CHECK block, rather than the + # function being absorbed into data's CHECK-NEXT block. + self.assertEqual(sum(';; CHECK:' in line and '(func $after' in line + for line in lines), 1, result.stdout) + self.assertFalse(any(';; CHECK-NEXT:' in line and '(func $after' in line + for line in lines), result.stdout) + self.assertTrue(any(';; CHECK:' in line and '(data $data' in line + and line.rstrip().endswith('")') for line in lines), result.stdout)