#!/usr/bin/env python3 # Automatically formatted with yapf (https://github.com/google/yapf) # Script for automatic 'opt' pipeline reduction for when using the new # pass-manager (NPM). Based around the '-print-pipeline-passes' option. # # The reduction algorithm consists of several phases (steps). # # Step #0: Verify that input fails with the given pipeline and make note of the # error code. # # Step #1: Split pipeline in two starting from front and move forward as long as # first pipeline exits normally and the second pipeline fails with the expected # error code. Move on to step #2 with the IR from the split point and the # pipeline from the second invocation. # # Step #2: Remove passes from end of the pipeline as long as the pipeline fails # with the expected error code. # # Step #3: Make several sweeps over the remaining pipeline trying to remove one # pass at a time. Repeat sweeps until unable to remove any more passes. # # Usage example: # reduce_pipeline.py --opt-binary=./build-all-Debug/bin/opt --input=input.ll --output=output.ll --passes=PIPELINE [EXTRA-OPT-ARGS ...] import argparse import pipeline import shutil import subprocess import tempfile parser = argparse.ArgumentParser( description="Automatic opt pipeline reducer. Unrecognized arguments are forwarded to opt." ) parser.add_argument("--opt-binary", action="store", dest="opt_binary", default="opt") parser.add_argument("--passes", action="store", dest="passes", required=True) parser.add_argument("--input", action="store", dest="input", required=True) parser.add_argument("--output", action="store", dest="output") parser.add_argument( "--dont-expand-passes", action="store_true", dest="dont_expand_passes", help="Do not expand pipeline before starting reduction.", ) parser.add_argument( "--dont-remove-empty-pm", action="store_true", dest="dont_remove_empty_pm", help="Do not remove empty pass-managers from the pipeline during reduction.", ) [args, extra_opt_args] = parser.parse_known_args() print("The following extra args will be passed to opt: {}".format(extra_opt_args)) lst = pipeline.fromStr(args.passes) ll_input = args.input # Step #-1 # Launch 'opt' once with '-print-pipeline-passes' to expand pipeline before # starting reduction. Allows specifying a default pipelines (e.g. # '-passes=default'). if not args.dont_expand_passes: run_args = [ args.opt_binary, "-disable-symbolication", "-disable-output", "-print-pipeline-passes", "-passes={}".format(pipeline.toStr(lst)), ll_input, ] run_args.extend(extra_opt_args) opt = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if opt.returncode != 0: print("Failed to expand passes. Aborting.") print(run_args) print("exitcode: {}".format(opt.returncode)) print(opt.stderr.decode()) exit(1) stdout = opt.stdout.decode() stdout = stdout[: stdout.rfind("\n")] lst = pipeline.fromStr(stdout) print("Expanded pass sequence: {}".format(pipeline.toStr(lst))) # Step #0 # Confirm that the given input, passes and options result in failure. print("---Starting step #0---") run_args = [ args.opt_binary, "-disable-symbolication", "-disable-output", "-passes={}".format(pipeline.toStr(lst)), ll_input, ] run_args.extend(extra_opt_args) opt = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if opt.returncode >= 0: print("Input does not result in failure as expected. Aborting.") print(run_args) print("exitcode: {}".format(opt.returncode)) print(opt.stderr.decode()) exit(1) expected_error_returncode = opt.returncode print('-passes="{}"'.format(pipeline.toStr(lst))) # Step #1 # Try to narrow down the failing pass sequence by splitting the pipeline in two # opt invocations (A and B) starting with invocation A only running the first # pipeline pass and invocation B the remaining. Keep moving the split point # forward as long as invocation A exits normally and invocation B fails with # the expected error. This will accomplish two things first the input IR will be # further reduced and second, with that IR, the reduced pipeline for invocation # B will be sufficient to reproduce. print("---Starting step #1---") prevLstB = None prevIntermediate = None tmpd = tempfile.TemporaryDirectory() for idx in range(pipeline.count(lst)): [lstA, lstB] = pipeline.split(lst, idx) if not args.dont_remove_empty_pm: lstA = pipeline.prune(lstA) lstB = pipeline.prune(lstB) intermediate = "intermediate-0.ll" if idx % 2 else "intermediate-1.ll" intermediate = tmpd.name + "/" + intermediate run_args = [ args.opt_binary, "-disable-symbolication", "-S", "-o", intermediate, "-passes={}".format(pipeline.toStr(lstA)), ll_input, ] run_args.extend(extra_opt_args) optA = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) run_args = [ args.opt_binary, "-disable-symbolication", "-disable-output", "-passes={}".format(pipeline.toStr(lstB)), intermediate, ] run_args.extend(extra_opt_args) optB = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if not (optA.returncode == 0 and optB.returncode == expected_error_returncode): break prevLstB = lstB prevIntermediate = intermediate if prevLstB: lst = prevLstB ll_input = prevIntermediate print('-passes="{}"'.format(pipeline.toStr(lst))) # Step #2 # Try removing passes from the end of the remaining pipeline while still # reproducing the error. print("---Starting step #2---") prevLstA = None for idx in reversed(range(pipeline.count(lst))): [lstA, lstB] = pipeline.split(lst, idx) if not args.dont_remove_empty_pm: lstA = pipeline.prune(lstA) run_args = [ args.opt_binary, "-disable-symbolication", "-disable-output", "-passes={}".format(pipeline.toStr(lstA)), ll_input, ] run_args.extend(extra_opt_args) optA = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if optA.returncode != expected_error_returncode: break prevLstA = lstA if prevLstA: lst = prevLstA print('-passes="{}"'.format(pipeline.toStr(lst))) # Step #3 # Now that we have a pipeline that is reduced both front and back we do # exhaustive sweeps over the remainder trying to remove one pass at a time. # Repeat as long as reduction is possible. print("---Starting step #3---") while True: keepGoing = False for idx in range(pipeline.count(lst)): candLst = pipeline.remove(lst, idx) if not args.dont_remove_empty_pm: candLst = pipeline.prune(candLst) run_args = [ args.opt_binary, "-disable-symbolication", "-disable-output", "-passes={}".format(pipeline.toStr(candLst)), ll_input, ] run_args.extend(extra_opt_args) opt = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if opt.returncode == expected_error_returncode: lst = candLst keepGoing = True if not keepGoing: break print('-passes="{}"'.format(pipeline.toStr(lst))) print("---FINISHED---") if args.output: shutil.copy(ll_input, args.output) print("Wrote output to '{}'.".format(args.output)) print('-passes="{}"'.format(pipeline.toStr(lst))) exit(0)