opencrbasic 1.1.1__tar.gz → 1.2.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.1.1 → opencrbasic-1.2.0}/PKG-INFO +1 -1
  2. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/pyproject.toml +1 -1
  3. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/__init__.py +1 -1
  4. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/__init__.py +4 -0
  5. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/cr3xx.py +265 -46
  6. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/expressions.py +311 -62
  7. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/instr_data.py +216 -49
  8. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/instr_measure.py +297 -47
  9. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/instr_rules.py +306 -57
  10. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/intervals.py +2 -0
  11. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/structure.py +120 -20
  12. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/symbols.py +10 -5
  13. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/terminals.py +184 -6
  14. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/var_types.py +35 -6
  15. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/warnings.py +8 -0
  16. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/cli.py +6 -1
  17. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/measure.json +3 -1
  18. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/terminals.json +2 -0
  19. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/measure.json +7 -0
  20. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/status_members.json +0 -1
  21. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/terminals.json +1 -0
  22. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/measure.json +7 -0
  23. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/terminals.json +1 -0
  24. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/measure.json +3 -1
  25. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/notices_cr6.json +2 -61
  26. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/terminals.json +11 -1
  27. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/parser.py +96 -26
  28. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/syntax.py +5 -0
  29. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/tables.py +13 -9
  30. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/.gitignore +0 -0
  31. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/LICENSE +0 -0
  32. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/README.md +0 -0
  33. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/__main__.py +0 -0
  34. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/analysis.py +0 -0
  35. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/bindings.py +0 -0
  36. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/catalog.py +0 -0
  37. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checkconfig.py +0 -0
  38. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/ess.py +0 -0
  39. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/instructions.py +0 -0
  40. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/longconv.py +0 -0
  41. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/scan_time.py +0 -0
  42. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/storage.py +0 -0
  43. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/structs.py +0 -0
  44. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checker/unused.py +0 -0
  45. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/checks.py +0 -0
  46. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/commands.py +0 -0
  47. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/compiler.py +0 -0
  48. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/consteval.py +0 -0
  49. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/background_calibration.json +0 -0
  50. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/before_beginprog.json +0 -0
  51. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/background_calibration.json +0 -0
  52. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/before_beginprog.json +0 -0
  53. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/ess_variables.json +0 -0
  54. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/fnotch.json +0 -0
  55. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/functions.json +0 -0
  56. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/instructions.json +0 -0
  57. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/mode.json +0 -0
  58. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/pipeline_tasks.json +0 -0
  59. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/settings_fields.json +0 -0
  60. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/status_members.json +0 -0
  61. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr1000x/symbols.json +0 -0
  62. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/before_beginprog.json +0 -0
  63. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/ess_variables.json +0 -0
  64. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/fnotch.json +0 -0
  65. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/functions.json +0 -0
  66. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/instructions.json +0 -0
  67. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/mode.json +0 -0
  68. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/settings_fields.json +0 -0
  69. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr300/symbols.json +0 -0
  70. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/before_beginprog.json +0 -0
  71. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/ess_variables.json +0 -0
  72. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/fnotch.json +0 -0
  73. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/functions.json +0 -0
  74. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/instructions.json +0 -0
  75. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/mode.json +0 -0
  76. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/settings_fields.json +0 -0
  77. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/status_members.json +0 -0
  78. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/cr350/symbols.json +0 -0
  79. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/ess_variables.json +0 -0
  80. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/fnotch.json +0 -0
  81. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/functions.json +0 -0
  82. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/instructions.json +0 -0
  83. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/mode.json +0 -0
  84. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/pipeline_tasks.json +0 -0
  85. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/settings_fields.json +0 -0
  86. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/status_members.json +0 -0
  87. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/status_table.json +0 -0
  88. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/symbols.json +0 -0
  89. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/data/type_codes.json +0 -0
  90. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/diagnostics.py +0 -0
  91. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/editing.py +0 -0
  92. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/formatting.py +0 -0
  93. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/helpdocs.py +0 -0
  94. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/hover.py +0 -0
  95. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/inlay.py +0 -0
  96. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lexer.py +0 -0
  97. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/__init__.py +0 -0
  98. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/__main__.py +0 -0
  99. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/config.py +0 -0
  100. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/features.py +0 -0
  101. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/server.py +0 -0
  102. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/session.py +0 -0
  103. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/lsp/worker.py +0 -0
  104. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/macros.py +0 -0
  105. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/mode.py +0 -0
  106. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/navigation.py +0 -0
  107. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/positions.py +0 -0
  108. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/preprocess.py +0 -0
  109. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/profiles.py +0 -0
  110. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/provenance.py +0 -0
  111. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/quickfix.py +0 -0
  112. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/rename.py +0 -0
  113. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/signature.py +0 -0
  114. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/source.py +0 -0
  115. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/sources.py +0 -0
  116. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/tables_hook.py +0 -0
  117. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/tdf.py +0 -0
  118. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/src/opencrbasic/timing.py +0 -0
  119. {opencrbasic-1.1.1 → opencrbasic-1.2.0}/tests/corpus/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opencrbasic
