Schalter --improve-gotos und --globasl2params implementiert, um Code lesbarer zu machen. So kann man sicheren Lauffähigen Code in jedem Fall erzeugen, aber auch gleich die Lesbarkeit erhöhene

This commit is contained in:
2026-07-08 16:01:22 +02:00
parent 1d91bd4d7d
commit 8bc52d866c
4 changed files with 956 additions and 10 deletions
+28
View File
@@ -0,0 +1,28 @@
@echo off
REM ================================================================
REM Konvertiert .awl Dateien in .scl Dateien MIT Tiefenoptimierung:
REM --improve-gotos Kontrollfluss aufraeumen (Guard->IF, dedup, CASE)
REM --globals2params globale Merker-Zugriffe in Parameter umbauen
REM (+ toChange-Liste in data\ fuer die Aufrufer)
REM
REM Beide Schalter implizieren --force (generierte .scl werden neu erzeugt,
REM handgeschriebene bleiben geschuetzt).
REM
REM Weitere Argumente werden durchgereicht, z.B.:
REM bin\awl2scl_deep.bat --dir . --category A
REM bin\awl2scl_deep.bat <baustein.awl> ...
REM ================================================================
call "%~dp0setenv.bat"
REM .venv bevorzugen, sonst System-Python
set "PYTHON=python"
if exist "%PROJECT%\.venv\Scripts\python.exe" (
set "PYTHON=%PROJECT%\.venv\Scripts\python.exe"
)
"%PYTHON%" "%PV_LIB%\awl2scl.py" --improve-gotos --globals2params %*
if errorlevel 1 (
echo FEHLER: awl2scl.py wurde mit Fehler beendet.
exit /b 1
)
+11 -10
View File
@@ -38,6 +38,7 @@ if not exist "%PV_BIN%" mkdir "%PV_BIN%"
if not exist "%PV_LIB%" mkdir "%PV_LIB%" if not exist "%PV_LIB%" mkdir "%PV_LIB%"
if not exist "%PV_CFG%" mkdir "%PV_CFG%" if not exist "%PV_CFG%" mkdir "%PV_CFG%"
if not exist "%PV_DATA%" mkdir "%PV_DATA%" if not exist "%PV_DATA%" mkdir "%PV_DATA%"
if not exist "%PV_DOC%" mkdir "%PV_DOC%"
if not exist "%PV_TESTS%" mkdir "%PV_TESTS%" if not exist "%PV_TESTS%" mkdir "%PV_TESTS%"
if not exist "%PV_SCL_EXPORT%" mkdir "%PV_SCL_EXPORT%" if not exist "%PV_SCL_EXPORT%" mkdir "%PV_SCL_EXPORT%"
@@ -45,15 +46,15 @@ REM Umgebungsvariablen anzeigen
echo. echo.
echo ================================================================ echo ================================================================
echo PROJECT = %PROJECT% echo PROJECT = %PROJECT%
echo PV_BIN = %PV_BIN% @REM echo PV_BIN = %PV_BIN%
echo PV_LIB = %PV_LIB% @REM echo PV_LIB = %PV_LIB%
echo PV_CFG = %PV_CFG% @REM echo PV_CFG = %PV_CFG%
echo PV_DATA = %PV_DATA% @REM echo PV_DATA = %PV_DATA%
echo PV_LOG = %PV_LOG% @REM echo PV_LOG = %PV_LOG%
echo PV_RESULTS = %PV_RESULTS% @REM echo PV_RESULTS = %PV_RESULTS%
echo PV_EXAMPLES = %PV_EXAMPLES% @REM echo PV_EXAMPLES = %PV_EXAMPLES%
echo PV_TESTS = %PV_TESTS% @REM echo PV_TESTS = %PV_TESTS%
echo PV_SCL_EXPORT= %PV_SCL_EXPORT% @REM echo PV_SCL_EXPORT= %PV_SCL_EXPORT%
echo PYTHONPATH = %PYTHONPATH% @REM echo PYTHONPATH = %PYTHONPATH%
echo ================================================================ echo ================================================================
echo. echo.
+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env python3
"""globals2params: baut globale Merker-Zugriffe eines Bausteins in echte
Parameter um und verwaltet eine persistente toChange-Liste in data/ fuer die
Anpassung der Aufrufer (auch noch nicht konvertierter .awl-Dateien).
Verhalten (verhaltenserhaltend by construction): ein Global, das der Baustein
bisher direkt las/schrieb, wird Parameter; jeder Aufrufer forwardet exakt
dasselbe Global an den Parameter (Input ':=', Output/InOut '=>'). Damit bleibt
die Semantik identisch, solange der Aufrufer das Global vor/nach dem Aufruf
wie bisher nutzt.
Grenzen: indirekte Aufrufe (UC/CC FC[var]) koennen nicht automatisch
umgeschrieben werden -> in toChange als 'manual' vermerkt.
"""
import json
import re
from pathlib import Path
import sclopt
# Globale Operanden: symbolisch ("...") oder absolut (M/E/A/PE/PA + Groesse).
# Lokale (#...), Literale, Timer/Zaehler (T/C mit Nummer) sind KEINE Kandidaten.
_ABS_GLOBAL_RE = re.compile(r'^(M|E|A|PE|PA|I|Q)(X|B|W|D)?\d+(\.\d+)?$')
_SYM_GLOBAL_RE = re.compile(r'^"[^"]+"$')
def is_global_operand(tok):
tok = tok.strip()
if not tok or tok.startswith("#"):
return False
if _SYM_GLOBAL_RE.match(tok):
return True
return bool(_ABS_GLOBAL_RE.match(tok))
def param_name_for(global_tok):
"""Deterministischer, SCL-gueltiger Parametername aus dem Global-Token."""
core = global_tok.strip().strip('"')
name = re.sub(r"[^A-Za-z0-9_]", "_", core)
if not re.match(r"^[A-Za-z_]", name):
name = "g_" + name
return name
# ---------------------------------------------------------------------------
# Richtungsanalyse ueber die IR-Knoten
# ---------------------------------------------------------------------------
_TOKEN_RE = re.compile(r'"[^"]+"|#?[A-Za-z_]\w*(?:\.\w+)*|\b\d+\b')
def _iter_read_tokens(expr):
"""Alle Operanden-Token eines SCL-Ausdrucks (fuer Lese-Erkennung)."""
if not expr:
return
for m in re.finditer(r'"[^"]+"|#[A-Za-z_][\w.]*|[A-Za-z_][\w.]*', expr):
yield m.group(0)
def _base_token(tok):
"""Strukturzugriff auf Basis reduzieren: '"DB".Feld' -> '"DB"'; 'M10.0' bleibt."""
tok = tok.strip()
if tok.startswith('"'):
end = tok.find('"', 1)
return tok[:end + 1] if end != -1 else tok
return tok
_CALL_HEAD_RE = re.compile(r'^\s*(?:#\w+|"[^"]+")\s*\(')
def _call_param_values(call_text):
"""Liefert die Parameter-WERTE (rechts von := / =>) eines SCL-Aufrufs.
Zielbaustein und Parameternamen werden ausgelassen."""
m = _CALL_HEAD_RE.match(call_text)
if not m:
return
inner = call_text[m.end():]
depth = 1
end = 0
dq = sq = False
for i, ch in enumerate(inner):
if ch == '"' and not sq:
dq = not dq
elif ch == "'" and not dq:
sq = not sq
elif ch in "([" and not dq and not sq:
depth += 1
elif ch in ")]" and not dq and not sq:
depth -= 1
if depth == 0:
end = i
break
inner = inner[:end]
for piece in _split_commas(inner):
for sep in (":=", "=>"):
if sep in piece:
yield piece.split(sep, 1)[1].strip()
break
def _split_commas(text):
parts, buf, depth, dq, sq = [], [], 0, False, False
for ch in text:
if ch == '"' and not sq:
dq = not dq
elif ch == "'" and not dq:
sq = not sq
elif ch in "([" and not dq and not sq:
depth += 1
elif ch in ")]" and not dq and not sq:
depth -= 1
if ch == "," and depth == 0 and not dq and not sq:
parts.append("".join(buf)); buf = []; continue
buf.append(ch)
parts.append("".join(buf))
return parts
def analyze_directions(nodes, reads, writes):
"""Fuellt reads/writes (Sets globaler Basis-Token) durch Rekursion ueber die IR."""
for n in nodes:
if isinstance(n, sclopt.Assign):
base = _base_token(n.lhs)
if is_global_operand(base):
writes.add(base)
for t in _iter_read_tokens(n.rhs):
b = _base_token(t)
if is_global_operand(b):
reads.add(b)
elif isinstance(n, sclopt.IfDo):
for t in _iter_read_tokens(n.cond):
b = _base_token(t)
if is_global_operand(b):
reads.add(b)
analyze_directions(n.body, reads, writes)
elif isinstance(n, sclopt.Case):
b = _base_token(n.selector)
if is_global_operand(b):
reads.add(b)
for _, body, _ in n.branches:
analyze_directions(body, reads, writes)
elif isinstance(n, sclopt.Call):
# Nur die Parameter-WERTE eines Aufrufs sind Global-Kandidaten,
# nicht der Zielbaustein und nicht die Parameternamen.
for value in _call_param_values(n.text):
b = _base_token(value)
if is_global_operand(b):
reads.add(b)
writes.add(b) # Aufrufparameter: Richtung unklar -> konservativ InOut
# ---------------------------------------------------------------------------
# IR-Umschreibung: Global-Token -> #Parameter
# ---------------------------------------------------------------------------
def _replace_in_expr(expr, mapping):
if not expr:
return expr
def repl(m):
tok = m.group(0)
base = _base_token(tok)
if base in mapping:
return mapping[base] + tok[len(base):]
return tok
return re.sub(r'"[^"]+"(?:\.\w+)*|#?[A-Za-z_][\w.]*', repl, expr)
def _rewrite_call_values(call_text, mapping):
"""Ersetzt in einem Aufruf nur die Parameter-Werte (rechts von :=/=>)."""
m = _CALL_HEAD_RE.match(call_text)
if not m:
return call_text
head = call_text[:m.end()]
rest = call_text[m.end():]
depth = 1
end = 0
dq = sq = False
for i, ch in enumerate(rest):
if ch == '"' and not sq:
dq = not dq
elif ch == "'" and not dq:
sq = not sq
elif ch in "([" and not dq and not sq:
depth += 1
elif ch in ")]" and not dq and not sq:
depth -= 1
if depth == 0:
end = i
break
inner, tail = rest[:end], rest[end:]
new_pieces = []
for piece in _split_commas(inner):
for sep in (":=", "=>"):
if sep in piece:
name, value = piece.split(sep, 1)
new_pieces.append(f"{name}{sep}{_replace_in_expr(value, mapping)}")
break
else:
new_pieces.append(piece)
return head + ",".join(new_pieces) + tail
def rewrite_nodes(nodes, mapping):
for n in nodes:
if isinstance(n, sclopt.Assign):
n.lhs = _replace_in_expr(n.lhs, mapping)
n.rhs = _replace_in_expr(n.rhs, mapping)
elif isinstance(n, sclopt.IfDo):
n.cond = _replace_in_expr(n.cond, mapping)
rewrite_nodes(n.body, mapping)
elif isinstance(n, sclopt.Case):
n.selector = _replace_in_expr(n.selector, mapping)
for _, body, _ in n.branches:
rewrite_nodes(body, mapping)
elif isinstance(n, sclopt.Call):
n.text = _rewrite_call_values(n.text, mapping)
# ---------------------------------------------------------------------------
# Aufrufer-Suche in allen .awl-Quellen
# ---------------------------------------------------------------------------
def _station_of(path):
for part in Path(path).parts:
if part.startswith("=A"):
return part
return "_"
class CallerIndex:
"""Findet direkte und indirekte Aufrufer je Bausteinname, ueber .awl-Quellen."""
def __init__(self, roots):
self.direct = {} # name -> list[(station, caller_name, path)]
self.indirect = {} # name -> list[(station, caller_name, path)] (UC/CC FC[..])
self._build(roots)
def _build(self, roots):
awl_files = []
for root in roots:
awl_files.extend(Path(root).rglob("*.awl"))
direct_re = re.compile(r'\bCALL\s+(?:FB|FC|SFB|SFC)?\s*"([^"]+)"')
indirect_re = re.compile(r'\b(?:UC|CC)\s+FC\[')
header_re = re.compile(r'===\s*\w+\s+\S*:\s*(.+?)\s*\[STL\]\s*===|'
r'^(?:FUNCTION_BLOCK|FUNCTION|ORGANIZATION_BLOCK)\s+"([^"]+)"')
for p in awl_files:
try:
text = p.read_text(encoding="utf-8-sig", errors="replace")
except OSError:
continue
hm = header_re.search(text)
caller = (hm.group(1) or hm.group(2)) if hm else p.stem
station = _station_of(p)
for m in direct_re.finditer(text):
self.direct.setdefault(m.group(1), []).append((station, caller, str(p)))
if indirect_re.search(text):
self.indirect.setdefault("*", []).append((station, caller, str(p)))
# ---------------------------------------------------------------------------
# toChange-Persistenz
# ---------------------------------------------------------------------------
class ToChangeStore:
"""data/globals2params_tochange.json:
{ "<station>|<caller>": { "path":..., "changes":[ {func, call, status} ] } }"""
def __init__(self, path):
self.path = Path(path)
self.data = {}
if self.path.exists():
try:
self.data = json.loads(self.path.read_text(encoding="utf-8"))
except (OSError, ValueError):
self.data = {}
@staticmethod
def key(station, caller):
return f"{station}|{caller}"
def add_change(self, station, caller, path, func, call_text, status):
k = self.key(station, caller)
entry = self.data.setdefault(k, {"path": path, "changes": []})
for ch in entry["changes"]:
if ch["func"] == func:
ch["call"] = call_text
ch["status"] = status
return
entry["changes"].append({"func": func, "call": call_text, "status": status})
def pending_for(self, station, caller):
entry = self.data.get(self.key(station, caller))
if not entry:
return []
return [ch for ch in entry["changes"] if ch.get("status") == "pending"]
def mark_applied(self, station, caller, func):
entry = self.data.get(self.key(station, caller))
if not entry:
return
for ch in entry["changes"]:
if ch["func"] == func:
ch["status"] = "applied"
def save(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(self.data, indent=2, ensure_ascii=False), encoding="utf-8")
# ---------------------------------------------------------------------------
# Orchestrierung
# ---------------------------------------------------------------------------
class Paramizer:
def __init__(self, caller_index, store):
self.caller_index = caller_index
self.store = store
def parameterize(self, parsed, translated, translator, station, awl_path):
"""Baut globale Zugriffe des Bausteins in Parameter um; erzeugt toChange
fuer alle Aufrufer. Liefert (new_params, callers)."""
reads, writes = set(), set()
for _, nodes in translated:
analyze_directions(nodes, reads, writes)
globals_used = reads | writes
if not globals_used:
return [], []
# Richtung + Parametername je Global
params = [] # (global, name, direction)
mapping = {} # global-basis -> "#name"
used_names = set()
for g in sorted(globals_used):
r, w = g in reads, g in writes
direction = "InOut" if (r and w) else ("Output" if w else "Input")
name = param_name_for(g)
while name in used_names:
name += "_"
used_names.add(name)
params.append((g, name, direction))
mapping[g] = "#" + name
# Interface ergaenzen (Kommentar: war "<global>")
for g, name, direction in params:
sect = {"Input": "Input", "Output": "Output", "InOut": "InOut"}[direction]
parsed.sections.setdefault(sect, []).append(f'{name} : {_scl_type_for(g)}; // war {g}')
# Rumpf umschreiben
for _, nodes in translated:
rewrite_nodes(nodes, mapping)
# Aufrufer + toChange
callers = self._record_callers(parsed.name, params)
return params, callers
def _record_callers(self, func_name, params):
results = []
call_text = _build_param_call(func_name, params)
for station, caller, path in self.caller_index.direct.get(func_name, []):
self.store.add_change(station, caller, path, func_name, call_text, "pending")
results.append((f"{station}|{caller}", "pending"))
# indirekte Aufrufe (UC/CC FC[..]) -> manuell
for station, caller, path in self.caller_index.indirect.get("*", []):
self.store.add_change(station, caller, path, func_name,
f"// MANUELL: indirekter Aufruf von {func_name} auf Parameter umstellen",
"manual")
results.append((f"{station}|{caller}", "manual"))
return results
def apply_pending_call_rewrites(self, parsed, translated, station):
"""Wendet frueher vermerkte Aufruf-Rewrites an, wenn DIESER Baustein
(als Aufrufer) jetzt konvertiert wird."""
pending = self.store.pending_for(station, parsed.name)
if not pending:
return
by_func = {ch["func"]: ch["call"] for ch in pending}
for _, nodes in translated:
_rewrite_calls_in_nodes(nodes, by_func)
for ch in pending:
self.store.mark_applied(station, parsed.name, ch["func"])
def _scl_type_for(global_tok):
"""Grobe Typableitung aus dem Global-Token (mechanisch, im Zweifel Bool/Word)."""
core = global_tok.strip().strip('"')
m = re.match(r'^(M|E|A|PE|PA|I|Q)(X|B|W|D)?', core)
if m:
return {"X": "Bool", "B": "Byte", "W": "Word", "D": "DWord", None: "Bool"}[m.group(2)]
if "." in core or re.search(r"\d+\.\d+$", core):
return "Bool"
return "Word"
def _build_param_call(func_name, params):
parts = []
for g, name, direction in params:
sep = ":=" if direction == "Input" else "=>"
parts.append(f"{name} {sep} {g}")
return f'"{func_name}"(' + ", ".join(parts) + ");"
def _rewrite_calls_in_nodes(nodes, by_func):
call_name_re = re.compile(r'^"([^"]+)"\s*\(')
for i, n in enumerate(nodes):
if isinstance(n, sclopt.Call):
m = call_name_re.match(n.text.strip())
if m and m.group(1) in by_func:
nodes[i] = sclopt.Call(by_func[m.group(1)], n.comment)
elif isinstance(n, sclopt.IfDo):
_rewrite_calls_in_nodes(n.body, by_func)
elif isinstance(n, sclopt.Case):
for _, body, _ in n.branches:
_rewrite_calls_in_nodes(body, by_func)
class NullParamizer:
"""Platzhalter, wenn --globals2params aus ist (keine Umbauten)."""
def parameterize(self, *a, **k):
return [], []
def apply_pending_call_rewrites(self, *a, **k):
return
+493
View File
@@ -0,0 +1,493 @@
#!/usr/bin/env python3
"""SCL-Zwischenstruktur (IR) + Lesbarkeits-Optimierung fuer awl2scl.py.
Der Translator in awl2scl.py erzeugt IR-Knoten (statt direkt Textzeilen);
render_ir() rendert sie. Ohne aktive Optimierung ist die Ausgabe
byte-identisch zur frueheren stringbasierten Emission (Regressionsabsicherung).
Mit --improve-gotos laufen verlustfreie Peephole-Durchlaeufe:
- merge_adjacent_ifdo : gleiche Bedingung zusammenfassen
- guard_skip_to_if : "IF c THEN GOTO L; body; L:" -> "IF NOT c THEN body"
- synth_case : Verteilung auf eine Variable -> CASE
- drop_unused_labels : nicht angesprungene Marken entfernen
- normalize_bool : Klammern/NOT vereinfachen
"""
import re
# ---------------------------------------------------------------------------
# IR-Knoten
# ---------------------------------------------------------------------------
class Node:
comment = ""
def render(self):
raise NotImplementedError
class Raw(Node):
"""Beliebige vorgefertigte Zeile (Fallback, z.B. Kommentare, ENO-Zeilen)."""
def __init__(self, text):
self.text = text
def render(self):
return [self.text]
class Comment(Node):
def __init__(self, text):
self.text = text
def render(self):
return [f"// {self.text}"]
class Label(Node):
def __init__(self, name):
self.name = name
def render(self):
return [f"{self.name}: ;"]
class Goto(Node):
def __init__(self, target):
self.target = target
def render(self):
return [f"GOTO {self.target};"]
class Return(Node):
def render(self):
return ["RETURN;"]
class Assign(Node):
def __init__(self, lhs, rhs, comment=""):
self.lhs = lhs
self.rhs = rhs
self.comment = comment
def render(self):
line = f"{self.lhs} := {self.rhs};"
if self.comment:
line += f" // {self.comment}"
return [line]
class Call(Node):
def __init__(self, text, comment=""):
self.text = text
self.comment = comment
def render(self):
line = self.text
if self.comment:
line += f" // {self.comment}"
return [line]
class IfDo(Node):
"""Bedingte Ausfuehrung. body ist eine Liste von Node.
single_line=True rendert die urspruengliche einzeilige Form
'IF c THEN <stmt> END_IF;' (fuer Byte-Identitaet der mechanischen Ausgabe)."""
def __init__(self, cond, body, single_line=True):
self.cond = cond
self.body = body
self.single_line = single_line
def render(self):
if self.single_line and len(self.body) == 1:
inner = self.body[0].render()
if len(inner) == 1:
return [f"IF {self.cond} THEN {inner[0]} END_IF;"]
out = [f"IF {self.cond} THEN"]
for n in self.body:
for ln in n.render():
out.append(" " + ln)
out.append("END_IF;")
return out
class Case(Node):
"""CASE selector OF <konst[,konst]>: <body> ... END_CASE;"""
def __init__(self, selector, branches):
self.selector = selector
self.branches = branches # list of (labels:list[str], body:list[Node], comment:str)
def render(self):
out = [f"CASE {self.selector} OF"]
for labels, body, comment in self.branches:
head = " " + ", ".join(labels) + ":"
if comment:
head += f" // {comment}"
out.append(head)
for n in body:
for ln in n.render():
out.append(" " + ln)
out.append("END_CASE;")
return out
# ---------------------------------------------------------------------------
# Rendern
# ---------------------------------------------------------------------------
def render_ir(nodes):
lines = []
for n in nodes:
lines.extend(n.render())
return lines
# ---------------------------------------------------------------------------
# Hilfen: Bedingungen/Ausdruecke
# ---------------------------------------------------------------------------
def _find_matching(expr, open_idx):
"""Index der zu expr[open_idx]=='(' passenden ')'."""
depth = 0
dq = sq = False
for i in range(open_idx, len(expr)):
ch = expr[i]
if ch == '"' and not sq:
dq = not dq
elif ch == "'" and not dq:
sq = not sq
elif ch == "(" and not dq and not sq:
depth += 1
elif ch == ")" and not dq and not sq:
depth -= 1
if depth == 0:
return i
return -1
def strip_outer_parens(expr):
"""Entfernt genau umschliessende aeussere Klammern (mehrfach), verlustfrei."""
expr = expr.strip()
while expr.startswith("(") and _find_matching(expr, 0) == len(expr) - 1:
expr = expr[1:-1].strip()
return expr
def collapse_double_parens(expr):
"""'((x))' -> '(x)' wo die inneren Klammern direkt die aeusseren fuellen."""
changed = True
while changed:
changed = False
i = 0
while i < len(expr) - 1:
if expr[i] == "(" and expr[i + 1] == "(":
close = _find_matching(expr, i)
inner_close = _find_matching(expr, i + 1)
if close != -1 and inner_close == close - 1:
expr = expr[:i] + expr[i + 1:close] + expr[close + 1:]
changed = True
continue
i += 1
return expr
def normalize_cond(cond):
"""Verlustfreie Ausdrucks-Kosmetik: doppelte/aeussere Klammern weg,
NOT (a = b) -> a <> b, NOT (a <> b) -> a = b, NOT (NOT x) -> x."""
prev = None
cond = cond.strip()
while cond != prev:
prev = cond
cond = collapse_double_parens(cond)
# NOT (NOT x) -> x
m = re.match(r"^NOT \((NOT \(.*\))\)$", cond)
if m and _find_matching(cond, 4) == len(cond) - 1:
cond = strip_outer_parens(cond[4:]) # innere NOT(...) behalten
continue
# NOT (a <op> b) mit einzelnem Vergleich -> negierter Vergleich
m = re.match(r"^NOT \((.*)\)$", cond)
if m and _find_matching(cond, 4) == len(cond) - 1:
inner = m.group(1).strip()
flip = _flip_comparison(inner)
if flip is not None:
cond = flip
continue
cond2 = strip_outer_parens(cond)
if cond2 != cond:
cond = cond2
return cond
_CMP_FLIP = {"=": "<>", "<>": "=", ">": "<=", "<": ">=", ">=": "<", "<=": ">"}
_CMP_RE = re.compile(r"^(.*?)\s(<>|>=|<=|=|>|<)\s(.*)$")
def _flip_comparison(inner):
"""Falls inner ein einzelner Top-Level-Vergleich ist: negierten Vergleich zurueck."""
inner = strip_outer_parens(inner)
# Nur genau EIN Top-Level-Vergleich, keine AND/OR-Verknuepfung auf Top-Ebene.
if _has_top_level_boolop(inner):
return None
m = _CMP_RE.match(inner)
if not m:
return None
lhs, op, rhs = m.group(1).strip(), m.group(2), m.group(3).strip()
if _has_top_level_boolop(lhs) or _has_top_level_boolop(rhs):
return None
return f"{lhs} {_CMP_FLIP[op]} {rhs}"
def _has_top_level_boolop(expr):
depth = 0
dq = sq = False
tokens = re.finditer(r'"|\'|\(|\)|\bAND\b|\bOR\b|\bXOR\b', expr)
for m in tokens:
t = m.group(0)
if t == '"' and not sq:
dq = not dq
elif t == "'" and not dq:
sq = not sq
elif t == "(" and not dq and not sq:
depth += 1
elif t == ")" and not dq and not sq:
depth -= 1
elif t in ("AND", "OR", "XOR") and depth == 0 and not dq and not sq:
return True
return False
def negate_cond(cond):
"""Logische Negation einer Bedingung (fuer Guard-Umkehr)."""
cond = cond.strip()
inner = strip_outer_parens(cond)
flip = _flip_comparison(inner)
if flip is not None:
return flip
if inner.startswith("NOT (") and _find_matching(inner, 4) == len(inner) - 1:
return strip_outer_parens(inner[4:])
return f"NOT ({inner})"
# ---------------------------------------------------------------------------
# Improve-Passes (verlustfrei)
# ---------------------------------------------------------------------------
def _referenced_labels(nodes):
refs = {}
for n in nodes:
if isinstance(n, Goto):
refs[n.target] = refs.get(n.target, 0) + 1
elif isinstance(n, IfDo):
for b in n.body:
if isinstance(b, Goto):
refs[b.target] = refs.get(b.target, 0) + 1
return refs
def merge_adjacent_ifdo(nodes):
out = []
for n in nodes:
if (isinstance(n, IfDo) and out and isinstance(out[-1], IfDo)
and out[-1].cond == n.cond):
out[-1] = IfDo(out[-1].cond, out[-1].body + n.body, single_line=False)
else:
out.append(n)
return out
def guard_skip_to_if(nodes):
"""IfDo(c,[Goto L]); <B ohne L/Sprung nach L>; Label(L) -> IfDo(not c, B)
Nur wenn L genau einmal referenziert wird und direkt hinter B steht."""
refs = _referenced_labels(nodes)
out = []
i = 0
n = len(nodes)
while i < n:
node = nodes[i]
if (isinstance(node, IfDo) and len(node.body) == 1
and isinstance(node.body[0], Goto)
and refs.get(node.body[0].target, 0) == 1):
target = node.body[0].target
# Block B bis zur passenden Label(target) sammeln
j = i + 1
body = []
found = False
while j < n:
if isinstance(nodes[j], Label) and nodes[j].name == target:
found = True
break
if isinstance(nodes[j], Label):
break # andere Marke dazwischen -> kein sauberes Guard-Muster
body.append(nodes[j])
j += 1
if found and body:
inv = normalize_cond(negate_cond(node.cond))
out.append(IfDo(inv, guard_skip_to_if(body), single_line=(len(body) == 1)))
i = j + 1 # Label(target) ueberspringen
continue
out.append(node)
i += 1
return out
_EQ_RE = re.compile(r"^(.*?)\s=\s(.+)$")
def _eq_dispatch(cond):
"""Wenn cond ein einzelner '<sel> = <konst>' ist: (sel, konst), sonst None."""
cond = strip_outer_parens(cond)
if _has_top_level_boolop(cond):
return None
m = _EQ_RE.match(cond)
if not m:
return None
sel, const = m.group(1).strip(), m.group(2).strip()
if not re.match(r'^(#?\w+|"[^"]+")$', sel):
return None
if not re.match(r"^(16#[0-9A-Fa-f]+|w#16#[0-9A-Fa-f]+|\d+|W#\d+)$", const, re.IGNORECASE):
return None
return sel, const
def _body_key(body):
return "\n".join(render_ir(body))
def synth_case(nodes):
"""Folge von IfDo(sel = const, body) auf denselben sel -> Case.
Bodies, die auf Return enden, werden im CASE ohne das Return uebernommen."""
out = []
i = 0
n = len(nodes)
while i < n:
node = nodes[i]
disp = None
if isinstance(node, IfDo):
disp = _eq_dispatch(node.cond)
if disp is None:
out.append(node)
i += 1
continue
sel = disp[0]
# Alle direkt folgenden Dispatch-IfDo auf denselben sel sammeln
group = []
j = i
consts_seen = set()
while j < n and isinstance(nodes[j], IfDo):
d = _eq_dispatch(nodes[j].cond)
if d is None or d[0] != sel or d[1] in consts_seen:
break
consts_seen.add(d[1])
group.append((d[1], nodes[j].body))
j += 1
if len(group) < 2:
out.append(node)
i += 1
continue
# Bodies bereinigen (trailing Return entfernen) und gleiche zusammenfassen
branches = []
for const, body in group:
b = list(body)
if b and isinstance(b[-1], Return):
b = b[:-1]
key = _body_key(b)
for lab in branches:
if lab[2] == key:
lab[0].append(const)
break
else:
branches.append([[const], b, key])
out.append(Case(sel, [(labs, body, "") for labs, body, _ in branches]))
i = j
return out
def drop_unused_labels(nodes):
refs = _referenced_labels(nodes)
return [n for n in nodes if not (isinstance(n, Label) and refs.get(n.name, 0) == 0)]
def normalize_bool_pass(nodes):
for n in nodes:
if isinstance(n, IfDo):
n.cond = normalize_cond(n.cond)
normalize_bool_pass(n.body)
elif isinstance(n, Assign):
n.rhs = normalize_cond(n.rhs) if _looks_boolean(n.rhs) else n.rhs
elif isinstance(n, Case):
for _, body, _ in n.branches:
normalize_bool_pass(body)
return nodes
def _looks_boolean(rhs):
return bool(re.search(r"\b(AND|OR|XOR|NOT)\b", rhs)) or rhs.strip().startswith("(")
def improve_gotos(nodes):
"""Alle Passes in sinnvoller Reihenfolge; verlustfrei (je Netzwerk)."""
nodes = merge_adjacent_ifdo(nodes)
nodes = guard_skip_to_if(nodes)
nodes = synth_case(nodes)
nodes = drop_unused_labels(nodes)
nodes = normalize_bool_pass(nodes)
return nodes
def _region_single_dispatch(nodes):
"""Wenn eine REGION (nach improve_gotos) genau aus einem IfDo(sel=konst, body)
besteht (optional gefolgt von reinem Kommentar): (sel, konst, body), sonst None."""
real = [n for n in nodes if not isinstance(n, Comment)]
if len(real) != 1 or not isinstance(real[0], IfDo):
return None
disp = _eq_dispatch(real[0].cond)
if disp is None:
return None
return disp[0], disp[1], real[0].body
def synth_case_across_networks(networks):
"""Fasst aufeinanderfolgende REGIONs, die je eine Einzelverteilung auf DENSELBEN
Selektor sind, zu einer CASE-REGION zusammen. networks: list[(title, nodes)].
Liefert neue Netzwerkliste. REGION-Titel werden zu Branch-Kommentaren."""
out = []
i = 0
n = len(networks)
while i < n:
title, nodes = networks[i]
disp = _region_single_dispatch(nodes)
if disp is None:
out.append((title, nodes))
i += 1
continue
sel = disp[0]
group = [] # (title, const, body)
j = i
consts_seen = set()
while j < n:
d = _region_single_dispatch(networks[j][1])
if d is None or d[0] != sel or d[1] in consts_seen:
break
consts_seen.add(d[1])
group.append((networks[j][0], d[1], d[2]))
j += 1
if len(group) < 2:
out.append((title, nodes))
i += 1
continue
branches = []
for gtitle, const, body in group:
b = list(body)
if b and isinstance(b[-1], Return):
b = b[:-1]
key = _body_key(b)
for lab in branches:
if lab[2] == key and lab[3] == gtitle:
lab[0].append(const)
break
else:
branches.append([[const], b, key, gtitle])
case_branches = [(labs, body, gtitle) for labs, body, _, gtitle in branches]
out.append((f"CASE {sel}", [Case(sel, case_branches)]))
i = j
return out