Sprinter: добавить отладку C-исходников и интеграцию VS Code
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""Извлекает авторские логи из активного препроцессорного потока 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
|
||||
Reference in New Issue
Block a user