[ADD] dbg2lsp.py: AutoLISP-Funktionen automatisch mit ssg_dbg instrumentieren
Neues Entwickler-Tool (bin/dbg2lsp.bat + lib/dbg2lsp.py), das das manuelle Einfuegen von dbgf/dbg/dbgreturn/dbgopen/dbgclose automatisiert: - --method NAME: dbgf + dbg je Parameter + dbgreturn um die letzte Rumpf-Form - --recursive: verfolgt den Aufrufgraphen ueber Dateigrenzen hinweg (Builtins werden automatisch uebersprungen, da sie kein defun im Suchpfad haben) - --add-open DATEI METHODE: dbgopen/dbgclose fuer einen Einstiegspunkt (z.B. ein c:BEFEHL-Kommando), inkl. aller (exit)-Stellen Dokumentiert in doc/dbg2lsp.md, Kurzeintrag in CLAUDE.md. Ausserdem: set_attributs.py aus doc/tools.md und CLAUDE.md entfernt - das Skript existiert nicht mehr in lib/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+725
@@ -0,0 +1,725 @@
|
||||
"""
|
||||
Instrumentiert AutoLISP-Funktionen (.lsp) automatisch mit den ssg_dbg.lsp-
|
||||
Routinen (dbgf, dbg, dbgreturn, dbgopen, dbgclose) - so wie es beim manuellen
|
||||
Debuggen (siehe z.B. Lisp/KreiselInsert.lsp::ils-eckrad-insert) bereits
|
||||
gemacht wird.
|
||||
|
||||
Fuer eine gegebene Funktion (--method) wird eingefuegt:
|
||||
- (dbgf "funktionsname") direkt als erste Form im Rumpf
|
||||
- (dbg 'parameter) je Eingabeparameter (die Locals nach "/" NICHT)
|
||||
- (dbgreturn <letzte Rumpf-Form>) die letzte Form des Rumpfs wird
|
||||
eingeklammert, damit ihr Rueckgabe-
|
||||
wert mitgeloggt wird
|
||||
|
||||
Mit --recursive wird zusaetzlich jede Funktion instrumentiert, die im Rumpf
|
||||
(auch verschachtelt, aber NICHT innerhalb von 'quotierten Listen) aufgerufen
|
||||
wird - vorausgesetzt, ihr (defun ...) laesst sich in einem der --search-dir-
|
||||
Verzeichnisse finden (Bibliotheks-/AutoLISP-Builtins wie princ, command,
|
||||
strcat etc. haben dort kein defun und werden dadurch automatisch NICHT
|
||||
angefasst). Der Aufrufgraph wird rekursiv weiterverfolgt (mit --max-depth
|
||||
begrenzt), einmal besuchte Funktionen werden nicht doppelt instrumentiert.
|
||||
|
||||
Mit --add-open DATEI METHODE wird in der angegebenen Methode zusaetzlich
|
||||
- (dbgopen "<dbg-datei>" "<envvar>") als erste Rumpf-Form
|
||||
- (dbgclose) vor jedem (exit)-Aufruf im Rumpf
|
||||
UND am Ende des Rumpfs (falls die
|
||||
letzte Form nicht bereits (exit) ist)
|
||||
eingefuegt - fuer den Einstiegspunkt (typischerweise ein c:BEFEHL-Kommando),
|
||||
der die .dbg-Datei fuer einen ganzen Testlauf oeffnen/schliessen soll.
|
||||
--add-open ist unabhaengig von --method/--recursive nutzbar und mehrfach
|
||||
angebbar.
|
||||
|
||||
Aufruf-Beispiele:
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-separator-one
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --add-open tests/test_mubea.lsp c:TEST_MUBEA
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive \\
|
||||
--add-open tests/test_mubea.lsp c:TEST_MUBEA --dry-run
|
||||
|
||||
Details, Grenzen und weitere Beispiele: siehe doc/dbg2lsp.md
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ssg_dbg.lsp-Funktionen, die NIE instrumentiert oder in den Aufrufgraphen
|
||||
# aufgenommen werden duerfen - sonst wuerde --recursive versuchen, dbgf
|
||||
# selbst mit dbgf zu instrumentieren, sobald es auf eine bereits (manuell
|
||||
# oder durch einen frueheren Lauf) instrumentierte Funktion trifft.
|
||||
# ---------------------------------------------------------------------------
|
||||
DBG_FRAMEWORK_NAMES = {
|
||||
"dbgf", "dbg", "dbgp", "dbgmsg", "dbgreturn", "dbgopen", "dbgclose",
|
||||
"dbgflush", "dbgon", "dbgoff",
|
||||
"dbg-flush", "dbg-bufwrite", "dbg-tabs", "dbg-tostring", "dbg-list-tostring",
|
||||
}
|
||||
|
||||
DEFAULT_DBG_ENVVAR = "DXFM_LOG"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zeichenweises Scannen mit String-/Kommentar-Bewusstsein (identische Logik
|
||||
# wie beim manuellen Klammer-Balance-Check waehrend der Mubea-Instrumentierung)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LispSyntaxError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _skip_string(text, i):
|
||||
"""i zeigt auf das oeffnende '\"'. Gibt den Index NACH dem schliessenden '\"' zurueck."""
|
||||
n = len(text)
|
||||
i += 1
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if c == '"':
|
||||
return i + 1
|
||||
i += 1
|
||||
raise LispSyntaxError("Nicht geschlossener String ab Position %d" % i)
|
||||
|
||||
|
||||
def _skip_line_comment(text, i):
|
||||
nl = text.find("\n", i)
|
||||
return len(text) if nl == -1 else nl + 1
|
||||
|
||||
|
||||
def find_matching_paren(text, open_idx):
|
||||
"""open_idx zeigt auf '('. Gibt den Index der zugehoerigen ')' zurueck."""
|
||||
n = len(text)
|
||||
if text[open_idx] != "(":
|
||||
raise LispSyntaxError("Position %d ist keine '('" % open_idx)
|
||||
depth = 0
|
||||
i = open_idx
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
i = _skip_string(text, i)
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
if c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
i += 1
|
||||
raise LispSyntaxError("Keine schliessende Klammer fuer '(' ab Position %d gefunden" % open_idx)
|
||||
|
||||
|
||||
def _skip_ws_and_comments(text, i):
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c in " \t\r\n":
|
||||
i += 1
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
break
|
||||
return i
|
||||
|
||||
|
||||
_TOKEN_STOP_CHARS = set(" \t\r\n()\";'")
|
||||
|
||||
|
||||
def read_next_token(text, i, limit):
|
||||
"""Liest ab i (nach Whitespace/Kommentaren) das naechste Token (Symbol/Zahl)
|
||||
bis zum naechsten Trennzeichen. Gibt (token, end_index) oder (None, i) zurueck,
|
||||
wenn an Position i eine '(' / '"' / EOF folgt (kein einfaches Token)."""
|
||||
i = _skip_ws_and_comments(text, i)
|
||||
if i >= limit:
|
||||
return None, i
|
||||
if text[i] in "(\"'":
|
||||
return None, i
|
||||
start = i
|
||||
while i < limit and text[i] not in _TOKEN_STOP_CHARS:
|
||||
i += 1
|
||||
if i == start:
|
||||
return None, i
|
||||
return text[start:i], i
|
||||
|
||||
|
||||
def iter_top_level_spans(text, start, end):
|
||||
"""Liefert (form_start, form_end) fuer jede balancierte Top-Level-Form
|
||||
zwischen start und end (end exklusiv, typischerweise die Position der
|
||||
schliessenden ')' des umgebenden defun). Reine Atome (Symbole/Zahlen ohne
|
||||
umschliessende Klammer) werden ebenfalls als Form erkannt."""
|
||||
spans = []
|
||||
i = start
|
||||
while i < end:
|
||||
i = _skip_ws_and_comments(text, i)
|
||||
if i >= end:
|
||||
break
|
||||
c = text[i]
|
||||
if c == "(":
|
||||
close = find_matching_paren(text, i)
|
||||
spans.append((i, close + 1))
|
||||
i = close + 1
|
||||
elif c == "'":
|
||||
# Quotiertes Atom/Liste - als eine Form ab dem Apostroph behandeln.
|
||||
j = i + 1
|
||||
if j < end and text[j] == "(":
|
||||
close = find_matching_paren(text, j)
|
||||
spans.append((i, close + 1))
|
||||
i = close + 1
|
||||
else:
|
||||
tok, tend = read_next_token(text, j, end)
|
||||
spans.append((i, tend))
|
||||
i = tend
|
||||
elif c == '"':
|
||||
send = _skip_string(text, i)
|
||||
spans.append((i, send))
|
||||
i = send
|
||||
else:
|
||||
tok, tend = read_next_token(text, i, end)
|
||||
if tok is None:
|
||||
i += 1
|
||||
else:
|
||||
spans.append((i, tend))
|
||||
i = tend
|
||||
return spans
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (defun ...) - Erkennung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class DefunInfo:
|
||||
def __init__(self, name, path, defun_start, defun_end,
|
||||
arglist_start, arglist_end, body_start, body_end,
|
||||
params, locals_):
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.defun_start = defun_start
|
||||
self.defun_end = defun_end # Position NACH der schliessenden ')' des defun
|
||||
self.arglist_start = arglist_start
|
||||
self.arglist_end = arglist_end
|
||||
self.body_start = body_start
|
||||
self.body_end = body_end # Position der schliessenden ')' des defun (exklusiv)
|
||||
self.params = params
|
||||
self.locals = locals_
|
||||
|
||||
def __repr__(self):
|
||||
return "DefunInfo(%r in %s)" % (self.name, self.path)
|
||||
|
||||
|
||||
def _parse_arglist(text, arglist_start, arglist_end):
|
||||
"""arglist_start zeigt auf '(' der Parameterliste, arglist_end auf die
|
||||
zugehoerige ')'. Gibt (params, locals) zurueck - locals ist alles nach
|
||||
einem "/"-Trenner (leer, falls keiner vorhanden)."""
|
||||
params = []
|
||||
locals_ = []
|
||||
seen_slash = False
|
||||
i = arglist_start + 1
|
||||
while i < arglist_end:
|
||||
tok, tend = read_next_token(text, i, arglist_end)
|
||||
if tok is None:
|
||||
# z.B. eine verschachtelte Liste als Defaultwert (in AutoLISP
|
||||
# eigentlich nicht ueblich) - ueberspringen statt abzubrechen.
|
||||
i = _skip_ws_and_comments(text, i)
|
||||
if i < arglist_end and text[i] == "(":
|
||||
i = find_matching_paren(text, i) + 1
|
||||
continue
|
||||
i += 1
|
||||
continue
|
||||
if tok == "/":
|
||||
seen_slash = True
|
||||
elif seen_slash:
|
||||
locals_.append(tok)
|
||||
else:
|
||||
params.append(tok)
|
||||
i = tend
|
||||
return params, locals_
|
||||
|
||||
|
||||
def find_defuns_in_text(text, path):
|
||||
"""Findet alle (defun NAME (...) ...) Top-Level-Definitionen im Text."""
|
||||
defuns = []
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
i = _skip_string(text, i)
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
if c == "(":
|
||||
close = find_matching_paren(text, i)
|
||||
head_tok, head_end = read_next_token(text, i + 1, close)
|
||||
if head_tok is not None and head_tok.lower() == "defun":
|
||||
name_tok, name_end = read_next_token(text, head_end, close)
|
||||
if name_tok:
|
||||
al_start = _skip_ws_and_comments(text, name_end)
|
||||
if al_start < close and text[al_start] == "(":
|
||||
al_end = find_matching_paren(text, al_start)
|
||||
params, locals_ = _parse_arglist(text, al_start, al_end)
|
||||
body_start = al_end + 1
|
||||
defuns.append(DefunInfo(
|
||||
name=name_tok, path=path,
|
||||
defun_start=i, defun_end=close + 1,
|
||||
arglist_start=al_start, arglist_end=al_end,
|
||||
body_start=body_start, body_end=close,
|
||||
params=params, locals_=locals_,
|
||||
))
|
||||
i = close + 1
|
||||
continue
|
||||
i += 1
|
||||
return defuns
|
||||
|
||||
|
||||
def build_defun_index(paths):
|
||||
"""path -> Dateitext (einmal eingelesen) und name.lower() -> DefunInfo
|
||||
(erster Treffer gewinnt; mehrfach definierte Namen sind in dieser
|
||||
Code-Basis nicht vorgesehen)."""
|
||||
file_texts = {}
|
||||
index = {}
|
||||
for path in paths:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
file_texts[path] = text
|
||||
for d in find_defuns_in_text(text, path):
|
||||
index.setdefault(d.name.lower(), d)
|
||||
return file_texts, index
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Aufruf-Erkennung (fuer --recursive und --add-open/exit-Suche)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def scan_calls(text, start, end):
|
||||
"""Liefert eine Liste von (head_name, call_start, call_end) fuer jede
|
||||
NICHT quotierte Aufruf-Form (...) im Bereich [start, end) - beliebig
|
||||
verschachtelt, aber nicht innerhalb von 'quotierten Listen (deren Inhalt
|
||||
ist reine Datenliste, dort kann nichts "aufgerufen" werden)."""
|
||||
calls = []
|
||||
|
||||
def walk(s, e):
|
||||
i = s
|
||||
while i < e:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
i = _skip_string(text, i)
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
if c == "'":
|
||||
j = i + 1
|
||||
if j < e and text[j] == "(":
|
||||
# quotierte Liste: reine Daten, nicht weiter absteigen
|
||||
close = find_matching_paren(text, j)
|
||||
i = close + 1
|
||||
continue
|
||||
i = j
|
||||
continue
|
||||
if c == "(":
|
||||
close = find_matching_paren(text, i)
|
||||
head_tok, head_end = read_next_token(text, i + 1, close)
|
||||
if head_tok and head_tok[0] not in "0123456789.\"'":
|
||||
calls.append((head_tok, i, close + 1))
|
||||
walk(i + 1, close)
|
||||
i = close + 1
|
||||
continue
|
||||
i += 1
|
||||
|
||||
walk(start, end)
|
||||
return calls
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen fuer Einfuegetexte
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _line_indent(text, pos):
|
||||
"""Fuehrender Whitespace der Zeile, in der pos liegt (fuer die Einrueckung
|
||||
neu eingefuegter Debug-Zeilen)."""
|
||||
nl = text.rfind("\n", 0, pos)
|
||||
line_start = nl + 1
|
||||
i = line_start
|
||||
while i < pos and text[i] in " \t":
|
||||
i += 1
|
||||
indent = text[line_start:i]
|
||||
return indent if indent else " "
|
||||
|
||||
|
||||
def _body_indent(text, body_start, body_end):
|
||||
"""Einrueckung der ERSTEN echten Rumpf-Form. NICHT einfach _line_indent
|
||||
auf body_start anwenden: bei mehrzeiligen Parameterlisten (z.B.
|
||||
"(defun foo (a b /\n c d)" - "/"-Locals auf eigener Zeile) liegt
|
||||
body_start mitten in der Fortsetzungszeile der Parameterliste, deren
|
||||
Ausrichtung nichts mit der eigentlichen Rumpf-Einrueckung zu tun hat."""
|
||||
first = _skip_ws_and_comments(text, body_start)
|
||||
if first >= body_end:
|
||||
return " "
|
||||
return _line_indent(text, first)
|
||||
|
||||
|
||||
def _already_has_call(text, body_start, body_end, head_name):
|
||||
spans = iter_top_level_spans(text, body_start, body_end)
|
||||
if not spans:
|
||||
return False
|
||||
fs, fe = spans[0]
|
||||
if text[fs] != "(":
|
||||
return False
|
||||
tok, _ = read_next_token(text, fs + 1, fe)
|
||||
return bool(tok) and tok.lower() == head_name.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Edit-Sammlung: (position, prioritaet, text) - reine Einfuegungen, angewandt
|
||||
# in absteigender Position (und bei Gleichstand nach Prioritaet), damit
|
||||
# fruehere Positionen gueltig bleiben.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EditSet:
|
||||
def __init__(self):
|
||||
self.edits = {} # path -> list[(pos, prio, text)]
|
||||
|
||||
def insert(self, path, pos, prio, text):
|
||||
self.edits.setdefault(path, []).append((pos, prio, text))
|
||||
|
||||
def apply(self, file_texts):
|
||||
"""Gibt {path: neuer_text} fuer alle Dateien mit mindestens einer
|
||||
Einfuegung zurueck."""
|
||||
result = {}
|
||||
for path, items in self.edits.items():
|
||||
text = file_texts[path]
|
||||
# Bei gleicher Position: kleinere Prioritaet zuerst im Ergebnis-
|
||||
# text (prio dient als "vor/nach"-Reihenfolge an derselben Stelle).
|
||||
items_sorted = sorted(items, key=lambda t: (t[0], t[1]))
|
||||
out = []
|
||||
last = 0
|
||||
# Gruppieren nach Position, damit mehrere Einfuegungen an
|
||||
# derselben Stelle in Prioritaets-Reihenfolge zusammenkommen.
|
||||
grouped = {}
|
||||
order = []
|
||||
for pos, prio, ins_text in items_sorted:
|
||||
if pos not in grouped:
|
||||
grouped[pos] = []
|
||||
order.append(pos)
|
||||
grouped[pos].append(ins_text)
|
||||
for pos in order:
|
||||
out.append(text[last:pos])
|
||||
out.extend(grouped[pos])
|
||||
last = pos
|
||||
out.append(text[last:])
|
||||
result[path] = "".join(out)
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Instrumentierung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def instrument_method(defun, file_texts, edits, force=False, log=print):
|
||||
"""Fuegt (dbgf ...), (dbg 'param) je Parameter und (dbgreturn ...) um die
|
||||
letzte Rumpf-Form ein."""
|
||||
text = file_texts[defun.path]
|
||||
|
||||
if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgf"):
|
||||
log(" [skip] %s (%s) ist bereits instrumentiert (dbgf vorhanden) - --force zum Erzwingen" %
|
||||
(defun.name, defun.path))
|
||||
return False
|
||||
|
||||
indent = _body_indent(text, defun.body_start, defun.body_end)
|
||||
lines = ['(dbgf "%s")' % defun.name]
|
||||
for p in defun.params:
|
||||
lines.append("(dbg '%s)" % p)
|
||||
# Kein Trailing-Newline/-Indent: body_start hat im Original bereits sein
|
||||
# eigenes "\n<indent>" vor der ersten Rumpf-Form, das bleibt unveraendert
|
||||
# stehen und schliesst direkt an die letzte hier eingefuegte Zeile an.
|
||||
insertion = "\n" + "\n".join(indent + l for l in lines)
|
||||
edits.insert(defun.path, defun.body_start, 10, insertion)
|
||||
|
||||
spans = iter_top_level_spans(text, defun.body_start, defun.body_end)
|
||||
if spans:
|
||||
last_start, last_end = spans[-1]
|
||||
edits.insert(defun.path, last_start, 20, "(dbgreturn ")
|
||||
edits.insert(defun.path, last_end, 21, ")")
|
||||
else:
|
||||
log(" [warn] %s (%s) hat einen leeren Rumpf - kein dbgreturn eingefuegt" %
|
||||
(defun.name, defun.path))
|
||||
|
||||
log(" [ok] %s (%s): dbgf + %d x dbg + dbgreturn" %
|
||||
(defun.name, defun.path, len(defun.params)))
|
||||
return True
|
||||
|
||||
|
||||
def instrument_add_open(defun, file_texts, edits, dbg_file, dbg_envvar, force=False, log=print):
|
||||
"""Fuegt (dbgopen "dbg_file" "dbg_envvar") als erste Rumpf-Form ein sowie
|
||||
(dbgclose) vor jedem (exit)-Aufruf und (sofern die letzte Rumpf-Form nicht
|
||||
selbst ein (exit) ist) am Ende des Rumpfs."""
|
||||
text = file_texts[defun.path]
|
||||
|
||||
if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgopen"):
|
||||
log(" [skip] %s (%s) hat bereits (dbgopen ...) - --force zum Erzwingen" %
|
||||
(defun.name, defun.path))
|
||||
return False
|
||||
|
||||
indent = _body_indent(text, defun.body_start, defun.body_end)
|
||||
# Kein Trailing-Newline/-Indent (siehe instrument_method): die naechste
|
||||
# Einfuegung an derselben Position (z.B. dbgf durch --method) oder das
|
||||
# Original selbst liefert das fuehrende "\n<indent>" bereits mit.
|
||||
open_line = "\n" + indent + '(dbgopen "%s" "%s")' % (dbg_file, dbg_envvar)
|
||||
edits.insert(defun.path, defun.body_start, 5, open_line)
|
||||
|
||||
calls = scan_calls(text, defun.body_start, defun.body_end)
|
||||
exit_calls = [c for c in calls if c[0].lower() == "exit"]
|
||||
for _, cstart, _cend in exit_calls:
|
||||
cindent = _line_indent(text, cstart)
|
||||
edits.insert(defun.path, cstart, 15, "(dbgclose)\n" + cindent)
|
||||
|
||||
spans = iter_top_level_spans(text, defun.body_start, defun.body_end)
|
||||
last_is_exit = False
|
||||
if spans:
|
||||
last_start, last_end = spans[-1]
|
||||
tok, _ = read_next_token(text, last_start + 1, last_end) if text[last_start] == "(" else (None, 0)
|
||||
last_is_exit = bool(tok) and tok.lower() == "exit"
|
||||
if not last_is_exit:
|
||||
edits.insert(defun.path, defun.body_end, 25, indent + "(dbgclose)\n")
|
||||
|
||||
log(" [ok] %s (%s): dbgopen + dbgclose (%d exit-Stellen + Rumpfende)" %
|
||||
(defun.name, defun.path, len(exit_calls)))
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rekursion ueber den Aufrufgraphen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def instrument_recursive(start_defun, index, file_texts, edits, max_depth, exclude, force, log):
|
||||
visited = set()
|
||||
|
||||
def recurse(defun, depth):
|
||||
key = defun.name.lower()
|
||||
if key in visited:
|
||||
return
|
||||
visited.add(key)
|
||||
|
||||
instrument_method(defun, file_texts, edits, force=force, log=log)
|
||||
|
||||
if depth >= max_depth:
|
||||
log(" [info] max-depth (%d) bei %s erreicht - keine weiteren Aufrufe verfolgt" %
|
||||
(max_depth, defun.name))
|
||||
return
|
||||
|
||||
text = file_texts[defun.path]
|
||||
calls = scan_calls(text, defun.body_start, defun.body_end)
|
||||
seen_here = set()
|
||||
for head, _s, _e in calls:
|
||||
head_l = head.lower()
|
||||
if head_l in seen_here:
|
||||
continue
|
||||
seen_here.add(head_l)
|
||||
if head_l in DBG_FRAMEWORK_NAMES or head_l in exclude:
|
||||
continue
|
||||
callee = index.get(head_l)
|
||||
if callee is None or callee.name.lower() == key:
|
||||
continue
|
||||
recurse(callee, depth + 1)
|
||||
|
||||
recurse(start_defun, 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pfad-/Umgebungshilfen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def find_repo_root(start_path):
|
||||
cur = os.path.abspath(start_path)
|
||||
if os.path.isfile(cur):
|
||||
cur = os.path.dirname(cur)
|
||||
while True:
|
||||
if os.path.isdir(os.path.join(cur, ".git")) or os.path.isdir(os.path.join(cur, "Lisp")):
|
||||
return cur
|
||||
parent = os.path.dirname(cur)
|
||||
if parent == cur:
|
||||
return None
|
||||
cur = parent
|
||||
|
||||
|
||||
def resolve_file(path, repo_root):
|
||||
if os.path.isfile(path):
|
||||
return os.path.abspath(path)
|
||||
if repo_root:
|
||||
candidate = os.path.join(repo_root, path)
|
||||
if os.path.isfile(candidate):
|
||||
return os.path.abspath(candidate)
|
||||
return None
|
||||
|
||||
|
||||
def collect_lsp_files(dirs):
|
||||
paths = []
|
||||
seen = set()
|
||||
for d in dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for root, _dirnames, filenames in os.walk(d):
|
||||
for fn in filenames:
|
||||
if fn.lower().endswith(".lsp"):
|
||||
full = os.path.abspath(os.path.join(root, fn))
|
||||
if full not in seen:
|
||||
seen.add(full)
|
||||
paths.append(full)
|
||||
return paths
|
||||
|
||||
|
||||
def dbg_file_name_for(method_name):
|
||||
"""Leitet einen .dbg-Dateinamen aus einem Methodennamen ab, z.B.
|
||||
"c:TEST_MUBEA" -> "test_mubea.dbg", "mubea:build-kreisel" -> "build-kreisel.dbg"."""
|
||||
base = method_name.split(":")[-1]
|
||||
return base.lower() + ".dbg"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="dbg2lsp.py",
|
||||
description="Instrumentiert AutoLISP-Funktionen mit ssg_dbg.lsp (dbgf/dbg/dbgreturn/dbgopen/dbgclose).",
|
||||
)
|
||||
parser.add_argument("file", help="Anker-.lsp-Datei (Verzeichnis wird immer mit durchsucht)")
|
||||
parser.add_argument("--method", action="append", default=[],
|
||||
metavar="NAME",
|
||||
help="Funktionsname, der mit dbgf/dbg/dbgreturn instrumentiert wird. Mehrfach angebbar.")
|
||||
parser.add_argument("--recursive", action="store_true",
|
||||
help="Auch alle im Rumpf aufgerufenen projekteigenen Funktionen instrumentieren (rekursiv)")
|
||||
parser.add_argument("--search-dir", action="append", default=[],
|
||||
metavar="DIR",
|
||||
help="Weiteres Verzeichnis nach .lsp-Dateien durchsuchen (fuer --recursive). "
|
||||
"Default: Lisp/ und das Verzeichnis von FILE.")
|
||||
parser.add_argument("--max-depth", type=int, default=3,
|
||||
help="Maximale Rekursionstiefe fuer --recursive (Default: 3)")
|
||||
parser.add_argument("--exclude", action="append", default=[],
|
||||
metavar="NAME",
|
||||
help="Funktionsname von --recursive ausschliessen (mehrfach angebbar)")
|
||||
parser.add_argument("--add-open", nargs=2, action="append", default=[],
|
||||
metavar=("DATEI", "METHODE"),
|
||||
help="(dbgopen ...)/(dbgclose) in METHODE (aus DATEI) einfuegen. Mehrfach angebbar.")
|
||||
parser.add_argument("--dbg-file", default=None,
|
||||
help="Dateiname fuer (dbgopen ...) (Default: aus dem --add-open-Methodennamen abgeleitet, z.B. test_mubea.dbg)")
|
||||
parser.add_argument("--dbg-envvar", default=DEFAULT_DBG_ENVVAR,
|
||||
help='Umgebungsvariable fuer (dbgopen ...) (Default: "%s")' % DEFAULT_DBG_ENVVAR)
|
||||
parser.add_argument("--force", action="store_true",
|
||||
help="Auch instrumentieren, wenn bereits (dbgf ...)/(dbgopen ...) vorhanden zu sein scheint")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Nur ein Diff anzeigen, keine Dateien schreiben")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
repo_root = find_repo_root(args.file) or find_repo_root(".")
|
||||
main_file = resolve_file(args.file, repo_root)
|
||||
if main_file is None:
|
||||
print("FEHLER: Datei nicht gefunden: %s" % args.file, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
search_dirs = [os.path.dirname(main_file)]
|
||||
if repo_root:
|
||||
search_dirs.append(os.path.join(repo_root, "Lisp"))
|
||||
for d in args.search_dir:
|
||||
resolved = d if os.path.isdir(d) else os.path.join(repo_root or ".", d)
|
||||
search_dirs.append(resolved)
|
||||
for f, _m in args.add_open:
|
||||
resolved = resolve_file(f, repo_root)
|
||||
if resolved:
|
||||
search_dirs.append(os.path.dirname(resolved))
|
||||
|
||||
lsp_files = set(collect_lsp_files(search_dirs))
|
||||
lsp_files.add(main_file)
|
||||
lsp_files = sorted(lsp_files)
|
||||
|
||||
file_texts, index = build_defun_index(lsp_files)
|
||||
|
||||
if not args.method and not args.add_open:
|
||||
print("FEHLER: mindestens --method oder --add-open angeben.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
edits = EditSet()
|
||||
exclude = {n.lower() for n in args.exclude}
|
||||
touched_any = False
|
||||
|
||||
for method_name in args.method:
|
||||
defun = index.get(method_name.lower())
|
||||
if defun is None:
|
||||
print("FEHLER: Funktion '%s' nicht gefunden (durchsucht: %s)" %
|
||||
(method_name, ", ".join(lsp_files)), file=sys.stderr)
|
||||
continue
|
||||
print("== --method %s ==" % method_name)
|
||||
if args.recursive:
|
||||
instrument_recursive(defun, index, file_texts, edits,
|
||||
max_depth=args.max_depth, exclude=exclude,
|
||||
force=args.force, log=print)
|
||||
else:
|
||||
instrument_method(defun, file_texts, edits, force=args.force, log=print)
|
||||
touched_any = True
|
||||
|
||||
for file_arg, method_name in args.add_open:
|
||||
resolved = resolve_file(file_arg, repo_root)
|
||||
if resolved is None:
|
||||
print("FEHLER: Datei nicht gefunden: %s" % file_arg, file=sys.stderr)
|
||||
continue
|
||||
# Falls die Datei noch nicht im Index ist (z.B. ausserhalb der
|
||||
# Search-Dirs), jetzt einlesen und mit aufnehmen.
|
||||
if resolved not in file_texts:
|
||||
with open(resolved, "r", encoding="utf-8") as f:
|
||||
file_texts[resolved] = f.read()
|
||||
for d in find_defuns_in_text(file_texts[resolved], resolved):
|
||||
index.setdefault(d.name.lower(), d)
|
||||
defun = index.get(method_name.lower())
|
||||
if defun is None or defun.path != resolved:
|
||||
# ggf. gleichnamige Funktion aus anderer Datei ignorieren -
|
||||
# explizit in der genannten Datei suchen.
|
||||
local_defuns = find_defuns_in_text(file_texts[resolved], resolved)
|
||||
defun = next((d for d in local_defuns if d.name.lower() == method_name.lower()), None)
|
||||
if defun is None:
|
||||
print("FEHLER: Funktion '%s' nicht in %s gefunden" % (method_name, file_arg), file=sys.stderr)
|
||||
continue
|
||||
print("== --add-open %s %s ==" % (file_arg, method_name))
|
||||
dbg_file = args.dbg_file or dbg_file_name_for(method_name)
|
||||
instrument_add_open(defun, file_texts, edits, dbg_file=dbg_file, dbg_envvar=args.dbg_envvar,
|
||||
force=args.force, log=print)
|
||||
touched_any = True
|
||||
|
||||
if not touched_any:
|
||||
return 1
|
||||
|
||||
changed = edits.apply(file_texts)
|
||||
if not changed:
|
||||
print("Keine Aenderungen vorgenommen.")
|
||||
return 0
|
||||
|
||||
for path, new_text in changed.items():
|
||||
old_text = file_texts[path]
|
||||
if args.dry_run:
|
||||
rel = os.path.relpath(path, repo_root) if repo_root else path
|
||||
diff = difflib.unified_diff(
|
||||
old_text.splitlines(keepends=True),
|
||||
new_text.splitlines(keepends=True),
|
||||
fromfile=rel, tofile=rel,
|
||||
)
|
||||
sys.stdout.writelines(diff)
|
||||
else:
|
||||
with open(path, "w", encoding="utf-8", newline="") as f:
|
||||
f.write(new_text)
|
||||
print("Geschrieben: %s" % path)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user