import collections
import re
import string
import sys
import traceback
from typing import Union, Tuple, List, Set, Dict, Pattern
class C90Language:
"""Класс языка C90: лексика"""
# ========================================== Для лексического анализа
# поля, описывающие шаблоны лексем токенов в виде регулярных выражений
multi_line_comment = re.compile(r'/\*.*?\*/', re.S)
single_line_comment = re.compile(r'//.*')
integer = re.compile(r"[+-]?(0[0-7]+|0X[0-9A-F]+|\d+)U?L{,2}(?![0-9A-Z.])",
re.IGNORECASE)
float = re.compile(r"[+-]?(\d+(\.\d*)?(E[+-]?\d+)?|\.\d+(E[+-]?\d+)?)[FL]?(?![0-9A-Z.])",
re.IGNORECASE)
space = re.compile(r"\s+")
character = re.compile(
r"L?\'(\\(['\"?\\abfnrtv]|[0-7]{,3}|x[0-9a-fA-F]{,2}|u[0-9a-fA-F]{,4}|U[0-9a-fA-F]{,8})|[^\\'])\'"
)
string = re.compile(r'L?\".*?\"')
identifier = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*')
preprocessor_directive = re.compile(r'#.*') # обычно обрабатываются препроцессором до лексического анализа
# будем пропускать директивы препроцессора
keywords = { # множество ключевых слов языка
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum',
'extern', 'float', 'for', 'goto', 'if', 'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof',
'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while'
}
@staticmethod
def get_lexical_fsm() -> Dict[str,
Union[str,
dict,
Tuple[str, Pattern],
List[Tuple[str, Pattern]]]]:
"""Возвращает лексику языка в виде конечного автомата"""
# ----- ручное задание конечного автомата
fsm = {
':': '<colon>', ';': '<semicolon>', ',': '<comma>', '~': '<tilde>',
'(': '<open-bracket>', ')': '<close-bracket>',
'[': '<open-square-bracket>', ']': '<close-square-bracket>',
'{': '<open-curly-bracket>', '}': '<close-curly-bracket>',
'>>': '<right-shift>', '>=': '<greater-than-or-equal-to>', '>': '<greater-than>',
'<<': '<left-shift>', '<=': '<less-than-or-equal-to>', '<': '<less-than>',
'--': '<decrement>', '-=': '<minus-equals>', '->': '<pointer-access>', '-': '<minus>',
'++': '<increment>', '+=': '<plus-equals>', '+': '<plus>',
'.': '<dot>', '...': '<ellipsis>',
'&&': '<logical-and>', '&=': '<binary-and-equals>', '&': '<binary-and>',
'||': '<logical-or>', '|=': '<binary-or-equals>', '|': '<binary-or>',
'^=': '<binary-xor-equals>', '^': '<binary-xor>',
'*=': '<mul-equals>', '*': '<multiply-or-pointer>',
'/*': ('<multi-line-comment>', C90Language.multi_line_comment),
'//': ('<single-line-comment>', C90Language.single_line_comment),
'/=': '<div-equals>', '/': '<divide>',
'%=': '<modulus-equals>', '%': '<modulus>',
'==': '<equal-to>', '=': '<assignment>',
'!=': '<not-equals>', '!': '<logical-not>',
'#': ('<preprocessor-directive>', C90Language.preprocessor_directive),
"'": ('<character-constant>', C90Language.character),
'"': ('<string>', C90Language.string),
"L'": ('<character-constant>', C90Language.character),
'L"': ('<string>', C90Language.string)
}
for digit in string.digits:
fsm[digit] = [('<integer-constant>', C90Language.integer), ('<floating-constant>', C90Language.float)]
for letter in string.ascii_letters + '_':
fsm[letter] = ('<identifier>', C90Language.identifier)
for space in string.whitespace:
fsm[space] = ('<whitespace>', C90Language.space)
# ----- алгоритм преобразования структуры в конечный автомат
# -1- удаляем вложенность
q = collections.deque([('', fsm)]) # очередь: (lexeme, token)
fsm = dict() # новый конечный автомат
while q: # пока очередь не пуста
lexeme, token = q.popleft() # получаем из очереди очередной узел
if isinstance(token, dict): # если token -- это промежуточный подузел
for lex, tok in token.items(): # проходим по каждому подузлу промежуточного подузла
q.append((lexeme + lex, tok)) # добавляем в очередь, пример: ('-' + '=', '<minus-equals>')
else: # если token -- это конечный подузел
if fsm.get(lexeme, token) != token: # если уже есть запись по fsm[lexeme], не равная token
raise ULexicalFSMError(f'Invalid: {fsm[lexeme]} != {token} [{fsm}]')
fsm[lexeme] = token # если записи по fsm[lexeme] нет или есть, но равна token
# -2- формируем вложенность
for lexeme, token in list(fsm.items()): # проходим по каждому подузлу
lexeme_len = len(lexeme) # длина лексемы (например, len('-=') -> 2)
if lexeme_len > 1: # если длина лексемы больше 1
current = fsm # current указывает на текущий узел
i = 0 # индекс текущего символа в lexeme
while i < lexeme_len: # пока индекс < длины лексемы
ch = lexeme[i] # получаем i символ
if i == lexeme_len - 1: # если i == последнему индексу
# если уже есть запись по current[ch], не равная token
if current.get(ch, token) != token:
raise ULexicalFSMError(f'Invalid: {current[ch]} != {token} [{current}]')
current[ch] = token # если записи по current[ch] нет или есть, но равна token
elif ch in current: # если i < последний индекс и символ лексемы есть в current
if isinstance(current[ch], dict): # если current[ch] -- это промежуточный узел
current = current[ch] # переходим в него
else: # иначе
i += 1 # переходим к следующему символу
if i == lexeme_len - 1: # если символов не осталось, записываем как str/tuple
current[ch] = {lexeme[i]: token, '': current[ch]}
else: # иначе записываем как dict и переходим в него
d = dict()
current[ch] = {lexeme[i]: d, '': current[ch]}
current = d
else: # если i < последний индекс и символ лексемы отсутствует в current
if i >= lexeme_len - 1: # если символов не осталось, записываем как str/tuple
current[ch] = token
else: # иначе записываем как dict и переходим в него
d = dict()
current[ch] = d
current = d
i += 1 # переходим к следующему символу
fsm.pop(lexeme) # удаляем соответствие lexeme (длина > 1) -> token
return fsm
@staticmethod
def get_keywords() -> Set[str]:
"""Возвращает множество ключевых слов языка"""
return set(C90Language.keywords)
class ULexicalError(ValueError):
"""Исключение возникает из-за лексической ошибки"""
def __init__(self, **kwargs):
self.params = {k: repr(v) if isinstance(v, str) else v for k, v in kwargs.items()
if isinstance(v, (int, float)) or v}
super().__init__()
def __str__(self):
return '[' + ' | '.join(f"{name}: {p}" for name, p in self.params.items()) + ']'
class ULexicalFSMError(TypeError):
"""Исключение возникает из-за ошибки в описании лексики языка в виде конечного автомата"""
pass # не требует реализации; функциональность = TypeError
def lexical_analyzer(lexical_fsm: Dict[str,
Union[str,
dict,
Tuple[str, Pattern],
List[Tuple[str, Pattern]]]],
keywords: Union[List[str], Set[str]],
code: str) -> List[Tuple[str, str]]:
"""
Лексер анализирует код согласно лексике языка и возвращает список токенов и лексем, либо выбрасывает исключение
:param lexical_fsm: лексика языка в виде конечного автомата
:param keywords: список/множество ключевых слов языка
:param code: код для лексического анализа
:return: список токенов и лексем; например, [..., ('<identifier>', 'Animal'), ...]
"""
# проверка lexical_fsm -- лексики языка в виде конечного автомата
q = collections.deque(lexical_fsm.items()) # очередь для проверки
while q:
tuple_item = q.popleft()
if not isinstance(tuple_item, tuple): # tuple ключ:значение
raise ULexicalFSMError(f"Invalid type: {tuple_item} [{type(tuple_item)}] (must be tuple)")
key, value = tuple_item
if not isinstance(key, str): # ключ должен быть строкой
raise ULexicalFSMError(f"Invalid type: {repr(key)} [{type(key)}] (must be str)")
if len(key) > 1: # длина ключа должна быть <= 1
raise ULexicalFSMError(f"Length of string must be <= 1, got {repr(key)} with len = {len(key)}")
if not isinstance(value, (str, tuple, list, dict)): # значение может быть только str / tuple / list / dict
raise ULexicalFSMError(f"Invalid type: {value} [{type(value)}] (must be str / tuple / list / dict)")
if isinstance(value, (tuple, list)): # если значение tuple / list
tuples = [value] if isinstance(value, tuple) else value # создаем list
if len(tuples) == 0: # длина списка должна быть > 0
raise ULexicalFSMError(f"Number of values in list must be >= 1, got: {tuples}")
for tuple_item in tuples: # для каждого кортежа в списке
if len(tuple_item) != 2: # длина кортежа должна быть == 2
raise ULexicalFSMError(f'Number of values in tuple must be 2, got: {tuple_item}')
key, value = tuple_item
if not isinstance(key, str): # ключ должен быть str
raise ULexicalFSMError(f"Invalid type: {repr(key)} [{type(key)}] (must be str)")
if not isinstance(value, Pattern): # значение должно быть Pattern [re.compile(...)]
raise ULexicalFSMError(f"Invalid type: {repr(value)} [{type(value)}] (must be Pattern)")
elif isinstance(value, dict): # в случае словаря -- добавляем его элементы в очередь
q.extend(value.items())
# лексический анализ
tokens_and_lexemes = [] # список токенов и лексем
current_state = lexical_fsm # текущее состояние
p, i, n = 0, 0, len(code) # предыдущий индекс, текущий индекс и длина code
token, lexeme = '', '' # токен и лексема
line = 1 # текущий номер строки кода
while i < n: # пока текущий индекс < длины code
r = current_state.get(code[i]) # переходим к следующему состоянию конечного автомата
if r is None: # если следующего состояния нет, то смотрим состояние "по умолчанию"
r = current_state.get('') # пробуем перейти в него
i -= 1 # был считан один лишний символ (по индексу i), поэтому отнимаем 1
if r is None: # если нужной ветки нет, то получена неверная лексема
i += 1 # увеличиваем на 1, чтобы получить индекс неверной лексемы
prev_n = code[:i].rfind('\n') + 1 # начало строки
next_n = code[i:].find('\n') + i # конец строки
raise ULexicalError(code_startswith=code[prev_n if prev_n != -1 else i:next_n if next_n != -1 else i + 1],
symbol=code[i], line=line)
if isinstance(r, str): # если символ-атом
i += 1 # переход к следующему символу
token = r
lexeme = repr(code[p:i])[1:-1]
p = i # обновляем предыдущий индекс
line += code[p:i].count('\n') # обновляем номер текущей строки
elif isinstance(r, (tuple, list)): # если регулярное выражение
if not isinstance(r, list):
r = [r]
for attempt, t in enumerate(r, 1): # r заведомо не пустой
token, token_regexp = t
match = re.match(token_regexp, code[p:]) # поиск по регулярному выражению
if not match and attempt == len(r): # если неудачный поиск на последней попытке
pos = code[p:].find('\n')
startswith = code[p:(p + pos + 1 if pos != -1 else None)]
raise ULexicalError(code_startswith=startswith, line=line, regexp=token_regexp)
elif not match: # если неудачный поиск на текущей попытке, то переход к следующей
continue
match_len = match.span(0)[1] # если поиск удачный, получаем число найденных символов
i = p + match_len # обновляем текущую позицию
lexeme = code[p:i]
p = i # обновляем предыдущий индекс
if lexeme in keywords: # если лексема есть в множестве ключевых слов языка
token = lexeme
line += lexeme.count('\n') # обновляем номер текущей строки
break
else: # если несколько веток (dict)
i += 1 # один символ был считан, поэтому добавляем 1
current_state = r # переходим к следующему состоянию
continue
tokens_and_lexemes.append((token, lexeme)) # добавляем токен и лексему
current_state = lexical_fsm # текущее состояние -- начальное состояние
return tokens_and_lexemes
def main(code):
def print_center(*, message, width, is_start=True, end='\n'):
"""Печатает текст в центре согласно общей ширине width"""
print('=' * width * bool(is_start),
message.center(width, '='),
'=' * width * (not bool(is_start)), sep='\n', end=end)
# инициализация
lexical_fsm = C90Language.get_lexical_fsm() # лексика языка в виде конечного автомата
keywords = C90Language.get_keywords() # множество ключевых слов языка
maximum_left_indent = max(map(len, re.findall(r'\'([\w\-<>]+)\'', str(lexical_fsm)))) # макс. длина токена
format_function = ('{:>' + str(maximum_left_indent) + '} |\'{}\'|').format # функция для форматирования
# лексический анализ
print_center(message=' Lexical analysis ', width=maximum_left_indent * 2, is_start=True, end='')
try:
tokens_and_lexemes = lexical_analyzer(lexical_fsm=lexical_fsm, keywords=keywords, code=code)
print(*(format_function(token, repr(lexeme)[1:-1]) for token, lexeme in tokens_and_lexemes),
sep='\n', end='')
print_center(message=' Success ', width=maximum_left_indent * 2, is_start=False, end='\n\n')
except ULexicalError as e:
print('LexicalError:', e, end='')
print_center(message=' Error ', width=maximum_left_indent * 2, is_start=False, end='\n\n')
return
except Exception:
print(traceback.format_exc(), end='')
print_center(message=' Error ', width=maximum_left_indent * 2, is_start=False, end='\n\n')
return
if __name__ == "__main__":
if len(sys.argv) > 1:
try:
with open(sys.argv[-1], 'r') as fp:
main(fp.read())
except Exception as e:
print(*e.args, sep=', ')
else:
print(f'python3.6 main.py <filename>')