Skip to content

Commit

Permalink
Major rewrite of the rewriter and the static introspection tool
Browse files Browse the repository at this point in the history
The rewriter and the static introspection tool
used to be very broken, now it is *less* broken.

The most important changes are:

1. We now have class UnknownValue for more explicit handling
	of situations that are too complex/impossible.

2. If you write
	```
	var = 'foo'
	name = var
	var = 'bar'
	executable(name, 'foo.c')
	```
	the tool now knows that the name of the executable is foo and not bar.
	See dataflow_dag and node_to_runtime_value for details on how we do this.

Fixes mesonbuild#11763
  • Loading branch information
Volker-Weissmann committed Oct 2, 2023
1 parent 4cebf88 commit e70c34d
Show file tree
Hide file tree
Showing 23 changed files with 1,549 additions and 740 deletions.
3 changes: 1 addition & 2 deletions mesonbuild/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@
'AstVisitor',
'AstPrinter',
'IntrospectionInterpreter',
'BUILD_TARGET_FUNCTIONS',
]

from .interpreter import AstInterpreter
from .introspection import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS
from .introspection import IntrospectionInterpreter
from .visitor import AstVisitor
from .postprocess import AstConditionLevel, AstIDGenerator, AstIndentationGenerator
from .printer import AstPrinter, AstJSONPrinter
760 changes: 578 additions & 182 deletions mesonbuild/ast/interpreter.py

Large diffs are not rendered by default.

184 changes: 83 additions & 101 deletions mesonbuild/ast/introspection.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
is_header, is_object, is_source, clink_langs, sort_clink, all_languages,
is_known_suffix, detect_static_linker
)
from .interpreterbase import FeatureNew, FeatureDeprecated
from .interpreterbase import FeatureNew, FeatureDeprecated, UnknownValue

