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.
Files changed (117) hide show
  1. opencrbasic/__init__.py +3 -0
  2. opencrbasic/__main__.py +5 -0
  3. opencrbasic/analysis.py +184 -0
  4. opencrbasic/bindings.py +1034 -0
  5. opencrbasic/catalog.py +173 -0
  6. opencrbasic/checkconfig.py +144 -0
  7. opencrbasic/checker/__init__.py +977 -0
  8. opencrbasic/checker/cr3xx.py +3118 -0
  9. opencrbasic/checker/ess.py +81 -0
  10. opencrbasic/checker/expressions.py +3581 -0
  11. opencrbasic/checker/instr_data.py +1802 -0
  12. opencrbasic/checker/instr_measure.py +1547 -0
  13. opencrbasic/checker/instr_rules.py +3540 -0
  14. opencrbasic/checker/instructions.py +17 -0
  15. opencrbasic/checker/intervals.py +678 -0
  16. opencrbasic/checker/longconv.py +133 -0
  17. opencrbasic/checker/scan_time.py +580 -0
  18. opencrbasic/checker/storage.py +456 -0
  19. opencrbasic/checker/structs.py +240 -0
  20. opencrbasic/checker/structure.py +6213 -0
  21. opencrbasic/checker/symbols.py +609 -0
  22. opencrbasic/checker/terminals.py +249 -0
  23. opencrbasic/checker/unused.py +186 -0
  24. opencrbasic/checker/var_types.py +564 -0
  25. opencrbasic/checker/warnings.py +661 -0
  26. opencrbasic/checks.py +491 -0
  27. opencrbasic/cli.py +391 -0
  28. opencrbasic/commands.py +128 -0
  29. opencrbasic/compiler.py +320 -0
  30. opencrbasic/consteval.py +327 -0
  31. opencrbasic/data/background_calibration.json +4 -0
  32. opencrbasic/data/before_beginprog.json +99 -0
  33. opencrbasic/data/cr1000x/background_calibration.json +4 -0
  34. opencrbasic/data/cr1000x/before_beginprog.json +138 -0
  35. opencrbasic/data/cr1000x/ess_variables.json +1087 -0
  36. opencrbasic/data/cr1000x/fnotch.json +29 -0
  37. opencrbasic/data/cr1000x/functions.json +1337 -0
  38. opencrbasic/data/cr1000x/instructions.json +4836 -0
  39. opencrbasic/data/cr1000x/measure.json +121 -0
  40. opencrbasic/data/cr1000x/mode.json +111 -0
  41. opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
  42. opencrbasic/data/cr1000x/settings_fields.json +321 -0
  43. opencrbasic/data/cr1000x/status_members.json +318 -0
  44. opencrbasic/data/cr1000x/symbols.json +872 -0
  45. opencrbasic/data/cr1000x/terminals.json +225 -0
  46. opencrbasic/data/cr300/before_beginprog.json +42 -0
  47. opencrbasic/data/cr300/ess_variables.json +1087 -0
  48. opencrbasic/data/cr300/fnotch.json +22 -0
  49. opencrbasic/data/cr300/functions.json +1261 -0
  50. opencrbasic/data/cr300/instructions.json +2837 -0
  51. opencrbasic/data/cr300/measure.json +33 -0
  52. opencrbasic/data/cr300/mode.json +121 -0
  53. opencrbasic/data/cr300/settings_fields.json +259 -0
  54. opencrbasic/data/cr300/status_members.json +303 -0
  55. opencrbasic/data/cr300/symbols.json +584 -0
  56. opencrbasic/data/cr300/terminals.json +193 -0
  57. opencrbasic/data/cr350/before_beginprog.json +44 -0
  58. opencrbasic/data/cr350/ess_variables.json +1087 -0
  59. opencrbasic/data/cr350/fnotch.json +22 -0
  60. opencrbasic/data/cr350/functions.json +1262 -0
  61. opencrbasic/data/cr350/instructions.json +2848 -0
  62. opencrbasic/data/cr350/measure.json +35 -0
  63. opencrbasic/data/cr350/mode.json +121 -0
  64. opencrbasic/data/cr350/settings_fields.json +292 -0
  65. opencrbasic/data/cr350/status_members.json +303 -0
  66. opencrbasic/data/cr350/symbols.json +572 -0
  67. opencrbasic/data/cr350/terminals.json +190 -0
  68. opencrbasic/data/ess_variables.json +1087 -0
  69. opencrbasic/data/fnotch.json +34 -0
  70. opencrbasic/data/functions.json +1337 -0
  71. opencrbasic/data/instructions.json +4965 -0
  72. opencrbasic/data/measure.json +123 -0
  73. opencrbasic/data/mode.json +121 -0
  74. opencrbasic/data/notices_cr6.json +890 -0
  75. opencrbasic/data/pipeline_tasks.json +84 -0
  76. opencrbasic/data/settings_fields.json +322 -0
  77. opencrbasic/data/status_members.json +319 -0
  78. opencrbasic/data/status_table.json +684 -0
  79. opencrbasic/data/symbols.json +902 -0
  80. opencrbasic/data/terminals.json +195 -0
  81. opencrbasic/data/type_codes.json +249 -0
  82. opencrbasic/diagnostics.py +196 -0
  83. opencrbasic/editing.py +827 -0
  84. opencrbasic/formatting.py +310 -0
  85. opencrbasic/helpdocs.py +677 -0
  86. opencrbasic/hover.py +667 -0
  87. opencrbasic/inlay.py +169 -0
  88. opencrbasic/lexer.py +183 -0
  89. opencrbasic/lsp/__init__.py +5 -0
  90. opencrbasic/lsp/__main__.py +7 -0
  91. opencrbasic/lsp/config.py +202 -0
  92. opencrbasic/lsp/features.py +602 -0
  93. opencrbasic/lsp/server.py +741 -0
  94. opencrbasic/lsp/session.py +1189 -0
  95. opencrbasic/lsp/worker.py +128 -0
  96. opencrbasic/mode.py +143 -0
  97. opencrbasic/navigation.py +401 -0
  98. opencrbasic/parser.py +3264 -0
  99. opencrbasic/positions.py +274 -0
  100. opencrbasic/preprocess.py +269 -0
  101. opencrbasic/profiles.py +266 -0
  102. opencrbasic/provenance.py +17 -0
  103. opencrbasic/quickfix.py +331 -0
  104. opencrbasic/rename.py +662 -0
  105. opencrbasic/signature.py +33 -0
  106. opencrbasic/source.py +600 -0
  107. opencrbasic/sources.py +125 -0
  108. opencrbasic/syntax.py +337 -0
  109. opencrbasic/tables.py +2173 -0
  110. opencrbasic/tables_hook.py +14 -0
  111. opencrbasic/tdf.py +142 -0
  112. opencrbasic/timing.py +565 -0
  113. opencrbasic-1.0.0.dist-info/METADATA +223 -0
  114. opencrbasic-1.0.0.dist-info/RECORD +117 -0
  115. opencrbasic-1.0.0.dist-info/WHEEL +4 -0
  116. opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
  117. opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,456 @@
