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,310 @@
1
+ """Indentation formatter for editor tooling (LSP-07b), kept free of any protocol library.
2
+
3
+ `format_indentation` returns edits that replace the *leading whitespace* of physical lines and
4
+ nothing else: no text after the first non-blank character changes, the line count and the line
5
+ endings stay, and no physical line is made longer than the loader's 510-character buffer allows.
6
+ The language server converts a `TextEdit` to an LSP edit like a `editing.Span`. Rules, refusals and
7
+ the preservation evidence: `docs/lsp-api.md` "Formatting (LSP-07b)".
8
+
9
+ Block words are not listed here: the structure comes from `editing` (`_roles`, `_classify`,
10
+ `_block_events`), the same classification folding and the outline use.
11
+ """
12
+
13
+ import bisect
14
+ import re
15
+ from collections import defaultdict
16
+ from dataclasses import dataclass
17
+ from typing import NamedTuple
18
+
19
+ from . import lexer, source
20
+ from .editing import _block_events, _classify, _roles, _tolerant_tokens
21
+ from .parser import _DIRECTIVE_RE, _split_text_segments
22
+
23
+ __all__ = ["TextEdit", "apply_edits", "format_indentation", "format_range", "format_refusal"]
24
+
25
+ _UNITS_RE = re.compile(r"\s*Units\b", re.I)
26
+ _MID_WORDS = frozenset({"CASE", "ELSE", "ELSEIF"})
27
+ _CASE = "CASE" # a virtual frame: the body of one `Case` of a `Select Case`
28
+ _IMPLICIT_END = "SLOWSEQUENCE" # editing._IMPLICIT_END: ends at the next one or at EndProg
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class TextEdit:
33
+ """Replace `text[line][start:end]` by `new_text`. `line` is 0-based; columns are decoded
34
+ (Latin-1) columns, i.e. byte offsets into the physical line, before LSP position conversion.
35
+ Every edit of this module has `start == 0`."""
36
+
37
+ line: int
38
+ start: int
39
+ end: int
40
+ new_text: str
41
+
42
+
43
+ def apply_edits(text: str, edits: list[TextEdit]) -> str:
44
+ """`text` with `edits` applied (edits must not overlap; any order)."""
45
+ lines = text.split("\n")
46
+ for e in edits:
47
+ lines[e.line] = lines[e.line][: e.start] + e.new_text + lines[e.line][e.end :]
48
+ return "\n".join(lines)
49
+
50
+
51
+ def format_refusal(result) -> str | None:
52
+ """Why formatting must not be offered for the file behind an `AnalysisResult` (or
53
+ `CompileResult`): it has errors. None: formatting may be offered. Formatting itself works on
54
+ text alone and never reads a result; this is the policy gate for the server."""
55
+ if getattr(result, "implementation_failures", None) or getattr(result, "implementation_exceptions", None):
56
+ return "the analysis failed internally"
57
+ if not result.ok:
58
+ return "the program has errors; fix them before formatting"
59
+ return None
60
+
61
+
62
+ # ------------------------------------------------------------------ the plan
63
+
64
+
65
+ class _Frame(NamedTuple):
66
+ family: str
67
+ last: int # last physical line of the opener statement (0-based)
68
+
69
+
70
+ class _Dir(NamedTuple):
71
+ base: list # block stack at the `#If`
72
+ ends: list # block stack at the end of each earlier branch
73
+ first: int
74
+ last: int
75
+ has_else: bool
76
+
77
+
78
+ class _Group(NamedTuple):
79
+ first: int
80
+ last: int
81
+ level: int
82
+ enter: int # block depth before the statement
83
+ directive: bool
84
+ skip: bool # edits suppressed (the statement is left alone)
85
+
86
+
87
+ def _load(text: str) -> tuple[list[source.LogicalLine], int | None]:
88
+ """The loader's logical lines (includes unresolved) up to the first overflow line, and that
89
+ line (0-based; the loader, and the compiler, stop reading there)."""
90
+ lines, _ = source.load_text(text, "t", directory=None)
91
+ out = []
92
+ for ll in lines:
93
+ if ll.phys is None:
94
+ continue
95
+ if ll.kind == "overflow":
96
+ return out, ll.phys[0] - 1
97
+ if ll.kind in ("code", "include_warning", "include_error"):
98
+ out.append(ll)
99
+ return out, None
100
+
101
+
102
+ def _phys(ll: source.LogicalLine) -> tuple[int, int]:
103
+ """The physical span of a logical line, which `source.load_text` sets on every line it makes."""
104
+ assert ll.phys is not None
105
+ return ll.phys
106
+
107
+
108
+ def _inside_string(ll: source.LogicalLine) -> bool:
109
+ """True when the text cannot be read as tokens, a continuation line starts inside a string, or
110
+ a continued line holds a `Units` segment (its label is verbatim text, blanks included): changing
111
+ the leading blanks of a continuation line would change the program."""
112
+ if _phys(ll)[1] > _phys(ll)[0] and any(_UNITS_RE.match(seg) for seg in _split_text_segments(ll.text)):
113
+ return True
114
+ try:
115
+ tokens = lexer.tokenize(ll.text)
116
+ except lexer.LexError as e:
117
+ if e.message == "unterminated string" or _phys(ll)[1] > _phys(ll)[0]:
118
+ return True
119
+ return False
120
+ strings = [(t.col, t.col + len(t.text)) for t in tokens if t.kind == "STRING"]
121
+ for piece in ll.pieces:
122
+ if piece.line > _phys(ll)[0] and piece.col == 0:
123
+ if any(a < piece.start < b for a, b in strings):
124
+ return True
125
+ return False
126
+
127
+
128
+ def _plan(text: str, profile: str) -> tuple[dict[int, int], int]:
129
+ """`({0-based physical line: indent level}, exclusive line limit)`: the level every editable
130
+ line should have, and the first line from which on nothing is edited."""
131
+ roles = _roles(profile)
132
+ lines = [ln.removesuffix("\r") for ln in text.split("\n")]
133
+ logical, overflow = _load(text)
134
+ events: dict[int, list[tuple[str, str]]] = defaultdict(list)
135
+ for role, family, _name, first, _last in _block_events(text, profile):
136
+ if family != "#IF":
137
+ events[first].append((role, family))
138
+
139
+ limit = len(lines) if overflow is None else overflow
140
+ stack: list[_Frame] = []
141
+ dirs: list[_Dir] = []
142
+ groups: list[_Group] = []
143
+
144
+ def drop(k: int) -> None:
145
+ nonlocal limit
146
+ for f in stack[k + 1 :]:
147
+ if f.family not in (_IMPLICIT_END, _CASE):
148
+ limit = min(limit, f.last + 1) # never closed: nothing after it is edited
149
+ del stack[k:]
150
+
151
+ def close(family: str, first: int) -> None:
152
+ nonlocal limit
153
+ for k in range(len(stack) - 1, -1, -1):
154
+ if stack[k].family == family:
155
+ drop(k)
156
+ return
157
+ limit = min(limit, first) # a closer with no opener
158
+
159
+ halted = False
160
+ for ll in logical:
161
+ first, last = _phys(ll)[0] - 1, _phys(ll)[1] - 1
162
+ if halted or first >= limit:
163
+ break
164
+ enter = len(stack)
165
+ d = _DIRECTIVE_RE.match(ll.text) if ll.kind == "code" else None
166
+ if d:
167
+ kind = d.group(1).upper()
168
+ if kind in ("IF", "IF_NO_REMOVE"):
169
+ dirs.append(_Dir(list(stack), [], first, last, False))
170
+ elif kind in ("ELSE", "ELSEIF") and dirs:
171
+ top = dirs.pop()
172
+ top.ends.append(list(stack))
173
+ stack[:] = top.base
174
+ dirs.append(top._replace(has_else=top.has_else or kind == "ELSE"))
175
+ elif kind == "ENDIF" and dirs:
176
+ top = dirs.pop()
177
+ ends = top.ends + [list(stack)]
178
+ if not top.has_else:
179
+ ends.append(top.base)
180
+ if all([f.family for f in e] == [f.family for f in ends[0]] for e in ends):
181
+ stack[:] = ends[-1]
182
+ else:
183
+ limit = min(limit, last + 1) # branches leave different blocks open
184
+ groups.append(_Group(first, last, 0, enter, True, False))
185
+ continue
186
+ evs = [e for e in events.get(first, [])]
187
+ segs = _split_text_segments(ll.text) if ll.kind == "code" else []
188
+ toks = [t for t in _tolerant_tokens(segs[0]) if t.kind != "EOF"] if segs else []
189
+ word = toks[0].upper if toks and toks[0].kind == "IDENT" else ""
190
+ second = toks[1].upper if len(toks) > 1 and toks[1].kind == "IDENT" else None
191
+ lead = _classify(word, second, roles) if word else None
192
+ if word == "CASE":
193
+ if stack and stack[-1].family == _CASE:
194
+ stack.pop()
195
+ level = len(stack)
196
+ if stack and stack[-1].family == "SELECT":
197
+ stack.append(_Frame(_CASE, last))
198
+ elif word in ("ELSE", "ELSEIF"):
199
+ level = len(stack) - 1 if stack and stack[-1].family == "IF" else len(stack)
200
+ else:
201
+ if lead and lead[0] == "close" and evs and evs[0] == lead:
202
+ close(lead[1], first)
203
+ evs = evs[1:]
204
+ elif lead == ("open", _IMPLICIT_END):
205
+ for k in range(len(stack) - 1, -1, -1):
206
+ if stack[k].family == _IMPLICIT_END:
207
+ drop(k)
208
+ break
209
+ level = len(stack)
210
+ for role, family in evs:
211
+ if role == "open":
212
+ if family == _IMPLICIT_END:
213
+ for k in range(len(stack) - 1, -1, -1):
214
+ if stack[k].family == _IMPLICIT_END:
215
+ drop(k)
216
+ break
217
+ stack.append(_Frame(family, last))
218
+ else:
219
+ close(family, first)
220
+ if ll.kind == "code" and not dirs and not stack and lead == ("close", "BEGINPROG"):
221
+ halted = True # the compiler stops reading at EndProg
222
+ limit = min(limit, last + 1)
223
+ groups.append(_Group(first, last, level, enter, False, _inside_string(ll) if ll.kind == "code" else False))
224
+ for f in stack:
225
+ if f.family not in (_IMPLICIT_END, _CASE):
226
+ limit = min(limit, f.last + 1) # still open at the end of the text
227
+ for dr in dirs:
228
+ limit = min(limit, dr.last + 1)
229
+
230
+ levels: dict[int, int] = {}
231
+ for g in groups:
232
+ if g.skip:
233
+ continue
234
+ levels[g.first] = g.level
235
+ for k in range(g.first + 1, g.last + 1):
236
+ levels[k] = g.level + 1 # continuation lines (and pulled lines)
237
+ levels = {n: v for n, v in levels.items() if n < limit}
238
+ covered = {k for g in groups for k in range(g.first, g.last + 1)}
239
+ starts = [g.first for g in groups]
240
+ for n, line in enumerate(lines):
241
+ if n in covered or not line.lstrip(" \t").startswith("'") or n >= limit:
242
+ continue
243
+ j = bisect.bisect_right(starts, n)
244
+ if j >= len(groups):
245
+ levels[n] = len(stack) if not halted else 0
246
+ continue
247
+ g = groups[j]
248
+ levels[n] = g.enter if g.directive else g.level
249
+ return levels, limit
250
+
251
+
252
+ def _leading(line: str) -> int:
253
+ return len(line) - len(line.lstrip(" \t"))
254
+
255
+
256
+ def format_indentation(text: str, profile: str = "cr6", indent: str = " ") -> list[TextEdit]:
257
+ """Edits that set the leading whitespace of the lines of `text` (decoded Latin-1) to
258
+ `indent * level`. See the module docstring and `docs/lsp-api.md` for the rules."""
259
+ return _format(text, profile, indent, 0, None)
260
+
261
+
262
+ def format_range(text: str, start_line: int, end_line: int, profile: str = "cr6", indent: str = " ") -> list[TextEdit]:
263
+ """`format_indentation` restricted to the 0-based physical lines `start_line..end_line`
264
+ (inclusive). Levels are computed from the whole text, so the range needs no context."""
265
+ return _format(text, profile, indent, start_line, end_line)
266
+
267
+
268
+ def _format(text: str, profile: str, indent: str, lo: int, hi: int | None) -> list[TextEdit]:
269
+ if indent.strip(" \t") or not indent:
270
+ raise ValueError("indent must be a non-empty string of spaces and tabs")
271
+ levels, _limit = _plan(text, profile)
272
+ lines = text.split("\n")
273
+ edits: dict[int, TextEdit] = {}
274
+ for n, level in levels.items():
275
+ if n < lo or (hi is not None and n > hi):
276
+ continue
277
+ body = lines[n].removesuffix("\r")
278
+ if not body.strip(" \t"):
279
+ continue # blank lines keep their (trailing) blanks
280
+ width = _leading(body)
281
+ want = indent * level
282
+ if body[:width] != want:
283
+ edits[n] = TextEdit(n, 0, width, want)
284
+ return _within_buffer(text, lines, edits)
285
+
286
+
287
+ def _first_overflow(text: str) -> tuple[int, int] | None:
288
+ """Physical lines (first, last; 0-based) of the first overflow of `text`, None: no overflow."""
289
+ loaded, _ = source.load_text(text, "t", directory=None)
290
+ for ll in loaded:
291
+ if ll.kind == "overflow":
292
+ return _phys(ll)[0] - 1, _phys(ll)[1] - 1
293
+ return None
294
+
295
+
296
+ def _within_buffer(text: str, lines: list[str], edits: dict[int, TextEdit]) -> list[TextEdit]:
297
+ """Drop the edits of any statement that a new indentation would push over the loader's
298
+ 510-character buffer (a statement's lines are counted together when continued)."""
299
+ base = _first_overflow(text)
300
+ while edits:
301
+ new = apply_edits(text, list(edits.values()))
302
+ found = _first_overflow(new)
303
+ if found is None or found == base:
304
+ break
305
+ hit = [n for n in edits if found[0] <= n <= found[1]]
306
+ if not hit:
307
+ break
308
+ for n in hit:
309
+ del edits[n]
310
+ return [edits[n] for n in sorted(edits)]