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_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
@@ -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