1
+ """CR300/CR350 data-table allocator (`docs/tdf-rules.md` "Storage limit" and "Auto-sized tables").
2
+
3
+ After a clean parse the real compiler sizes every table in one pool (`cr300comp.exe` /
4
+ `cr350comp.exe`): up to 26 passes, then `Unable to allocate enough data table space to manage
5
+ flash write cycle limits.` and, when the last pass left the pool negative, `Not enough storage
6
+ area for DataTables.` `allocate` is a port of that allocator (decompiled in rounds 15 and 16,
7
+ confirmed with oracle threshold sweeps on both loggers), 32-bit wraparound and float32 arithmetic
8
+ included. It does three things at once:
9
+
10
+ - charges every called table of a fixed size `roundup4096(a + int(R * (N / k)) + 0x2000)`, raised
11
+ to the flash-wear minimum record count, against the pool less 16 KB per declared table
12
+ (`pool_bytes`);
13
+ - sizes the auto (`-1`) tables from what is left (`tables.build_tables` takes the TDF record
14
+ counts from here), and a table called only from a `Sub`/`AcceptDataRecords` is charged its auto
15
+ share instead of its own `N`;
16
+ - fails (both messages) when no pass fits, which also covers a size literal beyond +-2^31
17
+ (`INT_MIN`, whose wrapped charge sinks the pool whenever another table is auto-sized).
18
+
19
+ `PreserveVariables` / `PreserveOneVariable` take their store off the pool first
20
+ (`preserve_charge`); other unknown consumers could only shrink the real pool further, so the model
21
+ can miss a rejection or size an auto table too large but not invent a rejection.
22
+ """
23
+
24
+ import math
25
+ import struct
26
+
27
+ from .. import syntax as S
28
+
29
+ _POOL_PAGES = {"cr300": 2560, "cr350": 12800} # 0xA00000 / 0x3200000 bytes, in 4 KiB pages
30
+ _PAGE = 4096
31
+ _BLOCK_BYTES = 496 # a DataInterval table's block: 496 // bytes per record
32
+ _INT_MIN = -(1 << 31)
33
+ _BASE = 0x600000 # start of the table area
34
+
35
+ MSG_ALLOC = "Unable to allocate enough data table space to manage flash write cycle limits."
36
+ MSG_STORAGE = "Not enough storage area for DataTables."
37
+
38
+
39
+ def _f32(x: float) -> float:
40
+ try:
41
+ return struct.unpack("f", struct.pack("f", x))[0]
42
+ except OverflowError:
43
+ return math.copysign(math.inf, x)
44
+
45
+
46
+ def _cvt_int(x: float) -> int:
47
+ """`cvttss2si`: truncation, with the out-of-range / NaN result `INT_MIN`."""
48
+ if x != x or not (-2147483648.0 <= x < 2147483648.0):
49
+ return _INT_MIN
50
+ return int(x)
51
+
52
+
53
+ def _wrap32(v: int) -> int:
54
+ v &= 0xFFFFFFFF
55
+ return v - (1 << 32) if v & 0x80000000 else v
56
+
57
+
58
+ _I32 = (-(1 << 31), (1 << 31) - 1)
59
+
60
+
61
+ def _i32(v: int) -> int:
62
+ return _wrap32(v)
63
+
64
+
65
+ def _min_recs(sp: dict, k: int, cc: int) -> int:
66
+ """Flash-wear minimum record count of pass `cc`, `FUN_005b50c0` (also the `W == -1` and
67
+ `W == 0` branches). `sp` carries `flagged`, `day`, `own_ms`, `W` (call interval in seconds, `None`
68
+ when the call interval is unknown) and `B`'s block `k`."""
69
+ if sp["flagged"]:
70
+ period = 86400000.0
71
+ elif sp["day"]:
72
+ period = 2592000000.0
73
+ elif sp["own_ms"]:
74
+ period = _f32(float(sp["own_ms"]))
75
+ elif sp["W"] is None:
76
+ if cc == 0:
77
+ period = 50.0
78
+ elif cc < 5:
79
+ period = float(cc * 100)
80
+ elif cc < 10:
81
+ period = float(cc * 1000)
82
+ else:
83
+ period = float(cc * 5000)
84
+ elif sp["W"] == 0:
85
+ period = 50.0
86
+ for lim, val in (
87
+ (2, 2000.0),
88
+ (4, 5000.0),
89
+ (8, 10000.0),
90
+ (12, 20000.0),
91
+ (16, 30000.0),
92
+ (20, 60000.0),
93
+ (23, 120000.0),
94
+ ):
95
+ if cc > lim:
96
+ period = val
97
+ else:
98
+ period = _f32(_f32(sp["W"]) * 1000.0)
99
+ den = _f32(max(float(10 - cc), 1.0))
100
+ try:
101
+ wear = _f32(
102
+ _f32(_f32(_f32(3600000.0 / period) * 24.0) / _f32(float(k))) / _f32(_f32(100000.0) / _f32(den * 365.0))
103
+ )
104
+ except ZeroDivisionError:
105
+ return _INT_MIN
106
+ return _cvt_int(wear)
107
+
108
+
109
+ def preserve_charge(a: int, d: int, q_arg: int, min_tick: int, tick: int, public_bytes: int) -> int:
110
+ """Pool bytes taken by the variable-preservation store, `cr300comp` `FUN_005b4dd0(A, D, q_arg,
111
+ minimum)` (float32 at each step). `tick` is the smallest scan interval in 100 us units."""
112
+ t = max(tick, min_tick)
113
+ q = max(public_bytes // d, 1) if q_arg == 0 else q_arg # q = 0 charges as q = 1 (oracle, T = 10 s)
114
+ wear = _f32(_f32(_f32(3600000.0 / _f32(t / 10.0)) * 24.0) / 27.0)
115
+ recs = _f32(_f32(_f32(_f32(q * 0.6) * wear) + q) + 2.0)
116
+ n = _cvt_int(max(recs, 5.0))
117
+ return _round_page(max(_wrap32(n * a), 0x2000))
118
+
119
+
120
+ def _round_page(v: int) -> int:
121
+ """`FUN_005d4660`: round an unsigned 32-bit address up to 4096."""
122
+ return _wrap32((-(-(v & 0xFFFFFFFF) // _PAGE)) * _PAGE)
123
+
124
+
125
+ def allocate(specs: list[dict], pool: int) -> dict:
126
+ """Port of the table allocator (`cr300comp` `FUN_005ae6e0`, 26 passes; docs/tdf-rules.md
127
+ "Storage limit" and "Auto-sized tables"). `specs` are the user tables in declaration order:
128
+ `called`, `n` (record count argument; -1 auto, INT_MIN for a size beyond 32 bits), `flagged`,
129
+ `B` (bytes per record), `lapses`, `card`, `own_ms` (DataInterval in ms, 0 none), `W` (call
130
+ interval in seconds or None), `fillstop`, `kone`, `day`. Returns `{"sizes": [...], "msg1": bool,
131
+ "msg2": bool}` (sizes are the final record counts of the last pass)."""
132
+ st = []
133
+ for sp in specs:
134
+ st.append(
135
+ {
136
+ "N": sp["n"],
137
+ "k": 0,
138
+ "R": 0,
139
+ "lapses": sp["lapses"],
140
+ **{x: sp[x] for x in ("called", "flagged", "B", "card", "own_ms", "W", "fillstop", "kone", "day")},
141
+ }
142
+ )
143
+ e4 = pool
144
+ ok_exit = False
145
+ for cc in range(26):
146
+ e4 = pool
147
+ reserve = 0
148
+ rate = 0.0
149
+ d8 = False
150
+ ptr = _BASE
151
+ for t in st:
152
+ if not t["called"]:
153
+ t["N"] = 1
154
+ reserve = _i32(reserve + t["B"])
155
+ continue
156
+ if (t["N"] < 0 or t["W"] is None) and not t["flagged"]:
157
+ _blockfix(t, 512)
158
+ R, k = t["R"], t["k"]
159
+ c1 = (1 if R < 512 else 3) + (1024 // R) + (1 if k == 1 else 0)
160
+ c1 = max(c1 * R, 0x3000)
161
+ reserve = _i32(reserve + c1)
162
+ if not t["card"]:
163
+ reserve = _i32(reserve + _lap(t) * t["B"])
164
+ w = 1.0 if t["W"] is None else _f32(t["W"])
165
+ rate = _f32(_f32(float(R) / _f32(float(k) * w)) + rate)
166
+ else:
167
+ a1 = _lap(t)
168
+ _blockfix(t, _i32(a1 + t["N"]))
169
+ R, k = t["R"], t["k"]
170
+ a2 = _lap(t)
171
+ u = _i32(a2 + _cvt_int(_f32(_f32(float(R)) * _f32(_f32(float(t["N"])) / _f32(float(k))))) + 0x2000)
172
+ if not t["fillstop"]:
173
+ lo = _min_recs(t, k, cc)
174
+ if _cvt_int(_f32(_f32(float(u)) / _f32(float(R)))) < lo:
175
+ u = _i32(lo * R)
176
+ ptr, used = _alloc(ptr, u)
177
+ e4 = _i32(e4 - used)
178
+ dur = 0.0
179
+ if rate != 0.0:
180
+ dur = _f32(float(_i32(_i32(e4 - reserve) - 0x1000))) / rate if rate else 0.0
181
+ dur = _f32(dur)
182
+ if dur <= 0.0:
183
+ d8 = True
184
+ for t in st:
185
+ if t["N"] < 0 or (t["W"] is None and not t["flagged"]):
186
+ if t["called"] and t["B"]:
187
+ R, k = t["R"], t["k"]
188
+ w = 1.0 if t["W"] is None else _f32(t["W"])
189
+ n = _cvt_int(_f32(dur / w)) if w else _INT_MIN
190
+ u = _cvt_int(_f32(_f32(float(R)) * _f32(_f32(float(n)) / _f32(float(k)))))
191
+ u = _i32(u + _lap(t) + 0x2000)
192
+ if u < 0x3000:
193
+ u = 0x3000
194
+ if not t["fillstop"]:
195
+ lo = _min_recs(t, k, cc)
196
+ if _tdiv(u, R) < lo:
197
+ d8 = True
198
+ ptr, used = _alloc(ptr, u)
199
+ b4 = _tdiv(used, R)
200
+ if t["N"] < 0:
201
+ t["N"] = _i32((b4 - (4096 // R + 1)) * k)
202
+ e4 = _i32(e4 - used)
203
+ if t["N"] < 1:
204
+ t["N"] = 1
205
+ if t["N"] < t["k"]:
206
+ t["N"] = t["k"]
207
+ if e4 < 0:
208
+ d8 = True
209
+ if not d8:
210
+ ok_exit = True
211
+ break
212
+ return {"sizes": [t["N"] for t in st], "msg1": not ok_exit, "msg2": e4 < 0}
213
+
214
+
215
+ def _lap(t: dict) -> int:
216
+ """Records kept for the Lapses count: `Lapses * 16` wraps to 32 bits before the division."""
217
+ return _tdiv(_i32(t["lapses"] * 16), t["B"]) + 1 if t["lapses"] else 0
218
+
219
+
220
+ def _tdiv(a: int, b: int) -> int:
221
+ q = abs(a) // abs(b)
222
+ return q if (a >= 0) == (b >= 0) else -q
223
+
224
+
225
+ def _alloc(ptr: int, u: int) -> tuple[int, int]:
226
+ """One table's span from the running pointer: start and end rounded up to a page."""
227
+ start = _round_page(ptr)
228
+ end = _round_page(_i32(start + u))
229
+ return end, _i32(end - start)
230
+
231
+
232
+ def _blockfix(t: dict, arg: int) -> None:
233
+ """`FUN_005b7d00`: the record block (`k` records of `R = k * B + 16` bytes), fixed the first time."""
234
+ if t["R"]:
235
+ return
236
+ if t["lapses"] == 0:
237
+ t["k"] = 1
238
+ else:
239
+ t["k"] = min(_BLOCK_BYTES // t["B"], arg)
240
+ if t["k"] < 2:
241
+ t["k"], t["lapses"] = 1, 0
242
+ if t["kone"]:
243
+ t["k"], t["lapses"] = 1, 0
244
+ t["R"] = t["k"] * t["B"] + 16
245
+
246
+
247
+ def pool_bytes(profile_name: str, n_declared: int, has_public: bool, n_named_const: int) -> int:
248
+ """The pool left for the user's tables: 16 KB per table beyond the first (`DataTableInfo`, a
249
+ `Public` table and a named `ConstTable` are tables too)."""
250
+ extra = (1 if has_public else 0) + n_named_const
251
+ return (_POOL_PAGES[profile_name] - 4 * (n_declared + extra)) * _PAGE
252
+
253
+
254
+ def spec_of(m: dict) -> dict:
255
+ """The allocator input of one data table from its `tables._build_data_table` meta."""
256
+ from .. import tables as T
257
+
258
+ cls = T.size_class(m["size_raw"])
259
+ flagged = False
260
+ if cls is None:
261
+ n = 1
262
+ elif cls[0] == "auto":
263
+ n = -1
264
+ elif cls[0] in ("edge", "inf"):
265
+ n = 2147483647
266
+ flagged = cls[0] == "edge" and cls[1] < 0
267
+ elif cls[0] == "toxic":
268
+ n, flagged = _INT_MIN, True
269
+ else:
270
+ n, flagged = cls[1], cls[2]
271
+ di = bool(m["interval_seconds"]) or m["zero_interval"]
272
+ return {
273
+ "called": m["called"],
274
+ "n": n,
275
+ "flagged": flagged,
276
+ "B": m["bytes_per_record"],
277
+ "lapses": (m["lapses"] if m["lapses"] < 1 << 31 else _INT_MIN) if di else 0,
278
+ "card": False, # TableFile does not skip the Lapses term here
279
+ "own_ms": int(round(m["interval_seconds"] * 1000)) if m["interval_seconds"] else 0,
280
+ "W": m["call_w"],
281
+ "fillstop": m.get("fillstop", False),
282
+ "kone": False,
283
+ "day": False,
284
+ "zero_interval": m["zero_interval"],
285
+ }
286
+
287
+
288
+ def run(metas: list[dict], pool: int) -> dict | None:
289
+ """Runs the allocator on the data tables' metas (`None` when a table has no record bytes)."""
290
+ specs = [spec_of(m) for m in metas]
291
+ for sp in specs:
292
+ if sp["zero_interval"] and sp["W"]:
293
+ sp["own_ms"] = int(round(sp["W"] * 1000))
294
+ if any(not sp["B"] for sp in specs):
295
+ return None
296
+ return allocate(specs, pool)
297
+
298
+
299
+ _MAX_ALLOC = 0x8000000 # CR6/CR1000X: largest single allocation request, 128 MiB
300
+ MSG_OOM = "Out of memory."
301
+
302
+
303
+ MSG_ALLOC_TABLE = "Out of Memory; Unable to allocate table {}."
304
+
305
+
306
+ def cr6_table_oom(m: dict, auto_n: int | None = None, name: str = "") -> str | None:
307
+ """CR6/CR1000X: a table's single block request exceeds `_MAX_ALLOC` (docs/tdf-rules.md "CR6/
308
+ CR1000X Out of memory"): returns `MSG_OOM`, or for an auto-sized table whose request is zero
309
+ `MSG_ALLOC_TABLE`, else `None`. `auto_n` is the resolved record count of an auto (or negative)
310
+ size, only modelled without CardOut/TableFile; a beyond-range size is left alone."""
311
+ from .. import tables as T
312
+
313
+ cls = T.size_class(m["size_raw"])
314
+ auto = False
315
+ if cls is None:
316
+ n = 1
317
+ elif cls[0] == "fixed" and not cls[2]:
318
+ n = cls[1]
319
+ elif cls[0] == "auto" or (cls[0] == "fixed" and cls[2]): # CR6/CR1000X size every negative size as auto
320
+ if auto_n is None or not (-1 << 31) < auto_n < 1 << 31 or auto_n in (0, 1) or m.get("has_card"):
321
+ return None # a CardOut/TableFile table is not modelled
322
+ n = auto_n
323
+ auto = True
324
+ else:
325
+ return None
326
+ b = m["bytes_per_record"]
327
+ if not b or not m["called"] or b > 2147483647:
328
+ return None # (a String field past INT_MAX compiles; oracle, 2026-10-25, round 35)
329
+ lap = (m["lapses"] if m["lapses"] < 1 << 31 else _INT_MIN) if m["display_interval"] else 0
330
+ a = _tdiv(_wrap32(abs(lap) * 16 if lap != _INT_MIN else 0), b) + 1 if lap else 0
331
+ mm = _wrap32(n + a)
332
+ k = 1
333
+ if lap:
334
+ k = 1008 // b if auto else min(1008 // b, mm) # an auto table's block is fixed before its count is known
335
+ if k < 2:
336
+ k = 1
337
+ r = k * b + 16
338
+ if n == 1:
339
+ c = r
340
+ else:
341
+ c = _wrap32((2048 // r + _tdiv(mm, k) + (1 if r < 1024 else 3)) * r)
342
+ if c > _MAX_ALLOC or c < 0: # a wrapped negative request fails the host allocation
343
+ return MSG_OOM
344
+ return MSG_ALLOC_TABLE.format(name) if auto and c == 0 else None
345
+
346
+
347
+ def _var_words(program: S.Program, env, kinds: tuple) -> list[tuple[int, int]]:
348
+ """(elements, 4-byte words per element) of every `kinds` declaration (a Float/Long/Boolean
349
+ is one word, a Double two, a String its padded length / 4; any other type is not counted, so
350
+ the total can only be too small)."""
351
+ from .. import tables as T
352
+
353
+ out = []
354
+ for st in program.statements:
355
+ if not isinstance(st, S.Declaration) or st.kind not in kinds:
356
+ continue
357
+ for v in st.vars:
358
+ t = (v.type or "FLOAT").upper()
359
+ if t not in ("FLOAT", "LONG", "BOOLEAN", "DOUBLE", "STRING"):
360
+ continue
361
+ dims = [T._as_int(T._fold(d, env), default=1) for d in v.dims]
362
+ if len(dims) > 3 or any(not 1 <= d <= 2147483647 for d in dims):
363
+ continue # `Illegally dimension`: never allocated
364
+ info = T._make_var_info(v, env)
365
+ stored = info.stored_len or 0 # (set for every String)
366
+ if t == "STRING" and stored > 2147483647:
367
+ continue # the length wraps negative: compiles (oracle, 2026-10-25, round 35)
368
+ n = 1
369
+ for d in dims:
370
+ n *= d
371
+ out.append((n, 2 if t == "DOUBLE" else stored // 4 if t == "STRING" else 1))
372
+ return out
373
+
374
+
375
+ def _segment_oom(words: list[tuple[int, int]]) -> bool:
376
+ """One variable segment (`Dim` or `Public`) starts at 4 words and is requested as
377
+ `wrap32(words * 4)` bytes; the request fails above 128 MiB or when it wraps negative (zero
378
+ is a null request that crashes the compiler: not modelled)."""
379
+ total = 4
380
+ for n, w in words:
381
+ total = _wrap32(total + _wrap32(_wrap32(n) * w))
382
+ req = _wrap32(total * 4)
383
+ return req > _MAX_ALLOC or req < 0
384
+
385
+
386
+ def variables_oom(program: S.Program, profile, env) -> bool:
387
+ """CR6/CR1000X end of pass 1: a `Dim` or a `Public` segment too large to allocate (reported at
388
+ the program's last statement, ends the compile before pass 2)."""
389
+ return profile.name in ("cr6", "cr1000x") and any(
390
+ _segment_oom(_var_words(program, env, (k,))) for k in ("DIM", "PUBLIC")
391
+ )
392
+
393
+
394
+ def string_length_oom_line(program: S.Program, diags) -> int | None:
395
+ """All four loggers: a String length reported as `Variable illegal in Parameter` (a variable,
396
+ or on CR300/CR350 also an undefined name; `structure._report_decl_expr` tags the message with
397
+ the variable) leaves a length the end of pass 1 cannot allocate: `Out of memory.` at the
398
+ program's last statement, or at the declaration when it has an initializer (round 31)."""
399
+ tagged = {d.stmt for d in diags.errors if d.stmt and d.message.startswith("Variable illegal in Parameter:")}
400
+ if not tagged:
401
+ return None
402
+ for st in program.statements:
403
+ for v in st.vars if isinstance(st, S.Declaration) else st.params if isinstance(st, S.SubDef) else []:
404
+ if id(v) in tagged:
405
+ # (the parser keeps a String's `= init` inside `string_size`)
406
+ size = getattr(v, "string_size", None)
407
+ init = getattr(v, "init", None) or (isinstance(size, S.Binary) and size.op == "=")
408
+ return st.line if init else program.last_line
409
+ return None
410
+
411
+
412
+ def check_pass1(program: S.Program, profile, diags, env) -> None:
413
+ line = string_length_oom_line(program, diags)
414
+ if line is None and variables_oom(program, profile, env):
415
+ line = program.last_line
416
+ if line is not None:
417
+ diags.error(line, MSG_OOM)
418
+
419
+
420
+ def public_table_oom(program: S.Program, env) -> bool:
421
+ """The `Public` table is one record of the `Public` variables' bytes; its block request is
422
+ `2 * bytes + 32`, so it fails above 67,108,848 bytes."""
423
+ words = _var_words(program, env, ("PUBLIC",))
424
+ if any(n >= 1 << 31 for n, _ in words):
425
+ return False
426
+ return sum(n * w for n, w in words) * 4 > (_MAX_ALLOC - 32) // 2
427
+
428
+
429
+ def check(program: S.Program, profile, result, diags) -> None:
430
+ """Reports the storage-limit pair of messages (pass 2, only on an otherwise clean program)."""
431
+ if profile.name in ("cr6", "cr1000x") and not diags.errors:
432
+ from .. import consteval
433
+
434
+ if public_table_oom(program, consteval.Env(profile, dict(program.consts))):
435
+ diags.error(0, MSG_OOM)
436
+ else:
437
+ msgs = [
438
+ cr6_table_oom(t._storage, t._storage.get("_n_raw"), t.name)
439
+ for t in result.tables or []
440
+ if hasattr(t, "_storage")
441
+ ]
442
+ if MSG_OOM in msgs: # one message; a plain `Out of memory.` wins over a table's zero request
443
+ diags.error(0, MSG_OOM)
444
+ elif any(msgs):
445
+ diags.error(0, next(m for m in msgs if m))
446
+ return
447
+ if profile.name not in _POOL_PAGES or diags.errors:
448
+ return
449
+ declared = [t for t in (result.tables or []) if hasattr(t, "_storage")]
450
+ if not declared:
451
+ return
452
+ alloc = declared[0]._storage.get("alloc")
453
+ if alloc and alloc["msg1"]:
454
+ diags.error(0, MSG_ALLOC)
455
+ if alloc["msg2"]:
456
+ diags.error(0, MSG_STORAGE)