3
- Version: 1.1.1
3
+ Version: 1.2.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opencrbasic"
3
- version = "1.1.1"
3
+ version = "1.2.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.1.1"
3
+ __version__ = "1.2.0"
@@ -111,6 +111,7 @@ from . import (
111
111
  storage,
112
112
  structs,
113
113
  structure,
114
+ terminals,
114
115
  var_types,
115
116
  warnings,
116
117
  )
@@ -265,6 +266,7 @@ def _check(program: S.Program, profile: Profile, diags: Diagnostics, result) ->
265
266
  del diags.items[program.hash_cut :] # parser findings of later lines
266
267
  program = structure.open_program_at_slowsequence(program, profile)
267
268
  env = consteval.Env(profile, dict(program.consts))
269
+ expressions.set_arity_context(program, profile)
268
270
 
269
271
  _run_pass(structure.check_pass1, program, profile, diags, env)
270
272
  # Also pass 1 (unconditional -- oracle-confirmed, `docs/loggers.md`): a `Const`'s own value
@@ -411,6 +413,7 @@ def _check(program: S.Program, profile: Profile, diags: Diagnostics, result) ->
411
413
  # "stop entirely at the first placement error" behaviour.
412
414
  pass2_start = len(diags.items)
413
415
  _run_pass(instr_rules.check_spi_open_order, _with_inline(program), profile, leading)
416
+ _run_pass(terminals.check_i2c_open_order, _with_inline(program), profile, ranges, env)
414
417
  # Block-closer messages come before a same-line placement error (`EndIf illegally
415
418
  # occurs before BeginProg.` follows `END IF without IF...`).
416
419
  # CR300/CR350: collected aside in `leading` (printed first on their line, but they do not
@@ -743,6 +746,7 @@ def _check(program: S.Program, profile: Profile, diags: Diagnostics, result) ->
743
746
  # instructions placed outside `DataTable...EndTable`, and an orphan `EndTable`.
744
747
  table_stop = _run_pass_capture(instr_data.check_table_structure, _with_inline(program), profile, diags)
745
748
  _run_pass(instr_data.check_fieldnames, program, profile, diags, env)
749
+ _run_pass(instr_data.check_fieldclassify, program, profile, diags, env)
746
750
  apply_diags = Diagnostics()
747
751
  _run_pass(structure.check_apply_restart_sequence, program, profile, apply_diags)
748
752
  for extra in apply_diags.items: # in source order among the tier-C findings
@@ -7,6 +7,7 @@ evidence: `docs/loggers.md`.
7
7
 
8
8
  import collections
9
9
  import dataclasses
10
+ import math
10
11
  import re
11
12
  from collections.abc import Set as AbstractSet
12
13
  from typing import TypedDict
@@ -14,7 +15,7 @@ from typing import TypedDict
14
15
  from .. import bindings, consteval, lexer
15
16
  from .. import syntax as S
16
17
  from ..diagnostics import Diagnostic, Diagnostics
17
- from . import expressions, instr_rules, structure, symbols
18
+ from . import expressions, instr_rules, longconv, structure, symbols
18
19
 
19
20
  # `SerialOpen`'s own `ComPort` argument (position 0, the only position checked here) is the one
20
21
  # `Serial*` function on CR300/CR350 whose compile-time validator additionally requires a genuine
@@ -40,6 +41,8 @@ _SERIALOPEN = "SERIALOPEN"
40
41
  _SERIALOPEN_PORTS = {
41
42
  "cr6": frozenset(range(1, 19)) | frozenset(range(31, 100)),
42
43
  "cr1000x": frozenset(range(1, 15)) | frozenset(range(31, 100)),
44
+ "cr300": frozenset(range(1, 13)) | frozenset(range(32, 48)),
45
+ "cr350": frozenset(range(1, 18)) | frozenset(range(32, 48)),
43
46
  }
44
47
  _COMPORT_ARG_POSITION = 0
45
48
  # Every `Serial*` function whose first argument is a `ComPort` (`data/<logger>/functions.json`:
@@ -79,6 +82,33 @@ _COMMS_MODES = {
79
82
  "COMC3": frozenset({0, 1, 2, 3, 4, 6}),
80
83
  "COMC7": frozenset({0, 1, 2, 3, 4, 6}),
81
84
  }
