aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2019-09-25 13:44:31 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2019-09-30 15:29:50 +0200
commitea997c89f7ea59db0164c79ac0fda5b607754753 (patch)
tree2067c898434d9da4f6cadf0e50737f930b299616
parentc595aac0142436f64ada4f5412b821bfcc6db008 (diff)
Pattern matching support (switch): single value implementation
-rw-r--r--libbuild2/functions-regex.cxx2
-rw-r--r--libbuild2/parser.cxx265
-rw-r--r--libbuild2/parser.hxx9
-rw-r--r--tests/switch/buildfile5
-rw-r--r--tests/switch/testscript134
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