node-red-contrib-me-vplc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +42 -0
  2. package/me-vplc.html +64 -0
  3. package/me-vplc.js +603 -0
  4. package/package.json +26 -0
  5. package/project/README.md +1052 -0
  6. package/project/START_ME_VPLC.cmd +176 -0
  7. package/project/backend/active_project.json +3 -0
  8. package/project/backend/app.py +839 -0
  9. package/project/backend/connector_runtime.py +585 -0
  10. package/project/backend/requirements.txt +3 -0
  11. package/project/backend/st_compiler.py +1415 -0
  12. package/project/frontend/index.html +12 -0
  13. package/project/frontend/package.json +18 -0
  14. package/project/frontend/src/App.jsx +631 -0
  15. package/project/frontend/src/style.css +964 -0
  16. package/project/frontend/vite.config.js +14 -0
  17. package/wheelhouse/Flask_Cors-4.0.1-py2.py3-none-any.whl +0 -0
  18. package/wheelhouse/blinker-1.9.0-py3-none-any.whl +0 -0
  19. package/wheelhouse/click-8.3.3-py3-none-any.whl +0 -0
  20. package/wheelhouse/colorama-0.4.6-py2.py3-none-any.whl +0 -0
  21. package/wheelhouse/flask-3.0.3-py3-none-any.whl +0 -0
  22. package/wheelhouse/itsdangerous-2.2.0-py3-none-any.whl +0 -0
  23. package/wheelhouse/jinja2-3.1.6-py3-none-any.whl +0 -0
  24. package/wheelhouse/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
  25. package/wheelhouse/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
  26. package/wheelhouse/markupsafe-3.0.3-cp310-cp310-win_amd64.whl +0 -0
  27. package/wheelhouse/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
  28. package/wheelhouse/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
  29. package/wheelhouse/markupsafe-3.0.3-cp311-cp311-win_amd64.whl +0 -0
  30. package/wheelhouse/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
  31. package/wheelhouse/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
  32. package/wheelhouse/markupsafe-3.0.3-cp312-cp312-win_amd64.whl +0 -0
  33. package/wheelhouse/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
  34. package/wheelhouse/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
  35. package/wheelhouse/markupsafe-3.0.3-cp313-cp313-win_amd64.whl +0 -0
  36. package/wheelhouse/pymodbus-3.6.9-py3-none-any.whl +0 -0
  37. package/wheelhouse/werkzeug-3.1.8-py3-none-any.whl +0 -0