85
+ _CR350_MAX_COMMS_MODE = {1: 0, 2: 0, 3: 0, 4: 1, 7: 1, 8: 1, 9: 1, 10: 1}
86
+
87
+
88
+ def _check_cr350_comms_mode(
89
+ stmt: S.Call, port: int, profile, consts: dict, diags: Diagnostics, invalid_format: bool, shifted: bool
90
+ ) -> None:
91
+ maximum = _CR350_MAX_COMMS_MODE.get(port)
92
+ if maximum is None or (len(stmt.args) < 5 if shifted else len(stmt.args) != 6):
93
+ return
94
+ mode_arg = stmt.args[4 if shifted else 5]
95
+ if longconv._float_typed(mode_arg, frozenset()):
96
+ return
97
+ env = consteval.Env(profile, consts)
98
+ try:
99
+ raw = consteval.value_of(mode_arg, env)
100
+ except consteval.NotConstant:
101
+ return
102
+ if not consteval.is_finite_number(raw) or (
103
+ isinstance(raw, float) and (not -(2**31) <= raw < 2**31 or raw != int(raw))
104
+ ):
105
+ return
106
+ fmt = longconv.long_value(stmt.args[2], env, profile) if not shifted else None
107
+ if not shifted and not invalid_format and (fmt is None or not -1 <= fmt <= 15):
108
+ return
109
+ value = longconv.long_value(mode_arg, env, profile)
110
+ if value is not None and (mode := value & 0xFF) > maximum:
111
+ diags.error(stmt.line, f"{mode} is an invalid CommsMode parameter.", stmt.file)
82
112
 
83
113
 
