Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions scripts/update_lit_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -140,16 +135,28 @@ 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] == ')':
depth -= 1
return end
Comment on lines 140 to 156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is an off-by-one bug in the loop termination logic. If the matching closing parenthesis is the very last character of the module string, the loop terminates naturally without executing the next iteration where depth == 0 is checked. As a result, end is returned as len(module) - 1 instead of len(module), which incorrectly truncates the closing parenthesis when slicing.

We can fix this and simplify the logic by checking if depth == 0 immediately after decrementing it, and returning end + 1 directly from within the loop.

Suggested change
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] == ')':
depth -= 1
return end
for end in range(start + 1, len(module)):
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] == ')':
depth -= 1
if depth == 0:
return end + 1
return len(module)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like it's worth fixing this at the same time.




def find_annotations(module, start):
# Search backward to find the start of the line containing the first of the
# annotations preceding `start`, if any.
Expand Down
41 changes: 41 additions & 0 deletions test/unit/test_update_lit_checks_strings.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than a Python unit test, let's just have a small .wast lit test that contains parens inside strings and make sure that the auto-update script can properly generate output for it.

Original file line number Diff line number Diff line change
@@ -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)
Loading