xref: /llvm-project/llvm/utils/reduce_pipeline.py (revision 91d15aa0b8bff10bd1ccf279418560d17fea52ff)
1#!/usr/bin/env python3
2
3# Automatically formatted with yapf (https://github.com/google/yapf)
4
5# Script for automatic 'opt' pipeline reduction for when using the new
6# pass-manager (NPM). Based around the '-print-pipeline-passes' option.
7#
8# The reduction algorithm consists of several phases (steps).
9#
10# Step #0: Verify that input fails with the given pipeline and make note of the
11# error code.
12#
13# Step #1: Split pipeline in two starting from front and move forward as long as
14# first pipeline exits normally and the second pipeline fails with the expected
15# error code. Move on to step #2 with the IR from the split point and the
16# pipeline from the second invocation.
17#
18# Step #2: Remove passes from end of the pipeline as long as the pipeline fails
19# with the expected error code.
20#
21# Step #3: Make several sweeps over the remaining pipeline trying to remove one
22# pass at a time. Repeat sweeps until unable to remove any more passes.
23#
24# Usage example:
25# reduce_pipeline.py --opt-binary=./build-all-Debug/bin/opt --input=input.ll --output=output.ll --passes=PIPELINE [EXTRA-OPT-ARGS ...]
26
27import argparse
28import pipeline
29import shutil
30import subprocess
31import tempfile
32
33parser = argparse.ArgumentParser(
34    description=
35    'Automatic opt pipeline reducer. Unrecognized arguments are forwarded to opt.'
36)
37parser.add_argument('--opt-binary',
38                    action='store',
39                    dest='opt_binary',
40                    default='opt')
41parser.add_argument('--passes', action='store', dest='passes', required=True)
42parser.add_argument('--input', action='store', dest='input', required=True)
43parser.add_argument('--output', action='store', dest='output')
44parser.add_argument('--dont-expand-passes',
45                    action='store_true',
46                    dest='dont_expand_passes',
47                    help='Do not expand pipeline before starting reduction.')
48parser.add_argument(
49    '--dont-remove-empty-pm',
50    action='store_true',
51    dest='dont_remove_empty_pm',
52    help='Do not remove empty pass-managers from the pipeline during reduction.'
53)
54[args, extra_opt_args] = parser.parse_known_args()
55
56print('The following extra args will be passed to opt: {}'.format(
57    extra_opt_args))
58
59lst = pipeline.fromStr(args.passes)
60passes = '-passes={}'.format(pipeline.toStr(lst))
61ll_input = args.input
62
63# Step #-1
64# Launch 'opt' once with '-print-pipeline-passes' to expand pipeline before
65# starting reduction. Allows specifying a default pipelines (e.g.
66# '-passes=default<O3>').
67if not args.dont_expand_passes:
68    run_args = [
69        args.opt_binary, '-disable-symbolication', '-disable-output',
70        '-print-pipeline-passes', passes, ll_input
71    ]
72    run_args.extend(extra_opt_args)
73    opt = subprocess.run(run_args,
74                         stdout=subprocess.PIPE,
75                         stderr=subprocess.PIPE)
76    if opt.returncode != 0:
77        print('Failed to expand passes. Aborting.')
78        print(run_args)
79        print('exitcode: {}'.format(opt.returncode))
80        print(opt.stderr.decode())
81        exit(1)
82    stdout = opt.stdout.decode()
83    stdout = stdout[:stdout.rfind('\n')]
84    print('Expanded pass sequence: {}'.format(stdout))
85    passes = '-passes={}'.format(stdout)
86
87# Step #0
88# Confirm that the given input, passes and options result in failure.
89print('---Starting step #0---')
90run_args = [
91    args.opt_binary, '-disable-symbolication', '-disable-output', passes,
92    ll_input
93]
94run_args.extend(extra_opt_args)
95opt = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
96if opt.returncode >= 0:
97    print('Input does not result in failure as expected. Aborting.')
98    print(run_args)
99    print('exitcode: {}'.format(opt.returncode))
100    print(opt.stderr.decode())
101    exit(1)
102
103expected_error_returncode = opt.returncode
104print('-passes="{}"'.format(pipeline.toStr(lst)))
105
106# Step #1
107# Try to narrow down the failing pass sequence by splitting the pipeline in two
108# opt invocations (A and B) starting with invocation A only running the first
109# pipeline pass and invocation B the remaining. Keep moving the split point
110# forward as long as invocation A exits normally and invocation B fails with
111# the expected error. This will accomplish two things first the input IR will be
112# further reduced and second, with that IR, the reduced pipeline for invocation
113# B will be sufficient to reproduce.
114print('---Starting step #1---')
115prevLstB = None
116prevIntermediate = None
117tmpd = tempfile.TemporaryDirectory()
118
119for idx in range(pipeline.count(lst)):
120    [lstA, lstB] = pipeline.split(lst, idx)
121    if not args.dont_remove_empty_pm:
122        lstA = pipeline.prune(lstA)
123        lstB = pipeline.prune(lstB)
124    passesA = '-passes=' + pipeline.toStr(lstA)
125    passesB = '-passes=' + pipeline.toStr(lstB)
126
127    intermediate = 'intermediate-0.ll' if idx % 2 else 'intermediate-1.ll'
128    intermediate = tmpd.name + '/' + intermediate
129    run_args = [
130        args.opt_binary, '-disable-symbolication', '-S', '-o', intermediate,
131        passesA, ll_input
132    ]
133    run_args.extend(extra_opt_args)
134    optA = subprocess.run(run_args,
135                          stdout=subprocess.PIPE,
136                          stderr=subprocess.PIPE)
137    run_args = [
138        args.opt_binary, '-disable-symbolication', '-disable-output', passesB,
139        intermediate
140    ]
141    run_args.extend(extra_opt_args)
142    optB = subprocess.run(run_args,
143                          stdout=subprocess.PIPE,
144                          stderr=subprocess.PIPE)
145    if not (optA.returncode == 0
146            and optB.returncode == expected_error_returncode):
147        break
148    prevLstB = lstB
149    prevIntermediate = intermediate
150if prevLstB:
151    lst = prevLstB
152    ll_input = prevIntermediate
153print('-passes="{}"'.format(pipeline.toStr(lst)))
154
155# Step #2
156# Try removing passes from the end of the remaining pipeline while still
157# reproducing the error.
158print('---Starting step #2---')
159prevLstA = None
160for idx in reversed(range(pipeline.count(lst))):
161    [lstA, lstB] = pipeline.split(lst, idx)
162    if not args.dont_remove_empty_pm:
163        lstA = pipeline.prune(lstA)
164    passesA = '-passes=' + pipeline.toStr(lstA)
165    run_args = [
166        args.opt_binary, '-disable-symbolication', '-disable-output', passesA,
167        ll_input
168    ]
169    run_args.extend(extra_opt_args)
170    optA = subprocess.run(run_args,
171                          stdout=subprocess.PIPE,
172                          stderr=subprocess.PIPE)
173    if optA.returncode != expected_error_returncode:
174        break
175    prevLstA = lstA
176if prevLstA:
177    lst = prevLstA
178print('-passes="{}"'.format(pipeline.toStr(lst)))
179
180# Step #3
181# Now that we have a pipeline that is reduced both front and back we do
182# exhaustive sweeps over the remainder trying to remove one pass at a time.
183# Repeat as long as reduction is possible.
184print('---Starting step #3---')
185while True:
186    keepGoing = False
187    for idx in range(pipeline.count(lst)):
188        candLst = pipeline.remove(lst, idx)
189        if not args.dont_remove_empty_pm:
190            candLst = pipeline.prune(candLst)
191        passes = '-passes=' + pipeline.toStr(candLst)
192        run_args = [
193            args.opt_binary, '-disable-symbolication', '-disable-output',
194            passes, ll_input
195        ]
196        run_args.extend(extra_opt_args)
197        opt = subprocess.run(run_args,
198                             stdout=subprocess.PIPE,
199                             stderr=subprocess.PIPE)
200        if opt.returncode == expected_error_returncode:
201            lst = candLst
202            keepGoing = True
203    if not keepGoing:
204        break
205print('-passes="{}"'.format(pipeline.toStr(lst)))
206
207print('---FINISHED---')
208if args.output:
209    shutil.copy(ll_input, args.output)
210    print('Wrote output to \'{}\'.'.format(args.output))
211print('-passes="{}"'.format(pipeline.toStr(lst)))
212exit(0)
213