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
|
@@ -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)
|