|
|
|
# Copyright 2016-2017 The Meson development team
|
|
|
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
# This class contains the basic functionality needed to run any interpreter
|
|
|
|
# or an interpreter-based tool.
|
|
|
|
|
|
|
|
from .. import mparser, mesonlib
|
|
|
|
from .. import environment
|
|
|
|
|
|
|
|
from .baseobjects import (
|
|
|
|
InterpreterObject,
|
|
|
|
MesonInterpreterObject,
|
|
|
|
MutableInterpreterObject,
|
|
|
|
InterpreterObjectTypeVar,
|
|
|
|
ObjectHolder,
|
|
|
|
RangeHolder,
|
|
|
|
|
|
|
|
TYPE_elementary,
|
|
|
|
TYPE_var,
|
|
|
|
TYPE_kwargs,
|
|
|
|
|
|
|
|
HoldableTypes,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .exceptions import (
|
|
|
|
InterpreterException,
|
|
|
|
InvalidCode,
|
|
|
|
InvalidArguments,
|
|
|
|
SubdirDoneRequest,
|
|
|
|
ContinueRequest,
|
|
|
|
BreakRequest
|
|
|
|
)
|
|
|
|
|
|
|
|
from .decorators import FeatureNew, noKwargs
|
|
|
|
from .disabler import Disabler, is_disabled
|
|
|
|
from .helpers import default_resolve_key, flatten, resolve_second_level_holders
|
|
|
|
from .operator import MesonOperator
|
|
|
|
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
|
|
|
|
# see https://github.com/PyCQA/pyflakes/pull/632
|
|
|
|
from ..interpreter import Interpreter # noqa
|
|
|
|
|
|
|
|
HolderMapType = T.Dict[
|
|
|
|
T.Union[
|
|
|
|
T.Type[mesonlib.HoldableObject],
|
|
|
|
T.Type[int],
|
|
|
|
T.Type[bool],
|
|
|
|
T.Type[str],
|
|
|
|
],
|
|
|
|
# For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar]
|
|
|
|
T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]]
|
|
|
|
]
|
|
|
|
|
|
|
|
FunctionType = T.Dict[
|
|
|
|
str,
|
|
|
|
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 = {}
|
|
|
|
self.builtin: T.Dict[str, InterpreterObject] = {}
|
|
|
|
# Holder maps store a mapping from an HoldableObject to a class ObjectHolder
|
|
|
|
self.holder_map: HolderMapType = {}
|
|
|
|
self.bound_holder_map: HolderMapType = {}
|
|
|
|
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.argument_depth = 0
|
|
|
|
self.current_lineno = -1
|
|
|
|
# Current node set during a function call. This can be used as location
|
|
|
|
# when printing a warning message during a method call.
|
|
|
|
self.current_node = None # type: mparser.BaseNode
|
|
|
|
# This is set to `version_string` when this statement is evaluated:
|
|
|
|
# meson.version().compare_version(version_string)
|
|
|
|
# If it was part of a if-clause, it is used to temporally override the
|
|
|
|
# current meson version target within that if-block.
|
|
|
|
self.tmp_meson_version = None # type: T.Optional[str]
|
|
|
|
|
|
|
|
def load_root_meson_file(self) -> None:
|
|
|
|
mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename)
|
|
|
|
if not os.path.isfile(mesonfile):
|
|
|
|
raise InvalidArguments('Missing Meson file in %s' % mesonfile)
|
|
|
|
with open(mesonfile, encoding='utf-8') as mf:
|
|
|
|
code = mf.read()
|
Don't use len() to test emptiness vs not emptiness
Meson has a common pattern of using 'if len(foo) == 0:' or
'if len(foo) != 0:', however, this is a common anti-pattern in python.
Instead tests for emptiness/non-emptiness should be done with a simple
'if foo:' or 'if not foo:'
Consider the following:
>>> import timeit
>>> timeit.timeit('if len([]) == 0: pass')
0.10730923599840025
>>> timeit.timeit('if not []: pass')
0.030033907998586074
>>> timeit.timeit('if len(['a', 'b', 'c', 'd']) == 0: pass')
0.1154778649979562
>>> timeit.timeit("if not ['a', 'b', 'c', 'd']: pass")
0.08259823200205574
>>> timeit.timeit('if len("") == 0: pass')
0.089759664999292
>>> timeit.timeit('if not "": pass')
0.02340641999762738
>>> timeit.timeit('if len("foo") == 0: pass')
0.08848102600313723
>>> timeit.timeit('if not "foo": pass')
0.04032287199879647
And for the one additional case of 'if len(foo.strip()) == 0', which can
be replaced with 'if not foo.isspace()'
>>> timeit.timeit('if len(" ".strip()) == 0: pass')
0.15294511600222904
>>> timeit.timeit('if " ".isspace(): pass')
0.09413968399894657
>>> timeit.timeit('if len(" abc".strip()) == 0: pass')
0.2023209120015963
>>> timeit.timeit('if " abc".isspace(): pass')
0.09571301700270851
In other words, it's always a win to not use len(), when you don't
actually want to check the length.
8 years ago
|
|
|
if code.isspace():
|
|
|
|
raise InvalidCode('Builder file is empty.')
|
|
|
|
assert isinstance(code, str)
|
|
|
|
try:
|
|
|
|
self.ast = mparser.Parser(code, mesonfile).parse()
|
|
|
|
except mesonlib.MesonException as me:
|
|
|
|
me.file = mesonfile
|
|
|
|
raise me
|
|
|
|
|
|
|
|
def parse_project(self) -> None:
|
|
|
|
"""
|
|
|
|
Parses project() and initializes languages, compilers etc. Do this
|
|
|
|
early because we need this before we parse the rest of the AST.
|
|
|
|
"""
|
|
|
|
self.evaluate_codeblock(self.ast, end=1)
|
|
|
|
|
|
|
|
def sanity_check_ast(self) -> None:
|
|
|
|
if not isinstance(self.ast, mparser.CodeBlockNode):
|
|
|
|
raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.')
|
Don't use len() to test emptiness vs not emptiness
Meson has a common pattern of using 'if len(foo) == 0:' or
'if len(foo) != 0:', however, this is a common anti-pattern in python.
Instead tests for emptiness/non-emptiness should be done with a simple
'if foo:' or 'if not foo:'
Consider the following:
>>> import timeit
>>> timeit.timeit('if len([]) == 0: pass')
0.10730923599840025
>>> timeit.timeit('if not []: pass')
0.030033907998586074
>>> timeit.timeit('if len(['a', 'b', 'c', 'd']) == 0: pass')
0.1154778649979562
>>> timeit.timeit("if not ['a', 'b', 'c', 'd']: pass")
0.08259823200205574
>>> timeit.timeit('if len("") == 0: pass')
0.089759664999292
>>> timeit.timeit('if not "": pass')
0.02340641999762738
>>> timeit.timeit('if len("foo") == 0: pass')
0.08848102600313723
>>> timeit.timeit('if not "foo": pass')
0.04032287199879647
And for the one additional case of 'if len(foo.strip()) == 0', which can
be replaced with 'if not foo.isspace()'
>>> timeit.timeit('if len(" ".strip()) == 0: pass')
0.15294511600222904
>>> timeit.timeit('if " ".isspace(): pass')
0.09413968399894657
>>> timeit.timeit('if len(" abc".strip()) == 0: pass')
0.2023209120015963
>>> timeit.timeit('if " abc".isspace(): pass')
0.09571301700270851
In other words, it's always a win to not use len(), when you don't
actually want to check the length.
8 years ago
|
|
|
if not self.ast.lines:
|
|
|
|
raise InvalidCode('No statements in code.')
|
|
|
|
first = self.ast.lines[0]
|
|
|
|
if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project':
|
|
|
|
p = pathlib.Path(self.source_root).resolve()
|
|
|
|
found = p
|
|
|
|
for parent in p.parents:
|
|
|
|
if (parent / 'meson.build').is_file():
|
use even more informative error message for invoking meson in a subdir
Follow-up on commit 5a7b8d86d0c455680096d47cb87f0de08c0954ac
Sometimes, we find a parent meson.build which is also malformed, and we
shouldn't suggest that maybe the user meant to use that, if it isn't a
valid project() either. Do a rough and dirty check to see if the very
first line is a project() declaration, and if not, don't try to be
clever and suggest using it.
The "invalid source tree" error suffices here, since we're not
absolutely sure meson can be successfully run in that parent directory
and actually advising people about the wrong location is a lot more
confusing than just saying "please figure this out yourself, here is
what to look for".
Granted, we miss cases where project() comes after blank lines and/or
comments, but doing a full AST parse here is excessively overkill and
probably too painful to do, and we don't need to be *that* clever. So
let's be content with merely going above and beyond the call of duty.
3 years ago
|
|
|
with open(parent / 'meson.build', encoding='utf-8') as f:
|
|
|
|
if f.readline().startswith('project('):
|
|
|
|
found = parent
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
error = 'first statement must be a call to project()'
|
|
|
|
if found != p:
|
|
|
|
raise InvalidCode(f'Not the project root: {error}\n\nDid you mean to run meson from the directory: "{found}"?')
|
|
|
|
else:
|
|
|
|
raise InvalidCode(f'Invalid source tree: {error}')
|
|
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
# Evaluate everything after the first line, which is project() because
|
|
|
|
# we already parsed that in self.parse_project()
|
|
|
|
try:
|
|
|
|
self.evaluate_codeblock(self.ast, start=1)
|
|
|
|
except SubdirDoneRequest:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def evaluate_codeblock(self, node: mparser.CodeBlockNode, start: int = 0, end: T.Optional[int] = None) -> None:
|
|
|
|
if node is None:
|
|
|
|
return
|
|
|
|
if not isinstance(node, mparser.CodeBlockNode):
|
|
|
|
e = InvalidCode('Tried to execute a non-codeblock. Possibly a bug in the parser.')
|
|
|
|
e.lineno = node.lineno
|
|
|
|
e.colno = node.colno
|
|
|
|
raise e
|
|
|
|
statements = node.lines[start:end]
|
|
|
|
i = 0
|
|
|
|
while i < len(statements):
|
|
|
|
cur = statements[i]
|
|
|
|
try:
|
|
|
|
self.current_lineno = cur.lineno
|
|
|
|
self.evaluate_statement(cur)
|
|
|
|
except Exception as e:
|
|
|
|
if getattr(e, 'lineno', None) is None:
|
|
|
|
# We are doing the equivalent to setattr here and mypy does not like it
|
|
|
|
e.lineno = cur.lineno # type: ignore
|
|
|
|
e.colno = cur.colno # type: ignore
|
|
|
|
e.file = os.path.join(self.source_root, self.subdir, environment.build_filename) # type: ignore
|
|
|
|
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]]:
|
|
|
|
self.current_node = cur
|
|
|
|
if isinstance(cur, mparser.FunctionNode):
|
|
|
|
return self.function_call(cur)
|
|
|
|
elif isinstance(cur, mparser.AssignmentNode):
|
|
|
|
self.assignment(cur)
|
|
|
|
elif isinstance(cur, mparser.MethodNode):
|
|
|
|
return self.method_call(cur)
|
|
|
|
elif isinstance(cur, mparser.StringNode):
|
|
|
|
return self._holderify(cur.value)
|
|
|
|
elif isinstance(cur, mparser.BooleanNode):
|
|
|
|
return self._holderify(cur.value)
|
|
|
|
elif isinstance(cur, mparser.IfClauseNode):
|
|
|
|
return self.evaluate_if(cur)
|
|
|
|
elif isinstance(cur, mparser.IdNode):
|
|
|
|
return self.get_variable(cur.value)
|
|
|
|
elif isinstance(cur, mparser.ComparisonNode):
|
|
|
|
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 self._holderify(cur.value)
|
|
|
|
elif isinstance(cur, mparser.AndNode):
|
|
|
|
return self.evaluate_andstatement(cur)
|
|
|
|
elif isinstance(cur, mparser.OrNode):
|
|
|
|
return self.evaluate_orstatement(cur)
|
|
|
|
elif isinstance(cur, mparser.NotNode):
|
|
|
|
return self.evaluate_notstatement(cur)
|
|
|
|
elif isinstance(cur, mparser.UMinusNode):
|
|
|
|
return self.evaluate_uminusstatement(cur)
|
|
|
|
elif isinstance(cur, mparser.ArithmeticNode):
|
|
|
|
return self.evaluate_arithmeticstatement(cur)
|
|
|
|
elif isinstance(cur, mparser.ForeachClauseNode):
|
|
|
|
self.evaluate_foreach(cur)
|
|
|
|
elif isinstance(cur, mparser.PlusAssignmentNode):
|
|
|
|
self.evaluate_plusassign(cur)
|
|
|
|
elif isinstance(cur, mparser.IndexNode):
|
|
|
|
return self.evaluate_indexing(cur)
|
|
|
|
elif isinstance(cur, mparser.TernaryNode):
|
|
|
|
return self.evaluate_ternary(cur)
|
|
|
|
elif isinstance(cur, mparser.FormatStringNode):
|
|
|
|
return self.evaluate_fstring(cur)
|
|
|
|
elif isinstance(cur, mparser.ContinueNode):
|
|
|
|
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]]:
|
|
|
|
(arguments, kwargs) = self.reduce_arguments(cur.args)
|
|
|
|
if len(kwargs) > 0:
|
|
|
|
raise InvalidCode('Keyword arguments are invalid in array construction.')
|
|
|
|
return arguments
|
|
|
|
|
|
|
|
@FeatureNew('dict', '0.47.0')
|
|
|
|
def evaluate_dictstatement(self, cur: mparser.DictNode) -> T.Union[TYPE_var, 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)
|
|
|
|
str_key = _unholder(self.evaluate_statement(key))
|
|
|
|
if not isinstance(str_key, str):
|
|
|
|
raise InvalidArguments('Key must be a string')
|
|
|
|
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
|
|
|
|
|
|
|
|
@_holderify_result((bool, Disabler))
|
|
|
|
def evaluate_notstatement(self, cur: mparser.NotNode) -> T.Union[TYPE_var, 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)
|
|
|
|
|
|
|
|
def evaluate_if(self, node: mparser.IfClauseNode) -> T.Optional[Disabler]:
|
|
|
|
assert isinstance(node, mparser.IfClauseNode)
|
|
|
|
for i in node.ifs:
|
|
|
|
# Reset self.tmp_meson_version to know if it gets set during this
|
|
|
|
# statement evaluation.
|
|
|
|
self.tmp_meson_version = None
|
|
|
|
result = self.evaluate_statement(i.condition)
|
|
|
|
if isinstance(result, Disabler):
|
|
|
|
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):
|
|
|
|
raise InvalidCode(f'If clause {result!r} does not evaluate to true or false.')
|
|
|
|
if result:
|
|
|
|
prev_meson_version = mesonlib.project_meson_versions[self.subproject]
|
|
|
|
if self.tmp_meson_version:
|
|
|
|
mesonlib.project_meson_versions[self.subproject] = self.tmp_meson_version
|
|
|
|
try:
|
|
|
|
self.evaluate_codeblock(i.block)
|
|
|
|
finally:
|
|
|
|
mesonlib.project_meson_versions[self.subproject] = prev_meson_version
|
|
|
|
return None
|
|
|
|
if not isinstance(node.elseblock, mparser.EmptyNode):
|
|
|
|
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]:
|
|
|
|
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 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.')
|
|
|
|
|
|
|
|
@_holderify_result((bool, Disabler))
|
|
|
|
def evaluate_andstatement(self, cur: mparser.AndNode) -> T.Union[TYPE_var, 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
|
|
|
|
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)
|
|
|
|
|
|
|
|
@_holderify_result((bool, Disabler))
|
|
|
|
def evaluate_orstatement(self, cur: mparser.OrNode) -> T.Union[TYPE_var, 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
|
|
|
|
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)
|
|
|
|
|
|
|
|
@_holderify_result()
|
|
|
|
def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> T.Union[TYPE_var, 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)
|
|
|
|
|
|
|
|
def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> T.Union[TYPE_var, InterpreterObject]:
|
|
|
|
l = self.evaluate_statement(cur.left)
|
|
|
|
if isinstance(l, Disabler):
|
|
|
|
return l
|
|
|
|
r = self.evaluate_statement(cur.right)
|
|
|
|
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]:
|
|
|
|
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)
|
|
|
|
else:
|
|
|
|
return self.evaluate_statement(node.falseblock)
|
|
|
|
|
|
|
|
@FeatureNew('format strings', '0.58.0')
|
|
|
|
@_holderify_result(str)
|
|
|
|
def evaluate_fstring(self, node: mparser.FormatStringNode) -> str:
|
|
|
|
assert isinstance(node, mparser.FormatStringNode)
|
|
|
|
|
|
|
|
def replace(match: T.Match[str]) -> str:
|
|
|
|
var = str(match.group(1))
|
|
|
|
try:
|
|
|
|
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).')
|
|
|
|
|
|
|
|
return str(val)
|
|
|
|
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)
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
def evaluate_plusassign(self, node: mparser.PlusAssignmentNode) -> None:
|
|
|
|
assert isinstance(node, mparser.PlusAssignmentNode)
|
|
|
|
varname = node.var_name
|
|
|
|
addition = self.evaluate_statement(node.value)
|
|
|
|
|
|
|
|
# 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')
|
|
|
|
self.set_variable(varname, new_value)
|
|
|
|
|
|
|
|
def evaluate_indexing(self, node: mparser.IndexNode) -> T.Union[TYPE_elementary, 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
|
|
|
|
|
|
|
|
@_holderify_result()
|
|
|
|
def function_call(self, node: mparser.FunctionNode) -> T.Optional[T.Union[TYPE_var, InterpreterObject]]:
|
|
|
|
func_name = node.func_name
|
|
|
|
(h_posargs, h_kwargs) = self.reduce_arguments(node.args)
|
|
|
|
(posargs, kwargs) = self._unholder_args(h_posargs, h_kwargs)
|
|
|
|
if is_disabled(posargs, kwargs) and func_name not in {'get_variable', 'set_variable', 'unset_variable', 'is_disabler'}:
|
|
|
|
return Disabler()
|
|
|
|
if func_name in self.funcs:
|
|
|
|
func = self.funcs[func_name]
|
|
|
|
func_args = posargs
|
|
|
|
if not getattr(func, 'no-args-flattening', False):
|
|
|
|
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)
|
|
|
|
else:
|
|
|
|
self.unknown_function_called(func_name)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def method_call(self, node: mparser.MethodNode) -> T.Optional[T.Union[TYPE_var, InterpreterObject]]:
|
|
|
|
invokable = node.source_object
|
|
|
|
obj: T.Union[TYPE_var, InterpreterObject]
|
|
|
|
if isinstance(invokable, mparser.IdNode):
|
|
|
|
object_name = invokable.value
|
|
|
|
obj = self.get_variable(object_name)
|
|
|
|
else:
|
|
|
|
obj = self.evaluate_statement(invokable)
|
|
|
|
method_name = node.name
|
|
|
|
(h_args, h_kwargs) = self.reduce_arguments(node.args)
|
|
|
|
(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
|
|
|
|
if method_name == 'extract_objects':
|
|
|
|
if isinstance(obj, ObjectHolder):
|
|
|
|
self.validate_extraction(obj.held_object)
|
|
|
|
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))
|
|
|
|
|
|
|
|
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):
|
|
|
|
# Always check for an exact match first.
|
|
|
|
cls = self.holder_map.get(type(res), None)
|
|
|
|
if cls is not None:
|
|
|
|
# Casts to Interpreter are required here since an assertion would
|
|
|
|
# not work for the `ast` module.
|
|
|
|
return cls(res, T.cast('Interpreter', self))
|
|
|
|
# Try the boundary types next.
|
|
|
|
for typ, cls in self.bound_holder_map.items():
|
|
|
|
if isinstance(res, typ):
|
|
|
|
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]:
|
|
|
|
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]]
|
|
|
|
]:
|
|
|
|
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]] = {}
|
|
|
|
for key, val in args.kwargs.items():
|
|
|
|
reduced_key = key_resolver(key)
|
|
|
|
assert isinstance(val, mparser.BaseNode)
|
|
|
|
reduced_val = self.evaluate_statement(val)
|
|
|
|
if duplicate_key_error and reduced_key in reduced_kw:
|
|
|
|
raise InvalidArguments(duplicate_key_error.format(reduced_key))
|
|
|
|
reduced_kw[reduced_key] = reduced_val
|
|
|
|
self.argument_depth -= 1
|
|
|
|
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]]:
|
|
|
|
if 'kwargs' not in kwargs:
|
|
|
|
return kwargs
|
|
|
|
to_expand = kwargs.pop('kwargs')
|
|
|
|
if not isinstance(to_expand, dict):
|
|
|
|
raise InterpreterException('Value of "kwargs" must be dictionary.')
|
|
|
|
if 'kwargs' in to_expand:
|
|
|
|
raise InterpreterException('Kwargs argument must not contain a "kwargs" entry. Points for thinking meta, though. :P')
|
|
|
|
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
|
|
|
|
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 =.''')
|
|
|
|
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)
|
|
|
|
self.set_variable(var_name, value)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def set_variable(self, varname: str, variable: T.Union[TYPE_var, InterpreterObject], *, holderify: bool = False) -> None:
|
|
|
|
if variable is None:
|
|
|
|
raise InvalidCode('Can not assign None to variable.')
|
|
|
|
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)
|
|
|
|
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]:
|
|
|
|
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)')
|