opencrbasic 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
opencrbasic/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())
|
opencrbasic/commands.py
ADDED
|
@@ -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)
|