@@ -0,0 +1,1415 @@
1
+ import copy
2
+ import math
3
+ import re
4
+ import time
5
+ import zipfile
6
+ from datetime import datetime
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+
12
+ class STCompileError(Exception):
13
+ pass
14
+
15
+
16
+ class STArray:
17
+ def __init__(self, lower: int, upper: int, factory):
18
+ self.lower = int(lower)
19
+ self.upper = int(upper)
20
+ self._values = [factory() for _ in range(self.upper - self.lower + 1)]
21
+
22
+ def __getitem__(self, index: int) -> Any:
23
+ idx = int(index) - self.lower
24
+ if idx < 0 or idx >= len(self._values):
25
+ raise IndexError(f"ARRAY Index {index} ausserhalb {self.lower}..{self.upper}")
26
+ return self._values[idx]
27
+
28
+ def __setitem__(self, index: int, value: Any) -> None:
29
+ idx = int(index) - self.lower
30
+ if idx < 0 or idx >= len(self._values):
31
+ raise IndexError(f"ARRAY Index {index} ausserhalb {self.lower}..{self.upper}")
32
+ self._values[idx] = value
33
+
34
+ def to_list(self) -> List[Any]:
35
+ return [to_plain(v) for v in self._values]
36
+
37
+
38
+ class STStruct:
39
+ def __init__(self, **values):
40
+ object.__setattr__(self, "_values", dict(values))
41
+
42
+ def __getattr__(self, name: str) -> Any:
43
+ values = object.__getattribute__(self, "_values")
44
+ key = find_key(values, name)
45
+ if key is None:
46
+ raise AttributeError(name)
47
+ return values[key]
48
+
49
+ def __setattr__(self, name: str, value: Any) -> None:
50
+ values = object.__getattribute__(self, "_values")
51
+ key = find_key(values, name) or name
52
+ values[key] = value
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ return {k: to_plain(v) for k, v in self._values.items()}
56
+
57
+
58
+ def find_key(mapping: Dict[str, Any], name: str) -> Optional[str]:
59
+ if name in mapping:
60
+ return name
61
+ wanted = name.upper()
62
+ for key in mapping.keys():
63
+ if key.upper() == wanted:
64
+ return key
65
+ return None
66
+
67
+
68
+ def to_plain(value: Any) -> Any:
69
+ if isinstance(value, STArray):
70
+ return value.to_list()
71
+ if isinstance(value, STStruct):
72
+ return value.to_dict()
73
+ if isinstance(value, FunctionBlockInstance):
74
+ return value.export_public()
75
+ if hasattr(value, "__dict__"):
76
+ return {k: to_plain(v) for k, v in value.__dict__.items() if not k.startswith("_")}
77
+ return value
78
+
79
+
80
+ def clone_value(value: Any) -> Any:
81
+ if isinstance(value, FunctionBlockInstance):
82
+ return value.clone()
83
+ return copy.deepcopy(value)
84
+
85
+
86
+ class TON:
87
+ def __init__(self) -> None:
88
+ self.IN = False
89
+ self.PT = 0
90
+ self.Q = False
91
+ self.ET = 0
92
+ self._start: Optional[float] = None
93
+
94
+ def __call__(self, IN: Optional[bool] = None, PT: Optional[int] = None, **kwargs) -> "TON":
95
+ if IN is not None:
96
+ self.IN = bool(IN)
97
+ if PT is not None:
98
+ self.PT = int(PT)
99
+ now = time.perf_counter()
100
+ if self.IN:
101
+ if self._start is None:
102
+ self._start = now
103
+ self.ET = int((now - self._start) * 1000.0)
104
+ self.Q = self.ET >= int(self.PT)
105
+ else:
106
+ self._start = None
107
+ self.ET = 0
108
+ self.Q = False
109
+ return self
110
+
111
+
112
+ class TOF:
113
+ def __init__(self) -> None:
114
+ self.IN = False
115
+ self.PT = 0
116
+ self.Q = False
117
+ self.ET = 0
118
+ self._fall: Optional[float] = None
119
+ self._prev = False
120
+
121
+ def __call__(self, IN: Optional[bool] = None, PT: Optional[int] = None, **kwargs) -> "TOF":
122
+ if IN is not None:
123
+ self.IN = bool(IN)
124
+ if PT is not None:
125
+ self.PT = int(PT)
126
+ now = time.perf_counter()
127
+ if self.IN:
128
+ self.Q = True
129
+ self.ET = 0
130
+ self._fall = None
131
+ else:
132
+ if self._prev and self._fall is None:
133
+ self._fall = now
134
+ if self._fall is None:
135
+ self.Q = False
136
+ self.ET = 0
137
+ else:
138
+ self.ET = int((now - self._fall) * 1000.0)
139
+ self.Q = self.ET < int(self.PT)
140
+ self._prev = self.IN
141
+ return self
142
+
143
+
144
+ class TP:
145
+ def __init__(self) -> None:
146
+ self.IN = False
147
+ self.PT = 0
148
+ self.Q = False
149
+ self.ET = 0
150
+ self._start: Optional[float] = None
151
+ self._prev = False
152
+
153
+ def __call__(self, IN: Optional[bool] = None, PT: Optional[int] = None, **kwargs) -> "TP":
154
+ if IN is not None:
155
+ self.IN = bool(IN)
156
+ if PT is not None:
157
+ self.PT = int(PT)
158
+ now = time.perf_counter()
159
+ rising = self.IN and not self._prev
160
+ if rising and not self.Q:
161
+ self._start = now
162
+ self.Q = True
163
+ if self.Q and self._start is not None:
164
+ self.ET = int((now - self._start) * 1000.0)
165
+ if self.ET >= int(self.PT):
166
+ self.Q = False
167
+ self.ET = int(self.PT)
168
+ self._start = None
169
+ else:
170
+ self.ET = 0
171
+ self._prev = self.IN
172
+ return self
173
+
174
+
175
+ class R_TRIG:
176
+ def __init__(self) -> None:
177
+ self.CLK = False
178
+ self.Q = False
179
+ self.M = False
180
+
181
+ def __call__(self, CLK: Optional[bool] = None, **kwargs) -> "R_TRIG":
182
+ if CLK is not None:
183
+ self.CLK = bool(CLK)
184
+ self.Q = bool(self.CLK and not self.M)
185
+ self.M = bool(self.CLK)
186
+ return self
187
+
188
+
189
+ class F_TRIG:
190
+ def __init__(self) -> None:
191
+ self.CLK = False
192
+ self.Q = False
193
+ self.M = False
194
+
195
+ def __call__(self, CLK: Optional[bool] = None, **kwargs) -> "F_TRIG":
196
+ if CLK is not None:
197
+ self.CLK = bool(CLK)
198
+ self.Q = bool((not self.CLK) and self.M)
199
+ self.M = bool(self.CLK)
200
+ return self
201
+
202
+
203
+ class RS:
204
+ def __init__(self) -> None:
205
+ self.S = False
206
+ self.R1 = False
207
+ self.Q1 = False
208
+
209
+ def __call__(self, S: Optional[bool] = None, R1: Optional[bool] = None, **kwargs) -> "RS":
210
+ if S is not None:
211
+ self.S = bool(S)
212
+ if R1 is not None:
213
+ self.R1 = bool(R1)
214
+ self.Q1 = bool((self.Q1 or self.S) and not self.R1)
215
+ return self
216
+
217
+
218
+ class SR:
219
+ def __init__(self) -> None:
220
+ self.S1 = False
221
+ self.R = False
222
+ self.Q1 = False
223
+
224
+ def __call__(self, S1: Optional[bool] = None, R: Optional[bool] = None, **kwargs) -> "SR":
225
+ if S1 is not None:
226
+ self.S1 = bool(S1)
227
+ if R is not None:
228
+ self.R = bool(R)
229
+ self.Q1 = bool(self.S1 or (self.Q1 and not self.R))
230
+ return self
231
+
232
+
233
+ class CTU:
234
+ def __init__(self) -> None:
235
+ self.CU = False
236
+ self.RESET = False
237
+ self.PV = 0
238
+ self.Q = False
239
+ self.CV = 0
240
+ self._prev_cu = False
241
+
242
+ def __call__(self, CU: Optional[bool] = None, RESET: Optional[bool] = None, PV: Optional[int] = None, **kwargs) -> "CTU":
243
+ if CU is not None:
244
+ self.CU = bool(CU)
245
+ if RESET is not None:
246
+ self.RESET = bool(RESET)
247
+ if PV is not None:
248
+ self.PV = int(PV)
249
+ if self.RESET:
250
+ self.CV = 0
251
+ elif self.CU and not self._prev_cu:
252
+ self.CV += 1
253
+ self.Q = self.CV >= self.PV
254
+ self._prev_cu = self.CU
255
+ return self
256
+
257
+
258
+ BUILTIN_FB = {
259
+ "TON": TON,
260
+ "TOF": TOF,
261
+ "TP": TP,
262
+ "R_TRIG": R_TRIG,
263
+ "F_TRIG": F_TRIG,
264
+ "RS": RS,
265
+ "SR": SR,
266
+ "CTU": CTU,
267
+ }
268
+
269
+
270
+ @dataclass
271
+ class VarDecl:
272
+ name: str
273
+ type_name: str
274
+ default_text: Optional[str] = None
275
+ section: str = "VAR"
276
+ address: Optional[str] = None
277
+
278
+
279
+ @dataclass
280
+ class POU:
281
+ kind: str
282
+ name: str
283
+ return_type: Optional[str]
284
+ declarations: List[VarDecl] = field(default_factory=list)
285
+ statements: List[str] = field(default_factory=list)
286
+
287
+
288
+ class FunctionBlockInstance:
289
+ def __init__(self, pou: POU, compiler: "STCompiler") -> None:
290
+ self._pou = pou
291
+ self._compiler = compiler
292
+ self._vars = compiler.instantiate_declarations(pou.declarations, include_inputs=True)
293
+ self._temp_decls = [d for d in pou.declarations if d.section == "VAR_TEMP"]
294
+ self._runtime: Optional[STRuntime] = None
295
+ if pou.kind == "FUNCTION":
296
+ self._vars[pou.name] = compiler.default_for_type(pou.return_type or "BOOL")
297
+
298
+ def clone(self) -> "FunctionBlockInstance":
299
+ other = FunctionBlockInstance.__new__(FunctionBlockInstance)
300
+ other._pou = self._pou
301
+ other._compiler = self._compiler
302
+ other._vars = clone_value(self._vars)
303
+ other._temp_decls = list(self._temp_decls)
304
+ other._runtime = None
305
+ return other
306
+
307
+ def __getattr__(self, name: str) -> Any:
308
+ key = find_key(self._vars, name)
309
+ if key is None:
310
+ raise AttributeError(name)
311
+ return self._vars[key]
312
+
313
+ def __setattr__(self, name: str, value: Any) -> None:
314
+ if name.startswith("_"):
315
+ object.__setattr__(self, name, value)
316
+ else:
317
+ key = find_key(self._vars, name) or name
318
+ self._vars[key] = value
319
+ # Wichtig für verschachtelte FBs: Wird ein FB-Eingang per
320
+ # Instanz.Attribut := Wert gesetzt, muss auch die bereits erzeugte
321
+ # Runtime-Umgebung der Instanz aktualisiert werden. Sonst arbeitet
322
+ # der FB im nächsten Aufruf mit alten Eingängen. Das war die Ursache,
323
+ # warum z. B. LDC.Enabled, LDSC.PushButtonOn und EFuseControl.INx
324
+ # im Example nicht korrekt wirksam wurden.
325
+ runtime = getattr(self, "_runtime", None)
326
+ env = getattr(runtime, "env", None)
327
+ if isinstance(env, dict):
328
+ env[key] = value
329
+ env[key.upper()] = value
330
+
331
+ def _reset_temp_vars(self) -> None:
332
+ # IEC 61131-3: VAR_TEMP ist pro Bausteinaufruf temporär und darf nicht
333
+ # zyklusübergreifend speichern. STAT/VAR und VAR_OUTPUT behalten ihren Zustand.
334
+ for decl in self._temp_decls:
335
+ self._vars[decl.name] = self._compiler.make_default_value(decl.type_name, decl.default_text)
336
+
337
+ def __call__(self, **params) -> Any:
338
+ if self._runtime is None:
339
+ self._runtime = STRuntime(self._compiler, self._vars)
340
+ self._reset_temp_vars()
341
+ self._runtime.refresh_env()
342
+ for key, value in params.items():
343
+ real_key = find_key(self._vars, key) or key
344
+ self._vars[real_key] = value
345
+ self._runtime.env[real_key] = value
346
+ self._runtime.env[real_key.upper()] = value
347
+ self._runtime.execute_block(self._pou.statements)
348
+ if self._pou.kind == "FUNCTION":
349
+ key = find_key(self._vars, self._pou.name) or self._pou.name
350
+ return self._vars.get(key)
351
+ return self
352
+
353
+ def export_public(self) -> Dict[str, Any]:
354
+ return {k: to_plain(v) for k, v in self._vars.items() if not k.startswith("_")}
355
+
356
+
357
+ class STRuntime:
358
+ def __init__(self, compiler: "STCompiler", variables: Dict[str, Any]) -> None:
359
+ self.compiler = compiler
360
+ self.variables = variables
361
+ # Performance: environment once per runtime instead of once per expression.
362
+ # Scalar assignments keep this env synchronized in set_target().
363
+ self.env = self.make_env()
364
+
365
+ def refresh_env(self) -> None:
366
+ # Runtime-Umgebungen werden aus Performancegründen wiederverwendet.
367
+ # Vor jedem äußeren Bausteinaufruf müssen globale Werte und lokale
368
+ # Variablen wieder in die eval-Umgebung gespiegelt werden, sonst sehen
369
+ # verschachtelte FBs veraltete HMI-/GLOBAL-Werte.
370
+ for key, value in self.compiler.global_values.items():
371
+ self.env[key] = value
372
+ self.env[key.upper()] = value
373
+ for key, value in self.variables.items():
374
+ self.env[key] = value
375
+ self.env[key.upper()] = value
376
+
377
+ def execute_block(self, lines: List[str]) -> None:
378
+ i = 0
379
+ while i < len(lines):
380
+ line = lines[i].strip()
381
+ if not line:
382
+ i += 1
383
+ continue
384
+ upper = line.upper()
385
+ if upper.startswith("IF ") and "END_IF" in upper:
386
+ inline_match = re.match(r"IF\s+(.+?)\s+THEN\s+(.*?)\s*END_IF\s*$", line, flags=re.IGNORECASE | re.DOTALL)
387
+ if not inline_match:
388
+ raise STCompileError(f"Inline-IF Syntax nicht erkannt: {line}")
389
+ if bool(self.eval_expr(inline_match.group(1).strip())):
390
+ self.execute_block(self.compiler.split_statements(inline_match.group(2).strip()))
391
+ i += 1
392
+ continue
393
+ if upper.startswith("IF "):
394
+ branches, end_idx = self.collect_if(lines, i)
395
+ executed = False
396
+ for condition, block in branches:
397
+ if condition is None or bool(self.eval_expr(condition)):
398
+ self.execute_block(block)
399
+ executed = True
400
+ break
401
+ i = end_idx + 1
402
+ continue
403
+ if upper.startswith("FOR "):
404
+ var_name, start_expr, end_expr, step_expr, block, end_idx = self.collect_for(lines, i)
405
+ start_val = int(self.eval_expr(start_expr))
406
+ end_val = int(self.eval_expr(end_expr))
407
+ step_val = int(self.eval_expr(step_expr)) if step_expr else 1
408
+ if step_val == 0:
409
+ raise STCompileError("FOR STEP darf nicht 0 sein.")
410
+ cmp = (lambda a, b: a <= b) if step_val > 0 else (lambda a, b: a >= b)
411
+ value = start_val
412
+ while cmp(value, end_val):
413
+ self.set_target(var_name, value)
414
+ self.execute_block(block)
415
+ value += step_val
416
+ i = end_idx + 1
417
+ continue
418
+ if upper.startswith("WHILE "):
419
+ condition, block, end_idx = self.collect_while(lines, i)
420
+ guard = 0
421
+ while bool(self.eval_expr(condition)):
422
+ self.execute_block(block)
423
+ guard += 1
424
+ if guard > 100000:
425
+ raise STCompileError("WHILE Schutzlimit erreicht.")
426
+ i = end_idx + 1
427
+ continue
428
+ if upper.startswith("CASE "):
429
+ selector, branches, end_idx = self.collect_case(lines, i)
430
+ selector_value = self.eval_expr(selector)
431
+ selected_block = None
432
+ else_block = None
433
+ for labels, block in branches:
434
+ if labels is None:
435
+ else_block = block
436
+ continue
437
+ for label in labels:
438
+ if self.eval_expr(label) == selector_value:
439
+ selected_block = block
440
+ break
441
+ if selected_block is not None:
442
+ break
443
+ if selected_block is None:
444
+ selected_block = else_block
445
+ if selected_block is not None:
446
+ self.execute_block(selected_block)
447
+ i = end_idx + 1
448
+ continue
449
+ if upper in {"ELSE", "END_IF", "END_FOR", "END_WHILE", "END_CASE"} or upper.startswith("ELSIF "):
450
+ return
451
+ self.execute_statement(line)
452
+ i += 1
453
+
454
+ def collect_if(self, lines: List[str], start: int) -> Tuple[List[Tuple[Optional[str], List[str]]], int]:
455
+ first = lines[start].strip()
456
+ condition = first[2:].strip()
457
+ if condition.upper().endswith("THEN"):
458
+ condition = condition[:-4].strip()
459
+ branches: List[Tuple[Optional[str], List[str]]] = [(condition, [])]
460
+ active = branches[-1][1]
461
+ depth = 0
462
+ for idx in range(start + 1, len(lines)):
463
+ line = lines[idx].strip()
464
+ upper = line.upper()
465
+ if upper.startswith("IF "):
466
+ depth += 1
467
+ active.append(line)
468
+ continue
469
+ if upper in {"END_IF", "END_IF;"}:
470
+ if depth == 0:
471
+ return branches, idx
472
+ depth -= 1
473
+ active.append(line)
474
+ continue
475
+ if depth == 0 and upper.startswith("ELSIF "):
476
+ cond = line[5:].strip()
477
+ if cond.upper().endswith("THEN"):
478
+ cond = cond[:-4].strip()
479
+ branches.append((cond, []))
480
+ active = branches[-1][1]
481
+ continue
482
+ if depth == 0 and upper in {"ELSE", "ELSE;"}:
483
+ branches.append((None, []))
484
+ active = branches[-1][1]
485
+ continue
486
+ active.append(line)
487
+ raise STCompileError("IF ohne END_IF gefunden.")
488
+
489
+ def collect_for(self, lines: List[str], start: int):
490
+ first = lines[start].strip().rstrip(";")
491
+ match = re.match(r"FOR\s+(.+?)\s*:=\s*(.+?)\s+TO\s+(.+?)(?:\s+BY\s+(.+?))?\s+DO\s*$", first, re.IGNORECASE)
492
+ if not match:
493
+ raise STCompileError(f"FOR Syntax nicht erkannt: {first}")
494
+ var_name, start_expr, end_expr, step_expr = match.group(1), match.group(2), match.group(3), match.group(4)
495
+ block: List[str] = []
496
+ depth = 0
497
+ for idx in range(start + 1, len(lines)):
498
+ line = lines[idx].strip()
499
+ upper = line.upper()
500
+ if upper.startswith("FOR "):
501
+ depth += 1
502
+ block.append(line)
503
+ continue
504
+ if upper in {"END_FOR", "END_FOR;"}:
505
+ if depth == 0:
506
+ return var_name.strip(), start_expr.strip(), end_expr.strip(), (step_expr or "1").strip(), block, idx
507
+ depth -= 1
508
+ block.append(line)
509
+ continue
510
+ block.append(line)
511
+ raise STCompileError("FOR ohne END_FOR gefunden.")
512
+
513
+ def collect_while(self, lines: List[str], start: int):
514
+ first = lines[start].strip().rstrip(";")
515
+ match = re.match(r"WHILE\s+(.+?)\s+DO\s*$", first, re.IGNORECASE)
516
+ if not match:
517
+ raise STCompileError(f"WHILE Syntax nicht erkannt: {first}")
518
+ block: List[str] = []
519
+ depth = 0
520
+ for idx in range(start + 1, len(lines)):
521
+ line = lines[idx].strip()
522
+ upper = line.upper()
523
+ if upper.startswith("WHILE "):
524
+ depth += 1
525
+ block.append(line)
526
+ continue
527
+ if upper in {"END_WHILE", "END_WHILE;"}:
528
+ if depth == 0:
529
+ return match.group(1).strip(), block, idx
530
+ depth -= 1
531
+ block.append(line)
532
+ continue
533
+ block.append(line)
534
+ raise STCompileError("WHILE ohne END_WHILE gefunden.")
535
+
536
+ def collect_case(self, lines: List[str], start: int):
537
+ first = lines[start].strip().rstrip(";")
538
+ match = re.match(r"CASE\s+(.+?)\s+OF\s*$", first, re.IGNORECASE)
539
+ if not match:
540
+ raise STCompileError(f"CASE Syntax nicht erkannt: {first}")
541
+ selector = match.group(1).strip()
542
+ branches: List[Tuple[Optional[List[str]], List[str]]] = []
543
+ active: Optional[List[str]] = None
544
+ depth_case = 0
545
+ depth_if = 0
546
+ depth_for = 0
547
+ depth_while = 0
548
+
549
+ def inside_nested_block() -> bool:
550
+ return depth_case > 0 or depth_if > 0 or depth_for > 0 or depth_while > 0
551
+
552
+ for idx in range(start + 1, len(lines)):
553
+ line = lines[idx].strip()
554
+ upper = line.upper().rstrip(";")
555
+
556
+ # CASE muss IF/ELSE/END_IF innerhalb eines CASE-Zweigs unverändert als normalen Code behalten.
557
+ # Vorher wurde jedes ELSE auf CASE-Ebene als CASE-ELSE interpretiert. Dadurch wurden IF-Blöcke
558
+ # innerhalb der CASE-Zweige zerschnitten und die Runtime stoppte mit "IF ohne END_IF gefunden".
559
+ if upper.startswith("IF "):
560
+ if active is not None:
561
+ active.append(line)
562
+ depth_if += 1
563
+ # Einzeilige IFs mit END_IF direkt in derselben Zeile erhöhen die Tiefe nicht dauerhaft.
564
+ if "END_IF" in upper:
565
+ depth_if = max(0, depth_if - 1)
566
+ continue
567
+ if upper.startswith("ELSIF "):
568
+ if active is not None:
569
+ active.append(line)
570
+ continue
571
+ if upper == "ELSE" and depth_if > 0:
572
+ if active is not None:
573
+ active.append(line)
574
+ continue
575
+ if upper == "END_IF":
576
+ if active is not None:
577
+ active.append(line)
578
+ depth_if = max(0, depth_if - 1)
579
+ continue
580
+
581
+ if upper.startswith("FOR "):
582
+ if active is not None:
583
+ active.append(line)
584
+ depth_for += 1
585
+ continue
586
+ if upper == "END_FOR":
587
+ if active is not None:
588
+ active.append(line)
589
+ depth_for = max(0, depth_for - 1)
590
+ continue
591
+
592
+ if upper.startswith("WHILE "):
593
+ if active is not None:
594
+ active.append(line)
595
+ depth_while += 1
596
+ continue
597
+ if upper == "END_WHILE":
598
+ if active is not None:
599
+ active.append(line)
600
+ depth_while = max(0, depth_while - 1)
601
+ continue
602
+
603
+ if upper.startswith("CASE "):
604
+ if active is not None:
605
+ active.append(line)
606
+ depth_case += 1
607
+ continue
608
+ if upper == "END_CASE":
609
+ if depth_case == 0 and not inside_nested_block():
610
+ return selector, branches, idx
611
+ if active is not None:
612
+ active.append(line)
613
+ depth_case = max(0, depth_case - 1)
614
+ continue
615
+
616
+ if not inside_nested_block() and upper == "ELSE":
617
+ branches.append((None, []))
618
+ active = branches[-1][1]
619
+ continue
620
+ if not inside_nested_block() and upper.startswith("ELSE "):
621
+ # IEC-ST erlaubt auch kompakte CASE-ELSE-Zweige wie:
622
+ # ELSE CURRENT := 0; req := IN1;
623
+ # Diese Zeile ist kein normaler IF-ELSE, sondern der Default-Zweig des CASE.
624
+ branches.append((None, []))
625
+ active = branches[-1][1]
626
+ rest = line[4:].strip()
627
+ if rest:
628
+ active.append(rest)
629
+ continue
630
+ # CASE label can be either on its own line:
631
+ # 0:
632
+ # or on the same line as the first statement:
633
+ # 0: req := IN1
634
+ # BCC_OFF: nextState := BCC_ON
635
+ # Wichtig: Eine normale ST-Zuweisung wie "WAIT1 := wait" darf NICHT
636
+ # als CASE-Label "WAIT1" mit Rest "= wait" interpretiert werden.
637
+ # Deshalb akzeptieren wir nur Doppelpunkte, die nicht Teil von ":=" sind.
638
+ label_match = re.match(r"^(.+?)(?<!:):(?!\=)\s*(.*)$", line)
639
+ if not inside_nested_block() and label_match:
640
+ labels = [part.strip() for part in split_top_level(label_match.group(1), ",") if part.strip()]
641
+ branches.append((labels, []))
642
+ active = branches[-1][1]
643
+ rest = (label_match.group(2) or "").strip()
644
+ if rest:
645
+ active.append(rest)
646
+ continue
647
+ if active is not None:
648
+ active.append(line)
649
+ raise STCompileError("CASE ohne END_CASE gefunden.")
650
+
651
+ def execute_statement(self, line: str) -> None:
652
+ line = line.strip().rstrip(";")
653
+ if not line:
654
+ return
655
+ call_match = re.match(r"^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)\s*\((.*)\)$", line, re.DOTALL)
656
+ if call_match and ":=" not in call_match.group(1):
657
+ target = call_match.group(1).strip()
658
+ arg_text = call_match.group(2)
659
+ positional_raw, inputs, outputs = self.parse_call_arguments_raw(arg_text)
660
+
661
+ pou_key = self.compiler.find_pou_key(target)
662
+ if pou_key and self.compiler.pous[pou_key].kind == "FUNCTION":
663
+ positional_values = [self.eval_expr(item) for item in positional_raw]
664
+ result = self.compiler.call_function_instance(target, positional_values, inputs)
665
+ else:
666
+ fb = self.eval_expr(target)
667
+ if not callable(fb):
668
+ raise STCompileError(f"{target} ist nicht aufrufbar.")
669
+
670
+ positional_output_bindings: Dict[str, str] = {}
671
+ if positional_raw:
672
+ input_names: List[str] = []
673
+ output_names: List[str] = []
674
+ if isinstance(fb, FunctionBlockInstance):
675
+ input_names = [d.name for d in fb._pou.declarations if d.section.startswith("VAR_INPUT") or d.section == "VAR_IN_OUT"]
676
+ output_names = [d.name for d in fb._pou.declarations if d.section.startswith("VAR_OUTPUT")]
677
+ elif hasattr(fb, "__dict__"):
678
+ input_names = [name for name in fb.__dict__.keys() if not name.startswith("_")]
679
+
680
+ for name, expr in zip(input_names, positional_raw):
681
+ inputs[name] = self.eval_expr(expr)
682
+
683
+ # Einige im Example verwendete Aufrufe nutzen den letzten
684
+ # positionalen Parameter als Zielvariable für den ersten
685
+ # Ausgang, z. B.:
686
+ # HyX(A, B, H, CMP_Type#GT, ResultVar);
687
+ # Das entspricht praktisch Q => ResultVar und muss nach dem
688
+ # FB-Aufruf zurückgeschrieben werden.
689
+ extra_positionals = positional_raw[len(input_names):]
690
+ for out_name, dest in zip(output_names, extra_positionals):
691
+ positional_output_bindings[out_name] = dest
692
+
693
+ result = fb(**inputs)
694
+ outputs.update(positional_output_bindings)
695
+
696
+ source = result if result is not None else self.eval_expr(target)
697
+ for out_name, dest in outputs.items():
698
+ try:
699
+ value = getattr(source, out_name)
700
+ except Exception:
701
+ value = getattr(self.eval_expr(target), out_name)
702
+ self.set_target(dest, value)
703
+ return
704
+ if ":=" in line:
705
+ left, right = line.split(":=", 1)
706
+ self.set_target(left.strip(), self.eval_expr(right.strip()))
707
+ return
708
+ value = self.eval_expr(line)
709
+ if callable(value):
710
+ value()
711
+
712
+ def parse_call_arguments_raw(self, text: str) -> Tuple[List[str], Dict[str, Any], Dict[str, str]]:
713
+ positional: List[str] = []
714
+ inputs: Dict[str, Any] = {}
715
+ outputs: Dict[str, str] = {}
716
+ for part in split_top_level(text, ","):
717
+ item = part.strip()
718
+ if not item:
719
+ continue
720
+ if "=>" in item:
721
+ key, dest = item.split("=>", 1)
722
+ outputs[key.strip()] = dest.strip()
723
+ elif ":=" in item:
724
+ key, expr = item.split(":=", 1)
725
+ inputs[key.strip()] = self.eval_expr(expr.strip())
726
+ else:
727
+ positional.append(item)
728
+ return positional, inputs, outputs
729
+
730
+ def parse_call_arguments(self, text: str) -> Tuple[List[Any], Dict[str, Any], Dict[str, str]]:
731
+ positional: List[Any] = []
732
+ inputs: Dict[str, Any] = {}
733
+ outputs: Dict[str, str] = {}
734
+ for part in split_top_level(text, ","):
735
+ if not part.strip():
736
+ continue
737
+ if "=>" in part:
738
+ key, dest = part.split("=>", 1)
739
+ outputs[key.strip()] = dest.strip()
740
+ elif ":=" in part:
741
+ key, expr = part.split(":=", 1)
742
+ inputs[key.strip()] = self.eval_expr(expr.strip())
743
+ else:
744
+ positional.append(self.eval_expr(part.strip()))
745
+ return positional, inputs, outputs
746
+
747
+ def parse_call_params(self, text: str) -> Dict[str, Any]:
748
+ _, inputs, _ = self.parse_call_arguments(text)
749
+ return inputs
750
+
751
+ def eval_expr(self, expr: str) -> Any:
752
+ text = expr.strip().rstrip(";")
753
+ if not text:
754
+ return None
755
+ try:
756
+ code = self.compiler.compile_expr(text)
757
+ return eval(code, {"__builtins__": {}}, self.env)
758
+ except Exception as exc:
759
+ raise STCompileError(f"Ausdruck nicht auswertbar: '{expr}' -> {exc}") from exc
760
+
761
+ def make_env(self) -> Dict[str, Any]:
762
+ env: Dict[str, Any] = {
763
+ "ABS": abs,
764
+ "MIN": min,
765
+ "MAX": max,
766
+ "LIMIT": lambda mn, v, mx: max(mn, min(v, mx)),
767
+ "SEL": lambda g, in0, in1: in1 if g else in0,
768
+ "MUX": lambda k, *args: args[int(k)],
769
+ "INT": int,
770
+ "DINT": int,
771
+ "REAL": float,
772
+ "LREAL": float,
773
+ "BOOL": bool,
774
+ "INT_TO_REAL": float,
775
+ "REAL_TO_INT": lambda x: int(round(x)),
776
+ "DINT_TO_REAL": float,
777
+ "REAL_TO_DINT": lambda x: int(round(x)),
778
+ "BYTE_TO_INT": int,
779
+ "WORD_TO_INT": int,
780
+ "BIT": lambda value, bit: (int(value) >> int(bit)) & 1,
781
+ "SIN": math.sin,
782
+ "COS": math.cos,
783
+ "TAN": math.tan,
784
+ "SQRT": math.sqrt,
785
+ }
786
+ for key, value in self.compiler.constants.items():
787
+ env[key] = value
788
+ env[key.upper()] = value
789
+ for key, value in self.compiler.global_values.items():
790
+ env[key] = value
791
+ env[key.upper()] = value
792
+ for key, value in self.variables.items():
793
+ env[key] = value
794
+ env[key.upper()] = value
795
+ for name, pou in self.compiler.pous.items():
796
+ if pou.kind == "FUNCTION":
797
+ env[name] = self.compiler.make_function_callable(pou)
798
+ env[name.upper()] = env[name]
799
+ return env
800
+
801
+ def set_target(self, target: str, value: Any) -> None:
802
+ target = target.strip()
803
+ # Simple variable
804
+ if re.fullmatch(r"[A-Za-z_]\w*", target):
805
+ key = find_key(self.variables, target)
806
+ if key is not None:
807
+ self.variables[key] = value
808
+ self.env[key] = value
809
+ self.env[key.upper()] = value
810
+ else:
811
+ gkey = find_key(self.compiler.global_values, target)
812
+ if gkey is not None:
813
+ self.compiler.global_values[gkey] = value
814
+ self.env[gkey] = value
815
+ self.env[gkey.upper()] = value
816
+ else:
817
+ self.variables[target] = value
818
+ self.env[target] = value
819
+ self.env[target.upper()] = value
820
+ return
821
+ # Indexed target: ARR[i]
822
+ m = re.match(r"^(.+)\[([^\]]+)\]$", target)
823
+ if m:
824
+ arr = self.eval_expr(m.group(1).strip())
825
+ idx = self.eval_expr(m.group(2).strip())
826
+ arr[idx] = value
827
+ return
828
+ # Attribute target: Inst.Out or Struct.Field
829
+ if "." in target:
830
+ obj_expr, attr = target.rsplit(".", 1)
831
+ obj = self.eval_expr(obj_expr.strip())
832
+ attr_name = attr.strip()
833
+ setattr(obj, attr_name, value)
834
+ # Zusätzliche Absicherung für Objekte mit vorbereiteter Runtime.
835
+ # FunctionBlockInstance.__setattr__ synchronisiert bereits, aber
836
+ # STStruct/native Objekte können ebenfalls eine _runtime besitzen.
837
+ runtime = getattr(obj, "_runtime", None)
838
+ env = getattr(runtime, "env", None)
839
+ vars_dict = getattr(obj, "_vars", None)
840
+ if isinstance(env, dict):
841
+ key = find_key(vars_dict, attr_name) if isinstance(vars_dict, dict) else attr_name
842
+ key = key or attr_name
843
+ env[key] = value
844
+ env[key.upper()] = value
845
+ return
846
+ raise STCompileError(f"Zuweisungsziel nicht unterstuetzt: {target}")
847
+
848
+
849
+ class STProgram:
850
+ def __init__(self, compiler: "STCompiler", main: POU, source_files: List[str]) -> None:
851
+ self.compiler = compiler
852
+ self.main = main
853
+ self.name = main.name
854
+ self.source_files = source_files
855
+ self.variables = compiler.global_values
856
+ self.variables.update(compiler.instantiate_declarations(main.declarations, include_inputs=True))
857
+ self.statements = main.statements
858
+ self.last_error: Optional[str] = None
859
+ self._runtime = STRuntime(self.compiler, self.variables)
860
+
861
+ def execute(self, inputs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
862
+ if inputs:
863
+ for key, value in inputs.items():
864
+ real_key = find_key(self.variables, key) or key
865
+ self.variables[real_key] = value
866
+ self._runtime.env[real_key] = value
867
+ self._runtime.env[real_key.upper()] = value
868
+ try:
869
+ self._runtime.refresh_env()
870
+ self._runtime.execute_block(self.statements)
871
+ self.last_error = None
872
+ except Exception as exc:
873
+ self.last_error = str(exc)
874
+ raise
875
+ return {k: to_plain(v) for k, v in self.variables.items()}
876
+
877
+
878
+ class STCompiler:
879
+ SIMPLE_TYPES = {"BOOL", "BYTE", "WORD", "DWORD", "INT", "DINT", "SINT", "UINT", "UDINT", "REAL", "LREAL", "TIME", "STRING"}
880
+
881
+ def __init__(self) -> None:
882
+ self.pous: Dict[str, POU] = {}
883
+ self.struct_types: Dict[str, List[VarDecl]] = {}
884
+ self.alias_types: Dict[str, str] = {}
885
+ self.constants: Dict[str, Any] = {}
886
+ self.global_declarations: List[VarDecl] = []
887
+ self.global_values: Dict[str, Any] = {}
888
+ self._expr_cache: Dict[str, Any] = {}
889
+ self.compile_log: List[Dict[str, Any]] = []
890
+
891
+ def log(self, level: str, message: str, **details: Any) -> None:
892
+ entry: Dict[str, Any] = {
893
+ "time": datetime.now().strftime("%H:%M:%S.%f")[:-3],
894
+ "level": level.upper(),
895
+ "message": message,
896
+ }
897
+ if details:
898
+ entry["details"] = {key: to_plain(value) for key, value in details.items()}
899
+ self.compile_log.append(entry)
900
+
901
+ def info(self, message: str, **details: Any) -> None:
902
+ self.log("INFO", message, **details)
903
+
904
+ def warn(self, message: str, **details: Any) -> None:
905
+ self.log("WARN", message, **details)
906
+
907
+ def error(self, message: str, **details: Any) -> None:
908
+ self.log("ERROR", message, **details)
909
+
910
+ @staticmethod
911
+ def compile_path(path: Path) -> STProgram:
912
+ files: List[Tuple[str, str]] = []
913
+ if path.suffix.lower() == ".zip":
914
+ with zipfile.ZipFile(path, "r") as archive:
915
+ for name in archive.namelist():
916
+ if name.lower().endswith(".st") and not name.endswith("/"):
917
+ raw = archive.read(name)
918
+ files.append((name, raw.decode("utf-8", errors="replace")))
919
+ elif path.suffix.lower() == ".st":
920
+ files.append((path.name, path.read_text(encoding="utf-8", errors="replace")))
921
+ else:
922
+ files.append((path.name, path.read_text(encoding="utf-8", errors="replace")))
923
+ if not files:
924
+ raise STCompileError("Keine ST-Dateien im Projekt gefunden.")
925
+ return STCompiler.compile_sources(files)
926
+
927
+ @staticmethod
928
+ def compile_sources(files: List[Tuple[str, str]]) -> STProgram:
929
+ compiler = STCompiler()
930
+ source_files = [name for name, _ in files]
931
+ compiler.info("Compiler gestartet", source_files=len(files), files=source_files)
932
+ try:
933
+ for name, content in files:
934
+ compiler.info("Quelldatei geladen", file=name, chars=len(content), lines=len(content.splitlines()))
935
+
936
+ full_source = "\n".join(content for _, content in files)
937
+ compiler.info("Quellen zusammengeführt", chars=len(full_source), lines=len(full_source.splitlines()))
938
+
939
+ clean_source = compiler.remove_comments(full_source)
940
+ compiler.info("Kommentare entfernt", chars=len(clean_source), lines=len(clean_source.splitlines()))
941
+
942
+ compiler.parse_types(clean_source)
943
+ compiler.info(
944
+ "Datentypen ausgewertet",
945
+ structs=len(compiler.struct_types),
946
+ aliases=len(compiler.alias_types),
947
+ constants=len(compiler.constants),
948
+ struct_names=sorted(compiler.struct_types.keys()),
949
+ alias_names=sorted(compiler.alias_types.keys()),
950
+ )
951
+
952
+ compiler.extract_global_declarations(clean_source)
953
+ compiler.info("Globale Deklarationen ausgewertet", variables=len(compiler.global_declarations))
954
+
955
+ compiler.parse_pous(clean_source)
956
+ compiler.info("POUs ausgewertet", count=len(compiler.pous), names=[f"{p.kind} {p.name}" for p in compiler.pous.values()])
957
+
958
+ compiler.global_values = compiler.instantiate_declarations(compiler.global_declarations, include_inputs=True)
959
+ compiler.info("Globale Variablen instanziiert", variables=len(compiler.global_values))
960
+
961
+ if not compiler.pous:
962
+ decls, code = compiler.extract_vars_and_code(clean_source)
963
+ statements = compiler.split_statements(code)
964
+ main = POU(kind="PROGRAM", name="Main", return_type=None, declarations=decls, statements=statements)
965
+ compiler.pous[main.name] = main
966
+ compiler.warn("Kein PROGRAM/FB/FUNCTION gefunden, Fallback PROGRAM Main erzeugt", variables=len(decls), statements=len(statements))
967
+
968
+ program = compiler.find_main_program()
969
+ compiler.info("Main PROGRAM ausgewählt", program=program.name)
970
+
971
+ for pou in compiler.pous.values():
972
+ compiler.info(
973
+ "POU vorbereitet",
974
+ kind=pou.kind,
975
+ name=pou.name,
976
+ return_type=pou.return_type or "-",
977
+ variables=len(pou.declarations),
978
+ statements=len(pou.statements),
979
+ variable_names=[decl.name for decl in pou.declarations],
980
+ )
981
+
982
+ result = STProgram(compiler, program, source_files)
983
+ compiler.info(
984
+ "Compiler abgeschlossen",
985
+ program=program.name,
986
+ runtime_variables=len(result.variables),
987
+ statements=len(program.statements),
988
+ optimized_runtime=True,
989
+ )
990
+ return result
991
+ except Exception as exc:
992
+ compiler.error("Compiler abgebrochen", error=str(exc), error_type=type(exc).__name__)
993
+ if isinstance(exc, STCompileError):
994
+ setattr(exc, "compile_log", compiler.compile_log)
995
+ raise
996
+ wrapped = STCompileError(str(exc))
997
+ setattr(wrapped, "compile_log", compiler.compile_log)
998
+ raise wrapped from exc
999
+
1000
+ def compile_expr(self, expr: str):
1001
+ cached = self._expr_cache.get(expr)
1002
+ if cached is not None:
1003
+ return cached
1004
+ text = self.replace_literals(expr)
1005
+ text = re.sub(r"\b([A-Za-z_]\w*)#([A-Za-z_]\w*)\b", lambda m: str(self.constants.get(f"{m.group(1)}#{m.group(2)}", self.constants.get(m.group(2), 0))), text)
1006
+ text = re.sub(r"\b([A-Za-z_]\w*)\.(\d+)\b", r"BIT(\1, \2)", text)
1007
+ text = re.sub(r"\bTRUE\b", "True", text, flags=re.IGNORECASE)
1008
+ text = re.sub(r"\bFALSE\b", "False", text, flags=re.IGNORECASE)
1009
+ text = re.sub(r"\bAND\b", " and ", text, flags=re.IGNORECASE)
1010
+ text = re.sub(r"\bOR\b", " or ", text, flags=re.IGNORECASE)
1011
+ text = re.sub(r"\bXOR\b", " != ", text, flags=re.IGNORECASE)
1012
+ text = re.sub(r"\bNOT\b", " not ", text, flags=re.IGNORECASE)
1013
+ text = re.sub(r"\bMOD\b", " % ", text, flags=re.IGNORECASE)
1014
+ text = text.replace("<>", "!=")
1015
+ text = re.sub(r"(?<![:<>!])=(?!=)", "==", text).strip()
1016
+ try:
1017
+ code = compile(text, "<st-expression>", "eval")
1018
+ except SyntaxError as exc:
1019
+ raise STCompileError(f"Ausdruck nicht kompilierbar: '{expr}' -> {exc}") from exc
1020
+ self._expr_cache[expr] = code
1021
+ return code
1022
+
1023
+ def find_main_program(self) -> POU:
1024
+ programs = [p for p in self.pous.values() if p.kind == "PROGRAM"]
1025
+ if not programs:
1026
+ raise STCompileError("Kein PROGRAM gefunden. Bitte ein PROGRAM ... END_PROGRAM anlegen.")
1027
+ for p in programs:
1028
+ if p.name.upper() in {"PLC_PRG", "MAIN", "PRG"}:
1029
+ return p
1030
+ return programs[0]
1031
+
1032
+ def parse_types(self, source: str) -> None:
1033
+ for match in re.finditer(r"\bTYPE\b(.*?)\bEND_TYPE\b", source, flags=re.IGNORECASE | re.DOTALL):
1034
+ block = match.group(1)
1035
+ # STRUCT datatypes
1036
+ for sm in re.finditer(r"(\w+)\s*:\s*STRUCT(.*?)END_STRUCT\s*;?", block, flags=re.IGNORECASE | re.DOTALL):
1037
+ name = sm.group(1)
1038
+ decls = self.parse_var_lines(sm.group(2), "STRUCT")
1039
+ self.struct_types[name] = decls
1040
+ # ENUM / alias datatypes, e.g. TYPE X : INT ( A := 0, B := 1 ); END_TYPE or TYPE E : (A,B); END_TYPE
1041
+ for em in re.finditer(r"(\w+)\s*:\s*(?:(\w+)\s*)?\((.*?)\)\s*;?", block, flags=re.IGNORECASE | re.DOTALL):
1042
+ type_name = em.group(1)
1043
+ base_type = em.group(2) or "INT"
1044
+ enum_body = em.group(3)
1045
+ self.alias_types[type_name] = base_type
1046
+ value = 0
1047
+ for raw_item in split_top_level(enum_body, ","):
1048
+ item = raw_item.strip()
1049
+ if not item:
1050
+ continue
1051
+ if ":=" in item:
1052
+ const_name, const_expr = item.split(":=", 1)
1053
+ const_name = const_name.strip()
1054
+ value = int(self.literal(const_expr.strip()))
1055
+ else:
1056
+ const_name = item.strip()
1057
+ if const_name:
1058
+ self.constants[const_name] = value
1059
+ self.constants[f"{type_name}#{const_name}"] = value
1060
+ value += 1
1061
+ # Simple alias, e.g. TYPE X : INT; END_TYPE
1062
+ for am in re.finditer(r"(\w+)\s*:\s*(\w+)\s*;", block, flags=re.IGNORECASE):
1063
+ name, base = am.group(1), am.group(2)
1064
+ if name not in self.struct_types:
1065
+ self.alias_types[name] = base
1066
+
1067
+ def extract_global_declarations(self, source: str) -> None:
1068
+ self.global_declarations = []
1069
+ var_pattern = re.compile(r"\b(VAR_GLOBAL(?:\s+CONSTANT)?)\b(.*?)\bEND_VAR\b", flags=re.IGNORECASE | re.DOTALL)
1070
+ for block in var_pattern.finditer(source):
1071
+ section = block.group(1).upper().replace(" ", "_")
1072
+ self.global_declarations.extend(self.parse_var_lines(block.group(2), section))
1073
+
1074
+ def parse_pous(self, source: str) -> None:
1075
+ pattern = re.compile(
1076
+ r"\b(FUNCTION_BLOCK|FUNCTION|PROGRAM)\s+(\w+)(?:\s*:\s*(\w+))?(.*?)(?:\bEND_FUNCTION_BLOCK\b|\bEND_FUNCTION\b|\bEND_PROGRAM\b)",
1077
+ flags=re.IGNORECASE | re.DOTALL,
1078
+ )
1079
+ for match in pattern.finditer(source):
1080
+ kind = match.group(1).upper()
1081
+ name = match.group(2)
1082
+ return_type = match.group(3)
1083
+ body = match.group(4)
1084
+ decls, code = self.extract_vars_and_code(body)
1085
+ pou = POU(kind=kind, name=name, return_type=return_type, declarations=decls, statements=self.split_statements(code))
1086
+ self.pous[name] = pou
1087
+
1088
+ def remove_comments(self, source: str) -> str:
1089
+ source = re.sub(r"\(\*.*?\*\)", "", source, flags=re.DOTALL)
1090
+ source = re.sub(r"//.*", "", source)
1091
+ return source
1092
+
1093
+ def extract_vars_and_code(self, body: str) -> Tuple[List[VarDecl], str]:
1094
+ declarations: List[VarDecl] = []
1095
+ code = body
1096
+ var_pattern = re.compile(r"\b(VAR(?:_INPUT|_OUTPUT|_IN_OUT|_GLOBAL|_TEMP|_EXTERNAL)?(?:\s+CONSTANT)?)\b(.*?)\bEND_VAR\b", flags=re.IGNORECASE | re.DOTALL)
1097
+ blocks = list(var_pattern.finditer(body))
1098
+ for block in blocks:
1099
+ section = block.group(1).upper().replace(" ", "_")
1100
+ declarations.extend(self.parse_var_lines(block.group(2), section))
1101
+ for block in reversed(blocks):
1102
+ code = code[:block.start()] + "\n" + code[block.end():]
1103
+ return declarations, code
1104
+
1105
+ def parse_var_lines(self, block: str, section: str) -> List[VarDecl]:
1106
+ block = re.sub(r"\{[^{}]*\}", "", block)
1107
+ decls: List[VarDecl] = []
1108
+ for raw in split_top_level(block, ";"):
1109
+ line = raw.strip()
1110
+ if not line or ":" not in line:
1111
+ continue
1112
+ names_part, type_part = line.split(":", 1)
1113
+ default_text = None
1114
+ if ":=" in type_part:
1115
+ type_name, default_text = type_part.split(":=", 1)
1116
+ else:
1117
+ type_name = type_part
1118
+ type_name = type_name.strip()
1119
+ for raw_name in names_part.split(","):
1120
+ raw_name = raw_name.strip()
1121
+ if not raw_name:
1122
+ continue
1123
+ address = None
1124
+ at_match = re.match(r"(?:(\w+)\s+)?AT\s+([^\s]+)$", raw_name, flags=re.IGNORECASE)
1125
+ if at_match:
1126
+ name = at_match.group(1) or at_match.group(2).replace("%", "_").replace(".", "_")
1127
+ address = at_match.group(2)
1128
+ else:
1129
+ name = raw_name
1130
+ if " AT " in raw_name.upper():
1131
+ parts = re.split(r"\s+AT\s+", raw_name, flags=re.IGNORECASE)
1132
+ name = parts[0].strip()
1133
+ address = parts[1].strip()
1134
+ decls.append(VarDecl(name=name, type_name=type_name, default_text=(default_text or None), section=section, address=address))
1135
+ return decls
1136
+
1137
+ def instantiate_declarations(self, declarations: List[VarDecl], include_inputs: bool = True) -> Dict[str, Any]:
1138
+ variables: Dict[str, Any] = {}
1139
+ for decl in declarations:
1140
+ if not include_inputs and decl.section == "VAR_INPUT":
1141
+ continue
1142
+ if decl.section == "VAR_EXTERNAL":
1143
+ continue
1144
+ variables[decl.name] = self.make_default_value(decl.type_name, decl.default_text)
1145
+ return variables
1146
+
1147
+ def make_default_value(self, type_name: str, default_text: Optional[str] = None) -> Any:
1148
+ type_clean = type_name.strip()
1149
+ type_clean = self.alias_types.get(type_clean, type_clean)
1150
+ array_match = re.match(r"ARRAY\s*\[\s*(-?\d+)\s*\.\.\s*(-?\d+)\s*\]\s+OF\s+(.+)$", type_clean, flags=re.IGNORECASE)
1151
+ if array_match:
1152
+ lower, upper, inner_type = int(array_match.group(1)), int(array_match.group(2)), array_match.group(3).strip()
1153
+ defaults = []
1154
+ if default_text:
1155
+ defaults = [self.literal(part.strip()) for part in split_top_level(default_text, ",") if part.strip()]
1156
+
1157
+ def factory(counter={"i": 0}):
1158
+ idx = counter["i"]
1159
+ counter["i"] += 1
1160
+ if idx < len(defaults):
1161
+ return clone_value(defaults[idx])
1162
+ return self.make_default_value(inner_type, None)
1163
+
1164
+ return STArray(lower, upper, factory)
1165
+ upper_type = type_clean.upper()
1166
+ if default_text is not None:
1167
+ return self.literal(default_text)
1168
+ if upper_type in BUILTIN_FB:
1169
+ return BUILTIN_FB[upper_type]()
1170
+ pou_key = self.find_pou_key(type_clean)
1171
+ if pou_key and self.pous[pou_key].kind in {"FUNCTION_BLOCK", "FUNCTION"}:
1172
+ return FunctionBlockInstance(self.pous[pou_key], self)
1173
+ struct_key = self.find_struct_key(type_clean)
1174
+ if struct_key:
1175
+ values = self.instantiate_declarations(self.struct_types[struct_key])
1176
+ return STStruct(**values)
1177
+ return self.default_for_type(type_clean)
1178
+
1179
+ def default_for_type(self, type_name: str) -> Any:
1180
+ type_name = self.alias_types.get((type_name or "").strip(), type_name)
1181
+ upper_type = (type_name or "").strip().upper()
1182
+ if upper_type == "BOOL":
1183
+ return False
1184
+ if upper_type in {"REAL", "LREAL"}:
1185
+ return 0.0
1186
+ if upper_type in {"STRING"}:
1187
+ return ""
1188
+ if upper_type in {"TIME", "TIME_OF_DAY", "DATE"}:
1189
+ return 0
1190
+ if upper_type in {"BYTE", "WORD", "DWORD", "INT", "DINT", "SINT", "UINT", "UDINT", "LINT", "ULINT"}:
1191
+ return 0
1192
+ return 0
1193
+
1194
+ def find_pou_key(self, name: str) -> Optional[str]:
1195
+ return find_key(self.pous, name)
1196
+
1197
+ def find_struct_key(self, name: str) -> Optional[str]:
1198
+ return find_key(self.struct_types, name)
1199
+
1200
+ def literal(self, text: str) -> Any:
1201
+ value = text.strip().rstrip(";")
1202
+ upper = value.upper().replace(" ", "")
1203
+ if upper == "TRUE":
1204
+ return True
1205
+ if upper == "FALSE":
1206
+ return False
1207
+ if value in self.constants:
1208
+ return self.constants[value]
1209
+ if upper in {k.upper(): v for k, v in self.constants.items()}:
1210
+ for k, v in self.constants.items():
1211
+ if k.upper() == upper:
1212
+ return v
1213
+ if upper.startswith("T#") or upper.startswith("TIME#"):
1214
+ return parse_time_literal(upper)
1215
+ if upper.startswith("16#"):
1216
+ return int(upper[3:].replace("_", ""), 16)
1217
+ if re.match(r"^(B|W|DW|L)?#16#", upper):
1218
+ return int(upper.split("#16#", 1)[1].replace("_", ""), 16)
1219
+ if upper.startswith("2#"):
1220
+ return int(upper[2:].replace("_", ""), 2)
1221
+ try:
1222
+ normalized = value.replace("_", "")
1223
+ if "." in normalized or "E" in normalized.upper():
1224
+ return float(normalized)
1225
+ return int(normalized)
1226
+ except ValueError:
1227
+ if (value.startswith("'") and value.endswith("'")) or (value.startswith('"') and value.endswith('"')):
1228
+ return value[1:-1]
1229
+ return value
1230
+
1231
+ def replace_literals(self, text: str) -> str:
1232
+ def repl_time(match):
1233
+ return str(parse_time_literal(match.group(0).upper().replace(" ", "")))
1234
+
1235
+ text = re.sub(r"\b(?:T|TIME)#(?:\d+D)?(?:\d+H)?(?:\d+M(?!S))?(?:\d+S)?(?:\d+MS)?\b", repl_time, text, flags=re.IGNORECASE)
1236
+ text = re.sub(r"\b(?:B|W|DW|L)?#16#([0-9A-Fa-f_]+)\b", lambda m: str(int(m.group(1).replace("_", ""), 16)), text)
1237
+ text = re.sub(r"\b16#([0-9A-Fa-f_]+)\b", lambda m: str(int(m.group(1).replace("_", ""), 16)), text)
1238
+ text = re.sub(r"\b2#([01_]+)\b", lambda m: str(int(m.group(1).replace("_", ""), 2)), text)
1239
+ return text
1240
+
1241
+ def make_function_callable(self, pou: POU):
1242
+ def call_function(*args, **kwargs):
1243
+ inst = FunctionBlockInstance(pou, self)
1244
+ input_decls = [d for d in pou.declarations if d.section.startswith("VAR_INPUT")]
1245
+ for decl, value in zip(input_decls, args):
1246
+ setattr(inst, decl.name, value)
1247
+ result = inst(**kwargs)
1248
+ if pou.return_type:
1249
+ key = find_key(inst._vars, pou.name) or pou.name
1250
+ return inst._vars.get(key)
1251
+ return result
1252
+ return call_function
1253
+
1254
+ def call_function_instance(self, name: str, args: List[Any], kwargs: Dict[str, Any]) -> Any:
1255
+ key = self.find_pou_key(name)
1256
+ if not key or self.pous[key].kind != "FUNCTION":
1257
+ raise STCompileError(f"Funktion {name} nicht gefunden.")
1258
+ pou = self.pous[key]
1259
+ inst = FunctionBlockInstance(pou, self)
1260
+ input_decls = [d for d in pou.declarations if d.section.startswith("VAR_INPUT")]
1261
+ for decl, value in zip(input_decls, args):
1262
+ setattr(inst, decl.name, value)
1263
+ for k, v in kwargs.items():
1264
+ setattr(inst, k, v)
1265
+ inst()
1266
+ if pou.return_type:
1267
+ rkey = find_key(inst._vars, pou.name) or pou.name
1268
+ return inst._vars.get(rkey)
1269
+ return inst
1270
+
1271
+ def split_statements(self, code: str) -> List[str]:
1272
+ normalized = code.replace("\r\n", "\n").replace("\r", "\n")
1273
+ lines: List[str] = []
1274
+ buffer = ""
1275
+ paren_depth = 0
1276
+ pending_ctrl = ""
1277
+ pending_until: Optional[str] = None
1278
+
1279
+ def flush_buffer() -> None:
1280
+ nonlocal buffer, paren_depth
1281
+ if buffer.strip():
1282
+ lines.append(buffer.strip().rstrip(";"))
1283
+ buffer = ""
1284
+ paren_depth = 0
1285
+
1286
+ for raw_line in normalized.split("\n"):
1287
+ line = raw_line.strip()
1288
+ if not line:
1289
+ continue
1290
+ upper = line.upper().rstrip(";")
1291
+
1292
+ if pending_ctrl:
1293
+ pending_ctrl = (pending_ctrl + " " + line).strip()
1294
+ if pending_until and re.search(r"\b" + re.escape(pending_until) + r"\b", pending_ctrl, flags=re.IGNORECASE):
1295
+ if pending_until in {"THEN", "DO", "OF"}:
1296
+ # keep keyword but remove everything after it onto following normal text only if needed
1297
+ pass
1298
+ lines.append(pending_ctrl.rstrip(";"))
1299
+ pending_ctrl = ""
1300
+ pending_until = None
1301
+ continue
1302
+
1303
+ # CASE labels are separate statements: Label: or A, B:
1304
+ if re.match(r"^[A-Za-z_]\w*(?:#[A-Za-z_]\w*)?(?:\s*,\s*[A-Za-z_]\w*(?:#[A-Za-z_]\w*)?)*:$", line):
1305
+ flush_buffer()
1306
+ lines.append(line.rstrip(";"))
1307
+ continue
1308
+
1309
+ if upper in {"ELSE", "END_IF", "END_FOR", "END_WHILE", "END_CASE"}:
1310
+ flush_buffer()
1311
+ lines.append(upper)
1312
+ continue
1313
+
1314
+ if upper.startswith("IF ") or upper.startswith("ELSIF "):
1315
+ flush_buffer()
1316
+ if re.search(r"\bTHEN\b", line, flags=re.IGNORECASE):
1317
+ lines.append(line.rstrip(";"))
1318
+ else:
1319
+ pending_ctrl = line
1320
+ pending_until = "THEN"
1321
+ continue
1322
+
1323
+ if upper.startswith("FOR ") or upper.startswith("WHILE "):
1324
+ flush_buffer()
1325
+ if re.search(r"\bDO\b", line, flags=re.IGNORECASE):
1326
+ lines.append(line.rstrip(";"))
1327
+ else:
1328
+ pending_ctrl = line
1329
+ pending_until = "DO"
1330
+ continue
1331
+
1332
+ if upper.startswith("CASE "):
1333
+ flush_buffer()
1334
+ if re.search(r"\bOF\b", line, flags=re.IGNORECASE):
1335
+ lines.append(line.rstrip(";"))
1336
+ else:
1337
+ pending_ctrl = line
1338
+ pending_until = "OF"
1339
+ continue
1340
+
1341
+ for ch in line:
1342
+ if ch == "(":
1343
+ paren_depth += 1
1344
+ elif ch == ")":
1345
+ paren_depth = max(0, paren_depth - 1)
1346
+ buffer = (buffer + " " + line).strip()
1347
+ if paren_depth == 0 and ";" in buffer:
1348
+ parts = split_top_level(buffer, ";")
1349
+ for part in parts[:-1]:
1350
+ if part.strip():
1351
+ lines.append(part.strip())
1352
+ buffer = parts[-1].strip()
1353
+ if pending_ctrl:
1354
+ lines.append(pending_ctrl.strip().rstrip(";"))
1355
+ flush_buffer()
1356
+ return lines
1357
+
1358
+
1359
+ def split_top_level(text: str, sep: str) -> List[str]:
1360
+ parts: List[str] = []
1361
+ current: List[str] = []
1362
+ depth_round = 0
1363
+ depth_square = 0
1364
+ in_string: Optional[str] = None
1365
+ i = 0
1366
+ while i < len(text):
1367
+ ch = text[i]
1368
+ if in_string:
1369
+ current.append(ch)
1370
+ if ch == in_string:
1371
+ in_string = None
1372
+ i += 1
1373
+ continue
1374
+ if ch in {'"', "'"}:
1375
+ in_string = ch
1376
+ current.append(ch)
1377
+ i += 1
1378
+ continue
1379
+ if ch == "(":
1380
+ depth_round += 1
1381
+ elif ch == ")":
1382
+ depth_round = max(0, depth_round - 1)
1383
+ elif ch == "[":
1384
+ depth_square += 1
1385
+ elif ch == "]":
1386
+ depth_square = max(0, depth_square - 1)
1387
+ if ch == sep and depth_round == 0 and depth_square == 0:
1388
+ parts.append("".join(current))
1389
+ current = []
1390
+ else:
1391
+ current.append(ch)
1392
+ i += 1
1393
+ parts.append("".join(current))
1394
+ return parts
1395
+
1396
+
1397
+ def parse_time_literal(upper: str) -> int:
1398
+ text = upper.replace("TIME#", "T#").replace(" ", "")
1399
+ if not text.startswith("T#"):
1400
+ return 0
1401
+ body = text[2:]
1402
+ total = 0
1403
+ for number, unit in re.findall(r"(\d+)(D|H|MS|M|S)", body):
1404
+ n = int(number)
1405
+ if unit == "D":
1406
+ total += n * 24 * 60 * 60 * 1000
1407
+ elif unit == "H":
1408
+ total += n * 60 * 60 * 1000
1409
+ elif unit == "M":
1410
+ total += n * 60 * 1000
1411
+ elif unit == "S":
1412
+ total += n * 1000
1413
+ elif unit == "MS":
1414
+ total += n
1415
+ return total