153 lines
6.8 KiB
Python
153 lines
6.8 KiB
Python
"""Извлекает авторские логи из активного препроцессорного потока SDCC."""
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import bisect
|
||
import re
|
||
import string
|
||
from pathlib import Path
|
||
|
||
_DIRECTIVE = re.compile(r'^#(?:line)?\s+(\d+)\s+"([^"]+)"')
|
||
_CALL = re.compile(r'\bSDBG_METADATA_(LOGIF|LOG)\s*\(')
|
||
_IDENT = re.compile(r'[A-Za-z_][A-Za-z_0-9]*\Z')
|
||
_LITERAL = re.compile(r'"(?:\\.|[^"\\])*"')
|
||
|
||
|
||
def _calls(text: str):
|
||
"""Находит metadata-вызовы вне строк/символьных литералов C."""
|
||
position = 0
|
||
while position < len(text):
|
||
char = text[position]
|
||
if char in ('"', "'"):
|
||
quote = char
|
||
position += 1
|
||
while position < len(text):
|
||
if text[position] == '\\': position += 2
|
||
elif text[position] == quote:
|
||
position += 1
|
||
break
|
||
else: position += 1
|
||
continue
|
||
match = _CALL.match(text, position)
|
||
if match:
|
||
yield match
|
||
position = match.end()
|
||
else:
|
||
position += 1
|
||
|
||
|
||
def _arguments(text: str, start: int) -> list[str]:
|
||
"""Делит аргументы вызова после `(`, не путая запятые в строках."""
|
||
args, begin, depth, quote, escape = [], start, 1, False, False
|
||
for position in range(start, len(text)):
|
||
char = text[position]
|
||
if quote:
|
||
if escape: escape = False
|
||
elif char == '\\': escape = True
|
||
elif char == '"': quote = False
|
||
continue
|
||
if char == '"': quote = True
|
||
elif char == '(': depth += 1
|
||
elif char == ')':
|
||
depth -= 1
|
||
if depth == 0:
|
||
args.append(text[begin:position].strip())
|
||
return args
|
||
elif char == ',' and depth == 1:
|
||
args.append(text[begin:position].strip())
|
||
begin = position + 1
|
||
raise ValueError('Незакрытый вызов SDBG_LOG в препроцессорном потоке')
|
||
|
||
|
||
def _message(value: str) -> str:
|
||
literals = []
|
||
position = 0
|
||
while position < len(value):
|
||
while position < len(value) and value[position].isspace(): position += 1
|
||
if position == len(value): break
|
||
match = _LITERAL.match(value, position)
|
||
if not match:
|
||
raise ValueError('SDBG_LOG: сообщение должно быть строковым литералом')
|
||
try:
|
||
decoded = ast.literal_eval(match.group())
|
||
except (ValueError, SyntaxError) as error:
|
||
raise ValueError('SDBG_LOG: неверный строковый литерал') from error
|
||
if not isinstance(decoded, str):
|
||
raise ValueError('SDBG_LOG: нужен обычный строковый литерал')
|
||
literals.append(decoded)
|
||
position = match.end()
|
||
message = ''.join(literals)
|
||
if not message or len(message) > 1024:
|
||
raise ValueError('SDBG_LOG: сообщение должно содержать 1..1024 символа')
|
||
validate_log_message(message)
|
||
return message
|
||
|
||
|
||
def validate_log_message(message: str) -> None:
|
||
"""Одна грамматика для C-макроса и DAP logMessage."""
|
||
if not isinstance(message, str) or not message or len(message) > 1024:
|
||
raise ValueError('logMessage должен содержать 1..1024 символа')
|
||
try:
|
||
fields = list(string.Formatter().parse(message))
|
||
except ValueError as error:
|
||
raise ValueError('Неверные фигурные скобки logMessage') from error
|
||
for _, name, spec, conversion in fields:
|
||
if name is not None and (not _IDENT.fullmatch(name) or spec or conversion):
|
||
raise ValueError('В logMessage разрешены только подстановки {variable}')
|
||
|
||
|
||
def extract(preprocessed: str, dependencies: set[str]) -> list[dict]:
|
||
"""Возвращает активные вызовы и проверенные исходные пути/строки."""
|
||
lines = preprocessed.splitlines(keepends=True)
|
||
offsets, indexed = [], []
|
||
offset, filename, number = 0, None, 0
|
||
for line in lines:
|
||
offsets.append(offset)
|
||
directive = _DIRECTIVE.match(line)
|
||
if directive:
|
||
filename, number = directive[2], int(directive[1])
|
||
indexed.append((None, 0))
|
||
else:
|
||
indexed.append((filename, number))
|
||
number += 1
|
||
offset += len(line)
|
||
found, tags = [], set()
|
||
for match in _calls(preprocessed):
|
||
index = bisect.bisect_right(offsets, match.start()) - 1
|
||
filename, number = indexed[index]
|
||
if not filename or number < 1:
|
||
raise ValueError('SDBG_LOG: препроцессор не сохранил позицию исходника')
|
||
source = str(Path(filename).resolve())
|
||
if source not in dependencies:
|
||
raise ValueError('SDBG_LOG: вызов вне проверенных исходников: ' + source)
|
||
args = _arguments(preprocessed, match.end())
|
||
expected = 3 if match[1] == 'LOGIF' else 2
|
||
if len(args) != expected:
|
||
raise ValueError('SDBG_LOG: неверное число аргументов')
|
||
tag = args[0]
|
||
if not _IDENT.fullmatch(tag) or tag in tags:
|
||
raise ValueError('SDBG_LOG: tag должен быть уникальным идентификатором TU: ' + tag)
|
||
tags.add(tag)
|
||
condition = args[1] if expected == 3 else None
|
||
if condition is not None and not _IDENT.fullmatch(condition):
|
||
raise ValueError('SDBG_LOGIF: пока поддержано только имя переменной')
|
||
found.append({'tag': tag, 'message': _message(args[-1]),
|
||
'condition': condition, 'source': source, 'line': number})
|
||
return found
|
||
|
||
|
||
def anchor_symbols(assembly: str, module: str, macros: list[dict]) -> tuple[str, list[dict]]:
|
||
"""Уникализирует asm-символы TU и требует ровно один якорь на macro tag."""
|
||
descriptions = []
|
||
for macro in macros:
|
||
old = '_spr_sdbg_log_' + macro['tag']
|
||
new = '_spr_sdbg_log_' + module + '_' + macro['tag']
|
||
definition = re.compile(r'(?m)^(\s*)' + re.escape(old) + r'(\s*=\s*\.\s*)$')
|
||
declaration = re.compile(r'(?m)^(\s*\.globl\s+)' + re.escape(old) + r'(\s*)$')
|
||
if len(definition.findall(assembly)) != 1 or len(declaration.findall(assembly)) != 1:
|
||
raise ValueError('SDBG_LOG: якорь отсутствует или развёрнут повторно: ' + macro['tag'])
|
||
assembly = definition.sub(lambda m: m[1] + new + m[2], assembly)
|
||
assembly = declaration.sub(lambda m: m[1] + new + m[2], assembly)
|
||
descriptions.append({**macro, 'symbol': new})
|
||
return assembly, descriptions
|