"""Извлекает авторские логи из активного препроцессорного потока 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