diff --git a/docs/markdown/Syntax.md b/docs/markdown/Syntax.md index ff4c14286..22b8be35e 100644 --- a/docs/markdown/Syntax.md +++ b/docs/markdown/Syntax.md @@ -283,6 +283,17 @@ Note appending to an array will always create a new array object and assign it to `my_array` instead of modifying the original since all objects in Meson are immutable. +Since 0.49.0, you can check if an array contains an element like this: +```meson +my_array = [1, 2] +if 1 in my_array +# This condition is true +endif +if 1 not in my_array +# This condition is false +endif +``` + #### Array methods The following methods are defined for all arrays: @@ -316,6 +327,20 @@ Dictionaries are available since 0.47.0. Visit the [Reference Manual](Reference-manual.md#dictionary-object) to read about the methods exposed by dictionaries. +Since 0.49.0, you can check if a dictionary contains a key like this: +```meson +my_dict = {'foo': 42, 'foo': 43} +if 'foo' in my_dict +# This condition is true +endif +if 42 in my_dict +# This condition is false +endif +if 'foo' not in my_dict +# This condition is false +endif +``` + Function calls -- @@ -432,6 +457,24 @@ foreach name, sources : components endforeach ``` +### Foreach `break` and `continue` + +Since 0.49.0 `break` and `continue` keywords can be used inside foreach loops. + +```meson +items = ['a', 'continue', 'b', 'break', 'c'] +result = [] +foreach i : items + if i == 'continue' + continue + elif i == 'break' + break + endif + result += i +endforeach +# result is ['a', 'b'] +``` + Comments -- diff --git a/docs/markdown/snippets/new_syntax.md b/docs/markdown/snippets/new_syntax.md new file mode 100644 index 000000000..98eccd033 --- /dev/null +++ b/docs/markdown/snippets/new_syntax.md @@ -0,0 +1,42 @@ +## Foreach `break` and `continue` + +`break` and `continue` keywords can be used inside foreach loops. + +```meson +items = ['a', 'continue', 'b', 'break', 'c'] +result = [] +foreach i : items + if i == 'continue' + continue + elif i == 'break' + break + endif + result += i +endforeach +# result is ['a', 'b'] +``` + +You can check if an array contains an element like this: +```meson +my_array = [1, 2] +if 1 in my_array +# This condition is true +endif +if 1 not in my_array +# This condition is false +endif +``` + +You can check if a dictionary contains a key like this: +```meson +my_dict = {'foo': 42, 'foo': 43} +if 'foo' in my_dict +# This condition is true +endif +if 42 in my_dict +# This condition is false +endif +if 'foo' not in my_dict +# This condition is false +endif +``` diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index e97ac8ec7..f2b8f3407 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -29,6 +29,7 @@ from .interpreterbase import check_stringlist, flatten, noPosargs, noKwargs, str from .interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest from .interpreterbase import InterpreterObject, MutableInterpreterObject, Disabler from .interpreterbase import FeatureNew, FeatureDeprecated, FeatureNewKwargs +from .interpreterbase import ObjectHolder from .modules import ModuleReturnValue import os, shutil, uuid @@ -57,14 +58,6 @@ def stringifyUserArguments(args): raise InvalidArguments('Function accepts only strings, integers, lists and lists thereof.') -class ObjectHolder: - def __init__(self, obj, subproject=None): - self.held_object = obj - self.subproject = subproject - - def __repr__(self): - return ''.format(self.held_object) - class FeatureOptionHolder(InterpreterObject, ObjectHolder): def __init__(self, env, option): InterpreterObject.__init__(self) diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index 1c74eeb41..c0064ab0d 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -21,6 +21,14 @@ from . import environment, dependencies import os, copy, re, types from functools import wraps +class ObjectHolder: + def __init__(self, obj, subproject=None): + self.held_object = obj + self.subproject = subproject + + def __repr__(self): + return ''.format(self.held_object) + # Decorators for method calls. def check_stringlist(a, msg='Arguments must be strings.'): @@ -292,6 +300,12 @@ class InvalidArguments(InterpreterException): class SubdirDoneRequest(BaseException): pass +class ContinueRequest(BaseException): + pass + +class BreakRequest(BaseException): + pass + class InterpreterObject: def __init__(self): self.methods = {} @@ -445,6 +459,10 @@ class InterpreterBase: return self.evaluate_indexing(cur) elif isinstance(cur, mparser.TernaryNode): return self.evaluate_ternary(cur) + elif isinstance(cur, mparser.ContinueNode): + raise ContinueRequest() + elif isinstance(cur, mparser.BreakNode): + raise BreakRequest() elif self.is_elementary_type(cur): return cur else: @@ -487,6 +505,13 @@ class InterpreterBase: return False return True + def evaluate_in(self, val1, val2): + if not isinstance(val1, (str, int, float, ObjectHolder)): + raise InvalidArguments('lvalue of "in" operator must be a string, integer, float, or object') + if not isinstance(val2, (list, dict)): + raise InvalidArguments('rvalue of "in" operator must be an array or a dict') + return val1 in val2 + def evaluate_comparison(self, node): val1 = self.evaluate_statement(node.left) if is_disabler(val1): @@ -494,6 +519,10 @@ class InterpreterBase: val2 = self.evaluate_statement(node.right) if is_disabler(val2): return val2 + if node.ctype == 'in': + return self.evaluate_in(val1, val2) + elif node.ctype == 'notin': + return not self.evaluate_in(val1, val2) valid = self.validate_comparison_types(val1, val2) # Ordering comparisons of different types isn't allowed since PR #1810 # (0.41.0). Since PR #2884 we also warn about equality comparisons of @@ -622,7 +651,12 @@ The result of this is undefined and will become a hard error in a future Meson r return items for item in items: self.set_variable(varname, item) - self.evaluate_codeblock(node.block) + try: + self.evaluate_codeblock(node.block) + except ContinueRequest: + continue + except BreakRequest: + break elif isinstance(items, dict): if len(node.varnames) != 2: raise InvalidArguments('Foreach on dict unpacks key and value') @@ -631,7 +665,12 @@ The result of this is undefined and will become a hard error in a future Meson r 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) + try: + self.evaluate_codeblock(node.block) + except ContinueRequest: + continue + except BreakRequest: + break else: raise InvalidArguments('Items of foreach loop must be an array or a dict') diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index 9af6daca1..be5c807c4 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -90,8 +90,9 @@ class Lexer: def __init__(self, code): self.code = code self.keywords = {'true', 'false', 'if', 'else', 'elif', - 'endif', 'and', 'or', 'not', 'foreach', 'endforeach'} - self.future_keywords = {'continue', 'break', 'in', 'return'} + 'endif', 'and', 'or', 'not', 'foreach', 'endforeach', + 'in', 'continue', 'break'} + self.future_keywords = {'return'} self.token_specification = [ # Need to be sorted longest to shortest. ('ignore', re.compile(r'[ \t]')), @@ -242,6 +243,12 @@ class StringNode(ElementaryNode): def __str__(self): return "String node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) +class ContinueNode(ElementaryNode): + pass + +class BreakNode(ElementaryNode): + pass + class ArrayNode: def __init__(self, args): self.subdir = args.subdir @@ -436,7 +443,9 @@ comparison_map = {'equal': '==', 'lt': '<', 'le': '<=', 'gt': '>', - 'ge': '>=' + 'ge': '>=', + 'in': 'in', + 'notin': 'not in', } # Recursive descent parser for Meson's definition language. @@ -543,6 +552,8 @@ class Parser: for nodename, operator_type in comparison_map.items(): if self.accept(nodename): return ComparisonNode(operator_type, left, self.e5()) + if self.accept('not') and self.accept('in'): + return ComparisonNode('notin', left, self.e5()) return left def e5(self): @@ -754,6 +765,10 @@ class Parser: block = self.foreachblock() self.block_expect('endforeach', block_start) return block + if self.accept('continue'): + return ContinueNode(self.current) + if self.accept('break'): + return BreakNode(self.current) return self.statement() def codeblock(self): diff --git a/test cases/common/17 comparison/meson.build b/test cases/common/17 comparison/meson.build index fb641edb9..bba01684a 100644 --- a/test cases/common/17 comparison/meson.build +++ b/test cases/common/17 comparison/meson.build @@ -137,3 +137,18 @@ assert(2 != 'st', 'not equal') assert(not ([] == 'st'), 'not equal') assert(not ([] == 1), 'not equal') assert(not (2 == 'st'), 'not equal') + +# "in" and "not in" operators + +assert(1 in [1, 2], '''1 should be in [1, 2]''') +assert(3 not in [1, 2], '''3 shouldn't be in [1, 2]''') +assert(not (3 in [1, 2]), '''3 shouldn't be in [1, 2]''') + +assert('b' in ['a', 'b'], ''''b' should be in ['a', 'b']''') +assert('c' not in ['a', 'b'], ''''c' shouldn't be in ['a', 'b']''') + +assert(exe1 in [exe1, exe2], ''''exe1 should be in [exe1, exe2]''') +assert(exe3 not in [exe1, exe2], ''''exe3 shouldn't be in [exe1, exe2]''') + +assert('a' in {'a': 'b'}, '''1 should be in {'a': 'b'}''') +assert('b' not in {'a': 'b'}, '''1 should be in {'a': 'b'}''') diff --git a/test cases/common/64 foreach/meson.build b/test cases/common/64 foreach/meson.build index e633de887..7084e8003 100644 --- a/test cases/common/64 foreach/meson.build +++ b/test cases/common/64 foreach/meson.build @@ -18,3 +18,16 @@ foreach i : tests # we definitely don't want that. tests = ['test4', 'prog4', 'prog4.c'] endforeach + +items = ['a', 'continue', 'b', 'break', 'c'] +result = [] +foreach i : items + if i == 'continue' + continue + elif i == 'break' + break + endif + result += i +endforeach + +assert(result == ['a', 'b'], 'Continue or break in foreach failed')