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