diff --git a/llvm/utils/pipeline.py b/llvm/utils/pipeline.py new file mode 100644 --- /dev/null +++ b/llvm/utils/pipeline.py @@ -0,0 +1,172 @@ +# Automatically formatted with yapf (https://github.com/google/yapf) +"""Utility functions for creating and manipulating LLVM 'opt' NPM pipeline objects.""" + + +def fromStr(pipeStr): + """Create pipeline object from string representation.""" + stack = [] + curr = [] + tok = '' + kind = '' + for c in pipeStr: + if c == ',': + if tok != '': + curr.append([None, tok]) + tok = '' + elif c == '(': + stack.append([kind, curr]) + kind = tok + curr = [] + tok = '' + elif c == ')': + if tok != '': + curr.append([None, tok]) + tok = '' + oldKind = kind + oldCurr = curr + [kind, curr] = stack.pop() + curr.append([oldKind, oldCurr]) + else: + tok += c + if tok != '': + curr.append([None, tok]) + return curr + + +def toStr(pipeObj): + """Create string representation of pipeline object.""" + res = '' + lastIdx = len(pipeObj) - 1 + for i, c in enumerate(pipeObj): + if c[0]: + res += c[0] + '(' + res += toStr(c[1]) + res += ')' + else: + res += c[1] + if i != lastIdx: + res += ',' + return res + + +def count(pipeObj): + """Count number of passes (pass-managers excluded) in pipeline object.""" + cnt = 0 + for c in pipeObj: + if c[0]: + cnt += count(c[1]) + else: + cnt += 1 + return cnt + + +def split(pipeObj, splitIndex): + """Create two new pipeline objects by splitting pipeObj in two directly after pass with index splitIndex.""" + def splitInt(src, splitIndex, dstA, dstB, idx): + for s in src: + if s[0]: + dstA2 = [] + dstB2 = [] + idx = splitInt(s[1], splitIndex, dstA2, dstB2, idx) + dstA.append([s[0], dstA2]) + dstB.append([s[0], dstB2]) + else: + if idx <= splitIndex: + dstA.append([None, s[1]]) + else: + dstB.append([None, s[1]]) + idx += 1 + return idx + + listA = [] + listB = [] + splitInt(pipeObj, splitIndex, listA, listB, 0) + return [listA, listB] + + +def remove(pipeObj, removeIndex): + """Create new pipeline object by removing pass with index removeIndex from pipeObj.""" + def removeInt(src, removeIndex, dst, idx): + for s in src: + if s[0]: + dst2 = [] + idx = removeInt(s[1], removeIndex, dst2, idx) + dst.append([s[0], dst2]) + else: + if idx != removeIndex: + dst.append([None, s[1]]) + idx += 1 + return idx + + dst = [] + removeInt(pipeObj, removeIndex, dst, 0) + return dst + + +def copy(srcPipeObj): + """Create copy of pipeline object srcPipeObj.""" + def copyInt(dst, src): + for s in src: + if s[0]: + dst2 = [] + copyInt(dst2, s[1]) + dst.append([s[0], dst2]) + else: + dst.append([None, s[1]]) + + dstPipeObj = [] + copyInt(dstPipeObj, srcPipeObj) + return dstPipeObj + + +def prune(srcPipeObj): + """Create new pipeline object by removing empty pass-managers (those with count = 0) from srcPipeObj.""" + def pruneInt(dst, src): + for s in src: + if s[0]: + if count(s[1]): + dst2 = [] + pruneInt(dst2, s[1]) + dst.append([s[0], dst2]) + else: + dst.append([None, s[1]]) + + dstPipeObj = [] + pruneInt(dstPipeObj, srcPipeObj) + return dstPipeObj + + +if __name__ == "__main__": + import unittest + + class Test(unittest.TestCase): + def test_0(self): + pipeStr = 'a,b,A(c,B(d,e),f),g' + pipeObj = fromStr(pipeStr) + + self.assertEqual(7, count(pipeObj)) + + self.assertEqual(pipeObj, pipeObj) + self.assertEqual(pipeObj, prune(pipeObj)) + self.assertEqual(pipeObj, copy(pipeObj)) + + self.assertEqual(pipeStr, toStr(pipeObj)) + self.assertEqual(pipeStr, toStr(prune(pipeObj))) + self.assertEqual(pipeStr, toStr(copy(pipeObj))) + + [pipeObjA, pipeObjB] = split(pipeObj, 3) + self.assertEqual('a,b,A(c,B(d))', toStr(pipeObjA)) + self.assertEqual('A(B(e),f),g', toStr(pipeObjB)) + + self.assertEqual('b,A(c,B(d,e),f),g', toStr(remove(pipeObj, 0))) + self.assertEqual('a,b,A(c,B(d,e),f)', toStr(remove(pipeObj, 6))) + + pipeObjC = remove(pipeObj, 4) + self.assertEqual('a,b,A(c,B(d),f),g', toStr(pipeObjC)) + pipeObjC = remove(pipeObjC, 3) + self.assertEqual('a,b,A(c,B(),f),g', toStr(pipeObjC)) + pipeObjC = prune(pipeObjC) + self.assertEqual('a,b,A(c,f),g', toStr(pipeObjC)) + + unittest.main() + exit(0) diff --git a/llvm/utils/reduce_pipeline.py b/llvm/utils/reduce_pipeline.py new file mode 100755 --- /dev/null +++ b/llvm/utils/reduce_pipeline.py @@ -0,0 +1,212 @@ +#!/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) +passes = '-passes={}'.format(pipeline.toStr(lst)) +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, 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')] + print('Expanded pass sequence: {}'.format(stdout)) + passes = '-passes={}'.format(stdout) + +# 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, + 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) + passesA = '-passes=' + pipeline.toStr(lstA) + passesB = '-passes=' + pipeline.toStr(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, + passesA, 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', passesB, + 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) + passesA = '-passes=' + pipeline.toStr(lstA) + run_args = [ + args.opt_binary, '-disable-symbolication', '-disable-output', passesA, + 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) + passes = '-passes=' + pipeline.toStr(candLst) + run_args = [ + args.opt_binary, '-disable-symbolication', '-disable-output', + passes, 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) diff --git a/llvm/utils/reduce_pipeline_test/fake_opt.py b/llvm/utils/reduce_pipeline_test/fake_opt.py new file mode 100755 --- /dev/null +++ b/llvm/utils/reduce_pipeline_test/fake_opt.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +# Automatically formatted with yapf (https://github.com/google/yapf) + +# Fake 'opt' program that can be made to crash on request. For testing +# the 'reduce_pipeline.py' automatic 'opt' NPM pipeline reducer. + +import argparse +import os +import shutil +import signal + +parser = argparse.ArgumentParser() +parser.add_argument('-passes', action='store', dest='passes', required=True) +parser.add_argument('-print-pipeline-passes', + dest='print_pipeline_passes', + action='store_true') +parser.add_argument('-crash-seq', + action='store', + dest='crash_seq', + required=True) +parser.add_argument('-o', action='store', dest='output') +parser.add_argument('input') +[args, unknown_args] = parser.parse_known_args() + +# Echo pipeline if '-print-pipeline-passes'. +if args.print_pipeline_passes: + print(args.passes) + exit(0) + +# Parse '-crash-seq'. +crash_seq = [] +tok = '' +for c in args.crash_seq: + if c == ',': + if tok != '': + crash_seq.append(tok) + tok = '' + else: + tok += c +if tok != '': + crash_seq.append(tok) +print(crash_seq) + +# Parse '-passes' and see if we need to crash. +tok = '' +for c in args.passes: + if c == ',': + if len(crash_seq) > 0 and crash_seq[0] == tok: + crash_seq.pop(0) + tok = '' + elif c == '(': + tok = '' + elif c == ')': + if len(crash_seq) > 0 and crash_seq[0] == tok: + crash_seq.pop(0) + tok = '' + else: + tok += c +if len(crash_seq) > 0 and crash_seq[0] == tok: + crash_seq.pop(0) + +# Copy input to output. +if args.output: + shutil.copy(args.input, args.output) + +# Crash if all 'crash_seq' passes occured in right order. +if len(crash_seq) == 0: + print('crash') + os.kill(os.getpid(), signal.SIGKILL) +else: + print('no crash') + exit(0) diff --git a/llvm/utils/reduce_pipeline_test/test.py b/llvm/utils/reduce_pipeline_test/test.py new file mode 100755 --- /dev/null +++ b/llvm/utils/reduce_pipeline_test/test.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# Automatically formatted with yapf (https://github.com/google/yapf) + +import subprocess +import unittest + + +def getFinalPasses(run): + stdout = run.stdout.decode() + stdout = stdout[:stdout.rfind('\n')] + stdout = stdout[stdout.rfind('\n') + 1:] + return stdout + + +class Test(unittest.TestCase): + def test_0(self): + """Test all passes are removed except those required to crash. Verify + that PM structure is intact.""" + run_args = [ + './utils/reduce_pipeline.py', + '--opt-binary=./utils/reduce_pipeline_test/fake_opt.py', + '--input=/dev/null', '--passes=a,b,c,A(d,B(e,f),g),h,i', + '-crash-seq=b,d,f' + ] + run = subprocess.run(run_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertEqual(run.returncode, 0) + self.assertEqual(getFinalPasses(run), '-passes="b,A(d,B(f))"') + + def test_1(self): + """Test all passes are removed except those required to crash. The + required passes in this case are the first and last in that order + (a bit of a corner-case for the reduction algorithm).""" + run_args = [ + './utils/reduce_pipeline.py', + '--opt-binary=./utils/reduce_pipeline_test/fake_opt.py', + '--input=/dev/null', '--passes=a,b,c,A(d,B(e,f),g),h,i', + '-crash-seq=a,i' + ] + run = subprocess.run(run_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertEqual(run.returncode, 0) + self.assertEqual(getFinalPasses(run), '-passes="a,i"') + + def test_2(self): + """Test the '--dont-expand-passes' option.""" + run_args = [ + './utils/reduce_pipeline.py', + '--opt-binary=./utils/reduce_pipeline_test/fake_opt.py', + '--input=/dev/null', '--passes=a,b,c,A(d,B(e,f),g),h,i', + '-crash-seq=b,d,f', '--dont-expand-passes' + ] + run = subprocess.run(run_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertEqual(run.returncode, 0) + self.assertEqual(getFinalPasses(run), '-passes="b,A(d,B(f))"') + + def test_3(self): + """Test that empty pass-managers get removed by default.""" + run_args = [ + './utils/reduce_pipeline.py', + '--opt-binary=./utils/reduce_pipeline_test/fake_opt.py', + '--input=/dev/null', '--passes=a,b,c,A(d,B(e,f),g),h,i', + '-crash-seq=b,d,h' + ] + run = subprocess.run(run_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertEqual(run.returncode, 0) + self.assertEqual(getFinalPasses(run), '-passes="b,A(d),h"') + + def test_4(self): + """Test the '--dont-remove-empty-pm' option.""" + run_args = [ + './utils/reduce_pipeline.py', + '--opt-binary=./utils/reduce_pipeline_test/fake_opt.py', + '--input=/dev/null', '--passes=a,b,c,A(d,B(e,f),g),h,i', + '-crash-seq=b,d,h', '--dont-remove-empty-pm' + ] + run = subprocess.run(run_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertEqual(run.returncode, 0) + self.assertEqual(getFinalPasses(run), '-passes="b,A(d,B()),h"') + + +unittest.main() +exit(0)