From 8d92e6d865e2a03875c29d2e2aa131ce774a5d5d Mon Sep 17 00:00:00 2001 From: Daniel Mensinger Date: Sun, 29 Aug 2021 13:08:20 +0200 Subject: interpreter: Add IntegerHolder --- mesonbuild/ast/interpreter.py | 3 +- mesonbuild/interpreter/__init__.py | 26 ++++++ mesonbuild/interpreter/interpreter.py | 5 ++ mesonbuild/interpreter/primitives/__init__.py | 8 ++ mesonbuild/interpreter/primitives/integer.py | 84 +++++++++++++++++ mesonbuild/interpreterbase/__init__.py | 9 ++ mesonbuild/interpreterbase/_unholder.py | 6 +- mesonbuild/interpreterbase/baseobjects.py | 4 + mesonbuild/interpreterbase/decorators.py | 2 +- mesonbuild/interpreterbase/interpreterbase.py | 100 +++++++++------------ run_mypy.py | 1 + .../failing/50 executable comparison/test.json | 3 +- 12 files changed, 186 insertions(+), 65 deletions(-) create mode 100644 mesonbuild/interpreter/primitives/__init__.py create mode 100644 mesonbuild/interpreter/primitives/integer.py diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 8c2be37..7e4bc62 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -368,7 +368,8 @@ class AstInterpreter(InterpreterBase): elif isinstance(src, bool): result = self.bool_method_call(src, node.name, margs, mkwargs) elif isinstance(src, int): - result = self.int_method_call(src, node.name, margs, mkwargs) + from ..interpreter import Interpreter, IntegerHolder + result = IntegerHolder(src, T.cast(Interpreter, self)).method_call(node.name, margs, mkwargs) elif isinstance(src, list): result = self.array_method_call(src, node.name, margs, mkwargs) elif isinstance(src, dict): diff --git a/mesonbuild/interpreter/__init__.py b/mesonbuild/interpreter/__init__.py index 62b09bf..0a7717f 100644 --- a/mesonbuild/interpreter/__init__.py +++ b/mesonbuild/interpreter/__init__.py @@ -16,6 +16,28 @@ """Meson interpreter.""" +__all__ = [ + 'Interpreter', + 'permitted_dependency_kwargs', + + 'CompilerHolder', + + 'ExecutableHolder', + 'BuildTargetHolder', + 'CustomTargetHolder', + 'CustomTargetIndexHolder', + 'MachineHolder', + 'Test', + 'ConfigurationDataObject', + 'SubprojectHolder', + 'DependencyHolder', + 'GeneratedListHolder', + 'ExternalProgramHolder', + 'extract_required_kwarg', + + 'IntegerHolder', +] + from .interpreter import Interpreter, permitted_dependency_kwargs from .compiler import CompilerHolder from .interpreterobjects import (ExecutableHolder, BuildTargetHolder, CustomTargetHolder, @@ -23,3 +45,7 @@ from .interpreterobjects import (ExecutableHolder, BuildTargetHolder, CustomTarg ConfigurationDataObject, SubprojectHolder, DependencyHolder, GeneratedListHolder, ExternalProgramHolder, extract_required_kwarg) + +from .primitives import ( + IntegerHolder, +) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 1a34885..db4f9e9 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -58,6 +58,7 @@ from .type_checking import ( NoneType, in_set_validator, ) +from . import primitives as P_OBJ from pathlib import Path import os @@ -376,6 +377,10 @@ class Interpreter(InterpreterBase, HoldableObject): holderify all returned values from methods and functions. ''' self.holder_map.update({ + # Primitives + int: P_OBJ.IntegerHolder, + + # Meson types mesonlib.File: OBJ.FileHolder, build.SharedLibrary: OBJ.SharedLibraryHolder, build.StaticLibrary: OBJ.StaticLibraryHolder, diff --git a/mesonbuild/interpreter/primitives/__init__.py b/mesonbuild/interpreter/primitives/__init__.py new file mode 100644 index 0000000..56af72a --- /dev/null +++ b/mesonbuild/interpreter/primitives/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2021 The Meson development team +# SPDX-license-identifier: Apache-2.0 + +__all__ = [ + 'IntegerHolder', +] + +from .integer import IntegerHolder diff --git a/mesonbuild/interpreter/primitives/integer.py b/mesonbuild/interpreter/primitives/integer.py new file mode 100644 index 0000000..296a3ca --- /dev/null +++ b/mesonbuild/interpreter/primitives/integer.py @@ -0,0 +1,84 @@ +# Copyright 2021 The Meson development team +# SPDX-license-identifier: Apache-2.0 + +from ...interpreterbase import ( + ObjectHolder, + MesonOperator, + typed_operator, + noKwargs, + noPosargs, + + TYPE_var, + TYPE_kwargs, + + InvalidArguments +) + +import typing as T + +if T.TYPE_CHECKING: + # Object holders need the actual interpreter + from ...interpreter import Interpreter + +class IntegerHolder(ObjectHolder[int]): + def __init__(self, obj: int, interpreter: 'Interpreter') -> None: + super().__init__(obj, interpreter) + self.methods.update({ + 'is_even': self.is_even_method, + 'is_odd': self.is_odd_method, + 'to_string': self.to_string_method, + }) + + + self.trivial_operators.update({ + # Arithmetic + MesonOperator.UMINUS: (None, lambda x: -self.held_object), + MesonOperator.PLUS: (int, lambda x: self.held_object + x), + MesonOperator.MINUS: (int, lambda x: self.held_object - x), + MesonOperator.TIMES: (int, lambda x: self.held_object * x), + + # Comparison + MesonOperator.EQUALS: (int, lambda x: self.held_object == x), + MesonOperator.NOT_EQUALS: (int, lambda x: self.held_object != x), + MesonOperator.GREATER: (int, lambda x: self.held_object > x), + MesonOperator.LESS: (int, lambda x: self.held_object < x), + MesonOperator.GREATER_EQUALS: (int, lambda x: self.held_object >= x), + MesonOperator.LESS_EQUALS: (int, lambda x: self.held_object <= x), + }) + + # Use actual methods for functions that require additional checks + self.operators.update({ + MesonOperator.DIV: self.op_div, + MesonOperator.MOD: self.op_mod, + }) + + def display_name(self) -> str: + return 'int' + + @noKwargs + @noPosargs + def is_even_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: + return self.held_object % 2 == 0 + + @noKwargs + @noPosargs + def is_odd_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: + return self.held_object % 2 != 0 + + @noKwargs + @noPosargs + def to_string_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: + return str(self.held_object) + + @typed_operator(MesonOperator.DIV, int) + def op_div(self, other: int) -> int: + if other == 0: + raise InvalidArguments('Tried to divide by 0') + return self.held_object // other + + @typed_operator(MesonOperator.MOD, int) + def op_mod(self, other: int) -> int: + if other == 0: + raise InvalidArguments('Tried to divide by 0') + return self.held_object % other + diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py index c3605b2..e630bbf 100644 --- a/mesonbuild/interpreterbase/__init__.py +++ b/mesonbuild/interpreterbase/__init__.py @@ -69,6 +69,9 @@ __all__ = [ 'TYPE_kwargs', 'TYPE_nkwargs', 'TYPE_key_resolver', + 'TYPE_HoldableTypes', + + 'HoldableTypes', ] from .baseobjects import ( @@ -88,6 +91,9 @@ from .baseobjects import ( TYPE_kwargs, TYPE_nkwargs, TYPE_key_resolver, + TYPE_HoldableTypes, + + HoldableTypes, ) from .decorators import ( @@ -102,6 +108,8 @@ from .decorators import ( typed_pos_args, ContainerTypeInfo, KwargInfo, + typed_operator, + unary_operator, typed_kwargs, FeatureCheckBase, FeatureNew, @@ -122,3 +130,4 @@ from .exceptions import ( from .disabler import Disabler, is_disabled from .helpers import check_stringlist, default_resolve_key, flatten, resolve_second_level_holders from .interpreterbase import MesonVersionString, InterpreterBase +from .operator import MesonOperator diff --git a/mesonbuild/interpreterbase/_unholder.py b/mesonbuild/interpreterbase/_unholder.py index 10c7cfc..55f3423 100644 --- a/mesonbuild/interpreterbase/_unholder.py +++ b/mesonbuild/interpreterbase/_unholder.py @@ -12,21 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .baseobjects import InterpreterObject, MesonInterpreterObject, ObjectHolder, TYPE_var +from .baseobjects import InterpreterObject, MesonInterpreterObject, ObjectHolder, TYPE_var, HoldableTypes from .exceptions import InvalidArguments from ..mesonlib import HoldableObject, MesonBugException import typing as T def _unholder(obj: T.Union[TYPE_var, InterpreterObject], *, permissive: bool = False) -> TYPE_var: - if isinstance(obj, (int, bool, str)): + if isinstance(obj, (bool, str)): return obj elif isinstance(obj, list): return [_unholder(x, permissive=permissive) for x in obj] elif isinstance(obj, dict): return {k: _unholder(v, permissive=permissive) for k, v in obj.items()} elif isinstance(obj, ObjectHolder): - assert isinstance(obj.held_object, HoldableObject) + assert isinstance(obj.held_object, HoldableTypes) return obj.held_object elif isinstance(obj, MesonInterpreterObject): return obj diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py index b28654a..4b538fb 100644 --- a/mesonbuild/interpreterbase/baseobjects.py +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -111,6 +111,9 @@ class InterpreterObject: )) def op_equals(self, other: TYPE_var) -> bool: + # We use `type(...) == type(...)` here to enforce an *exact* match for comparison. We + # don't want comparisons to be possible where `isinstance(derived_obj, type(base_obj))` + # would pass because this comparison must never be true: `derived_obj == base_obj` if type(self) != type(other): self._throw_comp_exception(other, '==') return self == other @@ -148,6 +151,7 @@ class ObjectHolder(InterpreterObject, T.Generic[InterpreterObjectTypeVar]): # Override default comparison operators for the held object def op_equals(self, other: TYPE_var) -> bool: + # See the comment from InterpreterObject why we are using `type()` here. if type(self.held_object) != type(other): self._throw_comp_exception(other, '==') return self.held_object == other diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index 717853f..156b4c9 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -139,7 +139,7 @@ def typed_operator(operator: MesonOperator, return inner def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: - """Decorator that does type checking for operator calls. + """Decorator that does type checking for unary operator calls. This decorator is for unary operators that do not take any other objects. It should be impossible for a user to accidentally break this. Triggering diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index b6e7d0d..d990e61 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -15,7 +15,7 @@ # This class contains the basic functionality needed to run any interpreter # or an interpreter-based tool. -from .. import mparser, mesonlib, mlog +from .. import mparser, mesonlib from .. import environment from .baseobjects import ( @@ -29,6 +29,8 @@ from .baseobjects import ( TYPE_elementary, TYPE_var, TYPE_kwargs, + + HoldableTypes, ) from .exceptions import ( @@ -54,7 +56,10 @@ if T.TYPE_CHECKING: from ..interpreter import Interpreter HolderMapType = T.Dict[ - T.Type[mesonlib.HoldableObject], + T.Union[ + T.Type[mesonlib.HoldableObject], + T.Type[int], + ], # For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar] T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]] ] @@ -68,7 +73,7 @@ class MesonVersionString(str): pass class InterpreterBase: - elementary_types = (int, str, bool, list) + elementary_types = (str, bool, list) def __init__(self, source_root: str, subdir: str, subproject: str): self.source_root = source_root @@ -197,7 +202,7 @@ class InterpreterBase: elif isinstance(cur, mparser.DictNode): return self.evaluate_dictstatement(cur) elif isinstance(cur, mparser.NumberNode): - return cur.value + return self._holderify(cur.value) elif isinstance(cur, mparser.AndNode): return self.evaluate_andstatement(cur) elif isinstance(cur, mparser.OrNode): @@ -396,13 +401,14 @@ class InterpreterBase: raise InterpreterException('Second argument to "or" is not a boolean.') return r - def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> T.Union[int, Disabler]: + def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> T.Union[TYPE_var, InterpreterObject]: v = self.evaluate_statement(cur.value) if isinstance(v, Disabler): return v - if not isinstance(v, int): - raise InterpreterException('Argument to negation is not an integer.') - 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 self._holderify(v.operator_call(MesonOperator.UMINUS, None)) @FeatureNew('/ with string arguments', '0.49.0') def evaluate_path_join(self, l: str, r: str) -> str: @@ -412,16 +418,7 @@ class InterpreterBase: raise InvalidCode('The division operator can only append a string.') return self.join_path_strings((l, r)) - def evaluate_division(self, l: T.Any, r: T.Any) -> T.Union[int, str]: - if isinstance(l, str) or isinstance(r, str): - return self.evaluate_path_join(l, r) - if isinstance(l, int) and isinstance(r, int): - if r == 0: - raise InvalidCode('Division by zero.') - return l // r - raise InvalidCode('Division works only with strings or integers.') - - def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> T.Union[int, str, dict, list, Disabler]: + def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> T.Union[TYPE_var, InterpreterObject]: l = self.evaluate_statement(cur.left) if isinstance(l, Disabler): return l @@ -455,17 +452,19 @@ class InterpreterBase: elif cur.operation == 'sub': if not isinstance(l, int) or not isinstance(r, int): raise InvalidCode('Subtraction works only with integers.') - return l - r + 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.') - return l * r + raise mesonlib.MesonBugException('The integer was not held by an ObjectHolder!') elif cur.operation == 'div': - return self.evaluate_division(l, r) + if isinstance(l, str) and isinstance(r, str): + return self.evaluate_path_join(l, r) + raise InvalidCode('Division works only with strings or integers.') elif cur.operation == 'mod': if not isinstance(l, int) or not isinstance(r, int): raise InvalidCode('Modulo works only with integers.') - return l % r + raise mesonlib.MesonBugException('The integer was not held by an ObjectHolder!') else: raise InvalidCode('You broke me.') @@ -488,7 +487,7 @@ class InterpreterBase: def replace(match: T.Match[str]) -> str: var = str(match.group(1)) try: - val = self.variables[var] + val = _unholder(self.variables[var]) if not isinstance(val, (str, int, float, bool)): raise InvalidCode(f'Identifier "{var}" does not name a formattable variable ' + '(has to be an integer, a string, a floating point number or a boolean).') @@ -508,7 +507,7 @@ class InterpreterBase: raise InvalidArguments('Foreach on array does not unpack') varname = node.varnames[0] for item in items: - self.set_variable(varname, item) + self.set_variable(varname, self._holderify(item, permissive=True)) try: self.evaluate_codeblock(node.block) except ContinueRequest: @@ -538,15 +537,12 @@ 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) - new_value = None # type: T.Union[str, int, float, bool, dict, list] + # 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, int): - if not isinstance(addition, int): - raise InvalidArguments('The += operator requires an int on the right hand side if the variable on the left is an int') - new_value = old_variable + addition elif isinstance(old_variable, list): if isinstance(addition, list): new_value = old_variable + addition @@ -556,6 +552,9 @@ class InterpreterBase: 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, permissive=True))) # Add other data types here. else: raise InvalidArguments('The += operator currently only works with arrays, dicts, strings or ints') @@ -569,7 +568,7 @@ class InterpreterBase: if not hasattr(iobject, '__getitem__'): raise InterpreterException( 'Tried to index an object that doesn\'t support indexing.') - index = self.evaluate_statement(node.index) + index = _unholder(self.evaluate_statement(node.index)) if isinstance(iobject, dict): if not isinstance(index, str): @@ -630,11 +629,11 @@ class InterpreterBase: if is_disabled(args, kwargs): return Disabler() if isinstance(obj, str): - return self.string_method_call(obj, method_name, args, kwargs) + return self._holderify(self.string_method_call(obj, method_name, args, kwargs)) if isinstance(obj, bool): - return self.bool_method_call(obj, method_name, args, kwargs) + return self._holderify(self.bool_method_call(obj, method_name, args, kwargs)) if isinstance(obj, int): - return self.int_method_call(obj, method_name, args, kwargs) + 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): @@ -656,16 +655,17 @@ class InterpreterBase: obj.current_node = node return self._holderify(obj.method_call(method_name, args, kwargs)) - def _holderify(self, res: T.Union[TYPE_var, InterpreterObject, None]) -> T.Union[TYPE_elementary, InterpreterObject]: + 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 - if isinstance(res, (int, bool, str)): + if isinstance(res, (bool, str)): return res elif isinstance(res, list): - return [self._holderify(x) for x in res] + return [self._holderify(x, permissive=permissive) for x in res] elif isinstance(res, dict): - return {k: self._holderify(v) for k, v in res.items()} - elif isinstance(res, mesonlib.HoldableObject): + return {k: self._holderify(v, permissive=permissive) for k, v in res.items()} + elif isinstance(res, HoldableTypes): # Always check for an exact match first. cls = self.holder_map.get(type(res), None) if cls is not None: @@ -678,6 +678,8 @@ 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 @@ -711,26 +713,6 @@ class InterpreterBase: else: raise InterpreterException('Unknown method "%s" for a boolean.' % method_name) - @noKwargs - def int_method_call(self, obj: int, method_name: str, posargs: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.Union[str, bool]: - if method_name == 'is_even': - if not posargs: - return obj % 2 == 0 - else: - raise InterpreterException('int.is_even() must have no arguments.') - elif method_name == 'is_odd': - if not posargs: - return obj % 2 != 0 - else: - raise InterpreterException('int.is_odd() must have no arguments.') - elif method_name == 'to_string': - if not posargs: - return str(obj) - else: - raise InterpreterException('int.to_string() must have no arguments.') - else: - raise InterpreterException('Unknown method "%s" for an integer.' % method_name) - @staticmethod def _get_one_string_posarg(posargs: T.List[TYPE_var], method_name: str) -> str: if len(posargs) > 1: @@ -856,7 +838,7 @@ class InterpreterBase: return False return check_contains([_unholder(x) for x in obj]) elif method_name == 'length': - return len(obj) + return self._holderify(len(obj)) elif method_name == 'get': index = posargs[0] fallback = None diff --git a/run_mypy.py b/run_mypy.py index fc69063..f6add90 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -15,6 +15,7 @@ modules = [ 'mesonbuild/cmake', 'mesonbuild/compilers', 'mesonbuild/dependencies', + 'mesonbuild/interpreter/primitives', 'mesonbuild/interpreterbase', 'mesonbuild/linkers', 'mesonbuild/scripts', diff --git a/test cases/failing/50 executable comparison/test.json b/test cases/failing/50 executable comparison/test.json index 585b382..7f6bd3b 100644 --- a/test cases/failing/50 executable comparison/test.json +++ b/test cases/failing/50 executable comparison/test.json @@ -1,7 +1,8 @@ { "stdout": [ { - "line": "test cases/failing/50 executable comparison/meson.build:6:0: ERROR: exe1 can only be compared for equality." + "match": "re", + "line": "test cases/failing/50 executable comparison/meson.build:6:0: ERROR: Object of type Executable does not support the `<` operator." } ] } -- cgit v1.1