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:
@@ -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
@@ -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.
|
||||
|
||||
+424
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user