opencrbasic 1.0.0__tar.gz → 1.1.1__tar.gz
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-1.0.0 → opencrbasic-1.1.1}/PKG-INFO +20 -5
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/README.md +19 -4
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/pyproject.toml +4 -2
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/__init__.py +1 -1
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/analysis.py +2 -1
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/__init__.py +3 -4
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/cr3xx.py +3 -4
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/expressions.py +3 -5
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/instr_data.py +5 -7
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/instr_rules.py +1 -1
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/structure.py +8 -16
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/terminals.py +3 -3
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/warnings.py +4 -9
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checks.py +4 -4
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/cli.py +22 -5
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/compiler.py +8 -4
- opencrbasic-1.1.1/src/opencrbasic/macros.py +161 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/parser.py +2 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/source.py +26 -1
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/tests/corpus/README.md +21 -21
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/.gitignore +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/LICENSE +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/__main__.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/bindings.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/catalog.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checkconfig.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/ess.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/instr_measure.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/instructions.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/intervals.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/longconv.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/scan_time.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/storage.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/structs.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/symbols.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/unused.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/checker/var_types.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/commands.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/consteval.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/background_calibration.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/before_beginprog.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/background_calibration.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/before_beginprog.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/ess_variables.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/fnotch.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/functions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/instructions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/measure.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/mode.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/pipeline_tasks.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/settings_fields.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/status_members.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/symbols.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/terminals.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/before_beginprog.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/ess_variables.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/fnotch.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/functions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/instructions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/measure.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/mode.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/settings_fields.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/status_members.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/symbols.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr300/terminals.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/before_beginprog.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/ess_variables.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/fnotch.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/functions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/instructions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/measure.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/mode.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/settings_fields.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/status_members.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/symbols.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr350/terminals.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/ess_variables.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/fnotch.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/functions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/instructions.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/measure.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/mode.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/notices_cr6.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/pipeline_tasks.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/settings_fields.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/status_members.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/status_table.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/symbols.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/terminals.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/type_codes.json +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/diagnostics.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/editing.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/formatting.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/helpdocs.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/hover.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/inlay.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lexer.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/__init__.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/__main__.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/config.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/features.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/server.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/session.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/lsp/worker.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/mode.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/navigation.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/positions.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/preprocess.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/profiles.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/provenance.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/quickfix.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/rename.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/signature.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/sources.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/syntax.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/tables.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/tables_hook.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/tdf.py +0 -0
- {opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/timing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opencrbasic
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Open-source re-implementation of the Campbell Scientific CRBasic pre-compilers (CR6, CR1000X, CR300, CR350)
|
|
5
5
|
Project-URL: Repository, https://github.com/UtrechtUniversity/open-crbasic
|
|
6
6
|
Author: Utrecht University
|
|
@@ -38,8 +38,8 @@ Status, measured against the real compilers: `PROGRESS.md`. Design and the v1.0
|
|
|
38
38
|
## Scope
|
|
39
39
|
|
|
40
40
|
- **Gated profiles: CR6 and CR1000X.** On these, v1.0 gates the reference verdict in the relevant
|
|
41
|
-
area (the
|
|
42
|
-
first error for realistic mistakes on relevant rules, measured on the
|
|
41
|
+
area (the station/GEUS instruction set and language constructs), the execution mode, and the
|
|
42
|
+
first error for realistic mistakes on relevant rules, measured on the station programs, the 507
|
|
43
43
|
CRBasic help examples, the 66 GEUS programs, the saved fuzz replay and fresh `--relevant` fuzz
|
|
44
44
|
seeds. The help examples and GEUS programs are not redistributed here; see
|
|
45
45
|
[tests/corpus/README.md](tests/corpus/README.md) for running against local copies. One relevant-area false accept is known: a `Sub` name that is also another routine's
|
|
@@ -118,6 +118,22 @@ crbc [options] program [messagefile]
|
|
|
118
118
|
--report FILE write a JSON report (reference verdict and diagnostics, check findings)
|
|
119
119
|
--config FILE use this opencrbasic.toml (default: nearest ancestor of the program)
|
|
120
120
|
--check CODE=SEVERITY set a check severity (error|warning|info|off); repeatable
|
|
121
|
+
--expand FILE expand #Define/#Macro macros into FILE, then compile FILE instead
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`#Define` and `#Macro` are an opencrbasic extension, not CRBasic: the reference compilers reject
|
|
125
|
+
them. `--expand` writes plain CRBasic (definitions commented out, uses expanded inline) that
|
|
126
|
+
`CR6Comp.exe` accepts and that is the file to send to the logger; write it next to the source so
|
|
127
|
+
its `Include`s resolve. The language server expands them the same way, so the editor reports
|
|
128
|
+
the expansion's diagnostics on the line of each use. Syntax and rules:
|
|
129
|
+
[`macros.py`](src/opencrbasic/macros.py).
|
|
130
|
+
|
|
131
|
+
```crbasic
|
|
132
|
+
#Define LOGD(msg) DebugMsg = msg ' release build: #Define LOGD(msg)
|
|
133
|
+
#Macro LOGV(name, value)
|
|
134
|
+
DebugMsg = name & "=" & value
|
|
135
|
+
CallTable Debug
|
|
136
|
+
#EndMacro
|
|
121
137
|
```
|
|
122
138
|
|
|
123
139
|
Check configuration and the report schema: `docs/checker.md` "Check configuration and report file".
|
|
@@ -143,8 +159,7 @@ result.tables # list of tdf.TdfTable (Status, DataTables, DataTableInfo, ConstT
|
|
|
143
159
|
`parser.parse_file()` gives the syntax tree (`syntax.py`) for other tools.
|
|
144
160
|
|
|
145
161
|
[openloggernet](https://github.com/UtrechtUniversity/open-loggernet) uses opencrbasic when
|
|
146
|
-
`CR6COMP=opencrbasic` is set
|
|
147
|
-
CI.
|
|
162
|
+
`CR6COMP=opencrbasic` is set.
|
|
148
163
|
|
|
149
164
|
## Editor support
|
|
150
165
|
|
|
@@ -21,8 +21,8 @@ Status, measured against the real compilers: `PROGRESS.md`. Design and the v1.0
|
|
|
21
21
|
## Scope
|
|
22
22
|
|
|
23
23
|
- **Gated profiles: CR6 and CR1000X.** On these, v1.0 gates the reference verdict in the relevant
|
|
24
|
-
area (the
|
|
25
|
-
first error for realistic mistakes on relevant rules, measured on the
|
|
24
|
+
area (the station/GEUS instruction set and language constructs), the execution mode, and the
|
|
25
|
+
first error for realistic mistakes on relevant rules, measured on the station programs, the 507
|
|
26
26
|
CRBasic help examples, the 66 GEUS programs, the saved fuzz replay and fresh `--relevant` fuzz
|
|
27
27
|
seeds. The help examples and GEUS programs are not redistributed here; see
|
|
28
28
|
[tests/corpus/README.md](tests/corpus/README.md) for running against local copies. One relevant-area false accept is known: a `Sub` name that is also another routine's
|
|
@@ -101,6 +101,22 @@ crbc [options] program [messagefile]
|
|
|
101
101
|
--report FILE write a JSON report (reference verdict and diagnostics, check findings)
|
|
102
102
|
--config FILE use this opencrbasic.toml (default: nearest ancestor of the program)
|
|
103
103
|
--check CODE=SEVERITY set a check severity (error|warning|info|off); repeatable
|
|
104
|
+
--expand FILE expand #Define/#Macro macros into FILE, then compile FILE instead
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`#Define` and `#Macro` are an opencrbasic extension, not CRBasic: the reference compilers reject
|
|
108
|
+
them. `--expand` writes plain CRBasic (definitions commented out, uses expanded inline) that
|
|
109
|
+
`CR6Comp.exe` accepts and that is the file to send to the logger; write it next to the source so
|
|
110
|
+
its `Include`s resolve. The language server expands them the same way, so the editor reports
|
|
111
|
+
the expansion's diagnostics on the line of each use. Syntax and rules:
|
|
112
|
+
[`macros.py`](src/opencrbasic/macros.py).
|
|
113
|
+
|
|
114
|
+
```crbasic
|
|
115
|
+
#Define LOGD(msg) DebugMsg = msg ' release build: #Define LOGD(msg)
|
|
116
|
+
#Macro LOGV(name, value)
|
|
117
|
+
DebugMsg = name & "=" & value
|
|
118
|
+
CallTable Debug
|
|
119
|
+
#EndMacro
|
|
104
120
|
```
|
|
105
121
|
|
|
106
122
|
Check configuration and the report schema: `docs/checker.md` "Check configuration and report file".
|
|
@@ -126,8 +142,7 @@ result.tables # list of tdf.TdfTable (Status, DataTables, DataTableInfo, ConstT
|
|
|
126
142
|
`parser.parse_file()` gives the syntax tree (`syntax.py`) for other tools.
|
|
127
143
|
|
|
128
144
|
[openloggernet](https://github.com/UtrechtUniversity/open-loggernet) uses opencrbasic when
|
|
129
|
-
`CR6COMP=opencrbasic` is set
|
|
130
|
-
CI.
|
|
145
|
+
`CR6COMP=opencrbasic` is set.
|
|
131
146
|
|
|
132
147
|
## Editor support
|
|
133
148
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opencrbasic"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.1"
|
|
4
4
|
description = "Open-source re-implementation of the Campbell Scientific CRBasic pre-compilers (CR6, CR1000X, CR300, CR350)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -31,7 +31,8 @@ crbc-lsp = "opencrbasic.lsp.server:main"
|
|
|
31
31
|
# `uv sync` installs `dev` by default: the test runner and the reverse-engineering tools
|
|
32
32
|
# (`tools/extract_tables.py`, `tools/message_catalog.py` read the exe with pefile), and pygls
|
|
33
33
|
# so the suite can test the language server; pytest-xdist for parallel test runs (`-n`); pyrefly
|
|
34
|
-
# checks the repository (`[tool.pyrefly]`); pre-commit runs ruff and pyrefly before each commit
|
|
34
|
+
# checks the repository (`[tool.pyrefly]`); pre-commit runs ruff and pyrefly before each commit;
|
|
35
|
+
# openloggernet drives a logger for `tools/logger_oracle.py`.
|
|
35
36
|
dev = [
|
|
36
37
|
"pytest>=8",
|
|
37
38
|
"pefile>=2024.8.26",
|
|
@@ -39,6 +40,7 @@ dev = [
|
|
|
39
40
|
"pytest-xdist>=3.6",
|
|
40
41
|
"pyrefly==1.3.1",
|
|
41
42
|
"pre-commit>=4",
|
|
43
|
+
"openloggernet>=0.3.0",
|
|
42
44
|
]
|
|
43
45
|
|
|
44
46
|
[build-system]
|
|
@@ -14,7 +14,7 @@ from .compiler import compile_file
|
|
|
14
14
|
from .sources import DiskSource, LimitedSource, OverlaySource, RecordingSource, SourceProvider
|
|
15
15
|
|
|
16
16
|
API_VERSION = 2 # 2: findings, notices, config_error, AnalysisRequest.checks (M6)
|
|
17
|
-
DOCS = "https://github.com/UtrechtUniversity/
|
|
17
|
+
DOCS = "https://github.com/UtrechtUniversity/open-crbasic/blob/main/"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _no_overlays() -> Mapping[Path, bytes]:
|
|
@@ -120,6 +120,7 @@ def analyze(request: AnalysisRequest) -> AnalysisResult:
|
|
|
120
120
|
warn_unused=request.warn_unused,
|
|
121
121
|
source=recorder,
|
|
122
122
|
signature=request.signature,
|
|
123
|
+
macros=True,
|
|
123
124
|
)
|
|
124
125
|
includes = getattr(result.program_ast, "include_paths", None) or {}
|
|
125
126
|
lowered = {name.lower(): path for name, path in includes.items()}
|
|
@@ -953,10 +953,9 @@ def _check(program: S.Program, profile: Profile, diags: Diagnostics, result) ->
|
|
|
953
953
|
|
|
954
954
|
if not diags.errors:
|
|
955
955
|
# `When Main Sequence Scan %d is running, background calibration will be disabled.`:
|
|
956
|
-
#
|
|
957
|
-
#
|
|
958
|
-
#
|
|
959
|
-
# `check_background_calibration` docstring has the oracle evidence). Independent of
|
|
956
|
+
# oracle-confirmed *not* ungated the way `_check_reachability`/`_check_fnotch` are
|
|
957
|
+
# (`checker/warnings.py`'s `check_background_calibration` docstring has the oracle evidence).
|
|
958
|
+
# Independent of
|
|
960
959
|
# `tables_hook`'s own availability, so called ahead of that guard rather than inside it.
|
|
961
960
|
_run_pass(warnings.check_background_calibration, program, profile, diags, env)
|
|
962
961
|
_run_pass(warnings.check_slow_scan_skips, program, profile, diags, env)
|
|
@@ -389,9 +389,8 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
|
|
|
389
389
|
# `acceptdatarecords_1.CR6`, `SerialOpen (COMC1,...)`'s `COMC1 is not defined.` taints every
|
|
390
390
|
# following two-plus-argument instruction to end of file, `Scan`'s own `Sec` and
|
|
391
391
|
# `AcceptDataRecords`'s own `32769`, despite that finding being **ungated** and thus, in this
|
|
392
|
-
# project's own pass model, skipping tier B
|
|
393
|
-
#
|
|
394
|
-
# tier-B block the way `check_expr_cascade` was: `checker/__init__.py` now calls it once,
|
|
392
|
+
# project's own pass model, skipping tier B entirely), this is not called from inside the
|
|
393
|
+
# `if not diags.errors:` tier-B block: `checker/__init__.py` calls it once,
|
|
395
394
|
# unconditionally, after every other diagnostic-producing pass has run, so it can see -- and rewrite
|
|
396
395
|
# -- the complete, final diagnostic list regardless of which tier first tainted the file. It is
|
|
397
396
|
# still a no-op when nothing in the file ever taints (no `_is_cascade_trigger` match and no
|
|
@@ -555,7 +554,7 @@ def check_too_many_cascade(program: S.Program, profile, diags: Diagnostics) -> N
|
|
|
555
554
|
"""CR300/CR350 only; a no-op on every other logger and when nothing in the file ever taints
|
|
556
555
|
(see this module's comment above for the oracle evidence and the exact shape implemented).
|
|
557
556
|
Called from `checker/__init__.py` once, unconditionally, after every other pass -- see that
|
|
558
|
-
comment for why this
|
|
557
|
+
comment for why this cannot be tier-B-only.
|
|
559
558
|
"""
|
|
560
559
|
if profile.name not in ("cr300", "cr350"):
|
|
561
560
|
return
|
|
@@ -25,9 +25,8 @@ _TYPE_CODES_PATH = Path(__file__).resolve().parent.parent / "data" / "type_codes
|
|
|
25
25
|
# separate function (`4`/`16`/`17`/`19` `FUN_004b5db0`, a different failure message). Restricting
|
|
26
26
|
# to this explicit include-list (rather than excluding the handful of kinds known *not* to
|
|
27
27
|
# resolve) is deliberate: the default for an unrecognised kind is "don't check", so a gap in this
|
|
28
|
-
# list only under-reports, never falsely rejects
|
|
29
|
-
#
|
|
30
|
-
# `ConstTable(GlobalConfigTable)`, kind `expr_marker`) is the concrete case that shape of bug
|
|
28
|
+
# list only under-reports, never falsely rejects; the station corpus's `Main.CR6`
|
|
29
|
+
# `ConstTable(GlobalConfigTable)` (kind `expr_marker`) is the concrete case that shape of bug
|
|
31
30
|
# looks like.
|
|
32
31
|
_RESOLVED_KINDS = frozenset(
|
|
33
32
|
{
|
|
@@ -74,8 +73,7 @@ _CONST_FAMILY_KINDS = frozenset({"const_expr", "const_expr_only", "datatype_kw"}
|
|
|
74
73
|
# separate `FooBar is not defined.` / `BazQux is not defined.` lines on CR300/CR350, in the source
|
|
75
74
|
# (left-to-right) order `_undefined_names` already walks in. Modelled below as a plain per-logger
|
|
76
75
|
# branch on `profile.name` (both loggers in each pair share identical behaviour, oracle-confirmed
|
|
77
|
-
# side by side) rather than a new `Profile` field
|
|
78
|
-
# `checker/expressions.py`/`checker/symbols.py` (`PLAN.md`'s ownership split).
|
|
76
|
+
# side by side) rather than a new `Profile` field.
|
|
79
77
|
_WHOLE_EXPR_ECHO_LOGGERS = frozenset({"cr6", "cr1000x"})
|
|
80
78
|
|
|
81
79
|
# `NAN`/`INF` are accepted as predefined float constants on all four loggers (oracle-confirmed,
|
|
@@ -45,13 +45,11 @@ from . import symbols
|
|
|
45
45
|
# `docs/instructions.md`'s "not independently oracle-confirmed" list for exactly which those are.
|
|
46
46
|
# One more call site exists (the unlabelled string `"Et"`, near the `SampleFieldCal` call site)
|
|
47
47
|
# that could not be attributed to a specific instruction with confidence and is deliberately left
|
|
48
|
-
# out
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# `
|
|
52
|
-
#
|
|
53
|
-
# below not yet recognising `FFT` as a field producer, before `FFT` was added here too) and is
|
|
54
|
-
# included as `FFT`.
|
|
48
|
+
# out: never guess into a false reject. The other unlabelled string nearby, `"FFT"`, is
|
|
49
|
+
# independently confirmed (a scratch probe, `tfft.cr6`, and two CHM corpus files whose
|
|
50
|
+
# `DataTable` bodies contain only `FFT(...)` calls, `fft_1.CR6`/`covspa_1.CR6`, which compile
|
|
51
|
+
# only when `FFT` also counts as a field producer for `Table %s declared with no output
|
|
52
|
+
# fields.`) and is included as `FFT`.
|
|
55
53
|
TABLE_ONLY_INSTRUCTIONS: dict[str, str] = {
|
|
56
54
|
"CARDOUT": "CardOut", # checked with the oracle
|
|
57
55
|
"MQTTPUBLISHTABLE": "MQTTPublishTable",
|
|
@@ -1223,7 +1223,7 @@ def check_data_interval(program: S.Program, profile, diags: Diagnostics, env: co
|
|
|
1223
1223
|
per-logger difference, not shared with CR300/CR350 (`_SCAN_VALUE_CHECK_LOGGERS`; found the
|
|
1224
1224
|
same way as `check_scan_params`'s own CR300/CR350 gate, same session).
|
|
1225
1225
|
- `DataTable interval %d uSec not integrally divisible by scan interval %d uSec.` (function
|
|
1226
|
-
`FUN_004f6540`) -- project knowledge (`
|
|
1226
|
+
`FUN_004f6540`) -- project knowledge (open-crbasic-skills `lessons.md` "`DataInterval` versus
|
|
1227
1227
|
`Scan`": "a table interval must be at least the scan interval and an exact integer multiple
|
|
1228
1228
|
of it", human-verified against `CR6Comp.exe`), reported at each `CallTable <name>` site whose
|
|
1229
1229
|
table has a `DataInterval` and which sits directly inside a `Scan...NextScan` block (main
|
|
@@ -2186,7 +2186,7 @@ def check_pass2(
|
|
|
2186
2186
|
"Before `BeginProg`" only means the top level of the program, never a statement lexically
|
|
2187
2187
|
inside a `Sub`/`Function` body (regardless of that body's own textual position, almost always
|
|
2188
2188
|
before `BeginProg` itself, `docs/checker.md` "Pass model": found as a **regression** against
|
|
2189
|
-
|
|
2189
|
+
the station corpus's `Main.CR6` and several siblings, all of which call a built-in
|
|
2190
2190
|
function bare, with no `Call` keyword, e.g. `SerialOpen(...)` inside a `Function`/`Sub` defined
|
|
2191
2191
|
above `BeginProg` -- perfectly ordinary CRBasic, since the body only executes when the
|
|
2192
2192
|
`Sub`/`Function` is later called from inside `Scan`/`NextScan`, oracle-reconfirmed by re-running
|
|
@@ -5120,8 +5120,7 @@ def _check_array_dims(stmt: S.Declaration, diags: Diagnostics, env: consteval.En
|
|
|
5120
5120
|
separate variables of the same declaration.
|
|
5121
5121
|
|
|
5122
5122
|
A dimension that does not fold to a constant at all (`NotConstant`) is left alone: no oracle
|
|
5123
|
-
evidence
|
|
5124
|
-
reject an accepted program) rules out it.
|
|
5123
|
+
evidence supports guessing at that shape, and a guess could reject an accepted program.
|
|
5125
5124
|
"""
|
|
5126
5125
|
for index, v in enumerate(stmt.vars):
|
|
5127
5126
|
before = len(diags.items)
|
|
@@ -5779,24 +5778,17 @@ def _check_alias_dims(
|
|
|
5779
5778
|
check in this project) into a single **row-major flat offset** (`_alias_flat_offset`:
|
|
5780
5779
|
`flat = i_n + sum_{k<n} (i_k - 1) * prod(d_{k+1..n})`, the ordinary multi-dimensional-array-
|
|
5781
5780
|
as-flat-buffer formula, last index varying fastest) that must land in `[1,
|
|
5782
|
-
prod(all declared dims)]`.
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
reasons) -- a fresh, wider oracle sweep this round (`docs/checker.md` "Round 7") found the
|
|
5788
|
-
"first index alone" account is wrong in general: `Alias grid(4,-2) = Ts` (first index `4`,
|
|
5789
|
-
out of `[1,3]` by the old account) **compiles clean** (`(4-1)*3+(-2)=7`, in `[1,9]`), while
|
|
5790
|
-
`Alias grid(2,-1) = Ts` (a valid first index, an out-of-range *second* index by the old
|
|
5791
|
-
account, which the old rule never checked at all) also compiles clean
|
|
5792
|
-
(`(2-1)*3+(-1)=2`, in `[1,9]`) -- both correctly predicted by the flat formula, neither by
|
|
5793
|
-
"only check position 0". A **scalar** (no declared dimensions at all) behaves exactly like a
|
|
5781
|
+
prod(all declared dims)]`. Oracle evidence (`docs/checker.md` "Round 7"), for
|
|
5782
|
+
`Public grid(3,3)`: `Alias grid(2,4) = Ts` compiles (`(2-1)*3+4=7`), `Alias grid(4,2) = Ts`
|
|
5783
|
+
fails (`11`), and `Alias grid(4,-2) = Ts` (`7`) and `Alias grid(2,-1) = Ts` (`2`) both
|
|
5784
|
+
compile clean: the first index is not checked on its own. A **scalar** (no declared
|
|
5785
|
+
dimensions at all) behaves exactly like a
|
|
5794
5786
|
size-1 array (`Public y` / `Alias y(1) = Ts` compiles, `Alias y(2) = Ts` fails: flat
|
|
5795
5787
|
formula with `dims=(1,)` reduces to `flat=i1`, range `[1,1]`), which is why `declared_all_
|
|
5796
5788
|
dims` maps every declared variable, not just array ones, the same convention `dim0_sizes`
|
|
5797
5789
|
already used. Skipped (no check at all, under-reporting only) when any declared dimension or
|
|
5798
5790
|
any index argument does not fold to a constant (`declared_all_dims`/the tuple-folding loop
|
|
5799
|
-
below, both
|
|
5791
|
+
below, both conservative the same way check 3 is) or when the index
|
|
5800
5792
|
tuple's own length does not match the variable's own declared dimension count (no oracle
|
|
5801
5793
|
evidence gathered for that shape either way).
|
|
5802
5794
|
3. **`Duplicate Alias`** (round 7; no trailing period, no echoed name): the source side's own
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Per-logger analog-channel and port terminal model.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Models the analog-channel range check for all four loggers, driven by oracle-probed per-logger
|
|
4
|
+
data (`data/<logger>/terminals.json`; evidence, probes and the message-text differences:
|
|
5
|
+
`docs/loggers.md`, `docs/instructions.md`).
|
|
6
6
|
|
|
7
7
|
Each logger's `terminals.json` names its own channel "kinds" -- CR6 has separate `u_terminal`
|
|
8
8
|
(single-ended-shaped: any `U1..U12`, no oddness constraint; `ACPower`, `CS616`'s `CS616Chan`,
|
|
@@ -405,15 +405,10 @@ def check_background_calibration(program: S.Program, profile: Profile, diags: Di
|
|
|
405
405
|
`Invalid symbol in expression` error present (`Delay never reached at run time.` alongside it,
|
|
406
406
|
same probe file). A warning-only file (no errors, e.g. a reachability warning with no error
|
|
407
407
|
anywhere) still gets *both* warnings together, confirming this is specifically errors, not "any
|
|
408
|
-
diagnostic", that gates it.
|
|
409
|
-
tier (alongside `warnings.check`/`tables_hook.attach
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
`instr_rules.check_scan_min_interval` (this round): dozens of pre-existing `false_accept`/
|
|
413
|
-
`message_diff` fuzz reproducers (`tests/corpus/fuzz/cr6/message_diff/ba333c2ee146.CR6` and
|
|
414
|
-
siblings) already showed this exact extra, spurious warning line alongside an unrelated real
|
|
415
|
-
error -- a genuine, previously-undocumented false-accept-shaped bug independent of this round's
|
|
416
|
-
own `Scan`-interval work, not a regression it introduced.
|
|
408
|
+
diagnostic", that gates it. It is therefore called from `checker/__init__.py`'s final
|
|
409
|
+
`if not diags.errors:` tier (alongside `warnings.check`/`tables_hook.attach`), not from
|
|
410
|
+
`check_early`'s unconditional call site. Fuzz reproducers such as
|
|
411
|
+
`tests/corpus/fuzz/cr6/message_diff/ba333c2ee146.CR6` cover the gate.
|
|
417
412
|
"""
|
|
418
413
|
data = profile.background_calibration
|
|
419
414
|
if not data:
|
|
@@ -147,7 +147,7 @@ def _ambiguous_names(program: S.Program) -> set[str]:
|
|
|
147
147
|
def _numeric_concatenation(program: S.Program, profile) -> list[Finding]:
|
|
148
148
|
"""OCB002: a String `+` (or `&`) assigned to a numeric variable (`L = 2000 + Mid(S,5,2)`). The
|
|
149
149
|
reference accepts it, but `+` with a String operand concatenates, and the variable receives
|
|
150
|
-
the converted text: a GPS clock set this way read 2057 (`
|
|
150
|
+
the converted text: a GPS clock set this way read 2057 (open-crbasic-skills `lessons.md` "`+` with
|
|
151
151
|
a string operand concatenates", hardware 2026-10-22). Only a chain mixing numeric and String
|
|
152
152
|
operands is reported: a String alone (`X = Mid(...)`) is a plain conversion, and Strings joined
|
|
153
153
|
into a number's text (`X = HexStr(6) + "." + HexStr(7)`, GEUS programs) are deliberate."""
|
|
@@ -263,8 +263,8 @@ def _zero_divisors(program: S.Program, profile) -> list[Finding]:
|
|
|
263
263
|
|
|
264
264
|
def _unsized_string_parameters(program: S.Program, profile) -> list[Finding]:
|
|
265
265
|
"""OCB005: a `Sub`/`Function` String parameter without `* size`. It holds 23 characters (24
|
|
266
|
-
bytes with the terminator) and a longer argument is cut (hardware,
|
|
267
|
-
lessons.md` "Default string parameter size"). Information only: a short argument is fine."""
|
|
266
|
+
bytes with the terminator) and a longer argument is cut (hardware, open-crbasic-skills
|
|
267
|
+
`lessons.md` "Default string parameter size"). Information only: a short argument is fine."""
|
|
268
268
|
return [
|
|
269
269
|
Finding(
|
|
270
270
|
"OCB005",
|
|
@@ -284,7 +284,7 @@ def _unsized_string_parameters(program: S.Program, profile) -> list[Finding]:
|
|
|
284
284
|
def _unaligned_intervals(program: S.Program, profile) -> list[Finding]:
|
|
285
285
|
"""OCB006: a constant `DataInterval` that divides neither an hour nor a day (`7 Min`, `25 Hr`).
|
|
286
286
|
Its records are phased from the start hour and may not align with midnight (hardware,
|
|
287
|
-
`
|
|
287
|
+
open-crbasic-skills `lessons.md` "`DataInterval` phase"). Information only; an interval of 0
|
|
288
288
|
(every call) is not reported."""
|
|
289
289
|
env, out = _const_env(program, profile), []
|
|
290
290
|
for st in _statements(program):
|
|
@@ -11,7 +11,8 @@ accepted (matching the exe's own option set) but has no implemented effect: orac
|
|
|
11
11
|
have none observable from the CLI either. Extra long options: --logger
|
|
12
12
|
{cr6,cr1000x,cr300,cr350} (default: from the file extension, else cr6) and --no-message-file (do
|
|
13
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"), --
|
|
14
|
+
(JSON report, `docs/checker.md` "Check configuration and report file"), --expand FILE (expand
|
|
15
|
+
`#Define`/`#Macro` macros into FILE and compile FILE instead, `macros.py`), --config FILE and
|
|
15
16
|
--check CODE=SEVERITY (check configuration; both only act together with --strict or --report).
|
|
16
17
|
Output goes to stdout and to the message file (`<LOGGER>cmple.xxx` by default, e.g. CR6cmple.xxx;
|
|
17
18
|
CRLF line endings);
|
|
@@ -47,6 +48,7 @@ class _Options(TypedDict):
|
|
|
47
48
|
strict: bool
|
|
48
49
|
report: str | None
|
|
49
50
|
config: str | None
|
|
51
|
+
expand: str | None
|
|
50
52
|
check: list[str]
|
|
51
53
|
|
|
52
54
|
|
|
@@ -68,6 +70,7 @@ def _parse(argv: list[str]) -> tuple[_Options, list[str]]:
|
|
|
68
70
|
"strict": False,
|
|
69
71
|
"report": None,
|
|
70
72
|
"config": None,
|
|
73
|
+
"expand": None,
|
|
71
74
|
"check": [],
|
|
72
75
|
}
|
|
73
76
|
positional: list[str] = []
|
|
@@ -75,10 +78,10 @@ def _parse(argv: list[str]) -> tuple[_Options, list[str]]:
|
|
|
75
78
|
while i < len(argv):
|
|
76
79
|
arg = argv[i]
|
|
77
80
|
if arg.startswith("--"):
|
|
78
|
-
if arg in ("--logger", "--report", "--config", "--check"):
|
|
81
|
+
if arg in ("--logger", "--report", "--config", "--expand", "--check"):
|
|
79
82
|
value = argv[i + 1] if i + 1 < len(argv) else ""
|
|
80
83
|
i += 2
|
|
81
|
-
elif arg.split("=", 1)[0] in ("--logger", "--report", "--config", "--check"):
|
|
84
|
+
elif arg.split("=", 1)[0] in ("--logger", "--report", "--config", "--expand", "--check"):
|
|
82
85
|
arg, value = arg.split("=", 1)
|
|
83
86
|
i += 1
|
|
84
87
|
else:
|
|
@@ -87,7 +90,7 @@ def _parse(argv: list[str]) -> tuple[_Options, list[str]]:
|
|
|
87
90
|
if arg == "--check":
|
|
88
91
|
opts["check"].append(value)
|
|
89
92
|
else:
|
|
90
|
-
opts[cast(Literal["logger", "report", "config"], arg[2:])] = value # noqa: F821 -- ruff reads the Literal strings as names
|
|
93
|
+
opts[cast(Literal["logger", "report", "config", "expand"], arg[2:])] = value # noqa: F821 -- ruff reads the Literal strings as names
|
|
91
94
|
continue
|
|
92
95
|
if arg == "--no-message-file":
|
|
93
96
|
opts["message_file"] = False
|
|
@@ -139,7 +142,7 @@ def _message_bytes(lines: list[str]) -> bytes:
|
|
|
139
142
|
"""`lines` joined the way the message file actually wants them: CRLF throughout, except the
|
|
140
143
|
blank line straight after a `-- Compile Failed!` header, which the real exe writes as a bare
|
|
141
144
|
LF (oracle-confirmed on both a plain failing compile and a `-t`/`-b` one, `docs/cli.md`
|
|
142
|
-
"Message file layout"
|
|
145
|
+
"Message file layout").
|
|
143
146
|
"""
|
|
144
147
|
if len(lines) >= 2 and lines[1] == "" and lines[0].endswith("Failed!"):
|
|
145
148
|
return (lines[0] + "\r\n\n" + "\r\n".join(lines[2:]) + "\r\n").encode("latin-1", "replace")
|
|
@@ -254,6 +257,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
254
257
|
_write_report(opts["report"], checks.missing_program_report(program.name, profile.name, opts["strict"]))
|
|
255
258
|
return 1
|
|
256
259
|
|
|
260
|
+
if opts["expand"]:
|
|
261
|
+
from . import macros
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
expanded = macros.expand(program.read_bytes().decode("latin-1"))
|
|
265
|
+
except macros.MacroError as e:
|
|
266
|
+
lines = [f"{positional[0]} -- Compile Failed!", "", f"line {e.line}: {e}"]
|
|
267
|
+
print("\n".join(lines))
|
|
268
|
+
if opts["message_file"]:
|
|
269
|
+
_write_message_file(message_path, lines)
|
|
270
|
+
return 1
|
|
271
|
+
program = Path(opts["expand"])
|
|
272
|
+
program.write_bytes(expanded.encode("latin-1", "replace"))
|
|
273
|
+
|
|
257
274
|
result = compile_file(program, profile, display_name=program.name, warn_unused=opts["u"], provenance=False)
|
|
258
275
|
if opts["p"]:
|
|
259
276
|
from . import preprocess
|
|
@@ -156,7 +156,7 @@ class CompileResult:
|
|
|
156
156
|
# no-line `-u` warning, regardless of which included file declared it, prints before every
|
|
157
157
|
# "table declared but never called" warning). `_no_line_kind` below reproduces that
|
|
158
158
|
# ordering by message shape rather than by which module raised it, so this stays correct
|
|
159
|
-
# without `checker/warnings.py`
|
|
159
|
+
# without `checker/warnings.py` needing to tag its own
|
|
160
160
|
# diagnostics; a stable sort keeps each kind's own relative (insertion) order within itself.
|
|
161
161
|
# On one line the reachability warning follows that line's errors (oracle: 15 of 15 corpus
|
|
162
162
|
# and fuzz transcripts); other warnings keep their check order.
|
|
@@ -200,9 +200,12 @@ def compile_file(
|
|
|
200
200
|
source: SourceProvider | None = None,
|
|
201
201
|
signature: bool = True,
|
|
202
202
|
provenance: bool = True,
|
|
203
|
+
macros: bool = False,
|
|
203
204
|
) -> CompileResult:
|
|
204
205
|
"""`provenance=False` (the CLI and the tools) skips the position data only the language server
|
|
205
206
|
reads (`LogicalLine.pieces`, `Program.line_map`, `Stmt.origin`); the compile output is unchanged.
|
|
207
|
+
`macros=True` (the language server) expands the entry file's `#Define`/`#Macro` macros
|
|
208
|
+
(`macros.py`), which the reference rejects.
|
|
206
209
|
|
|
207
210
|
Never lets an unexpected exception escape (`docs/checker.md` "Robustness"): the real
|
|
208
211
|
compiler never crashes on a syntactically-loadable file, so an internal bug in this project
|
|
@@ -215,7 +218,7 @@ def compile_file(
|
|
|
215
218
|
"""
|
|
216
219
|
profile = profile or profiles.for_file(path)
|
|
217
220
|
name = display_name or path.name
|
|
218
|
-
result = _compile_once(path, profile, name, None, source, provenance)
|
|
221
|
+
result = _compile_once(path, profile, name, None, source, provenance, macros)
|
|
219
222
|
if profile.single_pass:
|
|
220
223
|
# CR300/CR350 stop pulling a `Public`/`Dim` initializer's next line once any error has been
|
|
221
224
|
# reported ahead of the declaration (oracle, 2026-10-10, `docs/checker.md` "Round 8"): re-run
|
|
@@ -233,7 +236,7 @@ def compile_file(
|
|
|
233
236
|
if bad is None or (limit is not None and bad >= limit):
|
|
234
237
|
break
|
|
235
238
|
limit = bad
|
|
236
|
-
result = _compile_once(path, profile, name, limit, source, provenance)
|
|
239
|
+
result = _compile_once(path, profile, name, limit, source, provenance, macros)
|
|
237
240
|
if result.program_ast is None:
|
|
238
241
|
return result
|
|
239
242
|
return _finish(result, path, warn_unused, source, signature)
|
|
@@ -246,12 +249,13 @@ def _compile_once(
|
|
|
246
249
|
pull_limit: tuple[int, ...] | None,
|
|
247
250
|
source: SourceProvider | None = None,
|
|
248
251
|
provenance: bool = True,
|
|
252
|
+
macros: bool = False,
|
|
249
253
|
):
|
|
250
254
|
from . import checker, parser
|
|
251
255
|
|
|
252
256
|
try:
|
|
253
257
|
program, diags = parser.parse_file(
|
|
254
|
-
path, profile=profile, pull_limit=pull_limit, source=source, provenance=provenance
|
|
258
|
+
path, profile=profile, pull_limit=pull_limit, source=source, provenance=provenance, macros=macros
|
|
255
259
|
)
|
|
256
260
|
except Exception as exc:
|
|
257
261
|
diags = Diagnostics()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""`crbc --expand FILE`: opencrbasic's own macro step, not a reference-compiler feature.
|
|
2
|
+
|
|
3
|
+
The reference compilers reject `#Define` (`Unknown instruction`), so a program that uses macros is
|
|
4
|
+
expanded first into plain CRBasic, which any compiler (`crbc`, `CR6Comp.exe`) accepts and which is
|
|
5
|
+
the file to send to the logger. Macros expand inline, so they cost no call depth (the logger runs at
|
|
6
|
+
most four nested `Sub`/`Function` calls).
|
|
7
|
+
|
|
8
|
+
#Define DEBUG 1 object-like, one line
|
|
9
|
+
#Define LOGD(msg) DebugMsg = msg function-like, one line (an empty body is allowed)
|
|
10
|
+
#Macro LOGV(name, value) function-like, several lines
|
|
11
|
+
DebugMsg = name & "=" & value
|
|
12
|
+
CallTable Debug
|
|
13
|
+
#EndMacro
|
|
14
|
+
|
|
15
|
+
- Names and parameters are case-insensitive, like CRBasic. A function-like name not followed by
|
|
16
|
+
`(` is left alone; a macro is defined from its line on and cannot be redefined.
|
|
17
|
+
- Nothing inside a `"..."` string or a `'` comment is expanded. A one-line body loses its
|
|
18
|
+
trailing comment.
|
|
19
|
+
- A macro body is expanded again after substitution, without expanding itself (no recursion).
|
|
20
|
+
- Every other line, `#If` and the other reference directives included, is kept as written apart
|
|
21
|
+
from the expansion of macro names; the definition lines become comments so line numbers before
|
|
22
|
+
the first multi-line expansion stay the same. The lines of a multi-line expansion take the
|
|
23
|
+
indentation of the line that uses it.
|
|
24
|
+
- A macro call must close on its own line. Macros in `Include` files are not read or expanded.
|
|
25
|
+
- The language server expands macros the same way while analysing (`source.py`, the entry file
|
|
26
|
+
only), so a program with macros gets the diagnostics of its expansion, each on the line of the
|
|
27
|
+
macro use. `crbc` without `--expand` compiles like the reference and rejects `#Define`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
import textwrap
|
|
32
|
+
|
|
33
|
+
_DEFINE_RE = re.compile(r"^\s*#\s*(Define|Macro)\s+([A-Za-z_]\w*)(?:\(([^)]*)\))?(.*)$", re.IGNORECASE)
|
|
34
|
+
_END_RE = re.compile(r"^\s*#\s*EndMacro\b", re.IGNORECASE)
|
|
35
|
+
# strings and comments are copied whole; a number or `&H`/`&B` literal is never a name
|
|
36
|
+
_TOKEN_RE = re.compile(r'"[^"\n]*"?|\'[^\n]*|\d\w*|&[HhBb]\w*|[A-Za-z_]\w*|.', re.DOTALL)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MacroError(Exception):
|
|
40
|
+
def __init__(self, line: int, message: str):
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.line = line
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _Macro:
|
|
46
|
+
__slots__ = ("params", "body")
|
|
47
|
+
|
|
48
|
+
def __init__(self, params: list[str] | None, body: str):
|
|
49
|
+
self.params = params # None: object-like
|
|
50
|
+
self.body = body
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _strip_comment(text: str) -> str:
|
|
54
|
+
return "".join(t for t in _TOKEN_RE.findall(text) if not t.startswith("'"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _call_args(text: str, i: int, line: int, name: str) -> tuple[list[str], int]:
|
|
58
|
+
"""The arguments of the call whose `(` is at `text[i]`, and the index after its `)`."""
|
|
59
|
+
args, current, depth = [], [], 0
|
|
60
|
+
for m in _TOKEN_RE.finditer(text, i + 1):
|
|
61
|
+
tok = m.group()
|
|
62
|
+
if tok == "\n":
|
|
63
|
+
break
|
|
64
|
+
if tok == ")" and depth == 0:
|
|
65
|
+
args.append("".join(current).strip())
|
|
66
|
+
return ([] if args == [""] else args), m.end()
|
|
67
|
+
if tok == "," and depth == 0:
|
|
68
|
+
args.append("".join(current).strip())
|
|
69
|
+
current = []
|
|
70
|
+
continue
|
|
71
|
+
depth += {"(": 1, ")": -1}.get(tok, 0)
|
|
72
|
+
current.append(tok)
|
|
73
|
+
raise MacroError(line, f"Macro {name}: missing ).")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _expand(text: str, macros: dict[str, _Macro], line: int, disabled: frozenset[str] = frozenset()) -> str:
|
|
77
|
+
out: list[str] = []
|
|
78
|
+
i = 0
|
|
79
|
+
while m := _TOKEN_RE.match(text, i):
|
|
80
|
+
tok, i = m.group(), m.end()
|
|
81
|
+
key = tok.upper()
|
|
82
|
+
macro = macros.get(key)
|
|
83
|
+
if macro is None or key in disabled:
|
|
84
|
+
out.append(tok)
|
|
85
|
+
continue
|
|
86
|
+
body = macro.body
|
|
87
|
+
if macro.params is not None:
|
|
88
|
+
j = i
|
|
89
|
+
while j < len(text) and text[j] in " \t":
|
|
90
|
+
j += 1
|
|
91
|
+
if j >= len(text) or text[j] != "(":
|
|
92
|
+
out.append(tok)
|
|
93
|
+
continue
|
|
94
|
+
args, i = _call_args(text, j, line, tok)
|
|
95
|
+
if len(args) != len(macro.params):
|
|
96
|
+
raise MacroError(line, f"Macro {tok} takes {len(macro.params)} argument(s), got {len(args)}.")
|
|
97
|
+
values = dict(zip(macro.params, args, strict=True))
|
|
98
|
+
body = "".join(values.get(t.upper(), t) for t in _TOKEN_RE.findall(body))
|
|
99
|
+
body = _expand(body, macros, line, disabled | {key})
|
|
100
|
+
head = text[text.rfind("\n", 0, m.start()) + 1 : m.start()]
|
|
101
|
+
indent = head[: len(head) - len(head.lstrip(" \t"))]
|
|
102
|
+
out.append(body.replace("\n", "\n" + indent))
|
|
103
|
+
return "".join(out)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Macros:
|
|
107
|
+
"""The macros of one file, fed its lines in order (`expand` here, the loader for the editor)."""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
self.table: dict[str, _Macro] = {}
|
|
111
|
+
self._open: tuple[str, list[str] | None, int, list[str]] | None = None # a #Macro being read
|
|
112
|
+
|
|
113
|
+
def feed(self, text: str, line: int) -> str | None:
|
|
114
|
+
"""`text` with macros expanded, or None when it is part of a definition."""
|
|
115
|
+
if self._open is not None:
|
|
116
|
+
if _END_RE.match(text):
|
|
117
|
+
name, params, start, body = self._open
|
|
118
|
+
self._open = None
|
|
119
|
+
self._define(name, params, textwrap.dedent("\n".join(body)).strip("\n"), start)
|
|
120
|
+
else:
|
|
121
|
+
self._open[3].append(text)
|
|
122
|
+
return None
|
|
123
|
+
d = _DEFINE_RE.match(text)
|
|
124
|
+
if d is None:
|
|
125
|
+
return _expand(text, self.table, line)
|
|
126
|
+
kind, name, rest = d.group(1).upper(), d.group(2), d.group(4)
|
|
127
|
+
params = None if d.group(3) is None else [p.strip().upper() for p in d.group(3).split(",")]
|
|
128
|
+
if params == [""]:
|
|
129
|
+
params = []
|
|
130
|
+
if params is not None and (
|
|
131
|
+
not all(re.fullmatch(r"[A-Za-z_]\w*", p) for p in params) or len(set(params)) < len(params)
|
|
132
|
+
):
|
|
133
|
+
raise MacroError(line, f"Macro {name}: invalid parameter list.")
|
|
134
|
+
if kind == "MACRO":
|
|
135
|
+
if params is None or _strip_comment(rest).strip():
|
|
136
|
+
raise MacroError(line, f"#Macro {name} needs a (parameter list) and its body on the following lines.")
|
|
137
|
+
self._open = (name, params, line, [])
|
|
138
|
+
else:
|
|
139
|
+
self._define(name, params, _strip_comment(rest).strip(), line)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def finish(self) -> None:
|
|
143
|
+
if self._open is not None:
|
|
144
|
+
raise MacroError(self._open[2], f"#Macro {self._open[0]} without #EndMacro.")
|
|
145
|
+
|
|
146
|
+
def _define(self, name: str, params: list[str] | None, body: str, line: int) -> None:
|
|
147
|
+
if name.upper() in self.table:
|
|
148
|
+
raise MacroError(line, f"Macro {name} is already defined.")
|
|
149
|
+
self.table[name.upper()] = _Macro(params, body)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def expand(text: str) -> str:
|
|
153
|
+
"""`text` with its `#Define`/`#Macro` definitions commented out and their uses expanded."""
|
|
154
|
+
newline = "\r\n" if "\r\n" in text else "\n"
|
|
155
|
+
macros = Macros()
|
|
156
|
+
out = []
|
|
157
|
+
for n, raw in enumerate(text.splitlines(), 1):
|
|
158
|
+
expanded = macros.feed(raw, n)
|
|
159
|
+
out.append("'" + raw if expanded is None else expanded.replace("\n", newline))
|
|
160
|
+
macros.finish()
|
|
161
|
+
return newline.join(out) + (newline if text.endswith(("\n", "\r")) else "")
|
|
@@ -3174,6 +3174,7 @@ def parse_file(
|
|
|
3174
3174
|
pull_limit: tuple[int, ...] | None = None,
|
|
3175
3175
|
source: SourceProvider | None = None,
|
|
3176
3176
|
provenance: bool = True,
|
|
3177
|
+
macros: bool = False,
|
|
3177
3178
|
) -> tuple[S.Program, Diagnostics]:
|
|
3178
3179
|
"""`provenance=False` skips the pieces/line-map/origin data only the language server reads (the CLI
|
|
3179
3180
|
path); everything the checker sees is unchanged. `profile` is normally left to the file's own extension (`profiles.for_file`, the historical
|
|
@@ -3191,6 +3192,7 @@ def parse_file(
|
|
|
3191
3192
|
_CR3XX_INCLUDE_EXTS.get((profile or _profile_for(path.name)).name),
|
|
3192
3193
|
source,
|
|
3193
3194
|
provenance,
|
|
3195
|
+
macros,
|
|
3194
3196
|
)
|
|
3195
3197
|
pull_log = diags.pull_log
|
|
3196
3198
|
parser = _Parser(profile or _profile_for(path.name), diags, provenance)
|
|
@@ -29,6 +29,7 @@ from pathlib import Path
|
|
|
29
29
|
from typing import NamedTuple
|
|
30
30
|
|
|
31
31
|
from .diagnostics import Diagnostics
|
|
32
|
+
from .macros import MacroError, Macros
|
|
32
33
|
from .sources import DiskSource, SourceProvider
|
|
33
34
|
|
|
34
35
|
_INCLUDE_RE = re.compile(r'^\s*Include\s*\(?\s*"([^"]*)"\s*\)?\s*$', re.IGNORECASE)
|
|
@@ -302,7 +303,9 @@ class _Loader:
|
|
|
302
303
|
cr3xx_exts: tuple[str, ...] | None = None,
|
|
303
304
|
source: SourceProvider | None = None,
|
|
304
305
|
provenance: bool = True,
|
|
306
|
+
macros: bool = False,
|
|
305
307
|
):
|
|
308
|
+
self.macros = Macros() if macros else None # the entry file's macros (`macros.py`)
|
|
306
309
|
self.provenance = provenance # False: skip `LogicalLine.pieces` (only the language server reads them)
|
|
307
310
|
self.source = source or DiskSource()
|
|
308
311
|
self.includes: dict[str, Path] = {} # include display name -> the path it resolved to
|
|
@@ -416,6 +419,22 @@ class _Loader:
|
|
|
416
419
|
if text.strip() == "":
|
|
417
420
|
i += 1
|
|
418
421
|
continue
|
|
422
|
+
if self.macros is not None and occ == 0:
|
|
423
|
+
try:
|
|
424
|
+
expanded = self.macros.feed(text, start_line)
|
|
425
|
+
except MacroError as e:
|
|
426
|
+
self.diags.error(e.line, str(e), filename)
|
|
427
|
+
expanded = None
|
|
428
|
+
if expanded != text:
|
|
429
|
+
# an expansion is reported, and spans, where the macro is used; no `pieces`
|
|
430
|
+
for part in (expanded or "").split("\n"):
|
|
431
|
+
if part.strip():
|
|
432
|
+
phys = (start_line, i + 1)
|
|
433
|
+
out.append(
|
|
434
|
+
LogicalLine(part, last_line - shift, filename, section, phys=phys, occurrence=occ)
|
|
435
|
+
)
|
|
436
|
+
i += 1
|
|
437
|
+
continue
|
|
419
438
|
pulls, bad = 0, None
|
|
420
439
|
orig = text
|
|
421
440
|
orig_pieces = pieces
|
|
@@ -570,13 +589,19 @@ def load_program(
|
|
|
570
589
|
cr3xx_exts: tuple[str, ...] | None = None,
|
|
571
590
|
source: SourceProvider | None = None,
|
|
572
591
|
provenance: bool = True,
|
|
592
|
+
macros: bool = False,
|
|
573
593
|
) -> tuple[list[LogicalLine], Diagnostics]:
|
|
574
594
|
"""Load `path` and every file it `Include`s/`IncludeSection`s, as one logical-line stream."""
|
|
575
595
|
path = Path(path)
|
|
576
596
|
diags = Diagnostics()
|
|
577
|
-
loader = _Loader(path.parent, diags, own_ext, pull_init, cr3xx_exts, source, provenance)
|
|
597
|
+
loader = _Loader(path.parent, diags, own_ext, pull_init, cr3xx_exts, source, provenance, macros)
|
|
578
598
|
loader.pull_limit = pull_limit
|
|
579
599
|
lines = loader.load_file(path, section=None, in_include=False)
|
|
600
|
+
try:
|
|
601
|
+
if loader.macros is not None:
|
|
602
|
+
loader.macros.finish()
|
|
603
|
+
except MacroError as e:
|
|
604
|
+
diags.error(e.line, str(e), path.name)
|
|
580
605
|
diags.pull_log = loader.pull_log
|
|
581
606
|
diags.pull_site = loader.site
|
|
582
607
|
diags.include_paths = loader.includes
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
# Test corpora
|
|
2
|
-
|
|
3
|
-
| Directory | Contents | In git |
|
|
4
|
-
|---|---|---|
|
|
5
|
-
| `
|
|
6
|
-
| `fuzz/` | Saved fuzz reproducers seeded from `
|
|
7
|
-
| `chm/` | The 507 complete example programs in Campbell's CRBasic help. | no |
|
|
8
|
-
| `../../examples/Logger Programs/` | The 66 GEUS station programs used as a CR1000X corpus. | no |
|
|
9
|
-
|
|
10
|
-
The help examples and GEUS programs belong to others and are not redistributed
|
|
11
|
-
([docs/legal.md](../../docs/legal.md)). Tests that need them skip when they are absent. To run
|
|
12
|
-
those tests locally:
|
|
13
|
-
|
|
14
|
-
1. Extract the CRBasic help (the `.chm` installed with CRBasic Editor) into `extracted_chm/` at the
|
|
15
|
-
repository root, for example with `7z x CRBasic.chm -oextracted_chm`.
|
|
16
|
-
2. `uv run python tools/extract_chm_examples.py` writes `tests/corpus/chm/`.
|
|
17
|
-
3. Put the GEUS programs, if you have them, in `examples/Logger Programs/`.
|
|
18
|
-
4. For the differential tools, copy the Campbell compilers into `compiler/` (README, "How it was
|
|
19
|
-
built").
|
|
20
|
-
|
|
21
|
-
All of these paths are git-ignored.
|
|
1
|
+
# Test corpora
|
|
2
|
+
|
|
3
|
+
| Directory | Contents | In git |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| `station/` | A field station's CR6 programs and libraries (2026-11-01 snapshot). Tests pin results to these exact files, so refresh them only together with the expectations. | yes |
|
|
6
|
+
| `fuzz/` | Saved fuzz reproducers seeded from `station/`, replayed by `tools/fuzz_replay.py`. | yes |
|
|
7
|
+
| `chm/` | The 507 complete example programs in Campbell's CRBasic help. | no |
|
|
8
|
+
| `../../examples/Logger Programs/` | The 66 GEUS station programs used as a CR1000X corpus. | no |
|
|
9
|
+
|
|
10
|
+
The help examples and GEUS programs belong to others and are not redistributed
|
|
11
|
+
([docs/legal.md](../../docs/legal.md)). Tests that need them skip when they are absent. To run
|
|
12
|
+
those tests locally:
|
|
13
|
+
|
|
14
|
+
1. Extract the CRBasic help (the `.chm` installed with CRBasic Editor) into `extracted_chm/` at the
|
|
15
|
+
repository root, for example with `7z x CRBasic.chm -oextracted_chm`.
|
|
16
|
+
2. `uv run python tools/extract_chm_examples.py` writes `tests/corpus/chm/`.
|
|
17
|
+
3. Put the GEUS programs, if you have them, in `examples/Logger Programs/`.
|
|
18
|
+
4. For the differential tools, copy the Campbell compilers into `compiler/` (README, "How it was
|
|
19
|
+
built").
|
|
20
|
+
|
|
21
|
+
All of these paths are git-ignored.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencrbasic-1.0.0 → opencrbasic-1.1.1}/src/opencrbasic/data/cr1000x/background_calibration.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|