Merge pull request #3490 from MathieuDuponchelle/dict_builtin
Add new built-in type, dict
This commit is contained in:
commit
9ecd92c6fe
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
--
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
'''
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
#include <string.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3)
|
||||
return 1;
|
||||
|
||||
return strcmp(argv[1], argv[2]);
|
||||
}
|
Loading…
Reference in New Issue