diff options
author | Daniel Mensinger <daniel@mensinger-ka.de> | 2021-09-26 11:09:23 +0200 |
---|---|---|
committer | Daniel Mensinger <daniel@mensinger-ka.de> | 2021-10-06 22:37:18 +0200 |
commit | af0587cb490dfa2c2a00c2426c10d1de11d34fdd (patch) | |
tree | ea6cbdf3c861bebc3df145d4950211d45401dceb /mesonbuild/interpreterbase/interpreterbase.py | |
parent | b19530bd7dc99e0b5d71acd5cdf85af915b9ddcc (diff) | |
download | meson-af0587cb490dfa2c2a00c2426c10d1de11d34fdd.zip meson-af0587cb490dfa2c2a00c2426c10d1de11d34fdd.tar.gz meson-af0587cb490dfa2c2a00c2426c10d1de11d34fdd.tar.bz2 |
interpreter: Holderify arrays and dicts
This is the final refactoring for extracting the bultin object
logic out of Interpreterbase. I decided to do both arrays and
dicts in one go since splitting it would have been a lot more
confusing.
Diffstat (limited to 'mesonbuild/interpreterbase/interpreterbase.py')
-rw-r--r-- | mesonbuild/interpreterbase/interpreterbase.py | 507 |
1 files changed, 103 insertions, 404 deletions
diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index 4b4b3c0..bc21951 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -24,9 +24,8 @@ from .baseobjects import ( MutableInterpreterObject, InterpreterObjectTypeVar, ObjectHolder, - RangeHolder, + IterableObject, - TYPE_elementary, TYPE_var, TYPE_kwargs, @@ -42,7 +41,7 @@ from .exceptions import ( BreakRequest ) -from .decorators import FeatureNew, noKwargs +from .decorators import FeatureNew from .disabler import Disabler, is_disabled from .helpers import default_resolve_key, flatten, resolve_second_level_holders from .operator import MesonOperator @@ -51,7 +50,6 @@ from ._unholder import _unholder import os, copy, re, pathlib import typing as T import textwrap -from functools import wraps if T.TYPE_CHECKING: # T.cast is not handled by flake8 to detect quoted annotation use @@ -64,6 +62,8 @@ HolderMapType = T.Dict[ T.Type[int], T.Type[bool], T.Type[str], + T.Type[list], + T.Type[dict], ], # For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar] T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]] @@ -74,21 +74,7 @@ FunctionType = T.Dict[ T.Callable[[mparser.BaseNode, T.List[TYPE_var], T.Dict[str, TYPE_var]], TYPE_var] ] -__FN = T.TypeVar('__FN', bound=T.Callable[['InterpreterBase', T.Any], T.Union[TYPE_var, InterpreterObject]]) -def _holderify_result(types: T.Union[None, T.Type, T.Tuple[T.Type, ...]] = None) -> T.Callable[[__FN], __FN]: - def inner(f: __FN) -> __FN: - @wraps(f) - def wrapper(self: 'InterpreterBase', node: mparser.BaseNode) -> T.Union[TYPE_var, InterpreterObject]: - res = f(self, node) - if types is not None and not isinstance(res, types): - raise mesonlib.MesonBugException(f'Expected {types} but got object `{res}` of type {type(res).__name__}') - return self._holderify(res) - return T.cast(__FN, wrapper) - return inner - class InterpreterBase: - elementary_types = (list, ) - def __init__(self, source_root: str, subdir: str, subproject: str): self.source_root = source_root self.funcs: FunctionType = {} @@ -99,8 +85,7 @@ class InterpreterBase: self.subdir = subdir self.root_subdir = subdir self.subproject = subproject - # TODO: This should actually be more strict: T.Union[TYPE_elementary, InterpreterObject] - self.variables: T.Dict[str, T.Union[TYPE_var, InterpreterObject]] = {} + self.variables: T.Dict[str, InterpreterObject] = {} self.argument_depth = 0 self.current_lineno = -1 # Current node set during a function call. This can be used as location @@ -190,7 +175,7 @@ class InterpreterBase: raise e i += 1 # In THE FUTURE jump over blocks and stuff. - def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[T.Union[TYPE_var, InterpreterObject]]: + def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[InterpreterObject]: self.current_node = cur if isinstance(cur, mparser.FunctionNode): return self.function_call(cur) @@ -238,20 +223,18 @@ class InterpreterBase: raise ContinueRequest() elif isinstance(cur, mparser.BreakNode): raise BreakRequest() - elif isinstance(cur, self.elementary_types): - return cur else: raise InvalidCode("Unknown statement.") return None - def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> T.List[T.Union[TYPE_var, InterpreterObject]]: + def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> InterpreterObject: (arguments, kwargs) = self.reduce_arguments(cur.args) if len(kwargs) > 0: raise InvalidCode('Keyword arguments are invalid in array construction.') - return arguments + return self._holderify([_unholder(x) for x in arguments]) @FeatureNew('dict', '0.47.0') - def evaluate_dictstatement(self, cur: mparser.DictNode) -> T.Union[TYPE_var, InterpreterObject]: + def evaluate_dictstatement(self, cur: mparser.DictNode) -> InterpreterObject: def resolve_key(key: mparser.BaseNode) -> str: if not isinstance(key, mparser.StringNode): FeatureNew.single_use('Dictionary entry using non literal key', '0.53.0', self.subproject) @@ -261,17 +244,13 @@ class InterpreterBase: return str_key arguments, kwargs = self.reduce_arguments(cur.args, key_resolver=resolve_key, duplicate_key_error='Duplicate dictionary key: {}') assert not arguments - return kwargs + return self._holderify({k: _unholder(v) for k, v in kwargs.items()}) - @_holderify_result((bool, Disabler)) - def evaluate_notstatement(self, cur: mparser.NotNode) -> T.Union[TYPE_var, InterpreterObject]: + def evaluate_notstatement(self, cur: mparser.NotNode) -> InterpreterObject: v = self.evaluate_statement(cur.value) if isinstance(v, Disabler): return v - # TYPING TODO: Remove this check once `evaluate_statement` only returns InterpreterObjects - if not isinstance(v, InterpreterObject): - raise mesonlib.MesonBugException(f'Argument to not ({v}) is not an InterpreterObject but {type(v).__name__}.') - return v.operator_call(MesonOperator.NOT, None) + return self._holderify(v.operator_call(MesonOperator.NOT, None)) def evaluate_if(self, node: mparser.IfClauseNode) -> T.Optional[Disabler]: assert isinstance(node, mparser.IfClauseNode) @@ -284,10 +263,10 @@ class InterpreterBase: return result if not isinstance(result, InterpreterObject): raise mesonlib.MesonBugException(f'Argument to not ({result}) is not an InterpreterObject but {type(result).__name__}.') - result = result.operator_call(MesonOperator.BOOL, None) - if not isinstance(result, bool): + res = result.operator_call(MesonOperator.BOOL, None) + if not isinstance(res, bool): raise InvalidCode(f'If clause {result!r} does not evaluate to true or false.') - if result: + if res: prev_meson_version = mesonlib.project_meson_versions[self.subproject] if self.tmp_meson_version: mesonlib.project_meson_versions[self.subproject] = self.tmp_meson_version @@ -300,20 +279,7 @@ class InterpreterBase: self.evaluate_codeblock(node.elseblock) return None - def validate_comparison_types(self, val1: T.Any, val2: T.Any) -> bool: - if type(val1) != type(val2): - return False - return True - - def _evaluate_in(self, val1: T.Any, val2: T.Any) -> bool: - if not isinstance(val1, (str, int, float, mesonlib.HoldableObject)): - 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 - - @_holderify_result((bool, Disabler)) - def evaluate_comparison(self, node: mparser.ComparisonNode) -> T.Union[TYPE_var, InterpreterObject]: + def evaluate_comparison(self, node: mparser.ComparisonNode) -> InterpreterObject: val1 = self.evaluate_statement(node.left) if isinstance(val1, Disabler): return val1 @@ -334,105 +300,42 @@ class InterpreterBase: }[node.ctype] # Check if the arguments should be reversed for simplicity (this essentially converts `in` to `contains`) - if operator in (MesonOperator.IN, MesonOperator.NOT_IN) and isinstance(val2, InterpreterObject): - return val2.operator_call(operator, _unholder(val1)) - - # Normal evaluation, with the same semantics - elif operator not in (MesonOperator.IN, MesonOperator.NOT_IN) and isinstance(val1, InterpreterObject): - return val1.operator_call(operator, _unholder(val2)) - - # OLD CODE, based on the builtin types -- remove once we have switched - # over to all ObjectHolders. - - # Do not compare the ObjectHolders but the actual held objects - val1 = _unholder(val1) - val2 = _unholder(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 - # different types, which is now an error. - if not valid and (node.ctype == '==' or node.ctype == '!='): - raise InvalidArguments(textwrap.dedent( - f''' - Trying to compare values of different types ({type(val1).__name__}, {type(val2).__name__}) using {node.ctype}. - This was deprecated and undefined behavior previously and is as of 0.60.0 a hard error. - ''' - )) - if node.ctype == '==': - return val1 == val2 - elif node.ctype == '!=': - return val1 != val2 - elif not valid: - raise InterpreterException( - 'Values of different types ({}, {}) cannot be compared using {}.'.format(type(val1).__name__, - type(val2).__name__, - node.ctype)) - elif not isinstance(val1, self.elementary_types): - raise InterpreterException('{} can only be compared for equality.'.format(getattr(node.left, 'value', '<ERROR>'))) - elif not isinstance(val2, self.elementary_types): - raise InterpreterException('{} can only be compared for equality.'.format(getattr(node.right, 'value', '<ERROR>'))) - # Use type: ignore because mypy will complain that we are comparing two Unions, - # but we actually guarantee earlier that both types are the same - elif node.ctype == '<': - return val1 < val2 - elif node.ctype == '<=': - return val1 <= val2 - elif node.ctype == '>': - return val1 > val2 - elif node.ctype == '>=': - return val1 >= val2 - else: - raise InvalidCode('You broke my compare eval.') + if operator in (MesonOperator.IN, MesonOperator.NOT_IN): + val1, val2 = val2, val1 - @_holderify_result((bool, Disabler)) - def evaluate_andstatement(self, cur: mparser.AndNode) -> T.Union[TYPE_var, InterpreterObject]: + return self._holderify(val1.operator_call(operator, _unholder(val2))) + + def evaluate_andstatement(self, cur: mparser.AndNode) -> InterpreterObject: l = self.evaluate_statement(cur.left) if isinstance(l, Disabler): return l - if not isinstance(l, InterpreterObject): - raise mesonlib.MesonBugException(f'Firtst argument to and ({l}) is not an InterpreterObject but {type(l).__name__}.') l_bool = l.operator_call(MesonOperator.BOOL, None) if not l_bool: - return l_bool + return self._holderify(l_bool) r = self.evaluate_statement(cur.right) if isinstance(r, Disabler): return r - if not isinstance(r, InterpreterObject): - raise mesonlib.MesonBugException(f'Second argument to and ({r}) is not an InterpreterObject but {type(r).__name__}.') - return r.operator_call(MesonOperator.BOOL, None) + return self._holderify(r.operator_call(MesonOperator.BOOL, None)) - @_holderify_result((bool, Disabler)) - def evaluate_orstatement(self, cur: mparser.OrNode) -> T.Union[TYPE_var, InterpreterObject]: + def evaluate_orstatement(self, cur: mparser.OrNode) -> InterpreterObject: l = self.evaluate_statement(cur.left) if isinstance(l, Disabler): return l - if not isinstance(l, InterpreterObject): - raise mesonlib.MesonBugException(f'Firtst argument to or ({l}) is not an InterpreterObject but {type(l).__name__}.') l_bool = l.operator_call(MesonOperator.BOOL, None) if l_bool: - return l_bool + return self._holderify(l_bool) r = self.evaluate_statement(cur.right) if isinstance(r, Disabler): return r - if not isinstance(r, InterpreterObject): - raise mesonlib.MesonBugException(f'Second argument to ot ({r}) is not an InterpreterObject but {type(r).__name__}.') - return r.operator_call(MesonOperator.BOOL, None) + return self._holderify(r.operator_call(MesonOperator.BOOL, None)) - @_holderify_result() - def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> T.Union[TYPE_var, InterpreterObject]: + def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> InterpreterObject: v = self.evaluate_statement(cur.value) if isinstance(v, Disabler): return v - # TYPING TODO: Remove this check once `evaluate_statement` only returns InterpreterObjects - if not isinstance(v, InterpreterObject): - raise InterpreterException(f'Argument to negation ({v}) is not an InterpreterObject but {type(v).__name__}.') - return v.operator_call(MesonOperator.UMINUS, None) + return self._holderify(v.operator_call(MesonOperator.UMINUS, None)) - def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> T.Union[TYPE_var, InterpreterObject]: + def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> InterpreterObject: l = self.evaluate_statement(cur.left) if isinstance(l, Disabler): return l @@ -440,53 +343,21 @@ class InterpreterBase: if isinstance(r, Disabler): return r - # New code based on InterpreterObjects - if isinstance(l, InterpreterObject): - mapping: T.Dict[str, MesonOperator] = { - 'add': MesonOperator.PLUS, - 'sub': MesonOperator.MINUS, - 'mul': MesonOperator.TIMES, - 'div': MesonOperator.DIV, - 'mod': MesonOperator.MOD, - } - res = l.operator_call(mapping[cur.operation], _unholder(r)) - return self._holderify(res) - - # OLD CODE, based on the builtin types -- remove once we have switched - # over to all ObjectHolders. - - if cur.operation == 'add': - if isinstance(l, dict) and isinstance(r, dict): - return {**l, **r} - try: - # MyPy error due to handling two Unions (we are catching all exceptions anyway) - return l + r # type: ignore - except Exception as e: - raise InvalidCode('Invalid use of addition: ' + str(e)) - elif cur.operation == 'sub': - if not isinstance(l, int) or not isinstance(r, int): - raise InvalidCode('Subtraction works only with integers.') - raise mesonlib.MesonBugException('The integer was not held by an ObjectHolder!') - elif cur.operation == 'mul': - if not isinstance(l, int) or not isinstance(r, int): - raise InvalidCode('Multiplication works only with integers.') - raise mesonlib.MesonBugException('The integer was not held by an ObjectHolder!') - elif cur.operation == 'div': - raise mesonlib.MesonBugException('The integer or string was not held by an ObjectHolder!') - elif cur.operation == 'mod': - if not isinstance(l, int) or not isinstance(r, int): - raise InvalidCode('Modulo works only with integers.') - raise mesonlib.MesonBugException('The integer was not held by an ObjectHolder!') - else: - raise InvalidCode('You broke me.') - - def evaluate_ternary(self, node: mparser.TernaryNode) -> T.Union[TYPE_var, InterpreterObject]: + mapping: T.Dict[str, MesonOperator] = { + 'add': MesonOperator.PLUS, + 'sub': MesonOperator.MINUS, + 'mul': MesonOperator.TIMES, + 'div': MesonOperator.DIV, + 'mod': MesonOperator.MOD, + } + res = l.operator_call(mapping[cur.operation], _unholder(r)) + return self._holderify(res) + + def evaluate_ternary(self, node: mparser.TernaryNode) -> T.Optional[InterpreterObject]: assert isinstance(node, mparser.TernaryNode) result = self.evaluate_statement(node.condition) if isinstance(result, Disabler): return result - if not isinstance(result, InterpreterObject): - raise mesonlib.MesonBugException(f'Ternary condition ({result}) is not an InterpreterObject but {type(result).__name__}.') result_bool = result.operator_call(MesonOperator.BOOL, None) if result_bool: return self.evaluate_statement(node.trueblock) @@ -494,8 +365,7 @@ class InterpreterBase: return self.evaluate_statement(node.falseblock) @FeatureNew('format strings', '0.58.0') - @_holderify_result(str) - def evaluate_fstring(self, node: mparser.FormatStringNode) -> str: + def evaluate_fstring(self, node: mparser.FormatStringNode) -> InterpreterObject: assert isinstance(node, mparser.FormatStringNode) def replace(match: T.Match[str]) -> str: @@ -510,38 +380,37 @@ class InterpreterBase: except KeyError: raise InvalidCode(f'Identifier "{var}" does not name a variable.') - return re.sub(r'@([_a-zA-Z][_0-9a-zA-Z]*)@', replace, node.value) + res = re.sub(r'@([_a-zA-Z][_0-9a-zA-Z]*)@', replace, node.value) + return self._holderify(res) def evaluate_foreach(self, node: mparser.ForeachClauseNode) -> None: assert isinstance(node, mparser.ForeachClauseNode) items = self.evaluate_statement(node.items) - - if isinstance(items, (list, RangeHolder)): - if len(node.varnames) != 1: - raise InvalidArguments('Foreach on array does not unpack') - varname = node.varnames[0] - for item in items: - self.set_variable(varname, self._holderify(item, permissive=True)) - 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') - for key, value in sorted(items.items()): - self.set_variable(node.varnames[0], self._holderify(key)) - self.set_variable(node.varnames[1], self._holderify(value, permissive=True)) - 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') + if not isinstance(items, IterableObject): + raise InvalidArguments('Items of foreach loop do not support iterating') + + tsize = items.iter_tuple_size() + if len(node.varnames) != (tsize or 1): + raise InvalidArguments(f'Foreach expects exactly {tsize or 1} variables for iterating over objects of type {items.display_name()}') + + for i in items.iter_self(): + if tsize is None: + if isinstance(i, tuple): + raise mesonlib.MesonBugException(f'Iteration of {items} returned a tuple even though iter_tuple_size() is None') + self.set_variable(node.varnames[0], self._holderify(i)) + else: + if not isinstance(i, tuple): + raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}') + if len(i) != tsize: + raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}') + for j in range(tsize): + self.set_variable(node.varnames[j], self._holderify(i[j])) + try: + self.evaluate_codeblock(node.block) + except ContinueRequest: + continue + except BreakRequest: + break def evaluate_plusassign(self, node: mparser.PlusAssignmentNode) -> None: assert isinstance(node, mparser.PlusAssignmentNode) @@ -551,69 +420,21 @@ class InterpreterBase: # Remember that all variables are immutable. We must always create a # full new variable and then assign it. old_variable = self.get_variable(varname) - # TYPING TODO: This should only be InterpreterObject in the future - new_value: T.Union[None, TYPE_var, InterpreterObject] = None - if isinstance(old_variable, str): - if not isinstance(addition, str): - raise InvalidArguments('The += operator requires a string on the right hand side if the variable on the left is a string') - new_value = old_variable + addition - elif isinstance(old_variable, list): - if isinstance(addition, list): - new_value = old_variable + addition - else: - new_value = old_variable + [addition] - elif isinstance(old_variable, dict): - if not isinstance(addition, dict): - raise InvalidArguments('The += operator requires a dict on the right hand side if the variable on the left is a dict') - new_value = {**old_variable, **addition} - elif isinstance(old_variable, InterpreterObject): - # TODO: don't make _unholder permissive - new_value = self._holderify(old_variable.operator_call(MesonOperator.PLUS, _unholder(addition))) - # Add other data types here. - else: - raise InvalidArguments('The += operator currently only works with arrays, dicts, strings or ints') + new_value = self._holderify(old_variable.operator_call(MesonOperator.PLUS, _unholder(addition))) self.set_variable(varname, new_value) - def evaluate_indexing(self, node: mparser.IndexNode) -> T.Union[TYPE_elementary, InterpreterObject]: + def evaluate_indexing(self, node: mparser.IndexNode) -> InterpreterObject: assert isinstance(node, mparser.IndexNode) iobject = self.evaluate_statement(node.iobject) if isinstance(iobject, Disabler): return iobject index = _unholder(self.evaluate_statement(node.index)) - if isinstance(iobject, InterpreterObject): - return self._holderify(iobject.operator_call(MesonOperator.INDEX, index)) - if not hasattr(iobject, '__getitem__'): - raise InterpreterException( - 'Tried to index an object that doesn\'t support indexing.') - if isinstance(iobject, dict): - if not isinstance(index, str): - raise InterpreterException('Key is not a string') - try: - # The cast is required because we don't have recursive types... - return T.cast(T.Union[TYPE_elementary, InterpreterObject], 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: - # Ignore the MyPy error, since we don't know all indexable types here - # and we handle non indexable types with an exception - # TODO maybe find a better solution - res = iobject[index] # type: ignore - # Only holderify if we are dealing with `InterpreterObject`, since raw - # lists already store ObjectHolders - if isinstance(iobject, InterpreterObject): - return self._holderify(res) - else: - return res - except IndexError: - # We are already checking for the existence of __getitem__, so this should be save - raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) # type: ignore + if iobject is None: + raise InterpreterException('Tried to evaluate indexing on None') + return self._holderify(iobject.operator_call(MesonOperator.INDEX, index)) - @_holderify_result() - def function_call(self, node: mparser.FunctionNode) -> T.Optional[T.Union[TYPE_var, InterpreterObject]]: + def function_call(self, node: mparser.FunctionNode) -> T.Optional[InterpreterObject]: func_name = node.func_name (h_posargs, h_kwargs) = self.reduce_arguments(node.args) (posargs, kwargs) = self._unholder_args(h_posargs, h_kwargs) @@ -626,14 +447,15 @@ class InterpreterBase: func_args = flatten(posargs) if not getattr(func, 'no-second-level-holder-flattening', False): func_args, kwargs = resolve_second_level_holders(func_args, kwargs) - return func(node, func_args, kwargs) + res = func(node, func_args, kwargs) + return self._holderify(res) if res is not None else None else: self.unknown_function_called(func_name) return None - def method_call(self, node: mparser.MethodNode) -> T.Optional[T.Union[TYPE_var, InterpreterObject]]: + def method_call(self, node: mparser.MethodNode) -> T.Optional[InterpreterObject]: invokable = node.source_object - obj: T.Union[TYPE_var, InterpreterObject] + obj: T.Optional[InterpreterObject] if isinstance(invokable, mparser.IdNode): object_name = invokable.value obj = self.get_variable(object_name) @@ -644,16 +466,6 @@ class InterpreterBase: (args, kwargs) = self._unholder_args(h_args, h_kwargs) if is_disabled(args, kwargs): return Disabler() - if isinstance(obj, str): - raise mesonlib.MesonBugException('Strings are now wrapped in object holders!') - if isinstance(obj, bool): - raise mesonlib.MesonBugException('Booleans are now wrapped in object holders!') - if isinstance(obj, int): - raise mesonlib.MesonBugException('Integers are now wrapped in object holders!') - if isinstance(obj, list): - return self.array_method_call(obj, method_name, args, kwargs) - if isinstance(obj, dict): - return self.dict_method_call(obj, method_name, args, kwargs) if not isinstance(obj, InterpreterObject): raise InvalidArguments('Variable "%s" is not callable.' % object_name) # TODO: InterpreterBase **really** shouldn't be in charge of checking this @@ -663,17 +475,11 @@ class InterpreterBase: elif not isinstance(obj, Disabler): raise InvalidArguments(f'Invalid operation "extract_objects" on variable "{object_name}" of type {type(obj).__name__}') obj.current_node = node - return self._holderify(obj.method_call(method_name, args, kwargs)) + res = obj.method_call(method_name, args, kwargs) + return self._holderify(res) if res is not None else None - def _holderify(self, res: T.Union[TYPE_var, InterpreterObject, None], *, permissive: bool = False) -> T.Union[TYPE_elementary, InterpreterObject]: - # TODO: remove `permissive` once all primitives are ObjectHolders - if res is None: - return None - elif isinstance(res, list): - return [self._holderify(x, permissive=permissive) for x in res] - elif isinstance(res, dict): - return {k: self._holderify(v, permissive=permissive) for k, v in res.items()} - elif isinstance(res, HoldableTypes): + def _holderify(self, res: T.Union[TYPE_var, InterpreterObject]) -> InterpreterObject: + if isinstance(res, HoldableTypes): # Always check for an exact match first. cls = self.holder_map.get(type(res), None) if cls is not None: @@ -686,136 +492,42 @@ class InterpreterBase: return cls(res, T.cast('Interpreter', self)) raise mesonlib.MesonBugException(f'Object {res} of type {type(res).__name__} is neither in self.holder_map nor self.bound_holder_map.') elif isinstance(res, ObjectHolder): - if permissive: - return res raise mesonlib.MesonBugException(f'Returned object {res} of type {type(res).__name__} is an object holder.') elif isinstance(res, MesonInterpreterObject): return res raise mesonlib.MesonBugException(f'Unknown returned object {res} of type {type(res).__name__} in the parameters.') def _unholder_args(self, - args: T.List[T.Union[TYPE_var, InterpreterObject]], - kwargs: T.Dict[str, T.Union[TYPE_var, InterpreterObject]]) -> T.Tuple[T.List[TYPE_var], TYPE_kwargs]: + args: T.List[InterpreterObject], + kwargs: T.Dict[str, InterpreterObject]) -> T.Tuple[T.List[TYPE_var], TYPE_kwargs]: return [_unholder(x) for x in args], {k: _unholder(v) for k, v in kwargs.items()} - @staticmethod - def _get_one_string_posarg(posargs: T.List[TYPE_var], method_name: str) -> str: - if len(posargs) > 1: - raise InterpreterException(f'{method_name}() must have zero or one arguments') - elif len(posargs) == 1: - s = posargs[0] - if not isinstance(s, str): - raise InterpreterException(f'{method_name}() argument must be a string') - return s - return None - def unknown_function_called(self, func_name: str) -> None: raise InvalidCode('Unknown function "%s".' % func_name) - @noKwargs - def array_method_call(self, - obj: T.List[T.Union[TYPE_elementary, InterpreterObject]], - method_name: str, - posargs: T.List[TYPE_var], - kwargs: TYPE_kwargs) -> T.Union[TYPE_var, InterpreterObject]: - if method_name == 'contains': - def check_contains(el: T.List[TYPE_var]) -> bool: - if len(posargs) != 1: - raise InterpreterException('Contains method takes exactly one argument.') - item = posargs[0] - for element in el: - if isinstance(element, list): - found = check_contains(element) - if found: - return True - if element == item: - return True - return False - return self._holderify(check_contains([_unholder(x) for x in obj])) - elif method_name == 'length': - return self._holderify(len(obj)) - elif method_name == 'get': - index = posargs[0] - fallback = None - if len(posargs) == 2: - fallback = self._holderify(posargs[1]) - elif len(posargs) > 2: - m = 'Array method \'get()\' only takes two arguments: the ' \ - 'index and an optional fallback value if the index is ' \ - 'out of range.' - raise InvalidArguments(m) - if not isinstance(index, int): - raise InvalidArguments('Array index must be a number.') - if index < -len(obj) or index >= len(obj): - if fallback is None: - m = 'Array index {!r} is out of bounds for array of size {!r}.' - raise InvalidArguments(m.format(index, len(obj))) - if isinstance(fallback, mparser.BaseNode): - return self.evaluate_statement(fallback) - return fallback - return obj[index] - raise InterpreterException(f'Arrays do not have a method called {method_name!r}.') - - @noKwargs - def dict_method_call(self, - obj: T.Dict[str, T.Union[TYPE_elementary, InterpreterObject]], - method_name: str, - posargs: T.List[TYPE_var], - kwargs: TYPE_kwargs) -> T.Union[TYPE_var, InterpreterObject]: - 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 self._holderify(has_key) - - if has_key: - return obj[key] - - if len(posargs) == 2: - fallback = self._holderify(posargs[1]) - if isinstance(fallback, mparser.BaseNode): - return self.evaluate_statement(fallback) - return fallback - - raise InterpreterException(f'Key {key!r} is not in the dictionary.') - - if method_name == 'keys': - if len(posargs) != 0: - raise InterpreterException('keys() takes no arguments.') - return sorted(obj.keys()) - - raise InterpreterException('Dictionaries do not have a method called "%s".' % method_name) - def reduce_arguments( self, args: mparser.ArgumentNode, key_resolver: T.Callable[[mparser.BaseNode], str] = default_resolve_key, duplicate_key_error: T.Optional[str] = None, ) -> T.Tuple[ - T.List[T.Union[TYPE_var, InterpreterObject]], - T.Dict[str, T.Union[TYPE_var, InterpreterObject]] + T.List[InterpreterObject], + T.Dict[str, InterpreterObject] ]: assert isinstance(args, mparser.ArgumentNode) if args.incorrect_order(): raise InvalidArguments('All keyword arguments must be after positional arguments.') self.argument_depth += 1 - reduced_pos: T.List[T.Union[TYPE_var, InterpreterObject]] = [self.evaluate_statement(arg) for arg in args.arguments] - reduced_kw: T.Dict[str, T.Union[TYPE_var, InterpreterObject]] = {} + reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] + if any(x is None for x in reduced_pos): + raise InvalidArguments(f'At least one value in the arguments is void.') + reduced_kw: T.Dict[str, InterpreterObject] = {} for key, val in args.kwargs.items(): reduced_key = key_resolver(key) assert isinstance(val, mparser.BaseNode) reduced_val = self.evaluate_statement(val) + if reduced_val is None: + raise InvalidArguments(f'Value of key {reduced_key} is void.') if duplicate_key_error and reduced_key in reduced_kw: raise InvalidArguments(duplicate_key_error.format(reduced_key)) reduced_kw[reduced_key] = reduced_val @@ -823,10 +535,10 @@ class InterpreterBase: final_kw = self.expand_default_kwargs(reduced_kw) return reduced_pos, final_kw - def expand_default_kwargs(self, kwargs: T.Dict[str, T.Union[TYPE_var, InterpreterObject]]) -> T.Dict[str, T.Union[TYPE_var, InterpreterObject]]: + def expand_default_kwargs(self, kwargs: T.Dict[str, T.Optional[InterpreterObject]]) -> T.Dict[str, T.Optional[InterpreterObject]]: if 'kwargs' not in kwargs: return kwargs - to_expand = kwargs.pop('kwargs') + to_expand = _unholder(kwargs.pop('kwargs')) if not isinstance(to_expand, dict): raise InterpreterException('Value of "kwargs" must be dictionary.') if 'kwargs' in to_expand: @@ -834,20 +546,20 @@ class InterpreterBase: for k, v in to_expand.items(): if k in kwargs: raise InterpreterException(f'Entry "{k}" defined both as a keyword argument and in a "kwarg" entry.') - kwargs[k] = v + kwargs[k] = self._holderify(v) return kwargs def assignment(self, node: mparser.AssignmentNode) -> None: assert isinstance(node, mparser.AssignmentNode) if self.argument_depth != 0: - raise InvalidArguments('''Tried to assign values inside an argument list. -To specify a keyword argument, use : instead of =.''') + raise InvalidArguments(textwrap.dedent('''\ + Tried to assign values inside an argument list. + To specify a keyword argument, use : instead of =. + ''')) var_name = node.var_name if not isinstance(var_name, str): raise InvalidArguments('Tried to assign value to a non-variable.') value = self.evaluate_statement(node.value) - if not self.is_assignable(value): - raise InvalidCode(f'Tried to assign the invalid value "{value}" of type {type(value).__name__} to variable.') # For mutable objects we need to make a copy on assignment if isinstance(value, MutableInterpreterObject): value = copy.deepcopy(value) @@ -860,36 +572,23 @@ To specify a keyword argument, use : instead of =.''') if holderify: variable = self._holderify(variable) else: - # Ensure that we are never storing a HoldableObject - def check(x: T.Union[TYPE_var, InterpreterObject]) -> None: - if isinstance(x, mesonlib.HoldableObject): - raise mesonlib.MesonBugException(f'set_variable in InterpreterBase called with a HoldableObject {x} of type {type(x).__name__}') - elif isinstance(x, list): - for y in x: - check(y) - elif isinstance(x, dict): - for v in x.values(): - check(v) - check(variable) + # Ensure that we are always storing ObjectHolders + if not isinstance(variable, InterpreterObject): + raise mesonlib.MesonBugException(f'set_variable in InterpreterBase called with a non InterpreterObject {variable} of type {type(variable).__name__}') if not isinstance(varname, str): raise InvalidCode('First argument to set_variable must be a string.') - if not self.is_assignable(variable): - raise InvalidCode(f'Assigned value "{variable}" of type {type(variable).__name__} is not an assignable type.') if re.match('[_a-zA-Z][_0-9a-zA-Z]*$', varname) is None: raise InvalidCode('Invalid variable name: ' + varname) if varname in self.builtin: raise InvalidCode('Tried to overwrite internal variable "%s"' % varname) self.variables[varname] = variable - def get_variable(self, varname: str) -> T.Union[TYPE_var, InterpreterObject]: + def get_variable(self, varname: str) -> InterpreterObject: if varname in self.builtin: return self.builtin[varname] if varname in self.variables: return self.variables[varname] raise InvalidCode('Unknown variable "%s".' % varname) - def is_assignable(self, value: T.Any) -> bool: - return isinstance(value, (InterpreterObject, list, dict)) - def validate_extraction(self, buildtarget: mesonlib.HoldableObject) -> None: raise InterpreterException('validate_extraction is not implemented in this context (please file a bug)') |