diff options
Diffstat (limited to 'mesonbuild')
-rw-r--r-- | mesonbuild/astinterpreter.py | 241 | ||||
-rw-r--r-- | mesonbuild/interpreter.py | 603 | ||||
-rw-r--r-- | mesonbuild/interpreterbase.py | 636 | ||||
-rw-r--r-- | mesonbuild/mparser.py | 127 | ||||
-rw-r--r-- | mesonbuild/optinterpreter.py | 2 |
5 files changed, 967 insertions, 642 deletions
diff --git a/mesonbuild/astinterpreter.py b/mesonbuild/astinterpreter.py new file mode 100644 index 0000000..3691d64 --- /dev/null +++ b/mesonbuild/astinterpreter.py @@ -0,0 +1,241 @@ +# Copyright 2016 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 interpreterbase, mlog, mparser, mesonlib +from . import environment + +from .interpreterbase import InterpreterException, InvalidArguments + +import os, sys + +class DontCareObject(interpreterbase.InterpreterObject): + pass + +class MockExecutable(interpreterbase.InterpreterObject): + pass + +class MockStaticLibrary(interpreterbase.InterpreterObject): + pass + +class MockSharedLibrary(interpreterbase.InterpreterObject): + pass + +class MockCustomTarget(interpreterbase.InterpreterObject): + pass + +class MockRunTarget(interpreterbase.InterpreterObject): + pass + +ADD_SOURCE = 0 +REMOVE_SOURCE = 1 + +class AstInterpreter(interpreterbase.InterpreterBase): + def __init__(self, source_root, subdir): + super().__init__(source_root, subdir) + self.asts = {} + self.funcs.update({'project' : self.func_do_nothing, + 'test' : self.func_do_nothing, + 'benchmark' : self.func_do_nothing, + 'install_headers' : self.func_do_nothing, + 'install_man' : self.func_do_nothing, + 'install_data' : self.func_do_nothing, + 'install_subdir' : self.func_do_nothing, + 'configuration_data' : self.func_do_nothing, + 'configure_file' : self.func_do_nothing, + 'find_program' : self.func_do_nothing, + 'include_directories' : self.func_do_nothing, + 'add_global_arguments' : self.func_do_nothing, + 'add_global_link_arguments' : self.func_do_nothing, + 'add_project_arguments' : self.func_do_nothing, + 'add_project_link_arguments' : self.func_do_nothing, + 'message' : self.func_do_nothing, + 'generator' : self.func_do_nothing, + 'error' : self.func_do_nothing, + 'run_command' : self.func_do_nothing, + 'assert' : self.func_do_nothing, + 'subproject' : self.func_do_nothing, + 'dependency' : self.func_do_nothing, + 'get_option' : self.func_do_nothing, + 'join_paths' : self.func_do_nothing, + 'environment' : self.func_do_nothing, + 'import' : self.func_do_nothing, + 'vcs_tag' : self.func_do_nothing, + 'add_languages' : self.func_do_nothing, + 'declare_dependency' : self.func_do_nothing, + 'files' : self.func_files, + 'executable': self.func_executable, + 'static_library' : self.func_static_lib, + 'shared_library' : self.func_shared_lib, + 'library' : self.func_library, + 'build_target' : self.func_build_target, + 'custom_target' : self.func_custom_target, + 'run_target' : self.func_run_target, + 'subdir' : self.func_subdir, + 'set_variable' : self.func_set_variable, + 'get_variable' : self.func_get_variable, + 'is_variable' : self.func_is_variable, + }) + + def func_do_nothing(self, node, args, kwargs): + return True + + def method_call(self, node): + return True + + def func_executable(self, node, args, kwargs): + if args[0] == self.targetname: + if self.operation == ADD_SOURCE: + self.add_source_to_target(node, args, kwargs) + elif self.operation == REMOVE_SOURCE: + self.remove_source_from_target(node, args, kwargs) + else: + raise NotImplementedError('Bleep bloop') + return MockExecutable() + + def func_static_lib(self, node, args, kwargs): + return MockStaticLibrary() + + def func_shared_lib(self, node, args, kwargs): + return MockSharedLibrary() + + def func_library(self, node, args, kwargs): + return self.func_shared_lib(node, args, kwargs) + + def func_custom_target(self, node, args, kwargs): + return MockCustomTarget() + + def func_run_target(self, node, args, kwargs): + return MockRunTarget() + + def func_subdir(self, node, args, kwargs): + prev_subdir = self.subdir + subdir = os.path.join(prev_subdir, args[0]) + self.subdir = subdir + buildfilename = os.path.join(self.subdir, environment.build_filename) + absname = os.path.join(self.source_root, buildfilename) + if not os.path.isfile(absname): + self.subdir = prev_subdir + raise InterpreterException('Nonexistant build def file %s.' % buildfilename) + with open(absname, encoding='utf8') as f: + code = f.read() + assert(isinstance(code, str)) + try: + codeblock = mparser.Parser(code, self.subdir).parse() + self.asts[subdir] = codeblock + except mesonlib.MesonException as me: + me.file = buildfilename + raise me + self.evaluate_codeblock(codeblock) + self.subdir = prev_subdir + + def func_files(self, node, args, kwargs): + if not isinstance(args, list): + return [args] + return args + + def evaluate_arithmeticstatement(self, cur): + return 0 + + def evaluate_plusassign(self, node): + return 0 + + def evaluate_indexing(self, node): + return 0 + + def reduce_arguments(self, args): + assert(isinstance(args, mparser.ArgumentNode)) + if args.incorrect_order(): + raise InvalidArguments('All keyword arguments must be after positional arguments.') + return (args.arguments, args.kwargs) + + def transform(self): + self.load_root_meson_file() + self.asts[''] = self.ast + self.sanity_check_ast() + self.parse_project() + self.run() + + def add_source(self, targetname, filename): + self.operation = ADD_SOURCE + self.targetname = targetname + self.filename = filename + self.transform() + + def remove_source(self, targetname, filename): + self.operation = REMOVE_SOURCE + self.targetname = targetname + self.filename = filename + self.transform() + + def unknown_function_called(self, func_name): + mlog.warning('Unknown function called: ' + func_name) + + def add_source_to_target(self, node, args, kwargs): + namespan = node.args.arguments[0].bytespan + buildfilename = os.path.join(self.source_root, self.subdir, environment.build_filename) + raw_data = open(buildfilename, 'r').read() + updated = raw_data[0:namespan[1]] + (", '%s'" % self.filename) + raw_data[namespan[1]:] + open(buildfilename, 'w').write(updated) + sys.exit(0) + + def remove_argument_item(self, args, i): + assert(isinstance(args, mparser.ArgumentNode)) + namespan = args.arguments[i].bytespan + # Usually remove the comma after this item but if it is + # the last argument, we need to remove the one before. + if i >= len(args.commas): + i -= 1 + if i < 0: + commaspan = (0, 0) # Removed every entry in the list. + else: + commaspan = args.commas[i].bytespan + if commaspan[0] < namespan[0]: + commaspan, namespan = namespan, commaspan + buildfilename = os.path.join(self.source_root, args.subdir, environment.build_filename) + raw_data = open(buildfilename, 'r').read() + intermediary = raw_data[0:commaspan[0]] + raw_data[commaspan[1]:] + updated = intermediary[0:namespan[0]] + intermediary[namespan[1]:] + open(buildfilename, 'w').write(updated) + sys.exit(0) + + def hacky_find_and_remove(self, node_to_remove): + for a in self.asts[node_to_remove.subdir].lines: + if a.lineno == node_to_remove.lineno: + if isinstance(a, mparser.AssignmentNode): + v = a.value + if not isinstance(v, mparser.ArrayNode): + raise NotImplementedError('Not supported yet, bro.') + args = v.args + for i in range(len(args.arguments)): + if isinstance(args.arguments[i], mparser.StringNode) and self.filename == args.arguments[i].value: + self.remove_argument_item(args, i) + raise NotImplementedError('Sukkess') + + def remove_source_from_target(self, node, args, kwargs): + for i in range(1, len(node.args)): + # Is file name directly in function call as a string. + if isinstance(node.args.arguments[i], mparser.StringNode) and self.filename == node.args.arguments[i].value: + self.remove_argument_item(node.args, i) + # Is file name in a variable that gets expanded here. + if isinstance(node.args.arguments[i], mparser.IdNode): + avar = self.get_variable(node.args.arguments[i].value) + if not isinstance(avar, list): + raise NotImplementedError('Non-arrays not supported yet, sorry.') + for entry in avar: + if isinstance(entry, mparser.StringNode) and entry.value == self.filename: + self.hacky_find_and_remove(entry) + sys.exit('Could not find source %s in target %s.' % (self.filename, args[0])) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index e9273e4..2167b81 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -22,58 +22,17 @@ from . import optinterpreter from . import compilers from .wrap import wrap from . import mesonlib +from mesonbuild.interpreterbase import InterpreterBase +from mesonbuild.interpreterbase import check_stringlist, noPosargs, noKwargs, stringArgs +from mesonbuild.interpreterbase import InterpreterException, InvalidArguments, InvalidCode +from mesonbuild.interpreterbase import InterpreterObject, MutableInterpreterObject import os, sys, subprocess, shutil, uuid, re -from functools import wraps import importlib -import copy run_depr_printed = False -class InterpreterException(mesonlib.MesonException): - pass - -class InvalidCode(InterpreterException): - pass - -class InvalidArguments(InterpreterException): - pass - -# Decorators for method calls. - -def check_stringlist(a, msg='Arguments must be strings.'): - if not isinstance(a, list): - mlog.debug('Not a list:', str(a)) - raise InvalidArguments('Argument not a list.') - if not all(isinstance(s, str) for s in a): - mlog.debug('Element not a string:', str(a)) - raise InvalidArguments(msg) - -def noPosargs(f): - @wraps(f) - def wrapped(self, node, args, kwargs): - if len(args) != 0: - raise InvalidArguments('Function does not take positional arguments.') - return f(self, node, args, kwargs) - return wrapped - -def noKwargs(f): - @wraps(f) - def wrapped(self, node, args, kwargs): - if len(kwargs) != 0: - raise InvalidArguments('Function does not take keyword arguments.') - return f(self, node, args, kwargs) - return wrapped - -def stringArgs(f): - @wraps(f) - def wrapped(self, node, args, kwargs): - assert(isinstance(args, list)) - check_stringlist(args) - return f(self, node, args, kwargs) - return wrapped - def stringifyUserArguments(args): if isinstance(args, list): return '[%s]' % ', '.join([stringifyUserArguments(x) for x in args]) @@ -83,18 +42,6 @@ def stringifyUserArguments(args): return "'%s'" % args raise InvalidArguments('Function accepts only strings, integers, lists and lists thereof.') -class InterpreterObject(): - def __init__(self): - self.methods = {} - - def method_call(self, method_name, args, kwargs): - if method_name in self.methods: - return self.methods[method_name](args, kwargs) - raise InvalidCode('Unknown method "%s" in object.' % method_name) - -class MutableInterpreterObject(InterpreterObject): - def __init__(self): - super().__init__() class TryRunResultHolder(InterpreterObject): def __init__(self, res): @@ -1139,16 +1086,15 @@ class MesonMain(InterpreterObject): return args[1] raise InterpreterException('Unknown cross property: %s.' % propname) -class Interpreter(): +class Interpreter(InterpreterBase): def __init__(self, build, backend, subproject='', subdir='', subproject_dir='subprojects'): + super().__init__(build.environment.get_source_dir(), subdir) self.build = build self.environment = build.environment self.coredata = self.environment.get_coredata() self.backend = backend self.subproject = subproject - self.subdir = subdir - self.source_root = build.environment.get_source_dir() self.subproject_dir = subproject_dir option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt') if os.path.exists(option_file): @@ -1156,22 +1102,9 @@ class Interpreter(): self.build.environment.cmd_line_options.projectoptions) oi.process(option_file) self.build.environment.merge_options(oi.options) - 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='utf8') as mf: - code = mf.read() - if len(code.strip()) == 0: - raise InvalidCode('Builder file is empty.') - assert(isinstance(code, str)) - try: - self.ast = mparser.Parser(code).parse() - except mesonlib.MesonException as me: - me.file = environment.build_filename - raise me + self.load_root_meson_file() self.sanity_check_ast() - self.variables = {} - self.builtin = {'meson': MesonMain(build, self)} + self.builtin.update({'meson': MesonMain(build, self)}) self.generators = [] self.visited_subdirs = {} self.args_frozen = False @@ -1196,7 +1129,7 @@ class Interpreter(): self.build_def_files = [os.path.join(self.subdir, environment.build_filename)] def build_func_dict(self): - self.funcs = {'project' : self.func_project, + self.funcs.update({'project' : self.func_project, 'message' : self.func_message, 'error' : self.func_error, 'executable': self.func_executable, @@ -1241,14 +1174,7 @@ class Interpreter(): 'assert': self.func_assert, 'environment' : self.func_environment, 'join_paths' : self.func_join_paths, - } - - def parse_project(self): - """ - 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 module_method_callback(self, invalues): unwrap_single = False @@ -1295,16 +1221,6 @@ class Interpreter(): def get_variables(self): return self.variables - def sanity_check_ast(self): - if not isinstance(self.ast, mparser.CodeBlockNode): - raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') - if len(self.ast.lines) == 0: - raise InvalidCode('No statements in code.') - first = self.ast.lines[0] - if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': - raise InvalidCode('First statement must be a call to project') - - def check_cross_stdlibs(self): if self.build.environment.is_cross_build(): cross_info = self.build.environment.cross_info @@ -1321,71 +1237,6 @@ class Interpreter(): except KeyError as e: pass - def run(self): - # Evaluate everything after the first line, which is project() because - # we already parsed that in self.parse_project() - self.evaluate_codeblock(self.ast, start=1) - mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets)))) - - def evaluate_codeblock(self, node, start=0, end=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.evaluate_statement(cur) - except Exception as e: - if not(hasattr(e, 'lineno')): - e.lineno = cur.lineno - e.colno = cur.colno - e.file = os.path.join(self.subdir, 'meson.build') - raise e - i += 1 # In THE FUTURE jump over blocks and stuff. - - def get_variable(self, varname): - 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 func_set_variable(self, node, args, kwargs): - if len(args) != 2: - raise InvalidCode('Set_variable takes two arguments.') - varname = args[0] - value = self.to_native(args[1]) - self.set_variable(varname, value) - - @noKwargs - def func_get_variable(self, node, args, kwargs): - if len(args)<1 or len(args)>2: - raise InvalidCode('Get_variable takes one or two arguments.') - varname = args[0] - if not isinstance(varname, str): - raise InterpreterException('First argument must be a string.') - try: - return self.variables[varname] - except KeyError: - pass - if len(args) == 2: - return args[1] - raise InterpreterException('Tried to get unknown variable "%s".' % varname) - - @stringArgs - @noKwargs - def func_is_variable(self, node, args, kwargs): - if len(args) != 1: - raise InvalidCode('Is_variable takes two arguments.') - varname = args[0] - return varname in self.variables - @stringArgs @noKwargs def func_import(self, node, args, kwargs): @@ -1446,63 +1297,6 @@ class Interpreter(): if not value: raise InterpreterException('Assert failed: ' + message) - def set_variable(self, varname, variable): - if variable is None: - raise InvalidCode('Can not assign None to 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('Assigned value not of 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 evaluate_statement(self, cur): - if isinstance(cur, mparser.FunctionNode): - return self.function_call(cur) - elif isinstance(cur, mparser.AssignmentNode): - return self.assignment(cur) - elif isinstance(cur, mparser.MethodNode): - return self.method_call(cur) - elif isinstance(cur, mparser.StringNode): - return cur.value - elif isinstance(cur, mparser.BooleanNode): - return 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.NumberNode): - return 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): - return self.evaluate_foreach(cur) - elif isinstance(cur, mparser.PlusAssignmentNode): - return 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 self.is_elementary_type(cur): - return cur - else: - raise InvalidCode("Unknown statement.") - def validate_arguments(self, args, argcount, arg_types): if argcount is not None: if argcount != len(args): @@ -2193,7 +1987,7 @@ requirements use the version keyword argument instead.''') code = f.read() assert(isinstance(code, str)) try: - codeblock = mparser.Parser(code).parse() + codeblock = mparser.Parser(code, self.subdir).parse() except mesonlib.MesonException as me: me.file = buildfilename raise me @@ -2376,21 +2170,9 @@ requirements use the version keyword argument instead.''') def func_join_paths(self, node, args, kwargs): return os.path.join(*args).replace('\\', '/') - def flatten(self, args): - if isinstance(args, mparser.StringNode): - return args.value - if isinstance(args, (int, str, InterpreterObject)): - return args - result = [] - for a in args: - if isinstance(a, list): - rest = self.flatten(a) - result = result + rest - elif isinstance(a, mparser.StringNode): - result.append(a.value) - else: - result.append(a) - return result + def run(self): + super().run() + mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets)))) def source_strings_to_files(self, sources): results = [] @@ -2487,145 +2269,6 @@ requirements use the version keyword argument instead.''') if not os.path.isfile(fname): raise InterpreterException('Tried to add non-existing source file %s.' % s) - def function_call(self, node): - func_name = node.func_name - (posargs, kwargs) = self.reduce_arguments(node.args) - if func_name in self.funcs: - return self.funcs[func_name](node, self.flatten(posargs), kwargs) - else: - raise InvalidCode('Unknown function "%s".' % func_name) - - def is_assignable(self, value): - return isinstance(value, (InterpreterObject, dependencies.Dependency, - str, int, list, mesonlib.File)) - - def assignment(self, node): - assert(isinstance(node, mparser.AssignmentNode)) - 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) - value = self.to_native(value) - if not self.is_assignable(value): - raise InvalidCode('Tried to assign an invalid value 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 value - - def reduce_arguments(self, args): - assert(isinstance(args, mparser.ArgumentNode)) - if args.incorrect_order(): - raise InvalidArguments('All keyword arguments must be after positional arguments.') - reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] - reduced_kw = {} - for key in args.kwargs.keys(): - if not isinstance(key, str): - raise InvalidArguments('Keyword argument name is not a string.') - a = args.kwargs[key] - reduced_kw[key] = self.evaluate_statement(a) - if not isinstance(reduced_pos, list): - reduced_pos = [reduced_pos] - return (reduced_pos, reduced_kw) - - def bool_method_call(self, obj, method_name, args): - obj = self.to_native(obj) - (posargs, _) = self.reduce_arguments(args) - if method_name == 'to_string': - if len(posargs) == 0: - if obj == True: - return 'true' - else: - return 'false' - elif len(posargs) == 2 and isinstance(posargs[0], str) and isinstance(posargs[1], str): - if obj == True: - return posargs[0] - else: - return posargs[1] - else: - raise InterpreterException('bool.to_string() must have either no arguments or exactly two string arguments that signify what values to return for true and false.') - elif method_name == 'to_int': - if obj == True: - return 1 - else: - return 0 - else: - raise InterpreterException('Unknown method "%s" for a boolean.' % method_name) - - def int_method_call(self, obj, method_name, args): - obj = self.to_native(obj) - (posargs, _) = self.reduce_arguments(args) - if method_name == 'is_even': - if len(posargs) == 0: - return obj % 2 == 0 - else: - raise InterpreterException('int.is_even() must have no arguments.') - elif method_name == 'is_odd': - if len(posargs) == 0: - return obj % 2 != 0 - else: - raise InterpreterException('int.is_odd() must have no arguments.') - else: - raise InterpreterException('Unknown method "%s" for an integer.' % method_name) - - def string_method_call(self, obj, method_name, args): - obj = self.to_native(obj) - (posargs, _) = self.reduce_arguments(args) - if method_name == 'strip': - return obj.strip() - elif method_name == 'format': - return self.format_string(obj, args) - elif method_name == 'to_upper': - return obj.upper() - elif method_name == 'to_lower': - return obj.lower() - elif method_name == 'underscorify': - return re.sub(r'[^a-zA-Z0-9]', '_', obj) - elif method_name == 'split': - if len(posargs) > 1: - raise InterpreterException('Split() must have at most one argument.') - elif len(posargs) == 1: - s = posargs[0] - if not isinstance(s, str): - raise InterpreterException('Split() argument must be a string') - return obj.split(s) - else: - return obj.split() - elif method_name == 'startswith' or method_name == 'contains' or method_name == 'endswith': - s = posargs[0] - if not isinstance(s, str): - raise InterpreterException('Argument must be a string.') - if method_name == 'startswith': - return obj.startswith(s) - elif method_name == 'contains': - return obj.find(s) >= 0 - return obj.endswith(s) - elif method_name == 'to_int': - try: - return int(obj) - except Exception: - raise InterpreterException('String {!r} cannot be converted to int'.format(obj)) - elif method_name == 'join': - if len(posargs) != 1: - raise InterpreterException('Join() takes exactly one argument.') - strlist = posargs[0] - check_stringlist(strlist) - return obj.join(strlist) - elif method_name == 'version_compare': - if len(posargs) != 1: - raise InterpreterException('Version_compare() takes exactly one argument.') - cmpr = posargs[0] - if not isinstance(cmpr, str): - raise InterpreterException('Version_compare() argument must be a string.') - return mesonlib.version_compare(obj, cmpr) - raise InterpreterException('Unknown method "%s" for a string.' % method_name) - - def to_native(self, arg): - if isinstance(arg, (mparser.StringNode, mparser.NumberNode, - mparser.BooleanNode)): - return arg.value - return arg def format_string(self, templ, args): templ = self.to_native(templ) @@ -2638,32 +2281,6 @@ requirements use the version keyword argument instead.''') templ = templ.replace('@{}@'.format(i), str(arg)) return templ - def method_call(self, node): - invokable = node.source_object - 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 - args = node.args - if isinstance(obj, mparser.StringNode): - obj = obj.get_value() - if isinstance(obj, str): - return self.string_method_call(obj, method_name, args) - if isinstance(obj, bool): - return self.bool_method_call(obj, method_name, args) - if isinstance(obj, int): - return self.int_method_call(obj, method_name, args) - if isinstance(obj, list): - return self.array_method_call(obj, method_name, self.reduce_arguments(args)[0]) - if not isinstance(obj, InterpreterObject): - raise InvalidArguments('Variable "%s" is not callable.' % object_name) - (args, kwargs) = self.reduce_arguments(args) - if method_name == 'extract_objects': - self.validate_extraction(obj.held_object) - return obj.method_call(method_name, self.flatten(args), kwargs) - # Only permit object extraction from the same subproject def validate_extraction(self, buildtarget): if not self.subdir.startswith(self.subproject_dir): @@ -2675,20 +2292,6 @@ requirements use the version keyword argument instead.''') if self.subdir.split('/')[1] != buildtarget.subdir.split('/')[1]: raise InterpreterException('Tried to extract objects from a different subproject.') - def array_method_call(self, obj, method_name, args): - if method_name == 'contains': - return self.check_contains(obj, args) - elif method_name == 'length': - return len(obj) - elif method_name == 'get': - index = args[0] - if not isinstance(index, int): - raise InvalidArguments('Array index must be a number.') - if index < -len(obj) or index >= len(obj): - raise InvalidArguments('Array index %s is out of bounds for array of size %d.' % (index, len(obj))) - return obj[index] - raise InterpreterException('Arrays do not have a method called "%s".' % method_name) - def check_contains(self, obj, args): if len(args) != 1: raise InterpreterException('Contains method takes exactly one argument.') @@ -2705,183 +2308,5 @@ requirements use the version keyword argument instead.''') pass return False - def evaluate_if(self, node): - assert(isinstance(node, mparser.IfClauseNode)) - for i in node.ifs: - result = self.evaluate_statement(i.condition) - if not(isinstance(result, bool)): - raise InvalidCode('If clause {!r} does not evaluate to true or false.'.format(result)) - if result: - self.evaluate_codeblock(i.block) - return - if not isinstance(node.elseblock, mparser.EmptyNode): - self.evaluate_codeblock(node.elseblock) - - def evaluate_ternary(self, node): - assert(isinstance(node, mparser.TernaryNode)) - result = self.evaluate_statement(node.condition) - if not isinstance(result, bool): - raise InterpreterException('Ternary condition is not boolean.') - if result: - return self.evaluate_statement(node.trueblock) - else: - return self.evaluate_statement(node.falseblock) - - def evaluate_foreach(self, node): - assert(isinstance(node, mparser.ForeachClauseNode)) - varname = node.varname.value - items = self.evaluate_statement(node.items) - if not isinstance(items, list): - raise InvalidArguments('Items of foreach loop is not an array') - for item in items: - self.set_variable(varname, item) - self.evaluate_codeblock(node.block) - - def evaluate_plusassign(self, node): - 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) - 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 not isinstance(old_variable, list): - raise InvalidArguments('The += operator currently only works with arrays, strings or ints ') - # Add other data types here. - else: - if isinstance(addition, list): - new_value = old_variable + addition - else: - new_value = old_variable + [addition] - self.set_variable(varname, new_value) - - def evaluate_indexing(self, node): - assert(isinstance(node, mparser.IndexNode)) - iobject = self.evaluate_statement(node.iobject) - if not isinstance(iobject, list): - raise InterpreterException('Tried to index a non-array object.') - index = self.evaluate_statement(node.index) - if not isinstance(index, int): - raise InterpreterException('Index value is not an integer.') - if index < -len(iobject) or index >= len(iobject): - raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) - return iobject[index] - - def is_elementary_type(self, v): - return isinstance(v, (int, float, str, bool, list)) - - def evaluate_comparison(self, node): - v1 = self.evaluate_statement(node.left) - v2 = self.evaluate_statement(node.right) - if self.is_elementary_type(v1): - val1 = v1 - else: - val1 = v1.value - if self.is_elementary_type(v2): - val2 = v2 - else: - val2 = v2.value - if node.ctype == '==': - return val1 == val2 - elif node.ctype == '!=': - return val1 != val2 - 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.') - - def evaluate_andstatement(self, cur): - l = self.evaluate_statement(cur.left) - if isinstance(l, mparser.BooleanNode): - l = l.value - if not isinstance(l, bool): - raise InterpreterException('First argument to "and" is not a boolean.') - if not l: - return False - r = self.evaluate_statement(cur.right) - if isinstance(r, mparser.BooleanNode): - r = r.value - if not isinstance(r, bool): - raise InterpreterException('Second argument to "and" is not a boolean.') - return r - - def evaluate_orstatement(self, cur): - l = self.evaluate_statement(cur.left) - if isinstance(l, mparser.BooleanNode): - l = l.get_value() - if not isinstance(l, bool): - raise InterpreterException('First argument to "or" is not a boolean.') - if l: - return True - r = self.evaluate_statement(cur.right) - if isinstance(r, mparser.BooleanNode): - r = r.get_value() - if not isinstance(r, bool): - raise InterpreterException('Second argument to "or" is not a boolean.') - return r - - def evaluate_notstatement(self, cur): - v = self.evaluate_statement(cur.value) - if isinstance(v, mparser.BooleanNode): - v = v.value - if not isinstance(v, bool): - raise InterpreterException('Argument to "not" is not a boolean.') - return not v - - def evaluate_uminusstatement(self, cur): - v = self.evaluate_statement(cur.value) - if isinstance(v, mparser.NumberNode): - v = v.value - if not isinstance(v, int): - raise InterpreterException('Argument to negation is not an integer.') - return -v - - def evaluate_arithmeticstatement(self, cur): - l = self.to_native(self.evaluate_statement(cur.left)) - r = self.to_native(self.evaluate_statement(cur.right)) - - if cur.operation == 'add': - try: - return l + r - 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.') - return l - r - elif cur.operation == 'mul': - if not isinstance(l, int) or not isinstance(r, int): - raise InvalidCode('Multiplication works only with integers.') - return l * r - elif cur.operation == 'div': - if not isinstance(l, int) or not isinstance(r, int): - raise InvalidCode('Division works only with integers.') - return l // r - elif cur.operation == 'mod': - if not isinstance(l, int) or not isinstance(r, int): - raise InvalidCode('Modulo works only with integers.') - return l % r - else: - raise InvalidCode('You broke me.') - - def evaluate_arraystatement(self, cur): - (arguments, kwargs) = self.reduce_arguments(cur.args) - if len(kwargs) > 0: - raise InvalidCode('Keyword arguments are invalid in array construction.') - return arguments - def is_subproject(self): return self.subproject != '' diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py new file mode 100644 index 0000000..97814f4 --- /dev/null +++ b/mesonbuild/interpreterbase.py @@ -0,0 +1,636 @@ +# Copyright 2016 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, mlog +from . import environment, dependencies + +import os, copy, re +from functools import wraps + +# Decorators for method calls. + +def check_stringlist(a, msg='Arguments must be strings.'): + if not isinstance(a, list): + mlog.debug('Not a list:', str(a)) + raise InvalidArguments('Argument not a list.') + if not all(isinstance(s, str) for s in a): + mlog.debug('Element not a string:', str(a)) + raise InvalidArguments(msg) + +def noPosargs(f): + @wraps(f) + def wrapped(self, node, args, kwargs): + if len(args) != 0: + raise InvalidArguments('Function does not take positional arguments.') + return f(self, node, args, kwargs) + return wrapped + +def noKwargs(f): + @wraps(f) + def wrapped(self, node, args, kwargs): + if len(kwargs) != 0: + raise InvalidArguments('Function does not take keyword arguments.') + return f(self, node, args, kwargs) + return wrapped + +def stringArgs(f): + @wraps(f) + def wrapped(self, node, args, kwargs): + assert(isinstance(args, list)) + check_stringlist(args) + return f(self, node, args, kwargs) + return wrapped + + +class InterpreterException(mesonlib.MesonException): + pass + +class InvalidCode(InterpreterException): + pass + +class InvalidArguments(InterpreterException): + pass + +class InterpreterObject(): + def __init__(self): + self.methods = {} + + def method_call(self, method_name, args, kwargs): + if method_name in self.methods: + return self.methods[method_name](args, kwargs) + raise InvalidCode('Unknown method "%s" in object.' % method_name) + +class MutableInterpreterObject(InterpreterObject): + def __init__(self): + super().__init__() + + +class InterpreterBase: + def __init__(self, source_root, subdir): + self.source_root = source_root + self.funcs = {} + self.builtin = {} + self.subdir = subdir + self.variables = {} + + def load_root_meson_file(self): + 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='utf8') as mf: + code = mf.read() + if len(code.strip()) == 0: + raise InvalidCode('Builder file is empty.') + assert(isinstance(code, str)) + try: + self.ast = mparser.Parser(code, self.subdir).parse() + except mesonlib.MesonException as me: + me.file = environment.build_filename + raise me + + def parse_project(self): + """ + 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): + if not isinstance(self.ast, mparser.CodeBlockNode): + raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') + if len(self.ast.lines) == 0: + raise InvalidCode('No statements in code.') + first = self.ast.lines[0] + if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': + raise InvalidCode('First statement must be a call to project') + + def run(self): + # Evaluate everything after the first line, which is project() because + # we already parsed that in self.parse_project() + self.evaluate_codeblock(self.ast, start=1) + + def evaluate_codeblock(self, node, start=0, end=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.evaluate_statement(cur) + except Exception as e: + if not(hasattr(e, 'lineno')): + e.lineno = cur.lineno + e.colno = cur.colno + e.file = os.path.join(self.subdir, 'meson.build') + raise e + i += 1 # In THE FUTURE jump over blocks and stuff. + + def evaluate_statement(self, cur): + if isinstance(cur, mparser.FunctionNode): + return self.function_call(cur) + elif isinstance(cur, mparser.AssignmentNode): + return self.assignment(cur) + elif isinstance(cur, mparser.MethodNode): + return self.method_call(cur) + elif isinstance(cur, mparser.StringNode): + return cur.value + elif isinstance(cur, mparser.BooleanNode): + return 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.NumberNode): + return 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): + return self.evaluate_foreach(cur) + elif isinstance(cur, mparser.PlusAssignmentNode): + return 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 self.is_elementary_type(cur): + return cur + else: + raise InvalidCode("Unknown statement.") + + def evaluate_arraystatement(self, cur): + (arguments, kwargs) = self.reduce_arguments(cur.args) + if len(kwargs) > 0: + raise InvalidCode('Keyword arguments are invalid in array construction.') + return arguments + + def evaluate_notstatement(self, cur): + v = self.evaluate_statement(cur.value) + if isinstance(v, mparser.BooleanNode): + v = v.value + if not isinstance(v, bool): + raise InterpreterException('Argument to "not" is not a boolean.') + return not v + + def evaluate_if(self, node): + assert(isinstance(node, mparser.IfClauseNode)) + for i in node.ifs: + result = self.evaluate_statement(i.condition) + if not(isinstance(result, bool)): + raise InvalidCode('If clause {!r} does not evaluate to true or false.'.format(result)) + if result: + self.evaluate_codeblock(i.block) + return + if not isinstance(node.elseblock, mparser.EmptyNode): + self.evaluate_codeblock(node.elseblock) + + def evaluate_comparison(self, node): + v1 = self.evaluate_statement(node.left) + v2 = self.evaluate_statement(node.right) + if self.is_elementary_type(v1): + val1 = v1 + else: + val1 = v1.value + if self.is_elementary_type(v2): + val2 = v2 + else: + val2 = v2.value + if node.ctype == '==': + return val1 == val2 + elif node.ctype == '!=': + return val1 != val2 + 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.') + + def evaluate_andstatement(self, cur): + l = self.evaluate_statement(cur.left) + if isinstance(l, mparser.BooleanNode): + l = l.value + if not isinstance(l, bool): + raise InterpreterException('First argument to "and" is not a boolean.') + if not l: + return False + r = self.evaluate_statement(cur.right) + if isinstance(r, mparser.BooleanNode): + r = r.value + if not isinstance(r, bool): + raise InterpreterException('Second argument to "and" is not a boolean.') + return r + + def evaluate_orstatement(self, cur): + l = self.evaluate_statement(cur.left) + if isinstance(l, mparser.BooleanNode): + l = l.get_value() + if not isinstance(l, bool): + raise InterpreterException('First argument to "or" is not a boolean.') + if l: + return True + r = self.evaluate_statement(cur.right) + if isinstance(r, mparser.BooleanNode): + r = r.get_value() + if not isinstance(r, bool): + raise InterpreterException('Second argument to "or" is not a boolean.') + return r + + def evaluate_uminusstatement(self, cur): + v = self.evaluate_statement(cur.value) + if isinstance(v, mparser.NumberNode): + v = v.value + if not isinstance(v, int): + raise InterpreterException('Argument to negation is not an integer.') + return -v + + def evaluate_arithmeticstatement(self, cur): + l = self.to_native(self.evaluate_statement(cur.left)) + r = self.to_native(self.evaluate_statement(cur.right)) + + if cur.operation == 'add': + try: + return l + r + 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.') + return l - r + elif cur.operation == 'mul': + if not isinstance(l, int) or not isinstance(r, int): + raise InvalidCode('Multiplication works only with integers.') + return l * r + elif cur.operation == 'div': + if not isinstance(l, int) or not isinstance(r, int): + raise InvalidCode('Division works only with integers.') + return l // r + elif cur.operation == 'mod': + if not isinstance(l, int) or not isinstance(r, int): + raise InvalidCode('Modulo works only with integers.') + return l % r + else: + raise InvalidCode('You broke me.') + + def evaluate_ternary(self, node): + assert(isinstance(node, mparser.TernaryNode)) + result = self.evaluate_statement(node.condition) + if not isinstance(result, bool): + raise InterpreterException('Ternary condition is not boolean.') + if result: + return self.evaluate_statement(node.trueblock) + else: + return self.evaluate_statement(node.falseblock) + + def evaluate_foreach(self, node): + assert(isinstance(node, mparser.ForeachClauseNode)) + varname = node.varname.value + items = self.evaluate_statement(node.items) + if not isinstance(items, list): + raise InvalidArguments('Items of foreach loop is not an array') + for item in items: + self.set_variable(varname, item) + self.evaluate_codeblock(node.block) + + def evaluate_plusassign(self, node): + 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) + 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 not isinstance(old_variable, list): + raise InvalidArguments('The += operator currently only works with arrays, strings or ints ') + # Add other data types here. + else: + if isinstance(addition, list): + new_value = old_variable + addition + else: + new_value = old_variable + [addition] + self.set_variable(varname, new_value) + + def evaluate_indexing(self, node): + assert(isinstance(node, mparser.IndexNode)) + iobject = self.evaluate_statement(node.iobject) + if not isinstance(iobject, list): + raise InterpreterException('Tried to index a non-array object.') + index = self.evaluate_statement(node.index) + if not isinstance(index, int): + raise InterpreterException('Index value is not an integer.') + if index < -len(iobject) or index >= len(iobject): + raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) + return iobject[index] + + def function_call(self, node): + func_name = node.func_name + (posargs, kwargs) = self.reduce_arguments(node.args) + if func_name in self.funcs: + return self.funcs[func_name](node, self.flatten(posargs), kwargs) + else: + self.unknown_function_called(func_name) + + def method_call(self, node): + invokable = node.source_object + 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 + args = node.args + if isinstance(obj, mparser.StringNode): + obj = obj.get_value() + if isinstance(obj, str): + return self.string_method_call(obj, method_name, args) + if isinstance(obj, bool): + return self.bool_method_call(obj, method_name, args) + if isinstance(obj, int): + return self.int_method_call(obj, method_name, args) + if isinstance(obj, list): + return self.array_method_call(obj, method_name, self.reduce_arguments(args)[0]) + if not isinstance(obj, InterpreterObject): + raise InvalidArguments('Variable "%s" is not callable.' % object_name) + (args, kwargs) = self.reduce_arguments(args) + if method_name == 'extract_objects': + self.validate_extraction(obj.held_object) + return obj.method_call(method_name, self.flatten(args), kwargs) + + def bool_method_call(self, obj, method_name, args): + obj = self.to_native(obj) + (posargs, _) = self.reduce_arguments(args) + if method_name == 'to_string': + if len(posargs) == 0: + if obj == True: + return 'true' + else: + return 'false' + elif len(posargs) == 2 and isinstance(posargs[0], str) and isinstance(posargs[1], str): + if obj == True: + return posargs[0] + else: + return posargs[1] + else: + raise InterpreterException('bool.to_string() must have either no arguments or exactly two string arguments that signify what values to return for true and false.') + elif method_name == 'to_int': + if obj == True: + return 1 + else: + return 0 + else: + raise InterpreterException('Unknown method "%s" for a boolean.' % method_name) + + def int_method_call(self, obj, method_name, args): + obj = self.to_native(obj) + (posargs, _) = self.reduce_arguments(args) + if method_name == 'is_even': + if len(posargs) == 0: + return obj % 2 == 0 + else: + raise InterpreterException('int.is_even() must have no arguments.') + elif method_name == 'is_odd': + if len(posargs) == 0: + return obj % 2 != 0 + else: + raise InterpreterException('int.is_odd() must have no arguments.') + else: + raise InterpreterException('Unknown method "%s" for an integer.' % method_name) + + def string_method_call(self, obj, method_name, args): + obj = self.to_native(obj) + (posargs, _) = self.reduce_arguments(args) + if method_name == 'strip': + return obj.strip() + elif method_name == 'format': + return self.format_string(obj, args) + elif method_name == 'to_upper': + return obj.upper() + elif method_name == 'to_lower': + return obj.lower() + elif method_name == 'underscorify': + return re.sub(r'[^a-zA-Z0-9]', '_', obj) + elif method_name == 'split': + if len(posargs) > 1: + raise InterpreterException('Split() must have at most one argument.') + elif len(posargs) == 1: + s = posargs[0] + if not isinstance(s, str): + raise InterpreterException('Split() argument must be a string') + return obj.split(s) + else: + return obj.split() + elif method_name == 'startswith' or method_name == 'contains' or method_name == 'endswith': + s = posargs[0] + if not isinstance(s, str): + raise InterpreterException('Argument must be a string.') + if method_name == 'startswith': + return obj.startswith(s) + elif method_name == 'contains': + return obj.find(s) >= 0 + return obj.endswith(s) + elif method_name == 'to_int': + try: + return int(obj) + except Exception: + raise InterpreterException('String {!r} cannot be converted to int'.format(obj)) + elif method_name == 'join': + if len(posargs) != 1: + raise InterpreterException('Join() takes exactly one argument.') + strlist = posargs[0] + check_stringlist(strlist) + return obj.join(strlist) + elif method_name == 'version_compare': + if len(posargs) != 1: + raise InterpreterException('Version_compare() takes exactly one argument.') + cmpr = posargs[0] + if not isinstance(cmpr, str): + raise InterpreterException('Version_compare() argument must be a string.') + return mesonlib.version_compare(obj, cmpr) + raise InterpreterException('Unknown method "%s" for a string.' % method_name) + + def unknown_function_called(self, func_name): + raise InvalidCode('Unknown function "%s".' % func_name) + + def array_method_call(self, obj, method_name, args): + if method_name == 'contains': + return self.check_contains(obj, args) + elif method_name == 'length': + return len(obj) + elif method_name == 'get': + index = args[0] + if not isinstance(index, int): + raise InvalidArguments('Array index must be a number.') + if index < -len(obj) or index >= len(obj): + raise InvalidArguments('Array index %s is out of bounds for array of size %d.' % (index, len(obj))) + return obj[index] + raise InterpreterException('Arrays do not have a method called "%s".' % method_name) + + + def reduce_arguments(self, args): + assert(isinstance(args, mparser.ArgumentNode)) + if args.incorrect_order(): + raise InvalidArguments('All keyword arguments must be after positional arguments.') + reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] + reduced_kw = {} + for key in args.kwargs.keys(): + if not isinstance(key, str): + raise InvalidArguments('Keyword argument name is not a string.') + a = args.kwargs[key] + reduced_kw[key] = self.evaluate_statement(a) + if not isinstance(reduced_pos, list): + reduced_pos = [reduced_pos] + return (reduced_pos, reduced_kw) + + def flatten(self, args): + if isinstance(args, mparser.StringNode): + return args.value + if isinstance(args, (int, str, InterpreterObject)): + return args + result = [] + for a in args: + if isinstance(a, list): + rest = self.flatten(a) + result = result + rest + elif isinstance(a, mparser.StringNode): + result.append(a.value) + else: + result.append(a) + return result + + def assignment(self, node): + assert(isinstance(node, mparser.AssignmentNode)) + 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) + value = self.to_native(value) + if not self.is_assignable(value): + raise InvalidCode('Tried to assign an invalid value 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 value + + def set_variable(self, varname, variable): + if variable is None: + raise InvalidCode('Can not assign None to 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('Assigned value not of 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): + 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 to_native(self, arg): + if isinstance(arg, (mparser.StringNode, mparser.NumberNode, + mparser.BooleanNode)): + return arg.value + return arg + + def is_assignable(self, value): + return isinstance(value, (InterpreterObject, dependencies.Dependency, + str, int, list, mesonlib.File)) + + def func_build_target(self, node, args, kwargs): + if 'target_type' not in kwargs: + raise InterpreterException('Missing target_type keyword argument') + target_type = kwargs.pop('target_type') + if target_type == 'executable': + return self.func_executable(node, args, kwargs) + elif target_type == 'shared_library': + return self.func_shared_lib(node, args, kwargs) + elif target_type == 'static_library': + return self.func_static_lib(node, args, kwargs) + elif target_type == 'library': + return self.func_library(node, args, kwargs) + elif target_type == 'jar': + return self.func_jar(node, args, kwargs) + else: + raise InterpreterException('Unknown target_type.') + + def func_set_variable(self, node, args, kwargs): + if len(args) != 2: + raise InvalidCode('Set_variable takes two arguments.') + varname = args[0] + value = self.to_native(args[1]) + self.set_variable(varname, value) + +# @noKwargs + def func_get_variable(self, node, args, kwargs): + if len(args)<1 or len(args)>2: + raise InvalidCode('Get_variable takes one or two arguments.') + varname = args[0] + if not isinstance(varname, str): + raise InterpreterException('First argument must be a string.') + try: + return self.variables[varname] + except KeyError: + pass + if len(args) == 2: + return args[1] + raise InterpreterException('Tried to get unknown variable "%s".' % varname) + + @stringArgs + @noKwargs + def func_is_variable(self, node, args, kwargs): + if len(args) != 1: + raise InvalidCode('Is_variable takes two arguments.') + varname = args[0] + return varname in self.variables + + def is_elementary_type(self, v): + return isinstance(v, (int, float, str, bool, list)) + diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index f593c8e..ad1fedd 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -22,10 +22,12 @@ class ParseException(MesonException): self.colno = colno class Token: - def __init__(self, tid, lineno, colno, value): + def __init__(self, tid, subdir, lineno, colno, bytespan, value): self.tid = tid + self.subdir = subdir self.lineno = lineno self.colno = colno + self.bytespan = bytespan self.value = value def __eq__(self, other): @@ -71,7 +73,7 @@ class Lexer: ('questionmark', re.compile(r'\?')), ] - def lex(self, code): + def lex(self, code, subdir): lineno = 1 line_start = 0 loc = 0; @@ -87,7 +89,10 @@ class Lexer: curline = lineno col = mo.start()-line_start matched = True + span_start = loc loc = mo.end() + span_end = loc + bytespan = (span_start, span_end) match_text = mo.group() if tid == 'ignore' or tid == 'comment': break @@ -123,40 +128,41 @@ class Lexer: tid = match_text else: value = match_text - yield Token(tid, curline, col, value) + yield Token(tid, subdir, curline, col, bytespan, value) break if not matched: raise ParseException('lexer', lineno, col) -class BooleanNode: - def __init__(self, token, value): +class ElementaryNode: + def __init__(self, token): self.lineno = token.lineno + self.subdir = token.subdir self.colno = token.colno + self.value = token.value + self.bytespan = token.bytespan + +class BooleanNode(ElementaryNode): + def __init__(self, token, value): + super().__init__(token) self.value = value assert(isinstance(self.value, bool)) -class IdNode: +class IdNode(ElementaryNode): def __init__(self, token): - self.lineno = token.lineno - self.colno = token.colno - self.value = token.value + super().__init__(token) assert(isinstance(self.value, str)) def __str__(self): return "Id node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) -class NumberNode: +class NumberNode(ElementaryNode): def __init__(self, token): - self.lineno = token.lineno - self.colno = token.colno - self.value = token.value + super().__init__(token) assert(isinstance(self.value, int)) -class StringNode: +class StringNode(ElementaryNode): def __init__(self, token): - self.lineno = token.lineno - self.colno = token.colno - self.value = token.value + super().__init__(token) assert(isinstance(self.value, str)) def __str__(self): @@ -164,20 +170,23 @@ class StringNode: class ArrayNode: def __init__(self, args): + self.subdir = args.subdir self.lineno = args.lineno self.colno = args.colno self.args = args class EmptyNode: def __init__(self): + self.subdir ='' self.lineno = 0 self.colno = 0 self.value = None class OrNode: - def __init__(self, lineno, colno, left, right): - self.lineno = lineno - self.colno = colno + def __init__(self, left, right): + self.subdir = left.subdir + self.lineno = left.lineno + self.colno = left.colno self.left = left self.right = right @@ -189,42 +198,48 @@ class AndNode: self.right = right class ComparisonNode: - def __init__(self, lineno, colno, ctype, left, right): - self.lineno = lineno - self.colno = colno + def __init__(self, ctype, left, right): + self.lineno = left.lineno + self.colno = left.colno + self.subdir = left.subdir self.left = left self.right = right self.ctype = ctype class ArithmeticNode: - def __init__(self, lineno, colno, operation, left, right): - self.lineno = lineno - self.colno = colno + def __init__(self,operation, left, right): + self.subdir = left.subdir + self.lineno = left.lineno + self.colno = left.colno self.left = left self.right = right self.operation = operation class NotNode: - def __init__(self, lineno, colno, value): - self.lineno = lineno - self.colno = colno + def __init__(self, location_node, value): + self.subdir = location_node.subdir + self.lineno = location_node.lineno + self.colno = location_node.colno self.value = value class CodeBlockNode: - def __init__(self, lineno, colno): - self.lineno = lineno - self.colno = colno + def __init__(self, location_node): + self.subdir = location_node.subdir + self.lineno = location_node.lineno + self.colno = location_node.colno self.lines = [] class IndexNode: def __init__(self, iobject, index): self.iobject = iobject self.index = index + self.subdir = iobject.subdir self.lineno = iobject.lineno self.colno = iobject.colno class MethodNode: - def __init__(self, lineno, colno, source_object, name, args): + def __init__(self, subdir, lineno, colno, source_object, name, args): + self.subdir = subdir self.lineno = lineno self.colno = colno self.source_object = source_object @@ -233,7 +248,8 @@ class MethodNode: self.args = args class FunctionNode: - def __init__(self, lineno, colno, func_name, args): + def __init__(self, subdir, lineno, colno, func_name, args): + self.subdir = subdir self.lineno = lineno self.colno = colno self.func_name = func_name @@ -272,9 +288,10 @@ class IfClauseNode(): self.elseblock = EmptyNode() class UMinusNode(): - def __init__(self, lineno, colno, value): - self.lineno = lineno - self.colno = colno + def __init__(self, current_location, value): + self.subdir = current_location.subdir + self.lineno = current_location.lineno + self.colno = current_location.colno self.value = value class IfNode(): @@ -296,7 +313,9 @@ class ArgumentNode(): def __init__(self, token): self.lineno = token.lineno self.colno = token.colno + self.subdir = token.subdir self.arguments = [] + self.commas = [] self.kwargs = {} self.order_error = False @@ -351,8 +370,8 @@ comparison_map = {'equal': '==', # 9 plain token class Parser: - def __init__(self, code): - self.stream = Lexer().lex(code) + def __init__(self, code, subdir): + self.stream = Lexer().lex(code, subdir) self.getsym() self.in_ternary = False @@ -360,7 +379,7 @@ class Parser: try: self.current = next(self.stream) except StopIteration: - self.current = Token('eof', 0, 0, None) + self.current = Token('eof', '', 0, 0, (0, 0), None) def accept(self, s): if self.current.tid == s: @@ -409,7 +428,7 @@ class Parser: def e2(self): left = self.e3() while self.accept('or'): - left = OrNode(left.lineno, left.colno, left, self.e3()) + left = OrNode(left, self.e3()) return left def e3(self): @@ -422,7 +441,7 @@ class Parser: left = self.e5() for nodename, operator_type in comparison_map.items(): if self.accept(nodename): - return ComparisonNode(left.lineno, left.colno, operator_type, left, self.e5()) + return ComparisonNode(operator_type, left, self.e5()) return left def e5(self): @@ -431,38 +450,38 @@ class Parser: def e5add(self): left = self.e5sub() if self.accept('plus'): - return ArithmeticNode(left.lineno, left.colno, 'add', left, self.e5add()) + return ArithmeticNode('add', left, self.e5add()) return left def e5sub(self): left = self.e5mod() if self.accept('dash'): - return ArithmeticNode(left.lineno, left.colno, 'sub', left, self.e5sub()) + return ArithmeticNode('sub', left, self.e5sub()) return left def e5mod(self): left = self.e5mul() if self.accept('percent'): - return ArithmeticNode(left.lineno, left.colno, 'mod', left, self.e5mod()) + return ArithmeticNode('mod', left, self.e5mod()) return left def e5mul(self): left = self.e5div() if self.accept('star'): - return ArithmeticNode(left.lineno, left.colno, 'mul', left, self.e5mul()) + return ArithmeticNode('mul', left, self.e5mul()) return left def e5div(self): left = self.e6() if self.accept('fslash'): - return ArithmeticNode(left.lineno, left.colno, 'div', left, self.e5div()) + return ArithmeticNode('div', left, self.e5div()) return left def e6(self): if self.accept('not'): - return NotNode(self.current.lineno, self.current.colno, self.e7()) + return NotNode(self.current, self.e7()) if self.accept('dash'): - return UMinusNode(self.current.lineno, self.current.colno, self.e7()) + return UMinusNode(self.current, self.e7()) return self.e7() def e7(self): @@ -473,7 +492,7 @@ class Parser: if not isinstance(left, IdNode): raise ParseException('Function call must be applied to plain id', left.lineno, left.colno) - left = FunctionNode(left.lineno, left.colno, left.value, args) + left = FunctionNode(left.subdir, left.lineno, left.colno, left.value, args) go_again = True while go_again: go_again = False @@ -516,15 +535,19 @@ class Parser: a = ArgumentNode(s) while not isinstance(s, EmptyNode): + potential = self.current if self.accept('comma'): + a.commas.append(potential) a.append(s) elif self.accept('colon'): if not isinstance(s, IdNode): raise ParseException('Keyword argument must be a plain identifier.', s.lineno, s.colno) a.set_kwarg(s.value, self.statement()) + potential = self.current if not self.accept('comma'): return a + a.commas.append(potential) else: a.append(s) return a @@ -539,7 +562,7 @@ class Parser: self.expect('lparen') args = self.args() self.expect('rparen') - method = MethodNode(methodname.lineno, methodname.colno, source_object, methodname.value, args) + method = MethodNode(methodname.subdir, methodname.lineno, methodname.colno, source_object, methodname.value, args) if self.accept('dot'): return self.method_call(method) return method @@ -593,7 +616,7 @@ class Parser: return self.statement() def codeblock(self): - block = CodeBlockNode(self.current.lineno, self.current.colno) + block = CodeBlockNode(self.current) cond = True while cond: curline = self.line() diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 9f57fd6..4fe0843 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -79,7 +79,7 @@ class OptionInterpreter: def process(self, option_file): try: with open(option_file, 'r', encoding='utf8') as f: - ast = mparser.Parser(f.read()).parse() + ast = mparser.Parser(f.read(), '').parse() except mesonlib.MesonException as me: me.file = option_file raise me |