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