Files
Sprinter-SDCC/toolchain/sdbg/macros.py
T

153 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Извлекает авторские логи из активного препроцессорного потока 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