84
114
  def _commsmode_problem(
@@ -94,7 +124,7 @@ def _commsmode_problem(
94
124
  mode = consteval.value_of(stmt.args[index], env)
95
125
  except consteval.NotConstant, consteval.DivideByZero, TypeError, ValueError:
96
126
  return None
97
- if isinstance(mode, bool) or not isinstance(mode, (int, float)) or mode != int(mode):
127
+ if isinstance(mode, bool) or not consteval.is_finite_number(mode) or mode != int(mode):
98
128
  return None
99
129
  for key, modes in _COMMS_MODES.items():
100
130
  record = profile.symbols.get(key)
@@ -132,6 +162,34 @@ def _format_needs_com1_4(stmt: S.Call, profile, consts: dict | None) -> bool:
132
162
  return 1 <= port <= 8 and 16 <= fmt < 2**31
133
163
 
134
164
 
165
+ def _non_finite(arg: S.Expr | None, profile, consts: dict | None) -> str | None:
166
+ """`"divide"` for a division by zero, `"float"` for an infinite or NaN constant: neither is
167
+ a constant integer (oracle, all four loggers, 2026-11-14,
168
+ `tests/fixtures/r150_serialopen_non_finite.json`)."""
169
+ if arg is None:
170
+ return None
171
+ if isinstance(arg, S.Name) and arg.name.upper() == "INF" and "INF" not in (consts or {}):
172
+ return "float"
173
+ try:
174
+ value = consteval.value_of(arg, consteval.Env(profile, dict(consts or {})))
175
+ except consteval.DivideByZero:
176
+ return "divide"
177
+ except consteval.NotConstant, TypeError, ValueError, OverflowError, ZeroDivisionError:
178
+ return None
179
+ return "float" if isinstance(value, float) and not math.isfinite(value) else None
180
+
181
+
182
+ def _invalid_cr3xx_serial_format(arg: S.Expr, env: consteval.Env) -> bool:
183
+ """The integer format field on CR300/CR350 rejects options starting at 16 (round 134)."""
184
+ if not isinstance(arg, S.Name) and consteval.is_float(arg, env):
185
+ return False
186
+ try:
187
+ value = consteval.value_of(arg, env)
188
+ except consteval.NotConstant, ValueError, OverflowError:
189
+ return False
190
+ return consteval.is_finite_number(value) and 16 <= value <= 2**31 and value == int(value)
191
+
192
+
135
193
  def _non_numeric(expr, profile, consts: dict | None, variable_names) -> bool:
136
194
  if expressions._variable_refs(expr, variable_names):
137
195
  return True
@@ -143,7 +201,7 @@ def _non_numeric(expr, profile, consts: dict | None, variable_names) -> bool:
143
201
  return isinstance(value, str) or expressions._not_constant_integer(expr)
144
202
 
145
203
 
146
- def _check_baud(stmt: S.Call, profile, diags: Diagnostics, consts: dict | None = None) -> None:
204
+ def _check_baud(stmt: S.Call, profile, diags: Diagnostics, consts: dict | None = None, *, port11: bool = False) -> None:
147
205
  port = stmt.args[0] if stmt.args else None
148
206
  rs232 = isinstance(port, S.Name) and port.name.upper() == "COMRS232"
149
207
  # CR6/CR1000X check every valid port (oracle, 2026-10-25, round 35: ComUSB, ComC1, ComSDC7,
@@ -152,7 +210,7 @@ def _check_baud(stmt: S.Call, profile, diags: Diagnostics, consts: dict | None =
152
210
  isinstance(port, S.Name)
153
211
  and (port.name.upper() in profile.symbols or port.name.upper() in {str(c).upper() for c in consts or {}})
154
212
  )
155
- if len(stmt.args) < 2 or not (rs232 or (not profile.single_pass and known_port)):
213
+ if len(stmt.args) < 2 or not (rs232 or port11 or (not profile.single_pass and known_port)):
156
214
  return
157
215
  arg = stmt.args[1]
158
216
  sign = 1
@@ -209,9 +267,11 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
209
267
  alongside `expressions.check_consts`.
210
268
  """
211
269
  cr3 = profile.name in ("cr300", "cr350")
270
+ from .instr_data import _with_inline
271
+
212
272
  known = expressions._known_names(program, profile)
213
273
  variable_names = symbols.declared_variable_names(program)
214
- for stmt in program.statements:
274
+ for stmt in _with_inline(program.statements) if cr3 else program.statements:
215
275
  if not isinstance(stmt, S.Call):
216
276
  continue
217
277
  name = stmt.name.upper()
@@ -231,6 +291,8 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
231
291
  continue
232
292
  is_variable = bool(expressions._variable_refs(arg, variable_names))
233
293
  ports = _SERIALOPEN_PORTS.get(profile.name)
294
+ port = None
295
+ sep = "\n" if cr3 and stmt.at_eol else ""
234
296
  if ports and not (names or is_variable or isinstance(arg, S.String)):
235
297
  # a constant ComPort outside the port codes ends the statement (oracle, round 36/37,
236
298
  # docs/checker.md#serialopen-comport-codes-round-37)
@@ -238,17 +300,62 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
238
300
  port = consteval.value_of(arg, consteval.Env(profile, dict(program.consts or {})))
239
301
  except consteval.NotConstant, TypeError, ValueError, ZeroDivisionError:
240
302
  port = None
303
+ if cr3 and isinstance(port, bool):
304
+ port = -int(port)
305
+ if cr3 and isinstance(port, (int, float)) and port in (-(2**31), 2**31):
306
+ port = max(-(2**31 - 1), min(2**31 - 1, int(port)))
307
+ elif (
308
+ consteval.is_finite_number(port)
309
+ and not isinstance(port, bool)
310
+ and (
311
+ port != int(port)
312
+ or not -(2**31) <= port < 2**31
313
+ or (cr3 and expressions._not_constant_integer(arg))
314
+ )
315
+ ) or (cr3 and isinstance(port, str)):
316
+ diags.error(stmt.line, "ComPort 0 illegal.", stmt.file)
317
+ diags.error(stmt.line, f"Parameter must be a constant integer: {stmt.text}{sep}.", stmt.file)
318
+ continue
241
319
  if consteval.is_finite_number(port) and not isinstance(port, bool):
242
- if port != int(port) or not -(2**31) <= port < 2**31:
243
- diags.error(stmt.line, "ComPort 0 illegal.", stmt.file)
244
- diags.error(stmt.line, f"Parameter must be a constant integer: {stmt.text}.", stmt.file)
245
- continue
246
320
  if int(port) not in ports:
247
- diags.error(stmt.line, f"ComPort {int(port)} illegal.", stmt.file)
248
- diags.error(stmt.line, f"No Matching, or out of place ) in expression: {stmt.text}.", stmt.file)
321
+ diags.items.append(
322
+ Diagnostic("error", stmt.line, f"ComPort {int(port)} illegal.", stmt.file, trigger=cr3)
323
+ )
324
+ if profile.name != "cr350":
325
+ diags.error(
326
+ stmt.line, f"No Matching, or out of place ) in expression: {stmt.text}{sep}.", stmt.file
327
+ )
249
328
  continue
250
- _check_baud(stmt, profile, diags, program.consts)
329
+ _check_baud(stmt, profile, diags, program.consts, port11=cr3 and port == 11)
251
330
  constant_port = not (names or is_variable or isinstance(arg, S.String))
331
+ format_arg = stmt.args[2] if len(stmt.args) > 2 else None
332
+ invalid_format = (
333
+ cr3
334
+ and constant_port
335
+ and consteval.is_finite_number(port)
336
+ and format_arg is not None
337
+ and not expressions._variable_refs(format_arg, variable_names)
338
+ and _invalid_cr3xx_serial_format(format_arg, consteval.Env(profile, dict(program.consts)))
339
+ )
340
+ if invalid_format:
341
+ diags.error(stmt.line, "Invalid format option.", stmt.file)
342
+ cr3_shifted = (
343
+ cr3
344
+ and constant_port
345
+ and format_arg is not None
346
+ and not invalid_format
347
+ and (
348
+ _non_numeric(format_arg, profile, program.consts, variable_names)
349
+ or not isinstance(format_arg, S.Name)
350
+ and longconv._float_typed(format_arg, frozenset())
351
+ )
352
+ )
353
+ if cr3_shifted:
354
+ if profile.name == "cr350" and consteval.is_finite_number(port):
355
+ _check_cr350_comms_mode(stmt, int(port), profile, program.consts, diags, False, True)
356
+ elif len(stmt.args) > 5:
357
+ diags.error(stmt.line, f"No Matching, or out of place ) in expression: {stmt.text}{sep}.", stmt.file)
358
+ continue
252
359
  # CR6/CR1000X: a String or variable `SerialOpenFormat` makes the 5th argument the CommsMode, with
253
360
  # no TXDelay range and at most 5 arguments (oracle, 2026-10-28, round 57,
254
361
  # docs/checker.md#serialopen-format-and-commsmode-round-57)
@@ -260,7 +367,7 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
260
367
  and _non_numeric(stmt.args[2], profile, program.consts, variable_names)
261
368
  )
262
369
  # a bare undeclared TXDelay is lost in the same comma recovery (decompiled `FUN_00494aa0`,
263
- # oracle round 58, docs/checker.md#serialopen-recovery-round-58)
370
+ # oracle round 58, docs/checker.md#serialopen-recovery-rounds-58-and-142)
264
371
  shifted = shifted or (not cr3 and constant_port and bare_unknown_delay(stmt, known))
265
372
  if (
266
373
  not cr3
@@ -296,16 +403,12 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
296
403
  stmt.line, f"{int(value)} usec delay before transmit is illegal, must be 0..14000000", stmt.file
297
404
  )
298
405
  # a 6th argument that is no constant integer counts as mode 0, then is rejected (same probes)
299
- bad_mode = (
300
- not cr3
301
- and constant_port
302
- and not shifted
303
- and len(stmt.args) > 5
304
- and stmt.args[5] is not None
305
- and (
306
- expressions._not_constant_integer(stmt.args[5])
307
- or bool(expressions._variable_refs(stmt.args[5], variable_names))
308
- )
406
+ check_mode = not cr3 and constant_port and not shifted and len(stmt.args) > 5 and stmt.args[5] is not None
407
+ mode_kind = _non_finite(stmt.args[5], profile, program.consts) if check_mode else None
408
+ bad_mode = check_mode and (
409
+ expressions._not_constant_integer(stmt.args[5])
410
+ or bool(expressions._variable_refs(stmt.args[5], variable_names))
411
+ or mode_kind is not None
309
412
  )
310
413
  if (
311
414
  not cr3
@@ -321,19 +424,55 @@ def check(program: S.Program, profile, diags: Diagnostics) -> None:
321
424
  else:
322
425
  diags.warning(stmt.line, problem[1], stmt.file)
323
426
  if bad_mode:
324
- diags.error(stmt.line, f"Parameter must be a constant integer: {stmt.text}.", stmt.file)
427
+ verb = "Divide by zero in expression" if mode_kind == "divide" else "Parameter must be a constant integer"
428
+ diags.error(stmt.line, f"{verb}: {stmt.text}.", stmt.file)
325
429
  continue
326
430
  if shifted and len(stmt.args) > 5:
327
431
  diags.error(stmt.line, f"No Matching, or out of place ) in expression: {stmt.text}.", stmt.file)
328
432
  continue
329
433
  if (
330
- not (cr3 or names or is_variable or isinstance(arg, S.String))
434
+ not (names or is_variable or isinstance(arg, S.String))
331
435
  and len(stmt.args) > 4
332
436
  and expressions._variable_refs(stmt.args[4], variable_names)
333
437
  ):
334
438
  # a variable `BufferSize` (oracle, CR6/CR1000X, 2026-10-24, round 34)
335
- diags.error(stmt.line, f"Parameter must be a constant integer: {stmt.text}.", stmt.file)
439
+ diags.error(stmt.line, f"Parameter must be a constant integer: {stmt.text}{sep}.", stmt.file)
336
440
  continue
441
+ if constant_port and len(stmt.args) > 4 and (kind := _non_finite(stmt.args[4], profile, program.consts)):
442
+ # an infinite `BufferSize`, or one that divides by zero (CR300/CR350: one message for both)
443
+ verb = (
444
+ "Divide by zero in expression"
445
+ if kind == "divide" and not cr3
446
+ else "Parameter must be a constant integer"
447
+ )
448
+ diags.error(stmt.line, f"{verb}: {stmt.text}{sep}.", stmt.file)
449
+ continue
450
+ if profile.name == "cr300" and port == 11 and len(stmt.args) > 4:
451
+ buffer = stmt.args[4]
452
+ if not expressions._not_constant_integer(buffer):
453
+ try:
454
+ size = consteval.value_of(buffer, consteval.Env(profile, dict(program.consts)))
455
+ except consteval.NotConstant:
456
+ size = None
457
+ if consteval.is_finite_number(size) and 0 < size < 2**31:
458
+ diags.items.append(Diagnostic("error", stmt.line, "Out of memory.", stmt.file, trigger=True))
459
+ if profile.name == "cr350" and consteval.is_finite_number(port):
460
+ _check_cr350_comms_mode(stmt, int(port), profile, program.consts, diags, invalid_format, False)
461
+ if profile.name == "cr300" and constant_port and len(stmt.args) == 6 and stmt.args[5] is not None:
462
+ mode_arg = stmt.args[5]
463
+ env = consteval.Env(profile, dict(program.consts))
464
+ try:
465
+ mode = consteval.value_of(mode_arg, env)
466
+ except consteval.NotConstant:
467
+ mode = None
468
+ if (
469
+ not consteval.is_finite_number(mode)
470
+ or mode != int(mode)
471
+ or not isinstance(mode_arg, S.Name)
472
+ and consteval.is_float(mode_arg, env)
473
+ or expressions._variable_refs(mode_arg, variable_names)
474
+ ):
475
+ diags.error(stmt.line, f"Parameter must be a constant integer: {stmt.text}{sep}.", stmt.file)
337
476
  if names or is_variable or isinstance(arg, S.String):
338
477
  # A string or variable ComPort, all four loggers, pass 1 (oracle, 2026-10-01:
339
478
  # `SerialOpen("fuzz",...)`). CR300/CR350 echo the line end (`docs/loggers.md`).
@@ -883,18 +1022,38 @@ def check_float_destinations(program: S.Program, profile, diags: Diagnostics) ->
883
1022
  String variable, a literal, an expression) gives `Illegal destination variable type,
884
1023
  expecting Float.` (oracle, CR300, 2026-10-01: `PanelTemp(L,60)`, `PanelTemp(Nope,60)`,
885
1024
  `VoltSe(L,1,mV1000,...)`, `Battery(L)`)."""
1025
+ from .instr_data import _destination_env, constant_destination
1026
+
886
1027
  if profile.name not in ("cr300", "cr350"):
887
1028
  return
888
1029
  types: dict[str, str] | None = None
889
1030
  scoped: dict[int, dict[str, str]] = {}
890
1031
  known = ds = None
1032
+ constant_names = profile.symbols.keys() | expressions._EXTRA_PREDEFINED_SYMBOLS
1033
+ function_names = set(profile.functions)
1034
+ destination_env = _destination_env(consteval.Env(profile))
1035
+ destination_variables = symbols.declared_variable_names(program)
891
1036
  params: dict[str, str] = {} # the enclosing routine's parameters hide a global (round 31)
1037
+ scalar_params: set[str] = set()
892
1038
  per_line = collections.Counter((st.line, st.file) for st in program.statements)
893
1039
  for stmt in program.statements:
894
1040
  if isinstance(stmt, S.SubDef):
895
1041
  params = {p.name.upper(): (p.type or "FLOAT").upper() for p in stmt.params}
1042
+ scalar_params = {
1043
+ p.name.upper()
1044
+ for p in stmt.params
1045
+ if not p.dim_exprs and (p.dims == 0 or p.type is None) and (p.type or "").upper() != "STRING"
1046
+ }
1047
+ if stmt.keyword.upper() == "FUNCTION":
1048
+ function_names.add(stmt.name.upper())
1049
+ elif isinstance(stmt, S.ConstDecl):
1050
+ name = stmt.name.upper()
1051
+ constant_names.add(name)
1052
+ if name in program.consts:
1053
+ destination_env.consts[name] = program.consts[name]
896
1054
  elif S.ends_routine(stmt):
897
1055
  params = {}
1056
+ scalar_params = set()
898
1057
  if (
899
1058
  not isinstance(stmt, S.Instruction)
900
1059
  or not stmt.args
@@ -903,12 +1062,19 @@ def check_float_destinations(program: S.Program, profile, diags: Diagnostics) ->
903
1062
  ):
904
1063
  continue
905
1064
  info = profile.instructions.get(stmt.keyword.upper())
906
- literal = (
1065
+ send_variables = stmt.keyword.upper() == "SENDVARIABLES"
1066
+ named_destination = send_variables or stmt.keyword.upper() in {"MOVE", "ERASE", "READIO", "REALTIME"}
1067
+ invalid_destination = (
907
1068
  next(
908
1069
  (
909
1070
  i
910
1071
  for i, (code, a) in enumerate(zip(info["params"], stmt.args))
911
- if code in (27, 29) and a is not None and not isinstance(a, S.String) and not _expr_names(a)
1072
+ if code in (27, 29)
1073
+ and (
1074
+ constant_destination(a, destination_env, params.keys() | destination_variables)
1075
+ if named_destination
1076
+ else a is not None and not isinstance(a, S.String) and not _expr_names(a)
1077
+ )
912
1078
  ),
913
1079
  None,
914
1080
  )
@@ -916,13 +1082,12 @@ def check_float_destinations(program: S.Program, profile, diags: Diagnostics) ->
916
1082
  else None
917
1083
  )
918
1084
  if (
919
- literal is not None
1085
+ invalid_destination is not None
920
1086
  and per_line[(stmt.line, stmt.file)] == 1
921
- and not any((d.line, d.file) == (stmt.line, stmt.file) for d in diags.errors)
1087
+ and (send_variables or not any((d.line, d.file) == (stmt.line, stmt.file) for d in diags.errors))
922
1088
  ):
923
- # `RealTime(5)`, `Erase(5)`, `Matrix(5,(1+2),0,R)` -> `Invalid Variable or Array:
924
- # 5.`, then the argument list ends (oracle, CR300, 2026-10-01: `Move(5,1,X,1)` ->
925
- # `Too many parameters: 1.`)
1089
+ # The argument list ends after a rejected destination. The next token is
1090
+ # reported as excess (oracle, CR300/CR350, rounds 31 and 133).
926
1091
  at = next(
927
1092
  (i for i, d in enumerate(diags.items) if d.file == stmt.file and d.line > stmt.line), len(diags.items)
928
1093
  )
@@ -930,15 +1095,23 @@ def check_float_destinations(program: S.Program, profile, diags: Diagnostics) ->
930
1095
  Diagnostic(
931
1096
  "error",
932
1097
  stmt.line,
933
- f"Invalid Variable or Array: {expressions._arg_text(stmt, literal).strip()}.",
1098
+ f"Invalid Variable or Array: {expressions._arg_text(stmt, invalid_destination).strip()}.",
934
1099
  stmt.file,
935
1100
  )
936
1101
  ]
937
- if len(stmt.args) > literal + 1 and stmt.args[literal + 1] is not None:
938
- text = instr_rules._next_simple_token(
939
- expressions._arg_text(stmt, literal + 1)
940
- ) or expressions._arg_text(stmt, literal + 1)
1102
+ following = invalid_destination + 1
1103
+ if len(stmt.args) > following and stmt.args[following] is not None:
1104
+ raw = expressions._arg_text(stmt, following)
1105
+ text = instr_rules._next_simple_token(raw) or raw
941
1106
  new.append(Diagnostic("error", stmt.line, f"Too many parameters: {text}.", stmt.file))
1107
+ if send_variables:
1108
+ # The destination aborts before SendVariables reads its remaining arguments.
1109
+ at -= sum(
1110
+ (d.line, d.file) == (stmt.line, stmt.file) and d.severity == "error" for d in diags.items[:at]
1111
+ )
1112
+ diags.items[:] = [
1113
+ d for d in diags.items if (d.line, d.file) != (stmt.line, stmt.file) or d.severity != "error"
1114
+ ]
942
1115
  diags.items[at:at] = new
943
1116
  continue
944
1117
  if not info or not info["params"] or info["params"][0] != _FLOAT_DEST:
@@ -948,14 +1121,33 @@ def check_float_destinations(program: S.Program, profile, diags: Diagnostics) ->
948
1121
  ds = expressions.program_dims(program, profile, known)
949
1122
  assert known is not None and ds is not None
950
1123
  arg = stmt.args[0]
1124
+ assert arg is not None
951
1125
  if isinstance(arg, S.Name) and arg.name.upper() in profile.functions:
952
1126
  continue # a bare function name is `Expression expects (` instead (expressions.py)
953
1127
  key = (stmt.line, stmt.file)
1128
+ if isinstance(arg, S.Index) and arg.name.upper() in scalar_params and any(a is not None for a in arg.args):
1129
+ raw = expressions._arg_text(stmt, 0).strip()
1130
+ diags.items[:] = [
1131
+ d
1132
+ for d in diags.items
1133
+ if (d.line, d.file) != key
1134
+ or not d.message.startswith(("Dimensions not declared:", "Too many parameters:"))
1135
+ ]
1136
+ diags.items.append(
1137
+ Diagnostic("error", stmt.line, f"Dimensions not declared: {raw}.", stmt.file, trigger=True)
1138
+ )
1139
+ if len(stmt.args) > 1 and stmt.args[1] is not None:
1140
+ raw = expressions._arg_text(stmt, 1)
1141
+ token = instr_rules._next_simple_token(raw) or raw
1142
+ diags.error(stmt.line, f"Too many parameters: {token}.", stmt.file)
1143
+ continue
954
1144
  undefined = [n for n in _expr_names(arg) if n.upper() not in known]
1145
+ variables = {**types, **scoped.get(id(stmt), {}), **params}
1146
+ call = isinstance(arg, S.Index) and arg.name.upper() not in variables and arg.name.upper() in function_names
955
1147
  expression = False
956
1148
  dims = False
957
1149
  if undefined:
958
- if isinstance(arg, (S.Binary, S.Unary)):
1150
+ if isinstance(arg, (S.Binary, S.Unary)) or call:
959
1151
  # an arithmetic expression is no destination: after its names, `Dimensions not
960
1152
  # declared` or the destination-type error (oracle, CR300, 2026-10-03)
961
1153
  expression = True
@@ -963,11 +1155,21 @@ def check_float_destinations(program: S.Program, profile, diags: Diagnostics) ->
963
1155
  elif not isinstance(arg, (S.Name, S.Index)):
964
1156
  continue
965
1157
  keep = [profile.unresolved_symbol_message.format(name=n) for n in undefined]
966
- elif _is_bad_destination(arg, {**types, **scoped.get(id(stmt), {}), **params}):
1158
+ elif (
1159
+ call
1160
+ or _is_bad_destination(arg, variables)
1161
+ or isinstance(arg, S.Name)
1162
+ and arg.name.upper() not in variables
1163
+ and arg.name.upper() in constant_names
1164
+ ):
967
1165
  keep = []
968
1166
  else:
969
1167
  continue
970
1168
  own = [d for d in diags.items if (d.line, d.file) == key and d.severity == "error"]
1169
+ if call and any(
1170
+ d.message.startswith(("Incomplete expression:", "Invalid, or out of place expression:")) for d in own
1171
+ ):
1172
+ continue
971
1173
  if not keep and any(d.message.startswith("Dimensions not declared") for d in own):
972
1174
  continue # (`Battery(L(2))` for a Long scalar: over-indexing is reported first)
973
1175
  if keep and [d.message for d in own[: len(keep)]] != keep:
@@ -1236,12 +1438,18 @@ def _report_unregistered(
1236
1438
  if isinstance(st, S.SubDef) and id(st) in tainted_subs and st.keyword == "FUNCTION"
1237
1439
  }
1238
1440
  affected: set[tuple[int, str]] = set()
1441
+ removed_params: set[str] = set()
1239
1442
  for st in program.statements[taint_at + 1 :]:
1240
- if id(st) in dropped or id(st) in rewritten or isinstance(st, S.SubDef):
1443
+ if isinstance(st, S.SubDef):
1444
+ removed_params = {p.name.upper() for p in st.params} if id(st) in tainted_subs else set()
1445
+ continue
1446
+ if isinstance(st, S.Instruction) and structure._ends_sub(st, st.keyword.upper()):
1447
+ removed_params.clear()
1448
+ if id(st) in dropped or id(st) in rewritten:
1241
1449
  continue
1242
1450
  names: set[str] = set()
1243
1451
  unused._walk_stmt(st, names)
1244
- if names & unregistered or (not isinstance(st, S.Call) and names & functions):
1452
+ if names & unregistered or names & removed_params or (not isinstance(st, S.Call) and names & functions):
1245
1453
  affected.add((st.line, st.file))
1246
1454
  if not affected:
1247
1455
  return
@@ -2426,6 +2634,7 @@ _ARITY_PREFIXES = {
2426
2634
  "many": "No Matching, or out of place ) in expression: ",
2427
2635
  "few": "Expression expects , : ",
2428
2636
  "empty": "Invalid, or out of place expression: ",
2637
+ "network_constint": "Parameter must be a constant integer: ",
2429
2638
  }
2430
2639
 
2431
2640
 
@@ -2459,7 +2668,7 @@ def _arity_walk(expr: S.Expr | None, known: set[str], profile, state: _ArityStat
2459
2668
  )
2460
2669
  args, stop = args[:at], True
2461
2670
  state.setdefault("kind", "many")
2462
- elif problem in ("few", "empty"):
2671
+ elif problem in ("few", "empty", "network_constint"):
2463
2672
  state.setdefault("kind", problem)
2464
2673
  elif problem:
2465
2674
  state["bad"] = True
@@ -2512,7 +2721,13 @@ def order_names_around_arity(program: S.Program, profile, diags: Diagnostics) ->
2512
2721
  if state.get("bad") or kind is None:
2513
2722
  continue
2514
2723
  arity = [(i, d) for i, d in own if d.message.startswith(_ARITY_PREFIXES[kind])]
2515
- if not arity and isinstance(stmt, S.Assignment) and own and expressions._undefined_names(stmt.target, known):
2724
+ if (
2725
+ not arity
2726
+ and kind != "network_constint"
2727
+ and isinstance(stmt, S.Assignment)
2728
+ and own
2729
+ and expressions._undefined_names(stmt.target, known)
2730
+ ):
2516
2731
  # an unresolved target hides the pass-1 arity check, but the parse still reaches it
2517
2732
  arity = [
2518
2733
  (
@@ -2758,9 +2973,13 @@ def order_invalid_assignments(program: S.Program, profile, diags: Diagnostics) -
2758
2973
  expression:` line (oracle, 2026-10-10: `Sec = Nope`, `6 = estimated`), which pass 1 emitted first."""
2759
2974
  if profile.name not in ("cr300", "cr350"):
2760
2975
  return
2761
- for key in {(d.line, d.file) for d in diags.items if d.message.startswith("Invalid assignment in expression:")}:
2976
+ conditions = {id(st) for st in program.statements if isinstance(st, S.If)}
2977
+ markers = [
2978
+ d for d in diags.items if d.stmt not in conditions and d.message.startswith("Invalid assignment in expression:")
2979
+ ]
2980
+ for key in {(d.line, d.file) for d in markers}:
2762
2981
  own = [d for d in diags.items if (d.line, d.file) == key]
2763
- marks = [d for d in own if d.message.startswith("Invalid assignment in expression:")]
2982
+ marks = [d for d in markers if (d.line, d.file) == key]
2764
2983
  group = [d for d in own if d not in marks] + marks
2765
2984
  first = next(i for i, d in enumerate(diags.items) if (d.line, d.file) == key)
2766
2985
  rest = [d for d in diags.items if (d.line, d.file) != key]