diff --git a/bin/awl2scl_deep.bat b/bin/awl2scl_deep.bat new file mode 100644 index 0000000..da68f8f --- /dev/null +++ b/bin/awl2scl_deep.bat @@ -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 ... +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 +) diff --git a/bin/setenv.bat b/bin/setenv.bat index 022da40..dc57765 100644 --- a/bin/setenv.bat +++ b/bin/setenv.bat @@ -38,6 +38,7 @@ if not exist "%PV_BIN%" mkdir "%PV_BIN%" if not exist "%PV_LIB%" mkdir "%PV_LIB%" if not exist "%PV_CFG%" mkdir "%PV_CFG%" 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_SCL_EXPORT%" mkdir "%PV_SCL_EXPORT%" @@ -45,15 +46,15 @@ REM Umgebungsvariablen anzeigen echo. echo ================================================================ echo PROJECT = %PROJECT% -echo PV_BIN = %PV_BIN% -echo PV_LIB = %PV_LIB% -echo PV_CFG = %PV_CFG% -echo PV_DATA = %PV_DATA% -echo PV_LOG = %PV_LOG% -echo PV_RESULTS = %PV_RESULTS% -echo PV_EXAMPLES = %PV_EXAMPLES% -echo PV_TESTS = %PV_TESTS% -echo PV_SCL_EXPORT= %PV_SCL_EXPORT% -echo PYTHONPATH = %PYTHONPATH% +@REM echo PV_BIN = %PV_BIN% +@REM echo PV_LIB = %PV_LIB% +@REM echo PV_CFG = %PV_CFG% +@REM echo PV_DATA = %PV_DATA% +@REM echo PV_LOG = %PV_LOG% +@REM echo PV_RESULTS = %PV_RESULTS% +@REM echo PV_EXAMPLES = %PV_EXAMPLES% +@REM echo PV_TESTS = %PV_TESTS% +@REM echo PV_SCL_EXPORT= %PV_SCL_EXPORT% +@REM echo PYTHONPATH = %PYTHONPATH% echo ================================================================ echo. diff --git a/lib/paramize.py b/lib/paramize.py new file mode 100644 index 0000000..2f1abaa --- /dev/null +++ b/lib/paramize.py @@ -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: + { "|": { "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 "") + 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 diff --git a/lib/sclopt.py b/lib/sclopt.py new file mode 100644 index 0000000..9b208c0 --- /dev/null +++ b/lib/sclopt.py @@ -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 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 : ... 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 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]); ; 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 ' = ' 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