391 lines
16 KiB
Python
391 lines
16 KiB
Python
"""Extract original C++ compound bodies without choosing a build configuration.
|
|
|
|
This is a lexical test helper, not a C++ parser or preprocessor. Repeated,
|
|
normalized conditional expressions share symbolic choices, allowing a guarded
|
|
opening brace to be closed by a later identical guard. Other expressions are
|
|
opaque atoms, not evaluated C preprocessor arithmetic. All alternatives and
|
|
directives remain in the returned source for the real compiler to select.
|
|
Reusing a retained choice after a potentially invalidating macro change fails
|
|
closed; balanced local default definitions can join and forget their choices.
|
|
Expansion dependencies of opaque expressions are deliberately not simulated.
|
|
Conditions must be stable feature expressions, not aliases for volatile/location-dependent
|
|
macro expansions. Direct __LINE__/__COUNTER__ expressions are rejected too.
|
|
"""
|
|
|
|
import re
|
|
|
|
|
|
_RAW = re.compile(r'(?:u8|u|U|L)?R"([^\s()\\]{0,16})\(')
|
|
_DIRECTIVE = re.compile(r"#\s*([A-Za-z_]\w*)")
|
|
MAX_CONDITIONAL_PATHS = 64
|
|
|
|
|
|
def _quoted_end(text, start):
|
|
quote = text[start]
|
|
i = start + 1
|
|
while i < len(text):
|
|
if text[i] == "\\":
|
|
i += 3 if text.startswith("\r\n", i + 1) else 2
|
|
elif text[i] == quote:
|
|
return i + 1
|
|
elif text[i] in "\r\n":
|
|
raise AssertionError("unclosed C++ quoted literal")
|
|
else:
|
|
i += 1
|
|
raise AssertionError("unclosed C++ quoted literal")
|
|
|
|
|
|
def _number_separator(text, i):
|
|
if i + 1 == len(text) or not text[i + 1].isalnum():
|
|
return False
|
|
start = i
|
|
while start and (text[start - 1].isalnum() or text[start - 1] in "_'"):
|
|
start -= 1
|
|
return start < i and text[start].isdigit()
|
|
|
|
|
|
def _line_end(text, start):
|
|
end = text.find("\n", start)
|
|
while end >= 0 and text[max(0, end - 2):end].endswith(("\\", "\\\r")):
|
|
end = text.find("\n", end + 1)
|
|
return len(text) if end < 0 else end + 1
|
|
|
|
|
|
def _block_comment_end(text, start):
|
|
end = text.find("*/", start + 2)
|
|
if end < 0:
|
|
raise AssertionError("unclosed C++ block comment")
|
|
return end + 2
|
|
|
|
|
|
def _raw_end(text, start):
|
|
raw = _RAW.match(text, start) if text[start] in "uULR" else None
|
|
if raw:
|
|
closing = ")" + raw.group(1) + '"'
|
|
end = text.find(closing, raw.end())
|
|
if end < 0:
|
|
raise AssertionError("unclosed C++ raw string literal")
|
|
return end + len(closing)
|
|
if text.startswith('R"', start):
|
|
raise AssertionError("invalid C++ raw string delimiter")
|
|
return None
|
|
|
|
|
|
def _directive_end(text, start):
|
|
# Comments and raw literals may span physical lines inside a directive;
|
|
# only an unescaped newline outside those tokens ends its logical line.
|
|
i = start + 1
|
|
while i < len(text):
|
|
if text.startswith("\\\r\n", i):
|
|
i += 3
|
|
elif text.startswith("\\\n", i):
|
|
i += 2
|
|
elif text[i] in "\r\n":
|
|
return i + (2 if text.startswith("\r\n", i) else 1)
|
|
elif text.startswith("//", i):
|
|
return _line_end(text, i + 2)
|
|
elif text.startswith("/*", i):
|
|
i = _block_comment_end(text, i)
|
|
else:
|
|
raw_end = _raw_end(text, i)
|
|
if raw_end is not None:
|
|
i = raw_end
|
|
elif text[i] == '"' or (text[i] == "'" and not _number_separator(text, i)):
|
|
i = _quoted_end(text, i)
|
|
else:
|
|
i += 1
|
|
return i
|
|
|
|
|
|
def _events(text):
|
|
i = 0
|
|
line_start = True
|
|
while i < len(text):
|
|
start = i
|
|
if text.startswith("\\\n", i) or text.startswith("\\\r\n", i):
|
|
i += 3 if text.startswith("\\\r\n", i) else 2
|
|
yield "ignored", start, i, None
|
|
continue
|
|
if text[i].isspace():
|
|
if text[i] in "\r\n":
|
|
line_start = True
|
|
i += 1
|
|
continue
|
|
if text.startswith("//", i):
|
|
i = _line_end(text, i + 2)
|
|
line_start = True
|
|
yield "ignored", start, i, None
|
|
continue
|
|
if text.startswith("/*", i):
|
|
i = _block_comment_end(text, i)
|
|
if "\n" in text[start:i] or "\r" in text[start:i]:
|
|
line_start = True
|
|
yield "ignored", start, i, None
|
|
continue
|
|
if text[i] == "#" and line_start:
|
|
i = _directive_end(text, i)
|
|
logical = re.sub(r"\\\r?\n", "", text[start:i])
|
|
logical = re.sub(r"/\*.*?\*/", " ", logical, flags=re.DOTALL)
|
|
match = _DIRECTIVE.match(logical)
|
|
yield "directive", start, i, match.group(1) if match else ""
|
|
line_start = True
|
|
continue
|
|
raw_end = _raw_end(text, i)
|
|
if raw_end is not None:
|
|
i = raw_end
|
|
yield "literal", start, i, None
|
|
elif text[i] == '"' or (text[i] == "'" and not _number_separator(text, i)):
|
|
i = _quoted_end(text, i)
|
|
yield "literal", start, i, None
|
|
else:
|
|
i += 1
|
|
yield "brace" if text[start] in "{}" else "code", start, i, text[start]
|
|
line_start = False
|
|
|
|
|
|
def _uncommented(text):
|
|
"""Remove comments without changing literal contents or joining words."""
|
|
cursor, pieces = 0, []
|
|
for kind, begin, finish, _ in _events(text):
|
|
if kind == "ignored":
|
|
pieces.extend((text[cursor:begin], " "))
|
|
cursor = finish
|
|
pieces.append(text[cursor:])
|
|
return "".join(pieces)
|
|
|
|
|
|
def _directive_expression(text, start, end):
|
|
logical = re.sub(r"\\\r?\n", "", text[start:end])
|
|
# Scan after '#' so the lexer can see comments before the keyword too.
|
|
logical = "#" + _uncommented(logical[1:])
|
|
match = _DIRECTIVE.match(logical)
|
|
if not match:
|
|
raise AssertionError("invalid C++ conditional directive")
|
|
return logical[match.end():].strip()
|
|
|
|
|
|
def _outer_operand(expression):
|
|
if expression.startswith("("):
|
|
depth = 0
|
|
for kind, begin, _, value in _events(expression):
|
|
if kind == "code" and value in "()":
|
|
depth += 1 if value == "(" else -1
|
|
if depth == 0:
|
|
return expression[1:-1].strip() if begin == len(expression) - 1 else None
|
|
return None
|
|
|
|
|
|
def _simple_condition(expression):
|
|
match = re.fullmatch(r"defined\s*(?:\(\s*([A-Za-z_]\w*)\s*\)|([A-Za-z_]\w*))", expression)
|
|
if match:
|
|
return "defined(" + (match[1] or match[2]) + ")"
|
|
if re.fullmatch(r"[A-Za-z_]\w*|(?:0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*", expression):
|
|
return expression
|
|
return None
|
|
|
|
|
|
def _condition_identifiers(expression):
|
|
pieces, cursor = [], 0
|
|
for kind, begin, end, _ in _events(expression):
|
|
if kind == "literal":
|
|
pieces.extend((expression[cursor:begin], " "))
|
|
cursor = end
|
|
pieces.append(expression[cursor:])
|
|
return set(re.findall(r"\b[A-Za-z_]\w*", "".join(pieces)))
|
|
|
|
|
|
def _condition(text, start, end, directive):
|
|
expression = _directive_expression(text, start, end)
|
|
if directive in ("ifdef", "ifndef"):
|
|
if not re.fullmatch(r"[A-Za-z_]\w*", expression):
|
|
raise AssertionError("invalid C++ conditional macro name")
|
|
expression = "defined(" + expression + ")"
|
|
inverted = directive == "ifndef"
|
|
while expression:
|
|
outer = _outer_operand(expression)
|
|
if outer is not None:
|
|
expression = outer
|
|
continue
|
|
if expression.startswith("!") and not expression.startswith("!="):
|
|
# '!A && B' is not the complement of 'A && B'. Peel only a
|
|
# whole unary operand; arbitrary expressions stay opaque.
|
|
operand = expression[1:].lstrip()
|
|
probe = operand.lstrip("!").lstrip()
|
|
if _simple_condition(probe) is not None or _outer_operand(probe) is not None:
|
|
inverted = not inverted
|
|
expression = operand
|
|
continue
|
|
simple = _simple_condition(expression)
|
|
if simple is not None and simple.startswith("defined("):
|
|
return simple, inverted
|
|
if {"__LINE__", "__COUNTER__"} & _condition_identifiers(expression):
|
|
raise AssertionError("volatile C++ conditional expression is unsupported")
|
|
return simple or expression, inverted
|
|
raise AssertionError("empty C++ conditional expression")
|
|
|
|
|
|
class _Path:
|
|
def __init__(self, depth=1, choices=None, branches=()):
|
|
self.depth = depth
|
|
self.choices = {} if choices is None else choices
|
|
# Each frame is (parent active, previous alternative taken, active).
|
|
self.branches = branches
|
|
|
|
def active(self):
|
|
return not self.branches or self.branches[-1][2]
|
|
|
|
|
|
def _conditional_paths(paths, condition, inverted, invalidated, alternative=False):
|
|
result = []
|
|
for path in paths:
|
|
parent, taken = (path.branches[-1][:2] if alternative else (path.active(), False))
|
|
prefix = path.branches[:-1] if alternative else path.branches
|
|
if not parent or taken:
|
|
result.append(_Path(path.depth, path.choices, prefix + ((parent, taken, False),)))
|
|
continue
|
|
if condition in path.choices and condition in invalidated:
|
|
raise AssertionError("C++ macro mutation or directive invalidates a retained conditional choice")
|
|
values = (path.choices[condition],) if condition in path.choices else (False, True)
|
|
for value in values:
|
|
choices = dict(path.choices)
|
|
choices[condition] = value
|
|
active = value != inverted
|
|
result.append(_Path(path.depth, choices, prefix + ((True, active, active),)))
|
|
if len(result) > MAX_CONDITIONAL_PATHS:
|
|
raise AssertionError("C++ conditional path complexity exceeds 64")
|
|
if len(result) > MAX_CONDITIONAL_PATHS:
|
|
raise AssertionError("C++ conditional path complexity exceeds 64")
|
|
return result
|
|
|
|
|
|
def _merge_paths(paths):
|
|
# Forget irrelevant conditions as soon as both possibilities leave the
|
|
# same brace/conditional state. Only unresolved brace effects retain them.
|
|
# Pair and emit cubes in a stable order so conservative complexity limits
|
|
# do not depend on Python's randomized string/set iteration order.
|
|
def choice_order(choice):
|
|
return len(choice), tuple(sorted(choice))
|
|
|
|
grouped = {}
|
|
for path in paths:
|
|
grouped.setdefault((path.depth, path.branches), set()).add(frozenset(path.choices.items()))
|
|
result = []
|
|
for (depth, branches), choices in sorted(grouped.items()):
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
items = sorted(choices, key=choice_order)
|
|
for index, first in enumerate(items):
|
|
for second in items[index + 1:]:
|
|
if first <= second or second <= first:
|
|
choices.discard(second if first <= second else first)
|
|
elif (len(first ^ second) == 2 and
|
|
{key for key, _ in first} == {key for key, _ in second}):
|
|
choices.difference_update((first, second))
|
|
choices.add(first & second)
|
|
else:
|
|
continue
|
|
changed = True
|
|
break
|
|
if changed:
|
|
break
|
|
result.extend(_Path(depth, dict(sorted(choice)), branches)
|
|
for choice in sorted(choices, key=choice_order))
|
|
return result
|
|
|
|
|
|
def _invalidate_macro_choices(paths, invalidated, text, start, end):
|
|
name = re.match(r"([A-Za-z_]\w*)", _directive_expression(text, start, end))
|
|
if not name:
|
|
raise AssertionError("invalid C++ macro mutation")
|
|
for path in paths:
|
|
if not path.active():
|
|
continue
|
|
for condition in path.choices:
|
|
defined = re.fullmatch(r"defined\(([A-Za-z_]\w*)\)", condition)
|
|
# A numeric/opaque expression may expand through another macro
|
|
# supplied outside this body. Pure defined(NAME) does not expand
|
|
# NAME, so unrelated changes cannot alter that particular atom.
|
|
if defined is None or defined[1] == name[1]:
|
|
invalidated.add(condition)
|
|
|
|
|
|
def body(text, signature):
|
|
"""Return the original signature and complete compound body.
|
|
|
|
Preserve the legacy helper's ValueError for a missing signature and
|
|
AssertionError for incomplete or ambiguous bodies. Signatures in comments,
|
|
literals and macro definitions do not select a body.
|
|
"""
|
|
if not signature.strip():
|
|
raise ValueError("empty C++ signature")
|
|
start = text.find(signature)
|
|
leading = len(signature) - len(signature.lstrip())
|
|
anchor = start + leading if start >= 0 else -1
|
|
events = iter(_events(text))
|
|
for kind, begin, end, value in events:
|
|
if start < 0:
|
|
raise ValueError("C++ signature not found: " + signature)
|
|
while begin <= anchor < end and kind not in ("code", "brace"):
|
|
start = text.find(signature, end)
|
|
anchor = start + leading if start >= 0 else -1
|
|
if begin <= anchor < end:
|
|
break
|
|
else:
|
|
raise ValueError("C++ signature not found: " + signature)
|
|
|
|
for kind, begin, end, value in events:
|
|
if kind == "brace" and value == "{":
|
|
break
|
|
else:
|
|
raise AssertionError("missing C++ body opening brace")
|
|
|
|
paths = [_Path()]
|
|
branches = []
|
|
invalidated = set()
|
|
for kind, begin, end, value in events:
|
|
if kind == "directive":
|
|
if value in ("if", "ifdef", "ifndef"):
|
|
paths = _conditional_paths(paths, *_condition(text, begin, end, value), invalidated)
|
|
branches.append(False)
|
|
elif value in ("elif", "else"):
|
|
if not branches or branches[-1]:
|
|
raise AssertionError("unexpected C++ conditional alternative")
|
|
if value == "elif":
|
|
paths = _conditional_paths(paths, *_condition(text, begin, end, value), invalidated,
|
|
alternative=True)
|
|
else:
|
|
branches[-1] = True
|
|
for path in paths:
|
|
parent, taken, _ = path.branches[-1]
|
|
path.branches = path.branches[:-1] + ((parent, True, parent and not taken),)
|
|
elif value == "endif":
|
|
if not branches:
|
|
raise AssertionError("unexpected C++ conditional endif")
|
|
branches.pop()
|
|
for path in paths:
|
|
path.branches = path.branches[:-1]
|
|
paths = _merge_paths(paths)
|
|
# A balanced local #ifndef/#define needs no later correlation.
|
|
# Keep taint only while some outstanding brace/branch effect
|
|
# still depends on that choice; never reuse an invalidated one.
|
|
invalidated.intersection_update(condition for path in paths for condition in path.choices)
|
|
elif value in ("define", "undef"):
|
|
_invalidate_macro_choices(paths, invalidated, text, begin, end)
|
|
elif value in ("include", "include_next", "line") or (value == "pragma" and
|
|
re.match(r"(?:push_macro|pop_macro)\b", _directive_expression(text, begin, end))):
|
|
invalidated.update(condition for path in paths if path.active() for condition in path.choices)
|
|
elif kind != "ignored":
|
|
for path in paths:
|
|
if not path.active():
|
|
continue
|
|
if path.depth == 0:
|
|
raise AssertionError("code after a conditional C++ body closing brace")
|
|
if kind == "brace":
|
|
path.depth += 1 if value == "{" else -1
|
|
if path.depth < 0:
|
|
raise AssertionError("unexpected C++ closing brace")
|
|
if not branches and all(path.depth == 0 for path in paths):
|
|
return text[start:end]
|
|
raise AssertionError("unclosed C++ body or conditional")
|