diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index 1e0e4c9e5..6c2022135 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -1737,6 +1737,23 @@ The following methods are defined for all [arrays](Syntax.md#arrays): You can also iterate over arrays with the [`foreach` statement](Syntax.md#foreach-statements). +### `dictionary` object + +The following methods are defined for all [dictionaries](Syntax.md#dictionaries): + +- `has_key(key)` returns `true` if the dictionary contains the key + given as argument, `false` otherwise + +- `get(key, fallback)`, returns the value for the key given as first argument + if it is present in the dictionary, or the optional fallback value given + as the second argument. If a single argument was given and the key was not + found, causes a fatal error + +You can also iterate over dictionaries with the [`foreach` +statement](Syntax.md#foreach-statements). + +Dictionaries are available since 0.47.0. + ## Returned objects These are objects returned by the [functions listed above](#functions). diff --git a/docs/markdown/Syntax.md b/docs/markdown/Syntax.md index 30eedf814..42002f4ee 100644 --- a/docs/markdown/Syntax.md +++ b/docs/markdown/Syntax.md @@ -284,6 +284,31 @@ The following methods are defined for all arrays: - `contains`, returns `true` if the array contains the object given as argument, `false` otherwise - `get`, returns the object at the given index, negative indices count from the back of the array, indexing out of bounds is a fatal error. Provided for backwards-compatibility, it is identical to array indexing. +Dictionaries +-- + +Dictionaries are delimited by curly braces. A dictionary can contain an +arbitrary number of key value pairs. Keys are required to be literal +strings, values can be objects of any type. + +```meson +my_dict = {'foo': 42, 'bar': 'baz'} +``` + +Keys must be unique: + +```meson +# This will fail +my_dict = {'foo': 42, 'foo': 43} +``` + +Dictionaries are immutable. + +Dictionaries are available since 0.47.0. + +Visit the [Reference Manual](Reference-manual.md#dictionary-object) to read +about the methods exposed by dictionaries. + Function calls -- @@ -329,9 +354,17 @@ endif ## Foreach statements -To do an operation on all elements of an array, use the `foreach` -command. As an example, here's how you would define two executables -with corresponding tests. +To do an operation on all elements of an iterable, use the `foreach` +command. + +> Note that Meson variables are immutable. Trying to assign a new value +> to the iterated object inside a foreach loop will not affect foreach's +> control flow. + +### Foreach with an array + +Here's an example of how you could define two executables +with corresponding tests using arrays and foreach. ```meson progs = [['prog1', ['prog1.c', 'foo.c']], @@ -343,9 +376,31 @@ foreach p : progs endforeach ``` -Note that Meson variables are immutable. Trying to assign a new value -to `progs` inside a foreach loop will not affect foreach's control -flow. +### Foreach with a dictionary + +Here's an example of you could iterate a set of components that +should be compiled in according to some configuration. This uses +a [dictionary][dictionaries], which is available since 0.47.0. + +```meson +components = { + 'foo': ['foo.c'], + 'bar': ['bar.c'], + 'baz:' ['baz.c'], +} + +# compute a configuration based on system dependencies, custom logic +conf = configuration_data() +conf.set('USE_FOO', 1) + +# Determine the sources to compile +sources_to_compile = [] +foreach name, sources : components + if conf.get('USE_@0@'.format(name.to_upper()), 0) == 1 + sources_to_compile += sources + endif +endforeach +``` Logical operations -- diff --git a/docs/markdown/snippets/dict_builtin.md b/docs/markdown/snippets/dict_builtin.md new file mode 100644 index 000000000..1bd24cef9 --- /dev/null +++ b/docs/markdown/snippets/dict_builtin.md @@ -0,0 +1,19 @@ +## New built-in object dictionary + +Meson dictionaries use a syntax similar to python's dictionaries, +but have a narrower scope: they are immutable, keys can only +be string literals, and initializing a dictionary with duplicate +keys causes a fatal error. + +Example usage: + +```meson +dict = {'foo': 42, 'bar': 'baz'} + +foo = dict.get('foo') +foobar = dict.get('foobar', 'fallback-value') + +foreach key, value : dict + Do something with key and value +endforeach +``` diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 070845b4b..a63c14346 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -45,6 +45,8 @@ permitted_method_kwargs = { def stringifyUserArguments(args): if isinstance(args, list): return '[%s]' % ', '.join([stringifyUserArguments(x) for x in args]) + elif isinstance(args, dict): + return '{%s}' % ', '.join(['%s : %s' % (stringifyUserArguments(k), stringifyUserArguments(v)) for k, v in args.items()]) elif isinstance(args, int): return str(args) elif isinstance(args, str): @@ -2283,6 +2285,8 @@ to directly access options of other subprojects.''') arg = posargs[0] if isinstance(arg, list): argstr = stringifyUserArguments(arg) + elif isinstance(arg, dict): + argstr = stringifyUserArguments(arg) elif isinstance(arg, str): argstr = arg elif isinstance(arg, int): diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index 60b046565..9f323d113 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -265,6 +265,8 @@ class InterpreterBase: return self.evaluate_comparison(cur) elif isinstance(cur, mparser.ArrayNode): return self.evaluate_arraystatement(cur) + elif isinstance(cur, mparser.DictNode): + return self.evaluate_dictstatement(cur) elif isinstance(cur, mparser.NumberNode): return cur.value elif isinstance(cur, mparser.AndNode): @@ -296,6 +298,11 @@ class InterpreterBase: raise InvalidCode('Keyword arguments are invalid in array construction.') return arguments + def evaluate_dictstatement(self, cur): + (arguments, kwargs) = self.reduce_arguments(cur.args) + assert (not arguments) + return kwargs + def evaluate_notstatement(self, cur): v = self.evaluate_statement(cur.value) if not isinstance(v, bool): @@ -444,15 +451,28 @@ The result of this is undefined and will become a hard error in a future Meson r def evaluate_foreach(self, node): assert(isinstance(node, mparser.ForeachClauseNode)) - varname = node.varname.value items = self.evaluate_statement(node.items) - if is_disabler(items): - return items - if not isinstance(items, list): - raise InvalidArguments('Items of foreach loop is not an array') - for item in items: - self.set_variable(varname, item) - self.evaluate_codeblock(node.block) + + if isinstance(items, list): + if len(node.varnames) != 1: + raise InvalidArguments('Foreach on array does not unpack') + varname = node.varnames[0].value + if is_disabler(items): + return items + for item in items: + self.set_variable(varname, item) + self.evaluate_codeblock(node.block) + elif isinstance(items, dict): + if len(node.varnames) != 2: + raise InvalidArguments('Foreach on dict unpacks key and value') + if is_disabler(items): + return items + for key, value in items.items(): + self.set_variable(node.varnames[0].value, key) + self.set_variable(node.varnames[1].value, value) + self.evaluate_codeblock(node.block) + else: + raise InvalidArguments('Items of foreach loop must be an array or a dict') def evaluate_plusassign(self, node): assert(isinstance(node, mparser.PlusAssignmentNode)) @@ -491,12 +511,21 @@ The result of this is undefined and will become a hard error in a future Meson r raise InterpreterException( 'Tried to index an object that doesn\'t support indexing.') index = self.evaluate_statement(node.index) - if not isinstance(index, int): - raise InterpreterException('Index value is not an integer.') - try: - return iobject[index] - except IndexError: - raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) + + if isinstance(iobject, dict): + if not isinstance(index, str): + raise InterpreterException('Key is not a string') + try: + return iobject[index] + except KeyError: + raise InterpreterException('Key %s is not in dict' % index) + else: + if not isinstance(index, int): + raise InterpreterException('Index value is not an integer.') + try: + return iobject[index] + except IndexError: + raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) def function_call(self, node): func_name = node.func_name @@ -529,6 +558,8 @@ The result of this is undefined and will become a hard error in a future Meson r return self.int_method_call(obj, method_name, args) if isinstance(obj, list): return self.array_method_call(obj, method_name, args) + if isinstance(obj, dict): + return self.dict_method_call(obj, method_name, args) if isinstance(obj, mesonlib.File): raise InvalidArguments('File object "%s" is not callable.' % obj) if not isinstance(obj, InterpreterObject): @@ -687,6 +718,43 @@ The result of this is undefined and will become a hard error in a future Meson r m = 'Arrays do not have a method called {!r}.' raise InterpreterException(m.format(method_name)) + def dict_method_call(self, obj, method_name, args): + (posargs, kwargs) = self.reduce_arguments(args) + if is_disabled(posargs, kwargs): + return Disabler() + + if method_name in ('has_key', 'get'): + if method_name == 'has_key': + if len(posargs) != 1: + raise InterpreterException('has_key() takes exactly one argument.') + else: + if len(posargs) not in (1, 2): + raise InterpreterException('get() takes one or two arguments.') + + key = posargs[0] + if not isinstance(key, (str)): + raise InvalidArguments('Dictionary key must be a string.') + + has_key = key in obj + + if method_name == 'has_key': + return has_key + + if has_key: + return obj[key] + + if len(posargs) == 2: + return posargs[1] + + raise InterpreterException('Key {!r} is not in the dictionary.'.format(key)) + + if method_name == 'keys': + if len(posargs) != 0: + raise InterpreterException('keys() takes no arguments.') + return list(obj.keys()) + + raise InterpreterException('Dictionaries do not have a method called "%s".' % method_name) + def reduce_arguments(self, args): assert(isinstance(args, mparser.ArgumentNode)) if args.incorrect_order(): @@ -741,7 +809,7 @@ To specify a keyword argument, use : instead of =.''') def is_assignable(self, value): return isinstance(value, (InterpreterObject, dependencies.Dependency, - str, int, list, mesonlib.File)) + str, int, list, dict, mesonlib.File)) def is_elementary_type(self, v): return isinstance(v, (int, float, str, bool, list)) diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index 8cef37743..78683be51 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -104,6 +104,8 @@ class Lexer: ('rparen', re.compile(r'\)')), ('lbracket', re.compile(r'\[')), ('rbracket', re.compile(r'\]')), + ('lcurl', re.compile(r'\{')), + ('rcurl', re.compile(r'\}')), ('dblquote', re.compile(r'"')), ('string', re.compile(r"'([^'\\]|(\\.))*'")), ('comma', re.compile(r',')), @@ -134,6 +136,7 @@ class Lexer: loc = 0 par_count = 0 bracket_count = 0 + curl_count = 0 col = 0 while loc < len(self.code): matched = False @@ -160,6 +163,10 @@ class Lexer: bracket_count += 1 elif tid == 'rbracket': bracket_count -= 1 + elif tid == 'lcurl': + curl_count += 1 + elif tid == 'rcurl': + curl_count -= 1 elif tid == 'dblquote': raise ParseException('Double quotes are not supported. Use single quotes.', self.getline(line_start), lineno, col) elif tid == 'string': @@ -187,7 +194,7 @@ This will become a hard error in a future Meson release.""", self.getline(line_s elif tid == 'eol' or tid == 'eol_cont': lineno += 1 line_start = loc - if par_count > 0 or bracket_count > 0: + if par_count > 0 or bracket_count > 0 or curl_count > 0: break elif tid == 'id': if match_text in self.keywords: @@ -241,6 +248,13 @@ class ArrayNode: self.colno = args.colno self.args = args +class DictNode: + def __init__(self, args): + self.subdir = args.subdir + self.lineno = args.lineno + self.colno = args.colno + self.args = args + class EmptyNode: def __init__(self, lineno, colno): self.subdir = '' @@ -340,10 +354,10 @@ class PlusAssignmentNode: self.value = value class ForeachClauseNode: - def __init__(self, lineno, colno, varname, items, block): + def __init__(self, lineno, colno, varnames, items, block): self.lineno = lineno self.colno = colno - self.varname = varname + self.varnames = varnames self.items = items self.block = block @@ -601,6 +615,10 @@ class Parser: args = self.args() self.block_expect('rbracket', block_start) return ArrayNode(args) + elif self.accept('lcurl'): + key_values = self.key_values() + self.block_expect('rcurl', block_start) + return DictNode(key_values) else: return self.e9() @@ -618,6 +636,31 @@ class Parser: return StringNode(t) return EmptyNode(self.current.lineno, self.current.colno) + def key_values(self): + s = self.statement() + a = ArgumentNode(s) + + while not isinstance(s, EmptyNode): + potential = self.current + if self.accept('colon'): + if not isinstance(s, StringNode): + raise ParseException('Key must be a string.', + self.getline(), s.lineno, s.colno) + if s.value in a.kwargs: + # + 1 to colno to point to the actual string, not the opening quote + raise ParseException('Duplicate dictionary key: {}'.format(s.value), + self.getline(), s.lineno, s.colno + 1) + a.set_kwarg(s.value, self.statement()) + potential = self.current + if not self.accept('comma'): + return a + a.commas.append(potential) + else: + raise ParseException('Only key:value pairs are valid in dict construction.', + self.getline(), s.lineno, s.colno) + s = self.statement() + return a + def args(self): s = self.statement() a = ArgumentNode(s) @@ -629,7 +672,7 @@ class Parser: a.append(s) elif self.accept('colon'): if not isinstance(s, IdNode): - raise ParseException('Keyword argument must be a plain identifier.', + raise ParseException('Dictionary key must be a plain identifier.', self.getline(), s.lineno, s.colno) a.set_kwarg(s.value, self.statement()) potential = self.current @@ -664,10 +707,17 @@ class Parser: t = self.current self.expect('id') varname = t + varnames = [t] + + if self.accept('comma'): + t = self.current + self.expect('id') + varnames.append(t) + self.expect('colon') items = self.statement() block = self.codeblock() - return ForeachClauseNode(varname.lineno, varname.colno, varname, items, block) + return ForeachClauseNode(varname.lineno, varname.colno, varnames, items, block) def ifblock(self): condition = self.statement() diff --git a/run_unittests.py b/run_unittests.py index 3608d3e58..b581e120b 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -2328,6 +2328,20 @@ class FailureTests(BasePlatformTests): self.assertEqual(cm.exception.returncode, 2) self.wipe() + def test_dict_requires_key_value_pairs(self): + self.assertMesonRaises("dict = {3, 'foo': 'bar'}", + 'Only key:value pairs are valid in dict construction.') + self.assertMesonRaises("{'foo': 'bar', 3}", + 'Only key:value pairs are valid in dict construction.') + + def test_dict_forbids_duplicate_keys(self): + self.assertMesonRaises("dict = {'a': 41, 'a': 42}", + 'Duplicate dictionary key: a.*') + + def test_dict_forbids_integer_key(self): + self.assertMesonRaises("dict = {3: 'foo'}", + 'Key must be a string.*') + class WindowsTests(BasePlatformTests): ''' diff --git a/test cases/common/199 dict/meson.build b/test cases/common/199 dict/meson.build new file mode 100644 index 000000000..e1ee2e33b --- /dev/null +++ b/test cases/common/199 dict/meson.build @@ -0,0 +1,23 @@ +project('dict test', 'c') + +dict = {'foo' : 'bar', + 'baz' : 'foo', + 'foo bar': 'baz'} + +exe = executable('prog', sources : ['prog.c']) + +i = 0 + +foreach key, value : dict + test('dict test @0@'.format(key), exe, + args : [dict[key], value]) + i += 1 +endforeach + +assert(i == 3, 'There should be three elements in that dictionary') + +empty_dict = {} + +foreach key, value : empty_dict + assert(false, 'This dict should be empty') +endforeach diff --git a/test cases/common/199 dict/prog.c b/test cases/common/199 dict/prog.c new file mode 100644 index 000000000..bf0999d4a --- /dev/null +++ b/test cases/common/199 dict/prog.c @@ -0,0 +1,8 @@ +#include + +int main(int argc, char **argv) { + if (argc != 3) + return 1; + + return strcmp(argv[1], argv[2]); +}