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
opencrbasic/cli.py ADDED
@@ -0,0 +1,391 @@
1
+ """`crbc`: a drop-in for CR6Comp.exe (and the CR1000X/CR300/CR350 compilers).
2
+
3
+ crbc [options] program [messagefile]
4
+
5
+ CR6Comp-compatible options (docs/cli.md): -d, -v, -s, -z FILE (binary TDF), -l FILE / -c FILE
6
+ (table list), -p FILE / -i FILE (preprocessed source; -i also expands Include/IncludeSection),
7
+ -t (the `[Times]` section), -b FILE (CDM binary image; writes the "No CDM devices..." message
8
+ when the program uses none), -u (adds `%s declared but not used.` warnings for an unreferenced
9
+ Public/Dim/Sub-or-Function-parameter, and for an uncalled Sub; `checker/unused.py`). -m is
10
+ accepted (matching the exe's own option set) but has no implemented effect: oracle-confirmed to
11
+ have none observable from the CLI either. Extra long options: --logger
12
+ {cr6,cr1000x,cr300,cr350} (default: from the file extension, else cr6) and --no-message-file (do
13
+ not write the message file), --strict (opencrbasic checks, `checks.py`, on stderr only), --report FILE
14
+ (JSON report, `docs/checker.md` "Check configuration and report file"), --config FILE and
15
+ --check CODE=SEVERITY (check configuration; both only act together with --strict or --report).
16
+ Output goes to stdout and to the message file (`<LOGGER>cmple.xxx` by default, e.g. CR6cmple.xxx;
17
+ CRLF line endings);
18
+ exit status 0 on success, 1 on failure, and with --strict 3 when the program compiles but a
19
+ check reports an error, 4 when it compiles but the check configuration is invalid.
20
+ """
21
+
22
+ import io
23
+ import json
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Literal, TypedDict, cast
28
+
29
+ from . import __version__, profiles
30
+ from . import syntax as S
31
+
32
+
33
+ class _Options(TypedDict):
34
+ s: bool
35
+ v: bool
36
+ d: bool
37
+ z: str | None
38
+ l: str | None
39
+ c: str | None
40
+ p: str | None
41
+ i: bool
42
+ t: bool
43
+ b: str | None
44
+ u: bool
45
+ logger: str | None
46
+ message_file: bool
47
+ strict: bool
48
+ report: str | None
49
+ config: str | None
50
+ check: list[str]
51
+
52
+
53
+ def _parse(argv: list[str]) -> tuple[_Options, list[str]]:
54
+ opts: _Options = {
55
+ "s": False,
56
+ "v": False,
57
+ "d": False,
58
+ "z": None,
59
+ "l": None,
60
+ "c": None,
61
+ "p": None,
62
+ "i": False,
63
+ "t": False,
64
+ "b": None,
65
+ "u": False,
66
+ "logger": None,
67
+ "message_file": True,
68
+ "strict": False,
69
+ "report": None,
70
+ "config": None,
71
+ "check": [],
72
+ }
73
+ positional: list[str] = []
74
+ i = 0
75
+ while i < len(argv):
76
+ arg = argv[i]
77
+ if arg.startswith("--"):
78
+ if arg in ("--logger", "--report", "--config", "--check"):
79
+ value = argv[i + 1] if i + 1 < len(argv) else ""
80
+ i += 2
81
+ elif arg.split("=", 1)[0] in ("--logger", "--report", "--config", "--check"):
82
+ arg, value = arg.split("=", 1)
83
+ i += 1
84
+ else:
85
+ value = None
86
+ if value is not None:
87
+ if arg == "--check":
88
+ opts["check"].append(value)
89
+ else:
90
+ opts[cast(Literal["logger", "report", "config"], arg[2:])] = value # noqa: F821 -- ruff reads the Literal strings as names
91
+ continue
92
+ if arg == "--no-message-file":
93
+ opts["message_file"] = False
94
+ elif arg == "--strict":
95
+ opts["strict"] = True
96
+ elif arg in ("--help", "-h"):
97
+ print(__doc__)
98
+ sys.exit(0)
99
+ else:
100
+ print("illegal option!")
101
+ i += 1
102
+ continue
103
+ if arg.startswith("-") and len(arg) > 1 and not positional:
104
+ letters = arg[1:]
105
+ j = 0
106
+ while j < len(letters):
107
+ ch = letters[j].lower()
108
+ if ch in "zlcpib":
109
+ rest = letters[j + 1 :]
110
+ if rest:
111
+ value = rest
112
+ else:
113
+ i += 1
114
+ value = argv[i] if i < len(argv) else ""
115
+ key = cast(Literal["z", "l", "c", "p", "b"], "p" if ch == "i" else ch)
116
+ opts[key] = value
117
+ if ch == "i":
118
+ opts["i"] = True
119
+ break
120
+ if ch in "svd":
121
+ opts[cast(Literal["s", "v", "d"], ch)] = True # noqa: F821
122
+ elif ch == "t":
123
+ opts["t"] = True
124
+ elif ch == "u":
125
+ opts["u"] = True
126
+ elif ch == "m":
127
+ pass
128
+ else:
129
+ print("illegal option!")
130
+ j += 1
131
+ i += 1
132
+ continue
133
+ positional.append(arg)
134
+ i += 1
135
+ return opts, positional
136
+
137
+
138
+ def _message_bytes(lines: list[str]) -> bytes:
139
+ """`lines` joined the way the message file actually wants them: CRLF throughout, except the
140
+ blank line straight after a `-- Compile Failed!` header, which the real exe writes as a bare
141
+ LF (oracle-confirmed on both a plain failing compile and a `-t`/`-b` one, `docs/cli.md`
142
+ "Message file layout" -- previously assumed to be a uniform CRLF join, which this replaces).
143
+ """
144
+ if len(lines) >= 2 and lines[1] == "" and lines[0].endswith("Failed!"):
145
+ return (lines[0] + "\r\n\n" + "\r\n".join(lines[2:]) + "\r\n").encode("latin-1", "replace")
146
+ return ("\r\n".join(lines) + "\r\n").encode("latin-1", "replace")
147
+
148
+
149
+ def _write_message_file(path: Path, lines: list[str]) -> None:
150
+ path.write_bytes(_message_bytes(lines))
151
+
152
+
153
+ _P1_HIDERS = ("Could not find variables",)
154
+ _SYMBOL = "Invalid symbol in expression"
155
+
156
+
157
+ def _hides_times(program_ast, diags, main) -> bool:
158
+ """Errors that suppress `[Times]` entries though our checker raises them after pass 1
159
+ (`docs/cli.md` "-t"): undefined/invalid symbols, a missing include extension, and prose lines ahead of
160
+ `BeginProg` (`Expr illegally occurs before BeginProg.`). An undefined symbol hides them only
161
+ ahead of `BeginProg` and outside a `Sub`/`Function` body and an assignment (a table
162
+ instruction's argument, a prose line): `X = Nope` in the program or a table body,
163
+ `Randomize(Nope)` in a `Sub` or the scan still print (oracle, round 9)."""
164
+ stmts = program_ast.statements
165
+ begin = next((st for st in stmts if getattr(st, "keyword", "").upper() == "BEGINPROG"), None)
166
+ keep: set[tuple[str, int]] = set() # lines whose symbol error does not hide
167
+ in_sub = False
168
+ for st in stmts:
169
+ if isinstance(st, S.SubDef):
170
+ in_sub = True
171
+ if in_sub or isinstance(st, S.Assignment):
172
+ keep.add((st.file, st.line))
173
+ if S.ends_routine(st):
174
+ in_sub = False
175
+ for d in diags.errors:
176
+ if (
177
+ d.message.startswith(_P1_HIDERS)
178
+ or "must have valid program" in d.message
179
+ or d.message == "Expr illegally occurs before BeginProg."
180
+ ):
181
+ return True
182
+ if (
183
+ d.message.startswith(_SYMBOL)
184
+ and (begin is None or d.file != begin.file or d.line < begin.line)
185
+ and (d.file, d.line) not in keep
186
+ ):
187
+ return True
188
+ return False
189
+
190
+
191
+ # CR300/CR350 compile in one pass; the errors below come from an instruction's own argument checks
192
+ # and do not hide `[Times]` (CHM corpus, oracle 2026-10-12: every other error kind does; the settling
193
+ # range, tests/fixtures/timing_r16.json `cr300_measure`, oracle 2026-10-24).
194
+ _SINGLE_PASS_KEEPS = re.compile(
195
+ r"FileManage invalid drive|Invalid file type option|Illegal Semaphore|"
196
+ r"Invalid drive or no drive specified|IF or SELECT without EndIf|Could not find \S+\.BytesPerRecord|"
197
+ r"Illegal dimension|exceeds number of valid channels|Settling time -?\d+ too (short|long)|Variable \S+ out of bounds|is not a (Status or )?Settings Field Name|Could not find DataTable (?!variables)"
198
+ )
199
+
200
+
201
+ def _pass1_ok(result, profile=None) -> bool:
202
+ """`-t`'s `[Times]` entries follow the checker's own pass-1 gate, not `result.ok`:
203
+ `dhcprenew_1.CR6` (a CHM corpus example) fails on a pass-2-only channel-range error yet still
204
+ gets a full `[Times]` section, while `readonly_1.CR6` (a pass-1 parse error) gets none
205
+ (`docs/cli.md` "-t"). The checker records that gate as `result.pass1_errors`."""
206
+ if profile is not None and profile.single_pass:
207
+ return not any(not _SINGLE_PASS_KEEPS.search(d.message) for d in result.diagnostics.errors)
208
+ return result.ok if result.pass1_errors is None else not result.pass1_errors
209
+
210
+
211
+ def main(argv: list[str] | None = None) -> int:
212
+ from .compiler import compile_file
213
+
214
+ if argv is None: # a process run: stdout carries the source bytes, like the message file
215
+ cast(io.TextIOWrapper, sys.stdout).reconfigure(encoding="latin-1", errors="replace")
216
+ argv = sys.argv[1:]
217
+ opts, positional = _parse(argv)
218
+ profile = profiles.get(opts["logger"]) if opts["logger"] else None
219
+ version_line = None
220
+ if opts["v"] or opts["d"]:
221
+ p = profile or profiles.get("cr6")
222
+ version_line = f"crbc {__version__} VERSION:{p.version} DATE:{p.date}"
223
+ print(version_line)
224
+ if opts["d"]:
225
+ return 0
226
+ if len(positional) > 2:
227
+ return 1
228
+ # each compiler names its own message file: CR6cmple.xxx, CR1000Xcmple.xxx, ... (artifact matrix, M5)
229
+ message_path = (
230
+ Path(positional[1])
231
+ if len(positional) == 2
232
+ else Path(f"{(profile or profiles.get('cr6')).name.upper()}cmple.xxx")
233
+ )
234
+ if not positional:
235
+ lines = [" -- Compile Failed!", "", "No Program File"]
236
+ print("\n".join(lines))
237
+ if opts["message_file"]:
238
+ _write_message_file(message_path, lines)
239
+ return 1
240
+
241
+ program = Path(positional[0])
242
+ if profile is None:
243
+ profile = profiles.for_file(program)
244
+ if len(positional) < 2:
245
+ message_path = Path(f"{profile.name.upper()}cmple.xxx")
246
+ if not program.is_file():
247
+ lines = [f"{positional[0]} -- Compile Failed!", "", "No Program File"]
248
+ print("\n".join(lines))
249
+ if opts["message_file"]:
250
+ _write_message_file(message_path, lines)
251
+ if opts["report"]:
252
+ from . import checks
253
+
254
+ _write_report(opts["report"], checks.missing_program_report(program.name, profile.name, opts["strict"]))
255
+ return 1
256
+
257
+ result = compile_file(program, profile, display_name=program.name, warn_unused=opts["u"], provenance=False)
258
+ if opts["p"]:
259
+ from . import preprocess
260
+ from .compiler import dropped_message
261
+
262
+ dropped = dropped_message(result) if result.pass1_errors else None
263
+ stop = (
264
+ None
265
+ if dropped is None or not dropped.line
266
+ else (
267
+ re.sub(r"^[A-Za-z]+:", "", dropped.file or program.name).replace("\\", "/").rsplit("/", 1)[-1],
268
+ dropped.line,
269
+ )
270
+ )
271
+ try:
272
+ Path(opts["p"]).write_bytes(preprocess.preprocess(program, profile, expand_includes=opts["i"], stop=stop))
273
+ except OSError:
274
+ pass
275
+ lines = result.result_lines(show_signature=opts["s"])
276
+
277
+ # `-t` (`[Times]`) and `-b` (CDM image) both need the parsed program; re-parsed once, shared,
278
+ # only when asked for (docs/cli.md: this does not affect `result`, which already parsed its
279
+ # own copy inside `compile_file`).
280
+ program_ast = None
281
+ if opts["t"] or opts["b"]:
282
+ from . import parser as parser_mod
283
+
284
+ try:
285
+ program_ast, _ = parser_mod.parse_file(program, profile=profile, provenance=False)
286
+ except Exception:
287
+ program_ast = None
288
+
289
+ times_entries: list[str] = []
290
+ if (
291
+ opts["t"]
292
+ and program_ast is not None
293
+ and _pass1_ok(result, profile)
294
+ and not _hides_times(program_ast, result.diagnostics, program.name)
295
+ ):
296
+ from . import timing
297
+
298
+ try:
299
+ times_entries = timing.times_lines(program_ast, profile, program.name)
300
+ except Exception:
301
+ times_entries = []
302
+
303
+ cdm_message = None
304
+ if opts["b"]:
305
+ uses_cdm = False
306
+ if program_ast is not None:
307
+ for stmt in program_ast.statements:
308
+ kw = getattr(stmt, "keyword", "")
309
+ if kw.startswith("CDM") or kw in ("CPIADDMODULE2", "CPISPEED2", "CPIFILESEND"):
310
+ uses_cdm = True
311
+ break
312
+ try:
313
+ Path(opts["b"]).write_bytes(b"") # the image format itself is not implemented
314
+ except OSError:
315
+ pass
316
+ if not uses_cdm or result.pass1_errors:
317
+ # a pass-1 error stops the compile before any CDM device is registered (oracle, CR6,
318
+ # round 74, `docs/artifacts.md`)
319
+ cdm_message = "No CDM devices used in this program "
320
+ print(cdm_message) # before the result, as in the message file (artifact matrix, M5)
321
+ print("\n".join(lines))
322
+
323
+ if opts["message_file"]:
324
+ body = b""
325
+ if version_line:
326
+ body += ("[Version]\r\n" + version_line + "\r\n").encode("latin-1", "replace")
327
+ if opts["t"]:
328
+ times_blob = "[Times]" + "".join(f"\r\n{e}" for e in times_entries)
329
+ body += times_blob.encode("latin-1", "replace")
330
+ if times_entries:
331
+ body += b"\r\n"
332
+ if cdm_message is not None:
333
+ body += (cdm_message + "\r\n").encode("latin-1", "replace")
334
+ body += _message_bytes(lines)
335
+ Path(message_path).write_bytes(body)
336
+
337
+ if not result.ok and opts["z"]:
338
+ # a failed compile still writes the TDF version byte, and no table list (`docs/artifacts.md`)
339
+ Path(opts["z"]).write_bytes(b"\x01")
340
+ if result.ok:
341
+ if opts["z"] and result.tables:
342
+ from . import tdf
343
+
344
+ Path(opts["z"]).write_bytes(tdf.serialize(result.tables))
345
+ for key in ("l", "c"):
346
+ if (listing := opts[key]) and result.table_list is not None:
347
+ Path(listing).write_bytes(result.table_list.encode("latin-1", "replace"))
348
+ status = 0 if result.ok else 1
349
+ if opts["strict"] or opts["report"]:
350
+ status = _run_checks(opts, result, program, status)
351
+ return status
352
+
353
+
354
+ def _write_report(path: str, report: dict) -> None:
355
+ try:
356
+ Path(path).write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
357
+ except OSError as e:
358
+ print(f"opencrbasic: cannot write report: {e}", file=sys.stderr)
359
+
360
+
361
+ def _run_checks(opts: _Options, result, program: Path, status: int) -> int:
362
+ """opencrbasic checks (`checks.py`), after the reference output is final: stderr and the
363
+ report file only, and the exit status moves from 0 only (3: a check error, 4: invalid check
364
+ configuration, `--strict` only). Checks run only when the reference accepted the program."""
365
+ from . import checkconfig, checks
366
+
367
+ config_file, error = None, None
368
+ try:
369
+ config_file = Path(opts["config"]) if opts["config"] else checkconfig.find(program)
370
+ config = checkconfig.load(config_file, checks.DEFAULT_SEVERITY) if config_file else checkconfig.Config()
371
+ cli = checkconfig.parse_overrides(opts["check"], checks.DEFAULT_SEVERITY)
372
+ if result.ok:
373
+ checks.analyze(result, program, config, cli=cli)
374
+ except checkconfig.ConfigError as e:
375
+ error = str(e)
376
+ print(f"opencrbasic: configuration error: {error}", file=sys.stderr)
377
+ if opts["strict"]:
378
+ for f in result.findings:
379
+ print(f.text(program.name), file=sys.stderr)
380
+ if opts["report"]:
381
+ _write_report(opts["report"], checks.report(result, program.name, opts["strict"], config_file, error))
382
+ if status == 0 and opts["strict"]:
383
+ if error:
384
+ return 4
385
+ if any(f.severity == "error" for f in result.findings):
386
+ return 3
387
+ return status
388
+
389
+
390
+ if __name__ == "__main__":
391
+ sys.exit(main())
@@ -0,0 +1,128 @@
1
+ """Editor commands (LSP-09b): the compiler output `crbc` would print, and the data tables a
2
+ program defines. No second pipeline: `compiler_output` is `compiler.compile_file` over a
3
+ `SourceProvider` and `table_fields` is `tables.build_tables`, the call `tables_hook.attach` makes.
4
+ Neither writes a file. See `docs/lsp-api.md` "Commands (LSP-09b)".
5
+ """
6
+
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+
11
+ from . import profiles
12
+ from .compiler import compile_file
13
+ from .sources import DiskSource, LimitedSource, OverlaySource, SourceProvider
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class CompilerOutput:
18
+ text: str # exactly what `crbc` prints to stdout (newline-terminated)
19
+ ok: bool
20
+ program_name: str # the name in the header line (the entry's file name)
21
+ # files over `max_file_bytes` (path -> size): when not empty, `text` is "" (no output)
22
+ oversized: Mapping[Path, int] = field(default_factory=dict)
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class FieldInfo:
27
+ name: str
28
+ type: str
29
+ units: str | None
30
+ process: str | None # the TDF processing text (`Average`, `Sample`, ...); None: empty
31
+ dimension: int = 1
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TableInfo:
36
+ name: str
37
+ fields: tuple[FieldInfo, ...]
38
+ interval: str | None # as `-l` prints it ("5 MIN"); None: no interval
39
+
40
+
41
+ def compiler_output(
42
+ entry: Path, logger: str = "cr6", overlays: Mapping[Path, bytes] | None = None, max_file_bytes: int | None = None
43
+ ) -> CompilerOutput:
44
+ """The text `crbc [--logger LOGGER] --no-message-file ENTRY` prints, with `overlays` (path ->
45
+ encoded unsaved buffer) on top of the disk. An entry that neither exists nor is overlaid gives
46
+ the CLI's "No Program File" text. With `max_file_bytes`, a file larger than that (the entry or
47
+ one it includes) is not read (`sources.LimitedSource`); the result then has no text and lists
48
+ the files in `oversized`."""
49
+ entry = Path(entry).absolute()
50
+ profile = profiles.get(logger)
51
+ overlay = OverlaySource(DiskSource(), overlays)
52
+ limited = None if max_file_bytes is None else LimitedSource(overlay, max_file_bytes)
53
+ source: SourceProvider = overlay if limited is None else limited
54
+ if source.read(entry) is None:
55
+ if limited is not None and limited.oversized:
56
+ return CompilerOutput("", False, entry.name, dict(limited.oversized))
57
+ lines = [f"{entry} -- Compile Failed!", "", "No Program File"]
58
+ return CompilerOutput("\n".join(lines) + "\n", False, entry.name)
59
+ result = compile_file(entry, profile, display_name=entry.name, source=source, signature=False)
60
+ if limited is not None and limited.oversized:
61
+ return CompilerOutput("", False, result.program, dict(limited.oversized))
62
+ return CompilerOutput("\n".join(result.result_lines()) + "\n", result.ok, result.program)
63
+
64
+
65
+ _TYPE_NAMES = {
66
+ 1: "UINT1",
67
+ 2: "UINT2",
68
+ 3: "UINT4",
69
+ 6: "LONG",
70
+ 7: "FP2",
71
+ 9: "IEEE4",
72
+ 11: "String",
73
+ 14: "NSEC",
74
+ 17: "BOOL8",
75
+ 18: "IEEE8",
76
+ 28: "Boolean",
77
+ }
78
+
79
+
80
+ def table_fields(result) -> list[TableInfo]:
81
+ """The tables the compiler would define for `result` (an `AnalysisResult`), in TDF order, the
82
+ `Status`, `DataTableInfo` and `Public` tables included. Empty when the program did not
83
+ compile or has no parsed program (the compiler builds tables only after a clean check)."""
84
+ program = result.program
85
+ if program is None or not result.ok:
86
+ return []
87
+ from .tables import _seconds_to_display, build_tables
88
+
89
+ profile = profiles.get(result.request.logger)
90
+ try:
91
+ tables, _, _ = build_tables(program, profile, dict(program.consts))
92
+ except Exception:
93
+ return []
94
+ out = []
95
+ for t in tables:
96
+ interval = None
97
+ shown = getattr(t, "_interval_display", None)
98
+ if shown is not None:
99
+ value, unit = shown
100
+ interval = f"{int(value) if float(value).is_integer() else value} {unit}"
101
+ elif t.interval != (0, 0):
102
+ value, unit = _seconds_to_display(t.interval[0])
103
+ interval = f"{value} {unit}"
104
+ fields = tuple(
105
+ FieldInfo(
106
+ f.name, _TYPE_NAMES.get(f.type, f"type {f.type}"), f.units or None, f.processing or None, f.dimension
107
+ )
108
+ for f in t.fields
109
+ )
110
+ out.append(TableInfo(t.name, fields, interval))
111
+ return out
112
+
113
+
114
+ def render_table_fields(tables: list[TableInfo]) -> str:
115
+ """Markdown: a heading per table, then a field table."""
116
+ if not tables:
117
+ return "No tables (the program does not compile).\n"
118
+ lines = []
119
+ for t in tables:
120
+ lines.append(f"## {t.name}" + (f" (interval {t.interval})" if t.interval else ""))
121
+ lines.append("")
122
+ lines.append("| Field | Type | Units | Process |")
123
+ lines.append("|---|---|---|---|")
124
+ for f in t.fields:
125
+ name = f.name + (f"({f.dimension})" if f.dimension > 1 and f.type != "String" else "")
126
+ lines.append(f"| {name} | {f.type} | {f.units or ''} | {f.process or ''} |")
127
+ lines.append("")
128
+ return "\n".join(lines)