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/bindings.py
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
"""Binding index: declarations, references and their resolution over a parsed program.
|
|
2
|
+
|
|
3
|
+
Read-only. `build(program, profile)` walks the AST that `analysis.analyze` returns
|
|
4
|
+
(`AnalysisResult.program`; also `parser.parse_file`) and records every declaration and every name
|
|
5
|
+
use with the scope rules the compiler fixtures establish. It changes no compiler behaviour and
|
|
6
|
+
no checker uses it. Policies, evidence and limits: `docs/lsp-api.md` "Binding index (LSP-05a)".
|
|
7
|
+
|
|
8
|
+
A name is never resolved by searching source text. Resolution works on AST names; the only
|
|
9
|
+
re-lexing is `name_tokens`/`token_of`, which tokenizes a statement's own text so that LSP-05b can
|
|
10
|
+
find a record's column (string tokens are never name tokens).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import NamedTuple
|
|
18
|
+
|
|
19
|
+
from . import consteval, lexer, profiles, tables
|
|
20
|
+
from . import syntax as S
|
|
21
|
+
from .catalog import CatalogEntry, InstructionCatalog
|
|
22
|
+
|
|
23
|
+
# Reference roles (as far as the AST tells; see docs/lsp-api.md).
|
|
24
|
+
READ, WRITE, CALL, MEMBER_BASE, MEMBER = "read", "write", "call", "member_base", "member"
|
|
25
|
+
ALIAS_TARGET, UNITS_TARGET, TABLE_REF, ARGUMENT = "alias_target", "units_target", "table_ref", "argument"
|
|
26
|
+
TYPE, INSTRUCTION = "type", "instruction"
|
|
27
|
+
|
|
28
|
+
# Names the parser/expression layer treats as predefined although no extracted table lists them
|
|
29
|
+
# (checker/expressions.py `_EXTRA_PREDEFINED_SYMBOLS`).
|
|
30
|
+
_EXTRA_PREDEFINED = ("NAN", "INF")
|
|
31
|
+
# System tables: addressable as `Table.Field` in every program (checker/symbols.py).
|
|
32
|
+
_SYSTEM_TABLES = ("STATUS", "PUBLIC", "SETTINGS")
|
|
33
|
+
_TABLE_KINDS = ("datatable", "consttable")
|
|
34
|
+
_STRUCT_KINDS = ("structuretype", "objecttype")
|
|
35
|
+
_ROUTINE_KINDS = ("sub", "function")
|
|
36
|
+
_VARIABLE_KINDS = ("public", "dim", "static")
|
|
37
|
+
_TABLE_PROPERTIES = {n.upper(): n for n in tables.TABLE_PROPERTIES}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(eq=False)
|
|
41
|
+
class Declaration:
|
|
42
|
+
"""One declared name. Identity (not value) distinguishes declarations: the same spelling in
|
|
43
|
+
two routines, two includes of one file, or two statements gives distinct objects."""
|
|
44
|
+
|
|
45
|
+
name: str # source spelling
|
|
46
|
+
kind: str # public dim static const sub function param alias datatable
|
|
47
|
+
# consttable structuretype objecttype structure_member label
|
|
48
|
+
# field system_table external
|
|
49
|
+
scope: str # "global" | "routine" | "structure" | "table" | "predefined"
|
|
50
|
+
routine: S.SubDef | None # the owning Sub/Function (scope "routine"; params too)
|
|
51
|
+
container: Declaration | None # StructureType owning a member; ConstTable owning a Const;
|
|
52
|
+
# DataTable/system table owning a `field`
|
|
53
|
+
file: str # compiler display name
|
|
54
|
+
line: int # compiler-reported line
|
|
55
|
+
node: object # VarDecl | ConstDecl | Param | SubDef | Instruction | Label
|
|
56
|
+
stmt: S.Stmt | None # innermost statement holding the declaration
|
|
57
|
+
unit: S.Stmt | None # outermost statement of the source segment (owns the text)
|
|
58
|
+
index: int # position of `unit` in `program.statements` (-1: predefined)
|
|
59
|
+
occurrence: int | None = None # nth same-name token of `unit.text` (see `token_of`)
|
|
60
|
+
type: str | None = None # upper-case declared type, if any
|
|
61
|
+
quoted: bool = False # a DataTable/ConstTable name written as a string
|
|
62
|
+
external: CatalogEntry | None = None
|
|
63
|
+
alias_target: Reference | None = None
|
|
64
|
+
unverified_scope: bool = False # scope inside a routine is not established by evidence
|
|
65
|
+
tok_key: tuple = ()
|
|
66
|
+
modelled: bool = False # table/system_table: every field name is known (`fields_of`)
|
|
67
|
+
members: dict | None = None # StructureType: upper member name -> [Declaration]
|
|
68
|
+
site: str = "" # `field`: "fieldnames" (the FieldNames string token is the
|
|
69
|
+
# site, `quoted`), "instruction" (name derived from the Source;
|
|
70
|
+
# only stmt/line locate it, no token) or "property" (a
|
|
71
|
+
# `Table.Output`-style member, predefined)
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"Declaration({self.kind} {self.name!r} {self.file}:{self.line} {self.scope})"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(eq=False)
|
|
78
|
+
class Reference:
|
|
79
|
+
name: str # source spelling
|
|
80
|
+
role: str
|
|
81
|
+
file: str
|
|
82
|
+
line: int
|
|
83
|
+
resolved: tuple = () # candidate declarations; () unresolved; >1 ambiguous
|
|
84
|
+
ambiguity: str = "" # non-empty: why the candidates are not a proven single target
|
|
85
|
+
container: tuple = () # member reference: the table/structure declarations searched
|
|
86
|
+
routine: S.SubDef | None = None # routine the reference occurs in
|
|
87
|
+
node: object = None # Name | Index | Member | Call | Instruction | Param | VarDecl
|
|
88
|
+
stmt: S.Stmt | None = None
|
|
89
|
+
unit: S.Stmt | None = None
|
|
90
|
+
index: int = 0
|
|
91
|
+
occurrence: int | None = None
|
|
92
|
+
base: Reference | None = None # member reference: the reference of its base
|
|
93
|
+
is_write: bool = False
|
|
94
|
+
forward: bool = False # every candidate is declared in a later statement
|
|
95
|
+
tok_key: tuple = ()
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def status(self) -> str:
|
|
99
|
+
if self.ambiguity or len(self.resolved) > 1:
|
|
100
|
+
return "ambiguous"
|
|
101
|
+
if self.resolved:
|
|
102
|
+
return "resolved"
|
|
103
|
+
if self.role == MEMBER and any(
|
|
104
|
+
c.kind in ("datatable", "system_table") and not c.modelled for c in self.container
|
|
105
|
+
):
|
|
106
|
+
return "unmodelled" # a table whose field names the index cannot list
|
|
107
|
+
return "unresolved"
|
|
108
|
+
|
|
109
|
+
def __repr__(self) -> str:
|
|
110
|
+
return f"Reference({self.role} {self.name!r} {self.file}:{self.line} {self.status})"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class IndexFailure:
|
|
115
|
+
index: int # statement position, -1 for the whole build
|
|
116
|
+
file: str
|
|
117
|
+
line: int
|
|
118
|
+
message: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Resolution(NamedTuple):
|
|
122
|
+
candidates: tuple
|
|
123
|
+
ambiguity: str
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BindingIndex:
|
|
127
|
+
def __init__(self, profile: profiles.Profile):
|
|
128
|
+
self.profile = profile
|
|
129
|
+
self.declarations: tuple[Declaration, ...] = ()
|
|
130
|
+
self.references: tuple[Reference, ...] = ()
|
|
131
|
+
self.failures: tuple[IndexFailure, ...] = ()
|
|
132
|
+
self.unparsed: tuple[S.Unparsed, ...] = () # statements the parser could not take apart
|
|
133
|
+
self._by_name: dict[str, list[Declaration]] = {}
|
|
134
|
+
self._refs_to: dict[int, list[Reference]] = {}
|
|
135
|
+
self._by_unit: dict[int, list] = {}
|
|
136
|
+
self._glob: dict[str, list[Declaration]] = {}
|
|
137
|
+
self._local: dict[int, dict[str, list[Declaration]]] = {}
|
|
138
|
+
self._loose: dict[str, list[Declaration]] = {}
|
|
139
|
+
self._catalog = InstructionCatalog.for_profile(profile.name)
|
|
140
|
+
self._external: dict[tuple, Declaration] = {}
|
|
141
|
+
self._system: dict[str, Declaration] = {}
|
|
142
|
+
self._tokens: dict[int, list | None] = {}
|
|
143
|
+
self._fields: dict[int, list[Declaration]] = {} # id(DataTable decl) -> fields, in order
|
|
144
|
+
self._field_names: dict[tuple, list[Declaration]] = {} # (id(table), UPPER name) -> fields
|
|
145
|
+
self._sys_names: dict[str, dict[str, str] | None] = {} # system table -> upper -> spelling
|
|
146
|
+
self._sys_fields: dict[tuple, Declaration] = {}
|
|
147
|
+
self._slow_seq = 0
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------ queries
|
|
150
|
+
|
|
151
|
+
def fields_of(self, table: Declaration) -> tuple[Declaration, ...]:
|
|
152
|
+
"""The fields of a user DataTable declaration (kind `field`, in TDF order) as
|
|
153
|
+
`tables.py` derives them. Fields are not in `declarations_named`; `()` for any other
|
|
154
|
+
declaration or a table whose fields could not be derived (`table.modelled` is False)."""
|
|
155
|
+
return tuple(self._fields.get(id(table), ()))
|
|
156
|
+
|
|
157
|
+
def declarations_named(self, name: str) -> tuple[Declaration, ...]:
|
|
158
|
+
return tuple(self._by_name.get(name.upper(), ()))
|
|
159
|
+
|
|
160
|
+
def references_to(self, decl: Declaration) -> tuple[Reference, ...]:
|
|
161
|
+
"""References that list `decl` among their candidates (ambiguous ones included)."""
|
|
162
|
+
return tuple(self._refs_to.get(id(decl), ()))
|
|
163
|
+
|
|
164
|
+
def records_in(self, stmt: S.Stmt) -> tuple:
|
|
165
|
+
"""Declarations and references, in source order, of one top-level statement."""
|
|
166
|
+
return tuple(self._by_unit.get(id(stmt), ()))
|
|
167
|
+
|
|
168
|
+
def counts(self) -> dict:
|
|
169
|
+
refs = defaultdict(int)
|
|
170
|
+
for r in self.references:
|
|
171
|
+
refs[r.status] += 1
|
|
172
|
+
kinds = defaultdict(int)
|
|
173
|
+
for d in self.declarations:
|
|
174
|
+
kinds[d.kind] += 1
|
|
175
|
+
return {
|
|
176
|
+
"declarations": dict(kinds),
|
|
177
|
+
"references": dict(refs),
|
|
178
|
+
"failures": len(self.failures),
|
|
179
|
+
"unparsed": len(self.unparsed),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def lookup(self, name: str, routine: S.SubDef | None = None, role: str = READ) -> Resolution:
|
|
183
|
+
"""Candidates for a name used in `routine` (None: outside every routine)."""
|
|
184
|
+
return self._resolve_name(name.upper(), routine, role)
|
|
185
|
+
|
|
186
|
+
def token_of(self, record: Declaration | Reference) -> lexer.Token | None:
|
|
187
|
+
"""The lexer token (a `col` into `record.unit.text`) of the record's name, or None when the
|
|
188
|
+
statement text cannot be tokenized or has fewer such tokens than `occurrence` says."""
|
|
189
|
+
if record.unit is None or record.occurrence is None:
|
|
190
|
+
return None
|
|
191
|
+
toks = self._unit_tokens(record.unit)
|
|
192
|
+
if toks is None:
|
|
193
|
+
return None
|
|
194
|
+
kind = record.tok_key[1]
|
|
195
|
+
pool = [t for t, k in toks if k == kind and _tok_name(t, k) == record.tok_key[0]]
|
|
196
|
+
return pool[record.occurrence] if record.occurrence < len(pool) else None
|
|
197
|
+
|
|
198
|
+
def _unit_tokens(self, unit: S.Stmt):
|
|
199
|
+
key = id(unit)
|
|
200
|
+
if key not in self._tokens:
|
|
201
|
+
self._tokens[key] = classified_tokens(unit)
|
|
202
|
+
return self._tokens[key]
|
|
203
|
+
|
|
204
|
+
# --------------------------------------------------------- resolution
|
|
205
|
+
|
|
206
|
+
def _ext(self, entry: CatalogEntry) -> Declaration:
|
|
207
|
+
key = (entry.name.upper(), entry.kind)
|
|
208
|
+
decl = self._external.get(key)
|
|
209
|
+
if decl is None:
|
|
210
|
+
decl = Declaration(
|
|
211
|
+
entry.name,
|
|
212
|
+
"external",
|
|
213
|
+
"predefined",
|
|
214
|
+
None,
|
|
215
|
+
None,
|
|
216
|
+
"",
|
|
217
|
+
0,
|
|
218
|
+
None,
|
|
219
|
+
None,
|
|
220
|
+
None,
|
|
221
|
+
-1,
|
|
222
|
+
type=entry.kind,
|
|
223
|
+
external=entry,
|
|
224
|
+
)
|
|
225
|
+
self._external[key] = decl
|
|
226
|
+
return decl
|
|
227
|
+
|
|
228
|
+
def _predefined(self, upper: str, kinds: tuple[str, ...]) -> tuple[Declaration, ...]:
|
|
229
|
+
for kind in kinds:
|
|
230
|
+
entry = self._catalog.get(upper, kind)
|
|
231
|
+
if entry is not None:
|
|
232
|
+
return (self._ext(entry),)
|
|
233
|
+
if upper in _EXTRA_PREDEFINED and "symbol" in kinds:
|
|
234
|
+
entry = CatalogEntry(upper, "symbol", self.profile.name, "parser:extra-predefined")
|
|
235
|
+
return (self._ext(entry),)
|
|
236
|
+
return ()
|
|
237
|
+
|
|
238
|
+
def _system_table(self, upper: str) -> Declaration | None:
|
|
239
|
+
if upper == "DATATABLEINFO" and not any(d.kind == "datatable" for d in self.declarations):
|
|
240
|
+
return None
|
|
241
|
+
if upper not in _SYSTEM_TABLES + ("DATATABLEINFO",):
|
|
242
|
+
return None
|
|
243
|
+
decl = self._system.get(upper)
|
|
244
|
+
if decl is None:
|
|
245
|
+
decl = Declaration(
|
|
246
|
+
upper.capitalize() if upper != "DATATABLEINFO" else "DataTableInfo",
|
|
247
|
+
"system_table",
|
|
248
|
+
"predefined",
|
|
249
|
+
None,
|
|
250
|
+
None,
|
|
251
|
+
"",
|
|
252
|
+
0,
|
|
253
|
+
None,
|
|
254
|
+
None,
|
|
255
|
+
None,
|
|
256
|
+
-1,
|
|
257
|
+
)
|
|
258
|
+
self._system[upper] = decl
|
|
259
|
+
return decl
|
|
260
|
+
|
|
261
|
+
def _resolve_name(self, upper: str, routine, role: str) -> Resolution:
|
|
262
|
+
profile = self.profile
|
|
263
|
+
if role == INSTRUCTION:
|
|
264
|
+
# The parser made this token an instruction: only the catalog answers, never a user
|
|
265
|
+
# declaration of the same spelling (docs/lsp-api.md, "Scope policies and evidence").
|
|
266
|
+
return Resolution(self._predefined(upper, ("instruction", "keyword")), "")
|
|
267
|
+
locals_ = self._local.get(id(routine), {}).get(upper, []) if routine is not None else []
|
|
268
|
+
if locals_:
|
|
269
|
+
if len(locals_) > 1:
|
|
270
|
+
return Resolution(tuple(locals_), "duplicate declarations in one routine")
|
|
271
|
+
local = locals_[0]
|
|
272
|
+
if profile.name in ("cr300", "cr350") and upper in profile.functions and local.kind in ("dim", "param"):
|
|
273
|
+
fn = self._predefined(upper, ("function",))
|
|
274
|
+
if fn:
|
|
275
|
+
return Resolution(
|
|
276
|
+
fn + (local,),
|
|
277
|
+
f"{profile.name}: a local Dim/parameter does not shadow a "
|
|
278
|
+
"built-in function (docs/checker.md, function-named "
|
|
279
|
+
"assignment targets)",
|
|
280
|
+
)
|
|
281
|
+
return Resolution((local,), "")
|
|
282
|
+
glob = self._glob.get(upper, [])
|
|
283
|
+
if glob:
|
|
284
|
+
if len(glob) > 1:
|
|
285
|
+
return Resolution(tuple(glob), _duplicate_reason(glob))
|
|
286
|
+
return Resolution((glob[0],), "")
|
|
287
|
+
loose = self._loose.get(upper, [])
|
|
288
|
+
if loose:
|
|
289
|
+
return Resolution(
|
|
290
|
+
tuple(loose),
|
|
291
|
+
"declared inside a routine; its visibility outside that routine is not established by evidence",
|
|
292
|
+
)
|
|
293
|
+
if role == CALL:
|
|
294
|
+
kinds = ("instruction", "function", "symbol")
|
|
295
|
+
elif role == TYPE:
|
|
296
|
+
kinds = ("symbol",)
|
|
297
|
+
else:
|
|
298
|
+
kinds = ("symbol", "function")
|
|
299
|
+
pre = self._predefined(upper, kinds)
|
|
300
|
+
if not pre and role == ARGUMENT:
|
|
301
|
+
pre = self._predefined(upper, ("instruction",))
|
|
302
|
+
return Resolution(pre, "")
|
|
303
|
+
|
|
304
|
+
def _members_of(self, cand: Declaration, member_upper: str):
|
|
305
|
+
"""(containers, member declarations, ambiguity note) for one base candidate."""
|
|
306
|
+
if cand.kind == "consttable":
|
|
307
|
+
found = [d for d in self._glob.get(member_upper, []) if d.container is cand]
|
|
308
|
+
return [cand], found
|
|
309
|
+
if cand.kind == "system_table":
|
|
310
|
+
if cand.name.upper() == "PUBLIC":
|
|
311
|
+
found = [d for d in self._glob.get(member_upper, []) if d.kind in ("public", "alias")]
|
|
312
|
+
return [cand], found
|
|
313
|
+
sysf = self._system_field(cand, member_upper) or self._table_property(cand, member_upper)
|
|
314
|
+
return [cand], [sysf] if sysf is not None else []
|
|
315
|
+
if cand.kind == "datatable":
|
|
316
|
+
found = list(self._field_names.get((id(cand), member_upper), ()))
|
|
317
|
+
if not found:
|
|
318
|
+
prop = self._table_property(cand, member_upper)
|
|
319
|
+
found = [prop] if prop is not None else []
|
|
320
|
+
return [cand], found
|
|
321
|
+
if cand.kind in _TABLE_KINDS:
|
|
322
|
+
return [cand], []
|
|
323
|
+
tname = (cand.type or "").rstrip("!")
|
|
324
|
+
struct = next((d for d in self._glob.get(tname, []) if d.kind in _STRUCT_KINDS), None)
|
|
325
|
+
if struct is not None and struct.members is not None:
|
|
326
|
+
return [struct], list(struct.members.get(member_upper, []))
|
|
327
|
+
return [], []
|
|
328
|
+
|
|
329
|
+
def _system_names(self, table_upper: str) -> dict[str, str] | None:
|
|
330
|
+
"""Upper -> spelling of the fields the profile data lists for a system table, or None.
|
|
331
|
+
Status and DataTableInfo come from the CR6 TDF derivation of `tables.py` (CR6 only);
|
|
332
|
+
Settings from the profile's `settings_fields.json` `settings` list. A name outside the
|
|
333
|
+
list stays `unmodelled`: the lists are not proven complete (the project uses
|
|
334
|
+
`Status.CardBytesFree`, which is in neither Status list)."""
|
|
335
|
+
if table_upper not in self._sys_names:
|
|
336
|
+
names = None
|
|
337
|
+
try:
|
|
338
|
+
if table_upper == "SETTINGS":
|
|
339
|
+
names = {n.upper(): n for n in self.profile.settings_fields["settings"]}
|
|
340
|
+
elif self.profile.name == "cr6" and table_upper == "STATUS":
|
|
341
|
+
names = {f.name.upper(): f.name for f in tables._status_table(self._slow_seq).fields}
|
|
342
|
+
for n in self.profile.settings_fields["status"]:
|
|
343
|
+
names.setdefault(n.upper(), n)
|
|
344
|
+
elif self.profile.name == "cr6" and table_upper == "DATATABLEINFO":
|
|
345
|
+
names = {f.name.upper(): f.name for f in tables._datatableinfo_table(1).fields}
|
|
346
|
+
except Exception:
|
|
347
|
+
names = None
|
|
348
|
+
self._sys_names[table_upper] = names
|
|
349
|
+
return self._sys_names[table_upper]
|
|
350
|
+
|
|
351
|
+
def _table_property(self, table: Declaration, member_upper: str) -> Declaration | None:
|
|
352
|
+
if member_upper not in _TABLE_PROPERTIES or table.name.upper() in ("SETTINGS", "PUBLIC"):
|
|
353
|
+
return None
|
|
354
|
+
key = (id(table), member_upper)
|
|
355
|
+
decl = self._sys_fields.get(key)
|
|
356
|
+
if decl is None:
|
|
357
|
+
decl = Declaration(
|
|
358
|
+
_TABLE_PROPERTIES[member_upper],
|
|
359
|
+
"field",
|
|
360
|
+
"predefined",
|
|
361
|
+
None,
|
|
362
|
+
table,
|
|
363
|
+
"",
|
|
364
|
+
0,
|
|
365
|
+
None,
|
|
366
|
+
None,
|
|
367
|
+
None,
|
|
368
|
+
-1,
|
|
369
|
+
site="property",
|
|
370
|
+
)
|
|
371
|
+
self._sys_fields[key] = decl
|
|
372
|
+
return decl
|
|
373
|
+
|
|
374
|
+
def _system_field(self, table: Declaration, member_upper: str) -> Declaration | None:
|
|
375
|
+
names = self._system_names(table.name.upper())
|
|
376
|
+
if not names or member_upper not in names:
|
|
377
|
+
return None
|
|
378
|
+
key = (table.name.upper(), member_upper)
|
|
379
|
+
decl = self._sys_fields.get(key)
|
|
380
|
+
if decl is None:
|
|
381
|
+
decl = Declaration(names[member_upper], "field", "predefined", None, table, "", 0, None, None, None, -1)
|
|
382
|
+
self._sys_fields[key] = decl
|
|
383
|
+
return decl
|
|
384
|
+
|
|
385
|
+
def _resolve_member(self, ref: Reference) -> None:
|
|
386
|
+
base = ref.base
|
|
387
|
+
cands = [c for c in (base.resolved if base is not None else ()) if c.kind != "external"]
|
|
388
|
+
containers: list[Declaration] = []
|
|
389
|
+
found: list[Declaration] = []
|
|
390
|
+
for cand in cands:
|
|
391
|
+
c, f = self._members_of(cand, ref.name.upper())
|
|
392
|
+
containers += c
|
|
393
|
+
found += f
|
|
394
|
+
ref.container = tuple(containers)
|
|
395
|
+
ref.resolved = tuple(found)
|
|
396
|
+
if len(found) > 1:
|
|
397
|
+
ref.ambiguity = "duplicate member declarations"
|
|
398
|
+
elif base is not None and base.ambiguity and found:
|
|
399
|
+
ref.ambiguity = "base reference is ambiguous"
|
|
400
|
+
|
|
401
|
+
def _finish_reference(self, ref: Reference) -> None:
|
|
402
|
+
if ref.role == MEMBER:
|
|
403
|
+
self._resolve_member(ref)
|
|
404
|
+
else:
|
|
405
|
+
res = self._resolve_name(ref.name.upper(), ref.routine, ref.role)
|
|
406
|
+
if not res.candidates and ref.role == MEMBER_BASE:
|
|
407
|
+
sysd = self._system_table(ref.name.upper())
|
|
408
|
+
if sysd is not None:
|
|
409
|
+
res = Resolution((sysd,), "")
|
|
410
|
+
ref.resolved, ref.ambiguity = res.candidates, res.ambiguity
|
|
411
|
+
if (
|
|
412
|
+
ref.role in (READ, ARGUMENT)
|
|
413
|
+
and isinstance(ref.node, S.Index)
|
|
414
|
+
and ref.resolved
|
|
415
|
+
and all(_is_callable(d) for d in ref.resolved)
|
|
416
|
+
):
|
|
417
|
+
ref.role = CALL
|
|
418
|
+
if ref.role in (READ, ARGUMENT) and ref.resolved and all(d.kind in _TABLE_KINDS for d in ref.resolved):
|
|
419
|
+
ref.role = TABLE_REF
|
|
420
|
+
user = [
|
|
421
|
+
d
|
|
422
|
+
for d in ref.resolved
|
|
423
|
+
if d.index >= 0 and d.kind not in _ROUTINE_KINDS + _TABLE_KINDS and d.kind not in _STRUCT_KINDS
|
|
424
|
+
]
|
|
425
|
+
ref.forward = bool(user) and len(user) == len(ref.resolved) and all(d.index > ref.index for d in user)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _duplicate_reason(cands: list[Declaration]) -> str:
|
|
429
|
+
if len({(d.file, d.line) for d in cands}) == 1:
|
|
430
|
+
return "the same declaration is included more than once"
|
|
431
|
+
tables = [d.kind in _TABLE_KINDS for d in cands]
|
|
432
|
+
if any(tables) and not all(tables):
|
|
433
|
+
return (
|
|
434
|
+
"a table and another declaration share this spelling; whether tables and "
|
|
435
|
+
"variables are separate namespaces is not established here"
|
|
436
|
+
)
|
|
437
|
+
return "duplicate global declarations"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _is_callable(d: Declaration) -> bool:
|
|
441
|
+
if d.kind in _ROUTINE_KINDS:
|
|
442
|
+
return True
|
|
443
|
+
return d.kind == "external" and d.external is not None and d.external.kind in ("function", "instruction")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ------------------------------------------------------------- token helpers
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def closes_routine(stmt: S.Stmt) -> bool:
|
|
450
|
+
"""True when `stmt` ends a Sub or Function: `EndSub` / `EndFunction`, or the two-word `End Sub` /
|
|
451
|
+
`End Function`, which the parser gives as `Instruction(keyword="END", args=[Name("Sub")])`.
|
|
452
|
+
Shared by the binding index, hover and the routine scope they track."""
|
|
453
|
+
return S.ends_routine(stmt)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def classified_tokens(stmt: S.Stmt):
|
|
457
|
+
"""`[(token, kind)]` of `stmt.text`, kind `"id"` (identifier not after `.`), `"member"`
|
|
458
|
+
(identifier after `.`) or `"string"`; None when the text cannot be tokenized. A `Units`
|
|
459
|
+
statement is cut at its first `=` (the label is free text, as in the parser)."""
|
|
460
|
+
text = getattr(stmt, "text", "") or ""
|
|
461
|
+
if not text:
|
|
462
|
+
return None
|
|
463
|
+
if re.match(r"\s*Units\b", text, re.I):
|
|
464
|
+
eq = text.find("=")
|
|
465
|
+
if eq != -1:
|
|
466
|
+
text = text[:eq]
|
|
467
|
+
try:
|
|
468
|
+
toks = lexer.tokenize(text, stmt.line)
|
|
469
|
+
except lexer.LexError:
|
|
470
|
+
return None
|
|
471
|
+
out = []
|
|
472
|
+
prev = None
|
|
473
|
+
for t in toks:
|
|
474
|
+
if t.kind == "IDENT":
|
|
475
|
+
out.append((t, "member" if prev is not None and prev.kind == "OP" and prev.text == "." else "id"))
|
|
476
|
+
elif t.kind == "STRING":
|
|
477
|
+
out.append((t, "string"))
|
|
478
|
+
prev = t
|
|
479
|
+
return out
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _alias_junk(st: S.Instruction, n_items: int) -> list[list[tuple]]:
|
|
483
|
+
"""Per right-hand item of `Alias A = X junk, Y` the `(name, kind)` keys of the identifier and
|
|
484
|
+
string tokens after its name (and index) that the parser skips; the last list holds those of
|
|
485
|
+
every item after the parsed ones. Empty lists when the text cannot be tokenized."""
|
|
486
|
+
empty: list[list[tuple]] = [[] for _ in range(n_items + 1)]
|
|
487
|
+
try:
|
|
488
|
+
toks = lexer.tokenize(st.text or "", st.line)
|
|
489
|
+
except lexer.LexError:
|
|
490
|
+
return empty
|
|
491
|
+
depth, items, cur, prev, seen_eq = 0, [], [], None, False
|
|
492
|
+
for t in toks:
|
|
493
|
+
op = t.kind == "OP"
|
|
494
|
+
if op and t.text == "(":
|
|
495
|
+
depth += 1
|
|
496
|
+
elif op and t.text == ")":
|
|
497
|
+
depth = max(0, depth - 1)
|
|
498
|
+
if not seen_eq:
|
|
499
|
+
seen_eq = op and t.text == "=" and depth == 0
|
|
500
|
+
elif op and t.text == "," and depth == 0:
|
|
501
|
+
items.append(cur)
|
|
502
|
+
cur = []
|
|
503
|
+
else:
|
|
504
|
+
cur.append((t, prev))
|
|
505
|
+
prev = t
|
|
506
|
+
items.append(cur)
|
|
507
|
+
out: list[list[tuple]] = []
|
|
508
|
+
for j, item in enumerate(items):
|
|
509
|
+
skip = 0 if j >= n_items else 1
|
|
510
|
+
if skip and len(item) > 1 and item[1][0].kind == "OP" and item[1][0].text == "(":
|
|
511
|
+
d, skip = 0, len(item)
|
|
512
|
+
for idx in range(1, len(item)):
|
|
513
|
+
t = item[idx][0]
|
|
514
|
+
d += (t.text == "(") - (t.text == ")") if t.kind == "OP" else 0
|
|
515
|
+
if d == 0:
|
|
516
|
+
skip = idx + 1
|
|
517
|
+
break
|
|
518
|
+
keys = []
|
|
519
|
+
for t, pv in item[skip:]:
|
|
520
|
+
if t.kind == "IDENT":
|
|
521
|
+
k = "member" if pv is not None and pv.kind == "OP" and pv.text == "." else "id"
|
|
522
|
+
elif t.kind == "STRING":
|
|
523
|
+
k = "string"
|
|
524
|
+
else:
|
|
525
|
+
continue
|
|
526
|
+
keys.append((_tok_name(t, k), k))
|
|
527
|
+
out.append(keys)
|
|
528
|
+
tail = [k for keys in out[n_items:] for k in keys]
|
|
529
|
+
return (out[:n_items] + [[] for _ in range(max(0, n_items - len(out)))])[:n_items] + [tail]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def skipped_keys(unit: S.Stmt) -> list[tuple]:
|
|
533
|
+
"""Keys of the tokens of `unit` the index counts but has no record for (see `_alias_junk`)."""
|
|
534
|
+
if isinstance(unit, S.Instruction) and unit.keyword.upper() == "ALIAS":
|
|
535
|
+
return [k for keys in _alias_junk(unit, len(unit.args) - 1) for k in keys]
|
|
536
|
+
return []
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _tok_name(t: lexer.Token, kind: str) -> str:
|
|
540
|
+
return str(t.value).upper() if kind == "string" else t.upper
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def name_tokens(stmt: S.Stmt, name: str, *, member: bool = False, string: bool = False) -> list:
|
|
544
|
+
"""Tokens of `stmt.text` spelling `name` (case-insensitive): identifiers not after `.` by
|
|
545
|
+
default, identifiers after `.` with `member`, string tokens whose decoded value is `name` with
|
|
546
|
+
`string`. Columns are offsets into `stmt.text`; add the statement's offset in the logical line
|
|
547
|
+
(`segment_starts`) for a logical column. String contents are never matched as identifiers."""
|
|
548
|
+
toks = classified_tokens(stmt)
|
|
549
|
+
if toks is None:
|
|
550
|
+
return []
|
|
551
|
+
kind = "string" if string else ("member" if member else "id")
|
|
552
|
+
up = name.upper()
|
|
553
|
+
return [t for t, k in toks if k == kind and _tok_name(t, k) == up]
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def segment_starts(logical_text: str) -> list[tuple[int, str]]:
|
|
557
|
+
"""`(offset, segment text)` of the top-level `:`-separated segments of a logical line, as the
|
|
558
|
+
parser splits them. `Stmt.text` of a segment's statement equals the segment text (stripped
|
|
559
|
+
for `Units`); an inline `If` body's text is a slice of its segment."""
|
|
560
|
+
from .parser import _split_text_segments
|
|
561
|
+
|
|
562
|
+
out, pos = [], 0
|
|
563
|
+
for seg in _split_text_segments(logical_text):
|
|
564
|
+
out.append((pos, seg))
|
|
565
|
+
pos += len(seg) + 1
|
|
566
|
+
return out
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ------------------------------------------------------------------- builder
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class _Builder:
|
|
573
|
+
def __init__(self, index: BindingIndex):
|
|
574
|
+
self.ix = index
|
|
575
|
+
self.decls: list[Declaration] = []
|
|
576
|
+
self.refs: list[Reference] = []
|
|
577
|
+
self.order: list[tuple] = [] # (unit, record) in emission order
|
|
578
|
+
self.routine: S.SubDef | None = None
|
|
579
|
+
self.struct: Declaration | None = None
|
|
580
|
+
self.consttable: Declaration | None = None
|
|
581
|
+
self.unit: S.Stmt | None = None
|
|
582
|
+
self.idx = 0
|
|
583
|
+
self.counts: dict = {}
|
|
584
|
+
|
|
585
|
+
# -- emission --------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
def _occ(self, key: tuple) -> int:
|
|
588
|
+
n = self.counts.get(key, 0)
|
|
589
|
+
self.counts[key] = n + 1
|
|
590
|
+
return n
|
|
591
|
+
|
|
592
|
+
def _skip_tokens(self, keys) -> None:
|
|
593
|
+
"""Count the tokens the parser ignored (`Alias A = X junk, Y`), so that the ordinals of the
|
|
594
|
+
records after them still point at their own token."""
|
|
595
|
+
for key in keys:
|
|
596
|
+
self._occ(key)
|
|
597
|
+
|
|
598
|
+
def decl(
|
|
599
|
+
self,
|
|
600
|
+
stmt,
|
|
601
|
+
name,
|
|
602
|
+
kind,
|
|
603
|
+
*,
|
|
604
|
+
node=None,
|
|
605
|
+
scope=None,
|
|
606
|
+
routine="default",
|
|
607
|
+
container=None,
|
|
608
|
+
type=None,
|
|
609
|
+
quoted=False,
|
|
610
|
+
unverified=False,
|
|
611
|
+
count=True,
|
|
612
|
+
) -> Declaration:
|
|
613
|
+
if routine == "default":
|
|
614
|
+
routine = self.routine
|
|
615
|
+
if scope is None:
|
|
616
|
+
scope = "routine" if routine is not None else "global"
|
|
617
|
+
key = (name.upper(), "string" if quoted else "id")
|
|
618
|
+
owner = routine if scope == "routine" and isinstance(routine, S.SubDef) else None
|
|
619
|
+
d = Declaration(
|
|
620
|
+
name,
|
|
621
|
+
kind,
|
|
622
|
+
scope,
|
|
623
|
+
owner,
|
|
624
|
+
container,
|
|
625
|
+
stmt.file,
|
|
626
|
+
stmt.line,
|
|
627
|
+
node,
|
|
628
|
+
stmt,
|
|
629
|
+
self.unit,
|
|
630
|
+
self.idx,
|
|
631
|
+
self._occ(key) if count else None,
|
|
632
|
+
type,
|
|
633
|
+
quoted,
|
|
634
|
+
unverified_scope=unverified,
|
|
635
|
+
tok_key=key,
|
|
636
|
+
)
|
|
637
|
+
if kind in _STRUCT_KINDS:
|
|
638
|
+
d.members = {}
|
|
639
|
+
self.decls.append(d)
|
|
640
|
+
self.order.append((self.unit, d))
|
|
641
|
+
return d
|
|
642
|
+
|
|
643
|
+
def ref(self, stmt, name, role, node, *, member=False, base=None, write=False) -> Reference:
|
|
644
|
+
key = (name.upper(), "member" if member else "id")
|
|
645
|
+
r = Reference(
|
|
646
|
+
name,
|
|
647
|
+
role,
|
|
648
|
+
stmt.file,
|
|
649
|
+
stmt.line,
|
|
650
|
+
routine=self.routine,
|
|
651
|
+
node=node,
|
|
652
|
+
stmt=stmt,
|
|
653
|
+
unit=self.unit,
|
|
654
|
+
index=self.idx,
|
|
655
|
+
occurrence=self._occ(key),
|
|
656
|
+
base=base,
|
|
657
|
+
is_write=write,
|
|
658
|
+
tok_key=key,
|
|
659
|
+
)
|
|
660
|
+
self.refs.append(r)
|
|
661
|
+
self.order.append((self.unit, r))
|
|
662
|
+
return r
|
|
663
|
+
|
|
664
|
+
# -- expressions -----------------------------------------------------
|
|
665
|
+
|
|
666
|
+
def expr(self, e, stmt, role=READ, *, write=False, skip=None) -> Reference | None:
|
|
667
|
+
"""Walk `e` in source order. Returns the reference of the outermost name (the root of an
|
|
668
|
+
`a(i).b.c` chain)."""
|
|
669
|
+
if e is None or e is skip or isinstance(e, (S.Number, S.String)):
|
|
670
|
+
return None
|
|
671
|
+
if isinstance(e, S.Name):
|
|
672
|
+
return self.ref(stmt, e.name, WRITE if write else role, e, write=write)
|
|
673
|
+
if isinstance(e, S.Index):
|
|
674
|
+
r = self.ref(stmt, e.name, WRITE if write else role, e, write=write)
|
|
675
|
+
for a in e.args:
|
|
676
|
+
self.expr(a, stmt, READ)
|
|
677
|
+
return r
|
|
678
|
+
if isinstance(e, S.Member):
|
|
679
|
+
root = self.expr(e.base, stmt, MEMBER_BASE)
|
|
680
|
+
base_ref = self._last_chain_ref(e.base, root)
|
|
681
|
+
self.ref(stmt, e.member, MEMBER, e, member=True, base=base_ref, write=write)
|
|
682
|
+
for a in e.args or []:
|
|
683
|
+
self.expr(a, stmt, READ)
|
|
684
|
+
return root
|
|
685
|
+
if isinstance(e, S.Unary):
|
|
686
|
+
return self.expr(e.operand, stmt, role, write=write)
|
|
687
|
+
if isinstance(e, S.Binary):
|
|
688
|
+
self.expr(e.left, stmt, READ, skip=skip)
|
|
689
|
+
self.expr(e.right, stmt, READ, skip=skip)
|
|
690
|
+
return None
|
|
691
|
+
if isinstance(e, S.ArrayLiteral):
|
|
692
|
+
for item in e.items:
|
|
693
|
+
self.expr(item, stmt, READ)
|
|
694
|
+
return None
|
|
695
|
+
for f in getattr(e, "__dataclass_fields__", {}): # unknown node: walk its fields
|
|
696
|
+
v = getattr(e, f)
|
|
697
|
+
for x in v if isinstance(v, list) else [v]:
|
|
698
|
+
if isinstance(x, S.Expr):
|
|
699
|
+
self.expr(x, stmt, READ)
|
|
700
|
+
return None
|
|
701
|
+
|
|
702
|
+
def _last_chain_ref(self, base_expr, root):
|
|
703
|
+
"""The reference standing for `base_expr`: its member reference for a nested `a.b`, else the
|
|
704
|
+
root name's reference."""
|
|
705
|
+
if isinstance(base_expr, S.Member):
|
|
706
|
+
for r in reversed(self.refs):
|
|
707
|
+
if r.node is base_expr:
|
|
708
|
+
return r
|
|
709
|
+
return root
|
|
710
|
+
|
|
711
|
+
def args(self, args, stmt, role=ARGUMENT):
|
|
712
|
+
for a in args or []:
|
|
713
|
+
self.expr(a, stmt, role)
|
|
714
|
+
|
|
715
|
+
def type_ref(self, stmt, vtype, node):
|
|
716
|
+
if vtype:
|
|
717
|
+
name = vtype.rstrip("!")
|
|
718
|
+
self.ref(stmt, name, TYPE, node)
|
|
719
|
+
|
|
720
|
+
# -- statements ------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
def statement(self, st: S.Stmt) -> None:
|
|
723
|
+
if isinstance(st, S.Declaration):
|
|
724
|
+
self.declaration(st)
|
|
725
|
+
elif isinstance(st, S.ConstDecl):
|
|
726
|
+
self.const(st)
|
|
727
|
+
elif isinstance(st, S.Assignment):
|
|
728
|
+
self.expr(st.target, st, WRITE, write=True)
|
|
729
|
+
self.expr(st.value, st, READ, skip=st.target if st.compound else None)
|
|
730
|
+
elif isinstance(st, S.Instruction):
|
|
731
|
+
self.instruction(st)
|
|
732
|
+
elif isinstance(st, S.If):
|
|
733
|
+
self.expr(st.cond, st)
|
|
734
|
+
for sub in (st.inline or []) + (st.inline_else or []):
|
|
735
|
+
self.statement(sub)
|
|
736
|
+
elif isinstance(st, S.ElseIf):
|
|
737
|
+
self.expr(st.cond, st)
|
|
738
|
+
elif isinstance(st, S.For):
|
|
739
|
+
self.expr(st.var, st, WRITE, write=True)
|
|
740
|
+
for e in (st.start, st.end, st.step):
|
|
741
|
+
self.expr(e, st)
|
|
742
|
+
elif isinstance(st, S.DoLoop):
|
|
743
|
+
self.expr(st.cond, st)
|
|
744
|
+
elif isinstance(st, S.Case):
|
|
745
|
+
for item in st.items:
|
|
746
|
+
for e in item[1:]:
|
|
747
|
+
if isinstance(e, S.Expr):
|
|
748
|
+
self.expr(e, st)
|
|
749
|
+
elif isinstance(st, S.SubDef):
|
|
750
|
+
self.subdef(st)
|
|
751
|
+
elif isinstance(st, S.Call):
|
|
752
|
+
self.ref(st, st.name, CALL, st)
|
|
753
|
+
self.args(st.args, st)
|
|
754
|
+
self.expr(st.tail_expr, st)
|
|
755
|
+
for e in st.juxtaposed or []:
|
|
756
|
+
self.expr(e, st)
|
|
757
|
+
elif isinstance(st, S.Label):
|
|
758
|
+
self.decl(st, st.name, "label", node=st, unverified=self.routine is not None)
|
|
759
|
+
|
|
760
|
+
def declaration(self, st: S.Declaration) -> None:
|
|
761
|
+
for v in st.vars:
|
|
762
|
+
if st.kind == "STRUCTUREMEMBER":
|
|
763
|
+
d = self.decl(
|
|
764
|
+
st,
|
|
765
|
+
v.name,
|
|
766
|
+
"structure_member",
|
|
767
|
+
node=v,
|
|
768
|
+
scope="structure",
|
|
769
|
+
routine=None,
|
|
770
|
+
container=self.struct,
|
|
771
|
+
type=v.type,
|
|
772
|
+
)
|
|
773
|
+
if self.struct is not None and self.struct.members is not None:
|
|
774
|
+
self.struct.members.setdefault(v.name.upper(), []).append(d)
|
|
775
|
+
else:
|
|
776
|
+
self.decl(
|
|
777
|
+
st,
|
|
778
|
+
v.name,
|
|
779
|
+
st.kind.lower(),
|
|
780
|
+
node=v,
|
|
781
|
+
type=v.type,
|
|
782
|
+
unverified=st.kind == "PUBLIC" and self.routine is not None,
|
|
783
|
+
)
|
|
784
|
+
for dim in v.dims:
|
|
785
|
+
self.expr(dim, st)
|
|
786
|
+
self.type_ref(st, v.type, v)
|
|
787
|
+
self.expr(v.string_size, st)
|
|
788
|
+
for e in v.init or []:
|
|
789
|
+
self.expr(e, st)
|
|
790
|
+
|
|
791
|
+
def const(self, st: S.ConstDecl) -> None:
|
|
792
|
+
self.decl(
|
|
793
|
+
st, st.name, "const", node=st, type=st.type, container=self.consttable if self.routine is None else None
|
|
794
|
+
)
|
|
795
|
+
self.type_ref(st, st.type, st)
|
|
796
|
+
self.expr(st.value, st)
|
|
797
|
+
|
|
798
|
+
def subdef(self, st: S.SubDef) -> None:
|
|
799
|
+
kind = "function" if st.keyword == "FUNCTION" else "sub"
|
|
800
|
+
self.decl(st, st.name, kind, node=st, scope="global", routine=None, type=st.return_type)
|
|
801
|
+
for p in st.params:
|
|
802
|
+
self.decl(st, p.name, "param", node=p, scope="routine", routine=st, type=p.type)
|
|
803
|
+
for dim in p.dim_exprs: # `p(sz)`: a read in the routine's scope
|
|
804
|
+
self.expr(dim, st)
|
|
805
|
+
self.type_ref(st, p.type, p)
|
|
806
|
+
self.expr(p.string_size, st)
|
|
807
|
+
self.expr(p.default, st)
|
|
808
|
+
self.type_ref(st, st.return_type, st)
|
|
809
|
+
|
|
810
|
+
def instruction(self, st: S.Instruction) -> None:
|
|
811
|
+
kw = st.keyword.upper()
|
|
812
|
+
m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*\$?)", st.text or "")
|
|
813
|
+
spelling = m.group(1) if m and m.group(1).upper() == kw else kw
|
|
814
|
+
self.ref(st, spelling, INSTRUCTION, st)
|
|
815
|
+
if kw == "ALIAS" and st.args:
|
|
816
|
+
target = self.expr(st.args[0], st, ALIAS_TARGET)
|
|
817
|
+
junk = _alias_junk(st, len(st.args) - 1)
|
|
818
|
+
for j, rhs in enumerate(st.args[1:]):
|
|
819
|
+
name = rhs.name if isinstance(rhs, (S.Name, S.Index)) else None
|
|
820
|
+
if name is None:
|
|
821
|
+
self.expr(rhs, st)
|
|
822
|
+
continue
|
|
823
|
+
d = self.decl(st, name, "alias", node=rhs, unverified=self.routine is not None)
|
|
824
|
+
d.alias_target = target
|
|
825
|
+
if isinstance(rhs, S.Index):
|
|
826
|
+
for a in rhs.args:
|
|
827
|
+
self.expr(a, st)
|
|
828
|
+
self._skip_tokens(junk[j])
|
|
829
|
+
self._skip_tokens(junk[-1])
|
|
830
|
+
elif kw == "UNITS" and st.args:
|
|
831
|
+
self.expr(st.args[0], st, UNITS_TARGET)
|
|
832
|
+
elif kw in ("DATATABLE", "CONSTTABLE", "STRUCTURETYPE", "OBJECTTYPE") and st.args:
|
|
833
|
+
first = st.args[0]
|
|
834
|
+
dkind = kw.lower()
|
|
835
|
+
if isinstance(first, S.Name):
|
|
836
|
+
d = self.decl(st, first.name, dkind, node=first, scope="global", routine=None)
|
|
837
|
+
elif isinstance(first, S.String) and first.value and kw in ("DATATABLE", "CONSTTABLE"):
|
|
838
|
+
d = self.decl(st, first.value, dkind, node=first, scope="global", routine=None, quoted=True)
|
|
839
|
+
else:
|
|
840
|
+
d = None
|
|
841
|
+
self.expr(first, st, ARGUMENT)
|
|
842
|
+
if d is not None and kw == "STRUCTURETYPE":
|
|
843
|
+
self.struct = d
|
|
844
|
+
if d is not None and kw == "CONSTTABLE":
|
|
845
|
+
self.consttable = d
|
|
846
|
+
self.args(st.args[1:], st)
|
|
847
|
+
else:
|
|
848
|
+
self.args(st.args, st)
|
|
849
|
+
if kw == "ENDSTRUCTURETYPE":
|
|
850
|
+
self.struct = None
|
|
851
|
+
elif kw == "ENDCONSTTABLE":
|
|
852
|
+
self.consttable = None
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _add_data_table_fields(ix: BindingIndex, program, statements, decls, failures) -> None:
|
|
856
|
+
"""Declare the fields of every user DataTable. Names, order and renaming come from
|
|
857
|
+
`tables.py` (private helpers `_scan_program`, `_emit_output_fields`, `_apply_fieldnames`,
|
|
858
|
+
`_split_by_alias`, `_build_public_table`; not modified): the loop below mirrors
|
|
859
|
+
`_build_data_table`'s grouping of output instructions and `FieldNames`, to know which
|
|
860
|
+
statement declares which field, and its names are checked against `_build_data_table`'s own
|
|
861
|
+
field list (a table that differs is left unmodelled)."""
|
|
862
|
+
tbl_decls = {id(d.stmt): d for d in decls if d.kind == "datatable" and d.scope == "global"}
|
|
863
|
+
if not tbl_decls:
|
|
864
|
+
return
|
|
865
|
+
env = consteval.Env(ix.profile, dict(getattr(program, "consts", None) or {}))
|
|
866
|
+
scanned = tables._scan_program(program)
|
|
867
|
+
ix._slow_seq = scanned["slow_seq_count"]
|
|
868
|
+
var_info: dict = {}
|
|
869
|
+
for v in scanned["all_vars"]:
|
|
870
|
+
var_info[v.name.upper()] = tables._make_var_info(v, env)
|
|
871
|
+
var_info[v.name.upper()].struct = scanned["struct_types"].get((v.type or "").upper())
|
|
872
|
+
_, by_display, by_base = tables._build_public_table(
|
|
873
|
+
scanned["publics"], var_info, scanned["alias_stmts"], scanned["units_stmts"], scanned["readonly_names"]
|
|
874
|
+
)
|
|
875
|
+
pos = {id(st): i for i, st in enumerate(statements)}
|
|
876
|
+
added: list[Declaration] = []
|
|
877
|
+
anon_groups, anon_build = [0], [0] # two passes over the tables, each with its own `Anonymous<n>` count
|
|
878
|
+
for dt in scanned["data_tables"]:
|
|
879
|
+
head = statements[pos[id(dt.body[0])] - 1] if dt.body and id(dt.body[0]) in pos else None
|
|
880
|
+
table = tbl_decls.get(id(head))
|
|
881
|
+
try:
|
|
882
|
+
groups = _field_groups(dt, env, var_info, by_display, by_base, anon_groups)
|
|
883
|
+
tdf, _ = tables._build_data_table(dt, env, var_info, by_display, scanned["called"], by_base, anon_build)
|
|
884
|
+
for f in tdf.fields: # a later `Sample(1, Table.Field, ...)` source
|
|
885
|
+
if f.type != 11:
|
|
886
|
+
var_info[f"{dt.name}.{f.name}".upper()] = tables.VarInfo(
|
|
887
|
+
f.type, list(f.subdims) or ([f.dimension] if f.dimension > 1 else []), name=f.name
|
|
888
|
+
)
|
|
889
|
+
derived = [of.name for _, _, fs in groups for of in fs]
|
|
890
|
+
if table is None or derived != [of.name for of in getattr(tdf, "_dup_fields") if not of.name_only]:
|
|
891
|
+
continue
|
|
892
|
+
except Exception as e: # never raise out of the index
|
|
893
|
+
failures.append(IndexFailure(-1, "", dt.line, f"fields of {dt.name}: {type(e).__name__}: {e}"))
|
|
894
|
+
continue
|
|
895
|
+
out = ix._fields.setdefault(id(table), [])
|
|
896
|
+
table.modelled = True
|
|
897
|
+
for instr, fn, fields in groups:
|
|
898
|
+
for of in fields:
|
|
899
|
+
if fn is not None:
|
|
900
|
+
arg = fn.args[0]
|
|
901
|
+
unit, tok_key, occ, site = fn, (str(arg.value).upper(), "string"), 0, "fieldnames"
|
|
902
|
+
else:
|
|
903
|
+
unit, tok_key, occ, site = instr, (), None, "instruction"
|
|
904
|
+
d = Declaration(
|
|
905
|
+
of.name,
|
|
906
|
+
"field",
|
|
907
|
+
"table",
|
|
908
|
+
None,
|
|
909
|
+
table,
|
|
910
|
+
unit.file,
|
|
911
|
+
unit.line,
|
|
912
|
+
instr,
|
|
913
|
+
unit,
|
|
914
|
+
unit,
|
|
915
|
+
pos[id(unit)],
|
|
916
|
+
occ,
|
|
917
|
+
None,
|
|
918
|
+
fn is not None,
|
|
919
|
+
tok_key=tok_key,
|
|
920
|
+
site=site,
|
|
921
|
+
)
|
|
922
|
+
added.append(d)
|
|
923
|
+
out.append(d)
|
|
924
|
+
ix._field_names.setdefault((id(table), of.name.upper()), []).append(d)
|
|
925
|
+
ix.declarations += tuple(added)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _field_groups(dt, env, var_info, by_display, by_base, anon=None):
|
|
929
|
+
"""`[(output instruction, its FieldNames statement | None, [_OutField])]` for one table, the
|
|
930
|
+
fields exactly as `tables._build_data_table` builds them (name_only fields dropped)."""
|
|
931
|
+
groups: list = []
|
|
932
|
+
pending: list = []
|
|
933
|
+
pending_src = None
|
|
934
|
+
instr = None
|
|
935
|
+
|
|
936
|
+
def flush(fnames):
|
|
937
|
+
nonlocal pending, pending_src, instr
|
|
938
|
+
fields = pending
|
|
939
|
+
if pending_src is not None and by_base:
|
|
940
|
+
fields = tables._split_by_alias(pending, pending_src, var_info, by_base, env)
|
|
941
|
+
groups.append((instr, fnames, [of for of in fields if not of.name_only]))
|
|
942
|
+
pending, pending_src, instr = [], None, None
|
|
943
|
+
|
|
944
|
+
for s in dt.body:
|
|
945
|
+
kw = tables._kw(s)
|
|
946
|
+
if kw == "FIELDNAMES":
|
|
947
|
+
if s.args and isinstance(s.args[0], S.String) and pending:
|
|
948
|
+
pending = tables._apply_fieldnames(pending, s.args[0].value)
|
|
949
|
+
flush(s)
|
|
950
|
+
elif kw in tables._OUTPUT_KEYWORDS:
|
|
951
|
+
if pending:
|
|
952
|
+
flush(None)
|
|
953
|
+
s2 = tables._alias_source(s, var_info, by_display, by_base)
|
|
954
|
+
pending = tables._emit_output_fields(s2, var_info, by_display, env, by_base, anon)
|
|
955
|
+
pending_src = s2.args[1] if len(s2.args) > 1 and not any(f.struct_members for f in pending) else None
|
|
956
|
+
instr = s
|
|
957
|
+
elif kw == "FFT":
|
|
958
|
+
if pending:
|
|
959
|
+
flush(None)
|
|
960
|
+
pending, pending_src, instr = [tables._fft_field(s, var_info, by_display, env, anon)], None, s
|
|
961
|
+
if pending:
|
|
962
|
+
flush(None)
|
|
963
|
+
return groups
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _scope_tables(ix: BindingIndex, decls: Iterable[Declaration]) -> None:
|
|
967
|
+
for d in decls:
|
|
968
|
+
ix._by_name.setdefault(d.name.upper(), []).append(d)
|
|
969
|
+
key = d.name.upper()
|
|
970
|
+
if d.scope == "global":
|
|
971
|
+
ix._glob.setdefault(key, []).append(d)
|
|
972
|
+
elif d.scope == "routine" and d.routine is not None:
|
|
973
|
+
ix._local.setdefault(id(d.routine), {}).setdefault(key, []).append(d)
|
|
974
|
+
if d.unverified_scope:
|
|
975
|
+
ix._loose.setdefault(key, []).append(d)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def build(program, profile) -> BindingIndex:
|
|
979
|
+
"""Index `program` (a `syntax.Program`, e.g. `AnalysisResult.program`) for `profile` (a
|
|
980
|
+
profile name or a `Profile`). Never raises for any program; a statement the indexer cannot
|
|
981
|
+
handle is recorded in `BindingIndex.failures` and contributes no records. `ValueError` for an
|
|
982
|
+
unknown profile name, as `profiles.get`."""
|
|
983
|
+
prof = profile if isinstance(profile, profiles.Profile) else profiles.get(profile)
|
|
984
|
+
ix = BindingIndex(prof)
|
|
985
|
+
try:
|
|
986
|
+
_build(ix, program)
|
|
987
|
+
except Exception as e: # never raise out of the index
|
|
988
|
+
ix.failures += (IndexFailure(-1, "", 0, f"internal error: {e!r}"),)
|
|
989
|
+
return ix
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _build(ix: BindingIndex, program) -> None:
|
|
993
|
+
statements = list(getattr(program, "statements", None) or [])
|
|
994
|
+
b = _Builder(ix)
|
|
995
|
+
failures: list[IndexFailure] = []
|
|
996
|
+
unparsed: list = []
|
|
997
|
+
for i, st in enumerate(statements):
|
|
998
|
+
b.idx, b.unit, b.counts = i, st, {}
|
|
999
|
+
d0, r0, o0 = len(b.decls), len(b.refs), len(b.order)
|
|
1000
|
+
saved = (b.routine, b.struct, b.consttable)
|
|
1001
|
+
try:
|
|
1002
|
+
if isinstance(st, S.Unparsed):
|
|
1003
|
+
unparsed.append(st)
|
|
1004
|
+
else:
|
|
1005
|
+
if isinstance(st, S.SubDef):
|
|
1006
|
+
b.routine = st
|
|
1007
|
+
b.statement(st)
|
|
1008
|
+
except Exception as e:
|
|
1009
|
+
del b.decls[d0:], b.refs[r0:], b.order[o0:]
|
|
1010
|
+
b.routine, b.struct, b.consttable = saved
|
|
1011
|
+
failures.append(
|
|
1012
|
+
IndexFailure(i, getattr(st, "file", ""), getattr(st, "line", 0), f"{type(e).__name__}: {e}")
|
|
1013
|
+
)
|
|
1014
|
+
if closes_routine(st):
|
|
1015
|
+
b.routine = None
|
|
1016
|
+
ix.declarations = tuple(b.decls)
|
|
1017
|
+
ix.references = tuple(b.refs)
|
|
1018
|
+
ix.unparsed = tuple(unparsed)
|
|
1019
|
+
_scope_tables(ix, b.decls)
|
|
1020
|
+
try:
|
|
1021
|
+
_add_data_table_fields(ix, program, statements, b.decls, failures)
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
failures.append(IndexFailure(-1, "", 0, f"table fields: {type(e).__name__}: {e}"))
|
|
1024
|
+
for unit, rec in b.order:
|
|
1025
|
+
ix._by_unit.setdefault(id(unit), []).append(rec)
|
|
1026
|
+
for ref in b.refs: # emission order: bases before members
|
|
1027
|
+
try:
|
|
1028
|
+
ix._finish_reference(ref)
|
|
1029
|
+
except Exception as e:
|
|
1030
|
+
ref.resolved, ref.ambiguity, ref.container = (), "", ()
|
|
1031
|
+
failures.append(IndexFailure(ref.index, ref.file, ref.line, f"resolve {ref.name}: {type(e).__name__}: {e}"))
|
|
1032
|
+
for d in ref.resolved:
|
|
1033
|
+
ix._refs_to.setdefault(id(d), []).append(ref)
|
|
1034
|
+
ix.failures = tuple(failures)
|