From d5ad717f78ece61d55d08367b2cf25687f176b25 Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Sun, 3 May 2026 18:20:19 +0200 Subject: [PATCH] AuxPreprocessor: ignore M-codes inside paren comments Two bugs surfaced when macros got prose like: (Composed from atoms: M102 = RELEASE (V1 on), M103 = CLAMP) 1. _ATC_M_RE.finditer was being run against the raw line, so the M102/M103 *inside* the comment fired spurious release/clamp hooks at file load. 2. The simple _PAREN_COMMENT_RE = re.compile(r'\(\[^)]*\)') is a greedy non-nested match, so a header with a nested paren (e.g. 'M102 = RELEASE (V1 on)') only stripped the inner paren, leaving the trailing 'M103 = CLAMP)' visible to the matcher. Fix: - Add _strip_comments() that walks the line tracking paren depth and drops the trailing semicolon comment. Handles nested parens correctly. - Run _ATC_M_RE.finditer against the comment-stripped 'code' instead of the raw line, so prose mentions are inert. - Drop the original line's comments from the rewritten output; keeping them around led to the M-codes being matched twice (once stripped, once still in the trailing comment). - Use _strip_comments in file_uses_aux too. The grab.nc and drop.nc macros on the controller already had the prose headers; they now preprocess correctly to clean release / G4 / clamp and release / N x eject / Z0 / clamp sequences. --- src/py/bbctrl/AuxPreprocessor.py | 71 +++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/src/py/bbctrl/AuxPreprocessor.py b/src/py/bbctrl/AuxPreprocessor.py index c4084be..13318a1 100644 --- a/src/py/bbctrl/AuxPreprocessor.py +++ b/src/py/bbctrl/AuxPreprocessor.py @@ -46,8 +46,42 @@ import tempfile # Strip line comments so we don't get fooled by "(M100 not really)". +# Note this is a simple regex and doesn't handle nested parentheses +# - which actually occur in real macro headers like +# `(Composed from atoms: M102 = RELEASE (V1 on), M103 = CLAMP)`. +# Use _strip_comments() below for a parser that does handle them. _PAREN_COMMENT_RE = re.compile(r'\([^)]*\)') + +def _strip_comments(line): + """Return `line` with paren comments and the trailing semicolon + comment removed. Handles arbitrarily nested parentheses (RS274 + technically forbids them but real-world gcode comments often + contain prose with parens, e.g. `(M102 = RELEASE (V1 on))`). + + Returns just the executable code, with the original whitespace + preserved between tokens.""" + out = [] + depth = 0 + i = 0 + n = len(line) + while i < n: + c = line[i] + if c == ';' and depth == 0: + break + if c == '(': + depth += 1 + i += 1 + continue + if c == ')': + if depth > 0: depth -= 1 + i += 1 + continue + if depth == 0: + out.append(c) + i += 1 + return ''.join(out) + # ATC pneumatics M-codes mapped onto hook events. M101 is # deliberately unassigned (see header). _ATC_M_CODES = { @@ -135,8 +169,7 @@ class AuxPreprocessor(object): try: with open(path, 'r', encoding='utf-8', errors='replace') as f: for line in f: - code = _PAREN_COMMENT_RE.sub('', line) - code = code.split(';', 1)[0] + code = _strip_comments(line) if _ATC_M_RE.search(code): return True if couple_active: @@ -327,8 +360,7 @@ class AuxPreprocessor(object): line = raw.rstrip('\n') # Comment-only or blank lines pass through verbatim. - code = _PAREN_COMMENT_RE.sub('', line) - code = code.split(';', 1)[0] + code = _strip_comments(line) if not code.strip(): fout.write(raw) continue @@ -347,10 +379,14 @@ class AuxPreprocessor(object): if self._maybe_inject_a_down(code, fout): rewrote_any = True - # ATC M-codes (M100/M102/M103). Each ATC M-code on the line - # is replaced with its (MSG,HOOK::) line and - # stripped from the residual. - atc_matches = list(_ATC_M_RE.finditer(line)) + # ATC M-codes (M100/M102/M103). Match against the + # comment-stripped `code` so prose mentions like + # `(M102 = RELEASE)` inside a comment don't spuriously + # fire hooks. Each match emits a (MSG,HOOK::) + # line; the M-code is stripped from the executable + # residual but the original line's comments are kept + # for log readability. + atc_matches = list(_ATC_M_RE.finditer(code)) if atc_matches: rewrote_any = True for m in atc_matches: @@ -359,18 +395,13 @@ class AuxPreprocessor(object): event = _ATC_M_CODES.get(num) if event: fout.write('(MSG,HOOK:%s:)\n' % event) - line = _ATC_M_RE.sub('', line) - code = _PAREN_COMMENT_RE.sub('', line) - code = code.split(';', 1)[0] - if not code.strip(): - # Nothing meaningful left; preserve any trailing - # comment text but skip empty lines. - rest = line.rstrip() - if rest: - fout.write(rest + '\n') - continue - # Other gcode remains on the line - emit it. - fout.write(line + '\n') + code_stripped = _ATC_M_RE.sub('', code).strip() + if code_stripped: + # Mixed line: keep the residual executable + # gcode. Drop the comments to keep the + # rewritten file tidy (the original line's + # text already appears once as the input). + fout.write(code_stripped + '\n') continue # No rewrite needed.