if T.TYPE_CHECKING:
from typing_extensions import Literal
Expand Down Expand Up @@ -640,7 +640,7 @@ def get_id(self) -> str:
def process_kwargs_base(self, kwargs: T.Dict[str, T.Any]) -> None:
if 'build_by_default' in kwargs:
self.build_by_default = kwargs['build_by_default']
if not isinstance(self.build_by_default, bool):
if not isinstance(self.build_by_default, (bool, UnknownValue)):
raise InvalidArguments('build_by_default must be a boolean value.')
elif kwargs.get('install', False):
# For backward compatibility, if build_by_default is not explicitly
Expand Down Expand Up @@ -870,7 +870,7 @@ def can_compile_remove_sources(compiler: 'Compiler', sources: T.List['FileOrStri
removed = True
return removed

def process_compilers_late(self):
def process_compilers_late(self) -> None:
"""Processes additional compilers after kwargs have been evaluated.
This can add extra compilers that might be required by keyword
Expand Down
4 changes: 4 additions & 0 deletions mesonbuild/interpreterbase/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
'TYPE_HoldableTypes',

'HoldableTypes',

'UnknownValue',
]

from .baseobjects import (
Expand All @@ -100,6 +102,8 @@
SubProject,

HoldableTypes,

UnknownValue,
)

from .decorators import (
Expand Down
6 changes: 6 additions & 0 deletions mesonbuild/interpreterbase/baseobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ class MesonInterpreterObject(InterpreterObject):
class MutableInterpreterObject:
''' Dummy class to mark the object type as mutable '''

class UnknownValue(MesonInterpreterObject):
'''This class is only used for the rewriter/static introspection tool and
indicates that a value cannot be determined statically, either because of
limitations in our code or because the value differs from machine to
machine.'''

HoldableTypes = (HoldableObject, int, bool, str, list, dict)
TYPE_HoldableTypes = T.Union[TYPE_elementary, HoldableObject]
InterpreterObjectTypeVar = T.TypeVar('InterpreterObjectTypeVar', bound=TYPE_HoldableTypes)
Expand Down
7 changes: 5 additions & 2 deletions mesonbuild/interpreterbase/interpreterbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[InterpreterObj
return self.evaluate_statement(cur.inner)
elif isinstance(cur, mparser.TestCaseClauseNode):
return self.evaluate_testcase(cur)
elif isinstance(cur, mparser.EmptyNode):
return None
else:
raise InvalidCode("Unknown statement.")
return None
Expand Down Expand Up @@ -505,7 +507,7 @@ def evaluate_indexing(self, node: mparser.IndexNode) -> InterpreterObject:

def function_call(self, node: mparser.FunctionNode) -> T.Optional[InterpreterObject]:
func_name = node.func_name.value
(h_posargs, h_kwargs) = self.reduce_arguments(node.args)
(h_posargs, h_kwargs) = self.reduce_arguments(node.args, include_unknown_args = True)
(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()
Expand Down Expand Up @@ -533,7 +535,7 @@ def method_call(self, node: mparser.MethodNode) -> T.Optional[InterpreterObject]
object_display_name = invocable.__class__.__name__
obj = self.evaluate_statement(invocable)
method_name = node.name.value
(h_args, h_kwargs) = self.reduce_arguments(node.args)
(h_args, h_kwargs) = self.reduce_arguments(node.args, include_unknown_args = True)
(args, kwargs) = self._unholder_args(h_args, h_kwargs)
if is_disabled(args, kwargs):
return Disabler()
Expand Down Expand Up @@ -581,6 +583,7 @@ def reduce_arguments(
args: mparser.ArgumentNode,
key_resolver: T.Callable[[mparser.BaseNode], str] = default_resolve_key,
duplicate_key_error: T.Optional[str] = None,
include_unknown_args: bool = False,
) -> T.Tuple[
T.List[InterpreterObject],
T.Dict[str, InterpreterObject]
Expand Down
79 changes: 31 additions & 48 deletions mesonbuild/mintro.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,23 @@
import typing as T

from . import build, mesonlib, coredata as cdata
from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstJSONPrinter
from .ast import IntrospectionInterpreter, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstJSONPrinter
from .backend import backends
from .dependencies import Dependency
from . import environment
from .interpreterbase import ObjectHolder
from .interpreterbase import ObjectHolder, UnknownValue
from .mesonlib import OptionKey
from .mparser import FunctionNode, ArrayNode, ArgumentNode, BaseStringNode

if T.TYPE_CHECKING:
import argparse

from .interpreter import Interpreter
from .mparser import BaseNode

class IntrospectionEncoder(json.JSONEncoder):
def default(self, obj: T.Any) -> T.Any:
if isinstance(obj, UnknownValue):
return 'unknown'
return json.JSONEncoder.default(self, obj)

def get_meson_info_file(info_dir: str) -> str:
return os.path.join(info_dir, 'meson-info.json')
Expand All @@ -64,8 +68,7 @@ def __init__(self,

def get_meson_introspection_types(coredata: T.Optional[cdata.CoreData] = None,
builddata: T.Optional[build.Build] = None,
backend: T.Optional[backends.Backend] = None,
sourcedir: T.Optional[str] = None) -> 'T.Mapping[str, IntroCommand]':
backend: T.Optional[backends.Backend] = None) -> 'T.Mapping[str, IntroCommand]':
if backend and builddata:
benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks())
testdata = backend.create_test_serialisation(builddata.get_tests())
Expand Down Expand Up @@ -177,55 +180,34 @@ def get_target_dir(coredata: cdata.CoreData, subdir: str) -> str:
else:
return subdir

def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]:
tlist: T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]] = []
root_dir = Path(intr.source_root)

def nodes_to_paths(node_list: T.List[BaseNode]) -> T.List[Path]:
res: T.List[Path] = []
for n in node_list:
args: T.List[BaseNode] = []
if isinstance(n, FunctionNode):
args = list(n.args.arguments)
if n.func_name.value in BUILD_TARGET_FUNCTIONS:
args.pop(0)
elif isinstance(n, ArrayNode):
args = n.args.arguments
elif isinstance(n, ArgumentNode):
args = n.arguments
for j in args:
if isinstance(j, BaseStringNode):
assert isinstance(j.value, str)
res += [Path(j.value)]
elif isinstance(j, str):
res += [Path(j)]
res = [root_dir / i['subdir'] / x for x in res]
res = [x.resolve() for x in res]
return res
def list_targets_from_source(intr: IntrospectionInterpreter) -> T.Any:
tlist = []
root_dir = Path(intr.source_root).resolve()

for i in intr.targets:
sources = nodes_to_paths(i['sources'])
extra_f = nodes_to_paths(i['extra_files'])
outdir = get_target_dir(intr.coredata, i['subdir'])
sources = intr.nodes_to_pretty_filelist(root_dir, i.subdir, i.source_nodes)
extra_files = intr.nodes_to_pretty_filelist(root_dir, i.subdir, [i.extra_files] if i.extra_files else [])

outdir = get_target_dir(intr.coredata, i.subdir)

tlist += [{
'name': i['name'],
'id': i['id'],
'type': i['type'],
'defined_in': i['defined_in'],
'filename': [os.path.join(outdir, x) for x in i['outputs']],
'build_by_default': i['build_by_default'],
'name': i.name,
'id': i.id,
'type': i.typename,
'defined_in': i.defined_in,
'filename': [os.path.join(outdir, x) for x in i.outputs],
'build_by_default': i.build_by_default,
'target_sources': [{
'language': 'unknown',
'compiler': [],
'parameters': [],
'sources': [str(x) for x in sources],
'sources': sources,
'generated_sources': []
}],
'depends': [],
'extra_files': [str(x) for x in extra_f],
'extra_files': extra_files,
'subproject': None, # Subprojects are not supported
'installed': i['installed']
'installed': i.installed
}]

return tlist
Expand Down Expand Up @@ -388,7 +370,7 @@ def list_deps_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str,
'has_fallback',
'conditional',
]
result += [{k: v for k, v in i.items() if k in keys}]
result += [{k: v for k, v in i.__dict__.items() if k in keys}]
return result

def list_deps(coredata: cdata.CoreData, backend: backends.Backend) -> T.List[T.Dict[str, T.Union[str, T.List[str]]]]:
Expand Down Expand Up @@ -513,7 +495,7 @@ def print_results(options: argparse.Namespace, results: T.Sequence[T.Tuple[str,
return 1
elif len(results) == 1 and not options.force_dict:
# Make to keep the existing output format for a single option
print(json.dumps(results[0][1], indent=indent))
print(json.dumps(results[0][1], indent=indent, cls=IntrospectionEncoder))
else:
out = {}
for i in results:
Expand Down Expand Up @@ -542,10 +524,11 @@ def run(options: argparse.Namespace) -> int:
datadir = os.path.join(options.builddir, datadir)
indent = 4 if options.indent else None
results: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]] = []
sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11]
intro_types = get_meson_introspection_types(sourcedir=sourcedir)
intro_types = get_meson_introspection_types()

if 'meson.build' in [os.path.basename(options.builddir), options.builddir]:
# TODO: This if clause is undocumented.
if os.path.basename(options.builddir) == 'meson.build':
sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11]
# Make sure that log entries in other parts of meson don't interfere with the JSON output
with redirect_stdout(sys.stderr):
backend = backends.get_backend_from_name(options.backend)
Expand Down
7 changes: 7 additions & 0 deletions mesonbuild/mparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,13 @@ def set_kwarg(self, name: IdNode, value: BaseNode) -> None:
mlog.warning('This will be an error in future Meson releases.')
self.kwargs[name] = value

def get_kwarg_or_default(self, name: str, default: BaseNode) -> BaseNode:
for k, v in self.kwargs.items():
assert isinstance(k, IdNode)
if k.value == name:
return v
return default

def set_kwarg_no_check(self, name: BaseNode, value: BaseNode) -> None:
self.kwargs[name] = value

Expand Down
Loading

0 comments on commit e70c34d

Please sign in to comment.