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,564 @@
1
+ """Variable-data-type argument checks (pass 2, tier C). Rules, fixtures and oracle evidence:
2
+ `docs/instructions.md` "Variable data types" (`tests/fixtures/var_types.json`).
3
+
4
+ Not a uniform per-type-code rule: each instruction applies its own requirement, so this module is
5
+ a small table of them. Unknown names, functions and operand mixes the oracle was not probed on
6
+ have no type (`None`), and a check that needs a type it does not have stays silent.
7
+ """
8
+
9
+ import json
10
+ import math
11
+ import re
12
+ from pathlib import Path
13
+
14
+ from .. import consteval
15
+ from .. import syntax as S
16
+ from ..diagnostics import Diagnostics
17
+ from . import symbols
18
+
19
+ _NUMERIC = ("FLOAT", "LONG")
20
+ # built-in functions returning a String (`kind` 312, docs/binary.md), the same on all four loggers;
21
+ # `CType` returns the type its second argument names (oracle, round 67)
22
+ _STRING_FUNCTIONS = frozenset(
23
+ f["name"]
24
+ for f in json.loads((Path(__file__).resolve().parents[1] / "data" / "functions.json").read_text())
25
+ if f.get("kind") == 312
26
+ ) - {"CTYPE"}
27
+
28
+
29
+ def _types(program: S.Program) -> tuple[dict[str, str], dict[str, str]]:
30
+ """Declared variable types and `Alias` name -> target variable name (upper case)."""
31
+ types = symbols.declared_variable_types(program)
32
+ alias: dict[str, str] = {}
33
+ for st in program.statements:
34
+ if (
35
+ isinstance(st, S.Instruction)
36
+ and st.keyword.upper() == "ALIAS"
37
+ and st.args
38
+ and isinstance(st.args[0], (S.Name, S.Index))
39
+ ):
40
+ for a in st.args[1:]:
41
+ if isinstance(a, S.Name):
42
+ alias[a.name.upper()] = st.args[0].name.upper()
43
+ return types, alias
44
+
45
+
46
+ class _Typer:
47
+ def __init__(self, program: S.Program):
48
+ self.types, self.alias = _types(program)
49
+ self.consts = {st.name.upper(): st for st in program.statements if isinstance(st, S.ConstDecl)}
50
+ self.function_types = {
51
+ st.name.upper(): st.return_type.split("*")[0].strip().upper()
52
+ for st in program.statements
53
+ if isinstance(st, S.SubDef) and st.keyword.upper() == "FUNCTION" and st.return_type
54
+ }
55
+
56
+ def base(self, name: str) -> str:
57
+ return self.alias.get(name.upper(), name.upper())
58
+
59
+ def variable(self, expr: S.Expr | None) -> str | None:
60
+ """Upper-case declared-variable name an expression refers to, or None."""
61
+ if isinstance(expr, (S.Name, S.Index)):
62
+ name = self.base(expr.name)
63
+ return name if name in self.types else None
64
+ return None
65
+
66
+ def of(self, expr: S.Expr | None, depth: int = 0) -> str | None:
67
+ """FLOAT / LONG / STRING / BOOLEAN / DOUBLE, or None when not known."""
68
+ if isinstance(expr, S.Number):
69
+ text = expr.text.upper()
70
+ if text.startswith("&"):
71
+ return "LONG"
72
+ return "FLOAT" if "." in text or "E" in text else "LONG"
73
+ if isinstance(expr, S.String):
74
+ return "STRING"
75
+ var = self.variable(expr)
76
+ if var is not None:
77
+ return self.types[var]
78
+ if isinstance(expr, S.Index):
79
+ # a String-returning built-in or Function, or an `IIF` with a String arm (oracle, all
80
+ # four loggers, round 65, docs/instructions.md#string-operands)
81
+ name = expr.name.upper()
82
+ if (
83
+ name in _STRING_FUNCTIONS
84
+ or self.function_types.get(name) == "STRING"
85
+ or (name == "IIF" and len(expr.args) > 1 and self.of(expr.args[1], depth + 1) == "STRING")
86
+ or (
87
+ name == "CTYPE"
88
+ and len(expr.args) > 1
89
+ and isinstance(expr.args[1], S.Name)
90
+ and expr.args[1].name.upper() == "STRING"
91
+ )
92
+ ):
93
+ return "STRING"
94
+ if isinstance(expr, S.Name):
95
+ if expr.name.upper() in ("TRUE", "FALSE"):
96
+ return "LONG"
97
+ if expr.name.upper() == "NAN":
98
+ return "FLOAT"
99
+ const = self.consts.get(expr.name.upper())
100
+ if const is None or depth > 8:
101
+ return None
102
+ if const.type:
103
+ return const.type.upper()
104
+ return self.of(const.value, depth + 1)
105
+ if isinstance(expr, S.Unary) and expr.op in ("-", "+"):
106
+ return self.of(expr.operand, depth + 1) if self.of(expr.operand, depth + 1) in _NUMERIC else None
107
+ if isinstance(expr, S.Binary) and expr.op in ("+", "-", "*", "/", "&"):
108
+ left, right = self.of(expr.left, depth + 1), self.of(expr.right, depth + 1)
109
+ if "STRING" in (left, right) and expr.op in ("+", "&"):
110
+ return "STRING" # `2000 + Mid(...)` concatenates (round 65)
111
+ if left in _NUMERIC and right in _NUMERIC:
112
+ return "LONG" if left == right == "LONG" and expr.op != "/" else "FLOAT"
113
+ return None
114
+
115
+
116
+ # SNMPVariable: Type keywords a declared variable type may not be given (oracle, CR6/CR1000X).
117
+ # A Float variable takes only Float, a String one String or Float, the rest anything but String.
118
+ _SNMP_TYPES = {"INTEGER", "COUNTER", "STRING", "TIMETICKS", "OPAQUE", "GAUGE", "FLOAT"}
119
+ _SNMP_RE = re.compile(r"\s*SNMPVARIABLE\s*\(\s*(\w+)\s*,\s*[\d.]+\s*,\s*(\w+)", re.I)
120
+ _SNMP_ALLOWED = {"FLOAT": {"FLOAT"}, "STRING": {"STRING", "FLOAT"}}
121
+
122
+
123
+ # Operators that reject a String operand (oracle, all four loggers): the logical and shift
124
+ # operators with one shared message, `* / MOD` with one message per String side.
125
+ _STRING_ILLEGAL_OPS = {"AND", "OR", "XOR", "IMP", "EQV", "<<", ">>"}
126
+ _STRING_ARITH_OPS = {"*", "/", "\\", "MOD"} # `\` is also `INTDV` (round 36)
127
+
128
+
129
+ def _subexpressions(expr: S.Expr | None):
130
+ if expr is None:
131
+ return
132
+ yield expr
133
+ if isinstance(expr, S.Binary):
134
+ yield from _subexpressions(expr.left)
135
+ yield from _subexpressions(expr.right)
136
+ elif isinstance(expr, S.Unary):
137
+ yield from _subexpressions(expr.operand)
138
+ elif isinstance(expr, (S.Index, S.Member)):
139
+ for a in expr.args or []:
140
+ yield from _subexpressions(a)
141
+ elif isinstance(expr, S.ArrayLiteral):
142
+ for a in expr.items:
143
+ yield from _subexpressions(a)
144
+
145
+
146
+ def _statement_expressions(stmt: S.Stmt):
147
+ if isinstance(stmt, S.Assignment):
148
+ yield stmt.value
149
+ elif isinstance(stmt, (S.Instruction, S.Call)):
150
+ yield from stmt.args
151
+ elif isinstance(stmt, (S.If, S.ElseIf)):
152
+ yield stmt.cond
153
+ for inner in (getattr(stmt, "inline", None) or []) + (getattr(stmt, "inline_else", None) or []):
154
+ yield from _statement_expressions(inner)
155
+
156
+
157
+ def _string_typed(typer: _Typer, node: S.Expr | None) -> bool:
158
+ """A String operand, or a logical word whose right operand is one (`X AND "a" AND X`: the
159
+ first `AND` keeps its string operand's type, so the second also reports; `"a" AND X AND X`
160
+ reports once; oracle, all four loggers, 2026-10-12)."""
161
+ while isinstance(node, S.Binary) and node.op in _STRING_ILLEGAL_OPS:
162
+ node = node.right
163
+ return typer.of(node) == "STRING"
164
+
165
+
166
+ def _in_source_order(expr: S.Expr | None):
167
+ """The subexpressions of `expr` with each operator after its left operand and before its
168
+ right one (the order the reference reports String-operand errors in)."""
169
+ if isinstance(expr, S.Binary):
170
+ yield from _in_source_order(expr.left)
171
+ yield expr
172
+ yield from _in_source_order(expr.right)
173
+ elif isinstance(expr, S.Unary):
174
+ yield from _in_source_order(expr.operand)
175
+ elif isinstance(expr, (S.Index, S.Member)):
176
+ for a in expr.args or []:
177
+ yield from _in_source_order(a)
178
+ elif isinstance(expr, S.ArrayLiteral):
179
+ for a in expr.items:
180
+ yield from _in_source_order(a)
181
+
182
+
183
+ def _string_operand_messages(typer: _Typer, stmt: S.Stmt) -> list[str]:
184
+ """The String-operand errors of a statement, one entry per offending operator in source order
185
+ (oracle, all four loggers: `"a" and "b" and "c" .`, 2026-10-10; `(S * 2) + (S AND 1)` and
186
+ `(S * 2) * S`, round 65, docs/instructions.md#string-operands)."""
187
+ found: list[str] = []
188
+ for top in _statement_expressions(stmt):
189
+ for node in _in_source_order(top):
190
+ if not isinstance(node, S.Binary):
191
+ continue
192
+ left, right = _string_typed(typer, node.left), _string_typed(typer, node.right)
193
+ if node.op == "TO": # a String on the right only (`S To 3` compiles); round 9
194
+ if right:
195
+ found.append("String is illegal here")
196
+ elif node.op in _STRING_ILLEGAL_OPS and (left or right):
197
+ found.append("String is illegal here")
198
+ elif node.op in _STRING_ARITH_OPS and (left or right):
199
+ found += (["Illegal String operand"] if left else []) + (
200
+ ["Illegal operand of type STRING"] if right else []
201
+ )
202
+ return found
203
+
204
+
205
+ def _snmp_mismatch(typer: _Typer, name: S.Expr | None, snmp_type: str) -> bool:
206
+ var = typer.variable(name)
207
+ return (
208
+ var is not None
209
+ and snmp_type.upper() in _SNMP_TYPES
210
+ and snmp_type.upper() not in _SNMP_ALLOWED.get(typer.types[var], _SNMP_TYPES - {"STRING"})
211
+ )
212
+
213
+
214
+ def _same_type(dest: str, source: str) -> bool:
215
+ return dest == source or (dest == "STRING" and source == "FLOAT")
216
+
217
+
218
+ def _indexed_past_first(typer: _Typer, dest: S.Expr | None, env: consteval.Env) -> bool:
219
+ """`RoutersNeighbors` takes the whole array: a subscript that is a variable, a binary
220
+ expression or a literal of 2 or more is `Destination variable cannot be indexed` (oracle,
221
+ 2026-10-08: `L(1)`, `L(1.5)`, `L(-1)` and `(1,1)` compile, `L(0)` adds an out-of-bounds line
222
+ and is not modelled). A subscript that is a `Const` name, or a parenthesised literal, is not
223
+ probed (the parser drops the parentheses, so `(1)` reads as `1`). Also (oracle, all four
224
+ loggers, 2026-10-12, r15_routers_neighbors_index): a string literal and a
225
+ `Const` name whose value is 2 or more (`Const One=1.5` compiles; `Const Zero=0` adds the
226
+ out-of-bounds line, not modelled)."""
227
+ if not isinstance(dest, S.Index):
228
+ return False
229
+ for a in dest.args:
230
+ if (
231
+ isinstance(a, (S.Binary, S.String))
232
+ or (isinstance(a, S.Name) and typer.variable(a) is not None)
233
+ or (isinstance(a, S.Number) and a.value >= 2)
234
+ ):
235
+ return True
236
+ if isinstance(a, S.Name):
237
+ try:
238
+ value = consteval.value_of(a, env)
239
+ except consteval.NotConstant:
240
+ continue
241
+ if isinstance(value, (int, float)) and not isinstance(value, bool) and value >= 2:
242
+ return True
243
+ return False
244
+
245
+
246
+ def check_string_operands(program: S.Program, profile, diags: Diagnostics) -> None:
247
+ """The String-operand errors are reported while the statement is parsed: pass 1, whatever came
248
+ before and after, on all four loggers (oracle, 2026-10-19, round 21: `Abs(1,2) : X = "a" * 2`
249
+ and `X = "a" * 2 : Abs(1,2)` both report both; `"a" * 2 : Nope + 1` reports only the String
250
+ error on CR6/CR1000X, the String error then the name on CR300/CR350)."""
251
+ typer = _Typer(program)
252
+ for stmt in program.statements:
253
+ for text in _string_operand_messages(typer, stmt):
254
+ diags.error(stmt.line, text, stmt.file)
255
+
256
+
257
+ def check(program: S.Program, profile, diags: Diagnostics, env: consteval.Env) -> None:
258
+ typer = _Typer(program)
259
+ tables = symbols.scope_tables(program, env)
260
+ in_prog = False
261
+ for stmt in program.statements:
262
+ if isinstance(stmt, S.Instruction) and stmt.keyword.upper() == "BEGINPROG":
263
+ in_prog = True
264
+ if isinstance(stmt, S.Unparsed): # a dotted OID is not an expression
265
+ m = _SNMP_RE.match(stmt.text)
266
+ if (
267
+ m
268
+ and "SNMPVARIABLE" in profile.instructions
269
+ and _snmp_mismatch(typer, S.Name(stmt.line, name=m.group(1)), m.group(2))
270
+ ):
271
+ diags.error(stmt.line, "Miss match of types", stmt.file)
272
+ continue
273
+ if not isinstance(stmt, S.Instruction):
274
+ continue
275
+ kw, args = stmt.keyword.upper(), stmt.args
276
+ if kw not in profile.instructions:
277
+ continue
278
+ message = None
279
+ extra = None
280
+ if kw == "ROUTERSNEIGHBORS" and args:
281
+ kind = "STRING" if isinstance(args[0], S.String) else typer.of(args[0])
282
+ if typer.variable(args[0]) is not None or isinstance(args[0], S.String):
283
+ if kind != "LONG":
284
+ message = "Destination variable must be type LONG"
285
+ elif _indexed_past_first(typer, args[0], env):
286
+ message = "Destination variable cannot be indexed"
287
+ elif kw == "INSTRUCTIONTIMES" and args and typer.variable(args[0]) is not None:
288
+ # (oracle, all four loggers, 2026-10-12: a Float/String/Boolean destination anywhere;
289
+ # a Long one is only accepted before `BeginProg`)
290
+ kind = typer.of(args[0])
291
+ if kind is not None and kind != "LONG":
292
+ message = "Destination variable must be type LONG"
293
+ elif kind == "LONG" and in_prog:
294
+ message = "TimesOfInstructions() must be declared before BeginProg."
295
+ elif kw == "ARGOSERROR" and args:
296
+ if (
297
+ typer.variable(args[0]) is not None
298
+ and typer.of(args[0]) not in (None, "STRING")
299
+ or isinstance(args[0], (S.String, S.Number, S.Binary, S.Unary))
300
+ ):
301
+ # (also a literal or an expression: `ArgosError("fuzz")`, `ArgosError((1+2))`,
302
+ # oracle CR6/CR1000X 2026-10-12)
303
+ message = "A string variable destination is required"
304
+ elif kw == "SORTSPAINDEXED" and len(args) > 2:
305
+ dest, source = typer.of(args[0]), typer.of(args[2])
306
+ if typer.variable(args[0]) is not None and dest and source and not _same_type(dest, source):
307
+ message = "Source and Dest types must be the same."
308
+ elif kw == "SAMPLE" and len(args) > 2 and isinstance(args[2], S.Name) and args[2].name.upper() == "NSEC":
309
+ source = typer.of(args[1])
310
+ if source in ("STRING", "BOOLEAN", "DOUBLE"):
311
+ message = "Source type must be either LONG or FLOAT or NSEC for data type NSEC"
312
+ elif source == "FLOAT" and not any((d.line, d.file) == (stmt.line, stmt.file) for d in diags.errors):
313
+ size = _array_size(typer, tables(stmt).dims, args[1])
314
+ try:
315
+ reps = consteval.value_of(args[0], env)
316
+ except consteval.NotConstant:
317
+ reps = None
318
+ if size is not None and isinstance(reps, int) and reps >= 1 and size < 7 * reps:
319
+ message = "Float to NSEC expects source array size >= 7"
320
+ elif kw in ("SDMCD16MASK", "SDMCD16AC") and args:
321
+ if kw == "SDMCD16MASK" and any(typer.types.get(typer.variable(a) or "", "") == "BOOLEAN" for a in args[:2]):
322
+ message = "Source and Mask type must be LONG."
323
+ if isinstance(args[0], S.Number) or (isinstance(args[0], S.Name) and args[0].name.upper() in typer.consts):
324
+ extra = "Invalid Source."
325
+ elif (
326
+ kw == "SNMPVARIABLE"
327
+ and len(args) > 2
328
+ and isinstance(args[2], S.Name)
329
+ and _snmp_mismatch(typer, args[0], args[2].name)
330
+ ):
331
+ message = "Miss match of types"
332
+ elif (
333
+ kw == "MENURECOMPILE"
334
+ and len(args) > 1
335
+ and isinstance(args[1], (S.Name, S.Index))
336
+ and typer.of(args[1]) == "STRING"
337
+ ):
338
+ message = "String type variable not allowed."
339
+ elif (
340
+ kw == "SDMGENERIC"
341
+ and len(args) > 6
342
+ and typer.of(args[0]) == typer.of(args[4]) == "STRING"
343
+ and consteval.is_constant(args[6], env)
344
+ ):
345
+ try:
346
+ bytes_per_value = consteval.value_of(args[6], env)
347
+ except consteval.NotConstant:
348
+ bytes_per_value = None
349
+ if bytes_per_value != 1:
350
+ message = "If source and destination are of type STRING, bytes per value must be 1"
351
+ for text in (message, extra):
352
+ if text:
353
+ diags.error(stmt.line, text, stmt.file)
354
+
355
+
356
+ def _array_size(typer: _Typer, dims: dict[str, tuple[int, ...]], expr: S.Expr | None) -> int | None:
357
+ """Element count of the variable behind `expr`; 1 for an expression that is no variable."""
358
+ if isinstance(expr, (S.Name, S.Index)) and (variable := typer.variable(expr)) is not None:
359
+ shape = dims.get(variable)
360
+ return None if shape is None else math.prod(shape)
361
+ return 1
362
+
363
+
364
+ _CTYPE_NAMES = frozenset({"LONG", "FLOAT", "DOUBLE", "STRING"})
365
+
366
+
367
+ def _ctype_illegal(node: S.Index, env, legal: frozenset, known: set | None) -> bool:
368
+ """`CType(x)` (no type), or a type that is neither `Long`/`Float`/`Double`/`String` nor a
369
+ constant equal to the logger's `UINT1`/`IEEE4`/`IEEE8` code: `Illegal type`, a pass-1 finding
370
+ (oracle, all four loggers, round 41: `Boolean`, `FP2`, `2`, `1.5`, a variable, a string or an
371
+ undefined name; with `known`, CR6/CR1000X, also an undefined first argument)."""
372
+ args = node.args
373
+ if None in args:
374
+ return False
375
+ if len(args) == 1:
376
+ return True
377
+ if len(args) != 2:
378
+ return False
379
+ kind = args[1]
380
+ from . import expressions
381
+
382
+ if known is not None and expressions._undefined_names(args[0], known):
383
+ return True
384
+ if isinstance(kind, S.Name) and kind.name.upper() in _CTYPE_NAMES:
385
+ return False
386
+ try:
387
+ value = consteval.value_of(kind, env)
388
+ except consteval.NotConstant:
389
+ return True
390
+ return isinstance(value, (str, bool)) or value != int(value) or int(value) not in legal
391
+
392
+
393
+ def check_ctype(program: S.Program, profile, diags: Diagnostics) -> None:
394
+ if "CTYPE" not in profile.functions:
395
+ return
396
+ env = consteval.Env(profile, dict(program.consts))
397
+ legal = frozenset(v for n in ("UINT1", "IEEE4", "IEEE8") if isinstance(v := _symbol_value(env, n), int))
398
+ from . import expressions
399
+
400
+ known = None if profile.single_pass else expressions._known_names(program, profile)
401
+ for stmt in program.statements:
402
+ for top in _statement_expressions(stmt):
403
+ for node in _subexpressions(top):
404
+ if (
405
+ isinstance(node, S.Index)
406
+ and node.name.upper() == "CTYPE"
407
+ and _ctype_illegal(node, env, legal, known)
408
+ ):
409
+ diags.error(stmt.line, "Illegal type", stmt.file)
410
+
411
+
412
+ def _symbol_value(env, name: str):
413
+ try:
414
+ return consteval.value_of(S.Name(0, name=name), env)
415
+ except consteval.NotConstant:
416
+ return None
417
+
418
+
419
+ # Instructions whose arguments are plain expressions: the probed ones. `Scan`, `CallTable`,
420
+ # `Select`/`Case` and the table declarations report other errors for the same argument.
421
+ _POINTER_ARG_INSTRUCTIONS = frozenset({"DELAY", "MOVE", "PORTSET", "RANDOMIZE", "WHILE"})
422
+
423
+ POINTER_MESSAGE = "{name} must be declared as a pointer type, e.e., AS Float!, Long!, Double!, String!,..."
424
+
425
+
426
+ def _dereference_names(top: S.Expr | None):
427
+ """Pre-order `!operand` nodes of an expression: the operand's name, or None when the operand is
428
+ not a plain name / array element."""
429
+ for node in _subexpressions(top):
430
+ if isinstance(node, S.Unary) and node.op == "!":
431
+ operand = node.operand
432
+ yield operand.name if isinstance(operand, (S.Name, S.Index)) else None
433
+
434
+
435
+ def _routine_scopes(program: S.Program):
436
+ """`stmt id -> {UPPER name: (declared name, type)}` for the variables visible at a statement:
437
+ the globals, then the statement's own routine's locals and parameters (a local shadows)."""
438
+
439
+ def declarations(stmts):
440
+ out: dict[str, tuple[str, str]] = {}
441
+ clash: set[str] = set()
442
+ for st in stmts:
443
+ if isinstance(st, S.Declaration) and st.kind in ("PUBLIC", "DIM", "STATIC"):
444
+ for v in st.vars:
445
+ kind = "FLOAT" if v.paren_dim else (v.type or "FLOAT").upper()
446
+ key = v.name.upper()
447
+ if out.setdefault(key, (v.name, kind))[1] != kind:
448
+ clash.add(key)
449
+ return {k: v for k, v in out.items() if k not in clash}
450
+
451
+ groups: list[tuple[S.SubDef | None, list]] = [(None, [])]
452
+ for st in program.statements:
453
+ if isinstance(st, S.SubDef):
454
+ groups.append((st, []))
455
+ groups[-1][1].append(st)
456
+ if S.ends_routine(st):
457
+ groups.append((None, []))
458
+ glob = declarations(st for routine, stmts in groups if routine is None for st in stmts)
459
+ scopes: dict[int, dict] = {}
460
+ for routine, stmts in groups:
461
+ if routine is None:
462
+ scope = glob
463
+ else:
464
+ scope = dict(glob)
465
+ scope.update(declarations(stmts))
466
+ for prm in routine.params:
467
+ scope[prm.name.upper()] = (prm.name, (prm.type or "FLOAT").upper())
468
+ if routine.keyword.upper() == "FUNCTION":
469
+ scope.setdefault(routine.name.upper(), (routine.name, ""))
470
+ for st in stmts:
471
+ scopes[id(st)] = scope
472
+ return scopes
473
+
474
+
475
+ def check_pointer_deref(program: S.Program, profile, diags: Diagnostics) -> None:
476
+ """CR6/CR1000X: `!name` on a declared variable that is not of a pointer type (`As Long!`).
477
+ Rules and oracle evidence: `docs/checker.md` "Round 14: `!name` on a non-pointer variable"."""
478
+ if profile.single_pass:
479
+ return
480
+ from . import expressions, structure
481
+
482
+ scopes = _routine_scopes(program)
483
+
484
+ def offender(scope, tops):
485
+ """Declared name of the first `!` operand without a pointer type, or None."""
486
+ for top in tops:
487
+ for operand in _dereference_names(top):
488
+ entry = scope.get(operand.upper()) if operand else None
489
+ if entry is None or not entry[1]:
490
+ return None
491
+ if not entry[1].endswith("!"):
492
+ return entry[0]
493
+ return None
494
+
495
+ known = expressions._known_names(program, profile)
496
+ for stmt in program.statements:
497
+ scope = scopes[id(stmt)]
498
+ derefs = [
499
+ n if n and re.search(rf"!\s*{re.escape(n)}\b", stmt.text or "", re.I) else None
500
+ for top in expressions._stmt_exprs(stmt)
501
+ for n in _dereference_names(top)
502
+ ]
503
+ if any(n and n.upper() not in scope and n.upper() not in known for n in derefs):
504
+ # `!` on an undeclared name, once per statement; an assignment to a non-pointer whose
505
+ # first `!` operand is that name also gets the pointer message (oracle, CR6/CR1000X,
506
+ # round 59, docs/checker.md#dereferencing-an-undeclared-name-round-59)
507
+ diags.error(stmt.line, "Illegal ! operation", stmt.file)
508
+ entry = (
509
+ scope.get(stmt.target.name.upper())
510
+ if isinstance(stmt, S.Assignment) and isinstance(stmt.target, (S.Name, S.Index))
511
+ else None
512
+ )
513
+ if (
514
+ entry
515
+ and entry[1]
516
+ and not entry[1].endswith("!")
517
+ and derefs[0]
518
+ and derefs[0].upper() not in scope
519
+ and derefs[0].upper() not in known
520
+ ):
521
+ diags.error(stmt.line, POINTER_MESSAGE.format(name=entry[0]), stmt.file)
522
+ diags.error(stmt.line, structure._out_of_place(profile, stmt.text, stmt.at_eol), stmt.file)
523
+ continue
524
+ name = echo = None
525
+ if isinstance(stmt, S.Assignment):
526
+ target = stmt.target
527
+ base = target.operand if isinstance(target, S.Unary) and target.op == "!" else target
528
+ if not isinstance(base, (S.Name, S.Index)) or base.name.upper() not in scope:
529
+ continue
530
+ name = offender(scope, [target, stmt.value])
531
+ echo = structure._out_of_place(profile, stmt.text, stmt.at_eol)
532
+ elif isinstance(stmt, S.Unparsed):
533
+ if (
534
+ (m := re.fullmatch(r"\s*!\s*(\w+)(?:\s+\w+)?\s*", stmt.text or ""))
535
+ and (entry := scope.get(m.group(1).upper()))
536
+ and entry[1]
537
+ and not entry[1].endswith("!")
538
+ ):
539
+ name, echo = entry[0], structure._out_of_place(profile, stmt.text, stmt.at_eol)
540
+ elif isinstance(stmt, (S.If, S.ElseIf)):
541
+ text = expressions._if_condition_text(stmt)
542
+ if text is None:
543
+ continue
544
+ name = offender(scope, [stmt.cond])
545
+ echo = f"Invalid, or out of place expression: {text}."
546
+ elif isinstance(stmt, S.Call) or (
547
+ isinstance(stmt, S.Instruction) and stmt.keyword.upper() in _POINTER_ARG_INSTRUCTIONS
548
+ ):
549
+ for i, arg in enumerate(stmt.args):
550
+ if (found := offender(scope, [arg])) is not None:
551
+ name = found
552
+ echo = (
553
+ "Invalid, or out of place expression: "
554
+ + (
555
+ expressions._arg_text(stmt, i)
556
+ if isinstance(stmt, S.Instruction)
557
+ else expressions._call_arg_text(stmt.text, i)
558
+ )
559
+ + "."
560
+ )
561
+ break
562
+ if name is not None and echo is not None and re.search(rf"!\s*{re.escape(name)}\b", stmt.text or "", re.I):
563
+ diags.error(stmt.line, POINTER_MESSAGE.format(name=name), stmt.file)
564
+ diags.error(stmt.line, echo, stmt.file)