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/catalog.py ADDED
@@ -0,0 +1,173 @@
1
+ """Instruction catalog: the names a CRBasic program can use, per logger profile.
2
+
3
+ Built only from `profiles.get(name)` (`instructions`, `functions`, `symbols`, the tables extracted
4
+ from each logger's compiler by `tools/extract_tables.py`). There is no second instruction list.
5
+ `docs/lsp-api.md` ("Instruction catalog") describes the kinds, the overlaps and the keyword rule.
6
+ """
7
+
8
+ import functools
9
+ from collections.abc import Iterator
10
+ from dataclasses import dataclass
11
+
12
+ from . import profiles
13
+
14
+ KINDS = ("instruction", "function", "symbol", "keyword")
15
+
16
+ # Words that structure a program or a declaration. Each is also a row of the instruction table of
17
+ # the profiles that have it (the parser dispatches on it in `Parser._parse_statement`, or reads it
18
+ # as an ordinary instruction row such as `NextScan`, `EndTable`). A profile gets the entry only
19
+ # when its own instruction table has the word, with the table's spelling.
20
+ TABLE_KEYWORDS = (
21
+ "If",
22
+ "ElseIf",
23
+ "Else",
24
+ "EndIf",
25
+ "Select",
26
+ "Case",
27
+ "EndSelect",
28
+ "For",
29
+ "Next",
30
+ "Do",
31
+ "Loop",
32
+ "While",
33
+ "WEnd",
34
+ "Exit",
35
+ "ExitFor",
36
+ "ExitDo",
37
+ "ExitSub",
38
+ "ExitFunction",
39
+ "ExitScan",
40
+ "ContinueScan",
41
+ "Return",
42
+ "Sub",
43
+ "EndSub",
44
+ "Function",
45
+ "EndFunction",
46
+ "Call",
47
+ "End",
48
+ "Const",
49
+ "Public",
50
+ "Dim",
51
+ "Alias",
52
+ "Units",
53
+ "Include",
54
+ "IncludeSection",
55
+ "BeginProg",
56
+ "EndProg",
57
+ "Scan",
58
+ "SubScan",
59
+ "NextScan",
60
+ "NextSubScan",
61
+ "DataTable",
62
+ "EndTable",
63
+ "ConstTable",
64
+ "EndConstTable",
65
+ )
66
+
67
+ # Language words the instruction table lacks. Each is matched by name in the parser
68
+ # (`parser.py`): `Then` (`_parse_if`, `_parse_elseif`), `Step` (`_parse_for`), `As` (declarations,
69
+ # `_parse_subdef`, `_parse_param`), `Optional` (`_parse_param`). They are the same on every profile.
70
+ EXTRA_KEYWORDS = ("Then", "Step", "As", "Optional")
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class CatalogEntry:
75
+ name: str # the profile's spelling
76
+ kind: str # one of KINDS
77
+ profile: str # profile name ("cr6", ...)
78
+ source: str # "instructions.json" | "functions.json" | "symbols.json" |
79
+ # "keyword:table" | "keyword:parser"
80
+ params: tuple[int, ...] = () # instruction: type codes (docs/type-codes.md)
81
+ value: int | None = None # symbol: the integer value
82
+ code: int | None = None # function: table `code`
83
+ func_kind: int | None = None # function: table `kind`
84
+ argspec: int | None = None # function: table `argspec`
85
+ nargs: int | None = None # function: present only for some rows
86
+ argtypes: tuple[int, ...] | None = None
87
+
88
+
89
+ def _build(name: str) -> tuple[CatalogEntry, ...]:
90
+ prof = profiles.get(name)
91
+ out: list[CatalogEntry] = []
92
+ for row in prof.instructions.values():
93
+ out.append(
94
+ CatalogEntry(
95
+ row["name"], "instruction", prof.name, "instructions.json", params=tuple(row.get("params", ()))
96
+ )
97
+ )
98
+ for row in prof.functions.values():
99
+ argtypes = row.get("argtypes")
100
+ out.append(
101
+ CatalogEntry(
102
+ row["name"],
103
+ "function",
104
+ prof.name,
105
+ "functions.json",
106
+ code=row.get("code"),
107
+ func_kind=row.get("kind"),
108
+ argspec=row.get("argspec"),
109
+ nargs=row.get("nargs"),
110
+ argtypes=None if argtypes is None else tuple(argtypes),
111
+ )
112
+ )
113
+ for row in prof.symbols.values():
114
+ out.append(CatalogEntry(row["name"], "symbol", prof.name, "symbols.json", value=row.get("value")))
115
+ for word in TABLE_KEYWORDS:
116
+ row = prof.instructions.get(word.upper())
117
+ if row is not None:
118
+ out.append(CatalogEntry(row["name"], "keyword", prof.name, "keyword:table"))
119
+ for word in EXTRA_KEYWORDS:
120
+ out.append(CatalogEntry(word, "keyword", prof.name, "keyword:parser"))
121
+ return tuple(out)
122
+
123
+
124
+ class InstructionCatalog:
125
+ """Every name of one profile, by kind. Lookup is case-insensitive and keeps the profile's
126
+ spelling. A name may exist under several kinds (`If` is an instruction row, a symbol and a
127
+ keyword); `get` returns one entry by the priority instruction, function, symbol, keyword and
128
+ `entries` returns all of them."""
129
+
130
+ def __init__(self, profile: str):
131
+ self.profile = profiles.get(profile).name
132
+ self._entries = _build(self.profile)
133
+ self._by_name: dict[str, list[CatalogEntry]] = {}
134
+ for entry in self._entries:
135
+ self._by_name.setdefault(entry.name.upper(), []).append(entry)
136
+
137
+ @classmethod
138
+ def for_profile(cls, name: str) -> InstructionCatalog:
139
+ """The cached catalog of a profile name (`ValueError` for an unknown one)."""
140
+ return _cached(profiles.get(name).name)
141
+
142
+ def entries(self, name: str) -> tuple[CatalogEntry, ...]:
143
+ return tuple(self._by_name.get(name.upper(), ()))
144
+
145
+ def get(self, name: str, kind: str | None = None) -> CatalogEntry | None:
146
+ found = self._by_name.get(name.upper(), ())
147
+ if kind is not None:
148
+ return next((e for e in found if e.kind == kind), None)
149
+ for k in KINDS:
150
+ for e in found:
151
+ if e.kind == k:
152
+ return e
153
+ return None
154
+
155
+ def names(self, kind: str | None = None) -> tuple[str, ...]:
156
+ """Profile spellings of one kind (all kinds when None), sorted case-insensitively and
157
+ without repeats."""
158
+ seen = {e.name for e in self._entries if kind is None or e.kind == kind}
159
+ return tuple(sorted(seen, key=lambda n: (n.upper(), n)))
160
+
161
+ def __iter__(self) -> Iterator[CatalogEntry]:
162
+ return iter(self._entries)
163
+
164
+ def __len__(self) -> int:
165
+ return len(self._entries)
166
+
167
+ def __contains__(self, name: object) -> bool:
168
+ return isinstance(name, str) and name.upper() in self._by_name
169
+
170
+
171
+ @functools.cache
172
+ def _cached(name: str) -> InstructionCatalog:
173
+ return InstructionCatalog(name)
@@ -0,0 +1,144 @@
1
+ """`opencrbasic.toml`: check severities and scoped suppressions (`PLAN.md` "Configuration",
2
+ `docs/checker.md` "Check configuration and report file"). Configuration never touches the reference
3
+ channel: every problem is a `ConfigError` that the caller reports on stderr."""
4
+
5
+ import fnmatch
6
+ import os
7
+ import tomllib
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+
11
+ from . import profiles
12
+
13
+ FILE_NAME = "opencrbasic.toml"
14
+ SEVERITIES = ("error", "warning", "info", "off")
15
+
16
+
17
+ class ConfigError(Exception):
18
+ pass
19
+
20
+
21
+ @dataclass
22
+ class Suppression:
23
+ code: str
24
+ file: str # glob, relative to the configuration file, case-insensitive
25
+ line: int | None
26
+ reason: str
27
+
28
+
29
+ @dataclass
30
+ class Config:
31
+ path: Path | None = None # None: no file, built-in defaults
32
+ checks: dict[str, str] = field(default_factory=dict)
33
+ profile: dict[str, dict[str, str]] = field(default_factory=dict)
34
+ entry: dict[str, dict[str, str]] = field(default_factory=dict) # key: normalized relative path
35
+ suppress: list[Suppression] = field(default_factory=list)
36
+
37
+ @property
38
+ def root(self) -> Path | None:
39
+ return self.path.parent if self.path else None
40
+
41
+
42
+ def norm(path: str) -> str:
43
+ return Path(os.path.normpath(path)).as_posix().lower()
44
+
45
+
46
+ def find(entry: Path) -> Path | None:
47
+ """The workspace root: the nearest ancestor directory of the entry file holding the file."""
48
+ for directory in entry.resolve().parents:
49
+ if (directory / FILE_NAME).is_file():
50
+ return directory / FILE_NAME
51
+ return None
52
+
53
+
54
+ def _severities(table: object, codes, where: str) -> dict[str, str]:
55
+ if not isinstance(table, dict):
56
+ raise ConfigError(f"{where} must be a table")
57
+ out = {}
58
+ for code, value in table.items():
59
+ if code not in codes:
60
+ raise ConfigError(f"{where}: unknown check code {code}")
61
+ if value not in SEVERITIES:
62
+ raise ConfigError(f"{where}: unknown severity {value!r} for {code}; choose from {', '.join(SEVERITIES)}")
63
+ out[code] = value
64
+ return out
65
+
66
+
67
+ def parse_overrides(items: list[str], codes) -> dict[str, str]:
68
+ """`CODE=severity` strings (`--check`, an LSP setting) as a severity map."""
69
+ out = {}
70
+ for item in items:
71
+ code, sep, value = item.partition("=")
72
+ if not sep:
73
+ raise ConfigError(f"--check {item}: expected CODE=SEVERITY")
74
+ out.update(_severities({code: value}, codes, f"--check {item}"))
75
+ return out
76
+
77
+
78
+ def load(path: Path, codes) -> Config:
79
+ try:
80
+ text = path.read_text(encoding="utf-8")
81
+ except (OSError, UnicodeDecodeError) as e:
82
+ raise ConfigError(f"{path}: cannot read: {e}") from None
83
+ return loads(text, path, codes)
84
+
85
+
86
+ def loads(text: str, path: Path, codes) -> Config:
87
+ """`load` of a file's text already read (the language server reads it through its sources)."""
88
+ try:
89
+ data = tomllib.loads(text)
90
+ except tomllib.TOMLDecodeError as e:
91
+ raise ConfigError(f"{path}: {e}") from None
92
+ name = str(path)
93
+ if unknown := set(data) - {"checks", "profile", "entry", "suppress"}:
94
+ raise ConfigError(f"{name}: unknown key {sorted(unknown)[0]}")
95
+ cfg = Config(path=path.resolve())
96
+ cfg.checks = _severities(data.get("checks", {}), codes, f"{name} [checks]")
97
+ for logger, table in _tables(data, "profile", name).items():
98
+ if logger not in profiles.PROFILES:
99
+ raise ConfigError(f"{name} [profile.{logger}]: unknown logger profile")
100
+ cfg.profile[logger] = _severities(table.get("checks", {}), codes, f"{name} [profile.{logger}.checks]")
101
+ for entry, table in _tables(data, "entry", name).items():
102
+ cfg.entry[norm(entry)] = _severities(table.get("checks", {}), codes, f'{name} [entry."{entry}".checks]')
103
+ suppress = data.get("suppress", [])
104
+ if not isinstance(suppress, list):
105
+ raise ConfigError(f"{name}: suppress must be an array of tables ([[suppress]])")
106
+ for i, s in enumerate(suppress, 1):
107
+ where = f"{name} [[suppress]] #{i}"
108
+ if not isinstance(s, dict) or set(s) - {"code", "file", "line", "reason"}:
109
+ raise ConfigError(f"{where}: keys are code, file, line, reason")
110
+ if not isinstance(s.get("code"), str) or s["code"] not in codes:
111
+ raise ConfigError(f"{where}: unknown check code {s.get('code')}")
112
+ if not isinstance(s.get("file"), str) or not s["file"]:
113
+ raise ConfigError(f"{where}: file (a glob) is required")
114
+ if not isinstance(s.get("reason"), str) or not s["reason"].strip():
115
+ raise ConfigError(f"{where}: reason is required")
116
+ line = s.get("line")
117
+ if line is not None and (not isinstance(line, int) or isinstance(line, bool) or line < 1):
118
+ raise ConfigError(f"{where}: line must be a positive integer")
119
+ cfg.suppress.append(Suppression(s["code"], s["file"].lower(), line, s["reason"]))
120
+ return cfg
121
+
122
+
123
+ def _tables(data: dict, key: str, name: str) -> dict[str, dict]:
124
+ tables = data.get(key, {})
125
+ if not isinstance(tables, dict) or not all(isinstance(t, dict) for t in tables.values()):
126
+ raise ConfigError(f"{name}: {key} must hold tables ([{key}.<name>.checks])")
127
+ return tables
128
+
129
+
130
+ def severity(
131
+ cfg: Config, code: str, default: str, logger: str, entry: str, cli: dict[str, str], lsp: dict[str, str]
132
+ ) -> str:
133
+ """Highest layer that names `code`: CLI, LSP, entry, profile, `[checks]`, built-in default."""
134
+ for layer in (cli, lsp, cfg.entry.get(entry, {}), cfg.profile.get(logger, {}), cfg.checks):
135
+ if code in layer:
136
+ return layer[code]
137
+ return default
138
+
139
+
140
+ def suppression(cfg: Config, code: str, file: str, line: int) -> Suppression | None:
141
+ return next(
142
+ (s for s in cfg.suppress if s.code == code and fnmatch.fnmatchcase(file, s.file) and s.line in (None, line)),
143
+ None,
144
+ )