opencrbasic 1.0.0__py3-none-any.whl
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.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
opencrbasic/tables.py
ADDED
|
@@ -0,0 +1,2173 @@
|
|
|
1
|
+
"""Derive `-z` (TDF) and `-l` (table list) output from a parsed `Program`.
|
|
2
|
+
|
|
3
|
+
Rules are reverse-engineered black-box against `CR6Comp.exe`; see `docs/tdf-rules.md` for the
|
|
4
|
+
evidence and confidence tags. Section references below (`§N`) are to that document.
|
|
5
|
+
|
|
6
|
+
Entry points:
|
|
7
|
+
build_tables(program, profile, consts) -> (tables, table_list_text, diagnostics_or_None)
|
|
8
|
+
render_table_list(tables) -> str # the `-l`/`-c` text, given tables in TDF order
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import struct
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TypedDict
|
|
18
|
+
|
|
19
|
+
from . import consteval
|
|
20
|
+
from . import syntax as S
|
|
21
|
+
from .tdf import TdfField, TdfTable
|
|
22
|
+
|
|
23
|
+
# `TableName.<property>` members of a data (or Status) table that are not fields
|
|
24
|
+
# (extracted_chm/Info/datatableaccess.htm "Data Table Access").
|
|
25
|
+
TABLE_PROPERTIES = (
|
|
26
|
+
"BytesPerRecord",
|
|
27
|
+
"EventCount",
|
|
28
|
+
"EventEnd",
|
|
29
|
+
"Output",
|
|
30
|
+
"Record",
|
|
31
|
+
"TableBytes",
|
|
32
|
+
"TableFull",
|
|
33
|
+
"TableSize",
|
|
34
|
+
"TimeStamp",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
DATA = Path(__file__).resolve().parent / "data"
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------- type codes (§1, §8)
|
|
40
|
+
|
|
41
|
+
# TDF field type byte. Confirmed by oracle: 2 UINT2, 6 LONG, 7 FP2, 9 IEEE4, 11 ASCII, 14 NSEC,
|
|
42
|
+
# 17 BOOL8, 18 IEEE8, 28 BOOLEAN. UINT4=3 oracle-confirmed, UINT1 a guess (never produced; see
|
|
43
|
+
# docs/tdf-rules.md open question 12).
|
|
44
|
+
TYPE_CODES = {
|
|
45
|
+
"FLOAT": 9,
|
|
46
|
+
"IEEE4": 9,
|
|
47
|
+
"FP2": 7,
|
|
48
|
+
"LONG": 6,
|
|
49
|
+
"BOOLEAN": 28,
|
|
50
|
+
"BOOL8": 17,
|
|
51
|
+
"STRING": 11,
|
|
52
|
+
"DOUBLE": 18,
|
|
53
|
+
"IEEE8": 18,
|
|
54
|
+
"UINT2": 2,
|
|
55
|
+
"NSEC": 14,
|
|
56
|
+
"UINT1": 1,
|
|
57
|
+
"UINT4": 3,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _dtype_code(expr: S.Expr | None, env: consteval.Env) -> int:
|
|
62
|
+
"""TDF type code of an output instruction's DataType argument. A constant expression or named
|
|
63
|
+
`Const` resolves through its value (the predefined data-type symbols equal the TDF codes)."""
|
|
64
|
+
if isinstance(expr, S.Name) and expr.name.upper() in TYPE_CODES:
|
|
65
|
+
return TYPE_CODES[expr.name.upper()]
|
|
66
|
+
v = _fold(expr, env, None)
|
|
67
|
+
return int(v) if isinstance(v, (int, float)) and v in TYPE_CODES.values() else 9
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_UNIT_SECONDS = {"USEC": 1e-6, "MSEC": 1e-3, "SEC": 1, "MIN": 60, "HR": 3600, "DAY": 86400}
|
|
71
|
+
_UNIT_NS = {
|
|
72
|
+
"USEC": 1_000,
|
|
73
|
+
"MSEC": 1_000_000,
|
|
74
|
+
"SEC": 1_000_000_000,
|
|
75
|
+
"MIN": 60_000_000_000,
|
|
76
|
+
"HR": 3_600_000_000_000,
|
|
77
|
+
"DAY": 86_400_000_000_000,
|
|
78
|
+
}
|
|
79
|
+
# Numeric Scan units (`checker/scan_time.UNIT_NAMES`), keyed by the predefined symbol value.
|
|
80
|
+
_UNIT_NAME_BY_VALUE = {0: "USEC", 1: "MSEC", 2: "SEC", 3: "MIN", 4: "HR", 5: "DAY"}
|
|
81
|
+
|
|
82
|
+
_STATUS_JSON = json.loads((DATA / "status_table.json").read_text())
|
|
83
|
+
|
|
84
|
+
# Auto-size (§10): one finalisation algorithm for every table mix (`docs/tdf-rules.md` §10, rewritten
|
|
85
|
+
# 2026-10-10 from a re-read of the finalisation loop). Free memory is the compiler's own malloc probe,
|
|
86
|
+
# a binary search that returns 2**22 - 1 whenever every allocation succeeds. All table arithmetic is
|
|
87
|
+
# 32-bit float (`_f32`), as in the real compiler.
|
|
88
|
+
_FREE_MEMORY = 4_194_303
|
|
89
|
+
_DEFAULT_PROBE_BUFFER_BYTES = 1024
|
|
90
|
+
_DEFAULT_PROBE_OVERHEAD = 16
|
|
91
|
+
_TABLE_INIT_RESERVED = 4096
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _f32(x: float) -> float:
|
|
95
|
+
return struct.unpack("f", struct.pack("f", x))[0]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _f32_safe(x) -> float:
|
|
99
|
+
"""float32 rounding that overflows to +-inf (CPython 3.13 raises on some builds)."""
|
|
100
|
+
try:
|
|
101
|
+
return _f32(float(x))
|
|
102
|
+
except OverflowError:
|
|
103
|
+
return float("inf") if x > 0 else float("-inf")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_INT32_MAX = 2147483647
|
|
107
|
+
_TWO31 = 2147483648.0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def size_class(raw):
|
|
111
|
+
"""How the real compiler reads a `DataTable` size argument (oracle, all four loggers,
|
|
112
|
+
`docs/tdf-rules.md` "DataTable size argument"): the constant is rounded to float32 and
|
|
113
|
+
truncated toward zero (an integer constant that fits in 32 bits is kept exactly). Returns `("auto",)` for -1, `("fixed", N, flagged)` for any other
|
|
114
|
+
integer (`flagged`: it was negative, `N = -size`; zero is 1), `("edge", sign)` when the
|
|
115
|
+
float32 is exactly +-2^31, `("inf",)` for `+inf`, `("toxic",)` beyond it (and `-inf`), `None`
|
|
116
|
+
when the value is not a number."""
|
|
117
|
+
if isinstance(raw, bool) or not isinstance(raw, (int, float)):
|
|
118
|
+
return None
|
|
119
|
+
if isinstance(raw, int) and -_TWO31 <= raw < _TWO31:
|
|
120
|
+
v = raw # an integer literal that fits is kept exactly
|
|
121
|
+
else:
|
|
122
|
+
f = _f32_safe(raw)
|
|
123
|
+
if f != f:
|
|
124
|
+
return None
|
|
125
|
+
if f == float("inf"):
|
|
126
|
+
return ("inf",)
|
|
127
|
+
if f == _TWO31:
|
|
128
|
+
return ("edge", 1)
|
|
129
|
+
if f == -_TWO31:
|
|
130
|
+
return ("edge", -1)
|
|
131
|
+
if abs(f) > _TWO31:
|
|
132
|
+
return ("toxic",)
|
|
133
|
+
v = int(f)
|
|
134
|
+
if v == -1:
|
|
135
|
+
return ("auto",)
|
|
136
|
+
if v == 0:
|
|
137
|
+
return ("fixed", 1, False)
|
|
138
|
+
return ("fixed", min(-v, _INT32_MAX), True) if v < -1 else ("fixed", v, False)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def declared_size(raw, cr3xx: bool, bytes_per_record: int) -> int | None:
|
|
142
|
+
"""The TDF size of a called table with this size argument, or `None` when it is auto-sized
|
|
143
|
+
(the caller computes it). CR6/CR1000X auto-size every negative or out-of-range size and keep
|
|
144
|
+
only `+inf` as `INT32_MAX`; CR300/CR350 keep `-N` as a fixed `N`, saturate +-2^31, and give
|
|
145
|
+
a beyond-range size ("toxic", `checker/storage.py`) the minimum a table can get, three pages
|
|
146
|
+
less the first block: `(12288 // R) - (4096 // R + 1)` records, `R = bytes + 16`."""
|
|
147
|
+
c = size_class(raw)
|
|
148
|
+
if c is None or c[0] == "auto":
|
|
149
|
+
return None
|
|
150
|
+
if c[0] == "inf":
|
|
151
|
+
return _INT32_MAX
|
|
152
|
+
if cr3xx:
|
|
153
|
+
if c[0] == "edge":
|
|
154
|
+
return _INT32_MAX
|
|
155
|
+
if c[0] == "toxic":
|
|
156
|
+
rec = bytes_per_record + 16
|
|
157
|
+
return max(12288 // rec - (4096 // rec + 1), 1)
|
|
158
|
+
return c[1]
|
|
159
|
+
if c[0] in ("edge", "toxic") or c[2]:
|
|
160
|
+
return None
|
|
161
|
+
return c[1]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _fold(expr: S.Expr | None, env: consteval.Env, default=None):
|
|
165
|
+
if expr is None:
|
|
166
|
+
return default
|
|
167
|
+
try:
|
|
168
|
+
return consteval.value_of(expr, env)
|
|
169
|
+
except consteval.NotConstant:
|
|
170
|
+
return default
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _unit_value(expr: S.Expr | None, env: consteval.Env) -> int | None:
|
|
174
|
+
"""Units argument of `Scan` or `DataInterval` as an integer: the predefined name (`Min`), its
|
|
175
|
+
number (`3`) and a `Const` agree, and a fraction is truncated toward zero (`1.9` is mSec, `-0.5`
|
|
176
|
+
is uSec; oracle, round 20 W5, `docs/tdf-rules.md` "Numeric and Const units"). `None` when the
|
|
177
|
+
argument does not fold to a finite number."""
|
|
178
|
+
v = _fold(expr, env, None)
|
|
179
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
180
|
+
return None
|
|
181
|
+
if isinstance(v, float) and not math.isfinite(v):
|
|
182
|
+
return None
|
|
183
|
+
return int(v)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _scan_unit_name(expr: S.Expr | None, env: consteval.Env) -> str:
|
|
187
|
+
"""`_UNIT_NS` key of a Scan units argument (`_unit_value`). A non-constant reads as seconds. A
|
|
188
|
+
value outside 0..5 reads as uSec, the unit the real compiler leaves no table charge for."""
|
|
189
|
+
v = _unit_value(expr, env)
|
|
190
|
+
if v is None:
|
|
191
|
+
return "SEC"
|
|
192
|
+
return _UNIT_NAME_BY_VALUE.get(v, "USEC")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _as_int(v, default=0) -> int:
|
|
196
|
+
if v is None:
|
|
197
|
+
return default
|
|
198
|
+
try:
|
|
199
|
+
return int(round(float(v)))
|
|
200
|
+
except TypeError, ValueError, OverflowError:
|
|
201
|
+
return default
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------- const int/float inference (§7)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _const_is_int(expr: S.Expr | None, env: consteval.Env, int_names: dict) -> bool:
|
|
208
|
+
"""Whether a ConstTable `Const` expression folds to CRBasic-integer (-> LONG) rather than
|
|
209
|
+
float (-> IEEE4). Oracle-confirmed (docs/tdf-rules.md §7, exp `consttypes`/`negconst`):
|
|
210
|
+
integer literals, hex/binary literals, unary +/- of an int, True/False and predefined symbols,
|
|
211
|
+
and +/-/* of two ints stay LONG; `/` is always IEEE4 even when exact; other operators are
|
|
212
|
+
treated as float unless both operands are known int (best-effort, not exhaustively tested).
|
|
213
|
+
"""
|
|
214
|
+
if isinstance(expr, S.Number):
|
|
215
|
+
return isinstance(expr.value, int)
|
|
216
|
+
if isinstance(expr, S.Name):
|
|
217
|
+
upper = expr.name.upper()
|
|
218
|
+
if upper in int_names:
|
|
219
|
+
return int_names[upper]
|
|
220
|
+
sym = env.profile.symbols.get(upper)
|
|
221
|
+
if sym is not None:
|
|
222
|
+
return isinstance(sym.get("value"), int)
|
|
223
|
+
return True # unknown name (already resolved as constant elsewhere): assume int
|
|
224
|
+
if isinstance(expr, S.Unary):
|
|
225
|
+
if expr.op == "NOT":
|
|
226
|
+
return True
|
|
227
|
+
return _const_is_int(expr.operand, env, int_names)
|
|
228
|
+
if isinstance(expr, S.Binary):
|
|
229
|
+
if expr.op == "/":
|
|
230
|
+
return False
|
|
231
|
+
if expr.op in ("+", "-", "*"):
|
|
232
|
+
return _const_is_int(expr.left, env, int_names) and _const_is_int(expr.right, env, int_names)
|
|
233
|
+
if expr.op in ("\\", "MOD", "AND", "OR", "XOR", "=", "<>", "<", ">", "<=", ">=", "EQV", "IMP", "<<", ">>"):
|
|
234
|
+
return True
|
|
235
|
+
return False # "^" and anything else: unverified, assume float
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------- variable registry
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class VarInfo:
|
|
244
|
+
"""A declared Public/Dim variable: its TDF-relevant shape, for Public-table fields and for
|
|
245
|
+
resolving DataTable output instructions' `Source` arrays (§3, §8, §9)."""
|
|
246
|
+
|
|
247
|
+
type_code: int
|
|
248
|
+
shape: list[int] = field(default_factory=list) # declared array dims, e.g. [10] or [2,3]
|
|
249
|
+
stored_len: int | None = None # STRING only: rounded storage length
|
|
250
|
+
units: str = ""
|
|
251
|
+
struct: dict | None = None # StructureType body when typed as one
|
|
252
|
+
name: str = "" # declared spelling (CRBasic is
|
|
253
|
+
# case-insensitive; a `Source` reference may use different casing than the declaration, but
|
|
254
|
+
# the default output field name follows the declaration -- oracle-confirmed, ftpclient_4.CR6:
|
|
255
|
+
# `Public Count As Long` + `Sample(1,count,LONG)` -> field name `Count`, not `count`).
|
|
256
|
+
|
|
257
|
+
def full_shape(self) -> list[int]:
|
|
258
|
+
s = list(self.shape)
|
|
259
|
+
if self.stored_len is not None:
|
|
260
|
+
s.append(self.stored_len)
|
|
261
|
+
return s
|
|
262
|
+
|
|
263
|
+
def collapsed(self) -> tuple[int, list[int]]:
|
|
264
|
+
"""(dim, subdims) per the §3 collapse rule."""
|
|
265
|
+
shape = self.full_shape()
|
|
266
|
+
if not shape:
|
|
267
|
+
return 1, []
|
|
268
|
+
if len(shape) == 1 and shape[0] == 1:
|
|
269
|
+
return 1, []
|
|
270
|
+
dim = 1
|
|
271
|
+
for n in shape:
|
|
272
|
+
dim *= n
|
|
273
|
+
return dim, shape
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _string_stored_len(n: int) -> int:
|
|
277
|
+
"""§8: smallest multiple of 4 strictly greater than n."""
|
|
278
|
+
return (n // 4 + 1) * 4
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _var_shape(v: S.VarDecl, env: consteval.Env) -> list[int]:
|
|
282
|
+
dims = []
|
|
283
|
+
for d in v.dims:
|
|
284
|
+
n = _as_int(_fold(d, env), default=1)
|
|
285
|
+
dims.append(max(n, 1))
|
|
286
|
+
return dims
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _var_type_code(v: S.VarDecl) -> int:
|
|
290
|
+
t = (v.type or "FLOAT").upper()
|
|
291
|
+
return TYPE_CODES.get(t, 9)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _string_size_expr(v: S.VarDecl) -> S.Expr | None:
|
|
295
|
+
"""`v.string_size`, working around a parser quirk: `As String * 60 = "x"` currently parses
|
|
296
|
+
the size and the initializer together as one `Binary(op='=', left=60, right="x")` node
|
|
297
|
+
instead of splitting them; the real size is the left side."""
|
|
298
|
+
e = v.string_size
|
|
299
|
+
if isinstance(e, S.Binary) and e.op == "=":
|
|
300
|
+
return e.left
|
|
301
|
+
return e
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _make_var_info(v: S.VarDecl, env: consteval.Env) -> VarInfo:
|
|
305
|
+
type_code = _var_type_code(v)
|
|
306
|
+
shape = _var_shape(v, env)
|
|
307
|
+
stored_len = None
|
|
308
|
+
if (v.type or "").upper() == "STRING":
|
|
309
|
+
# A bare `As String` with no `* n` defaults to n=20 (oracle-confirmed: formatlong_1.CR6's
|
|
310
|
+
# `FormatString As String` and ctype_1.CR6's `Type(6) As String` both round to dim=24 per
|
|
311
|
+
# element, matching n=20 -- `(20 div 4 + 1) * 4`).
|
|
312
|
+
n = _as_int(_fold(_string_size_expr(v), env), default=20)
|
|
313
|
+
stored_len = _string_stored_len(max(n, 1))
|
|
314
|
+
return VarInfo(type_code, shape, stored_len, name=v.name)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------- statement block scanning
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _kw(s: S.Stmt) -> str:
|
|
321
|
+
return getattr(s, "keyword", "").upper() if isinstance(s, S.Instruction) else ""
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class ConstTableDecl:
|
|
326
|
+
name: str | None
|
|
327
|
+
hidden: int
|
|
328
|
+
consts: list[S.ConstDecl]
|
|
329
|
+
line: int
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dataclass
|
|
333
|
+
class DataTableDecl:
|
|
334
|
+
name: str
|
|
335
|
+
trig: S.Expr | None
|
|
336
|
+
size_expr: S.Expr | None
|
|
337
|
+
body: list[S.Stmt]
|
|
338
|
+
line: int
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class _Scanned(TypedDict):
|
|
342
|
+
uses_ch400: bool
|
|
343
|
+
struct_types: dict[str, dict]
|
|
344
|
+
uses_cpi: bool
|
|
345
|
+
publics: list[S.VarDecl]
|
|
346
|
+
all_vars: list[S.VarDecl]
|
|
347
|
+
const_tables: list[ConstTableDecl]
|
|
348
|
+
data_tables: list[DataTableDecl]
|
|
349
|
+
alias_stmts: list[S.Instruction]
|
|
350
|
+
units_stmts: list[S.Instruction]
|
|
351
|
+
readonly_names: set[str]
|
|
352
|
+
slow_seq_count: int
|
|
353
|
+
called: set[str]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _scan_program(program: S.Program) -> _Scanned:
|
|
357
|
+
"""One pass over the flat statement list: collects Public/Dim declarations (in order),
|
|
358
|
+
ConstTable blocks, DataTable blocks, Alias/Units statements and the SlowSequence count.
|
|
359
|
+
DataTable and ConstTable bodies do not nest (CRBasic), so simple non-nested pairing suffices.
|
|
360
|
+
"""
|
|
361
|
+
publics: list[S.VarDecl] = []
|
|
362
|
+
all_vars: list[S.VarDecl] = [] # Public + Dim (+ Static), for Source lookups
|
|
363
|
+
const_tables: list[ConstTableDecl] = []
|
|
364
|
+
data_tables: list[DataTableDecl] = []
|
|
365
|
+
alias_stmts: list[S.Instruction] = []
|
|
366
|
+
units_stmts: list[S.Instruction] = []
|
|
367
|
+
readonly_names: set[str] = set()
|
|
368
|
+
slow_seq_count = 0
|
|
369
|
+
struct_types: dict[str, dict] = {} # NAME -> {"members": [VarDecl], "units": {}, "ro": set}
|
|
370
|
+
|
|
371
|
+
stmts = program.statements
|
|
372
|
+
n = len(stmts)
|
|
373
|
+
i = 0
|
|
374
|
+
while i < n:
|
|
375
|
+
s = stmts[i]
|
|
376
|
+
kw = _kw(s)
|
|
377
|
+
if isinstance(s, S.Instruction) and (kw == "STRUCTURETYPE"):
|
|
378
|
+
st = {"members": [], "units": {}, "ro": set()}
|
|
379
|
+
if s.args and isinstance(s.args[0], S.Name):
|
|
380
|
+
struct_types[s.args[0].name.upper()] = st
|
|
381
|
+
i += 1
|
|
382
|
+
while i < n and _kw(stmts[i]) != "ENDSTRUCTURETYPE":
|
|
383
|
+
m = stmts[i]
|
|
384
|
+
if isinstance(m, S.Declaration):
|
|
385
|
+
st["members"].extend(m.vars)
|
|
386
|
+
elif isinstance(m, S.Instruction) and (
|
|
387
|
+
_kw(m) == "UNITS" and len(m.args) > 1 and isinstance(m.args[1], S.String)
|
|
388
|
+
):
|
|
389
|
+
st["units"][getattr(m.args[0], "name", "").upper()] = m.args[1].value
|
|
390
|
+
elif isinstance(m, S.Instruction) and (_kw(m) == "READONLY"):
|
|
391
|
+
st["ro"].update(a.name.upper() for a in m.args if isinstance(a, S.Name))
|
|
392
|
+
i += 1
|
|
393
|
+
i += 1
|
|
394
|
+
continue
|
|
395
|
+
if isinstance(s, S.Declaration) and s.kind in ("PUBLIC", "DIM", "STATIC"):
|
|
396
|
+
all_vars.extend(s.vars)
|
|
397
|
+
if s.kind == "PUBLIC":
|
|
398
|
+
publics.extend(s.vars)
|
|
399
|
+
i += 1
|
|
400
|
+
continue
|
|
401
|
+
if isinstance(s, S.Instruction) and (kw == "CONSTTABLE"):
|
|
402
|
+
args = s.args
|
|
403
|
+
name = args[0].name if args and isinstance(args[0], S.Name) else None
|
|
404
|
+
hidden = _as_int(args[1].value, 0) if len(args) > 1 and isinstance(args[1], S.Number) else 0
|
|
405
|
+
j = i + 1
|
|
406
|
+
const_body: list[S.ConstDecl] = []
|
|
407
|
+
while j < n and _kw(stmts[j]) != "ENDCONSTTABLE":
|
|
408
|
+
candidate = stmts[j]
|
|
409
|
+
if isinstance(candidate, S.ConstDecl):
|
|
410
|
+
const_body.append(candidate)
|
|
411
|
+
j += 1
|
|
412
|
+
const_tables.append(ConstTableDecl(name, hidden, const_body, s.line))
|
|
413
|
+
i = j + 1
|
|
414
|
+
continue
|
|
415
|
+
if isinstance(s, S.Instruction) and (kw == "DATATABLE"):
|
|
416
|
+
args = s.args
|
|
417
|
+
name = args[0].name if args and isinstance(args[0], S.Name) else ""
|
|
418
|
+
value = (program.consts or {}).get(name.upper()) if name else None
|
|
419
|
+
if isinstance(value, str) and value:
|
|
420
|
+
name = value # a String `Const` names the table its value spells (oracle, round 67)
|
|
421
|
+
trig = args[1] if len(args) > 1 else None
|
|
422
|
+
size_expr = args[2] if len(args) > 2 else None
|
|
423
|
+
j = i + 1
|
|
424
|
+
body: list[S.Stmt] = []
|
|
425
|
+
while j < n and _kw(stmts[j]) != "ENDTABLE":
|
|
426
|
+
body.append(stmts[j])
|
|
427
|
+
j += 1
|
|
428
|
+
data_tables.append(DataTableDecl(name, trig, size_expr, body, s.line))
|
|
429
|
+
i = j + 1
|
|
430
|
+
continue
|
|
431
|
+
if kw == "SLOWSEQUENCE":
|
|
432
|
+
slow_seq_count += 1
|
|
433
|
+
i += 1
|
|
434
|
+
continue
|
|
435
|
+
if isinstance(s, S.Instruction) and (kw == "ALIAS"):
|
|
436
|
+
alias_stmts.append(s)
|
|
437
|
+
i += 1
|
|
438
|
+
continue
|
|
439
|
+
if isinstance(s, S.Instruction) and (kw == "UNITS"):
|
|
440
|
+
units_stmts.append(s)
|
|
441
|
+
i += 1
|
|
442
|
+
continue
|
|
443
|
+
if isinstance(s, S.Instruction) and (kw == "READONLY"):
|
|
444
|
+
for arg in s.args:
|
|
445
|
+
if isinstance(arg, S.Name):
|
|
446
|
+
readonly_names.add(arg.name.upper())
|
|
447
|
+
i += 1
|
|
448
|
+
continue
|
|
449
|
+
i += 1
|
|
450
|
+
|
|
451
|
+
called: set[str] = set()
|
|
452
|
+
|
|
453
|
+
for s in _flat(stmts):
|
|
454
|
+
kw2 = _kw(s)
|
|
455
|
+
if kw2 == "CALLTABLE" and s.args and isinstance(s.args[0], S.Name):
|
|
456
|
+
called.add(s.args[0].name.upper())
|
|
457
|
+
elif kw2 == "ACCEPTDATARECORDS" and len(s.args) > 2 and isinstance(s.args[2], S.Name):
|
|
458
|
+
# AcceptDataRecords(PakBusAddr, TableNo, DestTableName): populates DestTableName
|
|
459
|
+
# without a `CallTable` (oracle-confirmed, acceptdatarecords_1.CR6: a fixed-size
|
|
460
|
+
# table named only here still gets its declared size, not the uncalled `size=1`).
|
|
461
|
+
called.add(s.args[2].name.upper())
|
|
462
|
+
|
|
463
|
+
publics = [v for v in publics if (v.type or "").upper() not in struct_types]
|
|
464
|
+
uses_ch400 = any(_kw(s) == "CH400STATUS" for s in stmts)
|
|
465
|
+
uses_cpi = any(_kw(s).startswith("CDM_") or _kw(s) == "CPIFILESEND" for s in stmts)
|
|
466
|
+
|
|
467
|
+
return _Scanned(
|
|
468
|
+
uses_ch400=uses_ch400,
|
|
469
|
+
struct_types=struct_types,
|
|
470
|
+
uses_cpi=uses_cpi,
|
|
471
|
+
publics=publics,
|
|
472
|
+
all_vars=all_vars,
|
|
473
|
+
const_tables=const_tables,
|
|
474
|
+
data_tables=data_tables,
|
|
475
|
+
alias_stmts=alias_stmts,
|
|
476
|
+
units_stmts=units_stmts,
|
|
477
|
+
readonly_names=readonly_names,
|
|
478
|
+
slow_seq_count=slow_seq_count,
|
|
479
|
+
called=called,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _flat(ss):
|
|
484
|
+
"""`ss` with the statements of every inline `If c Then ... Else ...` spliced in after it (an
|
|
485
|
+
inline `If c Then CallTable T` calls T too)."""
|
|
486
|
+
for x in ss:
|
|
487
|
+
yield x
|
|
488
|
+
yield from _flat((getattr(x, "inline", None) or []) + (getattr(x, "inline_else", None) or []))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _scan_weights(stmts: list[S.Stmt], env: consteval.Env) -> dict[str, float]:
|
|
492
|
+
"""Maps each `CallTable`d table name (upper) to the interval, in seconds, of the nearest
|
|
493
|
+
enclosing `Scan` block (a top-level `Scan` or a `SlowSequence`'s own `Scan`) that contains its
|
|
494
|
+
`CallTable` statement. Used as the auto-size weight for a table with no `DataInterval` (see
|
|
495
|
+
`_build_data_table`/`_resolve_auto_sizes` and docs/tdf-rules.md §10 "scan-interval weighting",
|
|
496
|
+
oracle-confirmed against `FinApp_tester.CR6`: real weight is the enclosing scan's interval, not
|
|
497
|
+
a flat 1 second). `SubScan` is transparent (does not change the enclosing weight) -- untested,
|
|
498
|
+
matches §4's "SubScan has no effect" finding for the Status table.
|
|
499
|
+
"""
|
|
500
|
+
stack: list[float] = []
|
|
501
|
+
weights: dict[str, float] = {}
|
|
502
|
+
for s in stmts:
|
|
503
|
+
kw = _kw(s)
|
|
504
|
+
if isinstance(s, S.Instruction) and (kw == "SCAN"):
|
|
505
|
+
secs = None
|
|
506
|
+
if s.args:
|
|
507
|
+
val = _fold(s.args[0], env, None)
|
|
508
|
+
# `_fold` legitimately returns a non-numeric Python value (e.g. a string literal's
|
|
509
|
+
# own text) for a `Scan` interval the real compiler itself rejects elsewhere with
|
|
510
|
+
# its own specific message (`Scan Interval parameter must be a non-negative
|
|
511
|
+
# integer`, an instruction-specific value check, not this module's concern, see
|
|
512
|
+
# `docs/fuzz-findings.md` "Crash"). This function unconditionally runs for every
|
|
513
|
+
# `Scan` block regardless of whether that check ran or found anything, so it must
|
|
514
|
+
# tolerate the value being un-usable here rather than assume `float()` succeeds.
|
|
515
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool):
|
|
516
|
+
unit_name = _scan_unit_name(s.args[1], env) if len(s.args) > 1 else "SEC"
|
|
517
|
+
secs = float(val) * _UNIT_SECONDS.get(unit_name, 1)
|
|
518
|
+
stack.append(secs if secs else 1.0)
|
|
519
|
+
elif kw == "NEXTSCAN":
|
|
520
|
+
if stack:
|
|
521
|
+
stack.pop()
|
|
522
|
+
elif isinstance(s, S.Instruction) and (
|
|
523
|
+
kw == "CALLTABLE" and s.args and isinstance(s.args[0], S.Name) and stack
|
|
524
|
+
):
|
|
525
|
+
weights[s.args[0].name.upper()] = stack[-1]
|
|
526
|
+
return weights
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _scan_calls(stmts: list[S.Stmt], env: consteval.Env) -> dict[str, list[float | None]]:
|
|
530
|
+
"""Maps each table name (upper) to the interval (seconds, None when not a plain constant) of
|
|
531
|
+
every enclosing `Scan` of each of its `CallTable` statements that is lexically inside one;
|
|
532
|
+
a call in a `Sub` or outside any `Scan` adds nothing (`checker/storage.py`)."""
|
|
533
|
+
stack: list[float | None] = []
|
|
534
|
+
calls: dict[str, list[float | None]] = {}
|
|
535
|
+
for s in _flat(stmts):
|
|
536
|
+
kw = _kw(s)
|
|
537
|
+
if kw == "SCAN":
|
|
538
|
+
secs = None
|
|
539
|
+
if s.args:
|
|
540
|
+
val = _fold(s.args[0], env, None)
|
|
541
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool) and val > 0:
|
|
542
|
+
unit_name = _scan_unit_name(s.args[1], env) if len(s.args) > 1 else "SEC"
|
|
543
|
+
secs = float(val) * _UNIT_SECONDS.get(unit_name, 1)
|
|
544
|
+
stack.append(secs)
|
|
545
|
+
elif kw == "NEXTSCAN":
|
|
546
|
+
if stack:
|
|
547
|
+
stack.pop()
|
|
548
|
+
elif kw == "CALLTABLE" and s.args and isinstance(s.args[0], S.Name) and stack:
|
|
549
|
+
calls.setdefault(s.args[0].name.upper(), []).append(stack[-1])
|
|
550
|
+
return calls
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _last_call_interval(stmts: list[S.Stmt], env: consteval.Env) -> dict[str, float | None]:
|
|
554
|
+
"""The call interval (seconds) the real compiler leaves on each table: that of the last
|
|
555
|
+
`CallTable` in source order (the enclosing `Scan`'s interval; `None` for a call outside every
|
|
556
|
+
`Scan`, e.g. in a `Sub`, or one whose interval is not a plain constant). `checker/storage.py`."""
|
|
557
|
+
stack: list[float | None] = []
|
|
558
|
+
last: dict[str, float | None] = {}
|
|
559
|
+
for s in _flat(stmts):
|
|
560
|
+
kw = _kw(s)
|
|
561
|
+
if kw == "SCAN":
|
|
562
|
+
secs = None
|
|
563
|
+
if s.args:
|
|
564
|
+
val = _fold(s.args[0], env, None)
|
|
565
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool) and val > 0:
|
|
566
|
+
unit_name = _scan_unit_name(s.args[1], env) if len(s.args) > 1 else "SEC"
|
|
567
|
+
secs = float(val) * _UNIT_SECONDS.get(unit_name, 1)
|
|
568
|
+
stack.append(secs)
|
|
569
|
+
elif kw == "NEXTSCAN":
|
|
570
|
+
if stack:
|
|
571
|
+
stack.pop()
|
|
572
|
+
elif kw == "CALLTABLE" and s.args and isinstance(s.args[0], S.Name):
|
|
573
|
+
last[s.args[0].name.upper()] = stack[-1] if stack else None
|
|
574
|
+
return last
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ---------------------------------------------------------------- Status table (§4)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
_STATUS_INSERTIONS = [
|
|
581
|
+
("SkippedSystemScan", "SkippedSlowScan", 6, False, ""),
|
|
582
|
+
("LastSystemScan", "LastSlowScan", 14, True, ""),
|
|
583
|
+
("SystemProcTime", "SlowProcTime", 6, True, "usec"),
|
|
584
|
+
("MaxSystemProcTime", "MaxSlowProcTime", 6, False, "usec"),
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _status_table(slow_seq_count: int) -> TdfTable:
|
|
589
|
+
fields = []
|
|
590
|
+
for f in _STATUS_JSON["fields"]:
|
|
591
|
+
fields.append(
|
|
592
|
+
TdfField(
|
|
593
|
+
f["type"],
|
|
594
|
+
f["read_only"],
|
|
595
|
+
f["name"],
|
|
596
|
+
list(f["aliases"]),
|
|
597
|
+
f["processing"],
|
|
598
|
+
f["units"],
|
|
599
|
+
f["description"],
|
|
600
|
+
f["begin"],
|
|
601
|
+
f["dim"],
|
|
602
|
+
list(f["subdims"]),
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
if slow_seq_count >= 1:
|
|
606
|
+
if slow_seq_count == 1:
|
|
607
|
+
dim, subdims = 1, []
|
|
608
|
+
else:
|
|
609
|
+
dim, subdims = slow_seq_count, [slow_seq_count]
|
|
610
|
+
for after, name, type_code, ro, units in _STATUS_INSERTIONS:
|
|
611
|
+
new_field = TdfField(type_code, ro, name, [], "", units, "", 1, dim, list(subdims))
|
|
612
|
+
idx = next(k for k, existing in enumerate(fields) if existing.name == after)
|
|
613
|
+
fields.insert(idx + 1, new_field)
|
|
614
|
+
return TdfTable(
|
|
615
|
+
"Status",
|
|
616
|
+
1,
|
|
617
|
+
_STATUS_JSON["time_type"],
|
|
618
|
+
tuple(_STATUS_JSON["time_into"]),
|
|
619
|
+
tuple(_STATUS_JSON["interval"]),
|
|
620
|
+
fields,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _build_struct_tables(all_vars: list[S.VarDecl], struct_types: dict, env: consteval.Env):
|
|
625
|
+
"""docs/tdf-rules.md §2: every Public/Dim variable of a StructureType is a table named after
|
|
626
|
+
the variable (size = product of its dims, one field per member), in reverse declaration order."""
|
|
627
|
+
out = []
|
|
628
|
+
for v in reversed(all_vars):
|
|
629
|
+
st = struct_types.get((v.type or "").upper())
|
|
630
|
+
if st is None:
|
|
631
|
+
continue
|
|
632
|
+
size = 1
|
|
633
|
+
for d in _var_shape(v, env):
|
|
634
|
+
size *= d
|
|
635
|
+
fields = []
|
|
636
|
+
for m in st["members"]:
|
|
637
|
+
info = _make_var_info(m, env)
|
|
638
|
+
dim, subdims = info.collapsed()
|
|
639
|
+
fields.append(
|
|
640
|
+
TdfField(
|
|
641
|
+
info.type_code,
|
|
642
|
+
m.name.upper() in st["ro"],
|
|
643
|
+
m.name,
|
|
644
|
+
[],
|
|
645
|
+
"",
|
|
646
|
+
st["units"].get(m.name.upper(), ""),
|
|
647
|
+
"",
|
|
648
|
+
1,
|
|
649
|
+
dim,
|
|
650
|
+
subdims,
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
out.append(TdfTable(v.name, size, 14, (0, 0), (0, 0), fields))
|
|
654
|
+
return out
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
_DEG_C = "\xef\xbf\xbdC"
|
|
658
|
+
# CH400Status instruction table (docs/tdf-rules.md §2): (name, units); None = 28-byte string. The
|
|
659
|
+
# temperature units are the bytes EF BF BD 'C' (a UTF-8 replacement character, then C) -- reproduced
|
|
660
|
+
# as latin-1 text.
|
|
661
|
+
_CH400_FIELDS = (
|
|
662
|
+
("ChargeAVoltage", "Volts"),
|
|
663
|
+
("ChargeACurrent", "Amps"),
|
|
664
|
+
("ChargeBVoltage", "Volts"),
|
|
665
|
+
("ChargeBCurrent", "Amps"),
|
|
666
|
+
("InputChargeStatus", None),
|
|
667
|
+
("InputCurrentLimit", "Amps"),
|
|
668
|
+
("BattAVoltage", "Volts"),
|
|
669
|
+
("BattACurrent", "Amps"),
|
|
670
|
+
("BattAChargeStatus", None),
|
|
671
|
+
("BattASetVoltage", "Volts"),
|
|
672
|
+
("BattACurrentLimit", "Amps"),
|
|
673
|
+
("BattADepthofDischarge", "%"),
|
|
674
|
+
("BattBVoltage", "Volts"),
|
|
675
|
+
("BattBCurrent", "Amps"),
|
|
676
|
+
("BattBChargeStatus", None),
|
|
677
|
+
("BattBSetVoltage", "Volts"),
|
|
678
|
+
("BattBCurrentLimit", "Amps"),
|
|
679
|
+
("BattBDepthofDischarge", "%"),
|
|
680
|
+
("SystemVoltage", "Volts"),
|
|
681
|
+
("SystemCurrent", "Amps"),
|
|
682
|
+
("AlwaysOn12Voltage", "Volts"),
|
|
683
|
+
("AlwaysOn12Current", "Amps"),
|
|
684
|
+
("Switched12Voltage", "Volts"),
|
|
685
|
+
("Switched12Current", "Amps"),
|
|
686
|
+
("Switched24Voltage", "Volts"),
|
|
687
|
+
("Switched24Current", "Amps"),
|
|
688
|
+
("PowerSwitchState", None),
|
|
689
|
+
("MaxBoardTemp", _DEG_C),
|
|
690
|
+
("MinBoardTemp", _DEG_C),
|
|
691
|
+
("PanelTemp", _DEG_C),
|
|
692
|
+
("BattAProbeTemp", _DEG_C),
|
|
693
|
+
("BattBProbeTemp", _DEG_C),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _ch400status_table() -> TdfTable:
|
|
698
|
+
fields = [
|
|
699
|
+
TdfField(
|
|
700
|
+
11 if u is None else 9, True, n, [], "", u or "", "", 1, 28 if u is None else 1, [28] if u is None else []
|
|
701
|
+
)
|
|
702
|
+
for n, u in _CH400_FIELDS
|
|
703
|
+
]
|
|
704
|
+
return TdfTable("CH400Status", 1, 14, (0, 0), (0, 0), fields)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _cpistatus_table() -> TdfTable:
|
|
708
|
+
"""docs/tdf-rules.md §2: present iff a `CDM_*` instruction or `CPIFileSend` is used."""
|
|
709
|
+
|
|
710
|
+
def f(t: int, ro: bool, name: str, dim: int = 1, sub: tuple[int, ...] = ()) -> TdfField:
|
|
711
|
+
return TdfField(t, ro, name, [], "", "", "", 1, dim, list(sub))
|
|
712
|
+
|
|
713
|
+
fields = [f(9, True, "BusLoad")] + [
|
|
714
|
+
f(6, False, n) for n in ("ModuleReportCount", "ActiveModules", "BuffErr", "RxErrMax", "TxErrMax", "FrameErr")
|
|
715
|
+
]
|
|
716
|
+
fields.append(f(11, False, "ModuleInfo", 6400, (50, 128)))
|
|
717
|
+
return TdfTable("CPIStatus", 1, 14, (0, 0), (0, 0), fields)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
# ---------------------------------------------------------------- Public table (§8)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _resolve_alias_target(expr: S.Expr | None, var_info: dict | None = None) -> tuple[str, int | None]:
|
|
724
|
+
"""`Var` -> (Var, None); `Var(i)` -> (Var, i) for a single constant index; `Var(i,j)` of a
|
|
725
|
+
declared multi-dimensional array -> its row-major flat 1-based index."""
|
|
726
|
+
if isinstance(expr, S.Name):
|
|
727
|
+
return expr.name.upper(), None
|
|
728
|
+
if isinstance(expr, S.Index) and len(expr.args) > 1 and var_info is not None:
|
|
729
|
+
info = var_info.get(expr.name.upper())
|
|
730
|
+
numbers = [a for a in expr.args if isinstance(a, S.Number)]
|
|
731
|
+
if info is not None and len(info.shape) == len(expr.args) == len(numbers):
|
|
732
|
+
flat = 0
|
|
733
|
+
for k, a in enumerate(numbers):
|
|
734
|
+
stride = 1
|
|
735
|
+
for d in info.shape[k + 1 :]:
|
|
736
|
+
stride *= d
|
|
737
|
+
flat += (_as_int(a.value) - 1) * stride
|
|
738
|
+
return expr.name.upper(), flat + 1
|
|
739
|
+
if isinstance(expr, S.Index) and expr.args:
|
|
740
|
+
idx = expr.args[0]
|
|
741
|
+
if isinstance(idx, S.Number):
|
|
742
|
+
return expr.name.upper(), _as_int(idx.value)
|
|
743
|
+
if isinstance(expr, S.Index):
|
|
744
|
+
return expr.name.upper(), None
|
|
745
|
+
return "", None
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _build_public_table(
|
|
749
|
+
publics: list[S.VarDecl],
|
|
750
|
+
var_info: dict[str, VarInfo],
|
|
751
|
+
alias_stmts: list[S.Instruction],
|
|
752
|
+
units_stmts: list[S.Instruction],
|
|
753
|
+
readonly_names: set[str],
|
|
754
|
+
):
|
|
755
|
+
"""Returns (TdfTable('Public') or None, display_name -> TdfField, base_name -> [TdfField])."""
|
|
756
|
+
if not publics:
|
|
757
|
+
return None, {}, {}
|
|
758
|
+
|
|
759
|
+
# base_name(upper) -> list of aliased element indices (1-based) -> alias display name
|
|
760
|
+
aliased_elements: dict[str, dict[int, str]] = {}
|
|
761
|
+
whole_alias: dict[str, str] = {}
|
|
762
|
+
for a in alias_stmts:
|
|
763
|
+
if len(a.args) < 2 or not isinstance(a.args[1], S.Name):
|
|
764
|
+
continue
|
|
765
|
+
base, idx = _resolve_alias_target(a.args[0], var_info)
|
|
766
|
+
alias_name = a.args[1].name
|
|
767
|
+
if idx is None:
|
|
768
|
+
whole_alias[base] = alias_name
|
|
769
|
+
else:
|
|
770
|
+
aliased_elements.setdefault(base, {})[idx] = alias_name
|
|
771
|
+
|
|
772
|
+
fields: list[TdfField] = []
|
|
773
|
+
by_display: dict[str, TdfField] = {}
|
|
774
|
+
by_base: dict[str, list[TdfField]] = {}
|
|
775
|
+
|
|
776
|
+
for v in publics:
|
|
777
|
+
base = v.name.upper()
|
|
778
|
+
info = var_info[base]
|
|
779
|
+
display_name = whole_alias.get(base, v.name)
|
|
780
|
+
ro = base in readonly_names
|
|
781
|
+
|
|
782
|
+
elements = aliased_elements.get(base)
|
|
783
|
+
if elements and info.shape:
|
|
784
|
+
array_size = 1
|
|
785
|
+
for d in info.shape:
|
|
786
|
+
array_size *= d
|
|
787
|
+
full_shape = info.full_shape()
|
|
788
|
+
group: list[TdfField] = []
|
|
789
|
+
pos = 1
|
|
790
|
+
es = info.stored_len or 1 # String elements are stored_len bytes (docs/tdf-rules.md §9a)
|
|
791
|
+
while pos <= array_size:
|
|
792
|
+
if pos in elements:
|
|
793
|
+
f = TdfField(
|
|
794
|
+
info.type_code, ro, elements[pos], [], "", "", "", 1, es, [es] if info.stored_len else []
|
|
795
|
+
)
|
|
796
|
+
f.alias_elem = True # type: ignore[attr-defined]
|
|
797
|
+
f.alias_of = (v.name, pos) # type: ignore[attr-defined]
|
|
798
|
+
group.append(f)
|
|
799
|
+
pos += 1
|
|
800
|
+
else:
|
|
801
|
+
run_start = pos
|
|
802
|
+
while pos <= array_size and pos not in elements:
|
|
803
|
+
pos += 1
|
|
804
|
+
run_len = pos - run_start
|
|
805
|
+
f = TdfField(
|
|
806
|
+
info.type_code,
|
|
807
|
+
ro,
|
|
808
|
+
v.name,
|
|
809
|
+
[],
|
|
810
|
+
"",
|
|
811
|
+
"",
|
|
812
|
+
"",
|
|
813
|
+
(run_start - 1) * es + 1,
|
|
814
|
+
run_len * es,
|
|
815
|
+
list(full_shape),
|
|
816
|
+
)
|
|
817
|
+
group.append(f)
|
|
818
|
+
fields.extend(group)
|
|
819
|
+
for f in group:
|
|
820
|
+
by_display[f.name.upper()] = f
|
|
821
|
+
by_base[base] = group
|
|
822
|
+
continue
|
|
823
|
+
|
|
824
|
+
dim, subdims = info.collapsed()
|
|
825
|
+
f = TdfField(info.type_code, ro, display_name, [], "", "", "", 1, dim, subdims)
|
|
826
|
+
fields.append(f)
|
|
827
|
+
by_display[display_name.upper()] = f
|
|
828
|
+
by_base[base] = [f]
|
|
829
|
+
|
|
830
|
+
for u in units_stmts:
|
|
831
|
+
if len(u.args) < 2 or not isinstance(u.args[1], S.String):
|
|
832
|
+
continue
|
|
833
|
+
target, label = u.args[0], u.args[1].value
|
|
834
|
+
targets: list[TdfField] = []
|
|
835
|
+
if isinstance(target, S.Index) and not target.args:
|
|
836
|
+
targets = by_base.get(target.name.upper(), [])
|
|
837
|
+
elif isinstance(target, (S.Name, S.Index)):
|
|
838
|
+
nm = target.name.upper()
|
|
839
|
+
if nm in by_base:
|
|
840
|
+
targets = by_base[nm] # Units on the declared name covers every alias split too
|
|
841
|
+
elif nm in by_display:
|
|
842
|
+
targets = [by_display[nm]]
|
|
843
|
+
for f in targets:
|
|
844
|
+
f.units = label
|
|
845
|
+
|
|
846
|
+
return TdfTable("Public", 1, 14, (0, 0), (0, 0), fields), by_display, by_base
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
# ---------------------------------------------------------------- ConstTable (§7)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _build_const_tables(const_tables: list[ConstTableDecl], env: consteval.Env) -> list[TdfTable]:
|
|
853
|
+
tables = []
|
|
854
|
+
int_names: dict[str, bool] = {}
|
|
855
|
+
for ct in const_tables:
|
|
856
|
+
fields = []
|
|
857
|
+
for c in ct.consts:
|
|
858
|
+
is_str = isinstance(c.value, S.String)
|
|
859
|
+
if c.type:
|
|
860
|
+
# An explicit `Const X As <Type> = ...` (oracle-confirmed,
|
|
861
|
+
# mqttpublishconsttable_1.CR6: `Const myDouble As Double = 987.654321` -> IEEE8,
|
|
862
|
+
# not the IEEE4 the literal alone would infer) overrides literal-based inference.
|
|
863
|
+
type_code = TYPE_CODES.get(c.type.upper(), TYPE_CODES["IEEE4"])
|
|
864
|
+
is_ascii = type_code == TYPE_CODES["STRING"]
|
|
865
|
+
dim, subdims = (80, [80]) if is_ascii else (1, [])
|
|
866
|
+
fields.append(TdfField(type_code, False, c.name, [], "", "", "", 1, dim, subdims))
|
|
867
|
+
int_names[c.name.upper()] = type_code == TYPE_CODES["LONG"]
|
|
868
|
+
elif is_str:
|
|
869
|
+
fields.append(TdfField(TYPE_CODES["STRING"], False, c.name, [], "", "", "", 1, 80, [80]))
|
|
870
|
+
int_names[c.name.upper()] = True
|
|
871
|
+
else:
|
|
872
|
+
is_int = _const_is_int(c.value, env, int_names)
|
|
873
|
+
int_names[c.name.upper()] = is_int
|
|
874
|
+
type_code = TYPE_CODES["LONG"] if is_int else TYPE_CODES["IEEE4"]
|
|
875
|
+
fields.append(TdfField(type_code, False, c.name, [], "", "", "", 1, 1, []))
|
|
876
|
+
if ct.name is None:
|
|
877
|
+
continue # bare ConstTable: no TDF table (§7)
|
|
878
|
+
fields.append(TdfField(TYPE_CODES["BOOLEAN"], False, "ApplyAndRestart", [], "", "", "", 1, 1, []))
|
|
879
|
+
tables.append(TdfTable(ct.name, 1, 14, (0, 0), (0, 0), fields))
|
|
880
|
+
return tables
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
# ---------------------------------------------------------------- DataTableInfo (§6)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _datatableinfo_table(n: int) -> TdfTable:
|
|
887
|
+
def dims(n):
|
|
888
|
+
return [] if n == 1 else [n]
|
|
889
|
+
|
|
890
|
+
fields = (
|
|
891
|
+
[]
|
|
892
|
+
if n == 0
|
|
893
|
+
else [
|
|
894
|
+
TdfField(11, True, "DataTableName", [], "", "", "", 1, 24 * n, [n, 24]),
|
|
895
|
+
TdfField(6, False, "SkippedRecord", [], "", "", "", 1, n, dims(n)),
|
|
896
|
+
TdfField(6, True, "DataRecordSize", [], "", "records", "", 1, 2 * n, [n, 2]),
|
|
897
|
+
TdfField(9, True, "SecsPerRecord", [], "", "", "", 1, n, dims(n)),
|
|
898
|
+
TdfField(9, True, "DataFillDays", [], "", "days", "", 1, 2 * n, [n, 2]),
|
|
899
|
+
TdfField(11, True, "DataFilled", [], "", "", "", 1, 64 * n, [n, 2, 32]),
|
|
900
|
+
TdfField(14, True, "LastPublish", [], "", "", "", 1, n, dims(n)),
|
|
901
|
+
]
|
|
902
|
+
)
|
|
903
|
+
return TdfTable("DataTableInfo", 1, 14, (0, 0), (0, 0), fields)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
# ---------------------------------------------------------------- DataTable output fields (§9)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _source_info(expr: S.Expr | None, var_info: dict[str, VarInfo]) -> VarInfo | None:
|
|
910
|
+
if isinstance(expr, (S.Name, S.Index)):
|
|
911
|
+
return var_info.get(expr.name.upper())
|
|
912
|
+
if isinstance(expr, S.Member) and isinstance(expr.base, (S.Name, S.Index)):
|
|
913
|
+
return var_info.get(f"{expr.base.name}.{expr.member}".upper())
|
|
914
|
+
return None
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _source_name(expr: S.Expr | None, var_info: dict[str, VarInfo] | None = None) -> str:
|
|
918
|
+
if var_info is not None:
|
|
919
|
+
info = _source_info(expr, var_info)
|
|
920
|
+
if info is not None and info.name:
|
|
921
|
+
return info.name # declared spelling; CRBasic is case-insensitive at the use site
|
|
922
|
+
if isinstance(expr, (S.Name, S.Index)):
|
|
923
|
+
return expr.name
|
|
924
|
+
return expr.member if isinstance(expr, S.Member) else "Anonymous"
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _is_anonymous_source(expr: S.Expr | None, var_info: dict, env: consteval.Env) -> bool:
|
|
928
|
+
"""docs/tdf-rules.md §9c: a Source that is not a variable (literal, `Const`, `NAN`/`True`, any
|
|
929
|
+
expression, a function call) is named `Anonymous<n>`; a `Table.Field` is not (it takes the
|
|
930
|
+
referenced field's name)."""
|
|
931
|
+
if isinstance(expr, S.Index):
|
|
932
|
+
return expr.name.upper() not in var_info
|
|
933
|
+
if isinstance(expr, S.Name):
|
|
934
|
+
return expr.name.upper() not in var_info and (_fold(expr, env, None) is not None or expr.name.upper() == "NAN")
|
|
935
|
+
return expr is not None and not isinstance(expr, S.Member)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _anonymous_string_len(expr: S.Expr | None, var_info: dict, env: consteval.Env) -> int:
|
|
939
|
+
"""Character count that sizes a non-variable String source (§9c): a literal is its own length,
|
|
940
|
+
a String variable its stored length, `+`/`&` of those the sum. String functions propagate a
|
|
941
|
+
temporary buffer length, not the length of their runtime result. Unknown/numeric sources
|
|
942
|
+
retain the 20-character conversion default (stored 24)."""
|
|
943
|
+
|
|
944
|
+
def known(e):
|
|
945
|
+
v = _fold(e, env, None)
|
|
946
|
+
if isinstance(v, str):
|
|
947
|
+
return len(v)
|
|
948
|
+
info = _source_info(e, var_info)
|
|
949
|
+
if info is not None:
|
|
950
|
+
return info.stored_len
|
|
951
|
+
if isinstance(e, (S.Name, S.Index)):
|
|
952
|
+
fn = env.profile.functions.get(e.name.upper())
|
|
953
|
+
if isinstance(e, S.Index) and fn and fn["kind"] == 312:
|
|
954
|
+
code = fn["code"]
|
|
955
|
+
if code == 229: # Chr / Chr$
|
|
956
|
+
return 2
|
|
957
|
+
if code == 243: # Hex
|
|
958
|
+
return 0
|
|
959
|
+
if code == 254: # IPInfo
|
|
960
|
+
return 104
|
|
961
|
+
if code in (239, 251) and e.args:
|
|
962
|
+
n = known(e.args[-1])
|
|
963
|
+
return (24 if n is None else n) + 60
|
|
964
|
+
if code == 252 and e.args: # FormatLongLong's single variable/string argument
|
|
965
|
+
return known(e.args[0])
|
|
966
|
+
if code == 250 and len(e.args) == 3: # Replace
|
|
967
|
+
first, last = known(e.args[0]), known(e.args[2])
|
|
968
|
+
return (24 if first is None else first) - 1 + (24 if last is None else last)
|
|
969
|
+
if code in (230, 237, 238, 245, 246, 247, 248, 249) and e.args:
|
|
970
|
+
n = known(e.args[0])
|
|
971
|
+
return 24 if n is None else n
|
|
972
|
+
return None
|
|
973
|
+
if isinstance(e, S.Binary) and e.op in ("+", "&"):
|
|
974
|
+
l, r = known(e.left), known(e.right)
|
|
975
|
+
return None if l is None or r is None else l + r
|
|
976
|
+
return None
|
|
977
|
+
|
|
978
|
+
n = known(expr)
|
|
979
|
+
return 20 if n is None else n
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _number_anonymous(
|
|
983
|
+
fields: list, src: S.Expr | None, var_info: dict, env: consteval.Env, anon: list[int] | None
|
|
984
|
+
) -> None:
|
|
985
|
+
"""Give the fields derived from a non-variable Source its `Anonymous<n>` name; `anon[0]` is the
|
|
986
|
+
program-wide count so far (one number per such Source, FieldNames or not)."""
|
|
987
|
+
if anon is None or not _is_anonymous_source(src, var_info, env):
|
|
988
|
+
return
|
|
989
|
+
anon[0] += 1
|
|
990
|
+
raw = _source_name(src, var_info)
|
|
991
|
+
for f in fields:
|
|
992
|
+
if f.name.startswith(raw):
|
|
993
|
+
f.name = f"Anonymous{anon[0]}" + f.name[len(raw) :]
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _source_shape(
|
|
997
|
+
expr: S.Expr | None, var_info: dict[str, VarInfo], env: consteval.Env, reps: int
|
|
998
|
+
) -> tuple[int, list[int]]:
|
|
999
|
+
"""(begin, subdims) for an output field's Source, per §3: subdims are the *declared* shape of the
|
|
1000
|
+
source array whatever `reps` is; begin is the 1-based row-major flat index of the first element."""
|
|
1001
|
+
if isinstance(expr, S.Member) and isinstance(expr.base, (S.Name, S.Index)):
|
|
1002
|
+
info = var_info.get(f"{expr.base.name}.{expr.member}".upper())
|
|
1003
|
+
if info is not None:
|
|
1004
|
+
return 1, list(info.shape)
|
|
1005
|
+
if isinstance(expr, (S.Index, S.Name)):
|
|
1006
|
+
info = var_info.get(expr.name.upper())
|
|
1007
|
+
if isinstance(expr, S.Index) and info is None and (len(expr.args) != 1 or isinstance(expr.args[0], S.String)):
|
|
1008
|
+
# The source-name parser accepts a single integer subscript, even on a function,
|
|
1009
|
+
# but not a comma-separated argument list or a quoted string (§9c).
|
|
1010
|
+
return 1, []
|
|
1011
|
+
array_shape = list(info.shape) if info and info.shape else ([reps] if isinstance(expr, S.Index) else [])
|
|
1012
|
+
if array_shape == [1]:
|
|
1013
|
+
array_shape = [] # §3 collapse rule: a one-element array has no subdims
|
|
1014
|
+
begin = 1
|
|
1015
|
+
if isinstance(expr, S.Index) and expr.args:
|
|
1016
|
+
idx = [_as_int(_fold(a, env, 1), 1) for a in expr.args]
|
|
1017
|
+
if info and info.shape and len(idx) == len(info.shape) > 1:
|
|
1018
|
+
if not all(isinstance(a, (S.Number, S.Name)) for a in expr.args):
|
|
1019
|
+
return 1, array_shape # docs/tdf-rules.md §9b: expression subscripts give begin=1
|
|
1020
|
+
begin = 0
|
|
1021
|
+
for k, v in enumerate(idx):
|
|
1022
|
+
stride = 1
|
|
1023
|
+
for d in info.shape[k + 1 :]:
|
|
1024
|
+
stride *= d
|
|
1025
|
+
begin += (v - 1) * stride
|
|
1026
|
+
begin += 1
|
|
1027
|
+
else:
|
|
1028
|
+
begin = idx[0]
|
|
1029
|
+
return begin, array_shape
|
|
1030
|
+
return 1, []
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _units_for_source(expr: S.Expr | None, var_info: dict[str, VarInfo], by_display: dict[str, TdfField]) -> str:
|
|
1034
|
+
if isinstance(expr, S.Member) and isinstance(expr.base, (S.Name, S.Index)):
|
|
1035
|
+
info = var_info.get(expr.base.name.upper())
|
|
1036
|
+
if info is not None and info.struct is not None:
|
|
1037
|
+
return info.struct["units"].get(expr.member.upper(), "")
|
|
1038
|
+
name = _source_name(expr, var_info).upper()
|
|
1039
|
+
f = by_display.get(name)
|
|
1040
|
+
if f is not None:
|
|
1041
|
+
return f.units
|
|
1042
|
+
info = var_info.get(name)
|
|
1043
|
+
return info.units if info else ""
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
@dataclass
|
|
1047
|
+
class _OutField:
|
|
1048
|
+
name: str
|
|
1049
|
+
type_code: int
|
|
1050
|
+
read_only: bool
|
|
1051
|
+
processing: str
|
|
1052
|
+
begin: int
|
|
1053
|
+
dim: int
|
|
1054
|
+
subdims: list[int]
|
|
1055
|
+
units: str = ""
|
|
1056
|
+
bytes: int = 0
|
|
1057
|
+
# Duplicate-field-name bookkeeping only (`checker/instr_data.check_duplicate_fields`):
|
|
1058
|
+
# `renamed` marks a name set by `FieldNames`; `name_only` fields (FFT, CH400Status source)
|
|
1059
|
+
# take part in that check but are never written to the TDF or counted in the record size;
|
|
1060
|
+
# `placeholder` distinguishes a generated Anonymous label from a user field of that name;
|
|
1061
|
+
# `rng` is the element range `(lo, hi)` the field covers (`None`: never conflicts by range).
|
|
1062
|
+
renamed: bool = False
|
|
1063
|
+
name_only: bool = False
|
|
1064
|
+
ch400: bool = False
|
|
1065
|
+
placeholder: bool = False
|
|
1066
|
+
rng: tuple[int, int] | None = None
|
|
1067
|
+
unit_list: list | None = None # per-element units when a FieldNames split must differ
|
|
1068
|
+
elem: int = 1 # bytes per element when splittable (a stored String element is stored_len)
|
|
1069
|
+
splittable: bool = False # dim == Reps * element size: follows the Source's alias decomposition
|
|
1070
|
+
struct_members: int = 0 # >0: field of a StructureType source (members per element)
|
|
1071
|
+
member: str = ""
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
_FFT_UNBOUNDED = 1 << 30
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _element_range(src: S.Expr | None, begin: int, dim: int, env: consteval.Env):
|
|
1078
|
+
if isinstance(src, S.Index) and len(src.args) > 1:
|
|
1079
|
+
if dim != 1:
|
|
1080
|
+
return None
|
|
1081
|
+
flat = 0
|
|
1082
|
+
for k, a in enumerate(src.args):
|
|
1083
|
+
v = _fold(a, env, None)
|
|
1084
|
+
if not isinstance(v, (int, float)):
|
|
1085
|
+
return None
|
|
1086
|
+
flat += int(v) << (20 * k)
|
|
1087
|
+
return flat, flat
|
|
1088
|
+
return begin, begin + max(dim, 1) - 1
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
_TYPE_BYTES = {2: 2, 6: 4, 7: 2, 9: 4, 11: 0, 14: 8, 17: 1, 18: 8, 28: 4, 1: 1, 4: 4}
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _type_bytes(type_code: int, dim_for_string: int = 0) -> int:
|
|
1095
|
+
if type_code == 11:
|
|
1096
|
+
return dim_for_string
|
|
1097
|
+
return _TYPE_BYTES.get(type_code, 4)
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def _elem_size(f: _OutField, info: VarInfo) -> int:
|
|
1101
|
+
return info.stored_len if (info.stored_len and f.type_code == 11) else 1
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _split_by_alias(fields: list[_OutField], src, var_info: dict, by_base: dict, env: consteval.Env) -> list[_OutField]:
|
|
1105
|
+
"""docs/tdf-rules.md §9a: an output field over an array that has element `Alias`es follows the
|
|
1106
|
+
Public table's field decomposition: one scalar field per aliased element (alias name + the
|
|
1107
|
+
instruction suffix, the alias' units), one field per un-aliased run (source name, `begin` =
|
|
1108
|
+
run start in the array). Only for fields with `dim == Reps * element size` (a `Covariance(6,A,...)`
|
|
1109
|
+
output of 21 values is one field named after the alias at its first element, unsplit); a
|
|
1110
|
+
`FieldNames`-named field is never split."""
|
|
1111
|
+
if not isinstance(src, (S.Name, S.Index)):
|
|
1112
|
+
return fields
|
|
1113
|
+
base = src.name.upper()
|
|
1114
|
+
groups = by_base.get(base)
|
|
1115
|
+
if not groups or not any(getattr(g, "alias_elem", False) for g in groups):
|
|
1116
|
+
return fields
|
|
1117
|
+
out = []
|
|
1118
|
+
declared = var_info[base].name
|
|
1119
|
+
for f in fields:
|
|
1120
|
+
if f.name_only or f.renamed or not f.name.startswith(declared):
|
|
1121
|
+
out.append(f)
|
|
1122
|
+
continue
|
|
1123
|
+
suffix = f.name[len(declared) :].lstrip("0123456789") # drop the flat-index digits (_idx_suffix)
|
|
1124
|
+
es = _elem_size(f, var_info[base])
|
|
1125
|
+
lo, hi = f.begin, f.begin + f.dim - 1
|
|
1126
|
+
if not f.splittable:
|
|
1127
|
+
# unsplit: named after the alias of the Source's first element, if it has one
|
|
1128
|
+
first = (_source_shape(src, var_info, env, 1)[0] - 1) * es + 1
|
|
1129
|
+
pos = 1
|
|
1130
|
+
for g in groups:
|
|
1131
|
+
n = es if getattr(g, "alias_elem", False) else g.dimension
|
|
1132
|
+
if pos <= first < pos + n:
|
|
1133
|
+
if getattr(g, "alias_elem", False):
|
|
1134
|
+
f.name = g.name + suffix
|
|
1135
|
+
break
|
|
1136
|
+
pos += n
|
|
1137
|
+
out.append(f)
|
|
1138
|
+
continue
|
|
1139
|
+
pos = 1
|
|
1140
|
+
for g in groups:
|
|
1141
|
+
if getattr(g, "alias_elem", False):
|
|
1142
|
+
glo, ghi = pos, pos + es - 1
|
|
1143
|
+
else:
|
|
1144
|
+
glo, ghi = g.begin_index, g.begin_index + g.dimension - 1
|
|
1145
|
+
pos = ghi + 1
|
|
1146
|
+
if ghi < lo or glo > hi:
|
|
1147
|
+
continue
|
|
1148
|
+
if getattr(g, "alias_elem", False):
|
|
1149
|
+
out.append(
|
|
1150
|
+
_OutField(
|
|
1151
|
+
g.name + suffix,
|
|
1152
|
+
f.type_code,
|
|
1153
|
+
f.read_only,
|
|
1154
|
+
f.processing,
|
|
1155
|
+
1,
|
|
1156
|
+
es,
|
|
1157
|
+
list(g.subdims),
|
|
1158
|
+
g.units,
|
|
1159
|
+
rng=(glo, ghi),
|
|
1160
|
+
)
|
|
1161
|
+
)
|
|
1162
|
+
else:
|
|
1163
|
+
a, b = max(lo, glo), min(hi, ghi)
|
|
1164
|
+
out.append(
|
|
1165
|
+
_OutField(
|
|
1166
|
+
f.name,
|
|
1167
|
+
f.type_code,
|
|
1168
|
+
f.read_only,
|
|
1169
|
+
f.processing,
|
|
1170
|
+
a,
|
|
1171
|
+
b - a + 1,
|
|
1172
|
+
list(g.subdims),
|
|
1173
|
+
f.units,
|
|
1174
|
+
rng=(a, b),
|
|
1175
|
+
)
|
|
1176
|
+
)
|
|
1177
|
+
return out
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _fft_field(
|
|
1181
|
+
s: S.Instruction, var_info: dict, by_display: dict, env: consteval.Env, anon: list[int] | None = None
|
|
1182
|
+
) -> _OutField:
|
|
1183
|
+
"""`FFT(Source, DataType, N, Tint, Units, Option)` (docs/tdf-rules.md §9b): one read-only field
|
|
1184
|
+
`<Source>[<flat element index if > 1>]_FFT`; layout by `Option mod 10`, description `FFT,N,<Tint seconds %.4e>,<Option mod 100>`.
|
|
1185
|
+
A `Table.Field` Source this module cannot resolve stays a name-only placeholder (no TDF field);
|
|
1186
|
+
any other non-variable Source is a field named `Anonymous<n>_FFT` (§9c)."""
|
|
1187
|
+
a = s.args
|
|
1188
|
+
src = a[0] if a else None
|
|
1189
|
+
placeholder = src is None or (
|
|
1190
|
+
isinstance(src, S.Member) and not var_info.get(f"{getattr(src.base, 'name', '')}.{src.member}".upper())
|
|
1191
|
+
)
|
|
1192
|
+
if placeholder:
|
|
1193
|
+
return _OutField(
|
|
1194
|
+
"Anonymous",
|
|
1195
|
+
9,
|
|
1196
|
+
True,
|
|
1197
|
+
"FFT",
|
|
1198
|
+
1,
|
|
1199
|
+
_FFT_UNBOUNDED,
|
|
1200
|
+
[],
|
|
1201
|
+
name_only=True,
|
|
1202
|
+
placeholder=True,
|
|
1203
|
+
rng=(1, _FFT_UNBOUNDED),
|
|
1204
|
+
)
|
|
1205
|
+
n = _as_int(_fold(a[2], env, 1), 1) if len(a) > 2 else 1
|
|
1206
|
+
tint = _fold(a[3], env, 0) if len(a) > 3 else 0
|
|
1207
|
+
unit = str(getattr(a[4], "name", "")).upper() if len(a) > 4 else "SEC"
|
|
1208
|
+
opt = _as_int(_fold(a[5], env, 0), 0) if len(a) > 5 else 0
|
|
1209
|
+
half = n // 2 + 1
|
|
1210
|
+
k = opt % 10
|
|
1211
|
+
if k == 5:
|
|
1212
|
+
dim, subdims = n, [n]
|
|
1213
|
+
elif k in (1, 3, 4):
|
|
1214
|
+
dim, subdims = half, [half]
|
|
1215
|
+
elif k in (0, 2):
|
|
1216
|
+
dim, subdims = 2 * half, [half, 2]
|
|
1217
|
+
else:
|
|
1218
|
+
dim, subdims = 2 * half, [half]
|
|
1219
|
+
first, shape = _source_shape(src, var_info, env, 1)
|
|
1220
|
+
if dim == 1:
|
|
1221
|
+
subdims = shape
|
|
1222
|
+
proc = "FFT,%d,%.4e,%d" % (n, float(tint) * _UNIT_SECONDS.get(unit, 1), opt % 100)
|
|
1223
|
+
out = _OutField(
|
|
1224
|
+
_source_name(src, var_info) + (str(first) if first > 1 else "") + "_FFT",
|
|
1225
|
+
(_dtype_code(a[1], env) if len(a) > 1 else 9),
|
|
1226
|
+
True,
|
|
1227
|
+
proc,
|
|
1228
|
+
1,
|
|
1229
|
+
dim,
|
|
1230
|
+
subdims,
|
|
1231
|
+
_units_for_source(src, var_info, by_display),
|
|
1232
|
+
rng=(1, _FFT_UNBOUNDED),
|
|
1233
|
+
)
|
|
1234
|
+
_number_anonymous([out], src, var_info, env, anon)
|
|
1235
|
+
return out
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def _idx_suffix(src: S.Expr | None, var_info: dict, env: consteval.Env) -> str:
|
|
1239
|
+
"""Flat first-element index appended to the source name when the Source is an array element
|
|
1240
|
+
other than the first (FFT, Covariance, Histogram, WindVector; docs/tdf-rules.md §9b)."""
|
|
1241
|
+
first, _ = _source_shape(src, var_info, env, 1)
|
|
1242
|
+
return str(first) if first > 1 else ""
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _alias_source(instr: S.Instruction, var_info: dict, by_display: dict, by_base: dict | None):
|
|
1246
|
+
if (
|
|
1247
|
+
by_base
|
|
1248
|
+
and len(instr.args) > 1
|
|
1249
|
+
and isinstance(instr.args[1], S.Name)
|
|
1250
|
+
and instr.args[1].name.upper() not in var_info
|
|
1251
|
+
):
|
|
1252
|
+
# a Source that names an element Alias is that element of its array (docs/tdf-rules.md §9a)
|
|
1253
|
+
target = by_display.get(instr.args[1].name.upper())
|
|
1254
|
+
of = getattr(target, "alias_of", None)
|
|
1255
|
+
if of is not None:
|
|
1256
|
+
a = instr.args[1]
|
|
1257
|
+
idx = S.Number(a.line, a.file, value=of[1], text=str(of[1]))
|
|
1258
|
+
args = list(instr.args)
|
|
1259
|
+
args[1] = S.Index(a.line, a.file, name=of[0], args=[idx])
|
|
1260
|
+
instr = dataclasses.replace(instr, args=args)
|
|
1261
|
+
return instr
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def _emit_output_fields(
|
|
1265
|
+
instr: S.Instruction,
|
|
1266
|
+
var_info: dict,
|
|
1267
|
+
by_display: dict,
|
|
1268
|
+
env: consteval.Env,
|
|
1269
|
+
by_base: dict | None = None,
|
|
1270
|
+
anon: list[int] | None = None,
|
|
1271
|
+
) -> list[_OutField]:
|
|
1272
|
+
instr = _alias_source(instr, var_info, by_display, by_base)
|
|
1273
|
+
fields = _emit_struct_fields(instr, var_info, env)
|
|
1274
|
+
if fields is None:
|
|
1275
|
+
fields = _emit_output_fields_raw(instr, var_info, by_display, env)
|
|
1276
|
+
if len(instr.args) > 1 and isinstance(instr.args[1], (S.Name, S.Index)):
|
|
1277
|
+
info = var_info.get(instr.args[1].name.upper())
|
|
1278
|
+
reps = _fold(instr.args[0], env, None)
|
|
1279
|
+
if info is not None and consteval.is_finite_number(reps):
|
|
1280
|
+
for f in fields:
|
|
1281
|
+
f.elem = _elem_size(f, info)
|
|
1282
|
+
f.splittable = f.dim == int(reps) * f.elem
|
|
1283
|
+
src = instr.args[1] if len(instr.args) > 1 else None
|
|
1284
|
+
if (
|
|
1285
|
+
instr.keyword.upper() == "SAMPLE"
|
|
1286
|
+
and isinstance(src, S.Name)
|
|
1287
|
+
and src.name.upper() == "CH400STATUS"
|
|
1288
|
+
and "CH400STATUS" not in var_info
|
|
1289
|
+
):
|
|
1290
|
+
for f in fields:
|
|
1291
|
+
f.ch400 = True
|
|
1292
|
+
for f in fields:
|
|
1293
|
+
if f.rng is None and not f.ch400:
|
|
1294
|
+
f.rng = _element_range(src, f.begin, f.dim, env)
|
|
1295
|
+
if instr.keyword.upper() != "HISTOGRAM":
|
|
1296
|
+
_number_anonymous(fields, src, var_info, env, anon)
|
|
1297
|
+
return fields
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
_STRUCT_SUFFIX = {
|
|
1301
|
+
"SAMPLE": ("", "Smp"),
|
|
1302
|
+
"AVERAGE": ("_Avg", "Avg"),
|
|
1303
|
+
"TOTALIZE": ("_Tot", "Tot"),
|
|
1304
|
+
"MAXIMUM": ("_Max", "Max"),
|
|
1305
|
+
"MINIMUM": ("_Min", "Min"),
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def _emit_struct_fields(instr: S.Instruction, var_info: dict, env: consteval.Env):
|
|
1310
|
+
"""docs/tdf-rules.md §9a: an output instruction whose Source is a StructureType variable emits
|
|
1311
|
+
one field per member per element (reps elements); None when not applicable."""
|
|
1312
|
+
kw = instr.keyword.upper()
|
|
1313
|
+
a = instr.args
|
|
1314
|
+
if kw not in _STRUCT_SUFFIX or len(a) < 2 or not isinstance(a[1], (S.Name, S.Index)):
|
|
1315
|
+
return None
|
|
1316
|
+
info = var_info.get(a[1].name.upper())
|
|
1317
|
+
if info is None or info.struct is None:
|
|
1318
|
+
return None
|
|
1319
|
+
if kw in ("MAXIMUM", "MINIMUM") and len(a) > 4 and str(getattr(a[4], "name", "")).upper() == "TRUE":
|
|
1320
|
+
return None # time-of-extreme fields of a structure source: not probed
|
|
1321
|
+
reps = _as_int(_fold(a[0], env, 1), 1)
|
|
1322
|
+
if env.profile.name in ("cr300", "cr350"):
|
|
1323
|
+
reps = 1 # CR300/CR350 expand one element whatever Reps says (oracle, 2026-10-12)
|
|
1324
|
+
suffix, tag = _STRUCT_SUFFIX[kw]
|
|
1325
|
+
st = info.struct
|
|
1326
|
+
out = []
|
|
1327
|
+
for _ in range(reps):
|
|
1328
|
+
for m in st["members"]:
|
|
1329
|
+
mi = _make_var_info(m, env)
|
|
1330
|
+
dim, subdims = mi.collapsed()
|
|
1331
|
+
out.append(
|
|
1332
|
+
_OutField(
|
|
1333
|
+
m.name + suffix,
|
|
1334
|
+
mi.type_code,
|
|
1335
|
+
m.name.upper() in st["ro"],
|
|
1336
|
+
tag,
|
|
1337
|
+
1,
|
|
1338
|
+
dim,
|
|
1339
|
+
subdims,
|
|
1340
|
+
st["units"].get(m.name.upper(), ""),
|
|
1341
|
+
struct_members=len(st["members"]),
|
|
1342
|
+
member=m.name,
|
|
1343
|
+
)
|
|
1344
|
+
)
|
|
1345
|
+
return out
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def _emit_output_fields_raw(
|
|
1349
|
+
instr: S.Instruction, var_info: dict, by_display: dict, env: consteval.Env
|
|
1350
|
+
) -> list[_OutField]:
|
|
1351
|
+
kw = instr.keyword.upper()
|
|
1352
|
+
a = instr.args
|
|
1353
|
+
|
|
1354
|
+
def type_of(idx):
|
|
1355
|
+
return _dtype_code(a[idx], env) if idx < len(a) else 9
|
|
1356
|
+
|
|
1357
|
+
def reps_of():
|
|
1358
|
+
return _as_int(_fold(a[0], env, 1), 1) if a else 1
|
|
1359
|
+
|
|
1360
|
+
def shaped(src, reps):
|
|
1361
|
+
begin, shape = _source_shape(src, var_info, env, reps)
|
|
1362
|
+
subdims = shape if shape else ([] if reps == 1 else [reps])
|
|
1363
|
+
units = _units_for_source(src, var_info, by_display)
|
|
1364
|
+
return begin, subdims, units
|
|
1365
|
+
|
|
1366
|
+
if kw == "SAMPLE":
|
|
1367
|
+
reps = reps_of()
|
|
1368
|
+
src = a[1] if len(a) > 1 else None
|
|
1369
|
+
tcode = type_of(2)
|
|
1370
|
+
begin, subdims, units = shaped(src, reps)
|
|
1371
|
+
info = _source_info(src, var_info)
|
|
1372
|
+
dim = reps
|
|
1373
|
+
if info is not None and info.stored_len is not None and tcode == 11:
|
|
1374
|
+
# String source (docs/tdf-rules.md §9a): element = stored_len bytes
|
|
1375
|
+
ln = info.stored_len
|
|
1376
|
+
dim = reps * ln
|
|
1377
|
+
subdims = info.full_shape()
|
|
1378
|
+
if isinstance(src, S.Index) and src.args:
|
|
1379
|
+
begin = (begin - 1) * ln + 1
|
|
1380
|
+
elif tcode == 11 and _is_anonymous_source(src, var_info, env):
|
|
1381
|
+
# a non-variable String source is as wide as its value (docs/tdf-rules.md §9c)
|
|
1382
|
+
dim = _string_stored_len(_anonymous_string_len(src, var_info, env))
|
|
1383
|
+
subdims = [dim]
|
|
1384
|
+
begin = ((begin - 1) * dim + 1) & 0xFFFFFFFF
|
|
1385
|
+
return [
|
|
1386
|
+
_OutField(
|
|
1387
|
+
_source_name(src, var_info),
|
|
1388
|
+
tcode,
|
|
1389
|
+
True,
|
|
1390
|
+
"Smp",
|
|
1391
|
+
begin,
|
|
1392
|
+
dim,
|
|
1393
|
+
subdims,
|
|
1394
|
+
units,
|
|
1395
|
+
placeholder=True,
|
|
1396
|
+
elem=dim,
|
|
1397
|
+
splittable=True,
|
|
1398
|
+
)
|
|
1399
|
+
]
|
|
1400
|
+
return [
|
|
1401
|
+
_OutField(
|
|
1402
|
+
_source_name(src, var_info),
|
|
1403
|
+
tcode,
|
|
1404
|
+
True,
|
|
1405
|
+
"Smp",
|
|
1406
|
+
begin,
|
|
1407
|
+
dim,
|
|
1408
|
+
subdims,
|
|
1409
|
+
units,
|
|
1410
|
+
placeholder=not isinstance(src, (S.Name, S.Index)) and info is None,
|
|
1411
|
+
)
|
|
1412
|
+
]
|
|
1413
|
+
|
|
1414
|
+
if kw in ("AVERAGE", "TOTALIZE", "STDDEV"):
|
|
1415
|
+
reps = reps_of()
|
|
1416
|
+
src = a[1] if len(a) > 1 else None
|
|
1417
|
+
tcode = type_of(2)
|
|
1418
|
+
begin, subdims, units = shaped(src, reps)
|
|
1419
|
+
suffix, tag = {"AVERAGE": ("_Avg", "Avg"), "TOTALIZE": ("_Tot", "Tot"), "STDDEV": ("_Std", "Std")}[kw]
|
|
1420
|
+
name = _source_name(src, var_info) + suffix
|
|
1421
|
+
return [_OutField(name, tcode, True, tag, begin, reps, subdims, units)]
|
|
1422
|
+
|
|
1423
|
+
if kw in ("MAXIMUM", "MINIMUM"):
|
|
1424
|
+
reps = reps_of()
|
|
1425
|
+
src = a[1] if len(a) > 1 else None
|
|
1426
|
+
tcode = type_of(2)
|
|
1427
|
+
begin, subdims, units = shaped(src, reps)
|
|
1428
|
+
time_flag = False
|
|
1429
|
+
if len(a) > 4:
|
|
1430
|
+
time_flag = str(getattr(a[4], "name", "")).upper() == "TRUE"
|
|
1431
|
+
base = _source_name(src, var_info)
|
|
1432
|
+
vsuf, vtag, tsuf, ttag = ("_Max", "Max", "_TMx", "TMx") if kw == "MAXIMUM" else ("_Min", "Min", "_TMn", "TMn")
|
|
1433
|
+
out = [_OutField(base + vsuf, tcode, True, vtag, begin, reps, subdims, units)]
|
|
1434
|
+
if time_flag:
|
|
1435
|
+
out.append(_OutField(base + tsuf, 14, True, ttag, 1, reps, [] if reps == 1 else [reps], units))
|
|
1436
|
+
return out
|
|
1437
|
+
|
|
1438
|
+
if kw == "MEDIAN":
|
|
1439
|
+
reps = reps_of()
|
|
1440
|
+
src = a[1] if len(a) > 1 else None
|
|
1441
|
+
tcode = type_of(3)
|
|
1442
|
+
begin, subdims, units = shaped(src, reps)
|
|
1443
|
+
return [_OutField(_source_name(src, var_info) + "_Med", tcode, True, "Med", begin, reps, subdims, units)]
|
|
1444
|
+
|
|
1445
|
+
if kw == "MOMENT":
|
|
1446
|
+
reps = reps_of()
|
|
1447
|
+
src = a[1] if len(a) > 1 else None
|
|
1448
|
+
tcode = type_of(3)
|
|
1449
|
+
begin, subdims, units = shaped(src, reps)
|
|
1450
|
+
return [_OutField(_source_name(src, var_info) + "_MMT", tcode, True, "MMT", begin, reps, subdims, units)]
|
|
1451
|
+
|
|
1452
|
+
if kw == "SAMPLEMAXMIN":
|
|
1453
|
+
reps = reps_of()
|
|
1454
|
+
src = a[1] if len(a) > 1 else None
|
|
1455
|
+
tcode = type_of(2)
|
|
1456
|
+
begin, subdims, units = shaped(src, reps)
|
|
1457
|
+
return [_OutField(_source_name(src, var_info) + "_SMM", tcode, True, "SMM", begin, reps, subdims, units)]
|
|
1458
|
+
|
|
1459
|
+
if kw == "COVARIANCE":
|
|
1460
|
+
src = a[1] if len(a) > 1 else None
|
|
1461
|
+
tcode = type_of(2)
|
|
1462
|
+
n_cov = _as_int(_fold(a[4], env, 1), 1) if len(a) > 4 else 1
|
|
1463
|
+
units = _units_for_source(src, var_info, by_display)
|
|
1464
|
+
return [
|
|
1465
|
+
_OutField(
|
|
1466
|
+
_source_name(src, var_info) + _idx_suffix(src, var_info, env) + "_Cov",
|
|
1467
|
+
tcode,
|
|
1468
|
+
True,
|
|
1469
|
+
"Cov",
|
|
1470
|
+
1,
|
|
1471
|
+
n_cov,
|
|
1472
|
+
[] if n_cov == 1 else [n_cov],
|
|
1473
|
+
units,
|
|
1474
|
+
)
|
|
1475
|
+
]
|
|
1476
|
+
|
|
1477
|
+
if kw == "WINDVECTOR":
|
|
1478
|
+
speed = a[1] if len(a) > 1 else None
|
|
1479
|
+
tcode = type_of(3)
|
|
1480
|
+
output_opt = _as_int(_fold(a[7], env, 0), 0) if len(a) > 7 else 0
|
|
1481
|
+
dim = {0: 3, 1: 2, 2: 4, 3: 1, 4: 2}.get(output_opt, 3)
|
|
1482
|
+
units = _units_for_source(speed, var_info, by_display)
|
|
1483
|
+
# per-element units by Output option (docs/tdf-rules.md §9a): speed units, then "Deg"
|
|
1484
|
+
ulist = {
|
|
1485
|
+
0: [units, "Deg", "Deg"],
|
|
1486
|
+
1: [units, "Deg"],
|
|
1487
|
+
2: [units, units, "Deg", "Deg"],
|
|
1488
|
+
3: ["Deg"],
|
|
1489
|
+
4: ["Deg", "Deg"],
|
|
1490
|
+
}.get(output_opt, [units] * dim)
|
|
1491
|
+
f = _OutField(
|
|
1492
|
+
_source_name(speed, var_info) + _idx_suffix(speed, var_info, env) + "_WVc",
|
|
1493
|
+
tcode,
|
|
1494
|
+
True,
|
|
1495
|
+
"WVc",
|
|
1496
|
+
1,
|
|
1497
|
+
dim,
|
|
1498
|
+
[] if dim == 1 else [dim],
|
|
1499
|
+
ulist[0],
|
|
1500
|
+
)
|
|
1501
|
+
f.unit_list = ulist
|
|
1502
|
+
return [f]
|
|
1503
|
+
|
|
1504
|
+
if kw == "HISTOGRAM":
|
|
1505
|
+
bin_select = a[0] if a else None
|
|
1506
|
+
tcode = type_of(1)
|
|
1507
|
+
bins = _as_int(_fold(a[3], env, 1), 1) if len(a) > 3 else 1
|
|
1508
|
+
wtval = _fold(a[5], env, 0.0) if len(a) > 5 else 0.0
|
|
1509
|
+
lolim = _fold(a[6], env, 0.0) if len(a) > 6 else 0.0
|
|
1510
|
+
uplim = _fold(a[7], env, 0.0) if len(a) > 7 else 0.0
|
|
1511
|
+
tag = f"Hst,{bins},{float(wtval):.4e},{float(lolim):.4e},{float(uplim):.4e}"
|
|
1512
|
+
units = _units_for_source(bin_select, var_info, by_display)
|
|
1513
|
+
return [
|
|
1514
|
+
_OutField(
|
|
1515
|
+
_source_name(bin_select, var_info) + _idx_suffix(bin_select, var_info, env) + "_Hst",
|
|
1516
|
+
tcode,
|
|
1517
|
+
True,
|
|
1518
|
+
tag,
|
|
1519
|
+
1,
|
|
1520
|
+
bins,
|
|
1521
|
+
[] if bins == 1 else [bins],
|
|
1522
|
+
units,
|
|
1523
|
+
)
|
|
1524
|
+
]
|
|
1525
|
+
|
|
1526
|
+
return []
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
def _apply_fieldnames(fields: list[_OutField], arg: str, profile_name: str = "") -> list[_OutField]:
|
|
1530
|
+
parts = arg.split(",")
|
|
1531
|
+
n_members = fields[0].struct_members if fields else 0
|
|
1532
|
+
if n_members and profile_name in ("cr300", "cr350"):
|
|
1533
|
+
return fields # `Fieldnames` after a structure source renames nothing there (oracle, 2026-10-12)
|
|
1534
|
+
if n_members:
|
|
1535
|
+
# structure source: name k prefixes every member of element k (`<name>_<member>`), the
|
|
1536
|
+
# instruction's own suffix (`_Avg`...) is dropped; unnamed elements keep default names
|
|
1537
|
+
raw = arg.split(",")
|
|
1538
|
+
for k, f in enumerate(fields):
|
|
1539
|
+
if k // n_members < len(raw):
|
|
1540
|
+
nm, _, desc = raw[k // n_members].partition(":")
|
|
1541
|
+
f.name = nm + "_" + f.member
|
|
1542
|
+
if desc:
|
|
1543
|
+
f.processing += ":" + desc
|
|
1544
|
+
f.renamed = True
|
|
1545
|
+
return fields
|
|
1546
|
+
if len(fields) == 1 and len(parts) == 1 and fields[0].dim == 1:
|
|
1547
|
+
name, _, desc = parts[0].partition(":")
|
|
1548
|
+
fields[0].name = name.strip()
|
|
1549
|
+
fields[0].renamed = True
|
|
1550
|
+
fields[0].placeholder = False
|
|
1551
|
+
fields[0].begin, fields[0].subdims = 1, []
|
|
1552
|
+
if desc:
|
|
1553
|
+
fields[0].processing += ":" + desc
|
|
1554
|
+
return fields
|
|
1555
|
+
if any(f.dim > 1 and f.splittable for f in fields) or (len(fields) == 1 and fields[0].dim > 1):
|
|
1556
|
+
# One name per repetition across the instruction's output fields in order (§9, exp #33/#37;
|
|
1557
|
+
# `Minimum(2,A,...,True)` + "M1,M2,T1,T2" names the Min then the TMn elements): each named
|
|
1558
|
+
# element is its own field and a field the names run out in keeps the remainder under the
|
|
1559
|
+
# original default name with the source array's full declared subdims (oracle-confirmed,
|
|
1560
|
+
# SbdPayload_tester.CR6: `Average(2,FinApp_Neutrons(),...) FieldNames("Neutrons")`).
|
|
1561
|
+
out = []
|
|
1562
|
+
names = list(parts)
|
|
1563
|
+
for base in fields:
|
|
1564
|
+
if not names:
|
|
1565
|
+
out.append(base)
|
|
1566
|
+
continue
|
|
1567
|
+
if not (base.dim > 1 and (base.splittable or len(fields) == 1)):
|
|
1568
|
+
name, _, desc = names.pop(0).partition(":")
|
|
1569
|
+
base.name = name.strip()
|
|
1570
|
+
base.renamed = True
|
|
1571
|
+
base.placeholder = False
|
|
1572
|
+
if desc:
|
|
1573
|
+
base.processing += ":" + desc
|
|
1574
|
+
out.append(base)
|
|
1575
|
+
continue
|
|
1576
|
+
es = base.elem if base.splittable else 1
|
|
1577
|
+
n_named = min(len(names), base.dim // es)
|
|
1578
|
+
for i in range(n_named):
|
|
1579
|
+
name, _, desc = names.pop(0).partition(":")
|
|
1580
|
+
proc = base.processing + (":" + desc if desc else "")
|
|
1581
|
+
rng = None if base.rng is None else (base.rng[0] + i * es, base.rng[0] + (i + 1) * es - 1)
|
|
1582
|
+
u = base.unit_list[i] if base.unit_list and i < len(base.unit_list) else base.units
|
|
1583
|
+
out.append(
|
|
1584
|
+
_OutField(
|
|
1585
|
+
name.strip(),
|
|
1586
|
+
base.type_code,
|
|
1587
|
+
base.read_only,
|
|
1588
|
+
proc,
|
|
1589
|
+
1,
|
|
1590
|
+
es,
|
|
1591
|
+
[es] if es > 1 else [],
|
|
1592
|
+
u,
|
|
1593
|
+
renamed=True,
|
|
1594
|
+
name_only=base.name_only,
|
|
1595
|
+
placeholder=False,
|
|
1596
|
+
rng=rng,
|
|
1597
|
+
)
|
|
1598
|
+
)
|
|
1599
|
+
remaining = base.dim - n_named * es
|
|
1600
|
+
if remaining > 0:
|
|
1601
|
+
rng = None if base.rng is None else (base.rng[0] + n_named * es, base.rng[1])
|
|
1602
|
+
out.append(
|
|
1603
|
+
_OutField(
|
|
1604
|
+
base.name,
|
|
1605
|
+
base.type_code,
|
|
1606
|
+
base.read_only,
|
|
1607
|
+
base.processing,
|
|
1608
|
+
base.begin + n_named * es,
|
|
1609
|
+
remaining,
|
|
1610
|
+
base.subdims,
|
|
1611
|
+
base.units,
|
|
1612
|
+
name_only=base.name_only,
|
|
1613
|
+
placeholder=base.placeholder,
|
|
1614
|
+
rng=rng,
|
|
1615
|
+
splittable=base.splittable,
|
|
1616
|
+
elem=base.elem,
|
|
1617
|
+
)
|
|
1618
|
+
)
|
|
1619
|
+
return out
|
|
1620
|
+
for f, part in zip(fields, parts):
|
|
1621
|
+
name, _, desc = part.partition(":")
|
|
1622
|
+
f.name = name.strip()
|
|
1623
|
+
f.renamed = True
|
|
1624
|
+
f.placeholder = False
|
|
1625
|
+
if desc:
|
|
1626
|
+
f.processing += ":" + desc
|
|
1627
|
+
return fields
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
# ---------------------------------------------------------------- user DataTable header (§5)
|
|
1631
|
+
|
|
1632
|
+
_TIME_KEYWORDS = {"DATAINTERVAL", "DATAEVENT"}
|
|
1633
|
+
_OUTPUT_KEYWORDS = {
|
|
1634
|
+
"SAMPLE",
|
|
1635
|
+
"AVERAGE",
|
|
1636
|
+
"MAXIMUM",
|
|
1637
|
+
"MINIMUM",
|
|
1638
|
+
"TOTALIZE",
|
|
1639
|
+
"STDDEV",
|
|
1640
|
+
"MEDIAN",
|
|
1641
|
+
"MOMENT",
|
|
1642
|
+
"SAMPLEMAXMIN",
|
|
1643
|
+
"HISTOGRAM",
|
|
1644
|
+
"WINDVECTOR",
|
|
1645
|
+
"COVARIANCE",
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
|
|
1649
|
+
_UNIT_MON = 6 # predefined `MON`; a DataInterval unit only (`Scan` has no month)
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
def _interval_from_args(args: list, env: consteval.Env, into_idx: int, val_idx: int, unit_idx: int):
|
|
1653
|
+
"""(TintoInt, Interval) as (sec, nsec) TDF pairs, the Interval value, and its unit name for the
|
|
1654
|
+
`-l` line. The units argument reads by value (`_unit_value`): 0..5 are the named units, 6 is MON
|
|
1655
|
+
(both TDF pairs (0,0) and TintoInt dropped, oracle round 20 W4), and any other value is invalid
|
|
1656
|
+
(Interval 1 Sec, TintoInt dropped; oracle round 20 W4). A non-constant units argument reads as
|
|
1657
|
+
seconds."""
|
|
1658
|
+
unit_val = _unit_value(args[unit_idx], env) if len(args) > unit_idx else None
|
|
1659
|
+
into_val = _fold(args[into_idx], env, 0) if len(args) > into_idx else 0
|
|
1660
|
+
interval_val = _fold(args[val_idx], env, 0) if len(args) > val_idx else 0
|
|
1661
|
+
# A String `TintoInt`/`Interval` (`DataInterval("x",15,Sec,10)`) compiles on CR300/CR350
|
|
1662
|
+
# (oracle, 2026-10-12): it is taken as 0 here rather than crashing in `float()`.
|
|
1663
|
+
into_val = into_val if consteval.is_finite_number(into_val) else 0
|
|
1664
|
+
interval_val = interval_val if consteval.is_finite_number(interval_val) else 0
|
|
1665
|
+
if unit_val == _UNIT_MON:
|
|
1666
|
+
return (0, 0), (0, 0), float(interval_val or 0), "MON"
|
|
1667
|
+
if unit_val is not None and unit_val not in _UNIT_NAME_BY_VALUE:
|
|
1668
|
+
into_val, interval_val, unit_val = 0, 1, 2
|
|
1669
|
+
unit_name = "SEC" if unit_val is None else _UNIT_NAME_BY_VALUE.get(unit_val, "SEC")
|
|
1670
|
+
ns_per_unit = _UNIT_NS[unit_name]
|
|
1671
|
+
into_ns = int(round(float(into_val or 0) * ns_per_unit))
|
|
1672
|
+
interval_ns = int(round(float(interval_val or 0) * ns_per_unit))
|
|
1673
|
+
into = (into_ns // 1_000_000_000, into_ns % 1_000_000_000)
|
|
1674
|
+
interval = (interval_ns // 1_000_000_000, interval_ns % 1_000_000_000)
|
|
1675
|
+
return into, interval, float(interval_val or 0), unit_name
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _build_data_table(
|
|
1679
|
+
dt: DataTableDecl,
|
|
1680
|
+
env: consteval.Env,
|
|
1681
|
+
var_info: dict,
|
|
1682
|
+
by_display: dict,
|
|
1683
|
+
called: set[str],
|
|
1684
|
+
by_base: dict | None = None,
|
|
1685
|
+
anon: list[int] | None = None,
|
|
1686
|
+
) -> tuple[TdfTable, dict]:
|
|
1687
|
+
"""Returns (TdfTable, meta) where meta carries auto-sizing inputs (bytes_per_record,
|
|
1688
|
+
interval_seconds, has_interval, has_card) for tables whose declared Size is -1.
|
|
1689
|
+
"""
|
|
1690
|
+
into = (0, 0)
|
|
1691
|
+
interval = (0, 0)
|
|
1692
|
+
interval_seconds = None
|
|
1693
|
+
display_interval = None # (value, unit_text) for the -l "Interval:" line
|
|
1694
|
+
zero_interval = False
|
|
1695
|
+
has_card = False
|
|
1696
|
+
lapses = 10
|
|
1697
|
+
own_weight = None # the DataInterval's own interval in seconds, kept even when the header is cleared
|
|
1698
|
+
|
|
1699
|
+
out_fields: list[_OutField] = []
|
|
1700
|
+
pending: list[_OutField] = []
|
|
1701
|
+
pending_src = None # Source of `pending` when it is eligible for alias splitting (after FieldNames)
|
|
1702
|
+
|
|
1703
|
+
def split_pending():
|
|
1704
|
+
if pending_src is not None and by_base:
|
|
1705
|
+
return _split_by_alias(pending, pending_src, var_info, by_base, env)
|
|
1706
|
+
return pending
|
|
1707
|
+
|
|
1708
|
+
for s in dt.body:
|
|
1709
|
+
kw = _kw(s)
|
|
1710
|
+
if isinstance(s, S.Instruction) and (kw == "DATAINTERVAL"):
|
|
1711
|
+
into, interval, ival, unit = _interval_from_args(s.args, env, 0, 1, 2)
|
|
1712
|
+
interval_seconds = ival * (_UNIT_SECONDS.get(unit, 1))
|
|
1713
|
+
display_interval = (ival, unit)
|
|
1714
|
+
zero_interval = interval_seconds == 0
|
|
1715
|
+
if unit == "MON":
|
|
1716
|
+
# no `-l` line for a zero MON interval, no enclosing-Scan weight, and no interval
|
|
1717
|
+
# for the auto-size arithmetic (oracle round 20 W5: Size -1 gives 232674 records for
|
|
1718
|
+
# 1, 2, 15 and 100 MON, the size of a table with no interval)
|
|
1719
|
+
zero_interval = False
|
|
1720
|
+
display_interval = (ival, unit) if ival else None
|
|
1721
|
+
interval_seconds = None
|
|
1722
|
+
if zero_interval:
|
|
1723
|
+
interval_seconds = None
|
|
1724
|
+
own_weight = interval_seconds
|
|
1725
|
+
if len(s.args) > 3:
|
|
1726
|
+
lapses = abs(_as_int(_fold(s.args[3], env, 10), 10))
|
|
1727
|
+
if len(s.args) > 3 and _fold(s.args[3], env, None) == 0:
|
|
1728
|
+
# Lapses = 0: TDF header and sizing as for a table with no DataInterval; only the
|
|
1729
|
+
# `-l` "Interval:" line remains (docs/tdf-rules.md §5)
|
|
1730
|
+
into, interval, interval_seconds = (0, 0), (0, 0), None
|
|
1731
|
+
elif kw == "DATAEVENT":
|
|
1732
|
+
pass # interval/into stay (0,0) (§5, oracle-confirmed)
|
|
1733
|
+
elif kw in ("CARDOUT", "TABLEFILE"):
|
|
1734
|
+
has_card = True
|
|
1735
|
+
elif isinstance(s, S.Instruction) and (kw == "FIELDNAMES"):
|
|
1736
|
+
if s.args and isinstance(s.args[0], S.String) and pending:
|
|
1737
|
+
pending = _apply_fieldnames(pending, s.args[0].value, env.profile.name)
|
|
1738
|
+
out_fields.extend(split_pending())
|
|
1739
|
+
pending, pending_src = [], None
|
|
1740
|
+
elif kw in _OUTPUT_KEYWORDS and isinstance(s, S.Instruction):
|
|
1741
|
+
if pending:
|
|
1742
|
+
out_fields.extend(split_pending())
|
|
1743
|
+
s = _alias_source(s, var_info, by_display, by_base)
|
|
1744
|
+
pending = _emit_output_fields(s, var_info, by_display, env, by_base, anon)
|
|
1745
|
+
pending_src = s.args[1] if len(s.args) > 1 and not any(f.struct_members for f in pending) else None
|
|
1746
|
+
elif isinstance(s, S.Instruction) and (kw == "FFT"):
|
|
1747
|
+
if pending:
|
|
1748
|
+
out_fields.extend(split_pending())
|
|
1749
|
+
pending, pending_src = [_fft_field(s, var_info, by_display, env, anon)], None
|
|
1750
|
+
elif kw in ("ETSZ", "ETCLEARSKY"):
|
|
1751
|
+
if pending:
|
|
1752
|
+
out_fields.extend(split_pending())
|
|
1753
|
+
pending, pending_src = (
|
|
1754
|
+
[_OutField(n, 7, True, n, 1, 1, [], name_only=True, rng=(1, _FFT_UNBOUNDED)) for n in ("ETrs", "Rso")],
|
|
1755
|
+
None,
|
|
1756
|
+
) # name-only: the TDF fields are not derived
|
|
1757
|
+
if pending:
|
|
1758
|
+
out_fields.extend(split_pending())
|
|
1759
|
+
|
|
1760
|
+
fields = []
|
|
1761
|
+
for of in out_fields:
|
|
1762
|
+
if of.name_only:
|
|
1763
|
+
continue
|
|
1764
|
+
fields.append(
|
|
1765
|
+
TdfField(
|
|
1766
|
+
of.type_code,
|
|
1767
|
+
of.read_only,
|
|
1768
|
+
of.name,
|
|
1769
|
+
[],
|
|
1770
|
+
of.processing,
|
|
1771
|
+
of.units,
|
|
1772
|
+
of.processing,
|
|
1773
|
+
of.begin,
|
|
1774
|
+
of.dim,
|
|
1775
|
+
of.subdims,
|
|
1776
|
+
)
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
size_raw = _fold(dt.size_expr, env, -1)
|
|
1780
|
+
|
|
1781
|
+
bytes_per_record = sum(_field_record_bytes(of) for of in out_fields if not of.name_only)
|
|
1782
|
+
size_val = declared_size(size_raw, env.profile.name in ("cr300", "cr350"), bytes_per_record)
|
|
1783
|
+
size_val = -1 if size_val is None else size_val
|
|
1784
|
+
|
|
1785
|
+
if interval_seconds is not None and 2 * bytes_per_record > _DEFAULT_PROBE_BUFFER_BYTES - _DEFAULT_PROBE_OVERHEAD:
|
|
1786
|
+
# fewer than 2 records fit the 1008-byte probe buffer: sized and stamped like a no-interval table
|
|
1787
|
+
into, interval, interval_seconds = (0, 0), (0, 0), None
|
|
1788
|
+
|
|
1789
|
+
meta = dict(
|
|
1790
|
+
own_weight=own_weight,
|
|
1791
|
+
lapses=lapses,
|
|
1792
|
+
zero_interval=zero_interval,
|
|
1793
|
+
size_val=size_val,
|
|
1794
|
+
size_raw=size_raw,
|
|
1795
|
+
bytes_per_record=bytes_per_record,
|
|
1796
|
+
has_event=any(_kw(s) == "DATAEVENT" for s in dt.body),
|
|
1797
|
+
fillstop=any(_kw(s) == "FILLSTOP" for s in dt.body),
|
|
1798
|
+
interval_seconds=interval_seconds,
|
|
1799
|
+
has_card=has_card,
|
|
1800
|
+
display_interval=display_interval,
|
|
1801
|
+
called=dt.name.upper() in called,
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
table = TdfTable(dt.name, 1, 14, into, interval, fields)
|
|
1805
|
+
table._dup_fields = out_fields # type: ignore[attr-defined]
|
|
1806
|
+
if display_interval is not None:
|
|
1807
|
+
table._interval_display = display_interval # type: ignore[attr-defined]
|
|
1808
|
+
return table, meta
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
def _field_record_bytes(of: _OutField) -> int:
|
|
1812
|
+
"""Per-record byte cost of one output field: for ASCII (type 11) `dim` already IS the byte
|
|
1813
|
+
length; for numeric types it is `dim` (element count) times the type's own byte width."""
|
|
1814
|
+
if of.type_code == 11:
|
|
1815
|
+
return of.dim
|
|
1816
|
+
return of.dim * _TYPE_BYTES.get(of.type_code, 4)
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
# ---------------------------------------------------------------- auto-size (§10)
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _table_default_min(bytes_per_record: int) -> int:
|
|
1823
|
+
"""`floor((1024-16)/bytes)`: a table's own default record count (probe buffer)."""
|
|
1824
|
+
if bytes_per_record <= 0:
|
|
1825
|
+
return 1
|
|
1826
|
+
return max((_DEFAULT_PROBE_BUFFER_BYTES - _DEFAULT_PROBE_OVERHEAD) // bytes_per_record, 1)
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def _resolve_auto_sizes(tables_meta: list[tuple[TdfTable, dict]], scan_weights: dict[str, float]):
|
|
1830
|
+
autos = [(t, m) for t, m in tables_meta if m["size_val"] == -1 and m["called"]]
|
|
1831
|
+
if not autos:
|
|
1832
|
+
return
|
|
1833
|
+
reserved = _TABLE_INIT_RESERVED
|
|
1834
|
+
rate = 0.0
|
|
1835
|
+
for t, m in autos:
|
|
1836
|
+
# weight: a DataInterval table's own interval; a DataEvent/no-interval table's enclosing
|
|
1837
|
+
# Scan/SlowSequence interval (§10 "scan-interval weighting"), 1 s if none was found.
|
|
1838
|
+
w = _f32(m["own_weight"] or scan_weights.get(t.name.upper(), 1.0))
|
|
1839
|
+
b = m["bytes_per_record"]
|
|
1840
|
+
lapses = m["lapses"] if m["interval_seconds"] else 0
|
|
1841
|
+
dflt = _table_default_min(b) if lapses else 1
|
|
1842
|
+
if dflt < 2:
|
|
1843
|
+
dflt, lapses = 1, 0
|
|
1844
|
+
t8 = dflt * b + _DEFAULT_PROBE_OVERHEAD
|
|
1845
|
+
m.update(_w=w, _dflt=dflt, _t8=t8, _lapses=lapses)
|
|
1846
|
+
m["_reserve"] = (
|
|
1847
|
+
(2 * _DEFAULT_PROBE_BUFFER_BYTES) // t8
|
|
1848
|
+
+ (1 if dflt == 1 else 0)
|
|
1849
|
+
+ (1 if t8 < _DEFAULT_PROBE_BUFFER_BYTES else 3)
|
|
1850
|
+
) * t8
|
|
1851
|
+
m["_rate"] = _f32(_f32(t8) / _f32(_f32(dflt) * w))
|
|
1852
|
+
# a CardOut/TableFile table (nonzero card size) skips the Lapses-buffer term
|
|
1853
|
+
reserved += m["_reserve"]
|
|
1854
|
+
if lapses and b and not m["has_card"]:
|
|
1855
|
+
if (
|
|
1856
|
+
0 < lapses < 1 << 31
|
|
1857
|
+
): # `Lapses * 16` wraps to 32 bits before the division (CR6/CR1000X `Out of memory.`)
|
|
1858
|
+
w16 = (lapses * 16 + (1 << 31)) % (1 << 32) - (1 << 31)
|
|
1859
|
+
reserved += (int(w16 / b) + 1) * b
|
|
1860
|
+
else:
|
|
1861
|
+
reserved += ((lapses * 16) // b + 1) * b
|
|
1862
|
+
rate = _f32(m["_rate"] + rate)
|
|
1863
|
+
duration = _f32(_f32(_FREE_MEMORY - reserved) / rate) if rate else float("inf")
|
|
1864
|
+
if rate:
|
|
1865
|
+
# tables whose interval is not shorter than the duration get 2 records and leave the pool
|
|
1866
|
+
for _t, m in autos:
|
|
1867
|
+
if duration > 0 and duration <= m["_w"]:
|
|
1868
|
+
m["_forced"] = True
|
|
1869
|
+
rate = _f32(rate - m["_rate"])
|
|
1870
|
+
reserved -= m["_reserve"]
|
|
1871
|
+
duration = _f32(_f32(_FREE_MEMORY - reserved) / rate) if rate else float("inf")
|
|
1872
|
+
for t, m in autos:
|
|
1873
|
+
if m.get("_forced"):
|
|
1874
|
+
t.size = 2
|
|
1875
|
+
continue
|
|
1876
|
+
records = int(_f32(duration / m["_w"])) if m["bytes_per_record"] else 0
|
|
1877
|
+
m["_n_raw"] = records # before the minimum: the count `checker/storage.cr6_table_oom` charges
|
|
1878
|
+
t.size = max(records, 2, m["_dflt"])
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _apply_sizes(tables_meta: list[tuple[TdfTable, dict]], scan_weights: dict[str, float], resolve_auto: bool = True):
|
|
1882
|
+
for t, m in tables_meta:
|
|
1883
|
+
w = scan_weights.get(t.name.upper())
|
|
1884
|
+
if (
|
|
1885
|
+
m["zero_interval"]
|
|
1886
|
+
and m["called"]
|
|
1887
|
+
and w
|
|
1888
|
+
and 2 * m["bytes_per_record"] <= _DEFAULT_PROBE_BUFFER_BYTES - _DEFAULT_PROBE_OVERHEAD
|
|
1889
|
+
):
|
|
1890
|
+
# DataInterval(0,0,...): the interval is the enclosing Scan's (docs/tdf-rules.md §5)
|
|
1891
|
+
ns = round(w * 1_000_000_000)
|
|
1892
|
+
t.interval = (ns // 1_000_000_000, ns % 1_000_000_000)
|
|
1893
|
+
m["interval_seconds"] = w
|
|
1894
|
+
unit = m["display_interval"][1]
|
|
1895
|
+
t._interval_display = (w / _UNIT_SECONDS.get(unit, 1), unit) # type: ignore[attr-defined]
|
|
1896
|
+
if not m["called"]:
|
|
1897
|
+
# A table with no `CallTable` anywhere in the compiled program never gets its
|
|
1898
|
+
# DataInterval/DataEvent finalised either (oracle-confirmed, docs/tdf-rules.md §13):
|
|
1899
|
+
# `size=1` *and* `interval`/`into` reset to (0,0), as if declared with neither.
|
|
1900
|
+
t.size = 1
|
|
1901
|
+
t.interval = (0, 0)
|
|
1902
|
+
t.time_into = (0, 0)
|
|
1903
|
+
elif m["size_val"] != -1:
|
|
1904
|
+
t.size = m["size_val"]
|
|
1905
|
+
if resolve_auto:
|
|
1906
|
+
_resolve_auto_sizes(tables_meta, scan_weights)
|
|
1907
|
+
|
|
1908
|
+
|
|
1909
|
+
# ---------------------------------------------------------------- top-level
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
def _register_table_sources(table: TdfTable, var_info: dict[str, VarInfo]) -> None:
|
|
1913
|
+
"""Descriptors for `Table.Field` sources (§9b/§9c), without inheriting table-field units."""
|
|
1914
|
+
for f in table.fields:
|
|
1915
|
+
shape = list(f.subdims)
|
|
1916
|
+
stored_len = None
|
|
1917
|
+
if f.type == 11:
|
|
1918
|
+
stored_len = shape.pop() if shape else f.dimension
|
|
1919
|
+
elif not shape and f.dimension > 1:
|
|
1920
|
+
shape = [f.dimension]
|
|
1921
|
+
var_info[f"{table.name}.{f.name}".upper()] = VarInfo(f.type, shape, stored_len, name=f.name)
|
|
1922
|
+
|
|
1923
|
+
|
|
1924
|
+
def build_tables(program: S.Program, profile, consts: dict):
|
|
1925
|
+
env = consteval.Env(profile, dict(consts))
|
|
1926
|
+
scanned = _scan_program(program)
|
|
1927
|
+
scan_weights = _scan_weights(program.statements, env)
|
|
1928
|
+
scan_calls = _scan_calls(program.statements, env)
|
|
1929
|
+
|
|
1930
|
+
var_info: dict[str, VarInfo] = {}
|
|
1931
|
+
for v in scanned["all_vars"]:
|
|
1932
|
+
var_info[v.name.upper()] = _make_var_info(v, env)
|
|
1933
|
+
var_info[v.name.upper()].struct = scanned["struct_types"].get((v.type or "").upper())
|
|
1934
|
+
|
|
1935
|
+
# Build the Public table first (source-variable units lookups for output fields need its
|
|
1936
|
+
# display-name map), but keep its position in the TDF (after DataTableInfo/ConstTables, §2).
|
|
1937
|
+
public_table, by_display, by_base = _build_public_table(
|
|
1938
|
+
scanned["publics"], var_info, scanned["alias_stmts"], scanned["units_stmts"], scanned["readonly_names"]
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
tables: list[TdfTable] = [_status_table(scanned["slow_seq_count"])]
|
|
1942
|
+
struct_tables = _build_struct_tables(scanned["all_vars"], scanned["struct_types"], env)
|
|
1943
|
+
tables.extend(struct_tables)
|
|
1944
|
+
if scanned["uses_ch400"]:
|
|
1945
|
+
struct_tables = struct_tables + [_ch400status_table()]
|
|
1946
|
+
tables.append(struct_tables[-1])
|
|
1947
|
+
for table in tables:
|
|
1948
|
+
_register_table_sources(table, var_info)
|
|
1949
|
+
|
|
1950
|
+
data_table_metas = []
|
|
1951
|
+
anon = [0] # program-wide `Anonymous<n>` counter (docs/tdf-rules.md §9c)
|
|
1952
|
+
for dt in scanned["data_tables"]:
|
|
1953
|
+
table, meta = _build_data_table(dt, env, var_info, by_display, scanned["called"], by_base, anon)
|
|
1954
|
+
tables.append(table)
|
|
1955
|
+
data_table_metas.append((table, meta))
|
|
1956
|
+
_register_table_sources(table, var_info)
|
|
1957
|
+
|
|
1958
|
+
if scanned["data_tables"] or struct_tables:
|
|
1959
|
+
tables.append(_datatableinfo_table(len(scanned["data_tables"])))
|
|
1960
|
+
|
|
1961
|
+
if scanned["uses_cpi"]:
|
|
1962
|
+
tables.append(_cpistatus_table())
|
|
1963
|
+
|
|
1964
|
+
tables.extend(_build_const_tables(scanned["const_tables"], env))
|
|
1965
|
+
|
|
1966
|
+
if public_table is not None:
|
|
1967
|
+
tables.append(public_table)
|
|
1968
|
+
|
|
1969
|
+
cr3xx = profile.name in ("cr300", "cr350")
|
|
1970
|
+
if cr3xx:
|
|
1971
|
+
last_call = _last_call_interval(program.statements, env)
|
|
1972
|
+
for table, meta in data_table_metas:
|
|
1973
|
+
meta["call_w"] = meta["own_weight"] or last_call.get(table.name.upper())
|
|
1974
|
+
_apply_sizes(data_table_metas, scan_weights, resolve_auto=not cr3xx)
|
|
1975
|
+
alloc = None
|
|
1976
|
+
if cr3xx and data_table_metas:
|
|
1977
|
+
from .checker import storage
|
|
1978
|
+
|
|
1979
|
+
pool = storage.pool_bytes(
|
|
1980
|
+
profile.name,
|
|
1981
|
+
len(data_table_metas),
|
|
1982
|
+
bool(scanned["publics"]),
|
|
1983
|
+
sum(1 for c in scanned["const_tables"] if c.name),
|
|
1984
|
+
) - _preserve_deduction(program, env, scanned, var_info)
|
|
1985
|
+
metas = [m for _, m in data_table_metas]
|
|
1986
|
+
alloc = storage.run(metas, pool)
|
|
1987
|
+
if alloc:
|
|
1988
|
+
for (table, m), n in zip(data_table_metas, alloc["sizes"]):
|
|
1989
|
+
if m["called"] and (m["size_val"] == -1 or n > table.size):
|
|
1990
|
+
table.size = n # a fixed size is raised to the record block `k` (Sub-called)
|
|
1991
|
+
else:
|
|
1992
|
+
for table, m in data_table_metas:
|
|
1993
|
+
if m["called"] and m["size_val"] == -1:
|
|
1994
|
+
table.size = 1
|
|
1995
|
+
for table, meta in data_table_metas:
|
|
1996
|
+
if alloc:
|
|
1997
|
+
meta["alloc"] = alloc
|
|
1998
|
+
table._storage = dict(meta, scans=scan_calls.get(table.name.upper(), [])) # type: ignore[attr-defined]
|
|
1999
|
+
|
|
2000
|
+
table_list_text = render_table_list(tables)
|
|
2001
|
+
return tables, table_list_text, None
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
def _preserve_deduction(program: S.Program, env, scanned, var_info) -> int:
|
|
2005
|
+
"""CR300/CR350 pool bytes taken before the tables by `PreserveVariables` and
|
|
2006
|
+
`PreserveOneVariable` (docs/tdf-rules.md "Preserved variables"; `checker/storage.py`)."""
|
|
2007
|
+
from .checker import storage
|
|
2008
|
+
|
|
2009
|
+
stmts = program.statements
|
|
2010
|
+
kws = [_kw(s) for s in stmts]
|
|
2011
|
+
if "PRESERVEVARIABLES" not in kws and "PRESERVEONEVARIABLE" not in kws:
|
|
2012
|
+
return 0
|
|
2013
|
+
ticks = None # smallest main-sequence Scan; SlowSequence scans only lift the 60 s cap
|
|
2014
|
+
slow = False
|
|
2015
|
+
for s in stmts:
|
|
2016
|
+
if _kw(s) == "SLOWSEQUENCE":
|
|
2017
|
+
slow = True
|
|
2018
|
+
elif isinstance(s, S.Instruction) and (_kw(s) == "SCAN"):
|
|
2019
|
+
if slow:
|
|
2020
|
+
ticks = 600000 if ticks is None else ticks
|
|
2021
|
+
elif s.args:
|
|
2022
|
+
val = _fold(s.args[0], env, None)
|
|
2023
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool) and val > 0:
|
|
2024
|
+
unit = _scan_unit_name(s.args[1], env) if len(s.args) > 1 else "SEC"
|
|
2025
|
+
t = int(val * _UNIT_NS.get(unit, _UNIT_NS["SEC"]) // 100_000)
|
|
2026
|
+
ticks = t if ticks is None else min(ticks, t)
|
|
2027
|
+
ticks = 100000 if ticks is None else min(ticks, 600000)
|
|
2028
|
+
|
|
2029
|
+
def size(v: S.VarDecl) -> tuple[int, int]:
|
|
2030
|
+
"""(element width, element count) of a declaration; a `Double` is 8 bytes."""
|
|
2031
|
+
info = _make_var_info(v, env)
|
|
2032
|
+
return info.stored_len or (8 if (v.type or "").upper() == "DOUBLE" else 4), math.prod(info.shape)
|
|
2033
|
+
|
|
2034
|
+
total = 0
|
|
2035
|
+
if "PRESERVEVARIABLES" in kws:
|
|
2036
|
+
pub = 16 # every Public, Dim and Sub-level Dim, plus each StructureType member once
|
|
2037
|
+
decls = [v for v in scanned["all_vars"] if (v.type or "").upper() not in scanned["struct_types"]]
|
|
2038
|
+
decls += [m for st in scanned["struct_types"].values() for m in st["members"]]
|
|
2039
|
+
pub += sum(w * n for w, n in map(size, decls))
|
|
2040
|
+
total += storage.preserve_charge(64, 60, 0, 100000, ticks, pub)
|
|
2041
|
+
by_name = {v.name.upper(): v for v in scanned["all_vars"]}
|
|
2042
|
+
payloads = []
|
|
2043
|
+
for s in stmts:
|
|
2044
|
+
if _kw(s) != "PRESERVEONEVARIABLE" or not isinstance(s, S.Instruction) or not s.args:
|
|
2045
|
+
continue
|
|
2046
|
+
a = s.args[0]
|
|
2047
|
+
v = by_name.get(getattr(a, "name", "").upper())
|
|
2048
|
+
if v is None:
|
|
2049
|
+
continue
|
|
2050
|
+
w, n = size(v)
|
|
2051
|
+
whole = not isinstance(a, S.Index) or not any(x is not None for x in a.args) # `V()` is the whole array
|
|
2052
|
+
payloads.append(w * n if whole else w)
|
|
2053
|
+
if payloads:
|
|
2054
|
+
top = max(payloads)
|
|
2055
|
+
total += storage.preserve_charge(top + 78, top, len(payloads), 20000, ticks, 0)
|
|
2056
|
+
return total
|
|
2057
|
+
|
|
2058
|
+
|
|
2059
|
+
# ---------------------------------------------------------------- `-l` table list (docs/cli.md)
|
|
2060
|
+
|
|
2061
|
+
# Defensive cap on how many index tuples a single field's `-l` listing will ever enumerate.
|
|
2062
|
+
# `checker/structure.py`'s array-dimension check (`docs/checker.md` "Array dimension limits")
|
|
2063
|
+
# rejects the specific fuzz-found case that made this hang outright (a `Public`/`Dim` dimension
|
|
2064
|
+
# above `INT32_MAX`, `docs/fuzz-findings.md` "Hang"), so `build_tables`/`tables_hook.attach` is
|
|
2065
|
+
# only ever reached for a declaration this checker considers legal -- but that still allows a
|
|
2066
|
+
# dimension anywhere up to `INT32_MAX` through to here, and the real compiler itself rejects most
|
|
2067
|
+
# of that range with `Out of memory.` (`checker/storage.py`, not this module). This is therefore pure robustness, not a claim about a
|
|
2068
|
+
# verified real-compiler limit: `_MAX_EXPLODE_COUNT` is comfortably above the largest single-field
|
|
2069
|
+
# element count this session could get the real compiler to actually accept (2,000,000, oracle-
|
|
2070
|
+
# confirmed), so no known-exact `-l` output is affected, and it keeps this generator from ever
|
|
2071
|
+
# building tens or hundreds of millions of index tuples for a declaration this checker did not
|
|
2072
|
+
# (or, for some other field this module derives a huge `dimension` for, cannot) reject.
|
|
2073
|
+
_MAX_EXPLODE_COUNT = 2_000_000
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def _explode_indices(subdims: list[int], drop_last: bool, begin: int = 1, dim: int = 1):
|
|
2077
|
+
"""Row-major (last dim fastest) index tuples for the `dim` stored elements starting at the
|
|
2078
|
+
1-based linear position `begin` within `subdims` (after dropping the trailing string-storage
|
|
2079
|
+
dim for ASCII fields, per §3/§8's alias-prefix/suffix fields and §3's Reps<array-size case:
|
|
2080
|
+
both need the *slice*, not every valid coordinate of the full declared shape). Bounded by
|
|
2081
|
+
`_MAX_EXPLODE_COUNT` regardless of how large `dim`/`subdims` claim to be; see that constant."""
|
|
2082
|
+
dims = subdims[:-1] if drop_last and subdims else subdims
|
|
2083
|
+
if drop_last and subdims and dim % subdims[-1] == 0 and begin % subdims[-1] == 1 % subdims[-1]:
|
|
2084
|
+
# string field: begin/dim are in bytes (§9a), convert to elements
|
|
2085
|
+
begin, dim = (begin - 1) // subdims[-1] + 1, dim // subdims[-1]
|
|
2086
|
+
if not dims:
|
|
2087
|
+
yield ()
|
|
2088
|
+
return
|
|
2089
|
+
total = 1
|
|
2090
|
+
for d in dims:
|
|
2091
|
+
total *= d
|
|
2092
|
+
strides = [1] * len(dims)
|
|
2093
|
+
acc = 1
|
|
2094
|
+
for i in range(len(dims) - 1, -1, -1):
|
|
2095
|
+
strides[i] = acc
|
|
2096
|
+
acc *= dims[i]
|
|
2097
|
+
start = max(min(begin - 1, total - 1), 0)
|
|
2098
|
+
if len(dims) == 1 and begin + dim - 1 > total and not drop_last:
|
|
2099
|
+
# a rank-1 field longer than its subdim (FFT Option mod 10 = 6..9: dim 2*(N/2+1) over
|
|
2100
|
+
# subdims [N/2+1]) lists its index running on past the subdim (docs/tdf-rules.md §9b)
|
|
2101
|
+
start, count = max(begin - 1, 0), min(dim, _MAX_EXPLODE_COUNT)
|
|
2102
|
+
for lin in range(start, start + count):
|
|
2103
|
+
yield (lin + 1,)
|
|
2104
|
+
return
|
|
2105
|
+
count = max(min(dim, total - start), 0)
|
|
2106
|
+
count = min(count, _MAX_EXPLODE_COUNT)
|
|
2107
|
+
for lin in range(start, start + count):
|
|
2108
|
+
coord = []
|
|
2109
|
+
rem = lin
|
|
2110
|
+
for _d, stride in zip(dims, strides):
|
|
2111
|
+
coord.append(rem // stride + 1)
|
|
2112
|
+
rem %= stride
|
|
2113
|
+
yield tuple(coord)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def render_table_list(tables: list[TdfTable]) -> str:
|
|
2117
|
+
lines: list[str] = []
|
|
2118
|
+
for t in tables:
|
|
2119
|
+
if t.name == "Status":
|
|
2120
|
+
continue
|
|
2121
|
+
lines.append("-" * 52)
|
|
2122
|
+
lines.append(f"Table: {t.name}")
|
|
2123
|
+
disp = getattr(t, "_interval_display", None)
|
|
2124
|
+
if disp is not None:
|
|
2125
|
+
value, unit = disp
|
|
2126
|
+
value = int(value) if float(value).is_integer() else value
|
|
2127
|
+
lines.append(f"Interval: {value} {unit}")
|
|
2128
|
+
elif t.interval != (0, 0):
|
|
2129
|
+
# Fallback when the original declared unit wasn't carried on the table (shouldn't
|
|
2130
|
+
# happen for a DataInterval-bearing table built via _build_data_table).
|
|
2131
|
+
secs = t.interval[0]
|
|
2132
|
+
value, unit = _seconds_to_display(secs)
|
|
2133
|
+
lines.append(f"Interval: {value} {unit}")
|
|
2134
|
+
lines.append("Fields:")
|
|
2135
|
+
for f in t.fields:
|
|
2136
|
+
drop_last = f.type == 11 and bool(f.subdims)
|
|
2137
|
+
dims_after_drop = f.subdims[:-1] if drop_last else f.subdims
|
|
2138
|
+
if f.type == 11 and len(dims_after_drop) >= 2:
|
|
2139
|
+
# Oracle-confirmed quirk (DataTableInfo's DataFilled at n=1,2,7; a 2D Public
|
|
2140
|
+
# String array, filelist_1.CR6's `Files(3,10) As String * 25`): an ASCII field
|
|
2141
|
+
# with 2+ non-string dims does NOT get the plain Cartesian product every other
|
|
2142
|
+
# multi-dim field gets (contrast DataRecordSize/DataFillDays, same [n,2] shape but
|
|
2143
|
+
# type LONG/IEEE4, which *do* show the full product): the `-l` lister instead
|
|
2144
|
+
# computes both displayed coordinates via `divmod` by the *string storage length*
|
|
2145
|
+
# (`stored_len` = `subdims[-1]`) rather than by the true adjacent declared
|
|
2146
|
+
# dimension -- confirmed exactly for `Files(3,10)` (`stored_len`=28, wrong stride
|
|
2147
|
+
# vs. the true 10): entries 1..28 show as `(1,1)..(1,28)`, then `(2,1)`, `(2,2)`
|
|
2148
|
+
# for the last 2 of its 30 true elements, not `(1,1)..(1,10)` then `(2,1)..`.
|
|
2149
|
+
# Total entry count is still correct (`dim / stored_len`). A field with 3+
|
|
2150
|
+
# non-string dims is unverified; this always yields exactly 2 coordinates like
|
|
2151
|
+
# the confirmed cases, which may not generalise. Mechanism not understood (a
|
|
2152
|
+
# legacy quirk in the exe's own lister); reproduced, not derived.
|
|
2153
|
+
stored_len = f.subdims[-1]
|
|
2154
|
+
total = f.dimension // stored_len if stored_len else 0
|
|
2155
|
+
total = min(total, _MAX_EXPLODE_COUNT)
|
|
2156
|
+
explode_iter = ((lin // stored_len + 1, lin % stored_len + 1) for lin in range(total))
|
|
2157
|
+
else:
|
|
2158
|
+
explode_iter = _explode_indices(f.subdims, drop_last, f.begin_index, f.dimension)
|
|
2159
|
+
for idx in explode_iter:
|
|
2160
|
+
suffix = "(" + ",".join(str(i) for i in idx) + ")" if idx else ""
|
|
2161
|
+
line = f" {f.name}{suffix}"
|
|
2162
|
+
if f.units:
|
|
2163
|
+
line += f"\tUnits: {f.units}"
|
|
2164
|
+
lines.append(line)
|
|
2165
|
+
lines.append("")
|
|
2166
|
+
return "\r\n".join(lines) + ("\r\n" if lines else "")
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
def _seconds_to_display(secs: int) -> tuple[int, str]:
|
|
2170
|
+
for unit, mult in (("DAY", 86400), ("HR", 3600), ("MIN", 60)):
|
|
2171
|
+
if secs % mult == 0 and secs // mult > 0:
|
|
2172
|
+
return secs // mult, unit
|
|
2173
|
+
return secs, "SEC"
|