The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
830 lines
32 KiB
830 lines
32 KiB
# Copyright 2014-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. |
|
|
|
from dataclasses import dataclass |
|
import re |
|
import codecs |
|
import typing as T |
|
from .mesonlib import MesonException |
|
from . import mlog |
|
|
|
if T.TYPE_CHECKING: |
|
from .ast import AstVisitor |
|
|
|
# This is the regex for the supported escape sequences of a regular string |
|
# literal, like 'abc\x00' |
|
ESCAPE_SEQUENCE_SINGLE_RE = re.compile(r''' |
|
( \\U[A-Fa-f0-9]{8} # 8-digit hex escapes |
|
| \\u[A-Fa-f0-9]{4} # 4-digit hex escapes |
|
| \\x[A-Fa-f0-9]{2} # 2-digit hex escapes |
|
| \\[0-7]{1,3} # Octal escapes |
|
| \\N\{[^}]+\} # Unicode characters by name |
|
| \\[\\'abfnrtv] # Single-character escapes |
|
)''', re.UNICODE | re.VERBOSE) |
|
|
|
class MesonUnicodeDecodeError(MesonException): |
|
def __init__(self, match: str) -> None: |
|
super().__init__(match) |
|
self.match = match |
|
|
|
def decode_match(match: T.Match[str]) -> str: |
|
try: |
|
return codecs.decode(match.group(0).encode(), 'unicode_escape') |
|
except UnicodeDecodeError: |
|
raise MesonUnicodeDecodeError(match.group(0)) |
|
|
|
class ParseException(MesonException): |
|
def __init__(self, text: str, line: str, lineno: int, colno: int) -> None: |
|
# Format as error message, followed by the line with the error, followed by a caret to show the error column. |
|
super().__init__(mlog.code_line(text, line, colno)) |
|
self.lineno = lineno |
|
self.colno = colno |
|
|
|
class BlockParseException(MesonException): |
|
def __init__( |
|
self, |
|
text: str, |
|
line: str, |
|
lineno: int, |
|
colno: int, |
|
start_line: str, |
|
start_lineno: int, |
|
start_colno: int, |
|
) -> None: |
|
# This can be formatted in two ways - one if the block start and end are on the same line, and a different way if they are on different lines. |
|
|
|
if lineno == start_lineno: |
|
# If block start and end are on the same line, it is formatted as: |
|
# Error message |
|
# Followed by the line with the error |
|
# Followed by a caret to show the block start |
|
# Followed by underscores |
|
# Followed by a caret to show the block end. |
|
super().__init__("{}\n{}\n{}".format(text, line, '{}^{}^'.format(' ' * start_colno, '_' * (colno - start_colno - 1)))) |
|
else: |
|
# If block start and end are on different lines, it is formatted as: |
|
# Error message |
|
# Followed by the line with the error |
|
# Followed by a caret to show the error column. |
|
# Followed by a message saying where the block started. |
|
# Followed by the line of the block start. |
|
# Followed by a caret for the block start. |
|
super().__init__("%s\n%s\n%s\nFor a block that started at %d,%d\n%s\n%s" % (text, line, '%s^' % (' ' * colno), start_lineno, start_colno, start_line, "%s^" % (' ' * start_colno))) |
|
self.lineno = lineno |
|
self.colno = colno |
|
|
|
TV_TokenTypes = T.TypeVar('TV_TokenTypes', int, str, bool) |
|
|
|
@dataclass(eq=False) |
|
class Token(T.Generic[TV_TokenTypes]): |
|
tid: str |
|
filename: str |
|
line_start: int |
|
lineno: int |
|
colno: int |
|
bytespan: T.Tuple[int, int] |
|
value: TV_TokenTypes |
|
|
|
def __eq__(self, other: object) -> bool: |
|
if isinstance(other, str): |
|
return self.tid == other |
|
elif isinstance(other, Token): |
|
return self.tid == other.tid |
|
return NotImplemented |
|
|
|
class Lexer: |
|
def __init__(self, code: str): |
|
self.code = code |
|
self.keywords = {'true', 'false', 'if', 'else', 'elif', |
|
'endif', 'and', 'or', 'not', 'foreach', 'endforeach', |
|
'in', 'continue', 'break'} |
|
self.future_keywords = {'return'} |
|
self.token_specification = [ |
|
# Need to be sorted longest to shortest. |
|
('ignore', re.compile(r'[ \t]')), |
|
('multiline_fstring', re.compile(r"f'''(.|\n)*?'''", re.M)), |
|
('fstring', re.compile(r"f'([^'\\]|(\\.))*'")), |
|
('id', re.compile('[_a-zA-Z][_0-9a-zA-Z]*')), |
|
('number', re.compile(r'0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|0|[1-9]\d*')), |
|
('eol_cont', re.compile(r'\\\n')), |
|
('eol', re.compile(r'\n')), |
|
('multiline_string', re.compile(r"'''(.|\n)*?'''", re.M)), |
|
('comment', re.compile(r'#.*')), |
|
('lparen', re.compile(r'\(')), |
|
('rparen', re.compile(r'\)')), |
|
('lbracket', re.compile(r'\[')), |
|
('rbracket', re.compile(r'\]')), |
|
('lcurl', re.compile(r'\{')), |
|
('rcurl', re.compile(r'\}')), |
|
('dblquote', re.compile(r'"')), |
|
('string', re.compile(r"'([^'\\]|(\\.))*'")), |
|
('comma', re.compile(r',')), |
|
('plusassign', re.compile(r'\+=')), |
|
('dot', re.compile(r'\.')), |
|
('plus', re.compile(r'\+')), |
|
('dash', re.compile(r'-')), |
|
('star', re.compile(r'\*')), |
|
('percent', re.compile(r'%')), |
|
('fslash', re.compile(r'/')), |
|
('colon', re.compile(r':')), |
|
('equal', re.compile(r'==')), |
|
('nequal', re.compile(r'!=')), |
|
('assign', re.compile(r'=')), |
|
('le', re.compile(r'<=')), |
|
('lt', re.compile(r'<')), |
|
('ge', re.compile(r'>=')), |
|
('gt', re.compile(r'>')), |
|
('questionmark', re.compile(r'\?')), |
|
] |
|
|
|
def getline(self, line_start: int) -> str: |
|
return self.code[line_start:self.code.find('\n', line_start)] |
|
|
|
def lex(self, filename: str) -> T.Generator[Token, None, None]: |
|
line_start = 0 |
|
lineno = 1 |
|
loc = 0 |
|
par_count = 0 |
|
bracket_count = 0 |
|
curl_count = 0 |
|
col = 0 |
|
while loc < len(self.code): |
|
matched = False |
|
value = None # type: T.Union[str, bool, int] |
|
for (tid, reg) in self.token_specification: |
|
mo = reg.match(self.code, loc) |
|
if mo: |
|
curline = lineno |
|
curline_start = line_start |
|
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 in {'ignore', 'comment'}: |
|
break |
|
elif tid == 'lparen': |
|
par_count += 1 |
|
elif tid == 'rparen': |
|
par_count -= 1 |
|
elif tid == 'lbracket': |
|
bracket_count += 1 |
|
elif tid == 'rbracket': |
|
bracket_count -= 1 |
|
elif tid == 'lcurl': |
|
curl_count += 1 |
|
elif tid == 'rcurl': |
|
curl_count -= 1 |
|
elif tid == 'dblquote': |
|
raise ParseException('Double quotes are not supported. Use single quotes.', self.getline(line_start), lineno, col) |
|
elif tid in {'string', 'fstring'}: |
|
# Handle here and not on the regexp to give a better error message. |
|
if match_text.find("\n") != -1: |
|
msg = ("Newline character in a string detected, use ''' (three single quotes) " |
|
"for multiline strings instead.\n" |
|
"This will become a hard error in a future Meson release.") |
|
mlog.warning(mlog.code_line(msg, self.getline(line_start), col), location=BaseNode(lineno, col, filename)) |
|
value = match_text[2 if tid == 'fstring' else 1:-1] |
|
try: |
|
value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, value) |
|
except MesonUnicodeDecodeError as err: |
|
raise MesonException(f"Failed to parse escape sequence: '{err.match}' in string:\n {match_text}") |
|
elif tid in {'multiline_string', 'multiline_fstring'}: |
|
# For multiline strings, parse out the value and pass |
|
# through the normal string logic. |
|
# For multiline format strings, we have to emit a |
|
# different AST node so we can add a feature check, |
|
# but otherwise, it follows the normal fstring logic. |
|
if tid == 'multiline_string': |
|
value = match_text[3:-3] |
|
tid = 'string' |
|
else: |
|
value = match_text[4:-3] |
|
lines = match_text.split('\n') |
|
if len(lines) > 1: |
|
lineno += len(lines) - 1 |
|
line_start = mo.end() - len(lines[-1]) |
|
elif tid == 'number': |
|
value = int(match_text, base=0) |
|
elif tid == 'eol_cont': |
|
lineno += 1 |
|
line_start = loc |
|
break |
|
elif tid == 'eol': |
|
lineno += 1 |
|
line_start = loc |
|
if par_count > 0 or bracket_count > 0 or curl_count > 0: |
|
break |
|
elif tid == 'id': |
|
if match_text in self.keywords: |
|
tid = match_text |
|
else: |
|
if match_text in self.future_keywords: |
|
mlog.warning(f"Identifier '{match_text}' will become a reserved keyword in a future release. Please rename it.", |
|
location=BaseNode(lineno, col, filename)) |
|
value = match_text |
|
yield Token(tid, filename, curline_start, curline, col, bytespan, value) |
|
break |
|
if not matched: |
|
raise ParseException('lexer', self.getline(line_start), lineno, col) |
|
|
|
@dataclass(eq=False) |
|
class BaseNode: |
|
lineno: int |
|
colno: int |
|
filename: str |
|
end_lineno: T.Optional[int] = None |
|
end_colno: T.Optional[int] = None |
|
|
|
def __post_init__(self) -> None: |
|
if self.end_lineno is None: |
|
self.end_lineno = self.lineno |
|
if self.end_colno is None: |
|
self.end_colno = self.colno |
|
|
|
# Attributes for the visitors |
|
self.level = 0 # type: int |
|
self.ast_id = '' # type: str |
|
self.condition_level = 0 # type: int |
|
|
|
def accept(self, visitor: 'AstVisitor') -> None: |
|
fname = 'visit_{}'.format(type(self).__name__) |
|
if hasattr(visitor, fname): |
|
func = getattr(visitor, fname) |
|
if callable(func): |
|
func(self) |
|
|
|
class ElementaryNode(T.Generic[TV_TokenTypes], BaseNode): |
|
def __init__(self, token: Token[TV_TokenTypes]): |
|
super().__init__(token.lineno, token.colno, token.filename) |
|
self.value = token.value # type: TV_TokenTypes |
|
self.bytespan = token.bytespan # type: T.Tuple[int, int] |
|
|
|
class BooleanNode(ElementaryNode[bool]): |
|
def __init__(self, token: Token[bool]): |
|
super().__init__(token) |
|
assert isinstance(self.value, bool) |
|
|
|
class IdNode(ElementaryNode[str]): |
|
def __init__(self, token: Token[str]): |
|
super().__init__(token) |
|
assert isinstance(self.value, str) |
|
|
|
def __str__(self) -> str: |
|
return "Id node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) |
|
|
|
class NumberNode(ElementaryNode[int]): |
|
def __init__(self, token: Token[int]): |
|
super().__init__(token) |
|
assert isinstance(self.value, int) |
|
|
|
class StringNode(ElementaryNode[str]): |
|
def __init__(self, token: Token[str]): |
|
super().__init__(token) |
|
assert isinstance(self.value, str) |
|
|
|
def __str__(self) -> str: |
|
return "String node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) |
|
|
|
class FormatStringNode(ElementaryNode[str]): |
|
def __init__(self, token: Token[str]): |
|
super().__init__(token) |
|
assert isinstance(self.value, str) |
|
|
|
def __str__(self) -> str: |
|
return f"Format string node: '{self.value}' ({self.lineno}, {self.colno})." |
|
|
|
class MultilineFormatStringNode(FormatStringNode): |
|
def __str__(self) -> str: |
|
return f"Multiline Format string node: '{self.value}' ({self.lineno}, {self.colno})." |
|
|
|
class ContinueNode(ElementaryNode): |
|
pass |
|
|
|
class BreakNode(ElementaryNode): |
|
pass |
|
|
|
class ArgumentNode(BaseNode): |
|
def __init__(self, token: Token[TV_TokenTypes]): |
|
super().__init__(token.lineno, token.colno, token.filename) |
|
self.arguments = [] # type: T.List[BaseNode] |
|
self.commas = [] # type: T.List[Token[TV_TokenTypes]] |
|
self.kwargs = {} # type: T.Dict[BaseNode, BaseNode] |
|
self.order_error = False |
|
|
|
def prepend(self, statement: BaseNode) -> None: |
|
if self.num_kwargs() > 0: |
|
self.order_error = True |
|
if not isinstance(statement, EmptyNode): |
|
self.arguments = [statement] + self.arguments |
|
|
|
def append(self, statement: BaseNode) -> None: |
|
if self.num_kwargs() > 0: |
|
self.order_error = True |
|
if not isinstance(statement, EmptyNode): |
|
self.arguments += [statement] |
|
|
|
def set_kwarg(self, name: IdNode, value: BaseNode) -> None: |
|
if any((isinstance(x, IdNode) and name.value == x.value) for x in self.kwargs): |
|
mlog.warning(f'Keyword argument "{name.value}" defined multiple times.', location=self) |
|
mlog.warning('This will be an error in future Meson releases.') |
|
self.kwargs[name] = value |
|
|
|
def set_kwarg_no_check(self, name: BaseNode, value: BaseNode) -> None: |
|
self.kwargs[name] = value |
|
|
|
def num_args(self) -> int: |
|
return len(self.arguments) |
|
|
|
def num_kwargs(self) -> int: |
|
return len(self.kwargs) |
|
|
|
def incorrect_order(self) -> bool: |
|
return self.order_error |
|
|
|
def __len__(self) -> int: |
|
return self.num_args() # Fixme |
|
|
|
class ArrayNode(BaseNode): |
|
def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int): |
|
super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno) |
|
self.args = args # type: ArgumentNode |
|
|
|
class DictNode(BaseNode): |
|
def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int): |
|
super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno) |
|
self.args = args |
|
|
|
class EmptyNode(BaseNode): |
|
def __init__(self, lineno: int, colno: int, filename: str): |
|
super().__init__(lineno, colno, filename) |
|
self.value = None |
|
|
|
class OrNode(BaseNode): |
|
def __init__(self, left: BaseNode, right: BaseNode): |
|
super().__init__(left.lineno, left.colno, left.filename) |
|
self.left = left # type: BaseNode |
|
self.right = right # type: BaseNode |
|
|
|
class AndNode(BaseNode): |
|
def __init__(self, left: BaseNode, right: BaseNode): |
|
super().__init__(left.lineno, left.colno, left.filename) |
|
self.left = left # type: BaseNode |
|
self.right = right # type: BaseNode |
|
|
|
class ComparisonNode(BaseNode): |
|
def __init__(self, ctype: str, left: BaseNode, right: BaseNode): |
|
super().__init__(left.lineno, left.colno, left.filename) |
|
self.left = left # type: BaseNode |
|
self.right = right # type: BaseNode |
|
self.ctype = ctype # type: str |
|
|
|
class ArithmeticNode(BaseNode): |
|
def __init__(self, operation: str, left: BaseNode, right: BaseNode): |
|
super().__init__(left.lineno, left.colno, left.filename) |
|
self.left = left # type: BaseNode |
|
self.right = right # type: BaseNode |
|
self.operation = operation # type: str |
|
|
|
class NotNode(BaseNode): |
|
def __init__(self, token: Token[TV_TokenTypes], value: BaseNode): |
|
super().__init__(token.lineno, token.colno, token.filename) |
|
self.value = value # type: BaseNode |
|
|
|
class CodeBlockNode(BaseNode): |
|
def __init__(self, token: Token[TV_TokenTypes]): |
|
super().__init__(token.lineno, token.colno, token.filename) |
|
self.lines = [] # type: T.List[BaseNode] |
|
|
|
class IndexNode(BaseNode): |
|
def __init__(self, iobject: BaseNode, index: BaseNode): |
|
super().__init__(iobject.lineno, iobject.colno, iobject.filename) |
|
self.iobject = iobject # type: BaseNode |
|
self.index = index # type: BaseNode |
|
|
|
class MethodNode(BaseNode): |
|
def __init__(self, filename: str, lineno: int, colno: int, source_object: BaseNode, name: str, args: ArgumentNode): |
|
super().__init__(lineno, colno, filename) |
|
self.source_object = source_object # type: BaseNode |
|
self.name = name # type: str |
|
assert isinstance(self.name, str) |
|
self.args = args # type: ArgumentNode |
|
|
|
class FunctionNode(BaseNode): |
|
def __init__(self, filename: str, lineno: int, colno: int, end_lineno: int, end_colno: int, func_name: str, args: ArgumentNode): |
|
super().__init__(lineno, colno, filename, end_lineno=end_lineno, end_colno=end_colno) |
|
self.func_name = func_name # type: str |
|
assert isinstance(func_name, str) |
|
self.args = args # type: ArgumentNode |
|
|
|
class AssignmentNode(BaseNode): |
|
def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode): |
|
super().__init__(lineno, colno, filename) |
|
self.var_name = var_name # type: str |
|
assert isinstance(var_name, str) |
|
self.value = value # type: BaseNode |
|
|
|
class PlusAssignmentNode(BaseNode): |
|
def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode): |
|
super().__init__(lineno, colno, filename) |
|
self.var_name = var_name # type: str |
|
assert isinstance(var_name, str) |
|
self.value = value # type: BaseNode |
|
|
|
class ForeachClauseNode(BaseNode): |
|
def __init__(self, token: Token, varnames: T.List[str], items: BaseNode, block: CodeBlockNode): |
|
super().__init__(token.lineno, token.colno, token.filename) |
|
self.varnames = varnames # type: T.List[str] |
|
self.items = items # type: BaseNode |
|
self.block = block # type: CodeBlockNode |
|
|
|
class IfNode(BaseNode): |
|
def __init__(self, linenode: BaseNode, condition: BaseNode, block: CodeBlockNode): |
|
super().__init__(linenode.lineno, linenode.colno, linenode.filename) |
|
self.condition = condition # type: BaseNode |
|
self.block = block # type: CodeBlockNode |
|
|
|
class IfClauseNode(BaseNode): |
|
def __init__(self, linenode: BaseNode): |
|
super().__init__(linenode.lineno, linenode.colno, linenode.filename) |
|
self.ifs = [] # type: T.List[IfNode] |
|
self.elseblock = None # type: T.Union[EmptyNode, CodeBlockNode] |
|
|
|
class UMinusNode(BaseNode): |
|
def __init__(self, current_location: Token, value: BaseNode): |
|
super().__init__(current_location.lineno, current_location.colno, current_location.filename) |
|
self.value = value # type: BaseNode |
|
|
|
class TernaryNode(BaseNode): |
|
def __init__(self, condition: BaseNode, trueblock: BaseNode, falseblock: BaseNode): |
|
super().__init__(condition.lineno, condition.colno, condition.filename) |
|
self.condition = condition # type: BaseNode |
|
self.trueblock = trueblock # type: BaseNode |
|
self.falseblock = falseblock # type: BaseNode |
|
|
|
comparison_map = {'equal': '==', |
|
'nequal': '!=', |
|
'lt': '<', |
|
'le': '<=', |
|
'gt': '>', |
|
'ge': '>=', |
|
'in': 'in', |
|
'notin': 'not in', |
|
} |
|
|
|
# Recursive descent parser for Meson's definition language. |
|
# Very basic apart from the fact that we have many precedence |
|
# levels so there are not enough words to describe them all. |
|
# Enter numbering: |
|
# |
|
# 1 assignment |
|
# 2 or |
|
# 3 and |
|
# 4 comparison |
|
# 5 arithmetic |
|
# 6 negation |
|
# 7 funcall, method call |
|
# 8 parentheses |
|
# 9 plain token |
|
|
|
class Parser: |
|
def __init__(self, code: str, filename: str): |
|
self.lexer = Lexer(code) |
|
self.stream = self.lexer.lex(filename) |
|
self.current = Token('eof', '', 0, 0, 0, (0, 0), None) # type: Token |
|
self.getsym() |
|
self.in_ternary = False |
|
|
|
def getsym(self) -> None: |
|
try: |
|
self.current = next(self.stream) |
|
except StopIteration: |
|
self.current = Token('eof', '', self.current.line_start, self.current.lineno, self.current.colno + self.current.bytespan[1] - self.current.bytespan[0], (0, 0), None) |
|
|
|
def getline(self) -> str: |
|
return self.lexer.getline(self.current.line_start) |
|
|
|
def accept(self, s: str) -> bool: |
|
if self.current.tid == s: |
|
self.getsym() |
|
return True |
|
return False |
|
|
|
def accept_any(self, tids: T.Tuple[str, ...]) -> str: |
|
tid = self.current.tid |
|
if tid in tids: |
|
self.getsym() |
|
return tid |
|
return '' |
|
|
|
def expect(self, s: str) -> bool: |
|
if self.accept(s): |
|
return True |
|
raise ParseException(f'Expecting {s} got {self.current.tid}.', self.getline(), self.current.lineno, self.current.colno) |
|
|
|
def block_expect(self, s: str, block_start: Token) -> bool: |
|
if self.accept(s): |
|
return True |
|
raise BlockParseException(f'Expecting {s} got {self.current.tid}.', self.getline(), self.current.lineno, self.current.colno, self.lexer.getline(block_start.line_start), block_start.lineno, block_start.colno) |
|
|
|
def parse(self) -> CodeBlockNode: |
|
block = self.codeblock() |
|
self.expect('eof') |
|
return block |
|
|
|
def statement(self) -> BaseNode: |
|
return self.e1() |
|
|
|
def e1(self) -> BaseNode: |
|
left = self.e2() |
|
if self.accept('plusassign'): |
|
value = self.e1() |
|
if not isinstance(left, IdNode): |
|
raise ParseException('Plusassignment target must be an id.', self.getline(), left.lineno, left.colno) |
|
assert isinstance(left.value, str) |
|
return PlusAssignmentNode(left.filename, left.lineno, left.colno, left.value, value) |
|
elif self.accept('assign'): |
|
value = self.e1() |
|
if not isinstance(left, IdNode): |
|
raise ParseException('Assignment target must be an id.', |
|
self.getline(), left.lineno, left.colno) |
|
assert isinstance(left.value, str) |
|
return AssignmentNode(left.filename, left.lineno, left.colno, left.value, value) |
|
elif self.accept('questionmark'): |
|
if self.in_ternary: |
|
raise ParseException('Nested ternary operators are not allowed.', |
|
self.getline(), left.lineno, left.colno) |
|
self.in_ternary = True |
|
trueblock = self.e1() |
|
self.expect('colon') |
|
falseblock = self.e1() |
|
self.in_ternary = False |
|
return TernaryNode(left, trueblock, falseblock) |
|
return left |
|
|
|
def e2(self) -> BaseNode: |
|
left = self.e3() |
|
while self.accept('or'): |
|
if isinstance(left, EmptyNode): |
|
raise ParseException('Invalid or clause.', |
|
self.getline(), left.lineno, left.colno) |
|
left = OrNode(left, self.e3()) |
|
return left |
|
|
|
def e3(self) -> BaseNode: |
|
left = self.e4() |
|
while self.accept('and'): |
|
if isinstance(left, EmptyNode): |
|
raise ParseException('Invalid and clause.', |
|
self.getline(), left.lineno, left.colno) |
|
left = AndNode(left, self.e4()) |
|
return left |
|
|
|
def e4(self) -> BaseNode: |
|
left = self.e5() |
|
for nodename, operator_type in comparison_map.items(): |
|
if self.accept(nodename): |
|
return ComparisonNode(operator_type, left, self.e5()) |
|
if self.accept('not') and self.accept('in'): |
|
return ComparisonNode('notin', left, self.e5()) |
|
return left |
|
|
|
def e5(self) -> BaseNode: |
|
return self.e5addsub() |
|
|
|
def e5addsub(self) -> BaseNode: |
|
op_map = { |
|
'plus': 'add', |
|
'dash': 'sub', |
|
} |
|
left = self.e5muldiv() |
|
while True: |
|
op = self.accept_any(tuple(op_map.keys())) |
|
if op: |
|
left = ArithmeticNode(op_map[op], left, self.e5muldiv()) |
|
else: |
|
break |
|
return left |
|
|
|
def e5muldiv(self) -> BaseNode: |
|
op_map = { |
|
'percent': 'mod', |
|
'star': 'mul', |
|
'fslash': 'div', |
|
} |
|
left = self.e6() |
|
while True: |
|
op = self.accept_any(tuple(op_map.keys())) |
|
if op: |
|
left = ArithmeticNode(op_map[op], left, self.e6()) |
|
else: |
|
break |
|
return left |
|
|
|
def e6(self) -> BaseNode: |
|
if self.accept('not'): |
|
return NotNode(self.current, self.e7()) |
|
if self.accept('dash'): |
|
return UMinusNode(self.current, self.e7()) |
|
return self.e7() |
|
|
|
def e7(self) -> BaseNode: |
|
left = self.e8() |
|
block_start = self.current |
|
if self.accept('lparen'): |
|
args = self.args() |
|
self.block_expect('rparen', block_start) |
|
if not isinstance(left, IdNode): |
|
raise ParseException('Function call must be applied to plain id', |
|
self.getline(), left.lineno, left.colno) |
|
assert isinstance(left.value, str) |
|
left = FunctionNode(left.filename, left.lineno, left.colno, self.current.lineno, self.current.colno, left.value, args) |
|
go_again = True |
|
while go_again: |
|
go_again = False |
|
if self.accept('dot'): |
|
go_again = True |
|
left = self.method_call(left) |
|
if self.accept('lbracket'): |
|
go_again = True |
|
left = self.index_call(left) |
|
return left |
|
|
|
def e8(self) -> BaseNode: |
|
block_start = self.current |
|
if self.accept('lparen'): |
|
e = self.statement() |
|
self.block_expect('rparen', block_start) |
|
return e |
|
elif self.accept('lbracket'): |
|
args = self.args() |
|
self.block_expect('rbracket', block_start) |
|
return ArrayNode(args, block_start.lineno, block_start.colno, self.current.lineno, self.current.colno) |
|
elif self.accept('lcurl'): |
|
key_values = self.key_values() |
|
self.block_expect('rcurl', block_start) |
|
return DictNode(key_values, block_start.lineno, block_start.colno, self.current.lineno, self.current.colno) |
|
else: |
|
return self.e9() |
|
|
|
def e9(self) -> BaseNode: |
|
t = self.current |
|
if self.accept('true'): |
|
t.value = True |
|
return BooleanNode(t) |
|
if self.accept('false'): |
|
t.value = False |
|
return BooleanNode(t) |
|
if self.accept('id'): |
|
return IdNode(t) |
|
if self.accept('number'): |
|
return NumberNode(t) |
|
if self.accept('string'): |
|
return StringNode(t) |
|
if self.accept('fstring'): |
|
return FormatStringNode(t) |
|
if self.accept('multiline_fstring'): |
|
return MultilineFormatStringNode(t) |
|
return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) |
|
|
|
def key_values(self) -> ArgumentNode: |
|
s = self.statement() # type: BaseNode |
|
a = ArgumentNode(self.current) |
|
|
|
while not isinstance(s, EmptyNode): |
|
if self.accept('colon'): |
|
a.set_kwarg_no_check(s, self.statement()) |
|
potential = self.current |
|
if not self.accept('comma'): |
|
return a |
|
a.commas.append(potential) |
|
else: |
|
raise ParseException('Only key:value pairs are valid in dict construction.', |
|
self.getline(), s.lineno, s.colno) |
|
s = self.statement() |
|
return a |
|
|
|
def args(self) -> ArgumentNode: |
|
s = self.statement() # type: BaseNode |
|
a = ArgumentNode(self.current) |
|
|
|
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('Dictionary key must be a plain identifier.', |
|
self.getline(), s.lineno, s.colno) |
|
a.set_kwarg(s, self.statement()) |
|
potential = self.current |
|
if not self.accept('comma'): |
|
return a |
|
a.commas.append(potential) |
|
else: |
|
a.append(s) |
|
return a |
|
s = self.statement() |
|
return a |
|
|
|
def method_call(self, source_object: BaseNode) -> MethodNode: |
|
methodname = self.e9() |
|
if not isinstance(methodname, IdNode): |
|
raise ParseException('Method name must be plain id', |
|
self.getline(), self.current.lineno, self.current.colno) |
|
assert isinstance(methodname.value, str) |
|
self.expect('lparen') |
|
args = self.args() |
|
self.expect('rparen') |
|
method = MethodNode(methodname.filename, methodname.lineno, methodname.colno, source_object, methodname.value, args) |
|
if self.accept('dot'): |
|
return self.method_call(method) |
|
return method |
|
|
|
def index_call(self, source_object: BaseNode) -> IndexNode: |
|
index_statement = self.statement() |
|
self.expect('rbracket') |
|
return IndexNode(source_object, index_statement) |
|
|
|
def foreachblock(self) -> ForeachClauseNode: |
|
t = self.current |
|
self.expect('id') |
|
assert isinstance(t.value, str) |
|
varname = t |
|
varnames = [t.value] # type: T.List[str] |
|
|
|
if self.accept('comma'): |
|
t = self.current |
|
self.expect('id') |
|
assert isinstance(t.value, str) |
|
varnames.append(t.value) |
|
|
|
self.expect('colon') |
|
items = self.statement() |
|
block = self.codeblock() |
|
return ForeachClauseNode(varname, varnames, items, block) |
|
|
|
def ifblock(self) -> IfClauseNode: |
|
condition = self.statement() |
|
clause = IfClauseNode(condition) |
|
self.expect('eol') |
|
block = self.codeblock() |
|
clause.ifs.append(IfNode(clause, condition, block)) |
|
self.elseifblock(clause) |
|
clause.elseblock = self.elseblock() |
|
return clause |
|
|
|
def elseifblock(self, clause: IfClauseNode) -> None: |
|
while self.accept('elif'): |
|
s = self.statement() |
|
self.expect('eol') |
|
b = self.codeblock() |
|
clause.ifs.append(IfNode(s, s, b)) |
|
|
|
def elseblock(self) -> T.Union[CodeBlockNode, EmptyNode]: |
|
if self.accept('else'): |
|
self.expect('eol') |
|
return self.codeblock() |
|
return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) |
|
|
|
def line(self) -> BaseNode: |
|
block_start = self.current |
|
if self.current == 'eol': |
|
return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) |
|
if self.accept('if'): |
|
ifblock = self.ifblock() |
|
self.block_expect('endif', block_start) |
|
return ifblock |
|
if self.accept('foreach'): |
|
forblock = self.foreachblock() |
|
self.block_expect('endforeach', block_start) |
|
return forblock |
|
if self.accept('continue'): |
|
return ContinueNode(self.current) |
|
if self.accept('break'): |
|
return BreakNode(self.current) |
|
return self.statement() |
|
|
|
def codeblock(self) -> CodeBlockNode: |
|
block = CodeBlockNode(self.current) |
|
cond = True |
|
while cond: |
|
curline = self.line() |
|
if not isinstance(curline, EmptyNode): |
|
block.lines.append(curline) |
|
cond = self.accept('eol') |
|
return block
|
|
|