Skip to content

Commit a892f8d

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 change makes it explicit in the ListOption construction that we would like to treat all ListOption as having a default value of "empty" unless otherwise specified (e.g. using `llvm::list_init`). It removes the requirement that lists are not printed if empty. Instead, lists are not printed if they do not have their default value. It is now clarified that the textual format `my-pass{string-list=""}` or `my-pass{string-list={}}` is interpreted as "empty list". This makes it imposssible to specify that ListOption `string-list` should be a size-1 list containing the empty string. However, `my-pass{string-list={"",""}}` *does* specify a size-2 list containing the empty string. This behavior seems preferable to allow for overriding non-empty defaults as described above.
1 parent fdb90ce commit a892f8d

File tree

4 files changed

+82
-30
lines changed

4 files changed

+82
-30
lines changed

mlir/include/mlir/Pass/PassOptions.h

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ class PassOptions : protected llvm::cl::SubCommand {
253253
assert(!(this->getMiscFlags() & llvm::cl::MiscFlags::CommaSeparated) &&
254254
"ListOption is implicitly comma separated, specifying "
255255
"CommaSeparated is extraneous");
256+
257+
// Make the default explicitly "empty" if no default was given.
258+
if (!this->isDefaultAssigned())
259+
this->setInitialValues({});
260+
256261
parent.options.push_back(this);
257262
elementParser.initialize();
258263
}
@@ -296,11 +301,21 @@ class PassOptions : protected llvm::cl::SubCommand {
296301
const llvm::cl::Option *getOption() const final { return this; }
297302

298303
/// Print the name and value of this option to the given stream.
304+
/// Note that there is currently a limitation with regards to
305+
/// `ListOption<string>`: parsing 'option=""` will result in `option` being
306+
/// set to the empty list, not to a size-1 list containing an empty string.
299307
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;
308+
// Don't print the list if the value is the default value.
309+
if (this->isDefaultAssigned() &&
310+
this->getDefault().size() == (**this).size()) {
311+
unsigned i = 0;
312+
for (unsigned e = (**this).size(); i < e; i++) {
313+
if (!this->getDefault()[i].compare((**this)[i]))
314+
break;
315+
}
316+
if (i == (**this).size())
317+
return;
318+
}
304319

305320
os << this->ArgStr << "={";
306321
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
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={} string=bar}' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_8 %s
25+
26+
// It is not possible to specify a size-1 list of empty string.
27+
// It is possible to specify a size > 1 list of empty strings.
28+
// RUN: mlir-opt %s -verify-each=false '--pass-pipeline=builtin.module(func.func(test-options-pass{string-list={""}}))' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_9 %s
29+
// RUN: mlir-opt %s -verify-each=false '--pass-pipeline=builtin.module(func.func(test-options-pass{string-list={,}}))' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_10 %s
30+
// RUN: mlir-opt %s -verify-each=false '--pass-pipeline=builtin.module(func.func(test-options-pass{string-list={"",}}))' -dump-pass-pipeline 2>&1 | FileCheck --check-prefix=CHECK_10 %s
31+
32+
1733
// CHECK_ERROR_1: missing closing '}' while processing pass options
1834
// CHECK_ERROR_2: no such option test-option
1935
// CHECK_ERROR_3: no such option invalid-option
@@ -27,3 +43,11 @@
2743
// 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} })))
2844
// 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 })))
2945
// 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 }}}))
46+
// CHECK_8{LITERAL}: builtin.module(func.func(test-options-super-pass{list={{enum=zero list={1} string=foo },{enum=one string=bar }}}))
47+
// CHECK_9: builtin.module(func.func(test-options-pass{enum=zero string= string-list={}}))
48+
// CHECK_10: builtin.module(func.func(test-options-pass{enum=zero string= string-list={,}}))
49+
50+
51+
func.func @func() {
52+
return
53+
}

mlir/test/lib/Pass/TestPassManager.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ struct TestOptionsPass
8585
enumOption = options.enumOption;
8686
}
8787

88-
void runOnOperation() final {}
88+
void runOnOperation() final {
89+
llvm::errs() << "string-list has size " << stringListOption.size() << "\n";
90+
}
8991
StringRef getArgument() const final { return "test-options-pass"; }
9092
StringRef getDescription() const final {
9193
return "Test options parsing capabilities";

0 commit comments

Comments
 (0)