Sprinter: добавить отладку C-исходников и интеграцию VS Code

This commit is contained in:
2026-09-15 17:58:41 +03:00
parent 50c6e56b7b
commit e4695b8281
62 changed files with 7147 additions and 27 deletions
+152
View File
@@ -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