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.
- package/README.md +42 -0
- package/me-vplc.html +64 -0
- package/me-vplc.js +603 -0
- package/package.json +26 -0
- package/project/README.md +1052 -0
- package/project/START_ME_VPLC.cmd +176 -0
- package/project/backend/active_project.json +3 -0
- package/project/backend/app.py +839 -0
- package/project/backend/connector_runtime.py +585 -0
- package/project/backend/requirements.txt +3 -0
- package/project/backend/st_compiler.py +1415 -0
- package/project/frontend/index.html +12 -0
- package/project/frontend/package.json +18 -0
- package/project/frontend/src/App.jsx +631 -0
- package/project/frontend/src/style.css +964 -0
- package/project/frontend/vite.config.js +14 -0
- package/wheelhouse/Flask_Cors-4.0.1-py2.py3-none-any.whl +0 -0
- package/wheelhouse/blinker-1.9.0-py3-none-any.whl +0 -0
- package/wheelhouse/click-8.3.3-py3-none-any.whl +0 -0
- package/wheelhouse/colorama-0.4.6-py2.py3-none-any.whl +0 -0
- package/wheelhouse/flask-3.0.3-py3-none-any.whl +0 -0
- package/wheelhouse/itsdangerous-2.2.0-py3-none-any.whl +0 -0
- package/wheelhouse/jinja2-3.1.6-py3-none-any.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp310-cp310-win_amd64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp311-cp311-win_amd64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp312-cp312-win_amd64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +0 -0
- package/wheelhouse/markupsafe-3.0.3-cp313-cp313-win_amd64.whl +0 -0
- package/wheelhouse/pymodbus-3.6.9-py3-none-any.whl +0 -0
- 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
|