#!/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