Skip to content

Commit fdecfbb

Browse files
[mlir] Attempt to resolve edge cases in PassPipeline textual format
This commit makes the following changes: 1. Previously certain pipeline options could cause the options parser to get stuck in an an infinite loop. An example is: ``` mlir-opt %s -verify-each=false -pass-pipeline='builtin.module(func.func(test-options-super-pass{list={list=1,2},{list=3,4}}))'' ``` In this example, the 'list' option of the `test-options-super-pass` is itself a pass options specification (this capability was added in #101118). However, while the textual format allows `ListOption<int>` to be given as `list=1,2,3`, it did not allow the same format for `ListOption<T>` when T is a subclass of `PassOptions` without extra enclosing `{....}`. Lack of enclosing `{...}` would cause the infinite looping in the parser. This change resolves the parser bug and also allows omitting the outer `{...}` for `ListOption`-of-options. 2. Previously, if you specified a default list value for your `ListOption`, e.g. `ListOption<int> opt{*this, "list", llvm::list_init({1,2,3})}`, it would be impossible to override that default value of `{1,2,3}` with an *empty* list on the command line, since `my-pass{list=}` was not allowed. This was not allowed because of ambiguous handling of lists-of-strings (no literal marker is currently required). This updates this behvior so that specifying empty lists (either `list={}` or just `list=` is allowed.
1 parent fdb90ce commit fdecfbb

File tree

5 files changed

+76
-33
lines changed

5 files changed

+76
-33
lines changed

mlir/include/mlir/Pass/PassOptions.h

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "llvm/ADT/StringRef.h"
2020
#include "llvm/Support/CommandLine.h"
2121
#include "llvm/Support/Compiler.h"
22+
#include "llvm/Support/Debug.h"
2223
#include <memory>
2324

2425
namespace mlir {
@@ -297,10 +298,18 @@ class PassOptions : protected llvm::cl::SubCommand {
297298

298299
/// Print the name and value of this option to the given stream.
299300
void print(raw_ostream &os) final {
300-
// Don't print the list if empty. An empty option value can be treated as
301-
// an element of the list in certain cases (e.g. ListOption<std::string>).
302-
if ((**this).empty())
303-
return;
301+
// Don't print the list if the value is the default value. An empty
302+
// option value should never be treated as an empty list.
303+
if (this->isDefaultAssigned() &&
304+
this->getDefault().size() == (**this).size()) {
305+
unsigned i = 0;
306+
for (unsigned e = (**this).size(); i < e; i++) {
307+
if (!this->getDefault()[i].compare((**this)[i]))
308+
break;
309+
}
310+
if (i == (**this).size())
311+
return;
312+
}
304313

305314
os << this->ArgStr << "={";
306315
auto printElementFn = [&](const DataType &value) {

mlir/lib/Pass/PassRegistry.cpp

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,27 @@ const PassPipelineInfo *mlir::PassPipelineInfo::lookup(StringRef pipelineArg) {
186186
// PassOptions
187187
//===----------------------------------------------------------------------===//
188188

189+
static size_t findChar(StringRef str, size_t index, char c) {
190+
for (size_t i = index, e = str.size(); i < e; ++i) {
191+
if (str[i] == c)
192+
return i;
193+
// Check for various range characters.
194+
if (str[i] == '{')
195+
i = findChar(str, i + 1, '}');
196+
else if (str[i] == '(')
197+
i = findChar(str, i + 1, ')');
198+
else if (str[i] == '[')
199+
i = findChar(str, i + 1, ']');
200+
else if (str[i] == '\"')
201+
i = str.find_first_of('\"', i + 1);
202+
else if (str[i] == '\'')
203+
i = str.find_first_of('\'', i + 1);
204+
if (i == StringRef::npos)
205+
return StringRef::npos;
206+
}
207+
return StringRef::npos;
208+
}
209+
189210
/// Extract an argument from 'options' and update it to point after the arg.
190211
/// Returns the cleaned argument string.
191212
static StringRef extractArgAndUpdateOptions(StringRef &options,
@@ -194,47 +215,37 @@ static StringRef extractArgAndUpdateOptions(StringRef &options,
194215
options = options.drop_front(argSize).ltrim();
195216

196217
// Early exit if there's no escape sequence.
197-
if (str.size() <= 2)
218+
if (str.size() <= 1)
198219
return str;
199220

200221
const auto escapePairs = {std::make_pair('\'', '\''),
201-
std::make_pair('"', '"'), std::make_pair('{', '}')};
222+
std::make_pair('"', '"')};
202223
for (const auto &escape : escapePairs) {
203224
if (str.front() == escape.first && str.back() == escape.second) {
204225
// Drop the escape characters and trim.
205-
str = str.drop_front().drop_back().trim();
206226
// Don't process additional escape sequences.
207-
break;
227+
return str.drop_front().drop_back().trim();
208228
}
209229
}
210230

231+
// Arguments may be wrapped in `{...}`. Unlike the quotation markers that
232+
// denote literals, we respect scoping here. The outer `{...}` should not
233+
// be stripped in cases such as "arg={...},{...}", which can be used to denote
234+
// lists of nested option structs.
235+
if (str.front() == '{') {
236+
unsigned match = findChar(str, 1, '}');
237+
if (match == str.size() - 1)
238+
str = str.drop_front().drop_back().trim();
239+
}
240+
211241
return str;
212242
}
213243

214244
LogicalResult detail::pass_options::parseCommaSeparatedList(
215245
llvm::cl::Option &opt, StringRef argName, StringRef optionStr,
216246
function_ref<LogicalResult(StringRef)> elementParseFn) {
217-
// Functor used for finding a character in a string, and skipping over
218-
// various "range" characters.
219-
llvm::unique_function<size_t(StringRef, size_t, char)> findChar =
220-
[&](StringRef str, size_t index, char c) -> size_t {
221-
for (size_t i = index, e = str.size(); i < e; ++i) {
222-
if (str[i] == c)
223-
return i;
224-
// Check for various range characters.
225-
if (str[i] == '{')
226-
i = findChar(str, i + 1, '}');
227-
else if (str[i] == '(')
228-
i = findChar(str, i + 1, ')');
229-
else if (str[i] == '[')
230-
i = findChar(str, i + 1, ']');
231-
else if (str[i] == '\"')
232-
i = str.find_first_of('\"', i + 1);
233-
else if (str[i] == '\'')
234-
i = str.find_first_of('\'', i + 1);
235-
}
236-
return StringRef::npos;
237-
};
247+
if (optionStr.empty())
248+
return success();
238249

239250
size_t nextElePos = findChar(optionStr, 0, ',');
240251
while (nextElePos != StringRef::npos) {

mlir/test/Pass/pipeline-options-parsing.mlir

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
// RUN: mlir-opt %s -verify-each=false '-test-options-super-pass-pipeline=super-list={{enum=zero list=1 string=foo},{enum=one list=2 string="bar"},{enum=two list=3 string={baz}}}' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_7 %s
1515
// RUN: mlir-opt %s -verify-each=false -pass-pipeline='builtin.module(func.func(test-options-super-pass{list={{enum=zero list={1} string=foo },{enum=one list={2} string=bar },{enum=two list={3} string=baz }}}))' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_7 %s
1616

17+
18+
// This test checks that lists-of-nested-options like 'option1={...},{....}' can be parsed
19+
// just like how 'option=1,2,3' is also allowed:
20+
21+
// RUN: mlir-opt %s -verify-each=false -pass-pipeline='builtin.module(func.func(test-options-super-pass{list={enum=zero list={1} string=foo },{enum=one list={2} string=bar },{enum=two list={3} string=baz }}))' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_7 %s
22+
23+
// This test checks that it is legal to specify an empty list using '{}'.
24+
// RUN: mlir-opt %s -verify-each=false '--test-options-super-pass=list={enum=zero list={1} string=foo},{enum=one list={} default-list= string=bar}' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_8 %s
25+
26+
// This test checks that it is possible to specify lists of empty strings.
27+
// RUN: mlir-opt %s -verify-each=false '--test-options-pass=string-list="",""' '--test-options-pass=string-list=""' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_9 %s
28+
29+
1730
// CHECK_ERROR_1: missing closing '}' while processing pass options
1831
// CHECK_ERROR_2: no such option test-option
1932
// CHECK_ERROR_3: no such option invalid-option
@@ -26,4 +39,8 @@
2639
// CHECK_4: builtin.module(builtin.module(func.func(test-options-pass{enum=zero list={3} string= }),func.func(test-options-pass{enum=one list={1,2,3,4} string=foobar })))
2740
// CHECK_5: builtin.module(builtin.module(func.func(test-options-pass{enum=zero list={3} string= }),func.func(test-options-pass{enum=one list={1,2,3,4} string={foo bar baz} })))
2841
// CHECK_6: builtin.module(builtin.module(func.func(test-options-pass{enum=zero list={3} string= }),func.func(test-options-pass{enum=one list={1,2,3,4} string=foo"bar"baz })))
29-
// CHECK_7{LITERAL}: builtin.module(func.func(test-options-super-pass{list={{enum=zero list={1} string=foo },{enum=one list={2} string=bar },{enum=two list={3} string=baz }}}))
42+
// CHECK_7{LITERAL}: builtin.module(func.func(test-options-super-pass{list={{ enum=zero list={1} string=foo },{ enum=one list={2} string=bar },{ enum=two list={3} string=baz }}}))
43+
44+
// CHECK_8{LITERAL}: builtin.module(func.func(test-options-super-pass{list={{ enum=zero list={1} string=foo },{default-list={} enum=one list={} string=bar }}}))
45+
// CHECK_9: builtin
46+
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// RUN: mlir-opt %s -pass-pipeline="builtin.module(inline)" -dump-pass-pipeline 2>&1 | FileCheck %s
2-
// CHECK: builtin.module(inline{default-pipeline=canonicalize inlining-threshold=4294967295 max-iterations=4 })
2+
// CHECK: builtin.module(inline{default-pipeline=canonicalize inlining-threshold=4294967295 max-iterations=4 op-pipelines={} })

mlir/test/lib/Pass/TestPassManager.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ struct TestOptionsPass
5959
struct Options : public PassPipelineOptions<Options> {
6060
ListOption<int> listOption{*this, "list",
6161
llvm::cl::desc("Example list option")};
62+
ListOption<int> listWithDefaultsOption{
63+
*this, "default-list",
64+
llvm::cl::desc("Example list option with defaults"),
65+
llvm::cl::list_init<int>({10, 9, 8})};
6266
ListOption<std::string> stringListOption{
63-
*this, "string-list", llvm::cl::desc("Example string list option")};
67+
*this, "string-list", llvm::cl::desc("Example string list option"),
68+
llvm::cl::list_init<std::string>({})};
6469
Option<std::string> stringOption{*this, "string",
6570
llvm::cl::desc("Example string option")};
6671
Option<Enum> enumOption{
@@ -94,7 +99,8 @@ struct TestOptionsPass
9499
ListOption<int> listOption{*this, "list",
95100
llvm::cl::desc("Example list option")};
96101
ListOption<std::string> stringListOption{
97-
*this, "string-list", llvm::cl::desc("Example string list option")};
102+
*this, "string-list", llvm::cl::desc("Example string list option"),
103+
llvm::cl::list_init<std::string>({})};
98104
Option<std::string> stringOption{*this, "string",
99105
llvm::cl::desc("Example string option")};
100106
Option<Enum> enumOption{

0 commit comments

Comments
 (0)