From 97e066f23f35b5bc65e31093fd074a6c1e61195b Mon Sep 17 00:00:00 2001 From: Anghelo Carvajal Date: Wed, 25 Aug 2021 01:11:41 -0400 Subject: [PATCH] Update asm-processor and diff.py (#278) * fix asm differ branch * git subrepo pull --force tools/asm-differ subrepo: subdir: "tools/asm-differ" merged: "fd0984c97" upstream: origin: "https://github.com/simonlindholm/asm-differ.git" branch: "main" commit: "fd0984c97" git-subrepo: version: "0.4.3" origin: "???" commit: "???" * delete asm-processor * git subrepo clone git@github.com:simonlindholm/asm-processor.git tools/asm-processor subrepo: subdir: "tools/asm-processor" merged: "755f734fb" upstream: origin: "git@github.com:simonlindholm/asm-processor.git" branch: "main" commit: "755f734fb" git-subrepo: version: "0.4.3" origin: "???" commit: "???" * re-add build.py * remove subrepo * git subrepo pull --force tools/asm-differ subrepo: subdir: "tools/asm-differ" merged: "1dfba80e1" upstream: origin: "https://github.com/simonlindholm/asm-differ.git" branch: "main" commit: "1dfba80e1" git-subrepo: version: "0.4.3" origin: "???" commit: "???" --- tools/asm-differ/.gitignore | 1 + tools/asm-differ/.gitrepo | 6 +- tools/asm-differ/LICENSE | 24 + tools/asm-differ/README.md | 6 +- tools/asm-differ/diff-stylesheet.css | 64 + tools/asm-differ/diff.py | 2289 ++++++++++++++++++-------- tools/asm-differ/diff_settings.py | 15 +- tools/asm-differ/mypy.ini | 1 + tools/asm-differ/screenshot.png | Bin 0 -> 99842 bytes tools/asm-processor/asm_processor.py | 96 +- 10 files changed, 1762 insertions(+), 740 deletions(-) create mode 100644 tools/asm-differ/LICENSE create mode 100644 tools/asm-differ/diff-stylesheet.css create mode 100644 tools/asm-differ/screenshot.png diff --git a/tools/asm-differ/.gitignore b/tools/asm-differ/.gitignore index e5daf0d15b..eb176dc56b 100644 --- a/tools/asm-differ/.gitignore +++ b/tools/asm-differ/.gitignore @@ -1 +1,2 @@ .mypy_cache/ +__pycache__/ diff --git a/tools/asm-differ/.gitrepo b/tools/asm-differ/.gitrepo index 619ae708c5..72e41e4e60 100644 --- a/tools/asm-differ/.gitrepo +++ b/tools/asm-differ/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/simonlindholm/asm-differ.git - branch = master - commit = eaf72269cf7329bc061e50d8788229575f656f06 - parent = fa02cf86ffdb88253181cda15d047b08576d3f99 + branch = main + commit = 1dfba80e1b36bb31c9ea64cb04583e7b36d839a6 + parent = f14e8c9d9e68107609c9eeb4408bbfd1cd850eee method = merge cmdver = 0.4.3 diff --git a/tools/asm-differ/LICENSE b/tools/asm-differ/LICENSE new file mode 100644 index 0000000000..cf1ab25da0 --- /dev/null +++ b/tools/asm-differ/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/tools/asm-differ/README.md b/tools/asm-differ/README.md index d7c56187d6..44ab2e6e48 100644 --- a/tools/asm-differ/README.md +++ b/tools/asm-differ/README.md @@ -1,17 +1,17 @@ # asm-differ -Nice differ for assembly code (currently MIPS, but should be easy to hack to support other instruction sets). +Nice differ for assembly code (MIPS and AArch64; should be easy to hack to support other instruction sets). ![](screenshot.png) ## Dependencies - Python >= 3.6 -- `python3 -m pip install --user colorama ansiwrap attrs watchdog` +- `python3 -m pip install --user colorama watchdog python-Levenshtein` (also `dataclasses` if on 3.6) ## Usage -Create a file `diff-settings.sh` in some directory (see the one in this repo for an example). Then from that directory, run +Create a file `diff_settings.sh` in some directory (see the one in this repo for an example). Then from that directory, run ``` /path/to/diff.sh [flags] (function|rom addr) diff --git a/tools/asm-differ/diff-stylesheet.css b/tools/asm-differ/diff-stylesheet.css new file mode 100644 index 0000000000..099c7af471 --- /dev/null +++ b/tools/asm-differ/diff-stylesheet.css @@ -0,0 +1,64 @@ +table.diff { + border: none; + font-family: Monospace; + white-space: pre; +} +.immediate { + color: lightblue; +} +.stack { + color: yellow; +} +.register { + color: yellow; +} +.delay-slot { + font-weight: bold; + color: gray; +} +.diff-change { + color: lightblue; +} +.diff-add { + color: green; +} +.diff-remove { + color: red; +} +.source-filename { + font-weight: bold; +} +.source-function { + font-weight: bold; + text-decoration: underline; +} +.source-other { + font-style: italic; +} +.rotation-0 { + color: magenta; +} +.rotation-1 { + color: cyan; +} +.rotation-2 { + color: green; +} +.rotation-3 { + color: red; +} +.rotation-4 { + color: yellow; +} +.rotation-5 { + color: pink; +} +.rotation-6 { + color: blue; +} +.rotation-7 { + color: lime; +} +.rotation-8 { + color: gray; +} diff --git a/tools/asm-differ/diff.py b/tools/asm-differ/diff.py index 0e1da09deb..f2064e395a 100755 --- a/tools/asm-differ/diff.py +++ b/tools/asm-differ/diff.py @@ -1,278 +1,450 @@ #!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK +import argparse import sys +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Match, + NoReturn, + Optional, + Pattern, + Set, + Tuple, + Type, + Union, +) -def fail(msg): + +def fail(msg: str) -> NoReturn: print(msg, file=sys.stderr) sys.exit(1) -# Prefer to use diff_settings.py from the current working directory -sys.path.insert(0, ".") -try: - import diff_settings -except ModuleNotFoundError: - fail("Unable to find diff_settings.py in the same directory.") -sys.path.pop(0) + +def static_assert_unreachable(x: NoReturn) -> NoReturn: + raise Exception("Unreachable! " + repr(x)) + # ==== COMMAND-LINE ==== -try: - import argcomplete # type: ignore -except ModuleNotFoundError: - argcomplete = None -import argparse +if __name__ == "__main__": + # Prefer to use diff_settings.py from the current working directory + sys.path.insert(0, ".") + try: + import diff_settings + except ModuleNotFoundError: + fail("Unable to find diff_settings.py in the same directory.") + sys.path.pop(0) -parser = argparse.ArgumentParser(description="Diff MIPS assembly.") + try: + import argcomplete # type: ignore + except ModuleNotFoundError: + argcomplete = None -start_argument = parser.add_argument("start", help="Function name or address to start diffing from.") -if argcomplete: - def complete_symbol(**kwargs): - prefix = kwargs["prefix"] - if prefix == "": - # skip reading the map file, which would - # result in a lot of useless completions - return [] - parsed_args = kwargs["parsed_args"] - config = {} - diff_settings.apply(config, parsed_args) - mapfile = config.get("mapfile") - if not mapfile: - return [] - completes = [] - with open(mapfile) as f: - data = f.read() - # assume symbols are prefixed by a space character - search = f" {prefix}" - pos = data.find(search) - while pos != -1: - # skip the space character in the search string - pos += 1 - # assume symbols are suffixed by either a space - # character or a (unix-style) line return - spacePos = data.find(" ", pos) - lineReturnPos = data.find("\n", pos) - if lineReturnPos == -1: - endPos = spacePos - elif spacePos == -1: - endPos = lineReturnPos - else: - endPos = min(spacePos, lineReturnPos) - if endPos == -1: - match = data[pos:] - pos = -1 - else: - match = data[pos:endPos] - pos = data.find(search, endPos) - completes.append(match) - return completes - start_argument.completer = complete_symbol + parser = argparse.ArgumentParser(description="Diff MIPS, PPC or AArch64 assembly.") -parser.add_argument("end", nargs="?", help="Address to end diff at.") -parser.add_argument( - "-o", - dest="diff_obj", - action="store_true", - help="Diff .o files rather than a whole binary. This makes it possible to see symbol names. (Recommended)", -) -parser.add_argument( - "-e", - "--elf", - dest="diff_elf_symbol", - help="Diff a given function in two ELFs, one being stripped and the other one non-stripped. Requires objdump from binutils 2.33+.", -) -parser.add_argument( - "--source", - action="store_true", - help="Show source code (if possible). Only works with -o and -e.", -) -parser.add_argument( - "--inlines", - action="store_true", - help="Show inline function calls (if possible). Only works with -o and -e.", -) -parser.add_argument( - "--base-asm", - dest="base_asm", - metavar="FILE", - help="Read assembly from given file instead of configured base img.", -) -parser.add_argument( - "--write-asm", - dest="write_asm", - metavar="FILE", - help="Write the current assembly output to file, e.g. for use with --base-asm.", -) -parser.add_argument( - "-m", - "--make", - dest="make", - action="store_true", - help="Automatically run 'make' on the .o file or binary before diffing.", -) -parser.add_argument( - "-l", - "--skip-lines", - dest="skip_lines", - type=int, - default=0, - help="Skip the first N lines of output.", -) -parser.add_argument( - "-s", - "--stop-jr-ra", - dest="stop_jrra", - action="store_true", - help="Stop disassembling at the first 'jr ra'. Some functions have multiple return points, so use with care!", -) -parser.add_argument( - "-i", - "--ignore-large-imms", - dest="ignore_large_imms", - action="store_true", - help="Pretend all large enough immediates are the same.", -) -parser.add_argument( - "-B", - "--no-show-branches", - dest="show_branches", - action="store_false", - help="Don't visualize branches/branch targets.", -) -parser.add_argument( - "-S", - "--base-shift", - dest="base_shift", - type=str, - default="0", - help="Diff position X in our img against position X + shift in the base img. " - 'Arithmetic is allowed, so e.g. |-S "0x1234 - 0x4321"| is a reasonable ' - "flag to pass if it is known that position 0x1234 in the base img syncs " - "up with position 0x4321 in our img. Not supported together with -o.", -) -parser.add_argument( - "-w", - "--watch", - dest="watch", - action="store_true", - help="Automatically update when source/object files change. " - "Recommended in combination with -m.", -) -parser.add_argument( - "-3", - "--threeway", - dest="threeway", - action="store_true", - help="Show a three-way diff between target asm, current asm, and asm " - "prior to -w rebuild. Requires -w.", -) -parser.add_argument( - "--width", - dest="column_width", - type=int, - default=50, - help="Sets the width of the left and right view column.", -) -parser.add_argument( - "--algorithm", - dest="algorithm", - default="levenshtein", - choices=["levenshtein", "difflib"], - help="Diff algorithm to use.", -) -parser.add_argument( - "--max-size", - "--max-lines", - dest="max_lines", - type=int, - default=1024, - help="The maximum length of the diff, in lines.", -) + start_argument = parser.add_argument( + "start", + help="Function name or address to start diffing from.", + ) -# Project-specific flags, e.g. different versions/make arguments. -if hasattr(diff_settings, "add_custom_arguments"): - diff_settings.add_custom_arguments(parser) # type: ignore + if argcomplete: -if argcomplete: - argcomplete.autocomplete(parser) + def complete_symbol( + prefix: str, parsed_args: argparse.Namespace, **kwargs: object + ) -> List[str]: + if not prefix or prefix.startswith("-"): + # skip reading the map file, which would + # result in a lot of useless completions + return [] + config: Dict[str, Any] = {} + diff_settings.apply(config, parsed_args) # type: ignore + mapfile = config.get("mapfile") + if not mapfile: + return [] + completes = [] + with open(mapfile) as f: + data = f.read() + # assume symbols are prefixed by a space character + search = f" {prefix}" + pos = data.find(search) + while pos != -1: + # skip the space character in the search string + pos += 1 + # assume symbols are suffixed by either a space + # character or a (unix-style) line return + spacePos = data.find(" ", pos) + lineReturnPos = data.find("\n", pos) + if lineReturnPos == -1: + endPos = spacePos + elif spacePos == -1: + endPos = lineReturnPos + else: + endPos = min(spacePos, lineReturnPos) + if endPos == -1: + match = data[pos:] + pos = -1 + else: + match = data[pos:endPos] + pos = data.find(search, endPos) + completes.append(match) + return completes + + setattr(start_argument, "completer", complete_symbol) + + parser.add_argument( + "end", + nargs="?", + help="Address to end diff at.", + ) + parser.add_argument( + "-o", + dest="diff_obj", + action="store_true", + help="""Diff .o files rather than a whole binary. This makes it possible to + see symbol names. (Recommended)""", + ) + parser.add_argument( + "-e", + "--elf", + dest="diff_elf_symbol", + metavar="SYMBOL", + help="""Diff a given function in two ELFs, one being stripped and the other + one non-stripped. Requires objdump from binutils 2.33+.""", + ) + parser.add_argument( + "--source", + "-c", + dest="source", + action="store_true", + help="Show source code (if possible). Only works with -o or -e.", + ) + parser.add_argument( + "--source-old-binutils", + "-C", + dest="source_old_binutils", + action="store_true", + help="Tweak --source handling to make it work with binutils < 2.33. Implies --source.", + ) + parser.add_argument( + "--inlines", + dest="inlines", + action="store_true", + help="Show inline function calls (if possible). Only works with -o or -e.", + ) + parser.add_argument( + "--base-asm", + dest="base_asm", + metavar="FILE", + help="Read assembly from given file instead of configured base img.", + ) + parser.add_argument( + "--write-asm", + dest="write_asm", + metavar="FILE", + help="Write the current assembly output to file, e.g. for use with --base-asm.", + ) + parser.add_argument( + "-m", + "--make", + dest="make", + action="store_true", + help="Automatically run 'make' on the .o file or binary before diffing.", + ) + parser.add_argument( + "-l", + "--skip-lines", + dest="skip_lines", + metavar="LINES", + type=int, + default=0, + help="Skip the first LINES lines of output.", + ) + parser.add_argument( + "-s", + "--stop-jr-ra", + dest="stop_jrra", + action="store_true", + help="""Stop disassembling at the first 'jr ra'. Some functions have + multiple return points, so use with care!""", + ) + parser.add_argument( + "-i", + "--ignore-large-imms", + dest="ignore_large_imms", + action="store_true", + help="Pretend all large enough immediates are the same.", + ) + parser.add_argument( + "-I", + "--ignore-addr-diffs", + dest="ignore_addr_diffs", + action="store_true", + help="Ignore address differences. Currently only affects AArch64.", + ) + parser.add_argument( + "-B", + "--no-show-branches", + dest="show_branches", + action="store_false", + help="Don't visualize branches/branch targets.", + ) + parser.add_argument( + "-S", + "--base-shift", + dest="base_shift", + metavar="N", + type=str, + default="0", + help="""Diff position N in our img against position N + shift in the base img. + Arithmetic is allowed, so e.g. |-S "0x1234 - 0x4321"| is a reasonable + flag to pass if it is known that position 0x1234 in the base img syncs + up with position 0x4321 in our img. Not supported together with -o.""", + ) + parser.add_argument( + "-w", + "--watch", + dest="watch", + action="store_true", + help="""Automatically update when source/object files change. + Recommended in combination with -m.""", + ) + parser.add_argument( + "-3", + "--threeway=prev", + dest="threeway", + action="store_const", + const="prev", + help="""Show a three-way diff between target asm, current asm, and asm + prior to -w rebuild. Requires -w.""", + ) + parser.add_argument( + "-b", + "--threeway=base", + dest="threeway", + action="store_const", + const="base", + help="""Show a three-way diff between target asm, current asm, and asm + when diff.py was started. Requires -w.""", + ) + parser.add_argument( + "--width", + dest="column_width", + metavar="COLS", + type=int, + default=50, + help="Sets the width of the left and right view column.", + ) + parser.add_argument( + "--algorithm", + dest="algorithm", + default="levenshtein", + choices=["levenshtein", "difflib"], + help="""Diff algorithm to use. Levenshtein gives the minimum diff, while difflib + aims for long sections of equal opcodes. Defaults to %(default)s.""", + ) + parser.add_argument( + "--max-size", + "--max-lines", + metavar="LINES", + dest="max_lines", + type=int, + default=1024, + help="The maximum length of the diff, in lines.", + ) + parser.add_argument( + "--no-pager", + dest="no_pager", + action="store_true", + help="""Disable the pager; write output directly to stdout, then exit. + Incompatible with --watch.""", + ) + parser.add_argument( + "--format", + choices=("color", "plain", "html"), + default="color", + help="Output format, default is color. --format=html implies --no-pager.", + ) + parser.add_argument( + "-U", + "--compress-matching", + metavar="N", + dest="compress_matching", + type=int, + help="""Compress streaks of matching lines, leaving N lines of context + around non-matching parts.""", + ) + parser.add_argument( + "-V", + "--compress-sameinstr", + metavar="N", + dest="compress_sameinstr", + type=int, + help="""Compress streaks of lines with same instructions (but possibly + different regalloc), leaving N lines of context around other parts.""", + ) + + # Project-specific flags, e.g. different versions/make arguments. + add_custom_arguments_fn = getattr(diff_settings, "add_custom_arguments", None) + if add_custom_arguments_fn: + add_custom_arguments_fn(parser) + + if argcomplete: + argcomplete.autocomplete(parser) # ==== IMPORTS ==== -import re -import os +# (We do imports late to optimize auto-complete performance.) + +import abc import ast -import subprocess +from dataclasses import dataclass, field, replace import difflib -import string +import enum +import html import itertools -import threading +import os import queue +import re +import string +import subprocess +import threading import time -from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union MISSING_PREREQUISITES = ( "Missing prerequisite python module {}. " - "Run `python3 -m pip install --user colorama ansiwrap watchdog python-Levenshtein cxxfilt` to install prerequisites (cxxfilt only needed with --source)." + "Run `python3 -m pip install --user colorama watchdog python-Levenshtein cxxfilt` to install prerequisites (cxxfilt only needed with --source)." ) try: - from colorama import Fore, Style, Back # type: ignore - import ansiwrap # type: ignore + from colorama import Fore, Style # type: ignore import watchdog # type: ignore except ModuleNotFoundError as e: fail(MISSING_PREREQUISITES.format(e.name)) # ==== CONFIG ==== -args = parser.parse_args() -# Set imgs, map file and make flags in a project-specific manner. -config: Dict[str, Any] = {} -diff_settings.apply(config, args) +@dataclass +class ProjectSettings: + arch_str: str + objdump_executable: str + build_command: List[str] + map_format: str + mw_build_dir: str + baseimg: Optional[str] + myimg: Optional[str] + mapfile: Optional[str] + source_directories: Optional[List[str]] + source_extensions: List[str] -arch = config.get("arch", "mips") -baseimg = config.get("baseimg", None) -myimg = config.get("myimg", None) -mapfile = config.get("mapfile", None) -makeflags = config.get("makeflags", []) -source_directories = config.get("source_directories", None) -objdump_executable = config.get("objdump_executable", None) -MAX_FUNCTION_SIZE_LINES = args.max_lines -MAX_FUNCTION_SIZE_BYTES = MAX_FUNCTION_SIZE_LINES * 4 +@dataclass +class Compress: + context: int + same_instr: bool -COLOR_ROTATION = [ - Fore.MAGENTA, - Fore.CYAN, - Fore.GREEN, - Fore.RED, - Fore.LIGHTYELLOW_EX, - Fore.LIGHTMAGENTA_EX, - Fore.LIGHTCYAN_EX, - Fore.LIGHTGREEN_EX, - Fore.LIGHTBLACK_EX, -] -BUFFER_CMD = ["tail", "-c", str(10 ** 9)] -LESS_CMD = ["less", "-SRic", "-#6"] +@dataclass +class Config: + arch: "ArchSettings" -DEBOUNCE_DELAY = 0.1 -FS_WATCH_EXTENSIONS = [".c", ".h"] + # Build/objdump options + diff_obj: bool + make: bool + source: bool + source_old_binutils: bool + inlines: bool + max_function_size_lines: int + max_function_size_bytes: int -# ==== LOGIC ==== + # Display options + formatter: "Formatter" + threeway: Optional[str] + base_shift: int + skip_lines: int + compress: Optional[Compress] + show_branches: bool + stop_jrra: bool + ignore_large_imms: bool + ignore_addr_diffs: bool + algorithm: str -if args.algorithm == "levenshtein": - try: - import Levenshtein # type: ignore - except ModuleNotFoundError as e: - fail(MISSING_PREREQUISITES.format(e.name)) -if args.source: - try: - import cxxfilt # type: ignore - except ModuleNotFoundError as e: - fail(MISSING_PREREQUISITES.format(e.name)) +def create_project_settings(settings: Dict[str, Any]) -> ProjectSettings: + return ProjectSettings( + arch_str=settings.get("arch", "mips"), + baseimg=settings.get("baseimg"), + myimg=settings.get("myimg"), + mapfile=settings.get("mapfile"), + build_command=settings.get( + "make_command", ["make", *settings.get("makeflags", [])] + ), + source_directories=settings.get("source_directories"), + source_extensions=settings.get( + "source_extensions", [".c", ".h", ".cpp", ".hpp", ".s"] + ), + objdump_executable=get_objdump_executable(settings.get("objdump_executable")), + map_format=settings.get("map_format", "gnu"), + mw_build_dir=settings.get("mw_build_dir", "build/"), + ) + + +def create_config(args: argparse.Namespace, project: ProjectSettings) -> Config: + formatter: Formatter + if args.format == "plain": + formatter = PlainFormatter(column_width=args.column_width) + elif args.format == "color": + formatter = AnsiFormatter(column_width=args.column_width) + elif args.format == "html": + formatter = HtmlFormatter() + else: + raise ValueError(f"Unsupported --format: {args.format}") + + compress = None + if args.compress_matching is not None: + compress = Compress(args.compress_matching, False) + if args.compress_sameinstr is not None: + if compress is not None: + raise ValueError( + "Cannot pass both --compress-matching and --compress-sameinstr" + ) + compress = Compress(args.compress_sameinstr, True) + + return Config( + arch=get_arch(project.arch_str), + # Build/objdump options + diff_obj=args.diff_obj, + make=args.make, + source=args.source or args.source_old_binutils, + source_old_binutils=args.source_old_binutils, + inlines=args.inlines, + max_function_size_lines=args.max_lines, + max_function_size_bytes=args.max_lines * 4, + # Display options + formatter=formatter, + threeway=args.threeway, + base_shift=eval_int( + args.base_shift, "Failed to parse --base-shift (-S) argument as an integer." + ), + skip_lines=args.skip_lines, + compress=compress, + show_branches=args.show_branches, + stop_jrra=args.stop_jrra, + ignore_large_imms=args.ignore_large_imms, + ignore_addr_diffs=args.ignore_addr_diffs, + algorithm=args.algorithm, + ) + + +def get_objdump_executable(objdump_executable: Optional[str]) -> str: + if objdump_executable is not None: + return objdump_executable -if objdump_executable is None: for objdump_cand in ["mips-linux-gnu-objdump", "mips64-elf-objdump"]: try: subprocess.check_call( @@ -280,53 +452,351 @@ if objdump_executable is None: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - objdump_executable = objdump_cand - break + return objdump_cand except subprocess.CalledProcessError: pass except FileNotFoundError: pass -if not objdump_executable: - fail( + return fail( "Missing binutils; please ensure mips-linux-gnu-objdump or mips64-elf-objdump exist, or configure objdump_executable." ) -def eval_int(expr, emsg=None): +def get_arch(arch_str: str) -> "ArchSettings": + if arch_str == "mips": + return MIPS_SETTINGS + if arch_str == "aarch64": + return AARCH64_SETTINGS + if arch_str == "ppc": + return PPC_SETTINGS + return fail(f"Unknown architecture: {arch_str}") + + +BUFFER_CMD: List[str] = ["tail", "-c", str(10 ** 9)] + +# -S truncates long lines instead of wrapping them +# -R interprets color escape sequences +# -i ignores case when searching +# -c something about how the screen gets redrawn; I don't remember the purpose +# -#6 makes left/right arrow keys scroll by 6 characters +LESS_CMD: List[str] = ["less", "-SRic", "-#6"] + +DEBOUNCE_DELAY: float = 0.1 + +# ==== FORMATTING ==== + + +@enum.unique +class BasicFormat(enum.Enum): + NONE = enum.auto() + IMMEDIATE = enum.auto() + STACK = enum.auto() + REGISTER = enum.auto() + DELAY_SLOT = enum.auto() + DIFF_CHANGE = enum.auto() + DIFF_ADD = enum.auto() + DIFF_REMOVE = enum.auto() + SOURCE_FILENAME = enum.auto() + SOURCE_FUNCTION = enum.auto() + SOURCE_OTHER = enum.auto() + + +@dataclass(frozen=True) +class RotationFormat: + group: str + index: int + key: str + + +Format = Union[BasicFormat, RotationFormat] +FormatFunction = Callable[[str], Format] + + +class Text: + segments: List[Tuple[str, Format]] + + def __init__(self, line: str = "", f: Format = BasicFormat.NONE) -> None: + self.segments = [(line, f)] if line else [] + + def reformat(self, f: Format) -> "Text": + return Text(self.plain(), f) + + def plain(self) -> str: + return "".join(s for s, f in self.segments) + + def __repr__(self) -> str: + return f"" + + def __bool__(self) -> bool: + return any(s for s, f in self.segments) + + def __str__(self) -> str: + # Use Formatter.apply(...) instead + return NotImplemented + + def __eq__(self, other: object) -> bool: + return NotImplemented + + def __add__(self, other: Union["Text", str]) -> "Text": + if isinstance(other, str): + other = Text(other) + result = Text() + result.segments = self.segments + other.segments + return result + + def __radd__(self, other: Union["Text", str]) -> "Text": + if isinstance(other, str): + other = Text(other) + result = Text() + result.segments = other.segments + self.segments + return result + + def finditer(self, pat: Pattern[str]) -> Iterator[Match[str]]: + """Replacement for `pat.finditer(text)` that operates on the inner text, + and returns the exact same matches as `Text.sub(pat, ...)`.""" + for chunk, f in self.segments: + for match in pat.finditer(chunk): + yield match + + def sub(self, pat: Pattern[str], sub_fn: Callable[[Match[str]], "Text"]) -> "Text": + result = Text() + for chunk, f in self.segments: + i = 0 + for match in pat.finditer(chunk): + start, end = match.start(), match.end() + assert i <= start <= end <= len(chunk) + sub = sub_fn(match) + result.segments.append((chunk[i:start], f)) + result.segments.extend(sub.segments) + i = end + result.segments.append((chunk[i:], f)) + return result + + def ljust(self, column_width: int) -> "Text": + length = sum(len(x) for x, _ in self.segments) + return self + " " * max(column_width - length, 0) + + +class Formatter(abc.ABC): + @abc.abstractmethod + def apply_format(self, chunk: str, f: Format) -> str: + """Apply the formatting `f` to `chunk` and escape the contents.""" + ... + + @abc.abstractmethod + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + """Format a multi-column table with an optional `header`""" + ... + + def apply(self, text: Text) -> str: + return "".join(self.apply_format(chunk, f) for chunk, f in text.segments) + + +@dataclass +class PlainFormatter(Formatter): + column_width: int + + def apply_format(self, chunk: str, f: Format) -> str: + return chunk + + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + if header: + lines = [header] + lines + return "\n".join( + "".join(self.apply(x.ljust(self.column_width)) for x in line) + for line in lines + ) + + +@dataclass +class AnsiFormatter(Formatter): + BASIC_ANSI_CODES = { + BasicFormat.NONE: "", + BasicFormat.IMMEDIATE: Fore.LIGHTBLUE_EX, + BasicFormat.STACK: Fore.YELLOW, + BasicFormat.REGISTER: Fore.YELLOW, + BasicFormat.DELAY_SLOT: Fore.LIGHTBLACK_EX, + BasicFormat.DIFF_CHANGE: Fore.LIGHTBLUE_EX, + BasicFormat.DIFF_ADD: Fore.GREEN, + BasicFormat.DIFF_REMOVE: Fore.RED, + BasicFormat.SOURCE_FILENAME: Style.DIM + Style.BRIGHT, + # Underline (not in colorama) + bright + dim + BasicFormat.SOURCE_FUNCTION: Style.DIM + Style.BRIGHT + "\u001b[4m", + BasicFormat.SOURCE_OTHER: Style.DIM, + } + + ROTATION_ANSI_COLORS = [ + Fore.MAGENTA, + Fore.CYAN, + Fore.GREEN, + Fore.RED, + Fore.LIGHTYELLOW_EX, + Fore.LIGHTMAGENTA_EX, + Fore.LIGHTCYAN_EX, + Fore.LIGHTGREEN_EX, + Fore.LIGHTBLACK_EX, + ] + + column_width: int + + def apply_format(self, chunk: str, f: Format) -> str: + if f == BasicFormat.NONE: + return chunk + if isinstance(f, BasicFormat): + ansi_code = self.BASIC_ANSI_CODES[f] + elif isinstance(f, RotationFormat): + ansi_code = self.ROTATION_ANSI_COLORS[ + f.index % len(self.ROTATION_ANSI_COLORS) + ] + else: + static_assert_unreachable(f) + return f"{ansi_code}{chunk}{Style.RESET_ALL}" + + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + if header: + lines = [header] + lines + return "\n".join( + "".join(self.apply(x.ljust(self.column_width)) for x in line) + for line in lines + ) + + +@dataclass +class HtmlFormatter(Formatter): + rotation_formats: int = 9 + + def apply_format(self, chunk: str, f: Format) -> str: + chunk = html.escape(chunk) + if f == BasicFormat.NONE: + return chunk + if isinstance(f, BasicFormat): + class_name = f.name.lower().replace("_", "-") + data_attr = "" + elif isinstance(f, RotationFormat): + class_name = f"rotation-{f.index % self.rotation_formats}" + rotation_key = html.escape(f"{f.group};{f.key}", quote=True) + data_attr = f'data-rotation="{rotation_key}"' + else: + static_assert_unreachable(f) + return f"{chunk}" + + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + def table_row(line: Tuple[Text, ...], cell_el: str) -> str: + output_row = " " + for cell in line: + cell_html = self.apply(cell) + output_row += f"<{cell_el}>{cell_html}" + output_row += "\n" + return output_row + + output = "\n" + if header: + output += " \n" + output += table_row(header, "th") + output += " \n" + output += " \n" + output += "".join(table_row(line, "td") for line in lines) + output += " \n" + output += "
\n" + return output + + +def format_fields( + pat: Pattern[str], + out1: Text, + out2: Text, + color1: FormatFunction, + color2: Optional[FormatFunction] = None, +) -> Tuple[Text, Text]: + diffs = [ + of.group() != nf.group() + for (of, nf) in zip(out1.finditer(pat), out2.finditer(pat)) + ] + + it = iter(diffs) + + def maybe_color(color: FormatFunction, s: str) -> Text: + return Text(s, color(s)) if next(it, False) else Text(s) + + out1 = out1.sub(pat, lambda m: maybe_color(color1, m.group())) + it = iter(diffs) + out2 = out2.sub(pat, lambda m: maybe_color(color2 or color1, m.group())) + + return out1, out2 + + +def symbol_formatter(group: str, base_index: int) -> FormatFunction: + symbol_formats: Dict[str, Format] = {} + + def symbol_format(s: str) -> Format: + # TODO: it would be nice to use a unique Format for each symbol, so we could + # add extra UI elements in the HTML version + f = symbol_formats.get(s) + if f is None: + index = len(symbol_formats) + base_index + f = RotationFormat(key=s, index=index, group=group) + symbol_formats[s] = f + return f + + return symbol_format + + +# ==== LOGIC ==== + +ObjdumpCommand = Tuple[List[str], str, Optional[str]] + + +def maybe_eval_int(expr: str) -> Optional[int]: try: ret = ast.literal_eval(expr) if not isinstance(ret, int): raise Exception("not an integer") return ret except Exception: - if emsg is not None: - fail(emsg) return None -def eval_line_num(expr): +def eval_int(expr: str, emsg: str) -> int: + ret = maybe_eval_int(expr) + if ret is None: + fail(emsg) + return ret + + +def eval_line_num(expr: str) -> int: return int(expr.strip().replace(":", ""), 16) -def run_make(target, capture_output=False): - if capture_output: - return subprocess.run( - ["make"] + makeflags + [target], - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - ) - else: - subprocess.check_call(["make"] + makeflags + [target]) +def run_make(target: str, project: ProjectSettings) -> None: + subprocess.check_call(project.build_command + [target]) -def restrict_to_function(dump, fn_name): - out = [] +def run_make_capture_output( + target: str, project: ProjectSettings +) -> "subprocess.CompletedProcess[bytes]": + return subprocess.run( + project.build_command + [target], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + +def restrict_to_function(dump: str, fn_name: str, config: Config) -> str: + out: List[str] = [] search = f"<{fn_name}>:" found = False for line in dump.split("\n"): if found: - if len(out) >= MAX_FUNCTION_SIZE_LINES: + if len(out) >= config.max_function_size_lines: break out.append(line) elif search in line: @@ -334,90 +804,149 @@ def restrict_to_function(dump, fn_name): return "\n".join(out) -def maybe_get_objdump_source_flags(): - if not args.source: +def maybe_get_objdump_source_flags(config: Config) -> List[str]: + if not config.source: return [] flags = [ "--source", - "--source-comment=| ", "-l", ] - if args.inlines: + if not config.source_old_binutils: + flags.append("--source-comment=│ ") + + if config.inlines: flags.append("--inlines") return flags -def run_objdump(cmd): +def run_objdump(cmd: ObjdumpCommand, config: Config, project: ProjectSettings) -> str: flags, target, restrict = cmd - out = subprocess.check_output( - [objdump_executable] + arch_flags + flags + [target], universal_newlines=True - ) + try: + out = subprocess.run( + [project.objdump_executable] + config.arch.arch_flags + flags + [target], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ).stdout + except subprocess.CalledProcessError as e: + print(e.stdout) + print(e.stderr) + if "unrecognized option '--source-comment" in e.stderr: + fail("** Try using --source-old-binutils instead of --source **") + raise e + if restrict is not None: - return restrict_to_function(out, restrict) + return restrict_to_function(out, restrict, config) return out -base_shift = eval_int( - args.base_shift, "Failed to parse --base-shift (-S) argument as an integer." -) - - -def search_map_file(fn_name): - if not mapfile: +def search_map_file( + fn_name: str, project: ProjectSettings +) -> Tuple[Optional[str], Optional[int]]: + if not project.mapfile: fail(f"No map file configured; cannot find function {fn_name}.") try: - with open(mapfile) as f: - lines = f.read().split("\n") + with open(project.mapfile) as f: + contents = f.read() except Exception: - fail(f"Failed to open map file {mapfile} for reading.") + fail(f"Failed to open map file {project.mapfile} for reading.") - try: - cur_objfile = None - ram_to_rom = None - cands = [] - last_line = "" - for line in lines: - if line.startswith(" .text"): - cur_objfile = line.split()[3] - if "load address" in line: - tokens = last_line.split() + line.split() - ram = int(tokens[1], 0) - rom = int(tokens[5], 0) - ram_to_rom = rom - ram - if line.endswith(" " + fn_name): - ram = int(line.split()[0], 0) - if cur_objfile is not None and ram_to_rom is not None: - cands.append((cur_objfile, ram + ram_to_rom)) - last_line = line - except Exception as e: - import traceback + if project.map_format == "gnu": + lines = contents.split("\n") - traceback.print_exc() - fail(f"Internal error while parsing map file") + try: + cur_objfile = None + ram_to_rom = None + cands = [] + last_line = "" + for line in lines: + if line.startswith(" .text"): + cur_objfile = line.split()[3] + if "load address" in line: + tokens = last_line.split() + line.split() + ram = int(tokens[1], 0) + rom = int(tokens[5], 0) + ram_to_rom = rom - ram + if line.endswith(" " + fn_name): + ram = int(line.split()[0], 0) + if cur_objfile is not None and ram_to_rom is not None: + cands.append((cur_objfile, ram + ram_to_rom)) + last_line = line + except Exception as e: + import traceback - if len(cands) > 1: - fail(f"Found multiple occurrences of function {fn_name} in map file.") - if len(cands) == 1: - return cands[0] + traceback.print_exc() + fail(f"Internal error while parsing map file") + + if len(cands) > 1: + fail(f"Found multiple occurrences of function {fn_name} in map file.") + if len(cands) == 1: + return cands[0] + elif project.map_format == "mw": + find = re.findall( + re.compile( + # ram elf rom + r" \S+ \S+ (\S+) (\S+) . " + + fn_name + # object name + + r"(?: \(entry of \.(?:init|text)\))? \t(\S+)" + ), + contents, + ) + if len(find) > 1: + fail(f"Found multiple occurrences of function {fn_name} in map file.") + if len(find) == 1: + rom = int(find[0][1], 16) + objname = find[0][2] + # The metrowerks linker map format does not contain the full object path, + # so we must complete it manually. + objfiles = [ + os.path.join(dirpath, f) + for dirpath, _, filenames in os.walk(project.mw_build_dir) + for f in filenames + if f == objname + ] + if len(objfiles) > 1: + all_objects = "\n".join(objfiles) + fail( + f"Found multiple objects of the same name {objname} in {project.mw_build_dir}, " + f"cannot determine which to diff against: \n{all_objects}" + ) + if len(objfiles) == 1: + objfile = objfiles[0] + # TODO Currently the ram-rom conversion only works for diffing ELF + # executables, but it would likely be more convenient to diff DOLs. + # At this time it is recommended to always use -o when running the diff + # script as this mode does not make use of the ram-rom conversion. + return objfile, rom + else: + fail(f"Linker map format {project.map_format} unrecognised.") return None, None -def dump_elf(): - if not baseimg or not myimg: +def dump_elf( + start: str, + end: Optional[str], + diff_elf_symbol: str, + config: Config, + project: ProjectSettings, +) -> Tuple[str, ObjdumpCommand, ObjdumpCommand]: + if not project.baseimg or not project.myimg: fail("Missing myimg/baseimg in config.") - if base_shift: + if config.base_shift: fail("--base-shift not compatible with -e") - start_addr = eval_int(args.start, "Start address must be an integer expression.") + start_addr = eval_int(start, "Start address must be an integer expression.") - if args.end is not None: - end_addr = eval_int(args.end, "End address must be an integer expression.") + if end is not None: + end_addr = eval_int(end, "End address must be an integer expression.") else: - end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES + end_addr = start_addr + config.max_function_size_bytes flags1 = [ f"--start-address={start_addr}", @@ -425,31 +954,37 @@ def dump_elf(): ] flags2 = [ - f"--disassemble={args.diff_elf_symbol}", + f"--disassemble={diff_elf_symbol}", ] objdump_flags = ["-drz", "-j", ".text"] return ( - myimg, - (objdump_flags + flags1, baseimg, None), - (objdump_flags + flags2 + maybe_get_objdump_source_flags(), myimg, None), + project.myimg, + (objdump_flags + flags1, project.baseimg, None), + ( + objdump_flags + flags2 + maybe_get_objdump_source_flags(config), + project.myimg, + None, + ), ) -def dump_objfile(): - if base_shift: +def dump_objfile( + start: str, end: Optional[str], config: Config, project: ProjectSettings +) -> Tuple[str, ObjdumpCommand, ObjdumpCommand]: + if config.base_shift: fail("--base-shift not compatible with -o") - if args.end is not None: + if end is not None: fail("end address not supported together with -o") - if args.start.startswith("0"): + if start.startswith("0"): fail("numerical start address not supported with -o; pass a function name") - objfile, _ = search_map_file(args.start) + objfile, _ = search_map_file(start, project) if not objfile: fail("Not able to find .o file for function.") - if args.make: - run_make(objfile) + if config.make: + run_make(objfile, project) if not os.path.isfile(objfile): fail(f"Not able to find .o file for function: {objfile} is not a file.") @@ -461,108 +996,259 @@ def dump_objfile(): objdump_flags = ["-drz"] return ( objfile, - (objdump_flags, refobjfile, args.start), - (objdump_flags + maybe_get_objdump_source_flags(), objfile, args.start), + (objdump_flags, refobjfile, start), + (objdump_flags + maybe_get_objdump_source_flags(config), objfile, start), ) -def dump_binary(): - if not baseimg or not myimg: +def dump_binary( + start: str, end: Optional[str], config: Config, project: ProjectSettings +) -> Tuple[str, ObjdumpCommand, ObjdumpCommand]: + if not project.baseimg or not project.myimg: fail("Missing myimg/baseimg in config.") - if args.make: - run_make(myimg) - start_addr = eval_int(args.start) + if config.make: + run_make(project.myimg, project) + start_addr = maybe_eval_int(start) if start_addr is None: - _, start_addr = search_map_file(args.start) + _, start_addr = search_map_file(start, project) if start_addr is None: fail("Not able to find function in map file.") - if args.end is not None: - end_addr = eval_int(args.end, "End address must be an integer expression.") + if end is not None: + end_addr = eval_int(end, "End address must be an integer expression.") else: - end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES - objdump_flags = ["-Dz", "-bbinary", "-mmips", "-EB"] + end_addr = start_addr + config.max_function_size_bytes + objdump_flags = ["-Dz", "-bbinary", "-EB"] flags1 = [ - f"--start-address={start_addr + base_shift}", - f"--stop-address={end_addr + base_shift}", + f"--start-address={start_addr + config.base_shift}", + f"--stop-address={end_addr + config.base_shift}", ] flags2 = [f"--start-address={start_addr}", f"--stop-address={end_addr}"] return ( - myimg, - (objdump_flags + flags1, baseimg, None), - (objdump_flags + flags2, myimg, None), + project.myimg, + (objdump_flags + flags1, project.baseimg, None), + (objdump_flags + flags2, project.myimg, None), ) -# Alignment with ANSI colors is broken, let's fix it. -def ansi_ljust(s, width): - needed = width - ansiwrap.ansilen(s) - if needed > 0: - return s + " " * needed - else: - return s +class DifferenceNormalizer: + def __init__(self, config: Config) -> None: + self.config = config + + def normalize(self, mnemonic: str, row: str) -> str: + """This should be called exactly once for each line.""" + row = self._normalize_arch_specific(mnemonic, row) + if self.config.ignore_large_imms: + row = re.sub(self.config.arch.re_large_imm, "", row) + return row + + def _normalize_arch_specific(self, mnemonic: str, row: str) -> str: + return row -if arch == "mips": - re_int = re.compile(r"[0-9]+") - re_comment = re.compile(r"<.*?>") - re_reg = re.compile(r"\$?\b(a[0-3]|t[0-9]|s[0-8]|at|v[01]|f[12]?[0-9]|f3[01]|k[01]|fp|ra)\b") - re_sprel = re.compile(r"(?<=,)([0-9]+|0x[0-9a-f]+)\(sp\)") - re_large_imm = re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}") - re_imm = re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(sp)|%(lo|hi)\([^)]*\)") - forbidden = set(string.ascii_letters + "_") - arch_flags = ["-m", "mips:4300"] - branch_likely_instructions = { - "beql", - "bnel", - "beqzl", - "bnezl", - "bgezl", - "bgtzl", - "blezl", - "bltzl", - "bc1tl", - "bc1fl", +class DifferenceNormalizerAArch64(DifferenceNormalizer): + def __init__(self, config: Config) -> None: + super().__init__(config) + self._adrp_pair_registers: Set[str] = set() + + def _normalize_arch_specific(self, mnemonic: str, row: str) -> str: + if self.config.ignore_addr_diffs: + row = self._normalize_adrp_differences(mnemonic, row) + row = self._normalize_bl(mnemonic, row) + return row + + def _normalize_bl(self, mnemonic: str, row: str) -> str: + if mnemonic != "bl": + return row + + row, _ = split_off_branch(row) + return row + + def _normalize_adrp_differences(self, mnemonic: str, row: str) -> str: + """Identifies ADRP + LDR/ADD pairs that are used to access the GOT and + suppresses any immediate differences. + + Whenever an ADRP is seen, the destination register is added to the set of registers + that are part of an ADRP + LDR/ADD pair. Registers are removed from the set as soon + as they are used for an LDR or ADD instruction which completes the pair. + + This method is somewhat crude but should manage to detect most such pairs. + """ + row_parts = row.split("\t", 1) + if mnemonic == "adrp": + self._adrp_pair_registers.add(row_parts[1].strip().split(",")[0]) + row, _ = split_off_branch(row) + elif mnemonic == "ldr": + for reg in self._adrp_pair_registers: + # ldr xxx, [reg] + # ldr xxx, [reg, ] + if f", [{reg}" in row_parts[1]: + self._adrp_pair_registers.remove(reg) + return normalize_imms(row, AARCH64_SETTINGS) + elif mnemonic == "add": + for reg in self._adrp_pair_registers: + # add reg, reg, + if row_parts[1].startswith(f"{reg}, {reg}, "): + self._adrp_pair_registers.remove(reg) + return normalize_imms(row, AARCH64_SETTINGS) + + return row + + +@dataclass +class ArchSettings: + re_int: Pattern[str] + re_comment: Pattern[str] + re_reg: Pattern[str] + re_sprel: Pattern[str] + re_large_imm: Pattern[str] + re_imm: Pattern[str] + branch_instructions: Set[str] + instructions_with_address_immediates: Set[str] + forbidden: Set[str] = field(default_factory=lambda: set(string.ascii_letters + "_")) + arch_flags: List[str] = field(default_factory=list) + branch_likely_instructions: Set[str] = field(default_factory=set) + difference_normalizer: Type[DifferenceNormalizer] = DifferenceNormalizer + + +MIPS_BRANCH_LIKELY_INSTRUCTIONS = { + "beql", + "bnel", + "beqzl", + "bnezl", + "bgezl", + "bgtzl", + "blezl", + "bltzl", + "bc1tl", + "bc1fl", +} +MIPS_BRANCH_INSTRUCTIONS = MIPS_BRANCH_LIKELY_INSTRUCTIONS.union( + { + "b", + "beq", + "bne", + "beqz", + "bnez", + "bgez", + "bgtz", + "blez", + "bltz", + "bc1t", + "bc1f", } - branch_instructions = branch_likely_instructions.union( - {"b", "beq", "bne", "beqz", "bnez", "bgez", "bgtz", "blez", "bltz", "bc1t", "bc1f"} - ) - instructions_with_address_immediates = branch_instructions.union({"jal", "j"}) -elif arch == "aarch64": - re_int = re.compile(r"[0-9]+") - re_comment = re.compile(r"(<.*?>|//.*$)") +) + +AARCH64_BRANCH_INSTRUCTIONS = { + "bl", + "b", + "b.eq", + "b.ne", + "b.cs", + "b.hs", + "b.cc", + "b.lo", + "b.mi", + "b.pl", + "b.vs", + "b.vc", + "b.hi", + "b.ls", + "b.ge", + "b.lt", + "b.gt", + "b.le", + "cbz", + "cbnz", + "tbz", + "tbnz", +} + +PPC_BRANCH_INSTRUCTIONS = { + "b", + "beq", + "beq+", + "beq-", + "bne", + "bne+", + "bne-", + "blt", + "blt+", + "blt-", + "ble", + "ble+", + "ble-", + "bdnz", + "bdnz+", + "bdnz-", + "bge", + "bge+", + "bge-", + "bgt", + "bgt+", + "bgt-", +} + +MIPS_SETTINGS = ArchSettings( + re_int=re.compile(r"[0-9]+"), + re_comment=re.compile(r"<.*?>"), + re_reg=re.compile( + r"\$?\b(a[0-3]|t[0-9]|s[0-8]|at|v[01]|f[12]?[0-9]|f3[01]|k[01]|fp|ra|zero)\b" + ), + re_sprel=re.compile(r"(?<=,)([0-9]+|0x[0-9a-f]+)\(sp\)"), + re_large_imm=re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}"), + re_imm=re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(sp)|%(lo|hi)\([^)]*\)"), + arch_flags=["-m", "mips:4300"], + branch_likely_instructions=MIPS_BRANCH_LIKELY_INSTRUCTIONS, + branch_instructions=MIPS_BRANCH_INSTRUCTIONS, + instructions_with_address_immediates=MIPS_BRANCH_INSTRUCTIONS.union({"jal", "j"}), +) + +AARCH64_SETTINGS = ArchSettings( + re_int=re.compile(r"[0-9]+"), + re_comment=re.compile(r"(<.*?>|//.*$)"), # GPRs and FP registers: X0-X30, W0-W30, [DSHQ]0..31 # The zero registers and SP should not be in this list. - re_reg = re.compile(r"\$?\b([dshq][12]?[0-9]|[dshq]3[01]|[xw][12]?[0-9]|[xw]30)\b") - re_sprel = re.compile(r"sp, #-?(0x[0-9a-fA-F]+|[0-9]+)\b") - re_large_imm = re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}") - re_imm = re.compile(r"(?|//.*$)"), + re_reg=re.compile(r"\$?\b([rf][0-9]+)\b"), + re_sprel=re.compile(r"(?<=,)(-?[0-9]+|-?0x[0-9a-f]+)\(r1\)"), + re_large_imm=re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}"), + re_imm=re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(r1)|[^@]*@(ha|h|lo)"), + branch_instructions=PPC_BRANCH_INSTRUCTIONS, + instructions_with_address_immediates=PPC_BRANCH_INSTRUCTIONS.union({"bl"}), +) -def hexify_int(row, pat): +def hexify_int(row: str, pat: Match[str], arch: ArchSettings) -> str: full = pat.group(0) if len(full) <= 1: # leave one-digit ints alone return full start, end = pat.span() - if start and row[start - 1] in forbidden: + if start and row[start - 1] in arch.forbidden: return full - if end < len(row) and row[end] in forbidden: + if end < len(row) and row[end] in arch.forbidden: return full return hex(int(full)) -def parse_relocated_line(line): - try: - ind2 = line.rindex(",") - except ValueError: - ind2 = line.rindex("\t") +def parse_relocated_line(line: str) -> Tuple[str, str, str]: + for c in ",\t ": + if c in line: + ind2 = line.rindex(c) + break + else: + raise Exception(f"failed to parse relocated line: {line}") before = line[: ind2 + 1] after = line[ind2 + 1 :] ind2 = after.find("(") @@ -575,7 +1261,7 @@ def parse_relocated_line(line): return before, imm, after -def process_mips_reloc(row, prev): +def process_mips_reloc(row: str, prev: str, arch: ArchSettings) -> str: before, imm, after = parse_relocated_line(prev) repl = row.split()[-1] if imm != "0": @@ -586,7 +1272,10 @@ def process_mips_reloc(row, prev): # TODO: handle unambiguous cases where all addends for a symbol are the # same, or show "+???". mnemonic = prev.split()[0] - if mnemonic in instructions_with_address_immediates and not imm.startswith("0x"): + if ( + mnemonic in arch.instructions_with_address_immediates + and not imm.startswith("0x") + ): imm = "0x" + imm repl += "+" + imm if int(imm, 0) > 0 else imm if "R_MIPS_LO16" in row: @@ -596,98 +1285,166 @@ def process_mips_reloc(row, prev): # correct addend for each, but objdump doesn't give us the order of # the relocations, so we can't find the right LO16. :( repl = f"%hi({repl})" + elif "R_MIPS_26" in row: + # Function calls + pass + elif "R_MIPS_PC16" in row: + # Branch to glabel. This gives confusing output, but there's not much + # we can do here. + pass else: - assert "R_MIPS_26" in row, f"unknown relocation type '{row}'" + assert False, f"unknown relocation type '{row}' for line '{prev}'" return before + repl + after -def pad_mnemonic(line): +def process_ppc_reloc(row: str, prev: str) -> str: + assert any( + r in row for r in ["R_PPC_REL24", "R_PPC_ADDR16", "R_PPC_EMB_SDA21"] + ), f"unknown relocation type '{row}' for line '{prev}'" + before, imm, after = parse_relocated_line(prev) + repl = row.split()[-1] + if "R_PPC_REL24" in row: + # function calls + pass + elif "R_PPC_ADDR16_HI" in row: + # absolute hi of addr + repl = f"{repl}@h" + elif "R_PPC_ADDR16_HA" in row: + # adjusted hi of addr + repl = f"{repl}@ha" + elif "R_PPC_ADDR16_LO" in row: + # lo of addr + repl = f"{repl}@l" + elif "R_PPC_ADDR16" in row: + # 16-bit absolute addr + if "+0x7" in repl: + # remove the very large addends as they are an artifact of (label-_SDA(2)_BASE_) + # computations and are unimportant in a diff setting. + if int(repl.split("+")[1], 16) > 0x70000000: + repl = repl.split("+")[0] + elif "R_PPC_EMB_SDA21" in row: + # small data area + pass + return before + repl + after + + +def pad_mnemonic(line: str) -> str: if "\t" not in line: return line mn, args = line.split("\t", 1) return f"{mn:<7s} {args}" -class Line(NamedTuple): +@dataclass +class Line: mnemonic: str diff_row: str original: str + normalized_original: str line_num: str branch_target: Optional[str] source_lines: List[str] comment: Optional[str] -def process(lines): +def process(lines: List[str], config: Config) -> List[Line]: + arch = config.arch + normalizer = arch.difference_normalizer(config) skip_next = False source_lines = [] - if not args.diff_obj: + if not config.diff_obj: lines = lines[7:] if lines and not lines[-1]: lines.pop() - output = [] + i = 0 + output: List[Line] = [] stop_after_delay_slot = False - for row in lines: - if args.diff_obj and (">:" in row or not row): + while i < len(lines): + row = lines[i] + i += 1 + + if config.diff_obj and (">:" in row or not row): continue - if args.source and (row and row[0] != " "): + if config.source and not config.source_old_binutils and (row and row[0] != " "): source_lines.append(row) continue - if "R_AARCH64_" in row: - # TODO: handle relocation + if ( + config.source + and config.source_old_binutils + and (row and not re.match(r"^ +[0-9a-f]+:\t", row)) + ): + source_lines.append(row) continue - if "R_MIPS_" in row: - # N.B. Don't transform the diff rows, they already ignore immediates - # if output[-1].diff_row != "": - # output[-1] = output[-1].replace(diff_row=process_mips_reloc(row, output[-1].row_with_imm)) - new_original = process_mips_reloc(row, output[-1].original) - output[-1] = output[-1]._replace(original=new_original) - continue - - m_comment = re.search(re_comment, row) + m_comment = re.search(arch.re_comment, row) comment = m_comment[0] if m_comment else None - row = re.sub(re_comment, "", row) + row = re.sub(arch.re_comment, "", row) row = row.rstrip() tabs = row.split("\t") row = "\t".join(tabs[2:]) line_num = tabs[0].strip() - row_parts = row.split("\t", 1) + + if "\t" in row: + row_parts = row.split("\t", 1) + else: + # powerpc-eabi-objdump doesn't use tabs + row_parts = [part.lstrip() for part in row.split(" ", 1)] mnemonic = row_parts[0].strip() - if mnemonic not in instructions_with_address_immediates: - row = re.sub(re_int, lambda s: hexify_int(row, s), row) + + if mnemonic not in arch.instructions_with_address_immediates: + row = re.sub(arch.re_int, lambda m: hexify_int(row, m, arch), row) + + # Let 'original' be 'row' with relocations applied, while we continue + # transforming 'row' into a coarser version that ignores registers and + # immediates. original = row + + while i < len(lines): + reloc_row = lines[i] + if "R_AARCH64_" in reloc_row: + # TODO: handle relocation + pass + elif "R_MIPS_" in reloc_row: + original = process_mips_reloc(reloc_row, original, arch) + elif "R_PPC_" in reloc_row: + original = process_ppc_reloc(reloc_row, original) + else: + break + i += 1 + + normalized_original = normalizer.normalize(mnemonic, original) if skip_next: skip_next = False row = "" mnemonic = "" - if mnemonic in branch_likely_instructions: + if mnemonic in arch.branch_likely_instructions: skip_next = True - row = re.sub(re_reg, "", row) - row = re.sub(re_sprel, "addr(sp)", row) + row = re.sub(arch.re_reg, "", row) + row = re.sub(arch.re_sprel, "addr(sp)", row) row_with_imm = row - if mnemonic in instructions_with_address_immediates: + if mnemonic in arch.instructions_with_address_immediates: row = row.strip() row, _ = split_off_branch(row) row += "" else: - row = normalize_imms(row) + row = normalize_imms(row, arch) branch_target = None - if mnemonic in branch_instructions: - target = row_parts[1].strip().split(",")[-1] - if mnemonic in branch_likely_instructions: - target = hex(int(target, 16) - 4)[2:] - branch_target = target.strip() + if mnemonic in arch.branch_instructions: + target = int(row_parts[1].strip().split(",")[-1], 16) + if mnemonic in arch.branch_likely_instructions: + target -= 4 + branch_target = hex(target)[2:] output.append( Line( mnemonic=mnemonic, diff_row=row, original=original, + normalized_original=normalized_original, line_num=line_num, branch_target=branch_target, source_lines=source_lines, @@ -696,7 +1453,7 @@ def process(lines): ) source_lines = [] - if args.stop_jrra and mnemonic == "jr" and row_parts[1].strip() == "ra": + if config.stop_jrra and mnemonic == "jr" and row_parts[1].strip() == "ra": stop_after_delay_slot = True elif stop_after_delay_slot: break @@ -704,41 +1461,20 @@ def process(lines): return output -def format_single_line_diff(line1, line2, column_width): - return f"{ansi_ljust(line1,column_width)}{line2}" +def normalize_imms(row: str, arch: ArchSettings) -> str: + return re.sub(arch.re_imm, "", row) -class SymbolColorer: - def __init__(self, base_index): - self.color_index = base_index - self.symbol_colors = {} - - def color_symbol(self, s, t=None): - try: - color = self.symbol_colors[s] - except: - color = COLOR_ROTATION[self.color_index % len(COLOR_ROTATION)] - self.color_index += 1 - self.symbol_colors[s] = color - t = t or s - return f"{color}{t}{Fore.RESET}" +def normalize_stack(row: str, arch: ArchSettings) -> str: + return re.sub(arch.re_sprel, "addr(sp)", row) -def maybe_normalize_large_imms(row): - if args.ignore_large_imms: - row = re.sub(re_large_imm, "", row) - return row +def imm_matches_everything(row: str, arch: ArchSettings) -> bool: + # (this should probably be arch-specific) + return "(." in row -def normalize_imms(row): - return re.sub(re_imm, "", row) - - -def normalize_stack(row): - return re.sub(re_sprel, "addr(sp)", row) - - -def split_off_branch(line): +def split_off_branch(line: str) -> Tuple[str, str]: parts = line.split(",") if len(parts) < 2: parts = line.split(None, 1) @@ -746,39 +1482,18 @@ def split_off_branch(line): return line[:off], line[off:] -def color_imms(out1, out2): - g1 = [] - g2 = [] - re.sub(re_imm, lambda s: g1.append(s.group()), out1) - re.sub(re_imm, lambda s: g2.append(s.group()), out2) - if len(g1) == len(g2): - diffs = [x != y for (x, y) in zip(g1, g2)] - it = iter(diffs) - - def maybe_color(s): - return f"{Fore.LIGHTBLUE_EX}{s}{Style.RESET_ALL}" if next(it) else s - - out1 = re.sub(re_imm, lambda s: maybe_color(s.group()), out1) - it = iter(diffs) - out2 = re.sub(re_imm, lambda s: maybe_color(s.group()), out2) - return out1, out2 - - -def color_branch_imms(br1, br2): - if br1 != br2: - br1 = f"{Fore.LIGHTBLUE_EX}{br1}{Style.RESET_ALL}" - br2 = f"{Fore.LIGHTBLUE_EX}{br2}{Style.RESET_ALL}" - return br1, br2 - - -def diff_sequences_difflib(seq1, seq2): +def diff_sequences_difflib( + seq1: List[str], seq2: List[str] +) -> List[Tuple[str, int, int, int, int]]: differ = difflib.SequenceMatcher(a=seq1, b=seq2, autojunk=False) return differ.get_opcodes() -def diff_sequences(seq1, seq2): +def diff_sequences( + seq1: List[str], seq2: List[str], algorithm: str +) -> List[Tuple[str, int, int, int, int]]: if ( - args.algorithm != "levenshtein" + algorithm != "levenshtein" or len(seq1) * len(seq2) > 4 * 10 ** 8 or len(seq1) + len(seq2) >= 0x110000 ): @@ -786,9 +1501,9 @@ def diff_sequences(seq1, seq2): # The Levenshtein library assumes that we compare strings, not lists. Convert. # (Per the check above we know we have fewer than 0x110000 unique elements, so chr() works.) - remapping = {} + remapping: Dict[str, str] = {} - def remap(seq): + def remap(seq: List[str]) -> str: seq = seq[:] for i in range(len(seq)): val = remapping.get(seq[i]) @@ -798,58 +1513,23 @@ def diff_sequences(seq1, seq2): seq[i] = val return "".join(seq) - seq1 = remap(seq1) - seq2 = remap(seq2) - return Levenshtein.opcodes(seq1, seq2) + rem1 = remap(seq1) + rem2 = remap(seq2) + import Levenshtein # type: ignore + + return Levenshtein.opcodes(rem1, rem2) # type: ignore -class OutputLine: - base: Optional[str] - fmt2: str - key2: str - - def __init__(self, base: Optional[str], fmt2: str, key2: str) -> None: - self.base = base - self.fmt2 = fmt2 - self.key2 = key2 - - def __eq__(self, other: object) -> bool: - if not isinstance(other, OutputLine): - return NotImplemented - return self.key2 == other.key2 - - def __hash__(self) -> int: - return hash(self.key2) - - -def do_diff(basedump: str, mydump: str) -> List[OutputLine]: - output: List[OutputLine] = [] - - lines1 = process(basedump.split("\n")) - lines2 = process(mydump.split("\n")) - - sc1 = SymbolColorer(0) - sc2 = SymbolColorer(0) - sc3 = SymbolColorer(4) - sc4 = SymbolColorer(4) - sc5 = SymbolColorer(0) - sc6 = SymbolColorer(0) - bts1: Set[str] = set() - bts2: Set[str] = set() - - if args.show_branches: - for (lines, btset, sc) in [ - (lines1, bts1, sc5), - (lines2, bts2, sc6), - ]: - for line in lines: - bt = line.branch_target - if bt is not None: - btset.add(bt + ":") - sc.color_symbol(bt + ":") - +def diff_lines( + lines1: List[Line], + lines2: List[Line], + algorithm: str, +) -> List[Tuple[Optional[Line], Optional[Line]]]: + ret = [] for (tag, i1, i2, j1, j2) in diff_sequences( - [line.mnemonic for line in lines1], [line.mnemonic for line in lines2] + [line.mnemonic for line in lines1], + [line.mnemonic for line in lines2], + algorithm, ): for line1, line2 in itertools.zip_longest(lines1[i1:i2], lines2[j1:j2]): if tag == "replace": @@ -861,133 +1541,215 @@ def do_diff(basedump: str, mydump: str) -> List[OutputLine]: assert line1 is None elif tag == "delete": assert line2 is None + ret.append((line1, line2)) - line_color1 = line_color2 = sym_color = Fore.RESET - line_prefix = " " - if line1 and line2 and line1.diff_row == line2.diff_row: - if maybe_normalize_large_imms( - line1.original - ) == maybe_normalize_large_imms(line2.original): - out1 = line1.original - out2 = line2.original - elif line1.diff_row == "": - out1 = f"{Style.BRIGHT}{Fore.LIGHTBLACK_EX}{line1.original}" - out2 = f"{Style.BRIGHT}{Fore.LIGHTBLACK_EX}{line2.original}" + return ret + + +@dataclass(frozen=True) +class OutputLine: + base: Optional[Text] = field(compare=False) + fmt2: Text = field(compare=False) + key2: Optional[str] + boring: bool = field(compare=False) + + +def do_diff(basedump: str, mydump: str, config: Config) -> List[OutputLine]: + if config.source: + import cxxfilt # type: ignore + arch = config.arch + fmt = config.formatter + output: List[OutputLine] = [] + + lines1 = process(basedump.split("\n"), config) + lines2 = process(mydump.split("\n"), config) + + sc1 = symbol_formatter("base-reg", 0) + sc2 = symbol_formatter("my-reg", 0) + sc3 = symbol_formatter("base-stack", 4) + sc4 = symbol_formatter("my-stack", 4) + sc5 = symbol_formatter("base-branch", 0) + sc6 = symbol_formatter("my-branch", 0) + bts1: Set[str] = set() + bts2: Set[str] = set() + + if config.show_branches: + for (lines, btset, sc) in [ + (lines1, bts1, sc5), + (lines2, bts2, sc6), + ]: + for line in lines: + bt = line.branch_target + if bt is not None: + text = f"{bt}:" + btset.add(text) + sc(text) + + for (line1, line2) in diff_lines(lines1, lines2, config.algorithm): + line_color1 = line_color2 = sym_color = BasicFormat.NONE + line_prefix = " " + out1 = Text() if not line1 else Text(pad_mnemonic(line1.original)) + out2 = Text() if not line2 else Text(pad_mnemonic(line2.original)) + if line1 and line2 and line1.diff_row == line2.diff_row: + if line1.normalized_original == line2.normalized_original: + pass + elif line1.diff_row == "": + out1 = out1.reformat(BasicFormat.DELAY_SLOT) + out2 = out2.reformat(BasicFormat.DELAY_SLOT) + else: + mnemonic = line1.original.split()[0] + branchless1, branch1 = out1.plain(), "" + branchless2, branch2 = out2.plain(), "" + if mnemonic in arch.instructions_with_address_immediates: + branchless1, branch1 = split_off_branch(branchless1) + branchless2, branch2 = split_off_branch(branchless2) + + out1 = Text(branchless1) + out2 = Text(branchless2) + out1, out2 = format_fields( + arch.re_imm, out1, out2, lambda _: BasicFormat.IMMEDIATE + ) + + same_relative_target = False + if line1.branch_target is not None and line2.branch_target is not None: + relative_target1 = eval_line_num( + line1.branch_target + ) - eval_line_num(line1.line_num) + relative_target2 = eval_line_num( + line2.branch_target + ) - eval_line_num(line2.line_num) + same_relative_target = relative_target1 == relative_target2 + + if normalize_imms(branchless1, arch) == normalize_imms( + branchless2, arch + ): + if imm_matches_everything(branchless2, arch): + out1 = out1.reformat(BasicFormat.NONE) + out2 = out2.reformat(BasicFormat.NONE) + elif not same_relative_target: + # only imms differences + sym_color = BasicFormat.IMMEDIATE + line_prefix = "i" else: - mnemonic = line1.original.split()[0] - out1, out2 = line1.original, line2.original - branch1 = branch2 = "" - if mnemonic in instructions_with_address_immediates: - out1, branch1 = split_off_branch(line1.original) - out2, branch2 = split_off_branch(line2.original) - branchless1 = out1 - branchless2 = out2 - out1, out2 = color_imms(out1, out2) - - same_relative_target = False - if line1.branch_target is not None and line2.branch_target is not None: - relative_target1 = eval_line_num(line1.branch_target) - eval_line_num(line1.line_num) - relative_target2 = eval_line_num(line2.branch_target) - eval_line_num(line2.line_num) - same_relative_target = relative_target1 == relative_target2 - - if not same_relative_target: - branch1, branch2 = color_branch_imms(branch1, branch2) - - out1 += branch1 - out2 += branch2 - if normalize_imms(branchless1) == normalize_imms(branchless2): - if not same_relative_target: - # only imms differences - sym_color = Fore.LIGHTBLUE_EX - line_prefix = "i" + out1, out2 = format_fields(arch.re_sprel, out1, out2, sc3, sc4) + if normalize_stack(branchless1, arch) == normalize_stack( + branchless2, arch + ): + # only stack differences (luckily stack and imm + # differences can't be combined in MIPS, so we + # don't have to think about that case) + sym_color = BasicFormat.STACK + line_prefix = "s" else: - out1 = re.sub( - re_sprel, lambda s: sc3.color_symbol(s.group()), out1, - ) - out2 = re.sub( - re_sprel, lambda s: sc4.color_symbol(s.group()), out2, - ) - if normalize_stack(branchless1) == normalize_stack(branchless2): - # only stack differences (luckily stack and imm - # differences can't be combined in MIPS, so we - # don't have to think about that case) - sym_color = Fore.YELLOW - line_prefix = "s" - else: - # regs differences and maybe imms as well - out1 = re.sub( - re_reg, lambda s: sc1.color_symbol(s.group()), out1 + # regs differences and maybe imms as well + out1, out2 = format_fields(arch.re_reg, out1, out2, sc1, sc2) + line_color1 = line_color2 = sym_color = BasicFormat.REGISTER + line_prefix = "r" + + if same_relative_target or branch1 == branch2: + branch_imm_fmt = BasicFormat.NONE + else: + branch_imm_fmt = BasicFormat.IMMEDIATE + out1 += Text(branch1, branch_imm_fmt) + out2 += Text(branch2, branch_imm_fmt) + elif line1 and line2: + line_prefix = "|" + line_color1 = line_color2 = sym_color = BasicFormat.DIFF_CHANGE + out1 = out1.reformat(line_color1) + out2 = out2.reformat(line_color2) + elif line1: + line_prefix = "<" + line_color1 = sym_color = BasicFormat.DIFF_REMOVE + out1 = out1.reformat(line_color1) + out2 = Text() + elif line2: + line_prefix = ">" + line_color2 = sym_color = BasicFormat.DIFF_ADD + out1 = Text() + out2 = out2.reformat(line_color2) + + if config.source and line2 and line2.comment: + out2 += f" {line2.comment}" + + def format_part( + out: Text, + line: Optional[Line], + line_color: Format, + btset: Set[str], + sc: FormatFunction, + ) -> Optional[Text]: + if line is None: + return None + in_arrow = Text(" ") + out_arrow = Text() + if config.show_branches: + if line.line_num in btset: + in_arrow = Text("~>", sc(line.line_num)) + if line.branch_target is not None: + out_arrow = " " + Text("~>", sc(line.branch_target + ":")) + return ( + Text(line.line_num, line_color) + " " + in_arrow + " " + out + out_arrow + ) + + part1 = format_part(out1, line1, line_color1, bts1, sc5) + part2 = format_part(out2, line2, line_color2, bts2, sc6) + + if line2: + for source_line in line2.source_lines: + line_format = BasicFormat.SOURCE_OTHER + if config.source_old_binutils: + if source_line and re.fullmatch(".*\.c(?:pp)?:\d+", source_line): + line_format = BasicFormat.SOURCE_FILENAME + elif source_line and source_line.endswith("():"): + line_format = BasicFormat.SOURCE_FUNCTION + try: + source_line = cxxfilt.demangle( + source_line[:-3], external_only=False ) - out2 = re.sub( - re_reg, lambda s: sc2.color_symbol(s.group()), out2 - ) - line_color1 = line_color2 = sym_color = Fore.YELLOW - line_prefix = "r" - elif line1 and line2: - line_prefix = "|" - line_color1 = Fore.LIGHTBLUE_EX - line_color2 = Fore.LIGHTBLUE_EX - sym_color = Fore.LIGHTBLUE_EX - out1 = line1.original - out2 = line2.original - elif line1: - line_prefix = "<" - line_color1 = sym_color = Fore.RED - out1 = line1.original - out2 = "" - elif line2: - line_prefix = ">" - line_color2 = sym_color = Fore.GREEN - out1 = "" - out2 = line2.original - - if args.source and line2 and line2.comment: - out2 += f" {line2.comment}" - - def format_part(out: str, line: Optional[Line], line_color: str, btset: Set[str], sc: SymbolColorer) -> Optional[str]: - if line is None: - return None - in_arrow = " " - out_arrow = "" - if args.show_branches: - if line.line_num in btset: - in_arrow = sc.color_symbol(line.line_num, "~>") + line_color - if line.branch_target is not None: - out_arrow = " " + sc.color_symbol(line.branch_target + ":", "~>") - out = pad_mnemonic(out) - return f"{line_color}{line.line_num} {in_arrow} {out}{Style.RESET_ALL}{out_arrow}" - - part1 = format_part(out1, line1, line_color1, bts1, sc5) - part2 = format_part(out2, line2, line_color2, bts2, sc6) - key2 = line2.original if line2 else "" - - mid = f"{sym_color}{line_prefix}" - - if line2: - for source_line in line2.source_lines: - color = Style.DIM + except: + pass + else: # File names and function names - if source_line and source_line[0] != "|": - color += Style.BRIGHT + if source_line and source_line[0] != "│": + line_format = BasicFormat.SOURCE_FILENAME # Function names if source_line.endswith("():"): - # Underline. Colorama does not provide this feature, unfortunately. - color += "\u001b[4m" + line_format = BasicFormat.SOURCE_FUNCTION try: source_line = cxxfilt.demangle( source_line[:-3], external_only=False ) except: pass - output.append(OutputLine(None, f" {color}{source_line}{Style.RESET_ALL}", source_line)) + output.append( + OutputLine( + None, + " " + Text(source_line, line_format), + source_line, + True, + ) + ) - fmt2 = mid + " " + (part2 or "") - output.append(OutputLine(part1, fmt2, key2)) + key2 = line2.original if line2 else None + boring = False + if line_prefix == " ": + # Canonicalize matching lines to have an empty string as key, to + # ensure they are treated as the same when three-way diffing. This + # matters for branches that match only relatively. + key2 = "" + boring = True + elif config.compress and config.compress.same_instr and line_prefix in "irs": + boring = True + fmt2 = Text(line_prefix, sym_color) + " " + (part2 or Text()) + output.append(OutputLine(part1, fmt2, key2, boring)) return output def chunk_diff(diff: List[OutputLine]) -> List[Union[List[OutputLine], OutputLine]]: + """Chunk a diff into an alternating list like A B A B ... A, where: + * A is a List[OutputLine] of insertions, + * B is a single non-insertion OutputLine, with .base != None.""" cur_right: List[OutputLine] = [] chunks: List[Union[List[OutputLine], OutputLine]] = [] for output_line in diff: @@ -1001,12 +1763,46 @@ def chunk_diff(diff: List[OutputLine]) -> List[Union[List[OutputLine], OutputLin return chunks -def format_diff(old_diff: List[OutputLine], new_diff: List[OutputLine]) -> Tuple[str, List[str]]: +def compress_matching( + li: List[Tuple[Tuple[Text, ...], bool]], context: int +) -> List[Tuple[Text, ...]]: + ret: List[Tuple[Text, ...]] = [] + matching_streak: List[Tuple[Text, ...]] = [] + context = max(context, 0) + + def flush_matching() -> None: + if len(matching_streak) <= 2 * context + 1: + ret.extend(matching_streak) + else: + ret.extend(matching_streak[:context]) + skipped = len(matching_streak) - 2 * context + filler = Text(f"<{skipped} lines>", BasicFormat.SOURCE_OTHER) + columns = len(matching_streak[0]) + ret.append(tuple([filler] + [Text()] * (columns - 1))) + if context > 0: + ret.extend(matching_streak[-context:]) + matching_streak.clear() + + for (line, matching) in li: + if matching: + matching_streak.append(line) + else: + flush_matching() + ret.append(line) + + flush_matching() + return ret + + +def format_diff( + old_diff: List[OutputLine], new_diff: List[OutputLine], config: Config +) -> Tuple[Optional[Tuple[Text, ...]], List[Tuple[Text, ...]]]: + fmt = config.formatter old_chunks = chunk_diff(old_diff) new_chunks = chunk_diff(new_diff) - output: List[Tuple[str, OutputLine, OutputLine]] = [] + output: List[Tuple[Text, OutputLine, OutputLine]] = [] assert len(old_chunks) == len(new_chunks), "same target" - empty = OutputLine("", "", "") + empty = OutputLine(Text(), Text(), None, True) for old_chunk, new_chunk in zip(old_chunks, new_chunks): if isinstance(old_chunk, list): assert isinstance(new_chunk, list) @@ -1018,74 +1814,92 @@ def format_diff(old_diff: List[OutputLine], new_diff: List[OutputLine]) -> Tuple for (tag, i1, i2, j1, j2) in differ.get_opcodes(): if tag in ["equal", "replace"]: for i, j in zip(range(i1, i2), range(j1, j2)): - output.append(("", old_chunk[i], new_chunk[j])) - elif tag == "insert": - for j in range(j1, j2): - output.append(("", empty, new_chunk[j])) - else: - for i in range(i1, i2): - output.append(("", old_chunk[i], empty)) + output.append((Text(), old_chunk[i], new_chunk[j])) + if tag in ["insert", "replace"]: + for j in range(j1 + i2 - i1, j2): + output.append((Text(), empty, new_chunk[j])) + if tag in ["delete", "replace"]: + for i in range(i1 + j2 - j1, i2): + output.append((Text(), old_chunk[i], empty)) else: assert isinstance(new_chunk, OutputLine) + assert new_chunk.base # old_chunk.base and new_chunk.base have the same text since # both diffs are based on the same target, but they might # differ in color. Use the new version. - output.append((new_chunk.base or "", old_chunk, new_chunk)) + output.append((new_chunk.base, old_chunk, new_chunk)) # TODO: status line, with e.g. approximate permuter score? - width = args.column_width - if args.threeway: - header_line = "TARGET".ljust(width) + " CURRENT".ljust(width) + " PREVIOUS" + header_line: Optional[Tuple[Text, ...]] + diff_lines: List[Tuple[Tuple[Text, ...], bool]] + if config.threeway: + header_line = (Text("TARGET"), Text(" CURRENT"), Text(" PREVIOUS")) diff_lines = [ - ansi_ljust(base, width) - + ansi_ljust(new.fmt2, width) - + (old.fmt2 or "-" if old != new else "") + ( + ( + base, + new.fmt2, + old.fmt2 or Text("-") if old != new else Text(), + ), + new.boring, + ) for (base, old, new) in output ] else: - header_line = "" + header_line = None diff_lines = [ - ansi_ljust(base, width) + new.fmt2 + ((base, new.fmt2), new.boring) for (base, old, new) in output - if base or new.key2 + if base or new.key2 is not None ] - return header_line, diff_lines + if config.compress: + ret_lines = compress_matching(diff_lines, config.compress.context) + else: + ret_lines = [line for line, _ in diff_lines] + return header_line, ret_lines -def debounced_fs_watch(targets, outq, debounce_delay): +def debounced_fs_watch( + targets: List[str], + outq: "queue.Queue[Optional[float]]", + config: Config, + project: ProjectSettings, +) -> None: import watchdog.events # type: ignore import watchdog.observers # type: ignore - class WatchEventHandler(watchdog.events.FileSystemEventHandler): - def __init__(self, queue, file_targets): + class WatchEventHandler(watchdog.events.FileSystemEventHandler): # type: ignore + def __init__( + self, queue: "queue.Queue[float]", file_targets: List[str] + ) -> None: self.queue = queue self.file_targets = file_targets - def on_modified(self, ev): + def on_modified(self, ev: object) -> None: if isinstance(ev, watchdog.events.FileModifiedEvent): self.changed(ev.src_path) - def on_moved(self, ev): + def on_moved(self, ev: object) -> None: if isinstance(ev, watchdog.events.FileMovedEvent): self.changed(ev.dest_path) - def should_notify(self, path): + def should_notify(self, path: str) -> bool: for target in self.file_targets: - if path == target: + if os.path.normpath(path) == target: return True - if args.make and any( - path.endswith(suffix) for suffix in FS_WATCH_EXTENSIONS + if config.make and any( + path.endswith(suffix) for suffix in project.source_extensions ): return True return False - def changed(self, path): + def changed(self, path: str) -> None: if self.should_notify(path): self.queue.put(time.time()) - def debounce_thread(): - listenq = queue.Queue() - file_targets = [] + def debounce_thread() -> NoReturn: + listenq: "queue.Queue[float]" = queue.Queue() + file_targets: List[str] = [] event_handler = WatchEventHandler(listenq, file_targets) observer = watchdog.observers.Observer() observed = set() @@ -1093,7 +1907,7 @@ def debounced_fs_watch(targets, outq, debounce_delay): if os.path.isdir(target): observer.schedule(event_handler, target, recursive=True) else: - file_targets.append(target) + file_targets.append(os.path.normpath(target)) target = os.path.dirname(target) or "." if target not in observed: observed.add(target) @@ -1103,7 +1917,7 @@ def debounced_fs_watch(targets, outq, debounce_delay): t = listenq.get() more = True while more: - delay = t + debounce_delay - time.time() + delay = t + DEBOUNCE_DELAY - time.time() if delay > 0: time.sleep(delay) # consume entire queue @@ -1121,22 +1935,36 @@ def debounced_fs_watch(targets, outq, debounce_delay): class Display: - def __init__(self, basedump, mydump): + basedump: str + mydump: str + config: Config + emsg: Optional[str] + last_diff_output: Optional[List[OutputLine]] + pending_update: Optional[Tuple[str, bool]] + ready_queue: "queue.Queue[None]" + watch_queue: "queue.Queue[Optional[float]]" + less_proc: "Optional[subprocess.Popen[bytes]]" + + def __init__(self, basedump: str, mydump: str, config: Config) -> None: + self.config = config self.basedump = basedump self.mydump = mydump self.emsg = None self.last_diff_output = None - def run_less(self): + def run_diff(self) -> str: if self.emsg is not None: - output = self.emsg - else: - diff_output = do_diff(self.basedump, self.mydump) - last_diff_output = self.last_diff_output or diff_output + return self.emsg + + diff_output = do_diff(self.basedump, self.mydump, self.config) + last_diff_output = self.last_diff_output or diff_output + if self.config.threeway != "base" or not self.last_diff_output: self.last_diff_output = diff_output - header, diff_lines = format_diff(last_diff_output, diff_output) - header_lines = [header] if header else [] - output = "\n".join(header_lines + diff_lines[args.skip_lines :]) + header, diff_lines = format_diff(last_diff_output, diff_output, self.config) + return self.config.formatter.table(header, diff_lines[self.config.skip_lines :]) + + def run_less(self) -> "Tuple[subprocess.Popen[bytes], subprocess.Popen[bytes]]": + output = self.run_diff() # Pipe the output through 'tail' and only then to less, to ensure the # write call doesn't block. ('tail' has to buffer all its input before @@ -1146,17 +1974,19 @@ class Display: BUFFER_CMD, stdin=subprocess.PIPE, stdout=subprocess.PIPE ) less_proc = subprocess.Popen(LESS_CMD, stdin=buffer_proc.stdout) + assert buffer_proc.stdin + assert buffer_proc.stdout buffer_proc.stdin.write(output.encode()) buffer_proc.stdin.close() buffer_proc.stdout.close() return (buffer_proc, less_proc) - def run_sync(self): + def run_sync(self) -> None: proca, procb = self.run_less() procb.wait() proca.wait() - def run_async(self, watch_queue): + def run_async(self, watch_queue: "queue.Queue[Optional[float]]") -> None: self.watch_queue = watch_queue self.ready_queue = queue.Queue() self.pending_update = None @@ -1164,10 +1994,10 @@ class Display: dthread.start() self.ready_queue.get() - def display_thread(self): + def display_thread(self) -> None: proca, procb = self.run_less() self.less_proc = procb - self.ready_queue.put(0) + self.ready_queue.put(None) while True: ret = procb.wait() proca.wait() @@ -1186,19 +2016,19 @@ class Display: self.emsg = msg proca, procb = self.run_less() self.less_proc = procb - self.ready_queue.put(0) + self.ready_queue.put(None) else: # terminated by user, or killed self.watch_queue.put(None) - self.ready_queue.put(0) + self.ready_queue.put(None) break - def progress(self, msg): + def progress(self, msg: str) -> None: # Write message to top-left corner sys.stdout.write("\x1b7\x1b[1;1f{}\x1b8".format(msg + " ")) sys.stdout.flush() - def update(self, text, error): + def update(self, text: str, error: bool) -> None: if not error and not self.emsg and text == self.mydump: self.progress("Unchanged. ") return @@ -1208,23 +2038,55 @@ class Display: self.less_proc.kill() self.ready_queue.get() - def terminate(self): + def terminate(self) -> None: if not self.less_proc: return self.less_proc.kill() self.ready_queue.get() -def main(): +def main() -> None: + args = parser.parse_args() + + # Apply project-specific configuration. + settings: Dict[str, Any] = {} + diff_settings.apply(settings, args) # type: ignore + project = create_project_settings(settings) + + config = create_config(args, project) + + if config.algorithm == "levenshtein": + try: + import Levenshtein + except ModuleNotFoundError as e: + fail(MISSING_PREREQUISITES.format(e.name)) + + if config.source: + try: + import cxxfilt + except ModuleNotFoundError as e: + fail(MISSING_PREREQUISITES.format(e.name)) + + if config.threeway and not args.watch: + fail("Threeway diffing requires -w.") + if args.diff_elf_symbol: - make_target, basecmd, mycmd = dump_elf() - elif args.diff_obj: - make_target, basecmd, mycmd = dump_objfile() + make_target, basecmd, mycmd = dump_elf( + args.start, args.end, args.diff_elf_symbol, config, project + ) + elif config.diff_obj: + make_target, basecmd, mycmd = dump_objfile( + args.start, args.end, config, project + ) else: - make_target, basecmd, mycmd = dump_binary() + make_target, basecmd, mycmd = dump_binary(args.start, args.end, config, project) + + map_build_target_fn = getattr(diff_settings, "map_build_target", None) + if map_build_target_fn: + make_target = map_build_target_fn(make_target=make_target) if args.write_asm is not None: - mydump = run_objdump(mycmd) + mydump = run_objdump(mycmd, config, project) with open(args.write_asm, "w") as f: f.write(mydump) print(f"Wrote assembly to {args.write_asm}.") @@ -1234,34 +2096,40 @@ def main(): with open(args.base_asm) as f: basedump = f.read() else: - basedump = run_objdump(basecmd) + basedump = run_objdump(basecmd, config, project) - mydump = run_objdump(mycmd) + mydump = run_objdump(mycmd, config, project) - display = Display(basedump, mydump) + display = Display(basedump, mydump, config) - if not args.watch: + if args.no_pager or args.format == "html": + print(display.run_diff()) + elif not args.watch: display.run_sync() else: if not args.make: yn = input( - "Warning: watch-mode (-w) enabled without auto-make (-m). You will have to run make manually. Ok? (Y/n) " + "Warning: watch-mode (-w) enabled without auto-make (-m). " + "You will have to run make manually. Ok? (Y/n) " ) if yn.lower() == "n": return if args.make: watch_sources = None - if hasattr(diff_settings, "watch_sources_for_target"): - watch_sources = diff_settings.watch_sources_for_target(make_target) - watch_sources = watch_sources or source_directories + watch_sources_for_target_fn = getattr( + diff_settings, "watch_sources_for_target", None + ) + if watch_sources_for_target_fn: + watch_sources = watch_sources_for_target_fn(make_target) + watch_sources = watch_sources or project.source_directories if not watch_sources: fail("Missing source_directories config, don't know what to watch.") else: watch_sources = [make_target] - q = queue.Queue() - debounced_fs_watch(watch_sources, q, DEBOUNCE_DELAY) + q: "queue.Queue[Optional[float]]" = queue.Queue() + debounced_fs_watch(watch_sources, q, config, project) display.run_async(q) - last_build = 0 + last_build = 0.0 try: while True: t = q.get() @@ -1272,7 +2140,7 @@ def main(): last_build = time.time() if args.make: display.progress("Building...") - ret = run_make(make_target, capture_output=True) + ret = run_make_capture_output(make_target, project) if ret.returncode != 0: display.update( ret.stderr.decode("utf-8-sig", "replace") @@ -1280,10 +2148,11 @@ def main(): error=True, ) continue - mydump = run_objdump(mycmd) + mydump = run_objdump(mycmd, config, project) display.update(mydump, error=False) except KeyboardInterrupt: display.terminate() -main() +if __name__ == "__main__": + main() diff --git a/tools/asm-differ/diff_settings.py b/tools/asm-differ/diff_settings.py index b9d88fe6cf..456ffdfdcc 100644 --- a/tools/asm-differ/diff_settings.py +++ b/tools/asm-differ/diff_settings.py @@ -1,7 +1,10 @@ -#!/usr/bin/env python3 - def apply(config, args): - config['baseimg'] = 'target.bin' - config['myimg'] = 'source.bin' - config['mapfile'] = 'build.map' - config['source_directories'] = ['.'] + config["baseimg"] = "target.bin" + config["myimg"] = "source.bin" + config["mapfile"] = "build.map" + config["source_directories"] = ["."] + # config["arch"] = "mips" + # config["map_format"] = "gnu" # gnu or mw + # config["mw_build_dir"] = "build/" # only needed for mw map format + # config["makeflags"] = [] + # config["objdump_executable"] = "" diff --git a/tools/asm-differ/mypy.ini b/tools/asm-differ/mypy.ini index f7a01b3428..c6b3bba20a 100644 --- a/tools/asm-differ/mypy.ini +++ b/tools/asm-differ/mypy.ini @@ -11,6 +11,7 @@ warn_redundant_casts = True warn_return_any = True warn_unused_ignores = True python_version = 3.6 +files = diff.py [mypy-diff_settings] ignore_errors = True diff --git a/tools/asm-differ/screenshot.png b/tools/asm-differ/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..3230555328cd9acdf388ce1626415ceb78760535 GIT binary patch literal 99842 zcmd431yG#Zx~@w?g1b9Gg1b8j?(Ul4?yezdaCdiicXxMpXxtjt+nIC8T?|I+RzvN^@;bCxKz`(%Z#l?gaz`)*jgWgV15TGr};=9eDAJ8^p z>h@q@2))1G@8YNtaKXSnfr$%!RdPu^S#woWo_ig*yyF8fki`5Th6sk)PK;JWNis|Z zsu^+aDCNg#=!G|=0-dGpsRFN!*d=R5V@HWtjq$!zFb{tE^mFtRRO-tUcq%diOwffV zZ^T4S^`iOdq5b%}ds!X2tgI|Lksp!(6cH8H*o@VW-y=UH*`6J5CQL_(Z*_qu*{tga7bR<#a{)OOf4fWVsTgfN-|O~^HU_AgLGLcgL@F$H4n zWr;G-r0}(Es%!V>?c=6wf1ONQ`%w_<1L{LxpY?I@Q-aQ`neWb-rQz3gs#AUH&UsU< zY)xdWjZP#W(P)lq3pRdOxv<-GjZ?%dMuI)7(gO+PE$QRg!DbZbrc6Ovt=^Fq766-5)c zGoJ(ITpZQBar&%xo?I~-vuyJnJBZ*?jigQ@u}@?ogV3@)qEfx%#l}N|r64mG?-`8F zC|c7jt0s+N&^*JzapP;uL>btv30uhMTA6lvAKR9Z$Y&$&h6d2+Y>bJ=mZsGoa5s;2HN!v8s<;Ip1Pf~_@MNjk&d(l7Sf(N7P_`YCW+gYaKmom&3px9qBtjw3|*?;4C zWYSRGs%><_9@!+@vPfgbZQ1gw9dAhe97l8AtHUX3V!}u0`Y2W}g)=yRVqpgc^@(8# zyTX-TaYA|I>_d|(sgQU~Dt+Q^E9jyoI)Is(F}N?#8#r6M#|}oXTZaGM8>l!KbYZx+ zWFc2-%4&qLqa(Fjac)$@qY-EyBh#wN){174%ry zAqm9t3_bi5842voGw*BplO2k@d(jWyeuN{uoyPw$4`s@$wLCykf6TeBCMWjPzM z%;z6e7*CsAvzNXpNt)f;I+bb;H+x3iZG0TgDY@m+4`3xfTljS^I7TWLRB1HU(Z zcp=R2bPm{VtswGqVfo{lV~W`6#Hw<|7CWS}pz0RHc)XngJ_zMzS9$roDh95$b*p|w zq)2{m^ou2Tw9Oy(K1l~rqilZCk-}CTnLCkx8!9EaE=xK^^?Fm6<1LCdA&rsbu6}vZ$nN3VBk^*_{vTVn6%)|Ofsb!l z-*oP1^F5~!k3DN19EQGb@0und49n(;=)6EeiR zlh&o_FGPZ(o|0;5UT~(mr&Fkd)XGI0nJ|x)-?~F^KIEw+NSN(%%0NP(pkay0O>$;W z2XFC3WM6#KN9QK&m-571d0|efRqK`yQuxeijWy)jBR(EtCmZvZzC4o6}C?EOp=3f`~j66SNK z#;z<<$zn6Qs?!WPyIE2h0xrRMcWcJ|H3KjBP(j@yx?3CDj;;G`S;^`WMZdxlosn?rLJheNxys8Q zPKs6=j$^7Jcn6;C7;TxGO38>&4YAW9A*H3?*-)p=Q1gG6$fkec4&SV4pq(Gije6=GAa zLLusT?mQz}LF+S#H+lJY5*k(8Id^*^zRg;|9^GOICQy+X2xWW+ zJU5l{#Xfg}3w-33rRTArnXJ3Y^4UYkR-B!#82O^HkS?XWikPq>%aF!rIAl(BsmVW< zb}Pm>&}s*-W403IT%z$kHco1EFXov=AD(~Xj6HvGXG{KLS{pMVy{Y@gRZY0p5O>Ur z>+Z}KqTZN)%+r-wyVKZKacHABdkz z z1OUA2+f#o)ph6FB9W$7AY&~OQ^j%ruo`G^MYVCbEp(GD?tM~w*glB3_-Ij17 zPeeaNa)%8rjTvzxe;y?|TTOf%SImy@O*bx;o*)`%7!9gp6RJ5L*qk2k-#oX6AkqI6 zG5L@7aU;rK%*-7OyKCj|InW&*sS-IGG6&pe)GCie*{sq93x)#-kCx3&u{vy!?J?<$ z>=u%-_GoA3jZu!<@x=v!*|m$4o>_fOA%q#N-+Ry0GcInr8i~WW=+<`tLveMtt0Rj| zw1gGw!_GN4JgxlKQzywUlp!*Ula|xuwe%MpsM#i@0C*D4jE?H~62mgyyXaK!+k?ph z8(x}^u}U}wJcrv#D6h$V?y9MS2#EW?Fz=)o`}1C0@)7LGAJ0$kE0(+G7S*!hz2d?V zCeB`pkQu@d-^_+jgsr!409xURbw-O98hD8vku!%p*H(9;%oe&7ak_?!nU7j;5Bl+h z;q;HQ+dm1t0J6PyE9B0>zzMH3ztiEbQi~q&7F<_- zh=eEIW*dVTYk zB=@W=Kt`%*J!5O>>9`MeC_7>-rs8`N^|d%m(aVq>yFHmty1HhwE?6U8I1xE{5_Y|m zPNh1dccOYjM0ZtD-=740%>xNs3{Umv`$Hbismpk!H}J-Pgpb+Ou=|DzmHX@Jy){30 zc(+2?tC-K|knBi23IufUJEQlgMO13DJy^>51M ztgPkx`U4s5MYm2ZLj2iMau4kB7AsSgxqmj9e$%J%AgghThUD&t-%oj4Yfs6{>gSex zXmAr{;Fm-@@xs6a_iKxvXbt8k!u{d03ahU}*or?cY@&U8bd(#;4;2 z{LMIMUe~f^J~ojf__b~U9_-&!Dd_b8Q@EIJRy5@PaCb42Xlh}~?4_d<6brtDd4KMg zWl=E`h>eV~PZwE+yQ>`IvgUWF_9HM8R_(3Khp1L?Z+>Nr z#amATJH4>9=8{7aNxm_%{17sUVXlu*ujg`B zSS3YWC)yp6YgNz&6e_c3?0(3EGDIZZ330~o?n3ciSD$>a*IOsM!<64z5?c^i$7@ZL zeM3?C?T(DBI0cUsEO2MCRzeC_5^AZG|=5AWY`VnN&$A1?k*bZt1(gf zqvARNpEv37bXk%WO%dPfUyq<~@!&Qwkg$w|V;p--r+S0=pJU|U*Dy6ySG7J_h%%@@ zJgZd!x5i}Ve%)X4wm(yno+CuD;$N>-(mHsasmtuKwayJCQ6C4^*w}M$cK%ZQj-{;I zwPIbj1<17Q-B^b)Zm4#WT-OdmRh?`qH;{`+P<}vnDjc5|0#l(I8(XK zgeU=fw9m#L)2uo3b3pYRxQvp?AJ^~G@9S4eCA)){j?*_v;4T?ANV2l%%!G^%1^tqI zG}e)4y^^1Zf1h$KknrB@grDZ+f-s&c&g6D$Arx+T^xP6qZPfai{noqZ#1Qi8)H#<- z$evbP5MGlSEgFs=J8X<`p(wXtZS>B$mX){25^W?sW=qx~+RS{mPa3nQytA++V!)0V z8ZI>J&HSv50B_=NhjvjUz7tYuGFcIy0<&(uQ0RlYm|O zw`ia+DW|RxR^yBEwcDgw9Yh>$27zZ?!M;IM8I@9Xv$53|PrQYlYpkWkZ}PdE3TQMb z2;X;sKBZh+B7_lN_S@cAzn2BfJ5+M!4RBcbl&g*4UG zN@&dXqXaz3{}j&$LtY`HQs{lvDDiuy(XE;djd_XsQM${9B1jk?Cs<1-(hkWTC$==S z1vMO4#Dxbj`?8_IH>`(~@=7?2kAh6^g@2;b*%5UI6e9$9d|6vK_<1KRgGIEpd8C6I z^X!F(-LG7{slL$lA$1Zt|be?VA?tucJ<*ca6_vI$k9 zrz>Frd$9j)M?l>Huhn@87l}Ps&);Kfv4WhT+I$gws$Or|;&K-*7Ij2r)NeKGqzsMt zYg0Z&BxnGnSU(Lt2#Aeif)EAM&RJeTvWSoxUUGPv%Tt}p`)MlqI7thBERpc!-XTHb zLAP_-{??Sy3Lo18>bX9ho*r&FX@G&g~HU^GopaZ_)-w?M{WTADp=;v@^M z#vOjA>6kc5U7Rf=g3)n1_3rM68vB*E13o<@!uGhqM}10;Hn!+-8Px~r;Cf#)8XCM3=1-yM6*`;v986Y&NGwlheuuapE%(k?CG6e4cvDhiR!)lWP%vw}r;xb1!#sIiFMp zI^ChJ7E9e`P#a*O1raQ4i5Ge$-uZJmrbQHbsop;`{rs}a#DCPcX3_C9{f1sb+n=#M zOM?|Rly$C>?R{DtYB{UIkW`b%ZdfSfSsaC$TU}O?D?kT!B*5c3{Wv1jN7HFtIKPJvkbI3LN+^{4pP&a>AUW* zK*|W)!p6nI4sayw36#)TPc=A0;%Ej`c~0TI+A|&c3=qJC^Z8<*Xya2G<9BJ^B1JcR zAH{iMs*`0ZX}|xq2{+h8FjltzaWjM9lk?f_kSAV#UUUt&)lKqLmL%qK z+bmSzY>dV89nULnB0H3@^wx9{wTZbaP$VM#`e5CEp>>ThqNDrJw}k`6%*6dFS2R*> z&r!!YLLZnw&4J-PkD2c0vR}bnoql z$!rl{m7lTCpWtQh`ZzRFTgLUa3XkwI*ux*(%h6p#MT@3=UCkk4I11>Q@35Izgs`Nl zavl;tN_idPj0|}6*Nc8AN6J2lNyU5sH6He7O(ZlDzM%HV?bbbEd@PLPey-zzq?N^O z4^!y#-nOlPjC2h#Idwg6@&I zYjN46?amN=z}2BS=QF1{1b_Un2|SUIc;ugZi!petRmj)i*wXU|cSJR+1}Gq}FDgdQ zeh8<2EVv^f#Dz{444~RN5r*(Rg@X%YV-v#^kfGwb#ol<~ zE;{5xDUv`F4dV^DM$v<&9`R<8Kf1Ia81a2uj6awzdb9`fKS$+IXy!+E1QG+u{hyV5 zC>@(0r`Kwt-=S8+68TX$MFRzRFdm4t)+y^*+`ajjA#1baW0Z$JB=%z@^jl(a856Wi zmwhJnN6KaawUw8PyCF8%Mp`-4%wDTq^LZ~euD$70jiDF-Ixml_0OS^9&+Y}t9SNh@ zV!mYU8h*nmXxwJ$N55McJ2hw`Kce4lW%I5WU5>-J^(MPH80$@hm#pGygTYqs4vQNo`J8!v<Iy!dJAbL_Hm@Y~)hQ3F z@kQ@{RMMj(Fj`qwwm=94blq(gn);OLI{#25yWXucds@Vzv>rvyE{*&rjLqz+d1K)- z!S4$&ROG}-3h(bGFfO1WW(HH*-r3HIr&WO=#ZgzB(-9OmQ#cb4l`Q{bVi|n!^@d{%kwY_;dc85NSM*roxT%wZW{y1 zf%pRkeU|D*!jeOOh99IV4Rd1177YEY!&gbTy}Q>aqzM!djQbNMi$MM-vRVR0b-0i% zUhB|3)s1qj0?#;oU~*E;FYkD(!6C-M8tbZmY;94wg~kR z4o0l67~MB;{p=oV@xCk8u+^FZVLS_Nw$@&*+2|NEJy8h%;Y0at+;Wc*%Sb zqQW`GJm7bPsuxxZb*(|TUtGybx+hsj1!9!VWhp~cJJ<`x!=?AOlldns5ma=SIZW4} zolvW1My6YfVbpH`3x4W+jC}@|1KBQ8L?nzaS?z`bK?S0Auh8P-8#T0~0O5zM7GT2( zsQ|)@$Y9pS{cDrXv-uE1Kt0Yc|L2{wBF; zDmO@?KegJ6D?I-x>=-;m8Y^SNEb&K!$E?0koX1n%uAIJ|G*=Hbb3|sojlm|NxlAUo zT|Z5Iew(C!f&f+*`pxy$0Hi( z{2Rv#L6WhYFvC=gqJDtTCHXvp_2fpdh`UD2M9JHrBtwxxTB zpHpq?c8LEtCv9*Z+P3OK@fDq>ZFzE(CvoiEf+3hcl3~E^Giprg1PLeoFeZL5rACef z%NbAnS$G@+SxoG)D%A6LGHPe?U4mURssuW%&nv7!rvuYp{Ciez?Q?|T>9pKzgiJr? zMi`8w(pTcWBCHLJbuUaOriLm-TWfv#^w>3^4C9opF$(L@YbDxb>(L>Z~tUoFf{g*@gk=;T?yi6e!L%Kjt5H{kYiCF zpBEeU*!@0r){aK{rCBQ-1Z zqOo&9AIk`h4@3m2ab1F_+pDD zJTMy75`v6w;FYK@Lut{HKCL|98N#auLQe7R+W{p<`z!Pt&gJ_e}q|E33JFwKW+kVO2;Tswz@sdCpQJyi}EL-w*K>vDP~pX3L)44AB^-HhNur`YG(5 zjN+dSh?!+wuw%tYE#?HrcP|kZpGYmx&*(kHXCUS#2@vU~Q@qeZMBtK{utqits(Za) zVtCIVvgX4O+ETjsYm!>5=!dBA_%j4=SWyoj0&rUv*2*RS3K`pJ3nyzGO$SDxb2F3VpU2 zSw_I!gHYWb0sfL(ynT0VE2KNp-F_I-n3Az)(HuNp#CKLdq{Tz1-MgT@yv4h(*ecjz zv>JhF>-FYCxH6lQUXxzKpVnQygGqz?t$QM4oOrV}UFGITFYiN4=6f7+9TRomSrT)O z^z$8R@OfCpVs;aOv0UB6x(kLP5oMFvB7Sh=@xFO$IBMAqk8er;&d(e`B@W~VfF5O1lsJW(5lhbdOk=*(XESZ*h2oX&6J!&;X=tQyA{UJQrylC(Q`mR zao>Xv3eL*#B86#c*hFu(ymD!ov`y{cW^Y&u1wmZZ)E5DSOvk>lkKIlmVs09b<3tJF zeSXAzU48#k#;(r6)y?EGF>%2GYqNHSGGVcM@f5N zQO(Ll2b~bw=t*jk9>z%3X$wb2Y4#Cq;foz)GM5>Axp4yC`gzOlXO zsT#zMcf{P=2>JWJgD#KWxn`o{INiG6mT*^LJ!JVqS#q+7S#ofaf0M%(%EYUQuw3_!?Xu%3TM z=+3Q^d{3fTZwa`w>|ibl;cZ?9S8sM@vtFb;K(&f2)c9s>9FQ$bLdWKB(TCSEY*Yw+ zNxI%{YEPYZP4jU?0wW@IKY#LOIN`Tde1-VsZ%FDo@G%~mD4pPs|BnqxI~^rF=ZMQc zL`VP85+RerY=ht2;Xvf=oE#V{WoV@k>%v6zoJ?l55k*2n_~4UL z=dFCHmdiL@$U(`%`8BHI44nAN0XqyQils8>Y~M36FBG)B`&B8P%&7mX`H5gfVp?yeuecZA4xMA@jg~;tg(4nYY1d_sr8@XG@cG*F%la%Hc%dRbg1=dB#&O$pJ$| z#qfvm+gz~rBUq%)rn~PR@0e`VU?R_hfE(T29~w`SDV?&)dJ88?N#HzFmW=J1;`oKF z@=ywT4kSk=Kps8FHlAkvgE9yxiT!hQC9L)bg-{X$naad+r9~U=6!_*i334CvXAh4g zohKwdGm8m=oM+IJ?e9VZ`lD7w3)9f`$} zh~d-94E9zsfmZkzw^zM$bg``MUeC50=09pdSADD-8R6v311|(5CsxID)0j{Bo}#PZ z1@9kyDM68(SuTN~D{(s=P{4(d$y0-I>RHVmguocdbJ5ynRh&e?D4;cvyXT47PFNc*rAP&QgX{-xwt@Y-PT8~aK{5n| znm~6M6l&_r1;K(B}Rxz_~oj+b}&w%h*ktYW;?0Xszx+SRz#I{8Z0lYYtK z9Sr7M-xb1&Ckqw5A;wcf$~Lf6(%T<8nUtdu&$k!HhH6dDI`hXEnGTzg41fOpR6zNx z|1~0ZO6;bWQ{kJ&>xAPklwnKrH29O%cc7-M8rn z@r$J5$7tCqx9Nz~4$q>B|H=d$bPtJ|M`y=`T0xeYk155>jA;APIrPcu-B)sfZi0veq=Yj_`t3#Gmg+RA$%+<8j*zEV17PT2 zCRO_UqQUYjKAb~CZ8}$3FqGwt<*vGKv5Kl5TJ@LLtYV5?R^_SU^K7jz4L)-+v?503 zRx9cYU^H9Sk^~Hm+5Lo8nK;5ie`5&f=a;(Id8Z3JHl)_5vkYE3^~XxlFIN)sne0| zGwp_R3E(CsaqjiILIKFa=&7q3o7!*h`7JpjTAU?UOH}seo)GIXQh7cSKB-i)7b z-)%VCcje%>rEu8pNKlxfXZgy#TPYAKqADg}Np97S!~Uv<#didh?kV)vfQwQ|9Q1S>>vQ}u^M5WL)@kn0K%D#k z8y0fysEFrcd&2XqGotsj>S~c-gC?N#2a4d{MIXtN`mcn;<)-^dNJ+SXo?t}vx;1#p zc&r@00MunD$O_qBBGM-LBRVGJPx{Z1uAl$!C^~4Ht{?ULzU0TY!0D=dTWIBP5^3e_ z4tJ$WSEv&pufhe?RUt(Pw?id3|4a6Z>0N$RU!E;)z<5%iwW2w+ILOwJ{>@F;uxV7a zVkKrz*`kr#G!;iwh=}gNI%u@}vg zzCveQwqTpEP%^}RVTlHBtMheYpz8j@m!DpQ%Xr!I_Om{2z{L%u;zdM!k)t2d-X9E{ z!ujt&CfySz{FI#8+d)vTo<_f~pCotwsQU+>m+-SekxK0wBD6@1k9| zfX!UWY1<*w%m0|pG5vRU#clMaS^9qqL-_HYle)CvR9&+2VKww9I_@I;TtJH|{vRok zagXE@xoMaczPsW5>E$Z8qc?(*w*+q*YyoNR!D+^vBnHEGxZ~VY-s3cnvutjKsYRNq zd@20N$zR_FLeIx-HXK{3IQO~Yu4_{w8a3#L-PT%y{{cgmJ#c0JWFb=G#6n3#oVpA{ z4^OJ{pxTP}e<(=eP0pVSDk@KNWE;O`oB5=rVF&YtH7=r6quZZQrDT!y#;DEWQkvmZ zWnpsK_hy{GvPPU9f|spP2O1>19NXfQQd(TJ9Io&wr6B(%+A7g8F^_ zYPj(J!*KbH&+b@dK*x^te~vyTZ^d~Amc-!G$CAG#CC~;NqPFs|Xz{v-?~*U^6$ckX zhV7q~E1Y;>G2dug+)WrvZ~OM{-Mx!L6%pfTY+mT!Wn7@vl~6unFZ{_zlX&rb#QJxz zF%+(YA%K?lH?WcLUxJNw4+hRGt@i93b6ED*J^Y&8BM-m64c03F>+~w|9N11g%=Z1_kdUg;(?|?aF+V*DL4F>V`j6c|`dr~}Xfw2?`PQ3D&gwt$Rlw5U6{ozgi z32wWi*++Ok2D@6iNv8~CQoV-Wy#xS1v+b&Y%B4CDo^sj_u%FR;qAq)FP9E>D{u{m^ zs?p37uMo93H=%`^^hwi3@9o}kg)r#NXBVhIPgYXS~fId z?U<7nq8>Gp$jhIa*I{Asb%SeS{GFMGe>1Iy+(0uLLeX?-3E02q<-=L};*HKHjQ4-2 z%-U5`O+#yVoL2SL^rSd=v=5{5qEJSiD&nxzH?mmuOmYG*EU9#Lgx-NNd!pJOLGZWi zHkN6~)tA@592q7*oQ^|XL-D0)ctYwg(f8URw9!<}$(=S+8nqh|T=t19#l37ouRQsL zdJXP=dad47|B$5BIx-R0k=mQ2-ezmj=}ElV-4_qm;e@Cj@{0|i{~@z`h;I-57~(-5QtkVAQ3pE`wMvaETyBy3+w>$3##mupaQ9!zl0X zM|lyV&!jJEc5rbzmks-2>T$2CXhiRZTEP#n>n&yN30TVxb7Y z&r|Y>`|f*W{{o0m_u^I7dBON!~FO zz=0q=63(~lV~_=a0XjKQ^<&5Xp|{hM*pzeORNp6%BCT-EiD02NUX0(h+S)cHrR8Ec z=zi^9Mb?3mq4TGGbA#TZpsl!!L2oFc% zyS8HyK9AdDQVUfCz3zSUJZ|r-a^#$OFLCyP)siBqt7Zu^jcO&DIk zUXJ&HFPFrleWr5mUyi3zCRDw?Xd5##Wlvn(JsOs-qYt0bnoYXOL&0VYm8R`>h1>c* z?0YwylbIA=jw`gE;diAv8(qmZe|MyePj9`nLew$a?XnNASJTAFF(S2bJ0+J|!t)`Z zE!Xa6Y82J=nxrIS3_dzSx)=(`>Xg(+gherZ^>QWS)CC)I1;-%ge4~g4 zPO#zsNX8jmzkyxCPx%s{CtT(vh;_Mz#zgF)?#hOXX08#I)YFKG`f%K`L=!Q;wVI1+ zJ=b9@4iPIKb9Z&VRlqp2IBoRY+8WalI?~?hO%7-1p2jztxELE_PgX-oddD&%KEk~f zD^Yv2_UlM}>Rw7mlHVP*T94lDAi-Mw)H}&RaNy23!}!CjR82P5Dc5ZCbg$9BjIm4i)N`VS%&Zm5v5x$ zW?txtGn38;Gc6Q&S`CqJT4EnkFtHh`aKp4mA6{MTF6$xXYO%G}s}sYbI5{4!^*8;N z5(^?W-w^f!Y_kCV@1|u5NEv!kFf|&hj=VAL@3Q)nFv`p#ZMQ{b)<$WK#kBuJxM){I z6M^V>fZ^U9UHN!?^SghY%;$#-y70ABsgQjHi)B2LnB4^ZXyETz#&6f-D=8quvPqky znuN2C{%YJ?2Zk+6K813&1Q2A$27{-Q<&KLxUV$@^y^$^NvqOHva#<5FT=m}x5wWK#GbspCXo9zPwA&wk z+@e{cAR>BgQQ@yy=w7lmu!M&vVhY=XJVRz!;Qlscd?Bx+b?+r85B4{8D>DkaS zAZuF(xK-h3iL2ARI#Wt({5kIU+1IUgbh)PkwLsy(_4{DFf+F~$H?i+!nNDv1a>xh3 zkN&>E1hYKb>kiQ-p!#Mjb6-N=CrI85*;#{=<@6z;k zg4syRIF7b6eU;PS_Y;qmrf017-91dq-4s?R;(5t? zG7d;t3wb#op0nM;YK-+JYfJn?Ft)2<`RU=f#{b)Kd8O9Wm{f13KUN5ka=uB;{D=H{ zYe6iG+m|Uc293E$raKI}O=CA+lR6DwmCS3NM;NKGA!1GCLIcMM56?%A@_|Ff>Z-2n zLGWP761tBg`GQyv9-m5N8m9(GB3;kDrPWHdt_zLtIfg}5T_-gD8L8i6{ad15{+pcp z)f4S!_F$#XOknpH(|6uivI34CI+tIJwKAM4ctvOFyk#~Hb5%SX(UbH7{F4Mk+__BTgUyXxXS0kM zi+^~D0(&P=?E^;i(4HO$K&}~0O|##RVP-ZSSFip~KOmdFdGM;A8<%P+RUZ@hs}5o+ z%YG+BhIwnyc$})sLPdU+*M`4?wM4|u?zGWAU|TVTteu)NONen(MgDyf>Mz-VfYW6g zBMC8WL7YHS3Mp}-hGxLC@^ZrVeAF*QC%w0BS*sv75~*#aCu`r0fZ;g8?LctxHMOYAgx$WbzFh3r$QDMDx|(-l7;FHv>+ zJRq27@&IQ1r>X>z=66JY)%cJA{7LrSb>j70DMNo)XG+xZ444`Kd>Tn0Zla2`G1D*LZgfcMcTa`c_~cxa%x(6E%W{?nFec zf#g50vrx-rF&XEPh_fh@@dIB4xR&}V9wUgw_e-X zRJ^ZAAj=2Q!_L4lwb3h@MEF*eZ<7 zQ=uy(+l^5JX}&w)GyGf;fy=U<2(UQ$8XzK3}L8yU?cT z>c~b*4COnRh;t&9nb34!QQMV^uIs~fB-TR)?uCl)&eN=JPu<9+;fBX{f zuGG_FIYtVa>Or*LesDU7pyzD2dh|WEh_|GGH4U=G^|?$R?py9UQ-MHL3-=>c)$r|L3D}*kzOypP|Mj~MH&Ec zk)GHJKoI&a?SOmRDPVOvNra1f8H^C7{%tTLa3mtAW%PEOM(S$m!QsLK!8X9uWX_=C;kJLGT*-aYSY|t4XKtBc2&ZCH(oii9o@| zrc8iTP0}+NKb@E`R+yA9l6eWDZc-bYTUDK_^+U)Q(M0sCvVYZYt!Jy79$=A}6x;Xa z!`^9aNvz4r*AYYpqiAOqWcBPBR{i}bDl>Y8}5y831UvC zt%!u_nT+tfy4^sAYjt%tCqLPRhUISTGj>dQq^EOc49q6A)}LLk0NgZInNlyJIil@@ z<#=$W4R^JL_N~9`jLx~FWD>}~KX@3V=2>6p4t`6SfPoezGK>BDz8DUnbPbC&8N#ju z@2@U7OLcOud<%g=L95d!Sn=Q+j=I)h@0v zEsAe7DY6yJ2(WEe)Oen_y;d_cxtINs9E1L`1MI&=3y(f0X{!HWzt2ie|1J4{6Ybz9 zOxYL0mUC`sqxgcp>;p?1AiGyDh6qWffC*aDy#q4h8-F84t2oG#@0|lJ%@Fd>CDBr)B&cmZwOxOHH1>LV z!Nj0V>R#q#c0ENd8^43xTo6oG}qmIi?#t4)Z(N3UMP?102Z8u4W7R~amrp#+AA4RM-g$- zJ4fSjCFqps;NW?}oqqEc3oT1Kpw>Rta=o8)PDuRb-{Rs9V9n@zf|;83m=0|9ltr!G zduvD@DK0qQ9XmDL(7epPxu6WiU5yKpqn|y|w|IHb=hJZeF~gbFD9U8ADrRN25gp*e z=hAd6A7a1QV1hA0CrzV`A&>h7z@5+hU5q{Xv2eH7O202V;=#*UB(DURs<~?e5q<{a z-O*j5TTU+PpB8B8=lM#S1L0q@KeAYDX==h_Lz+g5oA}ftKd$=@P1~J7?V5|^y<|l~ ze``%JW1epMDtb*5po6>e{o^3?X!`b0b<*9Xz1E&&PO(2D&8r=%Cjp^wMz&jQCurzBycF z`*Z}P?&3+bdR(ehb0lde;5LQb=S|pbm=#P9NYj-y?ko*7Hr>n4<8k4ukJlzML~!=d zJU{fUczBi~ofpbWwcQyK&^Jc_RAS4EbMA#0ZG>cJ(FrfMFvWKhEe=*a2`xaCBmY#YPMtb?pR@O#YtFgWuBsv97fg6VNx7H-xQ+XSA{gRm`Y2r5NFU2_LMET| zcwdaR^Y)~kXQzxt_qlk~+XNLOOGZPR`5!t6qChwLOicJgpCPHmT#Rqu*X9ji>Uhof6D%f#QBx{aPv5(N~CA; zn0_6k6lTk24Lfvw@H#awC|KC_1Cz9&D0cWb5MdJ&ogbsmiqG{5MQ@y&_i&?sQUY z++Nz=Sn8h`JywGwEq5?ekEAhE0=Q2JEa4{F2Ymkny^ct6Qow{IA4Wq-ADrsu$jR#01)U$eH)l*yZ*FNQ}dQ+t-&=E9`7TWf=LmS$}=En{! zCoFlNNP+9bMmOhERDLChYg$>u`;0KME*2MV`)$0{oOSYL>-h57o1ZRfm-d3Im(S=Z? z_f!F+8P@j8-H+iD&o!qz`UVw0gji-1l2XJBT6l{UA~8BM?2sCp`}#Ek%rMX#?tinv)eK{h&h@6s1_g1XW)PPe2+xee0L`uij*@W2@OT{83XqL%GDLjap?hqqjXm+m z5~!bTY|#3f*yA3I%oZ!*OpCIAGZ)yW1o!?#b)dAs{wMrDc!(en2puJ5_qG4aW~?&< z0T$RXWi!?XYf~vY&AxqxB2`y{9d?E;wby#Cj-d%_fy9G*zS($&XZ2|H5fGClV9lIL zuibF+`lvWRq{?+wVk7qjiz|Ie5Z3Au=zBtY*}bxYZEH)LQ2~ycR3hgT&LoIngUuKS z-|0dXPPq=3Hq)v}`Vu87PD8a4uc`?FZy8{JieM#Pxrx<67A94d-t7HbI`HR?0&qPr zYa4Gb3m&}O&6B{lILk95Q!~c&&6AeCQ5jy7bx^_X3DN?dJV^z%#qdMLPphjN=@#Q< zZ`iwy$-E|Cpxf5rB$~lhM;y}YOPFR$Twny(lhXKE{a23IGkuNP$j#Xj>a{-Q=pk%* zWx~`}cv!soh@yE#A`JIwmz#jdbA|r!dmcm6y+@>y&GlEjvvW(sTTxzz&6iTwKtxqP z#`x>RxD1ejbDIc10A7&*DKCh>)yRi%lQJyJD?awVcQKrnF(dlT;^RFNiDMBgZ&1B-Gj$s|9VXJ=79fms?)QS&P8xW!ixIUDx{ZdPkKLeXI!TF z;c0NCDD86VJHN!+VYR%t?k&Y+3s?86;oCTjtKh_?wAa;%z?&2;zL*P8mBx7g^xfoT z?R4>SYaQmy|0lX7OvL&N)VXKK#libpn8MG&=?#o(k8N>iB~EePuCePU$P|VX@%5Ag zp{-Kx7ami1UEH#7_L-V=7GAVWnj4#wihm>#04TTL;jf2mUEw<+;jY3dMaLyP=^}^^86VDB1MY_F_!B)QfK0JW185N$3kf zKd7{XfR_5QpPwH_r89xh?V&&`qs0kZu#2MsXzX2i_7<+!$Pe?jCa|5aM`p5x1`nq% z;;5?K@&agx{jy@(M^Ij^%F~{|Z{U?~yQY9Z4+9AF*zbyqK5!H=9gI{|z^L7x`987? zx}_#0VkESCe}*j2QiHeMHJeCmR@Lh(F0LXG!-eS3K7ABBNvhq)I1 zGK1#n#79^ERZ*}cxq#5CLmsvshbam&L^O#8M$HgZw`cso^XsaTZh~gT;2WF}Y)bEI zpT^wDeXC9Ll);0NO~g%!(HuE}P{%iRT~YCg1re+PPddk>_kj^!$@iD1lX0yHa^SK) z2aNC257sjad0FhI#620nTa%-ul3t=JJ^VrtP;dcK*Ki^bj|t|kd>%@}@pLHj*AG{^ zX2B_A?aVnR&rS;_9;Wc4@s8^~#)gFCz=9I_`0E8w`aVE03n1mtsWMaIj_v;ZqjZ(` z)xNW(9twI?-VVx2sJab9AMW+YnD0o?c8fQgdOHGgS>^fJQt*lKx&plYX`Yjdv7KDz z1KGokM>s8SXSPf5k=6Kg>l8AF6-ylidyUB%)%se7dIhVuW#g+>=duc(oSiLw>>+K( zWU{2M=~8X>gIAB@>Qc}BDq?+c^;06LTJ>l;lI%EeUe8^P&?0)rYdk)qtIuB%%lZ6Ww{|GpJKy)>77 zRwS|_3s+*jI~_RXy6m=WO1ppjDEezA&|!O#T*^yFcJL>LdV=CvsAj^$@vW-)+XNTM z+DZhq|X$SI{aATShn~N6t`3Debvrg z6tn21XYqN&(mfw@_BL5jWMK4I{H-7 zKE`OOId1iLCo$Y-?+b^8eDwWl+i9vS?X=Lfto|xlX`|FRjXQ2v!~_h9{YTc>)|w<{ z_Wkv7IT%B=mtB%4E}n270_+C@aTQ}^)jxsnUo{c9l?-JYez)JYvwP!LV&QvCW=y7} zE!$D&p!e7MTxymN|H@UnEKq+ZU%fx5LnW2jO$bow9t;~`OnGStFN@NEPo z5%`|AjfT%p=*XeDB4expsGxFVgP3(E?V`3Ml|;8Ds_0Mng{0WBU#e~-ojp*?G)P!TLAgA8&J&EHi8SsE*iP zseoMgFM8G2%j0n7FmCMliI-coa>i3nUQp$OA7qdTlGxQ{IwpdR4iCk$i*Glu>z)+Z z+8Y(QeLMOmS{I0((6phiHpuJMu}ps>Xz!>uKtvplrb|eftec zH;2M)Y0of`MSxB3z*}O{kuyYh`YE1I*%n@m168c9@KC!uz{B z8h#YX)7n_5{A#pqwBK_L>U(BDxu29^v8m$7aVGejLSKQM%F8=E@-yP+JkIDeSP+dg zb}n($GAM!h3*Vpa$s^jI1Wi_*zlZK|%A~i~qPG%;=yMd=ew<-R4Sv_s`lfvz_=(wt zxY*=nyjS$yBp1lPAiA;p)*<iB1-^ z?aoo8M?+VBu&tLSG(Y>>O|LFPN`7)<|SvFoU^4>p!@>YozKnoiWn$YHs$# znjeSGP{FK(VS%@zYFl`bYL?g6_5BW$siTEe+JWWbFuKl_+M;z`yEo*zUvk#0{=SRr zb-*W_Mx#>z8d_Ov4VIxR7x5qC$?J7z^HQ~Dg-akIoBY~Hi((Ibn`-*Gu;H1iCs4+bN+F-z}kn<7m=!mgYVKpu_N zWrmpF1k$)Y$+kZ$h%va#0HS}_1kb^dsrYU~yY6FU4-y>UbF`JdEDILKj7Krwm{ldRv0^WrDkxT~!8O{9%O_=ZUp zsc40WwvuwzO!#j9mF^PfkuKLutiSuUmP&Dw?5)KKK!M}?kQ9N&(wIM*xf_d(zv+_f ziEwI$1SQ+Kji0~g{<;%$f31_Khj!aJNhLEV82)F6|Akv6-e+a@hKX*d^IM$z<(}3N ze-zu}`<}}bfst`_hNww1dcfliSocBB7JCZ*F6cYa>Iz;H-xWOq>|}%r!ga-Ih9Kl` zz#+L|-DfRLIE#+&FJ>i}r3*8f?U0GAvqbt#BVpFc*4kcMG!7<4GtUIa?A`{+I!<2b zFx^k-eTgyob)hfR)_*ZN|I>l=hk#zyuv9dBAET1!NtGqLyo;Vze$mxx3Kx&JJ59pF|+6stGk{tq}nz*0x-M$67{X!Srq~vTjV``8AYx-9e+Jzl9k?g z_MS}*Lpk}sxObW|5cdw8OZw5btf%$-Su>u}t-BBmncD?tEsZ6AD0T}L*XM4syIPS- z(=)?y-O)aJvy${Ie|KwAt@=c$BQ0N93oiZ|Egm#GBxh7L{rvSytOc`W1Xa+{#@B}@ z!C!^J@kmr_CCL822zXA8e<$F3l!i%#8_55IfOi-EK^fRg10qL2gK&Xk_v}jqfFDG^ z%Nq{S*QcYTZ}=o^pj0ldV8Sc>cMx;O$W3kYReVA2GXw%Y7Sr)qEcy1K@!~L0c0OKi zp-8qh>E{Ozo+}F3o4Oi{vj{KG-SrtH0WEz{9|w20b-V-TF$?o}R$gSoNkvl=J3*?oq|dh?fVX~^4g4oMRg zS!F?=e2d1vS2eUCA`q$o35XP+x^0yI9s^DMR}6%6{Dl(YJA~BMn|_6noV|9T+qB6| z2dqt5m&kL~x%F5OdS9`FW`QU%fHBc=;24m$i?qnm+25 zztr7KR!y#t1%<5sBbQVUSA%~ zK@5cYiaWF_mI7YJ4UKMqZATwtUpf%X&tJBx_~B~%+1%7Gt%{a9x6g#pG8V;hZvwbB zo;v4|(9qbB{r?JZrfU8(z}bv=O>1s{qNti_JOULHy3M)E#8_Y*ezXKMCW7jSJQek1 z$$EI<*xQF04PTKYOC8T;q?7Utah{Lo*@nI$SC%^ot+Qm2aIWths?KjYPvSbcKny&K z6iM3-7P)pAWkXrCcA|E5N~Qk}zB%8&>`6K~%C>5#c>UdYWhQXIY0Y&C^S*cdIC=d- z8L?P#I?=yxIyJQ4u3{h!tXk0By>Fk8YHh+;M`Owxt)wlRR>38UX_oojcn#zLIWi=d zWvwY=c3zw-G%)Ixlk(jU6GF_3;ct7SEs0ro z5z{-L{bb)m3&p^vX51DDrOr~iYu&3hIlWm}RXk}myk0wgai<21r-V&ELPRW{-Tc!F z@WSPFK)>sgX6utFdavT>jBouR^E_oDkYLU1C_f&hEeb7yXYV=l>#hpw%#_=v7>6bc z-F-+t8f{eBZSgGbY${zd<Tjck8Rl|7(Oek5`JZ}A!2Q?74PH2Z(qGF8DKxrWQs6N zCNXFty*yS#Q8#Hw@}9))-wB#=e;cp2DL0m$XmMry-HVCt+Y-hT6O*U=3&KZ?qU%U* z?pAr$y5Z_7v{md6T@L!|1g0H8y6B)T?N!U)aGGS>Wn@J}n;Ke(YhH_kF{PKxSe(Y19Ea-Ghn1A*EbwZ&F7_@Z}(|7S^q9hdkh)ecOK?%~<6nmFO7 z%JlbQ3U^JFx{iadt*$VeLxtw1lNA)l#}f5(5+j61M1NIx1=V+L^{ULS(!kX}?lGo% za`rw%8>vh-_19XHkb@9VD#liKbCr4pPXrR{x;IHGXs%dmb_$F zj`Mbi(#XplaGy0|y5MSgKKZ1E!_=qvWya=xEQkP zZED-O(UxI_HpN2Hj@18-6j7LW`fvw~CeSDHd8XCTb@)W-mw@s4l<=bT@G64`TDgH< z^e7M7q6ePkjKahuCW}ek>8BdD8P+YZ$Rx<>*#z6Pc4*5xHSF1x7`sUocd9k^<5{Eip-Uw!fTv^8T&C$Grf z%nK*|Zo)JpV1mnIfpzWzo6QydF|!xSX+e(xeGB;M(1R4Uq=u4|dD|A9=ZO^t4WX6i ziRS)Xd&IV>ePo|;ZFuRxH8Fg4n3e(_yL{5zh`;rRZ z{|-fP|2-5@U{lJ1rcI-@GGtcx|BN(fT1VP7#Q1Z=0qI+-tN=hs6{Y;1#3Plb?OU z$nI^lsjw7Ot(bXDa23{m-J%Y_`x-6W~+-BYMDpt|Fj&e*tDh`(ELj?BqmJzT<4@in1tnWM= zj;<;`X>tw8E{AsR(gY9CZ0niOhif{1%9v=ooAX3L21ehM_D6l zTBs!KtsT~=VGe=>q7Iz~|8R(~>^FS;_>uPKTXE)Cg7|@^h~sf+?o9{5C)sVDxAjaZ zsQ;^XWz8o6lR4YaQzp+EeLN>FAJct>?nziw?-$Q-=Qv;qOO<0@44Wru0fT3pyU%la zRWN{nD~+Hp1nCsHk5*H0Bopj^ICg}VUdSrt72R#5@UlDG0~Zad3ybAutf{skH~8#k zka?bOAC^A8CC(=Y)vu+qmzndQBN<|{mykxCLO$+}|FHozX?d6KkZ6Wt+CxrPdOlk2 zKYrec2wiD`G~^?Fif+y%XmTown+Rv=4)H(bz1rWpQhe1iaYuz*P6=?9cXl61L8ga* z)8i8&k<@*UJzd((8|m>T*RlS}Rt`7Vz*|k+;twey@on00Xu86j(F6AD#MGAQJ1i{c zKyAa&WM%Is@e z7yPG;u}l-0|36fWL>_c`e-VcF>tW3OwSf${xr}QCC7H{@l}J{%y&@%^luM>jO6XOK z`GrK<7{t8q{{ZSTEu6hTi>V+d)8C8su%~9ZLKznRs3eQ_(IdT_&2Qsla_QQG$fKZBRKf&v2^5>?O(nf4bE6BL zFx*S}u#3#nMO5cLF1msx)UUOiwHs|44G@=uG0*uuYIO2dfqp78B6&DwD|osAB^;%Z)zxgD6JsGj+-s*l7bz z++R(Qzej6HXj}x3GU&_q9WkcGE_X}X+%XWmZOG+2J@;c1yBsApQzHnH#4@01ec+ZH zX!Gkv`r14Qj?%SWsIj4YIYD|c&z13EW$S~2;Z2Dj%l9Y|)&sHE`IzsL%#HhU+9e11 zGhZwK@$a5whoogo9|2o;j!Pg3M9$vAqQ6R#C-@sBmT&;`INuowuS5I@uH{_VOCkYT zKmrs_M)YpFo`YkRcBwG=At+f#NW0jg+T@VG#-Kuk^w_`((qHUXeU1scVm96tMWx9q zC+6j;T4+Lo#O=Z_Is3!d07tLS$O!029?3|3Klq?;gTPjm1bWIogiGXbY(wCb!Q6j( z@kBRV`zj&g%-JKieY#&e*LanabEISiwO(NB*jq{4(;><7CfIW&{0)3}-=L8_?q2lU zglq98hBjZ%`I)Ls4eS=;UzcWGsAg}e8;G?ZOnwjDHIw^u({qx%UBN$0^HjsbR9=(s zqHAXQZ7o!>&3WOR@UYA*{7XuFBq&E&_trI=%`thpkvAp57fVilTGSZ0@GERJ@yCIU z#Ly|SOQCa<{^;+#wE9IS6(<^&Rok&%b{4;W#r^pjE0P3N{_O91qo>EHg z^|=xl?{qC#7FPuq9ixN5!SGy3bRTPJHqR2dyn*;_sUF^sMgyw(+g=`=r&4U#-D&#Y z&hdF2fX@48tYpbkP^rysD>eJAv0dZYV}eh0+4QK0Y!ooA_}Cgz{^JKyJ@CE^2nV>G z7c=!@`GwY zco)u(dfa=GgQ*?O3Pz0E^yOe3kai%h*+jMW-M-I|k0xvVYzGtfMFAKd`$vfUts;Z2 zrD3u?%ZVb{4%tGVwKH&7qYKC~jpH=eZOM}_cY`f~iz%=ST?;zF$BM%?BQnZ?njO)L zOfAeLohzq4&LK}}3Pf%KpGvuGJDhPb9-jrG8i}MEi`dS|W2Oc9R{yxM)uKJvDFrXY zB>~JuONLN+K^7anQUtDY;Z(s4_-ndB-6eFUP-qcPL|ZyR6UhH`@ltHiquXL7!%)>l z1LD4#xMQbmDQjbH}tK!$}hECblL?8mYiGB!|}nIij?ea zg_JJT!_yc7q043ciRHvmbWiCQX2~H7-HH#X+P^mT#m{`2B!+5qkjfw|qajeiEpLaw ze;YuvYR`u5?ldYeMZf#0o!OiO}Z$(>9a!FbCQk)fFX%ELR=X@%% z!VVr*Z)UEK17!)hX`4yd-GN{8dk?qja8HZTF<=N48H)Vof z&(qDgCff}CED^or4w7B?6lA*(+31!xFAh9Q(6w3^<;rzCT%>i`A-8^AcQw0W%S?|=5>Tjqe9@YjQ80o^FY8fm#kXHySc z<_PgSU~1=Zi@QnXCHbo7<~{66xXsDRP`y2Wj`~qt2UC-H*2oX{n4VW*p-q|VZgD4Y z;FAX{=iE`=(TN$89;+%>y~ri)pfG@tw&$>x!!SJit7%1ayW(!6p=`4(~wids&0{-bA%Bq>j7UW!3-8=<|uD^3MvG$*sMmk;M-9cp; zfE+oO%v;`uBpbQT;J=WA`hsmv_P`mcC@CPOeky)R z+>UA*b6t=#3g0nSP)`FeBpPSW)P*RW5vWX4=S8 zkH)EpZ74V$GFBloN?9Jmg?rD64lH##r_BzhRz60Uy1VcPoC4qgS!u@SHJtQ(RMeYr zwV~xi-ILtXB0xLR+@Hk-$5Y|fkM=b+L<`ixk!B1>%;o0|pdR}#uZZcnQV%L>*wN=J zQnVhH=*hVHPq;rc&MBxF4T;;6+;hAaR_+~=lh*n!&m)Xa}F$7j+Q-BL1}Yd35RW6Q+mBX3~i$P@MX%NaF+o zo1+3QlRu&%r?s#$k4A|UTK4GjSk13zAj%(j{pMcDIhY`Vbt6VBcbof7b%2}+AA?%U z&9FZHPH|J_u2V~(tr*Gdnt&HOgV6Ef()If}>AfT80iMY!mU+!_d2{1<&(I{)_DrpL z|A|L9V^2pS(mAYbI5aA1$5-v3j>M5=$EBL3b7A>nsZY;-SlB{7kM5p;hN_XW?%-TQ zJQ?r_H7V$Wist%>o>6Vlv$$~6qbVkuZs~I=nGC1CN@i<5zHXR>)<58lLeDjiZZP7- z4TEZc`SL2spLrHaT8&yPP;|&FEO1>V0$N&p zC~~2yN_b~75Dv0g&&bJ&LM(68RAwE@WAu0_qaOZg)l~j%X2UOQ#W(M}oVhH_z=iq1 zgz5BhOw$t(bW4cn0g6l~(euvoy~!YMMd+H2n)r{03$ov-L0N3pZd2V>M_55QvNQv zA~0~A5x)w81GJ@qyqek@mxG;d<$zlQ?VwA4x_}}0ey%}Zp)sD$>Gj1~N{nfpffI4% zP9+RcTi)TmlZh9k$yWT4=+pY~Gq8q64rHh*g#VLzomRlQ%}52#D~ljQeU<-^7Tu#f zEn0{;zv6FhOlB@xmQvWwm|QKsw5Y;DvRe|VUm+h3*Q6Q$#b!%ru&HNf1gCc)V>JQf zgD0?v1zPvET|ka9Xw~?~3{)j?0pXv`(S8$kfOkv;mE(8(l5Gnlj;i`X z9dr!ZD}*&~l17zeAeg5dabu7DKud|vl3d)L@YPfoI=oWb{Fq2Nf_RqiPQlh8-a@hs zk%0+xeSc0Td9@0vo%h$PfA{zQT{owL=+A@zdRFCWhs%+eh4DwCw$BW$-rNjACcT zuBk|)_NooCpE(PI4sR7JwzlV-9B&P9>4A_?xWAT_qDPv}gsR0c-1TSoYNc*-0>}El zaM6n&nsg?s4@#3>E*Fh>IP5qOnV4Ek3C^QsjMljwA6CG~bn5YVg6sp@kdcmp(!3)p zczwr2B@tp(C96u%1Q{WuImcjt)4?7_zbRU;VoDQ*s29F-8*6lt} z+I@dh{BGlsDPt`-bruQQ^rwRalv*_7eq}|nwwf{ZLH6eE(@eCD{$rl3I#$9sb9R&V z7x`n5yVqYV(Bjkx+Il1ERP*QG(%Izk+{UP)uQf;g#ia65AXIow#n`Y+U%mfbC^PvXXI*nzK z{1_`p#tbLuV0?WpS#ha%3lThI<0?8dEeQ+x3Ci)&tCTzlZ*tm z_hikgg9?DPF^i1i;tiV{rKCu~0XR&h2mULFAV0-)A31zTzTk6jP<11&dD=6IE@}Pq1N#%8NxXy>pk|upBp`d^c&&#o%3AfY6dNzok)VW=b4-FF+T%htF)ZiO8 zd_Jl`4Hibhtpu?Eq_h3PI` z!FbiK2_DCmibVyr+Yv`9ZuheQqPaBL*6qopN(xCJe!I(tiqFY%`g+H+-NLmYrt6OH znD-^+-EL`4j6v_V;nbTKZVfP zJi+G2O!6nu@{Sy;Qdp59pjg5Y)uv|E_X{D9zOpA86zoX&%#Iq5@AmU{T`Wp<~3mWkSM@#R_er{0$@B~KDvSkGm#Gkz@A0o6l8@p6*B z!=Tw2a`;g~43wsG&c+l|oz@V2?t?!*YM8ydkyNFYoHJE$?sf(*Ycg=+Y?R&N*I;k( zSapJXON-|t*3<4z15ljQSu0zzeB9ed8td3L-fQ+9I$x17sBhiGM(2A=(p}0@!G`2! zpS2nL?8I{ZorERPD+2F*p8C?2f`Gi~hBYkB(a-zRV^GQ~g1VNJDdJk zBzHb9V6^C~lLafLSN^O!8LJcV`#hU%DUPyQ%!bUol`ZMfpJCKnckr(!+4UpXE|#dW z#Ik=i=Nh`X#UjwSps0t@zckx^Rod=Y0f^Z1AMf!0o)T@Qp8Cvfay2W9eBH(FRZCKQ z^COo!iTM4Vq4jyt@!s+LDmEgsH!`9Bg)Jl|DTB+l3>$po*F^n|i{L^&hV|O}x7{0l zkWJ2534mQ8GNGpy%Q!&({CM1;`goGvuJORqhzNZ8vrikX&}+Vsh{y=Lb0T^0-s;TrX>>W#-RK=hz=4yk@9G**k z{ct>o&-Oh635q}{6OD6dY!ZIq~&aHdI%O;zkz-f@N3ioGy=IV@PjnLQkq@@=X7p|CU^Z=E+8_f z%zqXTslFf;!#6wO`rqB0#*Jl@ffQB-XwZ2YvYi4gBVp2F-`GNga;7Ouf<&t`9$+Lp z^8uh*bp6am+>!3G$l<`?%EvV}?c=N8CY9E9Hr!y=6RasRL^`RbWFOgtrG?LR-g+v~ zR^nL6C&)fBk#(}>Ay@~@uVD+*8`6djVBKRRwO`Lq@{CV?O>S(;qgI!dA0s)rDr-la zL8|-;Eiq-MmB!NTV}Ul{o&(nc?tQFZ;@LT5kyMW)@Y}(HzN{28(LgEPNF^u(D+lH* zOi@gdNn>}WSlH2w(*rdV*AvK^!U#S3mL0BPz<#}9R$nqmoq`gT1H*^04C7tk@4ZjE zIFa49*zlRg+D0IMq+n3sXK`N#Ck@itbTgZv{r5&K7_6=$vOA^n8eYYCMY$*O0TgRB z-5a>HTzdt17{i+Wsvd(e%Ga8AF)htty|sHDV)x?g6>>8ynI2{)?CZNiHR}%$Odu|- zY1_gtUdd`0KrYUs?76Su6yD!r&0Al2Mivg&zy@_(+Erapn3$vv)lbfZQ|r@%6?TZ64AD{`Er4?-$D9joA^79|t1rs@XBfKu44t&!KMYG?gcqtQIjZ!SCeFw> za2KJgiCImbJu{T@6j+0F28<+?mY$ zA@lAhdL3>|e-;taT*}1fuh{cXmo361RHsv+y=V!KYpL4IwXj*;>D9;mFv~!*ns?}u zcaEBBYW=FvIdsAWjS(YB(TwOE5AThLm8?|``PRS3ikb{zLKV{4cgBmlZ^Ej#_K4v! z_x1F>?tbsm8UME9-qUAAy`ENdCeBJ!$Cy3Bbx0kj78x4sOR@m+C+k^6j}-xT4^0h^ zmHoSxdT@#Cm6@Ol{RM7>&VPCVpb+mu_~Ier*b39BOyU8L2qCPLz-43pvVdlsZ#p0*3?76(Gp>(HX1y3nIZ`JgMx6&}%S8y}~h z4T8;njD4LrQQT@#0@``tZ;TpIZaR$IAmX=a7ooMbFe-X`op*xul76xZ(JAceg!66u zXPQGE?t9q|JjYv2`t}7oQCZ9wzw@bf*g7Ay8>=B3d?61WOyO9W$RS40R0`Da2-xwS za}Q+F(|l#!kd`Z&z~^sEJvlU5{5Pbb(bRku7MCY_X06JmMr}k@}hrCk;pX|bUg-T4&^6nFZ*eR((E+B$s-ezUf4(43* z5|erl5Y+}i1t3;_)H(#3e}{$KXv}TKB;bSarX&4v{y}J3)v9kQM(_Obu%AJd|9q+k ze>E!I7Ge-ut_Zw~EJ&ax=j&3~TIR}!ed4EYPyFRi%!>h>{tfpzPmJl&)Iuh+kB@_N z1jl)_zJ4VHv89B~xUz-_urg4t#9QO8Ime$eSWdXR`gvPY6}}lX879`puQec)!iOu6 z$q>GWN9wijlQ_Ib4^G!3t!|0K$-x7OgL6ruNnEyunLzh*=ZGXSZ@*@UFQS6K?%og~ zYLW(>Ip6uKDD-&Bl0?|1@pSr8grNVi=dxj08MNf4{=V+#wB7Y9$f0} zl;tE7|Bn)P##{}%d+zXrta@Ru%60u52JYDxO4RV6NxJo4biaS+K!F)6Y+idoXXMChhFgJ^x96pdgqH%+i`JKg>eTf_9AafcC{xADbZ_CS?s4 z_C+*Tb2TIZ^nkmOgy`aA>Ec%xznv+7+nwi>p}p84W(~#5+k0<%xi7;H&rMIZVe&jO zMi?hZOLmi4&+1<5MQh$}Nz-n%;5o1DF#S)9hGJg@)J$yp#L>U`S5w)=2f3aRVnb1D zMxS_KdeTjvZNQx2{&{MH|GpDspAKH!1qB*mnr_MK!|(N#SoKJwrJi^qq@1@4TQ89X ztG7B*8Y1imml4q{=>-O=kNPkHg8?wO{cu8!G$d`?E6pyQOlaYfA&Q*6g0it5^s*&N ztew7S9_qxAdr8wsi3hvJu_sxGyj{4(_iS;kx0vK8eMh~AhgqnT^j_~2_1{1H<~-LM zp%ZDqY1D*m+~Q1hKNKM&4cu?-aQ&DK%0|LOlSJ}v8F#>pIvDuW)dr}x4=NedvM2P| z1E0ZlXF;5pwGnpFAxVAFlzdHQYl1`&GkG^cziB(TOQ^9w z5FO(3X5MrJH}2pG2z&lIdB1Ib4i~UDN3og(bU+rH!fXw=BFA@iOUJFKq2cx})!D}X zdVJ{Myuh;Q(;(S=K(J>_Aevf_nC%F=$a6(u@d@Jm9daWrRDjCf{MnWOkC6oOLJ?2o zL}P46hHK?x2fB4FAn|12&|*N%o-((-`zv_G8#=7U@ZDV|p6M`4uVtkOwomxYn<{As z?~jrWxH_f?&l0rW8v7WUuzJY2N?zAvT$^ORC*!`v8n$L+bw_zbSGbIDAD@rU%=*}zRckb{RG~=OHtee0W%3vhTm@lG; zK3KdmxS_P)vos<^PLY%}zC+Uk>{KQZs`$LI+%yYen+gBXy&8lcUXV@4@V z+OOTf`F3#c1I+>r&Pi#m@4+p1KIOpSENYtdAHF2B*_>k9s8s`6wNUx>Sh?)Kj3{NE zT{8MVagFKZ%4vB80uV(|k*Gh?#75gW5$4WGyHzz9a56@+>1#;tYW+D@Fd1*7|N=4#-&zO(*lgs*YGvW-GzjccgI!f+dUZA z{Sr5T&l$PWh-+%3V5&-Dhrk63I4Az?1o?J$p5-xH1tjX>=w%3yGLAKh&GLwIr$43x zE*Q+S<{kpas>t9`V}j_sUM6hgPGzHgN#aN=A5ar9qgJL)wHK**1;ZsG2Ura%SYkOl zV*qE#s${79m`l%uGs6yIw^`@qW4Mlm*Vpk$!8%=V)R*2f-cf;7%i-(KWLce9fEx7s zO%yl0Zk!d@W9afxbi%ey9XC=;be+E%0F@Rk^%8Dn`Y94qqz_JQ{FCoc%mvAY9$APT z0#A73olW_mN&Bl}m7;3*PPEIq)E z;iMiuC&$|rbN%QUX)PPet}FQ}_q&A4FV41NcGZ;ez{+McZiPrhJcijIPbW?p+w*?M zfsH_AADrUdkkCR~-oDRi0fQ(a2gtljLD?g-fW!bcPo4-u*)fKCAUoQDDoZ}vVJzEa zmb1_!NqI0}N|FMpRcIONr4m1m-393@ZIA#G-_VB&to%!su|;I2wLyHzZ(tb=y6l=M z-z10gARO1cQ}Q=bo72=RN$PJ;LmMaK*jc=yb$XdXiPh5224Hv3xaQ}5ti1MQzk7B{ zAIVWV7F}N_B?VXK{Zc|ckuPXO)z4l?grz``4*OP(KN^5@-^?m9X=Jcv;BzPwY`pg)k`}F=hREkZ;5eqSeZ3rgFI_iw-|+)Us^QSbFdVm} zuo=s~>+OSaW;22x`!d4~E77y-{xudbB1ZT!8qPt*4AYHFkX^^(8TBzL-ne@sZHQ+9 zMvwt2QYfdjZBPsD&8NJBc%$D(SPhvw(_`Jra=3}sW{(k6TY;&p724kYmc}^89V)x`)i)iHFqt#~13s63f8f0Jm8u zhux`TxAn{ev_dv>;|x$mVE7g}c-#ITm_&Q3E9z?l zJ=oWGZeT82S3AQ0X52h3Hf$yz>@+9+k~aR2Bi>qb@tw)2Z>Ur+WN{{|{i9GFQ2~9_ zlBWk1Tfm&tC4&3zRD0#Tu5;SiB+f_-qtPz`^;upNnFlE%jzihPhL$72* z^NQhB#3`Cn^WnKgqIGUfqJJfd;tQIOVFoH>vHY*3-gyevDwZAJ%%};ipab3n(c#-P z+{Zb9njfdo=;u^aTqEO&4*cloI{jJ7kX^;D^a#oXN=j>RCz$X#R+*S+@yV>f#LMA#}J zTl!FI`^W%#;tcAa0D(O&OeY4|la;3n|MsEbn?T)($!TwhU!mN8=Ls_t>Oanb!Qb$| zpJ-a+Ui6oK?@?8rIE?&hi&WW)&T&c2S;akz=_7_m3y%VIRnJE4$(FZGeP$*X_ zlyv~3;fJl|N$lp0mN!Uh?$&5xbSpa%5R~Yn2Yk0F`v`3k93U(@A_>z0XYO_|)is`{ zeRX=DJG>J=%VGK%{PGs!etkRTYE1RtVdOfv5wlHP5biO=m}*a(QX|C32!8ZjQ>ey6 zfOKA}1mED8pvyQ?%Y3UOF6S{tK{XhAA(iR7G@wJgY4@9dK%!lZ$-%Y$Tz>s(07)gJVP|R`(UzEszgx1h zPbg%rX~9uDwROfb6jEbtkHN9$u_J9lC7f^@aN7_naX^}T+xy}fvxPu-Xgwx{RZ=I> zipr|)V!hRgtvnN~{3&}wEglAJGp+a)1f*GyQzO(uI}#4%yE_6efw2C;q>^-*Xlu3v|Ut}7`glcp0!{~y^%Xq>ipV>(36-0KRO{t zL3Gm!AM6Akj?6AzpJA9zf|zrIe}GRAg`nld;8ANj1h}vcJTntarcY)D3XwV}OlYYF z)X*jSy(-!?41mF%hX@}%g0e;|^pRK;wo&5(IShqC^2HdNERx0zXi^Q7G92lPVO$P< zIj*rTejBsMd2FquW_vcXK@8@D^r?f08qw^U@>bWJkIHwx7^(ogzNm{2AN&I{sG0>> z1cU3XHh+uO1ik3oYJVX>a4dPGvbx(rH#~Ta5G$^6Ly=lSKx+!>T;em^L-kTbP~780H3MWQ18)%%J?oN_S4mx=}lQ9!4+C+ z?fxNUfb!|E`v>*l8L(`$JZ$Ax5lf$4@TOP8guTNs-cExrk$($@#(c1DH(b3RjOAw% zllxta^IoNxr#Bo!Igj-$ohwUjslC2ozAuNQ{6UHC-pJCYvXY;Fh1bMxb^_l|^uT8jh1yBI_Y_o>9~W zj@92G-_#NGiSQahXx+3I({88?`r@$b$f~25COp=2WE;;%H^QhLiKz>!pbmL<_!9nZ z%VO<^i7G|Hs2{zc3EOecQR;NExsYfhfvZ0I?g`ysJwBUrHo8!X6qpdD)9cJ5`?x1N z3D`RO;HjvTw61KeLG;uU)e1O;{bUrS+x(sdM5TJaN8M*xd(n?@6=62eY`Rz_U4R0d zF5uVv&YVby(fLZ`(n$@h_eSRR%O@q&izM6()UC%+0b%7)neSMv&5U*pT4j*OK+-s< zv!{v&!eOCURO1j`&PZhrik{L+$9aziPgmo||j-02G#RR^Cox$brA+bPi zotY2qYe}CEwo$;c2Nwi1??;r%uqz;dh))dq4MLeJ27}kds8$mPbsmf)7m5R`#g&k7AROLIOhMT?pM|wS7F0BP(%uw_Vg4aX8I`5o*5w#6= zd^Q{NWA>?Rd`DTl$0GwE{yx#aST?Wag9fDI`~QU51uQKH?*8@E;Ad&xx`w&)SojxL;afI-r9jBc40c{EjR3~aBGkihVC7@i>A5o+UZtiI4e9GVB z4k<822J7+K8*vG7ZGx#~bY^Qgt^N^Pwt6}8=*HBYTqtN0;QF69JAVK_h%+lIB;#=)0}(O@`{Bn-Tk#m z-3wBMlBN*MXtVH@YIq@iO~#yK><(x+g_?HZZ0Zz&fb5WD@^@JM2Hs*?S9>9D_!m85 ziAUsOM5;&6fdk@}@xM`jqH?M;WfT4zQl}*DSaGNh$1kWpafCaAGtBF;ZbDaU7T1v4 zyCSel^KB1rSEt`^(ycGagUc!pJL7ha%4>;i9Wg{TQu*K4Fjp|uunr;RMw%gjj>uR5&e z43Lp@%LzN^A~vi{9=?il8qP6mM!dfX)vq~DE(l@1{0TlH>$n@P!G_D!Wl)^Kj~O;o zG{0t7eNuCLuX!%Y)|Gx4x;jHmXJw=rEotVf zNew?1ir`+@?Ya{H_{gg4a_Hl5`8*k|2 zpaDhPcE=su+D>I~M)D3R7+uw=L&I}7Mjs{M-H|1&AMkdos3&{~(*?o0%Ilkz6c=^zsoS`IMck^kZi8_Zd?%1&?Yq4}Sa++VN2EMKY z<26#$h=)2|%|Eo9#hI1B>P_-i@VHao`QUnc%=(_E2W{SdP+gHL??QLpH$=Rbs0-S) zX%%V75*wPNO^|jXX4C4x{%AZvhQ|Ag3Y*1aH+&}M!u0au~XYe=33nygd|9L?AD*Z`y0g~ z2Cqh@)Nqt(<+-{5lewwOIU?&kvcl-D-}AE|_&e~OPouJ}w*0(25Sn&{QYF62Hr`Qd z-X1*}R70A`|0<4DXbR=XaPrKMWXfz5#`_40n9`?6nu5VEnIfh&H@w-$z~q7DbcJo+ zwK)ywecpy#}giyIGHyu=w%}_Q>pE zG8mR7_{ohp5$+?y2?f2~|1H^JFEGz0BCazElR|30=L~vLx(7RCe@LhKJl-n;OB@mo zeJttHNQI!F^1PyDn$39D;m(8pAPE65lBU>jpxuHks+{TOYy0OYhm9!Po#~oBBlp3b zIZ2+_2ha4me~P-!s2N+WS+zxEIj5*EM+24dnWB>nOtmWl<5JWlcHJ-S3fVAa2}b7B zL*PA_Nz`7QCW`OBgg`#x_akNMM=fgk!%&|k>pOuP#_Sie8y%95g0dCJ-EZBOs;bbF zhI#^ZBcJ$;%81!GV2L-PblVB6Dp=HXQ1Va;6V-%qBb`HLAHG;y*N)F*;+^(Roc5MT z96h@jj0j>-P_BloKH}&NKCzb(*I>(C2Wtv;tEU!vkbQ*qI=n%q(3F>>>iY%77#4H` zMeT%#YXh|-MB#+%9x5&`6yU;qGgNtaE}Hpj^E_VMuM^qo!{B#P5B6dxY`Z%9-r<7P z$LK=qlw9|3DtikAGir=94@`^+oXZ0!4$juULZ37 z{n?!SRUW|23eUBN&|^P*Bod3KABVLXZsNLqr@S(+qplai`Pwr_$XyA~!g!TvB_LIx zo*6p(K!}tNq6*c}h$Z=&HlN1`30sMM28DuRt3PBQq19wI+5ujS2if(6!*=MhgPll( ze;DBh&c+uyr)4ckrH}mm1z+Y1B7B16iNypE(N1cQWgWAyuWoYit^=v&!Y=Ug*j7R* zSK{*tR8IN{3GRQ_mCf{7SP#Y!xSmW`?(#onPhH%9u8X-$FP{m`q3V2FP`F?Z?%H#U zPAE~pD2~T1_<~0*&C#b!nI)NZZOMQFsY3mU`|J-F>WJz7)ria65uuv0VJ|Cq52b~W zRHF^guwOmc{@-4Jo+CjHsh8UOt($L@9bi?-Yy1`lrwsahDSq#}EWI#EG=hDFLakUM zZxKruOe+BRiqq+x*uKd?)u8hEKt1WS?aa06m&s2#P(IHaq#kI^CM?(v&tWCmK*-VL z(um%uL1$SFy7-fuQm|kUMMyM6IO4C~RjLzIb-#m(%OC!c-V$;QqbG_%?-orP!Ux27 zK{AluWNb89OaF$`w9Kg`#;k7jX|vitn+LDjRGLBIKWU@EvJ+$c{8_CQ20#86a&Rz~ zQ~09Ve_=DSyw;$6pLQCSk9{>CFvo!f2#F-=1Lzzs*be7(qn}~}ituy=URUID3L*9wI`^5rlG3Xb-xf0c*L>b!`4e061_A%=MsYY1(60SrN=wM=hmY> z1RJ#zu8aNPV&C{911Hh;jKFo9cJW@*0zxV+82Z)7MqDhWf_dN@2${{ zEPe=A$Kt%=Jh#4^Zz!%5BXa^%h&o1twI)`6S-WhLc;Nu|CIyM8^y0MIP`SFW3cQf| zgaVxp9W%GFGc;zw{l4M(v=fgkskg-Jou~ury z$3JY+qBwS=;$PUd_l4N>aPqXP`=jmqDmH&;G$L-34%+ylTG7YlLoP zzVb@mmQOPWD3kTrtjBWc^~7%fL)?>oSNDcgx854mBy%ei&P+(%?k!|QnSu_-h=RuQ z>L%GJc>Zc1PfEf>0hJ1RbZ%m9cvhxqOSr^sE?5O5Y`pXh)GGcbD^(V>)U?vu^^zPu zBjpn#RetaDhFIy+IP0+fjZP}||3asPFbZCaDIU$`BfTx`B{I)n|yFi+l*&%hs-CxMwK0Eysr)Vq;7Desv4hF zr^jF^r`|Ge--ySMLS>@Re+NS_mmcIZ>dQ zw4@WxYymHNL^QMEC(9rSR-vfv0zpi($xw3c5$~%GH04&(!`N~k>`V@1q>!bFWdR$O zjqJ{bhpY%`>VT_-=#9|4P8GI4i)CiCE2Wx^DV_J!#VeVnizm`AD2S@QjXnQK`Wsx0 z6;F(O%G(OTzn0f!l>{-bFf{!AWJRP3O&%VL@cp%QFym75)AMY0?^?RgY#n zxgIWJJV=)9;@)wvE$Q-nKuIK!l8HMOh+Xx!|(W0xuJG`(R86%_oc z9u~Hk#-o^kvqm{k148|Gy)lc<{==L7SXFQ42Xo^=Q6^%i>_kNW$f2}cpox7z|r`^-g_{;Ghz(vI>NiDLsX^3 z?O>MX(ZhM}>U!SPpG&-`Vb=XQK$+?-j11;k^q{*el!asMzdppRnMw9dr69^JE=5WuHUq~~tgv?MvV-+f+ zD31T-?GsA6ut1SMb*uQPRo8w;8ALL_bGuUlYpKDLE3L#KH=&gyWy@}+(a}_$cs507 zXs?q$h_BXgPhr9;t

Rh_Ur|5z(u7J+6o#YAG+~)h8c>7Y=PVrVP8_0GU>Li{FJJ z>Eio7z(}J7D3kV{c@TMjVfXj2(6p2`tQBUmfeg=)1r-7rfLUAso_>(!#W2~JD$>cW zyRdYboRp==%?fnl*!i%Zs~E4z4eq@OT6eU*GvCZ-tmgo$ktVVcvB+j@d9~a$tlS@2 zdFdgT@7I0rGlgnaD%5O}yU_B35hw=@g zb1deyV3hc4JUQEIzWd)vk9*FZ8oU;?(Am_?63{!d!fra~osm}B`NKQ8EPdghx9=~8 zggC;RhvPYeYbMsd$@8M=AdW-R5jrG)f&P~Smk(dx5;Rdom+*_vELrvb_2ufC!CD!u zE9M4{VB3Z>ABqfRxV?*{{zyb;pZEJJ#z@BN!Qh%Ln^`!AJvr^$eg&Q1A4# zX-a_rOG{81S8S0(hEphghSQYYSkk9|;@tNp)B>-zC%7b)ve`Hb1QJPD>ZqA;}r3Wk=)B+<7@B$(1H6{-ILcB3vr?X+A{FAA=^ zPeq3ozp!&66mPCM<=xc8#wp8G(F*=kB)kMwF;2ykr*=Xv3tO+ft;9liAIMVsSLf#o zC3A!!id)XKb+xViRbcKyt_+ReHM5lpP9!$$Z@_P}RyRGY4AE=~6IITjR~&QAgNTQD z&v$>xg}KU6;U=GwsKfLil||*31F$c*-)~P{0BA- z(?JA4Jr1okMS2zoS&8y)xFD^%PEVgA#U?BxXP%Wy&BFU`!6VYp>6Eotj%)6tUYt5p zoC(uldf8NlLe0|tb!H1nh*|il_PDx-9eqdDunllJNZJuP{S)GwWZSZIkv`J9`Q1L^ z98gj%c1M<(%@T~isUZIf%_sBSOv(k(3ecr_qFxGBARIA2He*{o9F4wnN+@df4zhanT2HI z22K4lo{S@u(Wn&D*>{LJn0B{Zc4&r3TN2N}^APQ;H@bSbv?B0^qa&cxBW-j}?>`{X zorJIpx!xS-e#=eRJF7*?h$rB6Z`D{t zndQk~yrNo+j?||KRq_4CR9B`MPPQGFdn%;7rSG!$7+a{(eBv4bGY$f#E_CN3=H%PU z8!O{oCa`Gehq+D=hd_;<)TzyF~kKM~v{J+$&gAEKr z{xGsZ0ngQ#(D22AU#t+Q8z?mjyo}u<@glJTORI6lh^>Xue}#}jD-efS}M)j6~C5p+YzpiMWPKkP>K$j&M9#~4tivofko;D zBNHW4Od0tjQFROw8p9=%e6n!qiy<$M)GcOK4q`6?lo9S=6V7GZ>Dk1T)_iM=NuAI3 z;sk;oX5fOS(Q{w)d*Mn0qmIIa-~IgFMgxQlTZqCCTmEILT9!7iOai`zW?;Sn^*@C* zdYqIGV?6)#mHsy~=>k^QG^R$n-wDj~%xXGtu~$FI@uA%>hmTD}O`AQEzI?+2QY9EZ zR+@JdnHXzc2MDDr;Z4E8Kzv@$xh}6%e-XAc_6#ifL%r`a(0K1v^eGl=qCT9xqcHC6 zNe%rPU!M!d3nQHhaj>H6tlRNvC0$pJBf%Bpa+%yG=k31g7gtnl=@$^WZ+f@?8xM0_ z3f{c9%tOY*6I~N1vBcRw?sbxwpDTi;_(A33VDjWE;HwL-`$L*l19EajXTs*&o|w~H zC@0O&NCBB6AMplyTp8D-toyK!f?{HjcR?zm@~CO7+b6~`n0TxioAaZ@0M0&j-DI6V zgTI|Pen?(OwolJg4jRnVWS8xsJXc(JNHl*qx^2Gl_q{x(DPK}sO8wfpjubd8!+u^y~B?W;IqRv z{R$MjqVvDi5EQ%NdoI)`#Vb!GhKP4kI^#M^>S}6oZ2cQJG&l;=;eCazr57*N@o;6atvv&OYJ@dG$`{v228Xl>u+-_PeAQiCH3GnV{+M2yO1# zIx5v#OdP-P7zvQjhTJaGTU4BIXMox>WD8>OUWo^T6DhgJ7Xdb(M`I5bxZbe%pLr#3 z0b;`MhtK$xE!%vw*ZUCs?wuaH?n`{S#2sOK$cw6daRvyql~YE1>ZY$@a%JcvA4$Lm zIUs68ggCU~PZ=o@gumaAVs^k!~A}N-AH`N~`=(c&`S?|g% z^dT)TC}?o+R;F)CYYWtz{awLo1|GpQGW{WK&GeZD*T8v`yk;lmZ9vXQWWTMHJPgGqYz3AL{{f#Pc)aO1)ig+Qx`3CS z!Fg|c-a?ORX)3cB8-v5-&zR-JROW5Qz~wNp;glR3A)9Y94`@DM&9|%*Vn+H|!_fBo zt3G#8KuWftvfvlX$nRiVvd41T>__tMVf_2dRIwRrp9`ypdw19C(fe1CZh1H*B^*AG z{{6;iQ+c|Mfu@9RZO8=b)ru7PzCGE|2W_)R28Qb+vAVdXrnJxcwCd4QF%D7!FOlLoYe})1WaS91wm;Vi>7xG_ zAbX`*_4c}QRIoruQ{TaKy^>|@?0+unrt9|04xROcc;Q|AOgOAwk2wv;q>N+Vzh~9o zSnJI=VB7&kXB;22RM2UCij=1tg3C)?+4DQ5C^1R+(L1cv zxG}Kq?lbe~-PJ7aJt$y-?W&r5MdhpF?tSdVRy&4>TD74fX!Qcv3AxT<#V$3-E`E(9 z0Kq^6BkKIm4!b!o)ZHS6KlL(I^oa?*kC;wJgtD;q$DYJSH#aE3`%AAO)#UQx4~-pf zY@1UnG#XvCW*@fdWm=D6a`yZ?mv>vMebU1Oxryw(#V+2!CG<~!$U!0N*gI@Ds9|I; zd|jLSSl!!Xen5bj1oy4PD^L*QXyW~a6T$*9rzC8{ytnEi!HUL51AU0%G zn%fNHIw5C~j4Gp{l)HvKSUuaVV_Txp(`VlHA%shb+4s1%o^z|!GOu8e&vv;XYLRes zKJq|qb_BX&X=a^6XoHcAj0Rgop-_-XLiF=7p^@qvkG6fp)tktyKKL|dCtnh$sKu|J z;Cg|m>#1YVLF-Gf0Rbnl3Y#Q8>N7AwisW=n)!Tz6W0wcjk5$7QMXBCYV6Ds!9?L< zB#*I>=Dy0|2(FJFeY+eDMX9P=&f_tNQkX^Skk<2p^SD>;Ih_X5)UXdO>1QHRfW9~y0SKpS3$d+-C~Gg-#19+gYV006ET_JiVPvE^ z3x7Oeck2)y)`d2Eq`JMLp^#b(Pai#(Mq4n*&>a_I{QJ{+c)~k}p(7$lyhDKQmf(8Z zs-mkrJaq3|mev5f@xzho2C5I;DRLBIR}1bU}b?d3>)(-x)rvsZq;!mi>u&@tX!4%A+bv0 z@TTR7m{wnq=dX`To{_AA*Am1~1UkG11gl)gW2pLyxaL6$^|^)SSTq|P zcQnAJP<7j1w7p>$>0&$BeB!XloZ8Aon}P4wilU z@dj4OE@!j&SzGwwBhF_aF(VENCf3(Nj^nwxb=kU}O~$O8z*)~C5HPLhVlyT82&!yA zhQIrwc;)CW@;BRWaqQ2`tSq!VvqM+T*A;;~X!lnLCvKpkta0#J@%r;F`HRrml5kI! zZ834PhX9XIZ$%|vq_xhcL|A9NEF8&AmbO2 zd;5_~(AzV=|3iuY)TZx&20Bg`SHd2X+I3W(%$kEHcBrdB^LksV8Q?j1NmcMSL^Q>g z=X`ncIBV8zRbW{6t#}V7mRXc;b{x{ zk5xm~98FPUvnV1PU<4mwXA>zNM;B6jG|?118O!W(lL!{hcg|b}g>I@nUD7Aa77Zw+ z%;aLsX3J+{>r+H7f`t4V0+z&{XzoI65#1XcJW4}gvi9c(kww;KX)}I>@4Sx${mNKU zwM8&!`U;olW9;7OoTQhoJq-QL)Xjk;B3wE`8R@j~3|ON-{>%1+Zi-}qSf{-S@BUI^ zr9{YkRA=DPwXr2k&l>d3jL=p~x8Y@v$>$ze?Q|&5#8ORO$anGn;N}5}qCJBy`!x69 zfX}&K#oa-3&?ws@-@;V3Ft-6&Iy}uiM1NezF|Dwm5tJ{GtXM0qE-1z|+dtehZRG)# zpR|%smAO|7?GscYo#)~QT?B$;5nPGzj700hXbp`>8%`m;C%SQ8`<|b_Ad&}lA{4YV zqdQ$R!UJ3gzzL|hfx^^;F$KPOrka65i|MC|o&(aUKs`zP%ThVcbnAVO!OsS|)R#j> z2c=xM(W96$T8J@J%uwk@(h4KJ_aCEMOIZ0s7CYkn!K>~Xw~&kZ&*sb9>AI&6-a;q% z3XtY`CnBUkaVkWBhb1BY>65rbtx@wLq*J|C`TZhmQ{!1@q^go3S_UE4YRN9%)rT}DJH8Hn;7rjZr zb(_9`>q-CGoksjXH2V8&AK|Z}A=wn98%EizUeW2tvI8lsdrT;u73hKlg{ON9_qIZ6 z9pAW4L@V}JEP(9&jiL9FPt}PV&%A8at(ihqol*LbZ7s?M#YYQeqItwu@sA^C)t3V5 zz|d6&TV{syqZDq}D`kS*7wy8Jv7+alvUc#&4R(h<8-d%@f`LGO#L~G>v-A0+2$U29 z*>Lo4zx;!bs=6+?Dd?+{O72xX)i8RH8PWIox!t+2KCyXil- z$5G!p_AI}5%ETMEVvOszRruQ<3(`~X;`xU}9I5@biHdJb<~(|4YRN5);nV(pv8_?q z!{IB+^=+Y}rs_=k+#@j-i4ce(tA>DNHJNk}H!Mf2U@}MvJs)yEnMpAZeXSx0)%})> z6deuXoEqo%_fe0EjPzT4=nfQbM>4ZXo0uAIq@5M-n3N5Y=X8yXgPVh% zAW4Jn{>75hwtR5$SSpM8m8&2{I+be@a4Isc+ifK8a2cnrIige9gF=*&9nRM@=_9{wwHj{% zG(UT@N_wSc<;>^CX7=Y>q|}`5&@iWR!Dm#5s+1>#Ozf)8mZ=xTbN7U&i067yx*w@+ z+ubhqbW$ADOt*hVz;1ZP@~DUuzUxyMJc`n3b74?oGwN%#QZA%#3P~aN`a!pN>p;m9 zw;g%mA5^K!ezZsV&GA4r275(I{MVbjadM*LYwitVUJ_KZGkw2sKjF9g zFY_G@1Nq+AK7*V|wX7&cZ>lR?2#O}=nJJD&Ll3(l1Y&+29fU_#eY}xuFE@TwOrBXn z-=DEp#8Hb!i4p3)(H05SNtSldaKjv3!jO zUvat)Gm*yZ2LsnLs!(pJ2Zu#v6O_sBC=6_F^bWnxyQ8cY(;$yF~i;7d>5yu zgZSRDwCjMC&COoJvlOm|ym4y2?c&oUu2$12R~&&Db2Cw*rmDSO@|PHlK$!e}I&=|f z?=v}JPav0YcPqXowZc#Ctr32c+niOr%D)p^78i^!m|Woi;e~QM+1oy2~~$=n93+>?-LewbuuyPfcq6 z>er>EnE>sJ&K%;BR*U!~bmgIk0=e1*EArqc0(%ld;8VHC3*@y-(nMmKdLe}b8mTsl zASl&R07_Xgr7~lVqQni%h7E{pp;cqap5cb9`{h9EnnhvBY(jJDHI4gqr;O~z>w`OZoyfR)rQ+VxJ9a|c7USI* zd)4zxzkyKM$?q)F+s>5hi}CvjgmgR02e$6G>jGoPRg7-A{z~^NulmgKkH`9SmYL1* zrI}&wEA&6z)K|-|Ds%$ci|%uzF^jR38Q2Pnx_*6W#^9zzPUH4r?e_xrrDeZHFYpix zb~VJl#>h5;V4xRMo>s;^KKd=o>@A69M1 zm(bMxMvQUK$qyZWwt7cW3;DfwgOTgW`*UwEtlfUY-M}!x+Pm*^3dIS{ey-#NkdjK( z)3Na4+6ya`fPeISm}&<<+lVrMPO|4<>Qd)quBE(kT9o_I1HD>jIM-SjT|C(r?OkM| zb~g(~nrIf%D4rGB4<%*hnHShFl_C_oKIB~Q5cR{$+3d~qjNr=|!@-q4&n6rIyW{sg z(F&GRw)=fZ9LOrj8+h38a6X19ZsDALcw0>w8EGv*4fV%4r|(ZlSw?@CaNkw$nKLI4co-w!GA8d4(#A zqeqFATjWP~A@cg}t-cH2+3{eWOnumB4DjZ}v^!S$A)OC&p6I_dv)||)=eeCW+pTXkDJKk^Tm^5Xg?a8xe%%q^9TL457QXAR?RdyKd6-KEh_Mv0pN?Z(wJQ;~+>g71 z{Fs~Dg3W}Zy6PZVQnZDAp=v#?Z)&N z554d9d6V9H1>ak<#BlK3#!r#V7uUNue!3y=a`^~Ze8|FuL z&E?FoL%%tCZU64r)4=z?|c1Sv1tJI>&w$#x^* zy$p%YD*eWWqWcW_LL<$I$yM7Ds$-tw5$r3e7R#C9V>G+m*u)GgYImwJE_FKlYMiUL z({nb%o~HD{W((P0%9EUqkNU%Xz}k!Vf3i%>u4C@6>uCZ%$GwHpiK6|m;$3>)qGDwF z_6&Gb-)us_{2B-_dbZEDou|)o$6fNfPx)iIxz(-Z4sRy%aWaz5zkl`2?|t9kh_n3` zar353{XkV0N zLH8ZI|Fb^zYiek3c9`|oMUrxVyLtTPESn&2&T> zj!to@0fxNSO3GZBce4Ty4%DvpJLW9@blBjSWE=9}mH4sAHrykBMa)Zq<#FSr8K5zm%H*nikma?9})^N%@r!oa#> z9ylpZ+N^Z2+#G$q(KjS)dfR2Ic$sN_c){rRd7fBRZuLVtA@2xL%kLDDk?!?j?as|i zr|X`wH!78b5q&L0-`x^(+TRB~JPm0J4cGmaBGiC#wb#0DA*1pqOEF|m4K1-h&<30z zpHgzR>85sucXWLR0QF5y#)a>8BBzX^;%wPbUr?4f z6hW{aw4+>2-b>Pd9Z1~Nj+n0oP7agrT>Xj>xu5z_zO2E<@9F`^Sh*aIAi1z7KQWv!6?Kd25K$b`db^51cfp*0OCs+;{Q#Vl&oeLCRuL_6UB#n)qhxp*m$d`{$jVlI zh@)?M?7FPjH?E*TwyaFA)vK2vw5)n({2eVZZ;&c8(Zv;5JuJ>DsW!`-DuXxBG)$5ibJ#S1HxUb}l@#a|-Z(Dy;c-`B zg76AdyrIbgBdnl(W_*<+ep1%k-qr|3S%$P^-?eQ7}F5T7d!^YXiyXk{HPz zJYe5AnN(QFCn|s+57a`J0WDt3_mA5Wyl^}ykA{! zb3xJARc=e3CH=&k{I-4HF{3q+>4}i4q^VP@4;P_Rp&hqev}qaO=g#~a6`sssuq9t$_5h|T1-sj;5Z-t#pND^#k$1rA!ZKl5v`$ZtB#S<4v!XL_vT8TqoblVPVe8DPy|_{T_tI=MakB6hH+E~%vRwSw4|dx> zZEuR&NP|7;DSbTZv|Zy(n)s+@u4pCmJb_LNa)uNRE?&})wX7yNV~v~XLkD4GCK-@b zm@nvMFGApk-u~SDs)F^|!a2ddul*}_Io6GRVcQ(7t?P!NO0D(~gvo?O zkdfiNIah3J&hbSZqYf%K>V12bOT%w`v!Hes$)N||zW}TY0lv=icolAr&O@Mlvfs)C z&P4U+=Kk|EHLKCnz;JhVS4M_3787xF;Uv=aU{@=pGedn`-#IV#&ip$i#u%UXbM)`= zv`*O*2YVK?csWsgd@Py*_HW_q7GXUD%{=G|u+dNZGTf09qu}(+S=#5;fxg^kd}$gr zX76)L-7*ijby;LT)W1nlfP%|gfmSIWYmO+-p9hJ#FPcmJ%9Q?;cARUeu0OIj`{I1y zC|QEm<_BxsoqBoz*#~ZtL}X=e;Z`*!yLqEE!CZE^Q35EYs;O znC6<#QVkL1o}skwBCgiceW4%RP32fnn>QF^h^pF|HEH)en!*LUikI)!6U7Kv8TbA? zPFY?_5dR~mG2h>jjbMlIj;P(pHz6ivZKARn@n`wDiKkM^?S+aVf`$3oPBlBn3j>cG zR^8%9+AM!Ke4NIsgRwIa;Yb|*Vt`d>Wjg+1W7e0rwxX92{+uA@1D4M^CD{tYJ^`hE zXjV1jx(Rl}jd`)t%OIOurWSn)0ShvXWBQfZs}SM6El08&mw1OoWt&$IpN0e+EfOkm zLyye~BuN&E%|h(UCyewS2uA)j?S!eZFp}KVKYF1kDx|i~`DO}<^ihb?e`NlP^oKs0o zg}S)bng1;xMF99$jV8mxUkaOHw&AArOacEcjY@P{_7z^v=2~bzS0f=dTIpJ*#oHYugNt3I%{uG!f z8?U+$|90SH&~<(!hwf+;hp9;DAQZ+q!IjzTh9Nttq{Q5IZz*1jYEdyBxYV6^HxclW z;IC{w#qm`>{Azc2oH*Ez_FDmXR_y}V_H(IqcSx-Eg}&>5u=W-}akSmKXbA4^4uk6; z!JPnuySo$I9fG^N6C^-z2@rz2Yw+Oi?sg{m{(s-S_uco@tvaV>rmBl>x?k(}S@Nv4 zx)<|Yi8G}iroHdSxM1sFU(e1v`Iul&cF?Fsn%}DDF`MrYPW2Z@cvy;qFdwpSPu{i% zoOL|zmxVTGbKh41Kw)zIo?z<(57_$PB-_sD&c>cRw!JCCywP1tzS?reBPb9FQ7&{e z93!zMbLZsW_wu|S`}7d&2aDSJ^iW8ESva9T9InbF_sZSP6ReV^*L8nRa9YWmxpu$@ zA3su+qLE4A9gXXeFmMuN#s(=%ef1>uhQ8dxsuaRDtU zf+3mr!CARFs$)qL`q+S<`OFInzm?d7EvVj)S(B{Dv5MR@u^Yw8yoa-5Z!s>vhQu`! z0SDr^?vsy2&>t7sFv1q{SGI~9Z<)6d0e+e?Y9ZRuPY7~8b{H{ex# z{NO0_ZCMJ6hl5c{O|;9nSA<7qpnGT9H1*}5LS5=LQBQ&o3Wn-cuKB^#cne^3*p?*22df_mgzoB@OJ z2Okq%$(B+v4*90{^{;@>0H-(1IY2BO#AS&Dkl=qbUU=*j}d?fpn{~x&PhGxU2A4I!r3zZD*x9OQuZE?&MfY zl`?ttcj%)Z^iM9l*fq)Nu>5E{v-?fJmk!h~ZB_<2pE7@BMOsN{lhBbTP6O546cvpC z_g=uzAGQmqh0L>GzPOg|1F^^Ay9}osvN!s4^~8HlU+!x0j!pKtmVoIZS32-*2I1BH zlRw(LYguG5gBCbo$Bs~0T||cr`DSIhitx-!Q0P(`)?`gx(N|ma0pocr7EVa@$ZRJc zM9UG+=kaf=n*dS|oWz>^T6H3b8*vB+3+782{WxFlF<4^xk=4*GA=ndKLtJctJ3d<} z%YtLI%rfuqM#*BkK(g}4vlU|6RP8O%{mA{kDVbYXy8B{f1manRDbpgbmb%lC_TP#3 zJVkU#U|)q}8K{V{+0at~AcE3pD@GIF?Xfd5y*+^D4uz^Ug?9zpM03y_O$9AQ&{b=| zoz$a)8R2p7w31DaO88fSp#mur(hSk+J2V*xxJ2vWGCfFI5W+5rzFtOrWGx`#P|kQ5 zFFe(oj`91{ZSoBwD3|0wHk~blF_ljL61;po3!)9ff8T|G?~*{Flcd17xV2=GGb5`- zl@nnfh0urx9Tli~GK4}(6?+pE zF1qjqD|1zz$W1|I*0^gGDNu@O-ej!RrNB}E8 zB#pj*K+g_Am_oJn<$e~6Ur-cwafHQ=!Zto4(zFN*Zxd+V18^Er>Dr3H8Ve#X-{>EU zaRlo80uN4NcQ)!!<=0&ApcAD_c~FuzU;3P@_i!n}DdY7gv=Bm{p%ZpWIeqNbmN$9>IK!6+23A5iCg~lA#Z*aWRIG ziwC7gTjX>`d?ynNRy0w*U3QTKD^UHMz~ffNj2!5OikdtATW)he4os4OOtrGA7|zE4 zo4UOlv=Dg4Zvl#J7vYIOup_X70Pf_pF1sQ@GbrHs0whas>eW&>iQJoo#3~p83W$bT z!0<4Ca!@N`)g~iUL4z5@^P1@ayZd}ZzJp@(G2L>QkjYo6$Jjwmo}`+1lnCQ7yl(x}_o((jcH zAJjv==!lGqP1GU_GSoBQ<|B_TQTX|6TsZEYxx8?K_Zu%zzq7C*Xdxq|G79+du3zmS zC;P}2IkJ5EI%(T?=LfHp7g^Ik*fBYO6ucAv)DU;592(-t%U)9&s}v>p_9eDcp>GU+ z=}ldlb;(ztzLa{9*#4rZRUGSmj;eY{(`l`^MYZuP}*QMSHSzsh7+h0-AohS)AAX%%bg zoE=!d30P&Kv9lMu-~maD0iFrmXdFI^H(P`);A&xlNEf6NY4O^*QFsg~DkW=>rt^2u z=($`{@XWJBPevLl02E? z8$x$xziNIy4RR61in$N%jY>Pm=P$MK{TkUtcQLq~AAS;ht3V)g(l2h%Yh{Is%(5=1 zvL*Cb-gVuE*a5Un3JgC%W%bDyJrzFX2MNo47OB^!ox2K$a*f_GVx;$RG_MOH5<-V< zmlGMbXjHD)LB9>~Y|U#GrTpU0eXB5;13Jd0*Ajy{0tk6ROT1?}&X8cVrkH|=b%fwQ zVctX!V5I_Hr&kgkd#u*mqBJUatIdlPAr<-P`R5l>Lz!Yt`-#P|G|`JuF36cbe`iDH zoBLHzP>@G_^j88B7$k^L{{cU*B}MbN*~cyNwI^H|qfZULo0C6;pb2e}U65nhj*3p1 zn;P~)7Jh>$H}rGHf;?>P=ejkg1~*KsJ_otQrtEX{SayM3WeE-qygM?;04~IEiMzGk z0@}{?a&5ZOXye+?%2<7R_}xYg=oO1^H?&)Lq#=bfr6f6~DM{P8vWob}#n-ASu^N}W z{x}~BP*D|G=|X|)T{>peGB_i>WQ`5PZ+ikM(gaTpaG^N3UHw%i!KiI44f&nQ_?<>| zXjMn;aahW(>KqnC8fIvB_uSMU$+8I+wYk2ezT779Fo=sq$y@+S}is_Ywv z_Ei?1i#{e*^3>(QUYbAgRWsRcbcBm6gbcycuXm}zF{v;aPtZs}KAA($u>P>O1O zPUG7M4Tg|gr2zQ>{Q(MLJ1+k{A?&at`Op{iKlcKFra>UVyZ>;D{uL+woRF9PPluOg zJm5>=ZODi1Z3Y_mh)MpDlW+6P8PeDW*iB$Y*0H>zVv2l}WE15P{`p6;)BGC{V&tSl z?y~IaMk-HiL?9WJOo4bFu_)VWPZ%bW6%l*ewnDf`o#+Qc^49iuXE-ysf_lF#Y%l+l z_#5Up1D30cg9;9mMn4QTEW@|zNagRGlfaRBdIPKs6aK0IP%WbU*6$O@ulnqdcJ+6I zi%kRyGeowK?$^*+UQ+(e1)wXQMg8h17>UC{Ng-xgezcqr+MU@8q!NDpMJrL*{zen& z-NjlPQwL7BJ&!Z>3u?|vAA!Y=@@bn)bmP@N4uH(@FIh_*=84YnTAQzS9i3YHE1-%; z|5AbqsO@^49JuQ~eH-`QxUAjk)KMlv#YGI0#9~iKPUCcGdAZV-xp+74g9}xNuV(0I zqp=SkueWH@s?TU)W=%>ly6f(5Z%JG9NS2FDaGne70323KEe)xG)m$@T6*E|9Y>B{X zT$A+W(3hGyh6ryjBsZLyy5RtrUhPg7U&34vU3amBR-NNVq9 zKrb)D@U}U<`N4f23;ELfzG8SRSxa(D+^xqPFdf<_n^_dJ-^#b zB5$bREZCx`Sp`k)jA2dGp?mk_E{~#9#CR*QhuAT+-^qkOss?jPyoR(3c90zbA@S?* zj=lDxVvXJ*2T>vdQ)K61_%Ig`T6%}7Hqb2uu_F1ht__X!L~3*HI1aqlVb zdL%1N4i%((-{(+;3^7YRAwBg&(lGV%LD^Zre2mvQYf0?ZE=bbOi?a_ayR!HuLG6si zJP2fuc-I`(r`Y7y&KO1&TM4DCmg*bvlN|I~fKqW1!qG}e+uE9w_ydPvwUPL{)ds*S z|KE+JcFe&Msl1xJOie0%bFhe=td&3OOs&PR=PIz`4V?ynA1d!+@Kto!`tuO^B@JFU zHD`1>yjoUED_W{d##S5&TY|I|xA}bpB12|kS5YSXDaV(AirzL}&-!IYi{HIHhIm8( z|6ut~l_|1K*to@h!)Y+A95~bWkxAEk9TS;SE}uWI-= zsbU9n#*_(*C=f_}OeQ*F!_>9!YwMI5IzY388R1|-v1zb<^)WE39sfkRW>vD|uUBL| z`6wS9`SAp64(V&9#EGjn;J~8*2OIn4Sd&W>3~xV#2Jsd2MQy{sD``~X4_FJ5#U0f`9gr2i^fQSc{v3~v2X{8!9DqMr8;!74 zhO|YLD;jaC#iB>0WG|X5elZFcFT@mGh?bazVD&Wk7_w^kUqp>n+zHi0WNL9oth;0~ zPZcq4NgAa>>djgCg4OXNWh`hsA2kN(6eUI+#mItY1~KmF4}qNhL`Q)(KXW|TVoanz zw+;biL4@sGk#JsoaWud|g##Xtu{}!n+MHI?X?;8sN`jC+P5Yt*_3}^frg9-3O)B)- z9INvO{y5gt6StYHzIN`I-kCrHVt4U^UMpl0M6+9(6$LbK872CzBYf(zf@4^Q()gL1}l&n(C1g z>v`iL(`8$D43Ux!SgJ}j5uhQMwlME0nu$j~vz+y>=t`eBS?|%L&B7IPHGOF}=sHD) zysmRDJt+h@D#Ue-8*8icOn%lxOs}%HTbTlB>@s)2{M?YRsbFsq4a$G619Eg7g$b>O%!a z8S8}(B8XaRXuO%w`9?xNGZK~Pzo=0)z`DwciEgL3>Pa!1#j1oY2{r<7PyusVO4M8` zK$(FzANVO8T4uR?LyD4l=z1KV2{klE6vakXiQ^mP_ry7%Z@H>`N#hmLyZ}~JabOlB zK$(mmp3yDEGzz%a%du=olNxJGm@CS00?PeJ@HB|!)fT3S;BYOC-HsVpM7VJym3M>aQo2d6ITEZjwTIB&MxgO2TYQ* zOJP!QX6F|RU>vf^vN<)z@C}R!yqm#1wpS-;ZR1H zpC~P&)JXY84yNQ^6Awo8Rm8HNuOPz}LMQ9y!egU4y=XqBajEc+NE{=3kF!gfTI-1y zjByW8T|BtxZcP_8;bKk@1%Vp`A@whHy1)n@vs-{CxR5cbaarWNvMbmv!|SeD8)U>Z z1*)7FXzKa(z^+D_wCIjL$u|o4&f56JO5AyZMQTSGM(<-(&?cx5VUP&*!2xACEImW~ z-R~abQXM`6`W}n{_4j4;GG`?mBeeUbN}~eL|HnNzY$xLvrz)fYhAn^zgt$g}GEjkbarW7gD z4x{Ioxau5KK_E`d-&Ue=S`pr^m*!1dviYT|_S9Cx2T4EIA0NoMc#uaJhA_rNN%?ylg z9)4s9_S&4MVI;;MHl%}%%Mmm)++U#vvxs_<8#cTP!}IT=p8C1_B{%jtzDd$(?Eo?k z)P>m-k@htuLM~K89Z?j%h=YMPX1_(cU^SRFVlxAs%wHQ%k{rN=J$;*SgV@#;R>5E< z?i}KX%7*4q|Jl;z?e^BZ4eWrR?2jX5vA_R=LS-x&|97CyE7?NxbfN`t00sDe95%%P z_j7@N9s~XoyZ(cZ;A45|uspaZteM>gr&ouQPE!2g;%JvkbUSMOM>QN&F5<#R+i0FM z7&~pYQvfl*{{Iit*7M`de_`4>S-y2r@kjl5nr-rU$;)|b^O(;da+JdK9KD}|{Ovo& zD3RLYuC;DH}il1wy3nNV2g`xulc`Ekdm-Rtb-H}@D*P?3uv+j!nNn3-99 zd(95^u(`>GN~EuVA{p_e5!h%Zb23B3cRV@3$&!BB{m5t;nV{hwWHUTc9vR<>UE?40 z?Rc>FYjxq4cn9Tq&pR`=fjus(tWS57Fk-aBWG9Z10>9k8+Y6ts*dwlQ;!HL$^cy8vKr8Ibw%4yM8l6LvuOZdRf!Qa3|wl2Ay=K+(RhrTS7#DM|` zU-|e92SKK*&;lF(ED@HD7Gqu4 zeA|1UA+h>6>)VQv3C^^i6DH|~E9HCK8+Aoi0m`n|E^GdVT(Y+>zu>*?I>fz31EcNT z67SelT{TBr)Sk`-g*Qv}jUbQ(v$Mgu#3< zaY1wqbo0rsIrXWheClgM_VVk*$Ce{D@kFZmSqJkiG{5t&IY^%gHVK=!HuQmsuygoN zH_LmAQ&D9)+;H<({)PDmSB3zo95?KrB@c9-*J{TZqM|dQ-4@j2A zh!F0%hCLUWHGkK#ZQK_4x%;X2#89IpD(j;rbI4r4PqWp{z9tp!gdis4M>B)HOhkDT zkK9;r5P`n;@x-&QFW=ItD1sO+vx%1%)g6CR5Q&1$_joIC+?h4bgrWEsYzzqPL-)(o zE!1*;2{-Je!HBuY>dC;(X(N>RTF~p_>SW9G$k149Ey=+=aMbY+VR6$;E+_GH;PO z%(=4|i7ZOzS|_t#po({6SghKR&M;sgIhi?Rx?VQ~5TB`?uX|zsIOPP#zsTaR44?Ob zMEyxcy=U6t{x{w_SQkCE@u0tM=1d4-1Cak9#nPs>m`S{Ug=(2{+d2v$KDnx)#!V~6a0+BD)KFYz zY4at?CP>VfUAEgwq7t(#U+MaD93}?4GHX;-t*E7aAg_^^M5eVPv1%EZ2 zKm=#5*W}7VGtWUwffyT;RH|p3Ujfj8m=O99lbf`|tw- z;9`IS>6TwyWu)CyYMXs;dCfxpFn!VyV+3SX@X6%t-+L$tVLEJ&m7I!3-}+sizwGE|7I_P ziL5&RCq!1NLkEr`D?(u6uT1q(Ei!+Ogl??c@0_%s?dl5?y79*1ncNGtkW5FdDyBG+ z7pAZjoqrhL{2y{$FAhHQCZl63-kEa>oOmS&#f>oI$@a`}U6ot7yWXPVuP~14i|ZRS z?Oj#Y+?bJgJASmN?i=953w;eaO3fKEvDPmTgJp~HsjG85t8-V+;xT*GlTBBQURX+2 zVV$Bp@;KXqav#kcs~K=ptPD^F#{Hly^tt{edEBPHt*eo^G+TYOx|I9AA%FeY-g&0k z_4zUN?);E`M<97gp)Qm7CQv0zBY%3hlMsT&6@$ZX*J!1R(xZ`X>p6g`Li`Y#in1_W zJqu(W@C-LD8nPeOsK4TXl>N_)@Zr*cNz8cfuyms9cU>L&YXHzVIpV5artk7ZB;QGk z1UmH%exvSzKt)Q`u5IqAN-Tz&TxD>Yqr`#Y<0QyeZE0~UA=qlWqCUnv54n}a$Vq-z|+>!pKUmE zOrDccQ)AFz!Rt}HQS-s?H0?2;jQF@EKtg(Wei6;qcr_mSNXZLw{uDRYLwd+$v+_IA z=~Rkdb$<{nCyq`Zb#!~qNGKjFhC^>^GESPaQIIul>7v%UJ%ow-^M~v_7SD z$vWgw6UuaBH{Qd?$I<-`@YtP6{Gq9M8wwX?G&Kas2)OBcHD!GkKtC5o5Wo2Wu7EA>QuyiT9_*@2SDXjr80fWNF@J5B&$Pvl9)b#^n#K zvy$r`?-%s@fd;~6D{1VwXIrSXM{sswe``oYKX1elb)Uh!rfr;WD6Jzuk9$<3HMJG@ z8z{qlFVGZKRoP4%DA&$-zL%7AVB~x$y;9mC!+ATmvtwN8e0zS_tFR8wZ&uhj%=ml_l{DLa(OvA_!sm6X-Dksz0&wZ0=F~9# z^wG2GH}dL-7F$T8K09o{``Y%Cn?x8K@HS8PKe=4@1&=2FjM(j8S}L9*Ml?~;%(W7F zwLO=+9e^{4t7r9H*&kMFAefcfJ85}vA7vYXqgqZWO53S2Z{mbW|XT%0TX zlWNt=9LEQtYum=1wTR+Z6EJHPZ+d;Bfdc#Xw2$MUA+~0z20_Yokw*K(?Hl_Z@JAhz ztIi%J{Vyka2AM(Cv!c_hpH7vrdN-`TqqL(9GcP+jm9?=e*<<&PAHz?W!kR;GMZ8^3@@cDAQT{#ZT7WhU*!u`6dv9vgVRk#U^=ZP8dueIc+|Y$#YJ_=*)FwG?=TFdm2;p^*J@0rOolI(TT=#F35=$))pk<4 zL_$rCaaaZ;Oyg-og4X&LLp?HMHCOz1>OoAR9=?qK7t^vU_5X`$$*1w(39Cl~`ON>D zaA9U|IWTVjZvuVa>fQG)Ty<|=-V7{Ke|}oS)mg<|l+X5%BUU-0l9wKuYG})lvqrm~JF3F~&ytHW)x1v9+K@bQWa;nsfVZu;P0<>hl(F zGjdsAt<#cNF>!SPhw9_Xu{XMKCtxw2&Eq^je_wFJu5YI(Yw!jy_@O?dq~r3k3^RMC zJC-ZX@$TeEE4L}8a$}~W!fr(7*=t$yVS2@2%Xxglvu!rtXKHMeos8qUdP{kk-66iA z?t!tRC9AY4k-@$_itj7wo`!*Eh#hXt5s%L=^BZJ$r;!AM`XMx_(!;VAC@3L z%dMy=)LD<@UGC+n01V#v^lgZ<4{;U21@~&*GUdI|e&@3qZ)vwR5Zlu@_2Wt<)VmOm zKuUVpNba-!oMn=y?8IPNEhpET-nv&Ug8xRpg55fA315@t0YdJm}CW9qG7de?B=2dx(3xL6gKV4AYjX zY>rZ}X$c+v8rJ+a)KfxzzcuOD06)<>YtwquQ_*40>?v?|_0v5QWHfa52%*d%hw~8%iUVpWp=VTDTN2vhxOkmBu`-edyx?m3PP*j8p7z@zbmzJM^3n5vd>(g%P1JiawHsxzb2$wHf7c^h=ZDWnSZqzPV$NnDw6E>>83 z@Xltv#lBt9s4RG{8C>3V*b1E45~%s{-M#DyT>Bi!w?7<0S9#KnD`3}duAZH9S!+Ku zd3Ow};6qoTQK%$dC0?1xI|)*zEoZR|2o!#zYO-DI4%-Xy+*potFvNmn$Z|F7|9YUY zw%MNfbbpw3y2Y}>a&gC!*Q~Z>G>)8=k-;nB>`s)zmtxkm2C`lI(0-%u6TR%4p;TP) z`24mza>SJTN_uzE5L}2xDgCEIqo8pMS>A4~T^NS4i{q^lgIDSf0T&4SDf~TGv(b~? zO8*B81M4^183Nc9Wt9h2y$6U>-4~Fwvs|_&Zj86Vj<>-JusX-FT2dE-OZKVuSpTZEI^S1T77H$aClcNW+r{%pf2S2G^=5r>4@<(SrMPEGs>YjsyNnFj~@dpL?(x zP+|;bG_bQc1zi*lCBBi@5N4@S`t;FKen!}SUuiNOoUkWW-hysFtIHAB9YfOj%^RZJo+C9F zf1~9Y@q+mt&FhXOe3moTMkNF-DQWw;xm0F{vA;qqG(jV9n(?1WQhc(}H7bgJjE?!s5C_?nG zTDscRqzlzZIUKn}^!77%sSLJ}tU*w&SK6fe_3C5oBMMX9u&<@^hv)w8sb? zZ{j$Nm5#G(oHDl2a>0_x@m63zsS8{y&f_r+6zc5;b$C=nd>#{X?nNE?c|o0pe)micNE|-m5NO~ ztUgfpZcnb=ftnKSd@^0wBBMwveTZ`xb;xeTlzl&-3FqAaTlUk+#$@Z-aRc$NHt!+ zGrViDOi@vy@uN#(v%}!Z?WGFgOJEic;}gs!N|xR%VjN@<&Y&KyaZsik91@{(V3(`3 znWG9$=LffjNiS?Ashc)8YHl)c>;3BkUKhY^E8$gsPs+^x!x6!-J5}yMyIg0&sC0qQ{v=d(x*cDame~g0tK!#{eGu)0Z(c*i zeGB<9DSF>QBCziHV#S@|m6^mD4`3RO_Nx4}o5XHMEO~Wt%SavyM`X_LANeWB#w}O| zS9SE!Q`P~q8T3wQcngBT3qs>`u|ft8=qWXhI&iWYlJiLGWkuEN5?Ly&VkM9ctXs1Wu>xQX1%deA>g=?HRY z*YYk0;JIl~o{n0d&-K=LJ#})=yVa2;ZEZCgan2fzu72jKs(BPH*Nzh*9%`J-SRU02 zvhR{^SltUhTXX%?QKyCb8AtpIS{S>416l$?n+)P0_w|RBX!uCbwhs(Zc4N-MEZ1g6 zS^m|<%-&S2>8*JsGjZjk7m{ndsWl6|qSrSQ+q(?_Ca!;TaI@@wF%{36gO^z8I{)Z5 z>~u95)+h$0GPmq1frVOt;npoluVd8YNZLl3ZlIDX(uMhPtiVEqOQ@N=gJa0AjWFExr<#q!qT2bbuuU-skhWp5xdIFSw05C9+ zDMppx9dA87lGe14Wtfhbjz2)S(NvvDkNy^%4%6?HN9LKNOli|9XFx49=k&@aDm11u z-^qV_uUE9plE#@E()<^S?UGHi^aHfKaY@d+d)NFfDC9qFH87L+?$a%bIxqv81f;dq z+X>zPdm-s)+r$WqyIC<>ZSUD>FL(!o zmaTby(T5nKa8qDx35OT^D)ASy!a-KgjGMH7kxU#1B&qo?B*R@pSMS5)BxFT=pQiq= zOMwm-s3T=;*K{L_1FPM<^NIC)w1t=RTjg!<@9rT1u}%6en89eq(|9L4GymSqHxgnc zBTm=cZ|l4Swf*R>iTg8b&Q>!M|1>k!sJzc$DuJcw*UI zzf)g9)buU9z&fM-g{>KBKP7Sfi=lVTTP}4S-M9{PM)P2YDGd!Y_eZ+&F@u!37|#oP z@gt_QPGtI&{oOZjeGiMAIvkimdE6)<^1Sj!=664-sVank_05O99iJUsJ&mgN41f9n z-vj4&;D%by+oR_EP6xtTH>ahY*L@v$u_k48#(dur=X13EfkC2!`J7GB3pFi7VKcX> z*8s7S7o_~bt{~$20=H&qNxrRKm!T`&kJ8)5HIl=c%*e$Oka1 zAi-|%m@{>;;s{U(Gx3ZnJkkSb(>dSKU#LbTr-o}IGI@j<-K6JGf#r+EToe0rhq&Mt zx~$0Qk?Kq2oF7!g^g7%*SR1wc_^!6cb&I)&{ljlh)+FBv;W=l&g%^Si=SVb6S$|3H z73r1>tLY(HX1|6@R}XXhfCW_0&7$x--?@Qv97)i;kUagDLTud_Pwf)AAhrWvz?#)@ z$IXYms;f0GLYm*XK?}D&*WFHVP1pXVk&{s}j^3;LLIECM^B;7xw{A)!pln6K#m`^FqpB*uzPYJ|d}(tmB;b766VtA{X}1@<5|e@1~r{^pPnX9wPS|zp~j}$(*h*g&g?rS z{&th1Sb<1lh0j1tg_9N$${*k=#zhaZ!=}B^xQbgA!PEZ(FybLeeRR-c88*2Ua!7SGtQ1Wqj(rMs_2Hs zD@MHdr4Q*_o$&h(T$u(bt^b=S)Ps@H++Z^Hzf9<|H3;HUa#10E31|o-$7+PyZQQF_jZnbOytU4=GHC& zz^#4S4KSpc=m#*ninyr7E&j5)*M0WJV|a~b^_M_t{L0$HRGY=_6?`o8aM1L`v>Z_! zzF@ZOco0pt{Th2prL#2k6KCR|PLM%1LDHC;Bv*Aux5wnv3A0EN`T+BmqGnq#){^9|~zdB|#R-$|&U>6@=sIglS~sF&Lr&X51d!lc1D~U>hVkxgg5dRgw@*?&TDMORJO zJZfUXCH)Y3y|AWcxSbhC?b~%E-&Kk%S7iEf?8?sb#&|nxq7E8Ha_Vwnrd;EixU-+( zxU2V*2%mrqX-4Wy0~MN|NgN!sb1A{KoltV42#-W}atHUMYX}r}*gr4FB%SA{F_H}K zBS^(uAqiD;_vFFCA~l_L-VZ$>-7#!|hv2z%X-WIw>c0TJqs!W6>R3xKTA@XQK`q#` zz1@n2*}c$e?8$aD)?c9?kbz0#dpikEEtdjgJ{uRMbR$>;%v7{U_1qlReZ6jd{Shk98cHX!wQ5KRRlX z8fEJPNZQw`Vv_^LFzzFzM0Q;7=64t73^_J9uP1Z_eb12Rx!&N?5q4j8gO|BI&7~WY z*K6muk3khG^uf(gX%^#mD$wY|cD4il6^V};K826(3lb{828wX@7?5Q1-OHy}NpFOo zGm=jCo0y*|+6*Zxtw+vJJt87;q;$WBj*k&e`CR~h_)_90Q5vKvfuHj5Q^+)w{aUSD z6wc9+xg#5PMwO9X8xmL|mw*a(aI?1I3Eps$YGy zHVY@r} zfVg(C zMp()8st>)007INgeQ(i?&~QYNE`JmsZ58mq_bZZ#T=+V7^$`?UWE>xR=YT2L*rC+b zSRP$zw|c)bh=Qhi(eNK8TAORL<4SDq&B;GS|Z zsgBwP3fYhoYszq>`J)dHSRrB57eJX8DUPka$~V`+jZ1HQzye1_(k3o{<7q^mOvI|j ze{+RMyb?t!^#2sRW%TAZF@$;~Mm4H4*?UZnIJ+It%6eoy^Q(<5Ca-P9zqox5Z&V9| z(tq6@thGCEw*2JJ=jbj$MvXR!@Nyzx5@okYSegr+hlt!oqXVJ)9MKZ2!vroAu@+io zANttji7Z)f#Az#%^v(%SY)7YzwJ2h(Ik5Qi`@^oi{8s1H%Q@We2f+cUvE;%K7nI!L zkJzNf^K`luibRwI86x+PSwOI~k?}eO=)UX@BDh3b-6fGh(R`3?&<# zbKSZpH?!sA{loO98*exXdrG$I>~8s(_J@#k*xVkj^XtIKk5dWP| zlIZC(Ku?d^^NOkB#jICyBcj=6mWqd^_Z+DKs-9jqvnlOz)X)CS7GFTlA~OlXs_Dt9 z#ydkuAYy8p?`WnR45=Rzq=PP(pgym52|2lT1J7~44yv#+w-+94ZJ(TcwV%Qk=S8Ln zP)BeNlNU(FvG1BMgj2|htUptH+Z=5tFy29M)))z4n#(S3F}8ok^uIjtCsozASS9^E zfNTDe`t~YBYGg3s!F?_oj!w=a)Y=fYDnRWS)xGV~vJK@1ldzDzHiu)jFRp)*e5>LaC#^o+8MXp`vY?_>##J|W_&y*;qUws; z#28y+DbF%LA&26lg?vkb5MOn=xmDiHn7!1I(+%`BO9*nG0H-DJbg9TfmRu!oa##G! zeX`xAg6cds)#yZ0Yjw51dJj?XOQoh{D_V`H6;ZIuoy04U)Zdme#o}1Q?a;~OgoE?L zw8v$~Rxrhfs#%%k3pa|o_9C=2R4}N~CjPvN@TN%CBMqT^xV}N5GhRsVqvP+xX+asb z-=LG<7~)}PoyfadEc<8uG?Vx}9esK~?_Tq|D{*@MV%BJN*xbPZQcc@Y?{{-*GkHahcD1hRVeQ454et43P`&-Ex0Ez7|QZsyXQWWy!999S#;*rRJ-__yi4HlZkp&JkosU0~qbJpjE&0 zO&SB9k(Tzzf~De71~_BgpQ#^tCZ^aLWIr5Xi0Zxq_Sk3A#O_}0zTsNAoWNyYV4$h} zmYL02@sQ^L3W>`x6}hW99)EnxM_-Y35{z<%Cm#pR&dm*TQIDt^e(1QndAc`!VD~v- z7czaLts4GM;B1VS#5E#kE-@=@w_F#3I)QlA-9)<4NeiR?=gI%*=n-Q7-HeSzGD{U+ zrZD)xba2=wLJxN#5>%;eJbStV86^iLDRqr@UIXlaN1# zXz&#jPt5I<$JEVV>$A#}_Oue7h|GLy(bo7q!mC}WCnqkzsQE`&i2e%H;_SAjqN|_L z9O8h73 zV=c*WzD(T~bNLNUElq_#4N2pH6fW-hJ%eZLNiWx_cAeqphLv_991zzVktm<2c4GT` ziLh{VYX97u_49a_4&m2m>AUlt9wuEN>u<{)wX1epJH@j0vd`yX$=WcTQZB_HziNZN&dW;Su89iFNSV%(e? zyJADzjxiGMxdw#om5gtGEQXOt3_6}s(OpC{m!IC@6f<{41PcDZ>V49*^7H^sgtZEZ zz>zcOLaZMOHezd>w^+sZnrqvK3t8GyY~&0<`av%+TG?*LQSer$s$cobrA1nzHub{T zf4XO@N*IVUhdKXYRb^$h-lMfL8-d2_z@6sdWY;nprT6*N<_k`*ML;yNS57W zk?C~B^x39(ieIci>=ampKi!ilx!CwJOPOOx-cPurIf{5!)BZEe43xI}jNrEYAWs?% zUN4R!kM^G#P`$7CX@vL>nZA7Z-TS*a#)f2T3_QYe)~c;dKQ}?bk0W~ z#Fjs`bMcN~1HR57s~mQbTX3z?YpdJVrpK0X0-ST4>XT|FZO#U? zLa$x{wi{0j%S=1aa=|H#eR({KHK+(t(O$=FYt9jyxG#A|KI6s5iP$_sldwE>Sb~#K zzxO>Abp61a>Dmc8kh+L`-JmT!(3mZ_epkViXaBvq-&JMt!9@Nb0N|8J@XcjV#)S)W z?2wMMf5aBNH?p>XU6_&~>ot$@b391;eYuu?%jFYNlg4;&WsY{nF+|m0vyJ>*iT?4p zj?~iHVt-O4M0iw7p-09`4?%wz)pCxVmaOG=A6!8Q6x&8oXBs zI)AvFq{Kw;JhGME`cG-e#k(AA!peT7y>c&a0@{e!O{u~3tg$_5`^`*`u#Tes3XBVc*4MN<<4*Qlx5od-<`{1S5e6?LBlfb zIjy}#^PrCXdfM=3@Tf!R3Y#0m`+iGUQH7SVf@VN+SAz6VR&PU(Km(K{cqre(pku@S zvmtdnvVbPIl7_}9u5y5eAVFE!7pfX9ODPKd~9C=Dr##s-)_OqtKz_XPuD#!kD zJH6PFH_s+3vW3>VgujQVZ-OWFIQ_>(hWfWnMo}RcCTDcg_o*^a`;hIgUTh8PXhQtf z!7hSlAN7{cIPLBg<-{nc6ZI!2qlLEd(~d?c1|T^*RTEd#)|Z5k<}ES($L>fF5RN&? zE4F%jbC#zrdgXDClcs^n5-6KXvi_@BpzXa{|DEG$k73~c+8rV;&ecQWFk*ZkhHN1r zYQYv6&6&pEYgONj)0ra+0z()OFvUkW1_%eu(5}c%VpW98D(FT_|1=AnqtzZP>~tc^ zQj23CrV2n-15LrmiQ4PxZt)D(VVLg6W~|@NG7UVhFvYMfcRC zi36}dYCDzH_5*;gUzu7O zRIa;X!#{}%yLXGIFG>rpzN2DvdWWtVB4nho&m?gh@_b(JgL}WS@A&HMs3R%0u@HG8iOTNEB_U)*-R1(|&!QsKa*T7j!w%k5<+$KIJj z3v=BwgnyiFw~`R|W<`r-EbGM$Z+g%eFq@Zj+G&`S`c)KoQnGiff>1GWNtn_ra{_g* zu{_qo)pyV@SAdmoN6P1XPJ5U`@D{JKK7wU(UP7O^Kvpy)5r67rN9XUaVez;jjfD8q zxZ2hzM58i{@k=&LuwPqCeS}F=N;-uyed^6X(`$n>xQ)#AVYAeW_|)c~sK%1)9d$Hohtl!Hw0H@!b0Ov@l&4!H1TFvOcz;AgPCVZoPkv2Za|&5y>P8|R2!xdWLSfuPtHcj2O~3kjN4EIi={5hFOrwD4)*Cp1>Ts>!saK(Kt&t+FEJ)+#n;I&VXL&5tBB#&xV^U@a z-MXOO%_%2zkB99|3%WqtK4i4fBK2v&-dIuZ-zA`*59eySixROXN}cdWAm#a4IYaxZ z%Ob%&0=!S-8(~i`mkblGjq8g_sG)E>h^`+XeJtnfD@c-`TU*%7Q3~uc#>~TY-jeyy zO^pgk-;oi?5pKQzSWgZa%_p2a|3zKRSCIS3OvCAujYn+bj!*9hM*ho9HJckwNOE1! zWTYer1he*Dfc6{z&a^zrnB8hrYeAsf(%%e{*ObSrt$rr{b{)4x0O>rt{vTrS>WDLY zbDqFN4Z!%K=CUYrk1m*qDWp1}w%zDFcgfR4-@R}v_f+><$fu+&d zoahEp4EZA^Rr623J7B;Awv8}7GBf%iJfJtW7Ip+HIr<8*L?}HwV=Ce|ZG~sI!JX$a z+~nmr^{hLn`>z*GuHp%_@pWzdv1*Sf)0BhU3UYpKz?n80bGSOW=+-Ie<+GN37gvy7 z9TBDRf7vT^yKSQ1H(3{pEAF3aTNQn>O?pEX5EqG`# zN?bZWKZs-Z+p@&`b3K*OC-Jo75M|Mc8-+-pjJI|~tG^ufbLjrmf3VoM+?*M~;#(cu z7Id@R)abg;Biw<|DR7Mr=F4~ zt#tikvH0h##I7WZl|4Jb&T|^ntKUKHo$4M96y#p>alT!_3Aqg`e9QW=>(Zufjhmhy zHG%uNq{b=p;S<-Je(1bU>Eu_pYphz=Y`f3&uU?kGZ|M*WgR@{-B6U8`qi0@XGlT0| zIBIuY<20v5hYb+SdmeUmiTRcuA!e+!*oZKU-prUb;_DyNK@geKa}m6pW9_^wcFGu8 zd=#_jv!c9Rrko&*Q-3A&n_dkWqbaOX$g}Y#KJY!fLeShORYfO2a-RCg?mG>=b^7i= z>wI9!`11zsmIjr#S}ui!qR)_&lb>f8=y2HPKpQPq7q|u#Z)wrIInYpj9u3g}EpHFX z+|bhH7CT3*g!I3!ZHZ+to$M~xi5oOyCmS0*Yz?$icQI=dALOUwCVkp>na?w|+1zPc z(%Sk4Y@ureBx>FYkXc$Knl8Wc3o?|QJM$wrGq8+maUr=*x=wvoplD!eBoACTBXBJu z>3_iucN`LR7|8qhs=w}y=$!Rx5=y%N@<8+p0oE`Hrxf?GcZR3qT?j=q7lMdeJIbY} zXM~JUa)Q|ler9lZbyaFJmnOs!R|vcu{Q?v`=Qc>4Xc8VP>WL1)SCzMaDi6z|;5;lT zyyN-X^&<4Pje*FAB%D6hae&^vw*M+LwAZ<{g9;=*W0%oI7#pVghx=e2A*)%T{K{w&Nq@~@%V<>nTap_Ib93KN6euaONpm? z4=4 z=G(C!_GRbxS_c$24=Nrs7TCnYV{7j$`j!eeB_|8iS%&b4+z!{@M8?Y|R z265?&>4s8Y=04q;y;a0I1LOoY)rV{wB1q`O*)~Su?Fmvr5jU*}-br~q&z}?dc&1v2 z?$3H*S)PGw9kqQ~E{Ga@)tKkwmsc;>qp!C#LTrC(j;Aqa*1stoOkt&A9>p!Sc$1;) zLr8K8aK~Co_#V?f?j|arqK)>)G}uQ@xi59qy^7n!+xhd^TTx`a$S70-7$?$E!`Gj= zDY?skA2p$#1Xe_%y!K4+W-LYX?tH1@R-y(j4|3Ij>~Ksi^beyiqlt3Z+x`}UWuKnuza$)2dHRykk=?ibIz?e%5AWV*>jUt9m7pv-jnD|aBu7|p9T3WFxWMKS3hpI+^BSFpA-g0c78 zKi+Lluq19WhZ_)HAB;~YVgeZ*rY%_=K)3aS=BWc$!a~NLrb_HP<*DHZa!u0^s!j~L zcNupWyrjLI$d`UdIWF&hs#?28mZHD(>EGZp)4NSJS?_%><4k3=G(Es2uIW}nGTj3o zJu0_{c2fmHLP@I7FJIi2E*E2K=18ISj19&Z2T-*dVioiwZ?KpL^N#r64K^V%1FthJWlWOsh86KJ!I%gXLmU5*Jl zj1_H-3XGj{Ay*G4%{AR}$i>4BIj%T6pA7XJ-<2T$Q=P^YdQx87hlGPI&@(10e4-Q= zw~`{Er>GaVi<1KnkiL~*v;u|Y#|J6yvF6J9TTLNa_@VPH*M!iHcNui+>+dd;sC&yY zKV`_olx>29a4$sjW#h3M<{1PlQWY#*^?fN*V-22mEb=z)?;(Kbx0>9KBN13C+|9n9*8OM3SCGBTHr2S|jT- zI+=SAskKeb><{BM{j#Vx(Tw)%4Q053LpOJST&UJ1Zp{ZK3BPXS^m=SH;(6k||3}lS zQGpk{PM}oFo0)tpS&1%Z<}p#X#e0a5hU=K~O;C74-P&K}2X4V2U6!9agIXjZmD7> z2NnrkE_;6u5kFKE<#=H$PV@p>;w6Uh#FUu_m)GK!aoUL+6n7yyWk3VuGPI+#fBrd- z3+eD7v&8eP_g$ofcR_~_wPytA&}QTyXT?o^QB<`WxjyZ66D>U_#L!GY$AI+I(jD(F zFP@$N352Aa6~e)kriSF?q=d9C;O5uVS~gCOk<6QXA_WiXi6r>4e#KK9RROmYBJj() zFJ43RX5Fi)#yktzfK-Fi=59+B?8EJ{1>Ox1cJsnjO+cu^`eQ6DUIoTj)bXBS;jEVvzs-u5%E{fJx(2BQ7 z=^w8C1h>H$NQiVjSpRfP6ni3$@O%n}?QkVoIl%XnckM-jt}l;so~}|K%gPoam(QpEWfXuyi^HAtsJCM6LxPJ zgu`Wjvd4}f!JPWbqU4umz=s)}gBE0xG4Tc-OkzaQ?RB|B^(xVb%Ew)u>N2cs0(+gCpxS$BBy8jiukwJVuZ(!x!9tt7>=K=NelF&0B5pgSnE<-ub>`cmO8z*FiVh0N<~->l|CJu zIUR*AhOkTi49;gD^B{mrfX+8*4);V^mZ*WGC}s>h%oVsCE~-K{u`3}xD<7Z?*)fMa z+An2-0J(YZjR|#&+g0gCucUx5d$+z(*V)PnxFVMEt_CoaiA#FN_@`><|5 z;j7}x2Z|^>Bnb|%8;j8PAb|XsZHu6Av+7$z3KP*9SwdIts_&!0&%oKm zp9=1f8-X-WHa5bDu`AEa%9nv_mC|}d&kQ@m^{&*)J5)%yVmkx4RXsK`TT!-H3*CdV zvnmFloEM$rpV1{%uS+3~PlVNYu0nQ;;9APQod#}w`?4WdSE0#{Mv;NPnMjW3MP`ZWF4~{lgRuNgf&gIF+&R5Cv^iDt<3?(G3zceb# z3^$8L*0(fdIfOX+GAsLIFxg=;dm0n(`HDZ@G&16GL#sxz7ORU-=;X?|ZMJftbxXa< zqgo@2cpU|IZkd2#`E)0Ukkq=oOTi2B;LL~;L9-N(;uidPBI=r7ouQ9Xp}xNUhIfdq z9lZ9Qo@e7Br1N#>Xd>&6xprEiCU8qr%-SR2nxlw1DCP`9Um%8k)F{n{&G-&>ES=-n z-zLhM!?>0nB$d6Ac?2di{w7Vg5+P1q5Hf!-_)U7fJt~v%59bBwfB(&S5vSt$M}7Mv zul_S=-`vxb&354#c(w1oi%IFBnms$A^EZ^W8=MSR%8#zBQEsIUS zksbE`j#m>^T^`e1>OaAi2#TR7XANhY0|cIu%%Lykq3Nm?_mj(&5(K|@!udP;ba)VX}-!TQw+0M zaAIUg8LjkY`Q`KGMCDNr)40AcGO`tu%cBNb6`uEb-zx5L%_gCV=b@JneSol%%u zCSK(+x%OkQ4Iu4Go)36jk3MAU3le61AKyCfUxx>eEN9>=Hj(!&@-GThBavu**Y?Oyr-)8Y0uK2~ zjHl*#Iw{@UmO`lKd_}o*<`TiKb{L3`9OI)z_v6J3n$>3&`z<*E%a< zNm!sN?&S~PwKOGmcb9+lv?(ln=P<{FQweIiD}hk}53{8lD%BWuJy?PvGFj}kAxiBZ zjlx~Bj)AB~59h-)Rt_C)I4U+QX{iYhbSC6qx){W*!69{lJf(p%v-Hetzeb}|+{4tF zOznZWoURvV#zF`gEkb99ZVQ6~Nwj0}aB`WcEG(kK&&hGANd9Aa*=BA?M>;+$+W^yzy=f zI;f|lk*z~;>u;IG+DqBopN?%vsVD2BOVc=N?hH!u7%aG6&N1C!`c-*J(8R~KyXI*D zH(KtwVaa!0@0?uS;>)02?s`5Cn`t>aRC;_6g~WPOJO}?~mJ|wCq#H3WLDJMZ;%ZtN zyz#r|svzfC=#g_wV{s^pE~v3V`PwyD;w~4_x;-y;Ew})n+dG7~)f&ZhUb2~#HgM=_ zdP;zzD30PVK{onwnq0u(^@I@9-jUp6y7n1EX>7-9?c!ZRg_>x0tJ z%Z#3ei0f*28^$}Has}mnQgpr6ts*=zdCRr~DH%1@K4ocD(ZW+9{E!l1Jitcm*8RaGsw1%{90saVKMl9x2X51d@0+P6g6On%jPap}? z&7MzAMmkT}^ng}=b>)id+cZAO7b-s+0%3@?Z^&ur#;lPF8PVq~Vjgraa&RO5OSngZ zm2046_dhFhhvK~K<}n5ZMgNxc5$y? zXKcrJfW?1VV`bdr0!D6Gb@<)w>o@%?Bn0Rz#OvSAQJuT!R@=&DCDhby&qq?#jcz%J{o`=6gLprpoF@NXBKW`CaMCM*peySP2kf}Q z^P-bK_|POsO>T9xV6|mwO??cBkY<02QzyQ{7Jq8aFhpc3tfkq*E8t3F*7ay82F(wA z-~DmDvf#>dFb1sVD;IOSb8n+_(!Od<1$^*u_+e#)FFUO`(fpj7N6ZWm}BI z!&F1T*Bv(WCrfk_q+!RhOiT|9rRG5VALf$dqr{jS;)nS!v$EcP-om*Q&MA{p=p@`^ zhrytsf04d;@_-z%A!4Y_8JL0V1%P8h;t96VZ4>OzJ#ww`CvW-`!3U=Eo@G$YBI*xv z2S&gR48I@#_i7t?KV_GjnfSscy1uc?r-$|f{?{M4RjaS8OYLK{F2_7u?MKI8e@kjS zRi3el`reVkTzh$(yK;Ws)z@$9mjGJIx6k5Kv5-nW%5fCCGp7{;gb`%Dx@$2T^nbqK2RvZrnPRd*Q#I;GrKef8V_{F~&~T6a9c9EF?IM4rv!|yUEBpHip^AygEN} zX6|;nVPBc1wrI&znbh~W(m!NDiLp?i)rOC)rS~_VNJ@TZ!&|hI}WoI$8@n}tFyhO5J?U3btm6L z{3X>kycaZ<+)Lj~_F=Of9_tSy(2&K8!+nKp>aJ+?`lW7lywf0YVmf{%TRLC%?^q^1i(+I~# zYZa8^#A0G4i+>~hfp}(1{FZnUA~aB)=xHb5`yN78@Tjzw3fTUl@-(5^lirPCsnr%x zE`SdS{vA!^sO_8Wsa#7-ONUo$2J#wMl<-Fn>gD%BXpJ~reN%<+#%&OXks|QL_hjgs zu%WE_vpv7YF8Q}g(b$`pq(39WTsQ_yz6+H(Ki4wSnKJr56Q*$0YGFjmE+LS_)d(0Mz!Ee3w zdi}sU9pPPCowpkkjD2vaY(X=yqbfG>vArq*i1s`+(VHU;S@(7mP2k--h_W*NTA*%n zmyv}BAIPHp_UdLnjamT30UL`xM=h9>I?gF$EL}{XxGp8ayeSqr@?L`74me zQDInosmX0y?@-L`)VL^Q|AeLYLY>TmE6O|3WMD&-+vJnhnzy&+^{2F4I67^NQcKQ& zB$MFnFrn?J_bOKf9t<&#;IZxAPtCL$)o}{OGA~T0F@e>xJ2-5?%cNyX*Au0lo{Wvs zrLL#!?`u4f<5{C(raT_Zp0~Ds5U-}Yaa||H4v$|8Pt4M0mO8GS&~4fVBbsZ)z0C+$ zXZjih{GPradGZoFcE}x>8vqWOL1!bRm<&5CN&B%l#N*UnKT^%m(T8>m=!yg{Zg(J4 z&`c9+i^rAb!@nzW)<|@HSvy7Li7V5|jrZ#CzK0Y((Wkimu=TMmx}$%LA6&J`6W_O! z?jYuXFEeu?>Qy4i?gvsLJ272&;G8A0jLe1P!9!(LP1FvIY$vdg(A4q`EPKFWiS`Xx z$6(glP-8VuPH$`!t86G*`zLW&mIn&nE7n+8fVVw*wz1f zpY^Qq#xH`e3Hc!p$?9u4G;8V>+Stiw$$r~z;s}cZzvbpyEbh0EQX zQi^-L!YS)&|Ni4hNk>w;Kyt?;ryp-dwG!@-4EN~UrE<;51IG_q$AR4B6S~?j%Rr`` zP}_^k72gHx367QGSP7?eb(S`TnvsKpX6<3Tlf6dJsHo@aRXUiP3X3_pME^DDZBcuLS*wJ+G{ivHAa(bdU#f7?g?!vuw#@0tsMAv2ZQ85pn3mfOG zaV@7{0}m!FlKbs_!O@gCN!S=?QU2(ha2o-+o$fhv2Uilbv{%Yw^z@^L8IUWtYs_XW zC;xOd+?8f-=we<%E&g|x$B+G3=GB;ymZVBWv`{9(MP^NUDRG2Gq>906^EetmczdWBh z(H-4MsWnV~&)tsfihuEViXad#4PzD9(CjrBFE1Vn+pZ+yKOD6>tPG_YLdRd5h>c@E zo}c|i#AY0V04fAfIRJsf%IGP#9Dob?UE>7EP^h7ijPG#Ar)1 z+61esYrnqD-6BRn{T^yhQ*61|+r8U{hEGmO@KEe_!8C`)6WE7hH6KEpWAT?3GuqYU z02B^q5|mQqteYd!SK!+orv1A&tX!H}ZE=YT!%&O$KfM6EsyroZI*gocL>7o|0uAuk z@*LF^`|>N*b_D6FX-HM49?6a%#n4*$#LeUM{l*6W600ziNqCZjf{}WYe{canpAxQjX z?~omK|4;Y8uqIX0)k^-27Tz+hlFk!-h-wRCMz>nOwldfjgwnj?GOx1a=4gyK-tR2@ z2g~tzK%*^v<;faSlS>oY7eoH#1Mfl!Qmx9a)#VSNo}(Ix=5*@6B08G-&TJB_UQKO& z^-t`Ivnz@H+90@=GpgcczPq_fW(ph{f+vT+&yTc+Yd+BaY|kZE$RZ1G7Mz4ygOF`| z(o;kw!^qkN&CU(^)P`zl`B}j#@MGe7YamlOl15ws=lHpyJffA}T7Eg7Zx#=f+B5R^?cYX0;FHix1+Yd&RYr-E~_(Q_~VcKnOuuYwy@~80X{7n z0Vv0B`4?j`-R0uz9gzx3<+kzk!92^DSXc;g#mQ%70`PH|NyfN_Am6@I>KmyZ))m>L zbGYF=6A|GB@^Bm$A9?j8YY(?p<+^^K>Bm4vCk~5H;buawz<`YQa$r3U?3W+#*b(eR zv!|7>MO0Rwe*d6~`|ukxU?O93?x*~{=Nsa?>;Bh`S6j40T5UbAlofq-od!b#NGP?Q zjhvN^*aA!DS}9ddPCbi@ZZTZTuc-O%y!r>p+1tpVbo6%c=dbi{_0W>+Tmg|8(`iit zR!$_Xj!zmgGU*mt}k&PKl*hkqZHas~O@9A8p-Qj) zGSv0;B=-bR>#nV$>zgYTP@6-zy$Q7FRQo(%UkoHe$S1a>vECCkTOjf&-rg>-cl5>E zHf(cCFxFd}4LFDd{ZM3qQ^P7!Jq`0^n+|b+Ypf7X0tu1kW@t@8?;v#pb#{%fvWb%V z)O8m#ID?(9vh@X0G?4vfW_xK8(&vKDY|P?VadfJlq{wrSIeHfc`UwEew?#AROrL;b8T2a!LpJi~C{`6)D=;J8Ydb12B;GEx zUWZ4T%_z(;)rK>PCVzjcy`Ds=bbJ60Lp!CMv+jr0+0uPW1N>EQju8l!OatedxU303 zok!M-U6|2Et)_#Ha&9Tz?o|UbXLCr0jn~M!&iJ^xM;Y%>LOGfgJC3ZGY|9@Rnzr&S zQ)^UNXX#vg-7ETfKX#9DK9UC_kqm8r_NbJ%=d^-OL*nZx69>3X*F`}bQ?FeTe22y` z)HWy$dWo+Mzw{;;ZH*q^-4Cl=`bDDC~M0@dAQ&P)jfesK0YO%j~|CH_ft^RcDGy({Lwrv=aN_r}FzaIo4%rM(i$_q;-2}`L9GB@6=?z?q|U=M-hD%u|s+l<(GU6 z>8N88z6L;tKc2Y$>WoA6kkRqvf@I>MjUaw->90THz~fWxVUJaUL)a70m+8y1#2O2^ zVhZ5L9@x^=scXJY=c>vO{FL})@LiIw?hEI}=vCFk-uYTrx1s}tCUf9nlx;&#IG2%5dv*o%34d*?FE zbcwCp^V}|=lDFCzEX+-Ybd-+@|8k8|@2WXeN2%UZO1zM)tQHluM=}{cq1vOvoqXY4 zlzhipga6Oe#YEEt?z<%o%B@|r0*816^X>tV2)-po3jtESH4#A7J;{G%h5pDjRh;cETWw zq^|slZFzBr`&K*?;dBYqyj``OEGp(XGO_6{xM%&=c5)Q`?X&3%Oh)H8=-Wq7k`>Ax z*Z||Ld=$Oe#!m;Ws7b)#n!a4erGIkQqXJOe=VSy6X21%)0u6#Uuj~Sx9Xg8_mI>0b zFeY4q4~NCV63Mt*st)khB-)LAMBnocqv)`tKs1Q3F9QL>Z#fVl-`{LI$eChH+Pr563~+Vx9B-5K9@ z>9Aa|L1Rbw&CK#mQv?JyA>azcDWRXp$jIVJqu86jldT0!9BfYBetRDjeRX-iaaFS>LUABVcW-J3A< z?hN_GF297U@z;U=&=)@6(-kK;`n07+eX?6Ezwx`0XokMn_T&R5z~cMo_$=7&VabK& zW7qjFmFip3TZlPwiQ(J`ppilT&dTNk2AZ1EV@609!MNW+v(dq~-+x%XJsj6zV(vwjzX?>ZcM74@Hk^ z9n_li5p-;p`WRLls*ek8l8H8+npF6PZ4^yqov^{F;J$@jO&^n$jRX>n(ohglG&mX*Z$o)uGDD8N|isJP6(K+X`=Atz} z!Z~!$v@TRRgG-sOQJpF7S598OjR*cJa7QUoe7Rl0t!0;(PYo@!Lt09JPAf~hPb0d4 z_&7Mf?RE0~%Ao{l7nCs+ea`ns(Ft#l&ZVYvGKA1mShD}n)T=A^jE~wt=i4>cyLv+! z>gMN{_B!K=PH`VFakUYk6w&oX8l0NTNM?5>-CjRmQokyFn88Df(aUrF@tEtK)q>Cp zv*`g3;Y01q7b|f|y817i&n%vfI(5^VfaAEvWEKmycg?0a1kqWV$)u#SV-{z!a%y}S zHwT1MpOvAONi4ul$i3&^7NY{(Gty9&N$F2Cu4qbT78`BzfRv}_8rd+&A3K@Fy8O!2hx2_K zqv!QJjPqq&FU(Mui@jHznw+{3ezk(m*0FA2ydlAW^NSck>h^CiEh*?$2;I+5gA5!u z1E9rX8)FPM&$Cw(7)cU4heCH+5F*6Jp8!K*GCPY>2Fn9v5na`2p{n&y38F;DlMfV) zF*Fb(Y>%$rcP{Oc&FV;+0~oChY2rG)G9MOny1;bzhiqp}(@$5kIUSn3=fK-4K>SWNnD{O-LAAc z>_nVWtdR?*`P};@sP&pR6-k-OcqLL)h-*?ov&yS zx=1%#x(N7>XaZBMK4o-jRA-aW=E=yAPEJ6uYjEQ~J9hub$u-Gl@ky-fQgex|YX#EU zH~%DEMUs0lX6)wELBWEvdcGUfXbe^ott1>^Ms78Rif5-88ZF6Qt>Q`@Ss4sVc~&J} zMq>pSWb!u0>Oj#O-_dg0-+K@A$lR-h3DHrubMNGh=z17ZC*8FCCqjbyRxVr`H;t8) zjDcyS!M~zmR1I*Qk-~rte_?t6kDU$W%J9_}<8Dicu>uQJz!H&i^UL|b*R)JEb@)ry z03yYlhOEkx{@4FQPdtWO0zHFuCqT|!XsGPv&TtDT!F~O(FeBE66`Ej6^Zy!}zc>ms z&{}j1evlM5?QYM8Z%rJkzH6~|bhlqy&4bgE$I4sKJWdom9@(7D){_{1gH}A5fX7)& zojYIUP`cA+JhmT-x3aUF{+tpvJVD*}+04$Y;QPXUoJwI7`lr^P6mAU~WTjESE8AeP z#I`G@P_F`%jo}-;oKqw#MH=;e_gf59=lh0(N)<{u{Vv@$ph6Sf&EQ()|H4&2PA+7u zn93P6YO2BgY4t8;t+GTE*ix3s%hJ=UzCP%ZAvEBI%C~3I)JR)h*KBoIQ&wtE5_M|@ z{zZ-K;eVK!WS+VQLs4-^4EOvi+RXlo5Gp|PnAI3NBexMS%D~OBs0U-v>JfvM^^HK( z@q%ZBWcfw|)fz(Z6AtnK*db+uWoYx9dm>;0^;d2eLlL*L(xi0FskAOMBv#&p{ax;K zYK&zo%W)u>bfs#)8Xmi4oPNk`zb*Z8C}zccVUrHO_CSE?4*Z4VwT+t_V(bi>kBx z=;q|^1!c__G@k*B*6~05L*>wQxc&Xu!8kwqH0a$z4EK3@r~J zGH~#NWApQ}_e@$BJ?xG_d}@~*1aApB;oul|wzIk4f&JLyf2f*sup->Es z^MlhH{Kuj195Y#?u|)5_RGGl!povU%YX%E@Jwp6pt9F`ocO-+IHlORF2Vw75Yj!Ri zMsj<;jgZBz>wvqCB#)KG!A1uU&GIoyT`A!5zta^=`q-GL`|fH5%WI~4C^Qd!&H3WL z!_{_PipqT+D8!=|rPglsBA|jv2vkM|1Z@BX|K z;}YgIs6drO!{`>oxc2%vt71*2pgQ7KUnVG^>+StjKylluJviulmSpC$Y|xW&?RC2h zr~N$a$^XPuyw%eGm8pKA{_jlH$^uK1#e+e=Y#YlK7WKuBs+C&emJHCCwkcr1Ut5a%gM|XH|(}^E-3AIfq(mX}eu6 zzVxglU03o81|Kn~bpXNhRq~Y_+Gv{?PU2K7Kvm%o)H)FPn|1dbwQm3b3-e#^t5LtL zCIxNi^lF~jjHO;9j#gWE6haS+AZa}+Fi<44jgBX7aXq}r9&8gqs|0W4UXN`m4DvES zekE<5a(H@fbMxV%DeUaVto74qbX|A%0U-t_=pbVtu%4DH7ge1?E>geOA2X1-`X)nz z==u1elYG4k_NTjW0VBEJ$HF%rHnWfA3J(sSbDB=LzdpdjQw%ADFKnb>o`d56_dW`M z9;W-OPa*CCpuPzbvCqGZ=x^g&8nS_V>u+4tDwk7evkvA55uEwo$CM+X?m>u{BMH&V z2zxx=1>IX{s7;y@uJGd|mN%V=Ll(s=B)O9vJ3;e^*&S6T0?uTvNEYh$x{^y2a`Z=A+*UOa@l6=#i&^kYwcF_ylTzrLJ@g z|J@k_|L~c*=kIMl@~|S9O*1idt_asuED@lZJ)bkdjMHzxQX!00X~m@Csjg1=rlGbUIWq>&_}i*B>}@>x&K&BQcF_mM~}9LT-SbH7!3Mm*gm+}e^KTyP5nug_gP(192nfpv8go4sg-JY6+8%xarmOG3eHF| z^TcQmRi^$^4FzR;t9zv;teG+wK4-d7xe|2j8rHo_n<)C_o5g^dePr!Sf*=~yK<>BR z9jR-@&)fLS!AKN)HI4&6q}AG5Ljxkyun3)_5*n?8!c+li_IZ6QmWZ{Ys@1z0D!@lw z>kQ?n5l9cZF{l1u~Zn&ImKXq$Vv{ZY15c@#I;1M2qBLxcIT7QZsX- zm4iH8J+b3k0f@suwWC&!m}Yob+3)vWCdl5!_?|%VK0!U0Y#@8c;Ma+RERSm(} z*{T~uU3How0f4SyEHnF_Bm5j&dgWmYz9G7)e?atsR+BHPYZKE!%z;Qo#$xa-f0QF1 zdNeJDxrHC|%CikR=y%g;COYX?yuR6Z?s(uUO<7qU2e>XZ8A~ zW6YB!a#(^m73PpR_Bib8KDD+Qp8Ii;Lcy-4@}=@=8jeH7%ZTUv`wNk};c{48r1oCg zZwYV(d3j%FOQuv~=0JA|MX68;2hp2dCu~5?aP5T@i-*Y*50Lg8scmp9US*-6{3Yuk z{;t-xHl9k1ulrKRyKgbLhEbl=^7i7rT|ieUhjtTyXt23#blu`8IJm?(7BoT8QJp<@}oky9xgJ z+)zH3ba~2nTjX%EAurD1WZZS5-EeG2t>AIlb0}vPLGBiH*8YyzLizgS&&2`8rhP)T z-ys`*1yP0y0%Dt*;O#%mwspHU)I};}QEC^%_fbwPqHWss1f^6Djd;;^P;u0qNJ-Ih zan$aVa^kUOc`X_4>VA;(A{83yovwv_#h{X`oW=PlaQGdBT)+Y0_HmUY34B#RKBmR+ zx5+88phcCQM!a~0X1ul`m(jd^Tiz!t!aXAuWy^i$eIwr@i1DIsw*({X*E>D}a2 zRwwm8srGDWMvj_u%6cfChsA*~e^>!XGE&1Lc{hlJtcEKYt<6`)g{D9?1uNrzcU^G1 z)RlKlELvV*+(>dp!JLi5gb3g09 zyPv&!J!|jUZ~fk?;$L*j0&MX(J;BPOgvMVekhIySCeAJQ@fc*Pf3#LL7RsKNkuUzW zXmRu{V-!2D-ecp)=XElkb3Nr3vVc(yYML+YRpal!zH17kP1&2;?9!da*OOkb`&d9m z%xy)^V18^8XsS3q3<6$nOL!4BCe73weqXpJL=jw2tZ!P|pOl(oICXImdb@TG&eIN8 zr^;brX1yo=yY5xV`zq<7N@xv|Yg?`{mZM-}^GEXc{FJjSQrvbvt8@f@!_j zlMFK|Lc#;mv%gGMvt--y+Y9Z%3Z4iz~MPAePf=7d3ed&1G0xfsP(_hBU}jF_k)z7oYX)}lqc z`ezb4`qvOs2nu=JyamxlTKSwUtMPk3k!GH6b+9?Qpmve&(bd_9MbtnnDtOfXRP>)$ z_f?-|bYO~9!c1Qj!DTt^v46;n=g;Wq5!44peidBqZQ>c3D9QbAmACUJXr=N(v6%Gz z%U7rL^J5voC{|Gsxn1g%%Efb>x8n_&pD4dP{c6ak8!WI0^wlq_@*pG~`T4_XcvdZw zam4(o$rOIN2#@Z|Q9GZuvoUG@uts;?dm8-N-{hff>6=6rx z^l~XG->8ObO>uOGRVlB&Qx30%VY^Y5$D*~El%D)h2kBmDN%U zsz*jd$Qv@dSVH*9L^f2sfKDgKxI1tf_3QtIP@+`i_IaPnUWRBxB6{xP;mtn&-1du` z4BSRWSWS$LIU=gumfTrvWI=eDkH9IzoL`SCs&95VHHNl#2XSIy8yTt7)RD z8ROMggvJ(yj0B+4npM|rqn7y{{)x(Qc-BIcewziM=rgpH@q;>9;4*%+9$Z7TQAAUyyp1OokT9>_zoi zifg4u$lr^6dqAnO(}~KS56T8Gm_kDISV-$fhN=!!eBz#WKsn(l50(d?3HAPqMF0Q7 zo^ca;QbN~FAfudag5@Rr;MTZ9-gIp>ssBZ_cYFOi)jnzWmRA7IIoL2L(#uD<&C}sK zubNqjU&Izdi1M=N^8`A!KsJkwobtm%*V}d;{hf;3-qk7m38^*!gt}H!0JaG+9uMn(~F!Iky`*XXPzb&8gV`_a+#Vz$DeE!A$CFCh~CzVC_ ztAUd6l<*|j-ung%d6a`vn|P5C9rh&+q_6v?tCQTGzVec9-q5u35ht&eEob(NT3j1 zfLGSW&a?=}b`C)bu%y(y=~IYJ*^E(XK!zl=v*TU-GNkNay2W2xz5R^lD`cOF$;0w4 z{I|DrUZBi3#)kh?CBy8<>SBRVCm7q^ma006@;R-qwN%nFPS@tUykoA}oL2;TdPGSZ zF105!MA=>QssDLCV~v)t!#x`1yA0QGGxyqA&pwZJBme`Eh!pD0HOJ&xqx}~%Y!#^H z4_hphyh=l}5C4?^kVuVyJVzKmc7oYi*T*SS#wLmJ%V?5)zH2&>rDCg&Yw-NvP%Gpw z)XFlLmkBi` z&_`Z}e>SCMSRGADA!~T;oM!2%@WDD|4wMRh4@u6%2eWw_lP?0fri*-LGX!03(a0q& zId4*lu$rjG3MiDgwszJs3hGmRHo;^#h)``cVHaJI>?w#qU z?`vx$dX6gwFVT!-7aS2;#$NZ}hIW%a8=d95(ns6BPVNcDcGTiAp(;TB7uY*ssb1OG!9b$wwmj6qq*Fp>xx^&Ps{!v<@6f6^)?Eh zDQl^qHl#oFe1*5ItT@*{ReX>Vsv)YCK5zk9aFq_zX@{GQ zT*CTGz|Zm0zmV4H43K0)vggQDmciM7Pr|$1AM|kscDOh~pMsEYTw2ko&hOio5A<-` z>WXDxD*!anl&+ZLq&8sI$E|4oux-CHk)T3o6t}gXIGN)8V5&@D~Kyj;>q#|10r-FsL zW9;_jbbGonSp^C?g9nnX^wD{GwC#d4v*)i2T37BQRCqkUk+;>Z%leH*!RbPB2MDaP z{&g}>Yq@E)GgkcPFWaUN{W!4jdOTnB=jau zB-!WXjUR2!NV-^{KrTPm`jaE0y4s-n>l_=2hW%i&jF94>3b-7h8ZU{*dkueBX10xt z3#~iXB+KfbEJRAHc&RlO&?pw9&dtq6krJb3fXf49UFo@=tm=)G9_yjgn&)XSmKR5! zI&h;F@n68=`GZK$MIj3@*KM2{FmY*SX3Mc{&Tz*$^n9cpx`JL5mSCR)kWAMOEhaeI zUh*LPTEQpbs;*dkZMBVsFzZp&r_DCb{yuaA&8>tp1QN82roY{?GvU}s_Q-*LUd_Oe z>X>})m{q3E3ip@zpEfdA@Lyp2AN7B`g58@xNp+TlONIVm3IJ|Zl$z}#Pb_e0t09B4gmfQPtj)gft`4{scf__`JcHQH z*MMwr2n|lT+wyK#Uv`2c361@4BtlF}Xt**F?{bL`Zi*o+aY|amLKbEYR0My7-wlfj zmqW&pjhSg=t^y=__lPde4jz)P$tSw_!lc1Cq;kW3rDscQ9AT9sp za%X=nGSMmxhfJlLV$Kb|ia6AF_esH7Pt@|5BmE5_oCh zF5Hz@k%UMl>A2LyaBNulFbsoER^|3G3W1F)6$AF&{(cYrmP7wz62O_^txC-kAKPTy zt@D&-Ju4EW{h`=f^)i8-3CUj3`j;4*DF7x!y z+%EP`3NY%DKcu>7Tx*krOU~?~PzT?bjC2`%2fq|Wrn%15mEq+WO(}oen<$DSR#7b$ zH-3vxYIahkVt%y`9n?aw8IgI4yz%7HNAk4$E^!qXZ|NUETqRscwj9;N19l{PuKHLA zt&P#lp%);1X?YMBi^>@1DcnODZY-l2y+g$yguY<`tNEGDW7aD zx^!fQ*SW>z_n8TJx>Gnx@5c)I{F@3{nq#Zi@)h_w&_2nI{$C*RpF8QOCde@{gmMIj zvcx-hj%YrAr3O+3|HWtgn+F$@a;Fte5z+?L_Q)bFz$4Pp>LYP* zL>JYIzzm%?{lDb^p}Ag?*s5p;Evj$M7aP+|tMEDP)==ZXABljGOp)R@rmAcv zY1VgUi8KXH@wD&7Bap|Mm%9ttgjAT)Fu}S```twEAel<5K(-Rg7|9_7XIkUs#>OGM z9;Q;bIS|u7aro0-4+=<1w~|U)S`I6q5bKBG>RHO%l@z{z*|7806K(sGpD0UvB4+$ zC8=g&q4D?i*<4$1(8omTqM3{`nfO}rcP}dZ9zB4KmAjkNpDJgh;&=D$KIO>&eqFR< zox70w7_T^n{AzGo)9tCv#-A|!m0(zYR`y!j*>h?e7s8DJ3YjTZ3)j7G$dAsOV$Ob6xx z;cUmoJXS-cHzp!Ro@pmG;r1v~SrN`OW(9aXBeyF;^?;>gIX4QQDTaKfS+mz1(gRR- zKF+fcnqDr`h|~ACB*7sec6KN`YF@avY`g_SaG{dh=E+GE1v9v}LuaR7j2R%DY3E)F z+5Fkp&>t_x%!bhsRW3ELU)L&n(3Mf5T$k<6`4x4~MCu(3Jvq-^vGlFihl3k{Di|e! zl5nS*aH6?g@I}0M^-cpIC<>7ZFp=_dRN143YS)qQ-y{)2j>=#;L&kwaO0L}&R2uNu!Q)a}#a07gcz52tnF_rK zL4Oh%^x!~1QpAr+k@$*4D_Y44l&FpXrCY-5|!;)KSoYdg5G&EDEx?Q zl;Jpxs_G92E7W8P>T2Sf(CE7c;8M%ejbb&_QBmR-(y?$FAS9d11>|rV%#{7en+ps z(Ydh6I$(H1T&%THBfg`zA+spV#gC~h9Ql|JtW{CphSg!PxI?dX-+`y{!lSx;d44T_ zeBDH(AkK?b_EFPyWK9akRGYE|ACn|A$F4=?@UmVEP+0UrhOG`DYxd;?Jhbk^=T^sn zkQT(gN5WmwBG0ZHp&q{jnXhH(g>+!t*YD7V+0e>Vi-05MuO^L_T1i@%;kfP$p17I| zA)xwl@Eu@TB>x>%Bkt>T`c00b?e3YT#+TNg9tzIKf>AWDj@>q8x{{J5fMWwm7>mT9 z_$!a5{f8Z-r=;&uioz=tNk%#3hDvq@BXy9Bi=+$7 z5Y+6|7`Q08dmERb?R)aW0oF?4+@f~|`&w2igiAQx>V?0AVDEQdU)Lg3TD>$np7U4U zVj780sh5%}#wYvXiUn_if&*E%pUNBYNG*C0WN)dfKX>`+N*#u}#vl0+VhRMEBJ>cD z9ENjnrhi1JccSf5@h%>xiQ5y1&uU+=_Bi<%wXN>HqGKuR*qXRulm=O9^7a29hxqaC zwaKI*zJl#-dPpfzG45=8g!dyMkC->)d<4IIzYX*KYk&>X%~zJJb!+yw&YaDf3Zdgb zl;~+dq5Lh*E-Qc~qg~w0QcXK*zIcTB$&qQ&A)_mZbW|I;Laxvy&F+QHpZ=K&(%<#3 z?=gr7q?gizSGBuMZ)}EBei2SI*dtkpE*6tgrRU&P9yY0qF#;3vc&DsL@?(~Jw5r{Z zf#Ln^-in^-GH-<(v-U1;fEQ0+)D^t_nLd=)LRq`>@4AS)rKoXWcq0?d&<8VBCik=T zBv!uhEFVF9yY*!zoYJ6LGeQX!FRfmz3aVCCE2N6S8qMZ|lPn^rt$-v@5eQS%j0L!# zy+kAj8EgpSiO)I@xCuu}m0zY+MbE*jRbH*h&eQiQzxj>2QPMh&5Yrk6lre1$O?A*) zr!BPZ7d>41XE$)tBy5L_O-Uu6c*B8OpmFM^#psl6siE>m0&+e=4R;2)U%Ih5*Od>E z&sBmQ%C25FZ}6iN?30|G+P8wI?jk7-5MjS!{@$)N7bsGXNFt*6Pb+p?0^EOFWQ&t2 zv9U^CCpolq6#hHeV2cC|M=-vfB*>M*`?JIhk^_qsze{qvjE6B6o>as{l9p_YjT7sGC zTcc%qVK(Gj$QwwJaZ4tuMLOMI3`Y-Y*Q`7A5uwww7{dM#I~YKBM?6>^R`NtmnV8BA zlA1zG9lgnkw!7brh5mPYged}N{Zyge0*v&#M zVqThLAr%eKmOgc~@q$3v!xvmnz~W*M01dDW5JufVM0y*Z8A3uvNrp<+L(8DgJ6fI1 z?z$E=HyTFSTmg)H9nSTZtc7P4p#+gOgqF=P#3x$l`%5GWfbX*On%cdj9|`=V1F_sv z$Bnmix>vdzPsP*hM>R!3#m~sT=RYxYrpABi1@L0js~@bR2qP`cvKA9(+*Es8NSXLG=Cj0?9-!yw+)=#JyNO5iR)Ijua+P{-iV94KIdCa&Nqu+I z@uOa-i(+=Izq<@jwJiSpocA+4Jt< zwpdhmc(Iq!JGtIb=&C0j7;o&xD05>(hd*f9jr`?$wdozaUf4uE$;~l+t+3>iURG>A zRvbSn(fMhjhwuK4Khw*dPpJErff$$)j_n^YQ!k9T@ewgsUcU}VJwH>!g~6~c^qtq5 zpK>~zv4Pfz7|I@mH@Rc3u6#J!sne9RLjBo!VAC`|b&tWQ?^984(s2qVdMMYi&qWni z+p7x>=y*AWju9^4`Zh>E$nHSKkN@hEd}h3%KRTrYtu|2Y{#$X95rKogA1>9AXLXCx zVEF!pXohS2ND$jYk^p(5E-joo*PbyU$0__c!irRBR^{R#V!U^loq?X@%RrfS3~0qv zD=|tHj}DdM)c`md1#QS4f1BLV!_0_Ji9!tYmV2!!WcW^LFu7+D1smK_t*EG#H46d2XRP>Flcon)>h(fI$f|! znfi&qk(f7B?Rev{P7@wWr!(##S}S94hscsX+7PjrBELfLHe{fNf?R1`YT$=A#r0vy zn@S7=fAk27d3JV0FcaUe(1Dn!v#-D?w`4 z%+z)~#v$M8rOtCh;7emA7v5VP(3a2?j2)Hlo{l3 zMpCB1Vkt2AFj3E+v|D890KeN_oYPPkTZtxsuu=p6_3rpPK~Qqbsm2KnwGEC$Dq4?z zsP_ql=cR_R8;(S`kgEKBBJDf22|eVR;`7@Z<)R+1+MnQ!)ynheHA~A^<^7lqxMTHK zq#(wUcWubfgHmiBw~UMTc2R1SoVnXkAf7EN4J9DN76G9<3N_+Zoso%*-x;QUp65Xa zWl?ZB9R|jE%N{i;s7G70GbhIlB&Kot$p6yd$*S+Gvrw4x_Pc-l+(Z0+6;w7PR ztBLl9VN>h2HrO!PuH@j1ipM&m4B_8n;(n5s>*BoWo}ypS|e$n&LAJuraCt zd9cog9X!(`K0*9uS^1rja~t*MFlqQ~BgswuNH9VnUhr-NT!POe4#Ama6&d3D+A(=i zN!e5%TXiEh{>*`#y!614i4boCGk4N1>fs)6-L%q6Y&lMFOOeUAj&FwtLVt942t>Z$ zpay%29Cjs80{7{0z?F|aAVe~xa)zsdkW%Wf*Rt`)k|Iq=*+lOVIG>u^8iKScRN zh%53C=6#8v;3Q!~M#o}I54NxyLOOKg1U9T!@S|81<9nM;U}`+$sb4*BPK*u2u)hMI_4Ot6n1aI0HD7}-|R zQz692tKO{xNG(P5+|OIk2ELe>BFkE%Lb7C$=Cj6ko`!(z0+1;BVdoO<#yrz#*F4*3 z=dL3*tu_s`UGEPHz+R68bbwe0=B5xAUYX+S>)smWpx0e^!@D-FDD7|F#2vK^NC{2R zpVJQvi$HxCKjhG zQ)64TS&#Nb>c!!+7iY34uFowJ3-O|eXBQOQEZi(!E|=Te+XG%d{X;y$;!(&q_ZKJ& z^&d#aI%=Z8!in~e2|b069=Lfn{8eT4GbS4(?^6!zojqFedkN1y$3Mulb#uDw1gdPW zMq^?QjqviK=SM&(4v5kHvY@4z0c{tO$WUa5V%$rWd40IkDcSj%ZXCm9(ztANsZQmr zZ*cbF1sp(HOjva~*xt|b{@RKINd3;-8jr=6=`%VHlYi9~+u3$Umjq{F`^G1RT$9y? z6rdv7-k8(zZuwEe`3bo@f#5v{HAmZsXE?{iFMpT!#wvy^X{Lrc9 z8h|~jJ@GGWLIsKOgvGSB{SG{ zZ**VQGHq88lg6lh8JEOWm~iE7pcoG4mb8d`l*FXX!EAc`jJmT={M>mMm&)ZNhg-37 zMzquxJXfF_2i(4L=b9>lx>CZ)Jp65{8A>qt1GYU9|5&;zTc_-*)y3Url}^&dPtE#r zy}n}Hp*y|`zj|Bh6g)vcS-VCsio(#_zPyRq_5S?;q82nbOyX$0tAorJy!uU%YaY21 zN(Xxx%!lTp>ey?gnvw^i*h$e>Yw?FZz7`px=8vEChj|^@=ALWHq7TlL&v$k4B6T}Y zScbBHi=Eh>OX@TH+Lr&ls za9yf7_;vNPekRSWcKaijO{TDwmCtbK9&7J{bf7vz9dG!2wJjaX^BJAMr=g8=x`OZ3 z0Fg^*tHgmfe;oVSF6V$(k@4k^(c<3CY2Iyja4u*~DQo&k3g0n5na1!=kB;WCoX!3m z!AVR|sWn5GX_smf9H$8I(n1TwQT@FAO#oSAfH+?_haj!2swFcz|F{)yRi05xU11RT zG3m=O)wfio&R8mO5?3k0@_Lc-0oEi2@}!Z|Sh!{G`-*KE`Mq>+;)_Qso{HseHsLAL z_*1#0T<<) zv9Tc9`90x2H0p)f$QCqyW!gt%aOLF+U;e5K`;Gb!4<$tIj^>y%kDeM?S?|*$eaT~I zzO#8;fEQ;50-1rtj8PU|?iYwLhvGeH!?3z*hnF9&d)a9Nt_nXZYR8j6xwO{M@|w38 zke`p&?)?Q>V_Nco_W=7rN~Nknzg$aFt!Lk1`BzbEqNX?tdk4o4vKqKpO$KS0n|or8 zPf33q-f1=^g)cfR5(;gRLm%CY24tP&2~+hmdb$&?vc_)M7_=U?w;F9*gN9Dr{F0e@ zg%wfc?;GMYX7H*m`b7>B#Yd`}_%>UUQ_EZZ2h4>$&xr5@_%al&q7Iv~?M5xDx3=E( z%WvY$IToj&vzW}3udgJq6F4g~oOINId!9+k>zm651P(vjHZex2fdmKUNDY*sJvf2- zzxq&?1r9z7tTg(RJrQiGI5nH{D^pFB0WkDZAK40 z-P-gtG&IIF3@SzQT@Sw|3U1|psvM`^an_R#4DDktfT1&qT%g@u#o&{-y!;6FO`1Fw z-{+fio-HhYo%bSnbs$#%dpK|!w6T4RD10|f67C%6<+X@@o5kyl6HA_Q5SF5;4m5}^ k{tn*UrvHejAu`Vh%yklKNleV!FQ6Z3aRsq*5ktTK00t$#1ONa4 literal 0 HcmV?d00001 diff --git a/tools/asm-processor/asm_processor.py b/tools/asm-processor/asm_processor.py index 302668d7de..6923752be4 100644 --- a/tools/asm-processor/asm_processor.py +++ b/tools/asm-processor/asm_processor.py @@ -269,6 +269,43 @@ class Section: assert self.sh_type == SHT_SYMTAB return self.symbol_entries[self.sh_info:] + def relocate_mdebug(self, original_offset): + assert self.sh_type == SHT_MIPS_DEBUG + new_data = bytearray(self.data) + shift_by = self.sh_offset - original_offset + + # Update the file-relative offsets in the Symbolic HDRR + hdrr_magic, hdrr_vstamp, hdrr_ilineMax, hdrr_cbLine, \ + hdrr_cbLineOffset, hdrr_idnMax, hdrr_cbDnOffset, hdrr_ipdMax, \ + hdrr_cbPdOffset, hdrr_isymMax, hdrr_cbSymOffset, hdrr_ioptMax, \ + hdrr_cbOptOffset, hdrr_iauxMax, hdrr_cbAuxOffset, hdrr_issMax, \ + hdrr_cbSsOffset, hdrr_issExtMax, hdrr_cbSsExtOffset, hdrr_ifdMax, \ + hdrr_cbFdOffset, hdrr_crfd, hdrr_cbRfdOffset, hdrr_iextMax, \ + hdrr_cbExtOffset = struct.unpack(">HHIIIIIIIIIIIIIIIIIIIIIII", self.data[0:0x60]) + + assert hdrr_magic == 0x7009 , "Invalid magic value for .mdebug symbolic header" + + hdrr_cbLineOffset += shift_by + hdrr_cbDnOffset += shift_by + hdrr_cbPdOffset += shift_by + hdrr_cbSymOffset += shift_by + hdrr_cbOptOffset += shift_by + hdrr_cbAuxOffset += shift_by + hdrr_cbSsOffset += shift_by + hdrr_cbSsExtOffset += shift_by + hdrr_cbFdOffset += shift_by + hdrr_cbRfdOffset += shift_by + hdrr_cbExtOffset += shift_by + + new_data[0:0x60] = struct.pack(">HHIIIIIIIIIIIIIIIIIIIIIII", hdrr_magic, hdrr_vstamp, hdrr_ilineMax, hdrr_cbLine, \ + hdrr_cbLineOffset, hdrr_idnMax, hdrr_cbDnOffset, hdrr_ipdMax, \ + hdrr_cbPdOffset, hdrr_isymMax, hdrr_cbSymOffset, hdrr_ioptMax, \ + hdrr_cbOptOffset, hdrr_iauxMax, hdrr_cbAuxOffset, hdrr_issMax, \ + hdrr_cbSsOffset, hdrr_issExtMax, hdrr_cbSsExtOffset, hdrr_ifdMax, \ + hdrr_cbFdOffset, hdrr_crfd, hdrr_cbRfdOffset, hdrr_iextMax, \ + hdrr_cbExtOffset) + + self.data = bytes(new_data) class ElfFile: def __init__(self, data): @@ -317,7 +354,7 @@ class ElfFile: s.late_init(self.sections) return s - def drop_irrelevant_sections(self): + def drop_mdebug_gptab(self): # We can only drop sections at the end, since otherwise section # references might be wrong. Luckily, these sections typically are. while self.sections[-1].sh_type in [SHT_MIPS_DEBUG, SHT_MIPS_GPTAB]: @@ -340,7 +377,11 @@ class ElfFile: for s in self.sections: if s.sh_type != SHT_NOBITS and s.sh_type != SHT_NULL: pad_out(s.sh_addralign) + old_offset = s.sh_offset s.sh_offset = outidx + if s.sh_type == SHT_MIPS_DEBUG and s.sh_offset != old_offset: + # The .mdebug section has moved, relocate offsets + s.relocate_mdebug(old_offset) write_out(s.data) pad_out(4) @@ -380,7 +421,7 @@ class Failure(Exception): class GlobalState: - def __init__(self, min_instr_count, skip_instr_count, use_jtbl_for_rodata): + def __init__(self, min_instr_count, skip_instr_count, use_jtbl_for_rodata, mips1): # A value that hopefully never appears as a 32-bit rodata constant (or we # miscompile late rodata). Increases by 1 in each step. self.late_rodata_hex = 0xE0123456 @@ -388,6 +429,7 @@ class GlobalState: self.min_instr_count = min_instr_count self.skip_instr_count = skip_instr_count self.use_jtbl_for_rodata = use_jtbl_for_rodata + self.mips1 = mips1 def next_late_rodata_hex(self): dummy_bytes = struct.pack('>I', self.late_rodata_hex) @@ -608,12 +650,14 @@ class GlobalAsmBlock: size = self.fn_section_sizes['.late_rodata'] // 4 skip_next = False needs_double = (self.late_rodata_alignment != 0) + extra_mips1_nop = False + jtbl_size = 11 if state.mips1 else 9 for i in range(size): if skip_next: skip_next = False continue - # Jump tables give 9 instructions for >= 5 words of rodata, and should be - # emitted when: + # Jump tables give 9 instructions (11 with -mips1) for >= 5 words of rodata, + # and should be emitted when: # - -O2 or -O2 -g3 are used, which give the right codegen # - we have emitted our first .float/.double (to ensure that we find the # created rodata in the binary) @@ -624,11 +668,12 @@ class GlobalAsmBlock: # - we have at least 10 more instructions to go in this function (otherwise our # function size computation will be wrong since the delay slot goes unused) if (not needs_double and state.use_jtbl_for_rodata and i >= 1 and - size - i >= 5 and num_instr - len(late_rodata_fn_output) >= 10): + size - i >= 5 and num_instr - len(late_rodata_fn_output) >= jtbl_size + 1): cases = " ".join("case {}:".format(case) for case in range(size - i)) late_rodata_fn_output.append("switch (*(volatile int*)0) { " + cases + " ; }") - late_rodata_fn_output.extend([""] * 8) + late_rodata_fn_output.extend([""] * (jtbl_size - 1)) jtbl_rodata_size = (size - i) * 4 + extra_mips1_nop = i != 2 break dummy_bytes = state.next_late_rodata_hex() late_rodata_dummy_bytes.append(dummy_bytes) @@ -638,12 +683,20 @@ class GlobalAsmBlock: fval, = struct.unpack('>d', dummy_bytes + dummy_bytes2) late_rodata_fn_output.append('*(volatile double*)0 = {};'.format(fval)) skip_next = True - needs_double = True + needs_double = False + if state.mips1: + # mips1 does not have ldc1/sdc1 + late_rodata_fn_output.append('') + late_rodata_fn_output.append('') + extra_mips1_nop = False else: fval, = struct.unpack('>f', dummy_bytes) late_rodata_fn_output.append('*(volatile float*)0 = {}f;'.format(fval)) + extra_mips1_nop = True late_rodata_fn_output.append('') late_rodata_fn_output.append('') + if state.mips1 and extra_mips1_nop: + late_rodata_fn_output.append('') text_name = None if self.fn_section_sizes['.text'] > 0 or late_rodata_fn_output: @@ -722,7 +775,7 @@ float_regexpr = re.compile(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?f") def repl_float_hex(m): return str(struct.unpack(">I", struct.pack(">f", float(m.group(0).strip().rstrip("f"))))[0]) -def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, print_source=None): +def parse_source(f, opt, framepointer, mips1, input_enc, output_enc, out_dependencies, print_source=None): if opt in ['O2', 'O1']: if framepointer: min_instr_count = 6 @@ -751,7 +804,7 @@ def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, if opt in ['O2', 'g3'] and not framepointer: use_jtbl_for_rodata = True - state = GlobalState(min_instr_count, skip_instr_count, use_jtbl_for_rodata) + state = GlobalState(min_instr_count, skip_instr_count, use_jtbl_for_rodata, mips1) global_asm = None asm_functions = [] @@ -803,7 +856,7 @@ def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, out_dependencies.append(fname) include_src = StringIO() with open(fname, encoding=input_enc) as include_file: - parse_source(include_file, opt, framepointer, input_enc, output_enc, out_dependencies, include_src) + parse_source(include_file, opt, framepointer, mips1, input_enc, output_enc, out_dependencies, include_src) include_src.write('#line ' + str(line_no + 1) + ' "' + f.name + '"') output_lines[-1] = include_src.getvalue() include_src.close() @@ -831,7 +884,7 @@ def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, return asm_functions -def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc): +def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc, drop_mdebug_gptab): SECTIONS = ['.data', '.text', '.rodata', '.bss'] with open(objfile_name, 'rb') as f: @@ -927,9 +980,12 @@ def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc): with open(o_name, 'rb') as f: asm_objfile = ElfFile(f.read()) - # Remove some clutter from objdump output + # Remove clutter from objdump output for tests, and make the tests + # portable by avoiding absolute paths. Outside of tests .mdebug is + # useful for showing source together with asm, though. mdebug_section = objfile.find_section('.mdebug') - objfile.drop_irrelevant_sections() + if drop_mdebug_gptab: + objfile.drop_mdebug_gptab() # Unify reginfo sections target_reginfo = objfile.find_section('.reginfo') @@ -1176,9 +1232,11 @@ def run_wrapped(argv, outfile, functions): parser.add_argument('--post-process', dest='objfile', help="path to .o file to post-process") parser.add_argument('--assembler', dest='assembler', help="assembler command (e.g. \"mips-linux-gnu-as -march=vr4300 -mabi=32\")") parser.add_argument('--asm-prelude', dest='asm_prelude', help="path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.)") - parser.add_argument('--input-enc', default='latin1', help="Input encoding (default: latin1)") - parser.add_argument('--output-enc', default='latin1', help="Output encoding (default: latin1)") + parser.add_argument('--input-enc', default='latin1', help="input encoding (default: %(default)s)") + parser.add_argument('--output-enc', default='latin1', help="output encoding (default: %(default)s)") + parser.add_argument('--drop-mdebug-gptab', dest='drop_mdebug_gptab', action='store_true', help="drop mdebug and gptab sections") parser.add_argument('-framepointer', dest='framepointer', action='store_true') + parser.add_argument('-mips1', dest='mips1', action='store_true') parser.add_argument('-g3', dest='g3', action='store_true') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-O1', dest='opt', action='store_const', const='O1') @@ -1190,25 +1248,27 @@ def run_wrapped(argv, outfile, functions): if opt != 'O2': raise Failure("-g3 is only supported together with -O2") opt = 'g3' + if args.mips1 and (opt != 'O2' or args.framepointer): + raise Failure("-mips1 is only supported together with -O2") if args.objfile is None: with open(args.filename, encoding=args.input_enc) as f: deps = [] - functions = parse_source(f, opt=opt, framepointer=args.framepointer, input_enc=args.input_enc, output_enc=args.output_enc, out_dependencies=deps, print_source=outfile) + functions = parse_source(f, opt=opt, framepointer=args.framepointer, mips1=args.mips1, input_enc=args.input_enc, output_enc=args.output_enc, out_dependencies=deps, print_source=outfile) return functions, deps else: if args.assembler is None: raise Failure("must pass assembler command") if functions is None: with open(args.filename, encoding=args.input_enc) as f: - functions = parse_source(f, opt=opt, framepointer=args.framepointer, input_enc=args.input_enc, out_dependencies=[], output_enc=args.output_enc) + functions = parse_source(f, opt=opt, framepointer=args.framepointer, mips1=args.mips1, input_enc=args.input_enc, out_dependencies=[], output_enc=args.output_enc) if not functions: return asm_prelude = b'' if args.asm_prelude: with open(args.asm_prelude, 'rb') as f: asm_prelude = f.read() - fixup_objfile(args.objfile, functions, asm_prelude, args.assembler, args.output_enc) + fixup_objfile(args.objfile, functions, asm_prelude, args.assembler, args.output_enc, args.drop_mdebug_gptab) def run(argv, outfile=sys.stdout.buffer, functions=None): try: