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,128 @@
1
+ """The jobs that run inside a spawned worker process: an analysis, and the compiler output of the
2
+ `opencrbasic.showCompilerOutput` command. Imports only the core compiler."""
3
+
4
+ import os
5
+ import pickle
6
+ import time
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ CRASH_ENV = "OPENCRBASIC_LSP_TEST_CRASH"
11
+ SLEEP_ENV = "OPENCRBASIC_LSP_TEST_SLEEP"
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class JobResult:
16
+ """Plain picklable data: no AST, no dataclasses from the facade."""
17
+
18
+ ok: bool
19
+ # (severity, message, line, resolved path or None, compiler file name,
20
+ # physical (first, last) line in that path's file or None)
21
+ diagnostics: tuple[tuple[str, str, int, str | None, str, tuple[int, int] | None], ...]
22
+ failures: tuple[str, ...]
23
+ reads: tuple[tuple[str, str | None], ...] # path -> sha256 or None (missing)
24
+ listings: tuple[str, ...] # directories whose listing was read
25
+ program: bytes | None = None # pickled parsed program (None: none or unpicklable)
26
+ oversized: tuple[tuple[str, int], ...] = () # files over max_file_bytes (read as missing)
27
+ # (channel, code, severity, message, line, resolved path or None, compiler file name, span, link)
28
+ findings: tuple[tuple[str, str, str, str, int, str | None, str, tuple[int, int] | None, str], ...] = ()
29
+ config_error: str | None = None # invalid `opencrbasic.toml` (no checks ran)
30
+
31
+
32
+ def _pickle_program(program: object) -> bytes | None:
33
+ if program is None:
34
+ return None
35
+ try:
36
+ return pickle.dumps(program, protocol=pickle.HIGHEST_PROTOCOL)
37
+ except Exception: # RecursionError, an unpicklable node: features fall back to text
38
+ return None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class OutputResult:
43
+ """`commands.CompilerOutput` as plain picklable data."""
44
+
45
+ text: str
46
+ ok: bool
47
+ oversized: tuple[tuple[str, int], ...] = () # files over max_file_bytes (not read; no text)
48
+
49
+
50
+ def _test_hooks(entry: str) -> None:
51
+ """Test hooks (docs/lsp-api.md "Test hooks"): only ever act on a substring of the entry path."""
52
+ crash = os.environ.get(CRASH_ENV)
53
+ if crash and crash in entry:
54
+ os._exit(3)
55
+ sleep = os.environ.get(SLEEP_ENV)
56
+ if sleep:
57
+ needle, _, seconds = sleep.rpartition(":")
58
+ if needle and needle in entry:
59
+ time.sleep(float(seconds))
60
+
61
+
62
+ def run_compiler_output(
63
+ entry: str, logger: str, overlays: tuple[tuple[str, bytes], ...], max_file_bytes: int | None = None
64
+ ) -> OutputResult:
65
+ _test_hooks(entry)
66
+ from .. import commands
67
+
68
+ out = commands.compiler_output(Path(entry), logger, {Path(p): data for p, data in overlays}, max_file_bytes)
69
+ return OutputResult(out.text, out.ok, tuple((str(p), size) for p, size in out.oversized.items()))
70
+
71
+
72
+ def run_analysis(
73
+ entry: str,
74
+ logger: str,
75
+ overlays: tuple[tuple[str, bytes], ...],
76
+ max_file_bytes: int | None = None,
77
+ checks: tuple[tuple[str, str], ...] = (),
78
+ notices: bool = False,
79
+ config_root: str | None = None,
80
+ ) -> JobResult:
81
+ _test_hooks(entry)
82
+ from .. import analysis
83
+
84
+ result = analysis.analyze(
85
+ analysis.AnalysisRequest(
86
+ Path(entry),
87
+ logger,
88
+ {Path(p): data for p, data in overlays},
89
+ max_file_bytes=max_file_bytes,
90
+ checks=dict(checks),
91
+ notices=notices,
92
+ config_root=None if config_root is None else Path(config_root),
93
+ )
94
+ )
95
+ return JobResult(
96
+ result.ok,
97
+ tuple(
98
+ (
99
+ d.severity,
100
+ d.message,
101
+ d.line,
102
+ None if d.path is None else str(d.path),
103
+ d.file,
104
+ None if d.span is None else (int(d.span[0]), int(d.span[1])),
105
+ )
106
+ for d in result.diagnostics
107
+ ),
108
+ tuple(result.implementation_failures),
109
+ tuple((str(p), h) for p, h in result.dependencies.reads.items()),
110
+ tuple(str(p) for p in result.dependencies.listings),
111
+ _pickle_program(result.program),
112
+ tuple((str(p), size) for p, size in result.dependencies.oversized.items()),
113
+ tuple(
114
+ (
115
+ f.channel,
116
+ f.code,
117
+ f.severity,
118
+ f.message,
119
+ f.line,
120
+ None if f.path is None else str(f.path),
121
+ f.file,
122
+ None if f.span is None else (int(f.span[0]), int(f.span[1])),
123
+ f.link,
124
+ )
125
+ for f in result.findings
126
+ ),
127
+ result.config_error,
128
+ )
opencrbasic/mode.py ADDED
@@ -0,0 +1,143 @@
1
+ """Sequential / Pipeline mode decision (CR6, CR1000X). Rules: docs/modes.md.
2
+
3
+ The trigger lists themselves are per-logger data (`data/<logger>/mode.json`, `Profile.mode_rules`
4
+ -- `docs/loggers.md`), not module constants: CR1000X's compiled mode-decision function
5
+ (`docs/loggers.md`'s Ghidra evidence) is structurally identical to CR6's but three
6
+ `task_instructions` names and two `nonconstant_arg` names are absent because those instructions
7
+ (`ExciteI`, `VibratingWire`, and -- `task_instructions` only -- `Resistance`) do not exist on
8
+ CR1000X at all, so the two loggers' *observable* behaviour is identical: a name missing from a
9
+ logger's list can never occur as a real instruction in a program that logger accepts anyway.
10
+ CR300/CR350 never print a mode line (`Profile.reports_mode`), so their `mode.json` is an unverified
11
+ copy of CR6's, kept only so every profile has one; `mode.decide`'s result for them is computed but
12
+ discarded by `compiler.py`.
13
+ """
14
+
15
+ from collections.abc import Callable
16
+
17
+ from . import syntax as S
18
+ from .profiles import Profile
19
+
20
+
21
+ def _const_value(expr, value_of: Callable) -> float | None:
22
+ try:
23
+ return value_of(expr)
24
+ except Exception:
25
+ return None
26
+
27
+
28
+ def _numeric_constant(expr, is_constant: Callable, value_of: Callable) -> bool:
29
+ """A `nonconstant_arg` argument that keeps PipelineMode: numbers and numeric `Const` names
30
+ joined by operators. A string value or a function call counts as variable (docs/modes.md)."""
31
+ if isinstance(expr, S.Number):
32
+ return True
33
+ if isinstance(expr, S.Name):
34
+ return is_constant(expr) and not isinstance(_const_value(expr, value_of), str)
35
+ if isinstance(expr, S.Unary):
36
+ return _numeric_constant(expr.operand, is_constant, value_of)
37
+ if isinstance(expr, S.Binary):
38
+ return _numeric_constant(expr.left, is_constant, value_of) and _numeric_constant(
39
+ expr.right, is_constant, value_of
40
+ )
41
+ return False
42
+
43
+
44
+ def decide(statements: list[S.Stmt], profile: Profile, is_constant: Callable, value_of: Callable) -> str:
45
+ """Return "Sequential" or "Pipeline".
46
+
47
+ `is_constant(expr)` tells whether an argument is a compile-time constant; `value_of(expr)`
48
+ returns its numeric value (raises when it has none). Trigger lists come from
49
+ `profile.mode_rules` (`data/<logger>/mode.json`).
50
+ """
51
+ rules = profile.mode_rules
52
+ always_sequential = set(rules["always_sequential"])
53
+ task_instructions = set(rules["task_instructions"])
54
+ nonconstant_arg = {k: tuple(v) for k, v in rules["nonconstant_arg"].items()}
55
+ variable_arg = dict(rules["variable_arg"])
56
+
57
+ sequential = False
58
+ pipeline_forced = False
59
+ depth = 0 # open If/Select/For/While/Do since the last Scan
60
+ in_scan = 0 # Scan nesting (Scan ... NextScan)
61
+ in_slow = 0 # SlowSequence count
62
+
63
+ def visit(stmt: S.Stmt) -> None:
64
+ nonlocal sequential, pipeline_forced, depth, in_scan, in_slow
65
+ kw = _keyword(stmt)
66
+ args = getattr(stmt, "args", []) or []
67
+ if kw == "SEQUENTIALMODE":
68
+ sequential = True
69
+ elif kw == "PIPELINEMODE":
70
+ pipeline_forced = True
71
+ if not sequential and not pipeline_forced:
72
+ if kw in always_sequential:
73
+ sequential = True
74
+ elif kw in task_instructions and (depth or not in_scan):
75
+ if kw == "SW12":
76
+ sequential = _const_value(_arg(args, 2), value_of) == 1
77
+ elif kw == "DELAY":
78
+ sequential = _const_value(_arg(args, 0), value_of) in (0, 2)
79
+ elif kw in ("SERIALOUTBLOCK", "SERIALINRECORD"):
80
+ sequential = not in_slow or depth > 0
81
+ else:
82
+ sequential = True
83
+ elif kw in nonconstant_arg:
84
+ sequential = any(
85
+ _arg(args, i) is not None and not _numeric_constant(_arg(args, i), is_constant, value_of)
86
+ for i in nonconstant_arg[kw]
87
+ )
88
+ elif kw in ("FOR", "IF", "SELECT", "WHILE", "DO"):
89
+ depth += 1
90
+ elif kw in ("NEXT", "ENDIF", "END", "WEND", "LOOP"):
91
+ target = _block_word(args)
92
+ if kw == "NEXT" and target == "SCAN":
93
+ in_scan -= 1
94
+ elif kw == "NEXT" and target == "SUBSCAN":
95
+ pass
96
+ elif kw == "END" and target not in ("IF", "SELECT"):
97
+ pass
98
+ else:
99
+ depth -= 1
100
+ if kw == "SLOWSEQUENCE":
101
+ in_slow += 1
102
+ elif kw == "SCAN":
103
+ depth = 0
104
+ in_scan += 1
105
+ elif kw == "NEXTSCAN":
106
+ in_scan -= 1
107
+ if kw in variable_arg and not pipeline_forced:
108
+ arg = _arg(args, variable_arg[kw])
109
+ if arg is None or not is_constant(arg):
110
+ sequential = True
111
+ # single-line If: the inline statements are inside a conditional
112
+ if isinstance(stmt, S.If) and stmt.inline is not None:
113
+ for inner in (stmt.inline or []) + (stmt.inline_else or []):
114
+ visit(inner)
115
+ if not sequential and not pipeline_forced:
116
+ depth -= 1
117
+
118
+ for stmt in statements:
119
+ visit(stmt)
120
+ return "Sequential" if sequential else "Pipeline"
121
+
122
+
123
+ def _keyword(stmt: S.Stmt) -> str:
124
+ if isinstance(stmt, S.Instruction):
125
+ return stmt.keyword.upper()
126
+ if isinstance(stmt, S.If):
127
+ return "IF"
128
+ if isinstance(stmt, S.For):
129
+ return "FOR"
130
+ if isinstance(stmt, S.DoLoop):
131
+ return stmt.keyword.upper()
132
+ return type(stmt).__name__.upper()
133
+
134
+
135
+ def _arg(args: list, i: int):
136
+ return args[i] if i < len(args) else None
137
+
138
+
139
+ def _block_word(args: list) -> str | None:
140
+ """`Next Scan`, `End If`: the word after a two-word block terminator."""
141
+ if args and isinstance(args[0], S.Name):
142
+ return args[0].name.upper()
143
+ return None
@@ -0,0 +1,401 @@
1
+ """Navigation logic for editor tooling: go to definition, find references, document highlights and
2
+ semantic tokens over an `AnalysisResult`. Protocol independent (no pygls): positions in and out are
3
+ physical, a 0-based line and a decoded-character column (a byte offset into the line, as in
4
+ `positions.py`); the server converts to the client's position encoding. See `docs/lsp-api.md`
5
+ "Navigation (LSP-05b-core)".
6
+
7
+ A position is mapped to a record by the records' own ranges, never by searching text for a name:
8
+ `bindings.BindingIndex.token_of` gives the token of each declaration/reference (a column into the
9
+ statement's text), the statement's `origin` (logical line and segment offset, set by the parser)
10
+ gives the logical column, and `positions.exact_range` maps it to the physical file. A token that
11
+ does not come from one contiguous physical slice, or whose token cannot be found, has no range and
12
+ is never navigated.
13
+ """
14
+
15
+ import os
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import NamedTuple
19
+
20
+ from . import bindings, positions
21
+
22
+ __all__ = [
23
+ "Location",
24
+ "Highlight",
25
+ "SemanticToken",
26
+ "Site",
27
+ "Navigation",
28
+ "navigation",
29
+ "definition",
30
+ "references",
31
+ "references_across",
32
+ "declaration_sites",
33
+ "document_highlights",
34
+ "semantic_tokens",
35
+ "path_keys",
36
+ "TOKEN_TYPES",
37
+ "TOKEN_MODIFIERS",
38
+ ]
39
+
40
+ # Semantic-token legend (indices into these tuples are the LSP wire values; the server declares
41
+ # them in this order).
42
+ TOKEN_TYPES = ("variable", "parameter", "function", "property", "namespace", "struct", "type")
43
+ TOKEN_MODIFIERS = ("declaration", "readonly")
44
+
45
+ _KIND_TYPE = {
46
+ "public": "variable",
47
+ "dim": "variable",
48
+ "static": "variable",
49
+ "alias": "variable",
50
+ "const": "variable",
51
+ "param": "parameter",
52
+ "sub": "function",
53
+ "function": "function",
54
+ "structure_member": "property",
55
+ "datatable": "namespace",
56
+ "consttable": "namespace",
57
+ "structuretype": "struct",
58
+ "objecttype": "type",
59
+ }
60
+ _READ_ROLES = {
61
+ bindings.READ,
62
+ bindings.CALL,
63
+ bindings.MEMBER_BASE,
64
+ bindings.MEMBER,
65
+ bindings.TABLE_REF,
66
+ bindings.ALIAS_TARGET,
67
+ bindings.TYPE,
68
+ bindings.INSTRUCTION,
69
+ }
70
+ _KIND_RANK = {"write": 3, "read": 2, "text": 1}
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Location:
75
+ path: Path | None # the file (None when the occurrence has no resolved path)
76
+ file: str # compiler display name
77
+ line: int # physical, 0-based
78
+ start: int # decoded column, inclusive
79
+ end: int # decoded column, exclusive
80
+ occurrences: tuple[int, ...] # `Occurrence.id` of each load of the file that gives this range
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class Highlight:
85
+ line: int
86
+ start: int
87
+ end: int
88
+ kind: str # "write" | "read" | "text"
89
+
90
+
91
+ class SemanticToken(NamedTuple):
92
+ line: int
93
+ start: int
94
+ length: int
95
+ type: str # one of TOKEN_TYPES
96
+ modifiers: tuple[str, ...] # subset of TOKEN_MODIFIERS
97
+
98
+
99
+ class Site(NamedTuple):
100
+ """The exact physical range of one declaration or reference (`rec`) of an analysis."""
101
+
102
+ rec: bindings.Declaration | bindings.Reference
103
+ path: Path | None
104
+ file: str
105
+ occurrence: int
106
+ line: int
107
+ start: int
108
+ end: int
109
+
110
+
111
+ def path_keys(path) -> set[str]:
112
+ """The normalised spellings (absolute and symlink-resolved) under which `path` is matched."""
113
+ p = Path(path)
114
+ out = {os.path.normcase(str(p.absolute()))}
115
+ try:
116
+ out.add(os.path.normcase(str(p.resolve())))
117
+ except OSError:
118
+ pass
119
+ return out
120
+
121
+
122
+ class Navigation:
123
+ """Ranges of every record of one analysis, built lazily on the first query."""
124
+
125
+ def __init__(self, result, index: bindings.BindingIndex | None = None):
126
+ self.result = result
127
+ program = getattr(result, "program", None)
128
+ self.index = index
129
+ if self.index is None and program is not None:
130
+ self.index = bindings.build(program, result.request.logger)
131
+ self._sites: list[Site] | None = None
132
+ self._by_line: dict[tuple[str, int], list[Site]] = {}
133
+ self._by_path: dict[str, list[Site]] = {}
134
+ self._site_of: dict[int, Site] = {}
135
+
136
+ # ---------------------------------------------------------------- ranges
137
+
138
+ def occurrence_paths(self) -> dict[int, Path | None]:
139
+ """`Occurrence.id` -> the file of that load (`None` when unresolved); id 0 is the entry."""
140
+ occs = getattr(getattr(self.result, "program", None), "occurrences", None) or []
141
+ paths = {o.id: o.path for o in occs}
142
+ if paths.get(0) is None:
143
+ paths[0] = Path(self.result.request.entry).absolute()
144
+ return paths
145
+
146
+ def _site(self, rec, paths) -> Site | None:
147
+ unit = rec.unit
148
+ origin = getattr(unit, "origin", None)
149
+ if origin is None:
150
+ return None
151
+ if self.index is None:
152
+ return None
153
+ tok = self.index.token_of(rec)
154
+ if tok is None:
155
+ return None
156
+ ll, seg = origin
157
+ text = unit.text
158
+ shift = None
159
+ for cand in (0, len(ll.text[seg:]) - len(ll.text[seg:].lstrip())): # `Units` text is stripped
160
+ if ll.text.startswith(text, seg + cand):
161
+ shift = cand
162
+ break
163
+ if shift is None:
164
+ return None
165
+ start = seg + shift + tok.col
166
+ end = start + len(tok.text)
167
+ if ll.text[start:end] != tok.text:
168
+ return None
169
+ r = positions.exact_range(ll, start, end)
170
+ if r is None or r.end_col <= r.start_col:
171
+ return None
172
+ return Site(rec, paths.get(r.occurrence), r.file, r.occurrence, r.line - 1, r.start_col, r.end_col)
173
+
174
+ def _build(self) -> list[Site]:
175
+ if self._sites is not None:
176
+ return self._sites
177
+ sites: list[Site] = []
178
+ if self.index is not None:
179
+ paths = self.occurrence_paths()
180
+ for rec in (*self.index.declarations, *self.index.references):
181
+ try:
182
+ site = self._site(rec, paths)
183
+ except Exception: # never raise out of navigation
184
+ site = None
185
+ if site is None:
186
+ continue
187
+ sites.append(site)
188
+ self._site_of[id(rec)] = site
189
+ if site.path is not None:
190
+ for key in path_keys(site.path):
191
+ self._by_line.setdefault((key, site.line), []).append(site)
192
+ self._by_path.setdefault(key, []).append(site)
193
+ self._sites = sites
194
+ return sites
195
+
196
+ def _path_sites(self, path) -> list[Site]:
197
+ self._build()
198
+ seen: set[int] = set()
199
+ out = []
200
+ for key in path_keys(path):
201
+ for s in self._by_path.get(key, ()):
202
+ if id(s) not in seen:
203
+ seen.add(id(s))
204
+ out.append(s)
205
+ return out
206
+
207
+ def sites_at(self, path, line: int, col: int) -> list[Site]:
208
+ """The sites whose name covers the position (`line` 0-based, `col` decoded). A position
209
+ inside a name selects that name; a position just after one selects it only when no name
210
+ contains the position. Empty when none."""
211
+ self._build()
212
+ seen: set[int] = set()
213
+ hits = []
214
+ for key in path_keys(path):
215
+ for s in self._by_line.get((key, line), ()):
216
+ if id(s) not in seen and s.start <= col <= s.end:
217
+ seen.add(id(s))
218
+ hits.append(s)
219
+ inside = [s for s in hits if col < s.end]
220
+ return inside or hits # a position just after a name still counts
221
+
222
+ def site_of(self, record) -> Site | None:
223
+ """The site of a declaration or reference of this analysis, `None` when its range is not
224
+ exact (see "Range policy" in `docs/lsp-api.md`)."""
225
+ self._build()
226
+ return self._site_of.get(id(record))
227
+
228
+ def _targets(self, path, line: int, col: int) -> list[bindings.Declaration]:
229
+ out: list[bindings.Declaration] = []
230
+ for s in self.sites_at(path, line, col):
231
+ cands = (s.rec,) if isinstance(s.rec, bindings.Declaration) else s.rec.resolved
232
+ for d in cands:
233
+ if all(d is not o for o in out):
234
+ out.append(d)
235
+ return out
236
+
237
+ # ---------------------------------------------------------------- queries
238
+
239
+ def definition(self, path, line: int, col: int) -> list[Location]:
240
+ self._build()
241
+ sites = [self._site_of.get(id(d)) for d in self._targets(path, line, col)]
242
+ return _locations(s for s in sites if s is not None)
243
+
244
+ def references(self, path, line: int, col: int, include_declaration: bool = True) -> list[Location]:
245
+ self._build()
246
+ return _locations(self._reference_sites(self._targets(path, line, col), include_declaration))
247
+
248
+ def declaration_sites(self, path, line: int, col: int) -> list[Site]:
249
+ """The sites of the declaration(s) the name at the position resolves to, for those that
250
+ have an exact range (external and predefined declarations have none)."""
251
+ self._build()
252
+ sites = [self._site_of.get(id(d)) for d in self._targets(path, line, col)]
253
+ return [s for s in sites if s is not None]
254
+
255
+ def declaration_at(self, path, line: int, start: int, end: int) -> list[bindings.Declaration]:
256
+ """The declarations whose own token is exactly at that range: how the same declaration is
257
+ found in another analysis that reads the same file (LSP-R4a)."""
258
+ return [
259
+ s.rec
260
+ for s in self.sites_at(path, line, start)
261
+ if isinstance(s.rec, bindings.Declaration) and (s.start, s.end) == (start, end)
262
+ ]
263
+
264
+ def _reference_sites(self, targets, include_declaration: bool) -> list[Site]:
265
+ sites: list[Site] = []
266
+ if self.index is None:
267
+ return sites
268
+ for d in targets:
269
+ if include_declaration and id(d) in self._site_of:
270
+ sites.append(self._site_of[id(d)])
271
+ for r in self.index.references_to(d):
272
+ if id(r) in self._site_of:
273
+ sites.append(self._site_of[id(r)])
274
+ return sites
275
+
276
+ def document_highlights(self, path, line: int, col: int) -> list[Highlight]:
277
+ self._build()
278
+ targets = self._targets(path, line, col)
279
+ if not targets:
280
+ return []
281
+ ids = {id(d) for d in targets}
282
+ best: dict[tuple[int, int, int], str] = {}
283
+ for s in self._path_sites(path):
284
+ rec = s.rec
285
+ if isinstance(rec, bindings.Declaration):
286
+ if id(rec) not in ids:
287
+ continue
288
+ kind = "write"
289
+ else:
290
+ if not any(id(d) in ids for d in rec.resolved):
291
+ continue
292
+ kind = _ref_kind(rec)
293
+ key = (s.line, s.start, s.end)
294
+ if _KIND_RANK[kind] > _KIND_RANK.get(best.get(key, ""), 0):
295
+ best[key] = kind
296
+ return [Highlight(l, a, b, k) for (l, a, b), k in sorted(best.items())]
297
+
298
+ def semantic_tokens(self, path) -> list[SemanticToken]:
299
+ out: dict[tuple[int, int], SemanticToken] = {}
300
+ for s in self._path_sites(path):
301
+ rec = s.rec
302
+ if not isinstance(rec, bindings.Declaration) or rec.scope == "predefined":
303
+ continue
304
+ ttype = _KIND_TYPE.get(rec.kind)
305
+ if ttype is None:
306
+ continue
307
+ if rec.kind == "const" and rec.container is not None:
308
+ ttype = "property"
309
+ mods = ("declaration", "readonly") if rec.kind == "const" else ("declaration",)
310
+ out.setdefault((s.line, s.start), SemanticToken(s.line, s.start, s.end - s.start, ttype, mods))
311
+ return [out[k] for k in sorted(out)]
312
+
313
+
314
+ def _ref_kind(ref: bindings.Reference) -> str:
315
+ if ref.is_write or ref.role == bindings.WRITE:
316
+ return "write"
317
+ return "read" if ref.role in _READ_ROLES else "text"
318
+
319
+
320
+ def _locations(sites) -> list[Location]:
321
+ merged: dict[tuple, list] = {}
322
+ for s in sites:
323
+ key = (str(s.path) if s.path is not None else "", s.file, s.line, s.start, s.end)
324
+ entry = merged.setdefault(key, [s, []])
325
+ if s.occurrence not in entry[1]:
326
+ entry[1].append(s.occurrence)
327
+ out = [Location(s.path, s.file, s.line, s.start, s.end, tuple(sorted(occ))) for s, occ in merged.values()]
328
+ return sorted(out, key=lambda l: (str(l.path or ""), l.file, l.line, l.start))
329
+
330
+
331
+ # ------------------------------------------------------------------ module functions
332
+
333
+ _last: tuple | None = None # (result, index argument, Navigation): the latest analysis
334
+
335
+
336
+ def navigation(result, index: bindings.BindingIndex | None = None) -> Navigation:
337
+ """The `Navigation` of `result`, reused while the same `AnalysisResult` object is queried."""
338
+ global _last
339
+ if _last is not None and _last[0] is result and _last[1] is index:
340
+ return _last[2]
341
+ nav = Navigation(result, index)
342
+ _last = (result, index, nav)
343
+ return nav
344
+
345
+
346
+ def definition(result, path, line: int, col: int, *, index=None) -> list[Location]:
347
+ """Declaration locations of the name at (`line`, `col`) of `path`; all candidates when the name
348
+ is ambiguous, none when unresolved, external (catalog) or not an indexed name."""
349
+ return navigation(result, index).definition(path, line, col)
350
+
351
+
352
+ def references(result, path, line: int, col: int, include_declaration: bool = True, *, index=None) -> list[Location]:
353
+ """Every reference to the declaration(s) of the name at the position, in every file."""
354
+ return navigation(result, index).references(path, line, col, include_declaration)
355
+
356
+
357
+ def _decl_key(site: Site) -> tuple:
358
+ return (site.path, site.line, site.start, site.end)
359
+
360
+
361
+ def declaration_sites(navs, path, line: int, col: int) -> list[Site]:
362
+ """The declaration sites the name at the position resolves to in any of `navs` (one
363
+ `Navigation` per analysis; those that do not read `path` contribute nothing), one per range."""
364
+ out: dict[tuple, Site] = {}
365
+ for nav in navs:
366
+ for s in nav.declaration_sites(path, line, col):
367
+ out.setdefault(_decl_key(s), s)
368
+ return list(out.values())
369
+
370
+
371
+ def references_across(navs, path, line: int, col: int, include_declaration: bool = True) -> list[Location]:
372
+ """References over several analyses (LSP-R4a): the name at the position is resolved in every
373
+ nav that reads `path`; each resolved declaration with an exact range is then found in every
374
+ nav by its token's file and position (`Navigation.declaration_at`), and the references of all
375
+ of them are united. A range listed by several analyses appears once, with the `occurrences`
376
+ of the first analysis (in `navs` order) that lists it."""
377
+ navs = list(navs)
378
+ keys = [_decl_key(s) for s in declaration_sites(navs, path, line, col)]
379
+ merged: dict[tuple, Location] = {}
380
+ for nav in navs:
381
+ nav._build()
382
+ targets: list[bindings.Declaration] = list(nav._targets(path, line, col))
383
+ for p, ln, a, b in keys:
384
+ if p is None:
385
+ continue
386
+ for d in nav.declaration_at(p, ln, a, b):
387
+ if all(d is not t for t in targets):
388
+ targets.append(d)
389
+ for loc in _locations(nav._reference_sites(targets, include_declaration)):
390
+ merged.setdefault((str(loc.path or ""), loc.file, loc.line, loc.start, loc.end), loc)
391
+ return sorted(merged.values(), key=lambda l: (str(l.path or ""), l.file, l.line, l.start))
392
+
393
+
394
+ def document_highlights(result, path, line: int, col: int, *, index=None) -> list[Highlight]:
395
+ """The occurrences in `path` of the declaration(s) of the name at the position."""
396
+ return navigation(result, index).document_highlights(path, line, col)
397
+
398
+
399
+ def semantic_tokens(result, path, *, index=None) -> list[SemanticToken]:
400
+ """Declared names of `path`, sorted by position."""
401
+ return navigation(result, index).semantic_tokens(path)