# SPDX-License-Identifier: Apache-2.0 # Copyright © 2022-2023 Intel Corporation """Rust CFG parser. Rust uses its `cfg()` format in cargo. https://doc.rust-lang.org/reference/conditional-compilation.html This may have the following functions: - all() - any() - not() And additionally is made up of `identifier [ = str]`. Where the str is optional, so you could have examples like: ``` [target.`cfg(unix)`.dependencies] [target.'cfg(target_arch = "x86_64")'.dependencies] [target.'cfg(all(target_arch = "x86_64", target_arch = "x86"))'.dependencies] ``` """ from __future__ import annotations import dataclasses import enum import typing as T from ..mesonlib import MesonBugException if T.TYPE_CHECKING: _T = T.TypeVar('_T') _LEX_TOKEN = T.Tuple['TokenType', T.Optional[str]] _LEX_STREAM = T.Iterator[_LEX_TOKEN] _LEX_STREAM_AH = T.Iterator[T.Tuple[_LEX_TOKEN, T.Optional[_LEX_TOKEN]]] class TokenType(enum.Enum): LPAREN = enum.auto() RPAREN = enum.auto() STRING = enum.auto() IDENTIFIER = enum.auto() ALL = enum.auto() ANY = enum.auto() NOT = enum.auto() COMMA = enum.auto() EQUAL = enum.auto() CFG = enum.auto() def lexer(raw: str) -> _LEX_STREAM: """Lex a cfg() expression. :param raw: The raw cfg() expression :return: An iterable of tokens """ start: int = 0 is_string: bool = False for i, s in enumerate(raw): if s.isspace() or s in {')', '(', ',', '=', '"'}: val = raw[start:i] start = i + 1 if s == '"' and is_string: yield (TokenType.STRING, val) is_string = False continue elif val == 'any': yield (TokenType.ANY, None) elif val == 'all': yield (TokenType.ALL, None) elif val == 'not': yield (TokenType.NOT, None) elif val == 'cfg': yield (TokenType.CFG, None) elif val: yield (TokenType.IDENTIFIER, val) if s == '(': yield (TokenType.LPAREN, None) elif s == ')': yield (TokenType.RPAREN, None) elif s == ',': yield (TokenType.COMMA, None) elif s == '=': yield (TokenType.EQUAL, None) elif s == '"': is_string = True val = raw[start:] if val: # This should always be an identifier yield (TokenType.IDENTIFIER, val) def lookahead(iter: T.Iterator[_T]) -> T.Iterator[T.Tuple[_T, T.Optional[_T]]]: """Get the current value of the iterable, and the next if possible. :param iter: The iterable to look into :yield: A tuple of the current value, and, if possible, the next :return: nothing """ current: _T next_: T.Optional[_T] try: next_ = next(iter) except StopIteration: # This is an empty iterator, there's nothing to look ahead to return while True: current = next_ try: next_ = next(iter) except StopIteration: next_ = None yield current, next_ if next_ is None: break @dataclasses.dataclass class IR: """Base IR node for Cargo CFG.""" @dataclasses.dataclass class String(IR): value: str @dataclasses.dataclass class Identifier(IR): value: str @dataclasses.dataclass class Equal(IR): lhs: Identifier rhs: String @dataclasses.dataclass class Any(IR): args: T.List[IR] @dataclasses.dataclass class All(IR): args: T.List[IR] @dataclasses.dataclass class Not(IR): value: IR def _parse(ast: _LEX_STREAM_AH) -> IR: (token, value), n_stream = next(ast) if n_stream is not None: ntoken, _ = n_stream else: ntoken, _ = (None, None) if token is TokenType.IDENTIFIER: assert value id_ = Identifier(value) if ntoken is TokenType.EQUAL: next(ast) (token, value), _ = next(ast) assert token is TokenType.STRING assert value is not None return Equal(id_, String(value)) return id_ elif token in {TokenType.ANY, TokenType.ALL}: type_ = All if token is TokenType.ALL else Any args: T.List[IR] = [] (token, value), n_stream = next(ast) assert token is TokenType.LPAREN if n_stream and n_stream[0] == TokenType.RPAREN: return type_(args) while True: args.append(_parse(ast)) (token, value), _ = next(ast) if token is TokenType.RPAREN: break assert token is TokenType.COMMA return type_(args) elif token in {TokenType.NOT, TokenType.CFG}: is_not = token is TokenType.NOT (token, value), _ = next(ast) assert token is TokenType.LPAREN arg = _parse(ast) (token, value), _ = next(ast) assert token is TokenType.RPAREN return Not(arg) if is_not else arg else: raise MesonBugException(f'Unhandled Cargo token:{token} {value}') def parse(ast: _LEX_STREAM) -> IR: """Parse the tokenized list into Meson AST. :param ast: An iterable of Tokens :return: An mparser Node to be used as a conditional """ ast_i: _LEX_STREAM_AH = lookahead(ast) return _parse(ast_i) def _eval_cfg(ir: IR, cfgs: T.Dict[str, str]) -> bool: if isinstance(ir, Identifier): return ir.value in cfgs elif isinstance(ir, Equal): return cfgs.get(ir.lhs.value) == ir.rhs.value elif isinstance(ir, Not): return not _eval_cfg(ir.value, cfgs) elif isinstance(ir, Any): return any(_eval_cfg(i, cfgs) for i in ir.args) elif isinstance(ir, All): return all(_eval_cfg(i, cfgs) for i in ir.args) else: raise MesonBugException(f'Unhandled Cargo cfg IR: {ir}') def eval_cfg(raw: str, cfgs: T.Dict[str, str]) -> bool: return _eval_cfg(parse(lexer(raw)), cfgs)