diff options
author | Boris Kolpackov <boris@codesynthesis.com> | 2019-09-25 13:44:31 +0200 |
---|---|---|
committer | Boris Kolpackov <boris@codesynthesis.com> | 2019-09-30 15:29:50 +0200 |
commit | ea997c89f7ea59db0164c79ac0fda5b607754753 (patch) | |
tree | 2067c898434d9da4f6cadf0e50737f930b299616 | |
parent | c595aac0142436f64ada4f5412b821bfcc6db008 (diff) |
Pattern matching support (switch): single value implementation
-rw-r--r-- | libbuild2/functions-regex.cxx | 2 | ||||
-rw-r--r-- | libbuild2/parser.cxx | 265 | ||||
-rw-r--r-- | libbuild2/parser.hxx | 9 | ||||
-rw-r--r-- | tests/switch/buildfile | 5 | ||||
-rw-r--r-- | tests/switch/testscript | 134 |
5 files changed, 369 insertions, 46 deletions
diff --git a/libbuild2/functions-regex.cxx b/libbuild2/functions-regex.cxx index 339224e..7d0ca20 100644 --- a/libbuild2/functions-regex.cxx +++ b/libbuild2/functions-regex.cxx @@ -373,7 +373,7 @@ namespace build2 // part of a value of an arbitrary type. Convert the value to string prior // to searching. Return the boolean value unless return_match or // return_subs flag is specified (see below) in which case return names - // (empty if no match). + // (empty if no match @@ Why not NULL?). // // The following flags are supported: // diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index ffe64fb..a854df7 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -384,10 +384,20 @@ namespace build2 n == "elif" || n == "elif!") { - // Valid ones are handled in if_else(). + // Valid ones are handled in parse_if_else(). // fail (t) << n << " without if"; } + else if (n == "switch") + { + f = &parser::parse_switch; + } + else if (n == "case") + { + // Valid ones are handled in parse_switch(). + // + fail (t) << n << " outside switch"; + } else if (n == "for") { f = &parser::parse_for; @@ -2010,6 +2020,8 @@ namespace build2 // // So we treat it as a block if it's followed immediately by newline. // + // Note: identical code in parse_switch(). + // if (next (t, tt) == type::lcbrace && peek () == type::newline) { next (t, tt); // Get newline. @@ -2063,6 +2075,165 @@ namespace build2 } void parser:: + parse_switch (token& t, type& tt) + { + // switch <value> + // { + // case <value> + // <line> + // + // case <value> + // { + // <block> + // } + // + // default + // ... + // } + + // Parse and evaluate the value we are switching on. Similar to if-else, + // we expand patterns. + // + next (t, tt); + if (tt == type::newline || tt == type::eos) + fail (t) << "expected switch expression instead of " << t; + + value v (parse_value (t, tt, pattern_mode::expand, "expression", nullptr)); + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t << " after " + << "the switch expression"; + + // Next we should always have a block. + // + if (next (t, tt) != type::lcbrace) + fail (t) << "expected '{' instead of " << t << " after switch"; + + if (next (t, tt) != type::newline) + fail (t) << "expected newline instead of " << t << " after '{'"; + + // Next we have zero or more `case` lines/blocks optionally followed by the + // `default` lines/blocks followed by the closing `}`. + // + bool taken (false); // One of the cases/default has been taken. + + next (t, tt); + for (bool seen_default (false); tt != type::eos; ) + { + if (tt == type::rcbrace) + break; + + string k; + if (tt == type::word && keyword (t)) + { + k = move (t.value); + + if (k == "case") + { + if (seen_default) + fail (t) << "case after default" << + info << "default must be last in the switch block"; + + goto good; + } + else if (k == "default") + { + if (seen_default) + fail (t) << "multiple defaults"; + + seen_default = true; + goto good; + } + } + + fail (t) << "expected case or default instead of " << t << endf; + + good: + + next (t, tt); + + bool take (false); // Take this case/default? + + if (seen_default) + take = !taken; + else + { + // Similar to if-else we are not going to evaluate the case conditions + // if we are skipping. + // + if (taken) + skip_line (t, tt); + else + { + // Parse the pattern and match it against the value. Note that here + // we don't expand patterns. + // + if (tt == type::newline || tt == type::eos) + fail (t) << "expected case pattern instead of " << t; + + const location l (get_location (t)); + + value p ( + parse_value ( + t, tt, pattern_mode::ignore, "pattern", nullptr)); + + take = compare_values (type::equal, v, p, l); + } + } + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t << " after " + << (seen_default ? "default" : "the case pattern"); + + // This can be a block or a single line (the same logic as in if-else). + // + if (next (t, tt) == type::lcbrace && peek () == type::newline) + { + next (t, tt); // Get newline. + next (t, tt); + + if (take) + { + parse_clause (t, tt); + taken = true; + } + else + skip_block (t, tt); + + if (tt != type::rcbrace) + fail (t) << "expected '}' instead of " << t << " at the end of " << k + << "-block"; + + next (t, tt); // Presumably newline after '}'. + next_after_newline (t, tt, '}'); // Should be on its own line. + } + else + { + if (take) + { + if (!parse_clause (t, tt, true)) + fail (t) << "expected " << k << "-line instead of " << t; + + taken = true; + } + else + { + skip_line (t, tt); + + if (tt == type::newline) + next (t, tt); + } + } + } + + if (tt != type::rcbrace) + fail (t) << "expected '}' instead of " << t << " after switch-block"; + + next (t, tt); // Presumably newline after '}'. + next_after_newline (t, tt, '}'); // Should be on its own line. + } + + void parser:: parse_for (token& t, type& tt) { // for <varname>: <value> @@ -2072,7 +2243,6 @@ namespace build2 // { // <block> // } - // // First take care of the variable name. There is no reason not to // support variable attributes. @@ -2945,51 +3115,9 @@ namespace build2 if (pre_parse_) continue; - // Use (potentially typed) comparison via value. If one of the values is - // typed while the other is not, then try to convert the untyped one to - // the other's type instead of complaining. This seems like a reasonable - // thing to do and will allow us to write: - // - // if ($build.version > 30000) - // - // Rather than having to write: - // - // if ($build.version > [uint64] 30000) - // - if (lhs.type != rhs.type) - { - // @@ Would be nice to pass location for diagnostics. - // - if (lhs.type == nullptr) - { - if (lhs) - typify (lhs, *rhs.type, nullptr); - } - else if (rhs.type == nullptr) - { - if (rhs) - typify (rhs, *lhs.type, nullptr); - } - else - fail (l) << "comparison between " << lhs.type->name << " and " - << rhs.type->name; - } - - bool r; - switch (op) - { - case type::equal: r = lhs == rhs; break; - case type::not_equal: r = lhs != rhs; break; - case type::less: r = lhs < rhs; break; - case type::less_equal: r = lhs <= rhs; break; - case type::greater: r = lhs > rhs; break; - case type::greater_equal: r = lhs >= rhs; break; - default: r = false; assert (false); - } - // Store the result as a bool value. // - lhs = value (r); + lhs = value (compare_values (op, lhs, rhs, l)); } return lhs; @@ -3105,6 +3233,53 @@ namespace build2 } } + bool parser:: + compare_values (type op, value& lhs, value& rhs, const location& loc) const + { + // Use (potentially typed) comparison via value. If one of the values is + // typed while the other is not, then try to convert the untyped one to + // the other's type instead of complaining. This seems like a reasonable + // thing to do and will allow us to write: + // + // if ($build.version > 30000) + // + // Rather than having to write: + // + // if ($build.version > [uint64] 30000) + // + if (lhs.type != rhs.type) + { + // @@ Would be nice to pass location for diagnostics. + // + if (lhs.type == nullptr) + { + if (lhs) + typify (lhs, *rhs.type, nullptr); + } + else if (rhs.type == nullptr) + { + if (rhs) + typify (rhs, *lhs.type, nullptr); + } + else + fail (loc) << "comparison between " << lhs.type->name << " and " + << rhs.type->name; + } + + bool r; + switch (op) + { + case type::equal: r = lhs == rhs; break; + case type::not_equal: r = lhs != rhs; break; + case type::less: r = lhs < rhs; break; + case type::less_equal: r = lhs <= rhs; break; + case type::greater: r = lhs > rhs; break; + case type::greater_equal: r = lhs >= rhs; break; + default: r = false; assert (false); + } + return r; + } + pair<bool, location> parser:: attributes_push (token& t, type& tt, bool standalone) { diff --git a/libbuild2/parser.hxx b/libbuild2/parser.hxx index 2f70a18..3014681 100644 --- a/libbuild2/parser.hxx +++ b/libbuild2/parser.hxx @@ -142,6 +142,9 @@ namespace build2 parse_if_else (token&, token_type&); void + parse_switch (token&, token_type&); + + void parse_for (token&, token_type&); void @@ -194,6 +197,12 @@ namespace build2 value parse_eval_value (token&, token_type&, pattern_mode, bool = false); + // Compare two values in a type-aware manner. If one value is typed while + // the other is not, convert the untyped one to the other's type. + // + bool + compare_values (token_type, value&, value&, const location&) const; + // Attributes stack. We can have nested attributes, for example: // // x = [bool] ([uint64] $x == [uint64] $y) diff --git a/tests/switch/buildfile b/tests/switch/buildfile new file mode 100644 index 0000000..0fe074a --- /dev/null +++ b/tests/switch/buildfile @@ -0,0 +1,5 @@ +# file : tests/switch/buildfile +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +./: testscript $b diff --git a/tests/switch/testscript b/tests/switch/testscript new file mode 100644 index 0000000..d59b33d --- /dev/null +++ b/tests/switch/testscript @@ -0,0 +1,134 @@ +# file : tests/switch/testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +# Test switch. + +.include ../common.testscript + +: basics +: +$* <<EOI >>EOO +for i: 1 2 3 +{ + switch $i + { + case 1 + print 1 + case 2 + { + print 2 + } + default + print default + } +} +EOI +1 +2 +default +EOO + +: empty +: +$* <<EOI +switch 1 +{ +} +EOI + +: default +: +$* <<EOI >>EOO +switch 1 +{ + default + print default +} +EOI +default +EOO + +: nested +: +$* <<EOI >>EOO +switch 1 +{ + case 1 + { + switch 2 + { + case 2 + print 2 + case 1 + assert + } + } + case 2 + assert +} +EOI +2 +EOO + +: default-before-case +: +$* <<EOI 2>>EOE != 0 +switch 1 +{ + default + x = 1 + case 2 + x = 2 +} +EOI +<stdin>:5:3: error: case after default + info: default must be last in the switch block +EOE + +: default-multiple +: +$* <<EOI 2>>EOE != 0 +switch 1 +{ + default + x = 1 + default + x = 2 +} +EOI +<stdin>:5:3: error: multiple defaults +EOE + +: empty-switch +: +$* <<EOI 2>>EOE != 0 +switch +{ +} +EOI +<stdin>:1:7: error: expected switch expression instead of <newline> +EOE + +: empty-case +: +$* <<EOI 2>>EOE != 0 +switch 1 +{ + case + x = 1 +} +EOI +<stdin>:3:7: error: expected case pattern instead of <newline> +EOE + +: junk-in-switch +: +$* <<EOI 2>>EOE != 0 +switch 1 +{ + x = 0 +} +EOI +<stdin>:3:3: error: expected case or default instead of 'x' +EOE |