From 86f70c873a46c63e7a6e12de562ad8c907eabbc5 Mon Sep 17 00:00:00 2001 From: Daniel Mensinger Date: Sun, 29 Aug 2021 13:06:56 +0200 Subject: [PATCH] interpreter: Introduce operators support for InterpreterObjects --- mesonbuild/interpreterbase/__init__.py | 4 + mesonbuild/interpreterbase/baseobjects.py | 83 ++++++++++++++++++- mesonbuild/interpreterbase/decorators.py | 44 ++++++++++ mesonbuild/interpreterbase/interpreterbase.py | 42 +++++++++- mesonbuild/interpreterbase/operator.py | 34 ++++++++ 5 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 mesonbuild/interpreterbase/operator.py diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py index f7b12a51c..c3605b20a 100644 --- a/mesonbuild/interpreterbase/__init__.py +++ b/mesonbuild/interpreterbase/__init__.py @@ -20,6 +20,8 @@ __all__ = [ 'MesonVersionString', 'MutableInterpreterObject', + 'MesonOperator', + 'Disabler', 'is_disabled', @@ -43,6 +45,8 @@ __all__ = [ 'permissive_unholder_return', 'disablerIfNotFound', 'permittedKwargs', + 'typed_operator', + 'unary_operator', 'typed_pos_args', 'ContainerTypeInfo', 'KwargInfo', diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py index ddfd4be83..b28654a86 100644 --- a/mesonbuild/interpreterbase/baseobjects.py +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -13,9 +13,11 @@ # limitations under the License. from .. import mparser -from .exceptions import InvalidCode +from .exceptions import InvalidCode, InvalidArguments from .helpers import flatten, resolve_second_level_holders -from ..mesonlib import HoldableObject +from .operator import MesonOperator +from ..mesonlib import HoldableObject, MesonBugException +import textwrap import typing as T @@ -36,17 +38,41 @@ TYPE_kwargs = T.Dict[str, TYPE_var] TYPE_nkwargs = T.Dict[str, TYPE_nvar] TYPE_key_resolver = T.Callable[[mparser.BaseNode], str] +if T.TYPE_CHECKING: + from typing_extensions import Protocol + __T = T.TypeVar('__T', bound=TYPE_var, contravariant=True) + class OperatorCall(Protocol[__T]): + def __call__(self, other: __T) -> TYPE_var: ... + class InterpreterObject: def __init__(self, *, subproject: T.Optional[str] = None) -> None: self.methods: T.Dict[ str, T.Callable[[T.List[TYPE_var], TYPE_kwargs], TYPE_var] ] = {} + self.operators: T.Dict[MesonOperator, 'OperatorCall'] = {} + self.trivial_operators: T.Dict[ + MesonOperator, + T.Tuple[ + T.Type[T.Union[TYPE_var, T.Tuple[TYPE_var, ...]]], + 'OperatorCall' + ] + ] = {} # Current node set during a method call. This can be used as location # when printing a warning message during a method call. self.current_node: mparser.BaseNode = None self.subproject: str = subproject or '' + # Some default operators supported by all objects + self.operators.update({ + MesonOperator.EQUALS: self.op_equals, + MesonOperator.NOT_EQUALS: self.op_not_equals, + }) + + # The type of the object that can be printed to the user + def display_name(self) -> str: + return type(self).__name__ + def method_call( self, method_name: str, @@ -62,13 +88,47 @@ class InterpreterObject: return method(args, kwargs) raise InvalidCode(f'Unknown method "{method_name}" in object {self} of type {type(self).__name__}.') + def operator_call(self, operator: MesonOperator, other: TYPE_var) -> TYPE_var: + if operator in self.trivial_operators: + op = self.trivial_operators[operator] + if op[0] is None and other is not None: + raise MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}') + if op[0] is not None and not isinstance(other, op[0]): + raise InvalidArguments(f'The `{operator.value}` of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})') + return op[1](other) + if operator in self.operators: + return self.operators[operator](other) + raise InvalidCode(f'Object {self} of type {self.display_name()} does not support the `{operator.value}` operator.') + + + # Default comparison operator support + def _throw_comp_exception(self, other: TYPE_var, opt_type: str) -> T.NoReturn: + raise InvalidArguments(textwrap.dedent( + f''' + Trying to compare values of different types ({self.display_name()}, {type(other).__name__}) using {opt_type}. + This was deprecated and undefined behavior previously and is as of 0.60.0 a hard error. + ''' + )) + + def op_equals(self, other: TYPE_var) -> bool: + if type(self) != type(other): + self._throw_comp_exception(other, '==') + return self == other + + def op_not_equals(self, other: TYPE_var) -> bool: + if type(self) != type(other): + self._throw_comp_exception(other, '!=') + return self != other + class MesonInterpreterObject(InterpreterObject): ''' All non-elementary objects and non-object-holders should be derived from this ''' class MutableInterpreterObject: ''' Dummy class to mark the object type as mutable ''' -InterpreterObjectTypeVar = T.TypeVar('InterpreterObjectTypeVar', bound=HoldableObject) +HoldableTypes = (HoldableObject, int) +TYPE_HoldableTypes = T.Union[HoldableObject, int] +InterpreterObjectTypeVar = T.TypeVar('InterpreterObjectTypeVar', bound=TYPE_HoldableTypes) class ObjectHolder(InterpreterObject, T.Generic[InterpreterObjectTypeVar]): def __init__(self, obj: InterpreterObjectTypeVar, interpreter: 'Interpreter') -> None: @@ -77,11 +137,26 @@ class ObjectHolder(InterpreterObject, T.Generic[InterpreterObjectTypeVar]): # HoldableObject, not the specialized type, so only do this assert in # non-type checking situations if not T.TYPE_CHECKING: - assert isinstance(obj, HoldableObject), f'This is a bug: Trying to hold object of type `{type(obj).__name__}` that is not an `HoldableObject`' + assert isinstance(obj, HoldableTypes), f'This is a bug: Trying to hold object of type `{type(obj).__name__}` that is not in `{HoldableTypes}`' self.held_object = obj self.interpreter = interpreter self.env = self.interpreter.environment + # Hide the object holder abstrction from the user + def display_name(self) -> str: + return type(self.held_object).__name__ + + # Override default comparison operators for the held object + def op_equals(self, other: TYPE_var) -> bool: + if type(self.held_object) != type(other): + self._throw_comp_exception(other, '==') + return self.held_object == other + + def op_not_equals(self, other: TYPE_var) -> bool: + if type(self.held_object) != type(other): + self._throw_comp_exception(other, '!=') + return self.held_object != other + def __repr__(self) -> str: return f'<[{type(self).__name__}] holds [{type(self.held_object).__name__}]: {self.held_object!r}>' diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index 33892be60..717853f85 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -17,6 +17,7 @@ from .baseobjects import TV_func, TYPE_var, TYPE_kwargs from .disabler import Disabler from .exceptions import InterpreterException, InvalidArguments from .helpers import check_stringlist +from .operator import MesonOperator from ._unholder import _unholder from functools import wraps @@ -110,6 +111,49 @@ class permittedKwargs: return f(*wrapped_args, **wrapped_kwargs) return T.cast(TV_func, wrapped) +if T.TYPE_CHECKING: + from .baseobjects import InterpreterObject + from typing_extensions import Protocol + + _TV_IntegerObject = T.TypeVar('_TV_IntegerObject', bound=InterpreterObject, contravariant=True) + _TV_ARG1 = T.TypeVar('_TV_ARG1', bound=TYPE_var, contravariant=True) + + class FN_Operator(Protocol[_TV_IntegerObject, _TV_ARG1]): + def __call__(s, self: _TV_IntegerObject, other: _TV_ARG1) -> TYPE_var: ... + _TV_FN_Operator = T.TypeVar('_TV_FN_Operator', bound=FN_Operator) + +def typed_operator(operator: MesonOperator, + types: T.Union[T.Type, T.Tuple[T.Type, ...]]) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: + """Decorator that does type checking for operator calls. + + The principle here is similar to typed_pos_args, however much simpler + since only one other object ever is passed + """ + def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': + @wraps(f) + def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: + if not isinstance(other, types): + raise InvalidArguments(f'The `{operator.value}` of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})') + return f(self, other) + return T.cast('_TV_FN_Operator', wrapper) + return inner + +def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: + """Decorator that does type checking for 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 + this check always indicates a bug in the Meson interpreter. + """ + def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': + @wraps(f) + def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: + if other is not None: + raise mesonlib.MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}') + return f(self, other) + return T.cast('_TV_FN_Operator', wrapper) + return inner + def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type, ...]]] = None, diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index 289e1d76e..b6e7d0d96 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -43,6 +43,7 @@ from .exceptions import ( from .decorators import FeatureNew, noKwargs from .disabler import Disabler, is_disabled from .helpers import check_stringlist, default_resolve_key, flatten, resolve_second_level_holders +from .operator import MesonOperator from ._unholder import _unholder import os, copy, re, pathlib @@ -290,13 +291,37 @@ class InterpreterBase: raise InvalidArguments('rvalue of "in" operator must be an array or a dict') return val1 in val2 - def evaluate_comparison(self, node: mparser.ComparisonNode) -> T.Union[bool, Disabler]: + def evaluate_comparison(self, node: mparser.ComparisonNode) -> T.Union[TYPE_var, InterpreterObject]: val1 = self.evaluate_statement(node.left) if isinstance(val1, Disabler): return val1 val2 = self.evaluate_statement(node.right) if isinstance(val2, Disabler): return val2 + + # New code based on InterpreterObjects + operator = { + 'in': MesonOperator.IN, + 'notin': MesonOperator.NOT_IN, + '==': MesonOperator.EQUALS, + '!=': MesonOperator.NOT_EQUALS, + '>': MesonOperator.GREATER, + '<': MesonOperator.LESS, + '>=': MesonOperator.GREATER_EQUALS, + '<=': MesonOperator.LESS_EQUALS, + }[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 self._holderify(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 self._holderify(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) @@ -404,6 +429,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} diff --git a/mesonbuild/interpreterbase/operator.py b/mesonbuild/interpreterbase/operator.py new file mode 100644 index 000000000..bf9b6b06a --- /dev/null +++ b/mesonbuild/interpreterbase/operator.py @@ -0,0 +1,34 @@ +# SPDX-license-identifier: Apache-2.0 + +from enum import Enum + +class MesonOperator(Enum): + # Arithmetic + PLUS = '+' + MINUS = '-' + TIMES = '*' + DIV = '/' + MOD = '%' + + UMINUS = 'uminus' + + # Logic + NOT = 'not' + AND = 'and' + OR = 'or' + + # Should return the boolsche interpretation of the value (`'' == false` for instance) + BOOL = 'bool()' + + # Comparision + EQUALS = '==' + NOT_EQUALS = '!=' + GREATER = '>' + LESS = '<' + GREATER_EQUALS = '>=' + LESS_EQUALS = '<=' + + # Container + IN = 'in' + NOT_IN = 'not in' + INDEX = '[]'