opencrbasic 1.0.0__tar.gz → 1.1.0__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.
Files changed (119) hide show
  1. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/PKG-INFO +17 -1
  2. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/README.md +16 -0
  3. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/pyproject.toml +1 -1
  4. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/__init__.py +1 -1
  5. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/analysis.py +1 -0
  6. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/cli.py +21 -4
  7. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/compiler.py +7 -3
  8. opencrbasic-1.1.0/src/opencrbasic/macros.py +161 -0
  9. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/parser.py +2 -0
  10. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/source.py +26 -1
  11. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/.gitignore +0 -0
  12. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/LICENSE +0 -0
  13. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/__main__.py +0 -0
  14. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/bindings.py +0 -0
  15. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/catalog.py +0 -0
  16. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checkconfig.py +0 -0
  17. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/__init__.py +0 -0
  18. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/cr3xx.py +0 -0
  19. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/ess.py +0 -0
  20. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/expressions.py +0 -0
  21. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/instr_data.py +0 -0
  22. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/instr_measure.py +0 -0
  23. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/instr_rules.py +0 -0
  24. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/instructions.py +0 -0
  25. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/intervals.py +0 -0
  26. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/longconv.py +0 -0
  27. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/scan_time.py +0 -0
  28. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/storage.py +0 -0
  29. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/structs.py +0 -0
  30. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/structure.py +0 -0
  31. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/symbols.py +0 -0
  32. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/terminals.py +0 -0
  33. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/unused.py +0 -0
  34. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/var_types.py +0 -0
  35. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checker/warnings.py +0 -0
  36. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/checks.py +0 -0
  37. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/commands.py +0 -0
  38. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/consteval.py +0 -0
  39. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/background_calibration.json +0 -0
  40. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/before_beginprog.json +0 -0
  41. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/background_calibration.json +0 -0
  42. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/before_beginprog.json +0 -0
  43. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/ess_variables.json +0 -0
  44. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/fnotch.json +0 -0
  45. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/functions.json +0 -0
  46. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/instructions.json +0 -0
  47. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/measure.json +0 -0
  48. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/mode.json +0 -0
  49. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/pipeline_tasks.json +0 -0
  50. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/settings_fields.json +0 -0
  51. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/status_members.json +0 -0
  52. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/symbols.json +0 -0
  53. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr1000x/terminals.json +0 -0
  54. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/before_beginprog.json +0 -0
  55. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/ess_variables.json +0 -0
  56. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/fnotch.json +0 -0
  57. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/functions.json +0 -0
  58. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/instructions.json +0 -0
  59. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/measure.json +0 -0
  60. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/mode.json +0 -0
  61. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/settings_fields.json +0 -0
  62. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/status_members.json +0 -0
  63. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/symbols.json +0 -0
  64. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr300/terminals.json +0 -0
  65. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/before_beginprog.json +0 -0
  66. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/ess_variables.json +0 -0
  67. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/fnotch.json +0 -0
  68. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/functions.json +0 -0
  69. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/instructions.json +0 -0
  70. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/measure.json +0 -0
  71. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/mode.json +0 -0
  72. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/settings_fields.json +0 -0
  73. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/status_members.json +0 -0
  74. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/symbols.json +0 -0
  75. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/cr350/terminals.json +0 -0
  76. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/ess_variables.json +0 -0
  77. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/fnotch.json +0 -0
  78. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/functions.json +0 -0
  79. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/instructions.json +0 -0
  80. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/measure.json +0 -0
  81. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/mode.json +0 -0
  82. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/notices_cr6.json +0 -0
  83. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/pipeline_tasks.json +0 -0
  84. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/settings_fields.json +0 -0
  85. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/status_members.json +0 -0
  86. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/status_table.json +0 -0
  87. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/symbols.json +0 -0
  88. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/terminals.json +0 -0
  89. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/data/type_codes.json +0 -0
  90. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/diagnostics.py +0 -0
  91. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/editing.py +0 -0
  92. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/formatting.py +0 -0
  93. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/helpdocs.py +0 -0
  94. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/hover.py +0 -0
  95. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/inlay.py +0 -0
  96. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lexer.py +0 -0
  97. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/__init__.py +0 -0
  98. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/__main__.py +0 -0
  99. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/config.py +0 -0
  100. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/features.py +0 -0
  101. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/server.py +0 -0
  102. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/session.py +0 -0
  103. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/lsp/worker.py +0 -0
  104. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/mode.py +0 -0
  105. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/navigation.py +0 -0
  106. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/positions.py +0 -0
  107. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/preprocess.py +0 -0
  108. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/profiles.py +0 -0
  109. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/provenance.py +0 -0
  110. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/quickfix.py +0 -0
  111. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/rename.py +0 -0
  112. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/signature.py +0 -0
  113. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/sources.py +0 -0
  114. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/syntax.py +0 -0
  115. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/tables.py +0 -0
  116. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/tables_hook.py +0 -0
  117. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/tdf.py +0 -0
  118. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/src/opencrbasic/timing.py +0 -0
  119. {opencrbasic-1.0.0 → opencrbasic-1.1.0}/tests/corpus/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opencrbasic
3
- Version: 1.0.0
3
+ Version: 1.1.0
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
@@ -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".
@@ -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".
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opencrbasic"
3
- version = "1.0.0"
3
+ version = "1.1.0"
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"
@@ -1,3 +1,3 @@
1
1
  """Open-source CRBasic (CR6) pre-compiler."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.1.0"
@@ -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()}
@@ -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"), --config FILE and
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
@@ -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
@@ -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
File without changes
File without changes