| # | |
| # Secret Labs' Regular Expression Engine | |
| # | |
| # optimizations for the compiler | |
| # | |
| # Copyright (c) 1997-2001 by Secret Labs AB. All rights reserved. | |
| # | |
| # See the __init__.py file for information on usage and redistribution. | |
| # | |
| """Internal support module for sre. | |
| Optimization passes used by the compiler: character-set optimization | |
| (:func:`_optimize_charset`), the "simple" repeat-body test (:func:`_simple`), | |
| and the literal/charset prefix info block (:func:`_compile_info`). | |
| """ | |
| import _sre | |
| from . import _parser | |
| from ._constants import * | |
| _CHARSET_ALL = [(NEGATE, None)] | |
| _UNIT_CODES = {LITERAL, NOT_LITERAL, ANY, IN, CATEGORY} | |
| def _combine_flags(flags, add_flags, del_flags, | |
| TYPE_FLAGS=_parser.TYPE_FLAGS): | |
| if add_flags & TYPE_FLAGS: | |
| flags &= ~TYPE_FLAGS | |
| return (flags | add_flags) & ~del_flags | |
| def _compile_charset(charset, flags, code): | |
| # compile charset subprogram | |
| emit = code.append | |
| for op, av in charset: | |
| emit(op) | |
| if op is NEGATE: | |
| pass | |
| elif op is LITERAL: | |
| emit(av) | |
| elif op is RANGE or op is RANGE_UNI_IGNORE: | |
| emit(av[0]) | |
| emit(av[1]) | |
| elif op is CHARSET: | |
| code.extend(av) | |
| elif op is BIGCHARSET: | |
| code.extend(av) | |
| elif op is CATEGORY: | |
| if flags & SRE_FLAG_LOCALE: | |
| emit(CH_LOCALE[av]) | |
| elif flags & SRE_FLAG_UNICODE: | |
| emit(CH_UNICODE[av]) | |
| else: | |
| emit(av) | |
| else: | |
| raise PatternError(f"internal: unsupported set operator {op!r}") | |
| emit(FAILURE) | |
| def _optimize_charset(charset, iscased=None, fixup=None, fixes=None): | |
| # internal: optimize character set. | |
| # | |
| # The engine's charset() walk toggles polarity on every NEGATE (see | |
| # Modules/_sre/sre_lib.h), so NEGATE markers split the set into | |
| # alternating-polarity segments: a leading NEGATE is a complemented class | |
| # [^...], an interior one is set difference (RL1.3). Each segment is a | |
| # plain union, optimized on its own with the NEGATE boundaries kept in place. | |
| negates = [i for i, (op, _av) in enumerate(charset) if op is NEGATE] | |
| if not negates or negates == [0]: | |
| # Fast path: a plain union, optionally complemented as a whole -- every | |
| # charset the parser produces today, optimized as before. | |
| return _optimize_charset_segment(charset, iscased, fixup, fixes) | |
| # Optimize each NEGATE-delimited run on its own. _allow_anyall is off: the | |
| # [\s\S] -> ANY_ALL / [^\s\S] -> empty shortcuts rewrite a whole set and | |
| # would inject or drop a NEGATE mid-segment. | |
| out = [] | |
| hascased = False | |
| start = 0 | |
| for i in negates + [len(charset)]: | |
| if i > start: # skip an empty run (e.g. a leading NEGATE) | |
| opt, cased = _optimize_charset_segment( | |
| charset[start:i], iscased, fixup, fixes, _allow_anyall=False) | |
| out.extend(opt) | |
| hascased |= cased | |
| if i < len(charset): | |
| out.append((NEGATE, None)) | |
| start = i + 1 | |
| return out, hascased | |
| def _optimize_charset_segment(charset, iscased=None, fixup=None, fixes=None, | |
| _allow_anyall=True): | |
| # internal: optimize one NEGATE-free union of character-set members | |
| out = [] | |
| tail = [] | |
| charmap = bytearray(256) | |
| hascased = False | |
| for op, av in charset: | |
| while True: | |
| try: | |
| if op is LITERAL: | |
| if fixup: # IGNORECASE and not LOCALE | |
| av = fixup(av) | |
| charmap[av] = 1 | |
| if fixes and av in fixes: | |
| for k in fixes[av]: | |
| charmap[k] = 1 | |
| if not hascased and iscased(av): | |
| hascased = True | |
| else: | |
| charmap[av] = 1 | |
| elif op is RANGE: | |
| r = range(av[0], av[1]+1) | |
| if fixup: # IGNORECASE and not LOCALE | |
| if fixes: | |
| for i in map(fixup, r): | |
| charmap[i] = 1 | |
| if i in fixes: | |
| for k in fixes[i]: | |
| charmap[k] = 1 | |
| else: | |
| for i in map(fixup, r): | |
| charmap[i] = 1 | |
| if not hascased: | |
| hascased = any(map(iscased, r)) | |
| else: | |
| for i in r: | |
| charmap[i] = 1 | |
| elif op is NEGATE: | |
| out.append((op, av)) | |
| elif op is CATEGORY and _allow_anyall and tail and (CATEGORY, CH_NEGATE[av]) in tail: | |
| # Optimize [\s\S] etc. | |
| out = [] if out else _CHARSET_ALL | |
| return out, False | |
| else: | |
| tail.append((op, av)) | |
| except IndexError: | |
| if len(charmap) == 256: | |
| # character set contains non-UCS1 character codes | |
| charmap += b'\0' * 0xff00 | |
| continue | |
| # Character set contains non-BMP character codes. | |
| # For range, all BMP characters in the range are already | |
| # proceeded. | |
| if fixup: # IGNORECASE and not LOCALE | |
| # For now, IN_UNI_IGNORE+LITERAL and | |
| # IN_UNI_IGNORE+RANGE_UNI_IGNORE work for all non-BMP | |
| # characters, because two characters (at least one of | |
| # which is not in the BMP) match case-insensitively | |
| # if and only if: | |
| # 1) c1.lower() == c2.lower() | |
| # 2) c1.lower() == c2 or c1.lower().upper() == c2 | |
| # Also, both c.lower() and c.lower().upper() are single | |
| # characters for every non-BMP character. | |
| if op is RANGE: | |
| if fixes: # not ASCII | |
| op = RANGE_UNI_IGNORE | |
| hascased = True | |
| else: | |
| assert op is LITERAL | |
| if not hascased and iscased(av): | |
| hascased = True | |
| tail.append((op, av)) | |
| break | |
| # compress character map | |
| runs = [] | |
| q = 0 | |
| while True: | |
| p = charmap.find(1, q) | |
| if p < 0: | |
| break | |
| if len(runs) >= 2: | |
| runs = None | |
| break | |
| q = charmap.find(0, p) | |
| if q < 0: | |
| runs.append((p, len(charmap))) | |
| break | |
| runs.append((p, q)) | |
| if runs is not None: | |
| # use literal/range | |
| for p, q in runs: | |
| if q - p == 1: | |
| out.append((LITERAL, p)) | |
| else: | |
| out.append((RANGE, (p, q - 1))) | |
| out += tail | |
| # if the case was changed or new representation is more compact | |
| if hascased or len(out) < len(charset): | |
| return out, hascased | |
| # else original character set is good enough | |
| return charset, hascased | |
| # use bitmap | |
| if len(charmap) == 256: | |
| data = _mk_bitmap(charmap) | |
| out.append((CHARSET, data)) | |
| out += tail | |
| return out, hascased | |
| # To represent a big charset, first a bitmap of all characters in the | |
| # set is constructed. Then, this bitmap is sliced into chunks of 256 | |
| # characters, duplicate chunks are eliminated, and each chunk is | |
| # given a number. In the compiled expression, the charset is | |
| # represented by a 32-bit word sequence, consisting of one word for | |
| # the number of different chunks, a sequence of 256 bytes (64 words) | |
| # of chunk numbers indexed by their original chunk position, and a | |
| # sequence of 256-bit chunks (8 words each). | |
| # Compression is normally good: in a typical charset, large ranges of | |
| # Unicode will be either completely excluded (e.g. if only cyrillic | |
| # letters are to be matched), or completely included (e.g. if large | |
| # subranges of Kanji match). These ranges will be represented by | |
| # chunks of all one-bits or all zero-bits. | |
| # Matching can be also done efficiently: the more significant byte of | |
| # the Unicode character is an index into the chunk number, and the | |
| # less significant byte is a bit index in the chunk (just like the | |
| # CHARSET matching). | |
| charmap = charmap.take_bytes() # should be hashable | |
| comps = {} | |
| mapping = bytearray(256) | |
| block = 0 | |
| data = bytearray() | |
| for i in range(0, 65536, 256): | |
| chunk = charmap[i: i + 256] | |
| if chunk in comps: | |
| mapping[i // 256] = comps[chunk] | |
| else: | |
| mapping[i // 256] = comps[chunk] = block | |
| block += 1 | |
| data += chunk | |
| data = _mk_bitmap(data) | |
| data[0:0] = [block] + _bytes_to_codes(mapping) | |
| out.append((BIGCHARSET, data)) | |
| out += tail | |
| return out, hascased | |
| _CODEBITS = _sre.CODESIZE * 8 | |
| MAXCODE = (1 << _CODEBITS) - 1 | |
| _BITS_TRANS = b'0' + b'1' * 255 | |
| def _mk_bitmap(bits, _CODEBITS=_CODEBITS, _int=int): | |
| s = bits.translate(_BITS_TRANS)[::-1] | |
| return [_int(s[i - _CODEBITS: i], 2) | |
| for i in range(len(s), 0, -_CODEBITS)] | |
| def _bytes_to_codes(b): | |
| # Convert block indices to word array | |
| a = memoryview(b).cast('I') | |
| assert a.itemsize == _sre.CODESIZE | |
| assert len(a) * a.itemsize == len(b) | |
| return a.tolist() | |
| def _simple(p): | |
| # check if this subpattern is a "simple" operator | |
| if len(p) != 1: | |
| return False | |
| op, av = p[0] | |
| if op is SUBPATTERN: | |
| return av[0] is None and _simple(av[-1]) | |
| return op in _UNIT_CODES | |
| def _generate_overlap_table(prefix): | |
| """ | |
| Generate an overlap table for the following prefix. | |
| An overlap table is a table of the same size as the prefix which | |
| informs about the potential self-overlap for each index in the prefix: | |
| - if overlap[i] == 0, prefix[i:] can't overlap prefix[0:...] | |
| - if overlap[i] == k with 0 < k <= i, prefix[i-k+1:i+1] overlaps with | |
| prefix[0:k] | |
| """ | |
| table = [0] * len(prefix) | |
| for i in range(1, len(prefix)): | |
| idx = table[i - 1] | |
| while prefix[i] != prefix[idx]: | |
| if idx == 0: | |
| table[i] = 0 | |
| break | |
| idx = table[idx - 1] | |
| else: | |
| table[i] = idx + 1 | |
| return table | |
| def _get_iscased(flags): | |
| if not flags & SRE_FLAG_IGNORECASE: | |
| return None | |
| elif flags & SRE_FLAG_UNICODE: | |
| return _sre.unicode_iscased | |
| else: | |
| return _sre.ascii_iscased | |
| def _get_literal_prefix(pattern, flags): | |
| # look for literal prefix | |
| prefix = [] | |
| prefixappend = prefix.append | |
| prefix_skip = None | |
| iscased = _get_iscased(flags) | |
| for op, av in pattern.data: | |
| if op is LITERAL: | |
| if iscased and iscased(av): | |
| break | |
| prefixappend(av) | |
| elif op is SUBPATTERN: | |
| group, add_flags, del_flags, p = av | |
| flags1 = _combine_flags(flags, add_flags, del_flags) | |
| if flags1 & SRE_FLAG_IGNORECASE and flags1 & SRE_FLAG_LOCALE: | |
| break | |
| prefix1, prefix_skip1, got_all = _get_literal_prefix(p, flags1) | |
| if prefix_skip is None: | |
| if group is not None: | |
| prefix_skip = len(prefix) | |
| elif prefix_skip1 is not None: | |
| prefix_skip = len(prefix) + prefix_skip1 | |
| prefix.extend(prefix1) | |
| if not got_all: | |
| break | |
| else: | |
| break | |
| else: | |
| return prefix, prefix_skip, True | |
| return prefix, prefix_skip, False | |
| def _get_charset_prefix(pattern, flags): | |
| while True: | |
| if not pattern.data: | |
| return None | |
| op, av = pattern.data[0] | |
| if op is not SUBPATTERN: | |
| break | |
| group, add_flags, del_flags, pattern = av | |
| flags = _combine_flags(flags, add_flags, del_flags) | |
| if flags & SRE_FLAG_IGNORECASE and flags & SRE_FLAG_LOCALE: | |
| return None | |
| iscased = _get_iscased(flags) | |
| if op is LITERAL: | |
| if iscased and iscased(av): | |
| return None | |
| return [(op, av)] | |
| elif op is CATEGORY: | |
| return [(op, av)] | |
| elif op is BRANCH: | |
| charset = [] | |
| charsetappend = charset.append | |
| for p in av[1]: | |
| if not p: | |
| return None | |
| op, av = p[0] | |
| if op is LITERAL and not (iscased and iscased(av)): | |
| charsetappend((op, av)) | |
| else: | |
| return None | |
| return charset | |
| elif op is IN: | |
| charset = av | |
| if iscased: | |
| for op, av in charset: | |
| if op is LITERAL: | |
| if iscased(av): | |
| return None | |
| elif op is RANGE: | |
| if av[1] > 0xffff: | |
| return None | |
| if any(map(iscased, range(av[0], av[1]+1))): | |
| return None | |
| return charset | |
| return None | |
| def _compile_info(code, pattern, flags): | |
| # internal: compile an info block. in the current version, | |
| # this contains min/max pattern width, and an optional literal | |
| # prefix or a character map | |
| lo, hi = pattern.getwidth() | |
| if hi > MAXCODE: | |
| hi = MAXCODE | |
| if lo == 0: | |
| code.extend([INFO, 4, 0, lo, hi]) | |
| return | |
| # look for a literal prefix | |
| prefix = [] | |
| prefix_skip = 0 | |
| charset = None # not used | |
| if not (flags & SRE_FLAG_IGNORECASE and flags & SRE_FLAG_LOCALE): | |
| # look for literal prefix | |
| prefix, prefix_skip, got_all = _get_literal_prefix(pattern, flags) | |
| # if no prefix, look for charset prefix | |
| if not prefix: | |
| charset = _get_charset_prefix(pattern, flags) | |
| if charset: | |
| charset, hascased = _optimize_charset(charset) | |
| assert not hascased | |
| if charset == _CHARSET_ALL: | |
| charset = None | |
| ## if prefix: | |
| ## print("*** PREFIX", prefix, prefix_skip) | |
| ## if charset: | |
| ## print("*** CHARSET", charset) | |
| # add an info block | |
| emit = code.append | |
| emit(INFO) | |
| skip = len(code); emit(0) | |
| # literal flag | |
| mask = 0 | |
| if prefix: | |
| mask = SRE_INFO_PREFIX | |
| if prefix_skip is None and got_all: | |
| mask = mask | SRE_INFO_LITERAL | |
| elif charset: | |
| mask = mask | SRE_INFO_CHARSET | |
| emit(mask) | |
| # pattern length | |
| if lo < MAXCODE: | |
| emit(lo) | |
| else: | |
| emit(MAXCODE) | |
| prefix = prefix[:MAXCODE] | |
| emit(hi) | |
| # add literal prefix | |
| if prefix: | |
| emit(len(prefix)) # length | |
| if prefix_skip is None: | |
| prefix_skip = len(prefix) | |
| emit(prefix_skip) # skip | |
| code.extend(prefix) | |
| # generate overlap table | |
| code.extend(_generate_overlap_table(prefix)) | |
| elif charset: | |
| _compile_charset(charset, flags, code) | |
| code[skip] = len(code) - skip | |
| # Difference-fusion peephole: rewrite [A--B]-style A(?<![B]) into a single | |
| # charset (see the engine's NEGATE polarity toggle). | |
| def _subpatterns(op, av): | |
| # Yield the nested SubPatterns of one item, to recurse into. | |
| if op is BRANCH: | |
| yield from av[1] | |
| elif op in (ASSERT, ASSERT_NOT): | |
| yield av[1] | |
| elif op is SUBPATTERN: | |
| yield av[3] | |
| elif op is ATOMIC_GROUP: | |
| yield av | |
| elif op in (MIN_REPEAT, MAX_REPEAT, POSSESSIVE_REPEAT): | |
| yield av[2] | |
| elif op is GROUPREF_EXISTS: | |
| yield av[1] # the "yes" branch is always present | |
| if av[2] is not None: # the "no" branch is optional | |
| yield av[2] | |
| def _fuse_branch(av): | |
| # Fold a BRANCH of one-character matchers into a single charset: their union | |
| # is the concatenation of the item lists. charset() lets only the final | |
| # polarity segment subtract, so at most one alternative may be | |
| # complement-bearing (carry a NEGATE) and it must trail; two would cross | |
| # (e.g. [a-z--b]||[a-z--c]) and are left as a BRANCH. | |
| items = [] | |
| tail = None | |
| for sp in av[1]: | |
| cs = _parser._flat_items(sp.data, True) | |
| if cs is None: | |
| return None | |
| if any(op is NEGATE for op, _av in cs): | |
| if tail is not None: | |
| return None | |
| tail = cs | |
| else: | |
| items += cs | |
| return items if tail is None else items + tail | |
| def _fuse_difference(data): | |
| # Replace <flat charset A> (?<![B1]) (?<![B2]) ... with the single charset | |
| # [NEGATE] B1 B2 ... [NEGATE] A. Each negative lookbehind over a flat | |
| # charset subtracts its set from the character A matches. | |
| out = [] | |
| head = None # _flat_items(A) for the fused difference now at out[-1] | |
| subtrahend = None # its accumulated B items, or None when not fusing | |
| for op, av in data: | |
| if op is ASSERT_NOT and av[0] < 0: # a negative lookbehind | |
| b = _parser._flat_items(av[1].data) | |
| if b is not None: | |
| if subtrahend is None and out: | |
| # the first lookbehind of a run: only now is it worth | |
| # checking whether the preceding item A is a flat charset. | |
| head = _parser._flat_items([out[-1]]) | |
| if head is not None: | |
| subtrahend = [] | |
| if subtrahend is not None: | |
| subtrahend += b | |
| out[-1] = (IN, [(NEGATE, None)] + subtrahend | |
| + [(NEGATE, None)] + head) | |
| continue | |
| head = subtrahend = None | |
| out.append((op, av)) | |
| data[:] = out | |
| def _walk(seq): | |
| for i, (op, av) in enumerate(seq): | |
| for sub in _subpatterns(op, av): | |
| _walk(sub.data) | |
| if op is BRANCH: | |
| items = _fuse_branch(av) | |
| if items is not None: | |
| seq[i] = (IN, items) | |
| _fuse_difference(seq) | |
| def optimize(pattern): | |
| """Rewrite a parsed pattern in place and return it.""" | |
| _walk(pattern.data) | |
| return pattern |